Skip to content

Trace globals - #126

Merged
RaoulSchaffranek merged 9 commits into
masterfrom
feat/globals-tracing
Aug 11, 2026
Merged

Trace globals#126
RaoulSchaffranek merged 9 commits into
masterfrom
feat/globals-tracing

Conversation

@RaoulSchaffranek

@RaoulSchaffranek RaoulSchaffranek commented Aug 10, 2026

Copy link
Copy Markdown
Member

Every instruction record in the execution trace now carries a globals object: the executing module's wasm globals, keyed by module-relative index, each value a [type, value] pair like locals:

{"pos": 605, "instr": ["local.get", 0], "stack": [], "locals": {}, "globals": {"0": ["i32", 1048560]}}

Unlike mem, it's repeated in full on every record and never null. @bbyalcinkaya I'm undecided whether we should print globals in full or diff. What are your thoughts on this?

Also

  • Locals2JSON → ValMap2JSON, now shared by locals and globals.
  • generateLedgerTrace / AccountBalances2JSONs, serializers for the ledger baseline record komet-node emits before a transaction

Testing

New test_globals_tracing.py (companion to test_memory_tracing.py) traces the increment example and asserts the field is well-formed on every instruction record.

Comment on lines +224 to +228
Trace records report the executing module's wasm globals, but `<globals>` is a K *cell
collection*, and those cannot be serialized the way `ValMap2JSON` serializes the locals
map: a cell collection's generated sort (`GlobalInstCellMap`) is not usable in a
hand-written `syntax` declaration, so no function can take the collection as an argument
(`Could not find sorts: [GlobalInstCellMap]`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could potentially be implemented as a contextual function?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Big yes! I always forget contextual functions exist. Let me re-implement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this would improve performance, but I believe implementing it would make the code much simpler and easier to follow compared to having an intermediate step.

@bbyalcinkaya bbyalcinkaya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me. Approving with a few minor/optional change requests:

Summary

  1. docs/tracing.md: It is possible to declare functions that takes configuration cell sorts as arguments. Update the paragraph that explains why #collectGlobals is implemented using rewrite rules.
  2. #collectGlobals (optional): Consider implementing this as a contextual function if you agree that it would make the implementation simpler.
  3. Remove the unused generateLedgerTrace.

Comment thread docs/tracing.md Outdated

A global appears only once it has been allocated, which happens after its own *initializer* has been evaluated. So the records that evaluate a module's initializers report the globals declared before them and not the one being defined: the first such record carries `{}`, the second carries global 0, and so on.

Because `<globals>` is a K *cell collection* — whose generated sort cannot appear in a hand-written `syntax` declaration, so no function can take it as an argument — the tracer cannot serialize it the way it serializes the locals map. Rules have no such restriction, so the values are read live, one global per rewrite step, by walking the executing module's `<globalAddrs>` and looking up each `<globalInst>`. See `tracing.md`'s *Collecting Globals*. Nothing is mirrored and no `wasm-semantics` rule is shadowed, so the reported values cannot drift from the real ones.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is not quite correct. It is possible to declare the generated sort name (GlobalsCell) and use it as a function argument:

syntax GlobalsCell
syntax Map ::= collectGlobals(Map, GlobalsCell)    [function]

That said, the function implementation following this approach may be less clear than using rewrite rules or a contextual function. I think these comments should be corrected to reflect that this is an implementation/design choice, rather than a limitation of K.

Comment on lines +482 to +484
decimal string, as with `locals`), each value a `[type, value]` pair. Unlike `mem` this
is repeated in full on every record and never `null`: a module has only a handful of
globals, so a consumer reads them off the current record with no scan.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with this decision.

