Skip to content

New trace mechanism#1069

Merged
denismerigoux merged 12 commits into
masterfrom
trace-everything
Jul 6, 2026
Merged

New trace mechanism#1069
denismerigoux merged 12 commits into
masterfrom
trace-everything

Conversation

@vincent-botbol

@vincent-botbol vincent-botbol commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

This PR replaces the existing logging mechanism with a trace-like mechanism that outputs metadata for important program execution points.
The existing logging displayed scope calls, scope variables definitions, exceptions. We augment those with local bindings, conditionals pathing, function calls, and more. The trace is also organized as a tree so that we have a good idea of the callstacks.

For now, traces are supported by the OCaml and java backend but are disabled in python. Traces outputs in backends are only available as JSON objects. Although, it is still possible to access them through the API.
The "user-friendly" output is still available through the interpreter when --trace-format human (default) is specified.
The new output may be observed in the cram tests diffs.

We also plugged a --trace option in clerk run.

JSON trace outputs generated by the interpreter and backends aim to be equivalent but, in some specific cases, it will not be. E.g., functions in java are not represented as values and, hence, are not displayed ("<function>"), the OCaml or operator is still lazy compared to the "official" semantics that the other backends follows.

P.S. Sorry for the huge commit, the original git history was not salvageable.

Edit:

Checklist

  • Update the CHANGELOG.md

If this PR adds a feature or has breaking changes

@vincent-botbol vincent-botbol self-assigned this Jun 25, 2026
@vincent-botbol vincent-botbol added ✨ enhancement New feature or request 🔧 compiler Issue concerns the compiler 🔚 backends Backend runtime or code generation labels Jun 25, 2026

@denismerigoux denismerigoux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Congratulations Vincent, this is a major piece of work and very well-done!

Comment thread compiler/dcalc/from_scopelang.ml
Comment thread compiler/scalc/from_lcalc.ml
Comment thread compiler/shared_ast/definitions.ml
Comment on lines +596 to +623
let tag_to_runtime = function
| ScopeCall scope_name ->
let name = ScopeName.original_base scope_name in
let _, decl_pos = ScopeName.get_info scope_name in
Runtime.ScopeCall { name; decl_pos = Expr.pos_to_runtime decl_pos }
| ScopeVarDef { var; io } ->
let name = ScopeVar.to_string var in
let _, decl_pos = ScopeVar.get_info var in
Runtime.ScopeVarDef
{ var = { name; decl_pos = Expr.pos_to_runtime decl_pos }; io }
| LocalVarDef { name } -> Runtime.LocalVarDef name
| LocalTupDef { names } -> Runtime.LocalTupDef names
| FunCall topdef ->
let name = TopdefName.original_base topdef in
let _, decl_pos = TopdefName.get_info topdef in
Runtime.FunCall { name; decl_pos = Expr.pos_to_runtime decl_pos }
| BranchingCondition -> BranchingCondition
| Branching None -> IfBranching
| Branching (Some constructor_name) -> MatchBranching { constructor_name }
| Exception { label; cons_pos } ->
let label =
match label with
| None -> None
| Some (l, p) -> Some (l, Expr.pos_to_runtime p)
in
Exception { label; cons_pos = Expr.pos_to_runtime cons_pos }
| Assertion -> Assertion

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a strong reason for the tags in Runtime to not be exactly the same as the tags in Definitions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep track of the UIds so that the renaming traversal could also handle these. I'm not sure it's currently mandatory but I thought it would be more resilient this way.

Comment thread compiler/shared_ast/interpreter.ml
Comment thread runtimes/java/catala/runtime/CatalaTrace.java
Comment thread runtimes/jsoo/runtime_jsoo.ml
Comment thread runtimes/ocaml/catala_runtime.mli Outdated
Comment thread runtimes/ocaml/catala_runtime.ml
Comment thread runtimes/ocaml/catala_runtime.mli Outdated
@AltGr AltGr force-pushed the trace-everything branch from 68e0997 to 37268b1 Compare June 29, 2026 14:16
@vincent-botbol vincent-botbol force-pushed the trace-everything branch 2 times, most recently from 1329df9 to 192d7e0 Compare June 30, 2026 08:52

@AltGr AltGr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thanks! 👏🏿

Comment thread build_system/backend/common.ml Outdated
Comment thread compiler/dcalc/from_scopelang.ml Outdated
let cond = translate_expr ctx cond in
let etrue = translate_expr ctx etrue in
let efalse = translate_expr ctx efalse in
Expr.eifthenelse cond etrue efalse m

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different from the base case Expr.map ~f:(translate_expr ctx) ~op:Operator.translate (e, m) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is 5 extra-lines :) Fixed up.

match io.io_input with
| Runtime.NoInput -> "NoInput"
| Runtime.OnlyInput -> "OnlyInput"
| Runtime.Reentrant -> "Reentrant"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reuse Catala_runtime.Json.io_input ?
(it takes a Buffer.t but there already is a str wrapper for most of its siblings)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this function altogether. Also, I tried to enforce snake case in the JSON so that won't do it.

Comment thread compiler/scopelang/ast.ml Outdated
typ : typ;
io : Desugared.Ast.io;
e : 'm expr;
e : 'm expr; (* sub_scope_var: *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, some refactoring artifact got in there. I'll remove it.

Comment thread compiler/shared_ast/interpreter.ml
Comment thread compiler/shared_ast/operator.ml Outdated
| Error (error, locs, message) as e
when trace_context.exception_handled = false
->
trace_context.exception_handled <- true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe try to preserve the backtrace ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, I swear. But this wouldn't give a proper backtrace. I suspected weird intertwining with clerk with in the end I couldn't understand what happened. I didn't dig much more into it. Do you want me to get back at it ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to stall the whole PR, we'll have a look later on if needed.

(** {3:instruments Logging instruments} *)
It also catches runtime's [error]s and insert a [single_trace] before
re-raising it. When it occurs, a flag is also set so that callers do not
catch it again. *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might expect this to return 'a * trace ; maybe at least make it clear that retrieve_trace can be called afterwards ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you expect that? The previous traces constructors all return unit, why would this one give you a trace ? Also, the val retrieve_trace : unit -> trace function is two lines below.
We could change the name from with_trace to wrap_trace or something if you'd prefer. I agree that it's not necessarily the best naming.

Comment thread tests/scope/good/scope_call3.catala_en
let default
~code_coverage
~(trace : [ `FileName of Catala_utils.Global.raw_file | `Stdout ] option)
~(trace_format : Catala_utils.Global.format_enum option)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish these two could be gathered in a single record (but that's in Catala CLI, outside of the scope of this PR).

@vincent-botbol vincent-botbol force-pushed the trace-everything branch 2 times, most recently from c3fb257 to f4797c3 Compare July 6, 2026 12:18
Co-authored-by: Louis Gesbert <louis.gesbert@inria.fr>
@denismerigoux denismerigoux merged commit 682c1f7 into master Jul 6, 2026
1 check passed
@denismerigoux denismerigoux deleted the trace-everything branch July 6, 2026 14:49
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Catala - language & tooling Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔚 backends Backend runtime or code generation 🔧 compiler Issue concerns the compiler ✨ enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants