-
Notifications
You must be signed in to change notification settings - Fork 2
Trace globals #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Trace globals #126
Changes from 3 commits
54e2d60
62d5ff9
4b2e13e
0dce9c4
35e5a4a
4772761
8e31ce3
c94e0e4
f09e134
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,29 +32,60 @@ It is currently equivalent to `Instr`, but defined as a separate sort to make th | |
|
|
||
| ## Internal Instructions | ||
|
|
||
| Two internal instructions drive the tracing mechanism: | ||
| Three internal instructions drive the tracing mechanism: | ||
|
|
||
| - `#traceInstr(I, POS)` -- performs the actual logging of instruction `I` at binary offset `POS` (`.Int` when no offset is available, e.g. for text format programs). | ||
| - `#collectGlobals(I, POS, TODO, ACC)` -- an intermediate state of `#traceInstr`: it reads the executing module's globals one at a time (see *Collecting Globals*) before emitting the record. `TODO` is the not-yet-read part of `<globalAddrs>` (module index |-> `<gAddr>`), `ACC` the globals read so far (module index |-> `Val`). | ||
| - `#resetAlreadyTraced` -- resets `<alreadyTraced>` to `false` after an instruction has been traced and executed, re-enabling tracing for the next instruction. | ||
|
|
||
| ```k | ||
| syntax InternalInstr ::= #traceInstr(Instr, OptionalInt) [symbol("#traceInstr")] | ||
| | #collectGlobals(Instr, OptionalInt, todo: Map, acc: Map) | ||
| [symbol("#collectGlobals")] | ||
| syntax HelperInstr ::= "#resetAlreadyTraced" [symbol(resetAlreadyTraced)] | ||
| ``` | ||
|
|
||
| ## Tracing Rules | ||
|
|
||
| ### Logging | ||
|
|
||
| The `traceInstr` rule performs the actual logging. It: | ||
| Logging takes two phases, because the globals cannot be read in a single match (see | ||
| *Collecting Globals* below): | ||
|
|
||
| 1. Generates the trace data for instruction `I` using the current value stack and locals. | ||
| 2. Appends it as a JSON record to the trace file. | ||
| 1. `traceInstr` picks up the executing module's `<globalAddrs>` and hands off to | ||
| `#collectGlobals`, which reads the globals one at a time. | ||
| 2. `collectGlobals-done` generates the trace data for instruction `I` from the current | ||
| value stack, locals, memory and collected globals, and appends it as a JSON record to | ||
| the trace file. | ||
|
|
||
| Everything but the globals is read in phase 2. That is safe because the intervening steps | ||
| only rewrite `#collectGlobals` at the top of `<instrs>`: no wasm instruction runs in | ||
| between, so the value stack, locals and memory are the same as when `#traceInstr` was | ||
| reached. | ||
|
|
||
| ```k | ||
| rule [traceInstr]: | ||
| <instrs> #traceInstr(I, POS) | ||
| => #appendFileJSONLn(PATH, generateInstrTrace(I, POS, STACK, LOCALS, MEM, PM)) | ||
| <instrs> #traceInstr(I, POS) => #collectGlobals(I, POS, GADDRS, .Map) ... </instrs> | ||
| <curModIdx> CUR </curModIdx> | ||
| <moduleInst> | ||
| <modIdx> CUR </modIdx> | ||
| <globalAddrs> GADDRS </globalAddrs> | ||
| ... | ||
| </moduleInst> | ||
|
|
||
| // Fallback for when there is no module instance to read globals from. Reports no | ||
| // globals rather than getting stuck, keeping `#traceInstr` total. | ||
| rule [traceInstr-nomodule]: | ||
| <instrs> #traceInstr(I, POS) => #collectGlobals(I, POS, .Map, .Map) ... </instrs> | ||
| [owise] | ||
| ``` | ||
|
|
||
| Once every global has been read, the record is emitted. | ||
|
|
||
| ```k | ||
| rule [collectGlobals-done]: | ||
| <instrs> #collectGlobals(I, POS, .Map, GLOBALS) | ||
| => #appendFileJSONLn(PATH, generateInstrTrace(I, POS, STACK, LOCALS, MEM, PM, GLOBALS)) | ||
| ... | ||
| </instrs> | ||
| <ioDir> PATH </ioDir> | ||
|
|
@@ -74,11 +105,12 @@ The `traceInstr` rule performs the actual logging. It: | |
| <prevMem> PM => MEM </prevMem> | ||
|
|
||
| // Fallback for programs without a linear memory (e.g. text-format tests): still | ||
| // trace, with an empty memory so `mem` is always `null`. Guarantees `#traceInstr` | ||
| // is always consumed even when the memory-matching rule above cannot fire. | ||
| rule [traceInstr-nomem]: | ||
| <instrs> #traceInstr(I, POS) | ||
| => #appendFileJSONLn(PATH, generateInstrTrace(I, POS, STACK, LOCALS, .SparseBytes, .SparseBytes)) | ||
| // trace, with an empty memory so `mem` is always `null`. Guarantees `#collectGlobals` | ||
| // is always consumed even when the memory-matching rule above cannot fire. Globals | ||
| // are still reported here — they are collected before this rule is reached. | ||
| rule [collectGlobals-done-nomem]: | ||
| <instrs> #collectGlobals(I, POS, .Map, GLOBALS) | ||
| => #appendFileJSONLn(PATH, generateInstrTrace(I, POS, STACK, LOCALS, .SparseBytes, .SparseBytes, GLOBALS)) | ||
| ... | ||
| </instrs> | ||
| <ioDir> PATH </ioDir> | ||
|
|
@@ -187,6 +219,53 @@ The `#resetAlreadyTraced` appended by `insert-traceInstr` after the `#block`/`#l | |
| [priority(20)] | ||
| ``` | ||
|
|
||
| ### Collecting Globals | ||
|
|
||
| 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]`). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could potentially be implemented as a contextual function?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Big yes! I always forget contextual functions exist. Let me re-implement.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| Rules have no such restriction — they just cannot match a *variable number* of | ||
| `<globalInst>` cells at once. So `#collectGlobals` reads the globals one per rewrite step, | ||
| draining the executing module's `<globalAddrs>` (module index |-> `<gAddr>`) into a plain | ||
| `Map` of module index |-> `Val` and looking up each `<globalInst>` as it goes. The result | ||
| is keyed by module index — the index space DWARF's `DW_OP_WASM_location` global operand | ||
| uses, so a debugger can index it directly — and reads live state, so no rule outside this | ||
| section needs to know that tracing exists. | ||
|
|
||
| The cost is one rewrite step per global per traced instruction. A module has only a handful | ||
| of globals, and this is dwarfed by the file append each record already performs. | ||
|
|
||
| ```k | ||
| rule [collectGlobals-step]: | ||
| <instrs> #collectGlobals(I, POS, (IDX:Int |-> GADDR:Int) TODO, ACC) | ||
| => #collectGlobals(I, POS, TODO, ACC [ IDX <- VAL ]) | ||
| ... | ||
| </instrs> | ||
| <globalInst> | ||
| <gAddr> GADDR </gAddr> | ||
| <gValue> VAL </gValue> | ||
| ... | ||
| </globalInst> | ||
| [preserves-definedness] | ||
| ``` | ||
|
|
||
| An address with no `<globalInst>` is skipped rather than reported as `null`, which a | ||
| consumer would read as a value. `allocglobal` adds the address to `<globalAddrs>` and the | ||
| `<globalInst>` to `<globals>` in a single step, so this rule should be unreachable; it | ||
| exists so that a dangling address cannot wedge the tracer. | ||
|
|
||
| ```k | ||
| rule [collectGlobals-skip]: | ||
| <instrs> #collectGlobals(I, POS, (_IDX:Int |-> _GADDR:Int) TODO, ACC) | ||
| => #collectGlobals(I, POS, TODO, ACC) | ||
| ... | ||
| </instrs> | ||
| [owise] | ||
| ``` | ||
|
|
||
| ## Instruction Filter | ||
|
|
||
| `shouldTraceInstr` filters out instructions that should not be traced in text format programs. | ||
|
|
@@ -399,21 +478,28 @@ Each instruction trace record is a JSON object with these fields: | |
| lowercase hex), or `null` when memory is unchanged. Zero-gaps are omitted; a consumer | ||
| reconstructs memory by taking the most recent non-`null` snapshot at or before the | ||
| record and treating unwritten bytes as `0`. | ||
| - `globals` — the executing module's wasm globals, keyed by MODULE-RELATIVE index (a | ||
| 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. | ||
|
Comment on lines
+466
to
+468
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with this decision. |
||
|
|
||
| Records are written one per line to the trace file. | ||
|
|
||
| ```k | ||
| syntax JSON ::= generateInstrTrace(Instr, OptionalInt, ValStack, Map, SparseBytes, SparseBytes) [function] | ||
| syntax JSON ::= generateInstrTrace(Instr, OptionalInt, ValStack, locals: Map, SparseBytes, SparseBytes, globals: Map) [function] | ||
| // --------------------------------------------------------- | ||
| rule generateInstrTrace(I:Instr, OFFSET, VS:ValStack, LOCALS:Map, MEM:SparseBytes, PM:SparseBytes) | ||
| rule generateInstrTrace(I:Instr, OFFSET, VS:ValStack, LOCALS:Map, MEM:SparseBytes, PM:SparseBytes, GLOBALS:Map) | ||
| => { | ||
| "pos" : #if OFFSET ==K .Int #then null #else {OFFSET}:>Int #fi , | ||
| "instr" : Instr2JSON(I) , | ||
| "stack" : ValStack2JSON(VS) , | ||
| "locals" : Locals2JSON(LOCALS) , | ||
| "locals" : ValMap2JSON(LOCALS) , | ||
| // Full sparse snapshot of linear memory when it changed since the previous | ||
| // snapshot, else `null` (memory unchanged — reuse the most recent snapshot). | ||
| "mem" : #if MEM ==K PM #then null #else [ memRuns(MEM, 0) ] #fi | ||
| "mem" : #if MEM ==K PM #then null #else [ memRuns(MEM, 0) ] #fi , | ||
| // Collected by `#collectGlobals`, already keyed by module-relative index; the | ||
| // same index |-> Val shape as `locals`, so the same serializer applies. | ||
| "globals": ValMap2JSON(GLOBALS) | ||
| } | ||
|
|
||
| // Serializes a SparseBytes memory as a JSON array of `{ "addr", "bytes" }` runs, one | ||
|
|
@@ -426,14 +512,65 @@ Records are written one per line to the trace file. | |
| rule memRuns(SBChunk(#empty(N)) REST, OFF) => memRuns(REST, OFF +Int N) | ||
| rule memRuns(SBChunk(#bytes(BS)) REST, OFF) | ||
| => ({ "addr" : OFF , "bytes" : Bytes2Hex(BS) }, memRuns(REST, OFF +Int lengthBytes(BS))) | ||
| ``` | ||
|
|
||
| `generateLedgerTrace` builds the whole-transaction **ledger baseline** record: the ledger | ||
| scalars plus every account's balance. A debugger seeds its ledger view from this and then | ||
| replays the per-operation events (storage writes, contract calls) on top, so it can show | ||
| chain state at any point of a recorded execution — not just the parts a contract touched. | ||
|
|
||
| komet-node emits it once, before a transaction's steps run (see its `#traceLedger`). | ||
| `contracts` and `codes` are reserved for the contract-instance and uploaded-code metadata | ||
| (wasm hash, instance/code TTLs); they are emitted empty for now, and a consumer must treat | ||
| an empty list as "not reported" rather than "none exist". | ||
|
|
||
| ```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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead/unused code?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| syntax JSON ::= generateHostCallTrace(String, String, Map) [function] | ||
| // ------------------------------------------------------------------------- | ||
| rule generateHostCallTrace(MOD, FUNC, LOCALS) | ||
| => { | ||
| "pos" : null , | ||
| "instr" : [ "hostCall" , MOD , FUNC ] , | ||
| "locals" : Locals2JSON(LOCALS) | ||
| "locals" : ValMap2JSON(LOCALS) | ||
| } | ||
|
|
||
| syntax JSON ::= generateContractDataTrace(ContractId, StorageType, String, List) [function] | ||
|
|
||
There was a problem hiding this comment.
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: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.