Comment on lines +527 to +566
```k
syntax JSON ::= generateLedgerTrace(sequence: Int, timestamp: Int, accounts: Map) [function]
// ---------------------------------------------------------------------------------------------
rule generateLedgerTrace(SEQ, TS, ACCTS)
=> {
"pos" : null ,
"instr" : [ "ledger" ] ,
"sequence" : SEQ ,
"timestamp" : TS ,
"accounts" : [ AccountBalances2JSONs(ACCTS) ] ,
"contracts" : [ .JSONs ] ,
"codes" : [ .JSONs ]
}
```

`AccountBalances2JSONs` serializes a plain `Map` of account `Address` |-> balance. The
`<accounts>` cell collection cannot be read here for the same reason `<globals>` cannot:
its generated collection sort is not usable as a declared function argument in a
hand-written module (`Could not find sorts: [AccountCellMap]`).

The caller builds that `Map` by walking the `<account>` cells one per rewrite step, the
same way `#collectGlobals` walks the globals. That walk lives in `komet-node`'s
`#collectAccounts`, beside the `#traceLedger` step that needs it, because the ledger
scalars it reports (`<ledgerSequenceNumber>`, `<ledgerTimestamp>`) are komet-node's cells.
Unlike the globals there is no index to drain, so the walk instead skips accounts already
in the accumulator.

```k
syntax JSONs ::= AccountBalances2JSONs(Map) [function]
// ----------------------------------------------------------
rule AccountBalances2JSONs(.Map) => .JSONs

rule AccountBalances2JSONs((ADDR:Address |-> BAL:Int) REST:Map)
=> { "account" : Address2JSON(ADDR) , "balance" : BAL } , AccountBalances2JSONs(REST)

rule AccountBalances2JSONs((_K |-> _V) REST:Map) => AccountBalances2JSONs(REST)
[owise]
```

```k

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dead/unused code?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Kind of. I'm working on a branch for komet-node in parallel, which actually calls generateLedgerTrace. But good catch - let me actually move this code to komet-node.

RaoulSchaffranek and others added 2 commits August 10, 2026 14:35
Trace records need the executing module's globals, but `<globals>` is a cell
collection whose generated sort cannot appear in a `syntax` declaration, so no
function could take it as an argument. The previous workaround drained
`<globalAddrs>` one global per rewrite step through an intermediate
`#collectGlobals` instruction, splitting logging into two phases.

K's global context in function rules lifts the restriction: a function rule may
match configuration cells alongside its own arguments. `moduleGlobals` and
`globalVals` use it to read `<moduleInst>` and `<globalInst>` directly, so the
whole record is emitted in the single step that `#traceInstr` takes.

This drops `#collectGlobals` and the argument its two-phase split required --
that the value stack, locals and memory could not move while the walk ran.

`moduleGlobals` takes an `OptionalInt` rather than an `Int` so that
`traceInstr-nomem` can pass `<curModIdx>` through unchanged; constraining it
would stop that rule's `owise` from matching when no module is current, wedging
`#traceInstr` instead of tracing it.

Trace output is unchanged: 1187 records against a from-scratch build of the
parent commit, zero differences in content. JSON key order within `locals` and
`globals` does shift, but that also moves for `locals`, which this commit does
not touch -- adding and removing symbols shifts term hashes, and so the order in
which `ValMap2JSONs` walks a `Map`. Both fields are documented as index-keyed
objects and are read by key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`generateLedgerTrace` and its `AccountBalances2JSONs` helper had no caller in
komet. The only call site is `collectAccounts-done` in komet-node's `node.md`,
which compiles komet's semantics as an include dir, so they were reachable only
from downstream: untested here (komet emits no ledger record and cannot easily
be made to), undocumented here (`docs/tracing.md` describes every record kind
except this one), and unbuildable downstream, since komet-node pins v0.1.86,
which predates them.

Moving them beside their caller fixes all three. What is left behind is a
pointer saying where the record is built and why komet has none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RaoulSchaffranek
RaoulSchaffranek merged commit 673087c into master Aug 11, 2026
3 of 6 checks passed
@RaoulSchaffranek
RaoulSchaffranek deleted the feat/globals-tracing branch August 11, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants