From cda5919cab478dfacb4597f8eecdabbe2e444d8f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 16 Jul 2026 17:36:35 -0400 Subject: [PATCH 1/2] Revert "bugc: sound per-instruction local-variable debug snapshot (#261)" This reverts commit 885f962f80905a99847dd58f9c468cc2572fedb7. --- .../debug/local-variables.resolve.test.ts | 142 ------ .../debug/local-variables.soundness.test.ts | 324 ------------- .../src/evmgen/debug/local-variables.test.ts | 154 ------ .../bugc/src/evmgen/debug/local-variables.ts | 441 ------------------ .../generation/control-flow/terminator.ts | 17 - packages/bugc/src/evmgen/pass.ts | 6 - packages/bugc/src/ir/spec/function.ts | 8 - packages/bugc/src/irgen/generate/process.ts | 12 - packages/bugc/src/irgen/generate/state.ts | 8 +- .../src/irgen/generate/statements/block.ts | 2 +- .../src/irgen/generate/statements/declare.ts | 13 +- 11 files changed, 5 insertions(+), 1122 deletions(-) delete mode 100644 packages/bugc/src/evmgen/debug/local-variables.resolve.test.ts delete mode 100644 packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts delete mode 100644 packages/bugc/src/evmgen/debug/local-variables.test.ts delete mode 100644 packages/bugc/src/evmgen/debug/local-variables.ts diff --git a/packages/bugc/src/evmgen/debug/local-variables.resolve.test.ts b/packages/bugc/src/evmgen/debug/local-variables.resolve.test.ts deleted file mode 100644 index c62c0dd535..0000000000 --- a/packages/bugc/src/evmgen/debug/local-variables.resolve.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Runtime-correctness tests for local-variable pointers. - * - * Shape tests verify what we emit; these verify it RESOLVES. Each - * emitted pointer is resolved against a real EVM trace's machine - * memory and must yield the variable's actual known value — proving - * the pointer points at the bytes the variable really occupies. - * - * Covers both location kinds: - * - frame-relative (`mem[ mem[FRAME_POINTER] + delta ]`) for - * function locals, and - * - static absolute (`mem[offset]`) for main/create locals. - */ -import { describe, it, expect } from "vitest"; - -import { compile } from "#compiler"; -import { Executor } from "@ethdebug/evm"; -import { bytesToHex } from "ethereum-cryptography/utils"; -import type * as Format from "@ethdebug/format"; - -/** Read a 32-byte big-endian word from a memory snapshot. */ -function readWord(mem: Uint8Array, offset: number): bigint { - let v = 0n; - for (let i = 0; i < 32; i++) { - const b = offset + i < mem.length ? mem[offset + i] : 0; - v = (v << 8n) | BigInt(b); - } - return v; -} - -/** - * Resolve a bugc-emitted local pointer against a memory snapshot — - * a minimal evaluator for the two shapes this feature emits. - */ -function resolve(pointer: unknown, mem: Uint8Array): bigint | undefined { - const p = pointer as Record; - if (p.location === "memory" && typeof p.offset === "number") { - // static absolute: mem[offset] - return readWord(mem, p.offset); - } - if (Array.isArray(p.group)) { - // frame-relative: mem[ mem[FRAME_POINTER] + delta ] - const frame = p.group[0] as { offset: number }; - const data = p.group[1] as { offset: { $sum: [unknown, number] } }; - const fp = readWord(mem, Number(frame.offset)); - const delta = Number(data.offset.$sum[1]); - return readWord(mem, Number(fp) + delta); - } - return undefined; -} - -/** Emitted pointer for each local, keyed by identifier. */ -function pointersByIdentifier(program: Format.Program): Map { - const out = new Map(); - for (const instr of program.instructions) { - const ctx = instr.context as Record | undefined; - if (!ctx || !Array.isArray(ctx.variables)) continue; - for (const v of ctx.variables as Array>) { - if (typeof v.identifier === "string" && !out.has(v.identifier)) { - out.set(v.identifier, v.pointer); - } - } - } - return out; -} - -/** - * Compile at O0, run with a memory-capturing trace, and assert each - * expected local's pointer resolves to its known value at some step. - */ -async function expectResolves( - source: string, - expected: Record, -): Promise { - const result = await compile({ - to: "bytecode", - source, - optimizer: { level: 0 }, - }); - if (!result.success) throw new Error("compile failed"); - const program = result.value.bytecode.runtimeProgram; - const pointers = pointersByIdentifier(program); - - const bc = result.value.bytecode; - const createCode = - bc.create && bc.create.length > 0 - ? bytesToHex(bc.create) - : bytesToHex(bc.runtime); - const executor = new Executor(); - await executor.deploy(createCode); - - const memories: Uint8Array[] = []; - await executor.execute({ data: "" }, (step) => { - if (step.memory) memories.push(step.memory); - }); - expect(memories.length).toBeGreaterThan(0); - - for (const [name, want] of Object.entries(expected)) { - const pointer = pointers.get(name); - expect(pointer, `pointer emitted for ${name}`).toBeDefined(); - const resolvedSomewhere = memories.some( - (mem) => resolve(pointer, mem) === want, - ); - expect( - resolvedSomewhere, - `pointer for ${name} resolves to ${want} at some live PC`, - ).toBe(true); - } -} - -describe("local-variable pointers resolve to the correct runtime bytes", () => { - it("frame-relative params resolve against machine memory", async () => { - // Distinctive values so a correct resolve can't be a coincidence. - await expectResolves( - `name FrameLocal; -define { - function add(a: uint256, b: uint256) -> uint256 { return a + b; }; -} -storage { [0] r: uint256; } -create {} -code { r = add(43690, 48059); }`, - { a: 43690n, b: 48059n }, - ); - }); - - it("static main local (no frame) resolves against machine memory", async () => { - // `x` crosses the branch, so it is spilled to static memory and - // homed at an absolute offset (main has no call frame). - await expectResolves( - `name StaticLocal; -storage { [0] r: uint256; [1] flag: uint256; } -create { flag = 1; } -code { - let x = 51966; - if (flag > 0) { r = 0; } - else { r = 1; } - r = x; -}`, - { x: 51966n }, - ); - }); -}); diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts deleted file mode 100644 index 37c331997c..0000000000 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Soundness of the per-instruction guaranteed-known snapshot. - * - * Proves the two bugs the dominance rework targets are gone: - * (a) a variable is NOT reported before its defining store - * (no stale/uninitialized value), and - * (b) a reassigned variable resolves to the CURRENT version's slot, - * never a stale one. - * Plus completeness: in-scope locals appear, with a type-only record - * where the value is not located (stack-resident). - */ -import { describe, it, expect } from "vitest"; - -import { compile } from "#compiler"; -import { Executor } from "@ethdebug/evm"; -import { bytesToHex } from "ethereum-cryptography/utils"; -import type * as Format from "@ethdebug/format"; -import { Program } from "@ethdebug/format"; - -async function runtimeProgram(source: string): Promise { - const r = await compile({ to: "bytecode", source, optimizer: { level: 0 } }); - if (!r.success) throw new Error("compile failed"); - return r.value.bytecode.runtimeProgram; -} - -async function executeProgram(source: string): Promise<{ - compiled: boolean; - value?: bigint; - program?: Format.Program; -}> { - const r = await compile({ to: "bytecode", source, optimizer: { level: 0 } }); - if (!r.success) return { compiled: false }; - const bc = r.value.bytecode; - const executor = new Executor(); - await executor.deploy( - bc.create && bc.create.length > 0 - ? bytesToHex(bc.create) - : bytesToHex(bc.runtime), - ); - await executor.execute({ data: "" }); - return { - compiled: true, - value: await executor.getStorage(0n), - program: bc.runtimeProgram, - }; -} - -function localsOf(ctx: unknown): Array> { - if (!ctx || typeof ctx !== "object") return []; - const vars = (ctx as { variables?: unknown }).variables; - return Array.isArray(vars) ? (vars as Array>) : []; -} - -function readWord(mem: Uint8Array, offset: number): bigint { - let v = 0n; - for (let i = 0; i < 32; i++) { - v = (v << 8n) | BigInt(offset + i < mem.length ? mem[offset + i] : 0); - } - return v; -} - -function resolve(pointer: unknown, mem: Uint8Array): bigint | undefined { - const p = pointer as Record; - if (p.location === "memory" && typeof p.offset === "number") { - return readWord(mem, p.offset); - } - if (Array.isArray(p.group)) { - const frame = p.group[0] as { offset: number }; - const data = p.group[1] as { offset: { $sum: [unknown, number] } }; - const fp = readWord(mem, Number(frame.offset)); - return readWord(mem, Number(fp) + Number(data.offset.$sum[1])); - } - return undefined; -} - -describe("guaranteed-known snapshot soundness", () => { - it("(a) does not report a variable before its defining store", async () => { - // `x` is defined mid-body and used in both branches (memory-homed). - // Instructions mapping to source BEFORE `let x` must not list `x`. - const source = `name PreDef; -define { - function f(n: uint256) -> uint256 { - let y = n * 2; - let x = y + 1; - if (n > 0) { return x; } - else { return x + y; } - }; -} -storage { [0] r: uint256; } -create {} -code { r = f(3); }`; - const program = await runtimeProgram(source); - const xDecl = source.indexOf("let x"); - - for (const instr of program.instructions) { - const ctx = instr.context as Record | undefined; - const code = ctx?.code as { range?: { offset: number } } | undefined; - // Instructions whose source is strictly before `let x` must - // not carry `x` (its value isn't defined yet there). - if (code?.range && code.range.offset < xDecl) { - const names = localsOf(ctx).map((v) => v.identifier); - expect( - names, - `x present at pre-def offset ${code.range.offset}`, - ).not.toContain("x"); - } - } - // Sanity: `x` IS reported somewhere (from its def onward). - const xAppears = program.instructions.some((i) => - localsOf(i.context).some((v) => v.identifier === "x"), - ); - expect(xAppears).toBe(true); - }); - - it("(b) a reassigned variable resolves to the current version", async () => { - // x = 111, then reassigned to 222; both memory-homed (cross-block - // uses). At the return, x must resolve to 222, never 111. - const source = `name Reassign; -define { - function g(n: uint256) -> uint256 { - let x = 111; - let keep = n; - if (n > 0) { x = 222; } - else { x = 333; } - return x + keep; - }; -} -storage { [0] r: uint256; } -create {} -code { r = g(7); }`; // n=7 > 0 → x becomes 222 - const program = await runtimeProgram(source); - - const r = await compile({ - to: "bytecode", - source, - optimizer: { level: 0 }, - }); - if (!r.success) throw new Error("compile failed"); - const bc = r.value.bytecode; - const executor = new Executor(); - await executor.deploy( - bc.create && bc.create.length > 0 - ? bytesToHex(bc.create) - : bytesToHex(bc.runtime), - ); - const mems: Uint8Array[] = []; - await executor.execute({ data: "" }, (s) => { - if (s.memory) mems.push(s.memory); - }); - - // Collect every distinct pointer emitted for `x`, resolve each - // against every memory snapshot. SOUNDNESS: no emitted `x` - // pointer ever resolves to a value that isn't a value `x` legally - // held (111 initial, or 222 final for n>0). It must NEVER resolve - // to 333 (the else-branch value, not taken) or garbage. - const xPointers: unknown[] = []; - for (const instr of program.instructions) { - for (const v of localsOf(instr.context)) { - if (v.identifier === "x" && v.pointer) xPointers.push(v.pointer); - } - } - expect(xPointers.length).toBeGreaterThan(0); - - // The final value read for x's live pointer must be 222 (n=7), - // and no x pointer resolves to the untaken 333. - const resolved = new Set(); - for (const p of xPointers) { - for (const mem of mems) { - const val = resolve(p, mem); - if (val !== undefined) resolved.add(val); - } - } - // 222 (taken branch) must be reachable; 333 (untaken) must never - // be what a live x-pointer holds at a PC where x is that version. - expect([...resolved]).toContain(222n); - }); - - it("(c) completeness: reports in-scope locals, type-only where unlocated", async () => { - // `s` is used only within one block (stack-resident, no pointer); - // it should still appear as a type-only in-scope record. - const source = `name Complete; -define { - function h(a: uint256, b: uint256) -> uint256 { - let s = a + b; - return s; - }; -} -storage { [0] r: uint256; } -create {} -code { r = h(3, 4); }`; - const program = await runtimeProgram(source); - - const seen = new Map>(); - for (const instr of program.instructions) { - for (const v of localsOf(instr.context)) { - if (typeof v.identifier === "string" && !seen.has(v.identifier)) { - seen.set(v.identifier, v); - } - } - } - // Params a, b appear with a pointer (memory-homed). - for (const name of ["a", "b"]) { - const entry = seen.get(name); - expect(entry, `param ${name}`).toBeDefined(); - expect(entry!.type).toBeDefined(); - expect(entry!.pointer).toBeDefined(); - } - // The `let s` is in scope and reported, with a type. Whether it - // carries a pointer depends on whether it's located (memory) or - // stack-resident (type-only) — either way it must appear, and a - // pointerless record is a sound "in scope, no value". - const s = seen.get("s"); - expect(s, "let s in scope").toBeDefined(); - expect(s!.type).toBeDefined(); - - // Every emitted record carries a type (always known in BUG). - for (const entry of seen.values()) { - expect(entry.type).toBeDefined(); - } - }); - - it("(e) shadowing never surfaces a wrong version's value", async () => { - // BUG's only nested scopes are if/for bodies (conditional); an - // inner shadow's def is on a conditional path and never dominates - // the post-block code, so the dominance snapshot picks the OUTER - // version there. Combined with no frame-slot reuse (monotonic - // allocator), a variable never resolves to another variable's - // value. Here the returned value must be the OUTER x (111), and - // no snapshot lists x twice. - const source = `name Shadow; -define { - function sh(n: uint256) -> uint256 { - let x = 111; - if (n > 0) { - let x = 222; - return x; - } - return x; - }; -} -storage { [0] r: uint256; } -create {} -code { r = sh(0); }`; // n=0 → else path → returns outer x = 111 - const res = await executeProgram(source); - // If BUG rejects shadowing, it's vacuously sound. - if (!res.compiled || !res.program) return; - expect(res.value).toBe(111n); - // And one-version-per-PC still holds across the shadowed program. - for (const instr of res.program.instructions) { - const names = localsOf(instr.context) - .map((v) => v.identifier) - .filter((id): id is string => typeof id === "string"); - expect(new Set(names).size).toBe(names.length); - } - }); - - it("(f) in-scope locals stay listed at and after a call", async () => { - // gnidan's SimpleFunctions case: a/b/c must NOT vanish at the - // `add(a, b)` call — they are still in scope there (the call is a - // block terminator, previously unstamped). - const source = `name SimpleFunctions; -define { - function add(x: uint256, y: uint256) -> uint256 { return x + y; }; -} -storage { [0] r: uint256; } -create {} -code { - let a = 3; - let b = 4; - let c = 5; - r = add(a, b); - r = r + c; -}`; - const { Context } = Program; - const r = await compile({ - to: "bytecode", - source, - optimizer: { level: 0 }, - }); - if (!r.success) throw new Error("compile failed"); - const program = r.value.bytecode.runtimeProgram; - - // The invoke JUMP into `add` must still list a, b and c. - const invokeJump = program.instructions.find( - (i) => - i.operation?.mnemonic === "JUMP" && - i.context !== undefined && - Context.isInvoke(i.context), - ); - expect(invokeJump, "invoke JUMP for add").toBeDefined(); - const names = localsOf(invokeJump!.context).map((v) => v.identifier); - for (const id of ["a", "b", "c"]) { - expect(names, `${id} at the call`).toContain(id); - } - }); - - it("(d) lists each identifier at most once per instruction", async () => { - // The one-version-per-PC guarantee: a snapshot never contains two - // records for the same source name (no stale-version duplicate). - const source = `name OnePerPc; -define { - function g(n: uint256) -> uint256 { - let x = 111; - let keep = n; - if (n > 0) { x = 222; } - else { x = 333; } - return x + keep; - }; -} -storage { [0] r: uint256; } -create {} -code { r = g(7); }`; - const program = await runtimeProgram(source); - - for (const instr of program.instructions) { - const names = localsOf(instr.context) - .map((v) => v.identifier) - .filter((id): id is string => typeof id === "string"); - expect(new Set(names).size, `duplicate identifier in [${names}]`).toBe( - names.length, - ); - } - }); -}); diff --git a/packages/bugc/src/evmgen/debug/local-variables.test.ts b/packages/bugc/src/evmgen/debug/local-variables.test.ts deleted file mode 100644 index 9e6b93e41c..0000000000 --- a/packages/bugc/src/evmgen/debug/local-variables.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Emission tests for memory-homed local-variable debug info (O0). - * - * Verifies that parameters and cross-block `let` bindings that the - * memory planner spills to frame-relative memory get a `variables` - * context with a frame-relative pointer, and that behavior is - * unaffected. - */ -import { describe, it, expect } from "vitest"; - -import { compile } from "#compiler"; -import { executeProgram } from "#test/evm/behavioral"; -import type * as Format from "@ethdebug/format"; - -async function compileProgram( - source: string, - level: 0 | 1 | 2 | 3, -): Promise { - const result = await compile({ - to: "bytecode", - source, - optimizer: { level }, - }); - if (!result.success) { - const errors = result.messages.error ?? []; - throw new Error( - "compile failed:\n" + - errors - .map((e: { message?: string }) => e.message ?? String(e)) - .join("\n"), - ); - } - return result.value.bytecode.runtimeProgram; -} - -/** All distinct `variables` entries in a program, by identifier. */ -function localEntries( - program: Format.Program, -): Map> { - const byId = new Map>(); - for (const instr of program.instructions) { - const ctx = instr.context as Record | undefined; - if (!ctx || !Array.isArray(ctx.variables)) continue; - for (const v of ctx.variables as Array>) { - if (typeof v.identifier !== "string") continue; - const existing = byId.get(v.identifier); - // Prefer the located record: a variable now appears type-only - // (in scope, no value) as well as with a pointer where located. - if (!existing || (!existing.pointer && v.pointer)) { - byId.set(v.identifier, v); - } - } - } - return byId; -} - -/** True if a pointer is a frame-relative group (name "frame" + $read). */ -function isFrameRelative(pointer: unknown): boolean { - if (!pointer || typeof pointer !== "object") return false; - const group = (pointer as { group?: unknown[] }).group; - if (!Array.isArray(group) || group.length < 2) return false; - const frame = group[0] as Record; - const data = group[1] as Record; - if (frame.name !== "frame" || frame.location !== "memory") return false; - const offset = data.offset as Record | undefined; - return !!offset && Array.isArray(offset.$sum); -} - -describe("local-variable debug emission (memory-homed, O0)", () => { - describe("function parameters", () => { - const source = `name Params; -define { - function add(a: uint256, b: uint256) -> uint256 { - let s = a + b; - return s; - }; -} -storage { [0] r: uint256; } -create {} -code { r = add(3, 4); }`; - - it("emits frame-relative variables for both params", async () => { - const program = await compileProgram(source, 0); - const locals = localEntries(program); - - for (const name of ["a", "b"]) { - const entry = locals.get(name); - expect(entry, `variables entry for ${name}`).toBeDefined(); - expect(entry!.identifier).toBe(name); - expect(isFrameRelative(entry!.pointer)).toBe(true); - expect(entry!.type).toEqual({ kind: "uint", bits: 256 }); - } - }); - - it("keeps runtime behavior correct", async () => { - const res = await executeProgram(source, { - calldata: "", - optimizationLevel: 0, - }); - expect(res.callSuccess).toBe(true); - expect(await res.getStorage(0n)).toBe(7n); - }); - }); - - describe("cross-block let binding", () => { - // `x` is defined before the branch and used in both arms, so it - // crosses block boundaries and is spilled to frame memory. - const source = `name CrossBlock; -define { - function f(n: uint256) -> uint256 { - let x = n + 1; - if (n > 0) { return x; } - else { return x + n; } - }; -} -storage { [0] r: uint256; } -create {} -code { r = f(5); }`; - - it("emits a frame-relative variable for the spilled let", async () => { - const program = await compileProgram(source, 0); - const locals = localEntries(program); - const x = locals.get("x"); - expect(x, "variables entry for x").toBeDefined(); - expect(isFrameRelative(x!.pointer)).toBe(true); - }); - - it("keeps runtime behavior correct", async () => { - const res = await executeProgram(source, { - calldata: "", - optimizationLevel: 0, - }); - expect(res.callSuccess).toBe(true); - expect(await res.getStorage(0n)).toBe(6n); - }); - }); - - describe("contract without functions", () => { - // No user functions, no frames — the pass is a no-op and must - // not disturb compilation. - const source = `name NoFns; -storage { [0] r: uint256; } -create { r = 0; } -code { r = r + 1; }`; - - it("compiles and runs", async () => { - const res = await executeProgram(source, { - calldata: "", - optimizationLevel: 0, - }); - expect(res.callSuccess).toBe(true); - }); - }); -}); diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts deleted file mode 100644 index bb29202fd7..0000000000 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ /dev/null @@ -1,441 +0,0 @@ -/** - * Local-variable debug info (emission, O0) — per-instruction snapshot - * splitting two independent axes: - * - * - LIST (which variables appear) = LEXICAL SCOPE. Every variable - * (parameter or `let`) lexically in scope at an instruction is - * listed with its name + type, regardless of whether its value is - * currently recoverable. A `let` is in scope over `[decl, scopeEnd)` - * (irgen records the enclosing scope's end); a parameter is in - * scope throughout the function. Stamped on EVERY instruction AND - * terminator (so an in-scope local doesn't vanish at a call). - * - POINTER (value vs type-only) = AVAILABILITY by DOMINANCE. A - * pointer is attached only where the value is located — the current - * SSA version's def dominates-or-equals the instruction and it is - * memory-homed. Otherwise the record is type-only ("in scope, no - * value"). This never shows a value before its def or a stale - * version, and it never drops an in-scope variable just because its - * value isn't currently located. - * - * Records are partial: `type` is always emitted (always known in - * BUG); `pointer` is emitted only where the value is located — - * memory-homed (in the frame/static allocation map). A variable that - * is in scope but not located (stack-resident) is emitted type-only - * ("in scope, no value") — sound, never a wrong value. - * - * Scope (this cut): O0 only (under optimization locations move or - * dissolve). Snapshot is stamped per instruction (redundant, accepted - * for now). Pointers cover memory-homed locals; stack-resident are - * type-only. Block-scope exit / shadowing precision is limited by - * dominance (a def dominates its whole dominated subtree) — see the - * scope note in `currentVersionAt`. - * - * The location pointer encodes bugc's runtime frame convention: the - * frame base lives in machine memory at `FRAME_POINTER` (0x80); a - * frame-homed local is at `mem[ mem[FRAME_POINTER] + delta ]`, - * expressed as a `group` naming the frame region and a data region - * whose offset reads it (`$read`) and adds the static delta (`$sum`). - * Functions without a frame (main/create) home locals at a static - * memory offset. - */ -import type * as Format from "@ethdebug/format"; - -import * as Ir from "#ir"; -import { Memory } from "#evmgen/analysis"; -import { convertToEthDebugType } from "../../irgen/debug/types.js"; - -type VariableEntry = Format.Program.Context.Variables["variables"][number]; - -/** Location of a definition: a block plus an instruction index - * (`-1` = block entry, i.e. a parameter or phi, before all - * instructions). */ -interface DefSite { - block: string; - index: number; -} - -/** - * Build the location pointer for a memory-homed local. Frame-homed - * (user function): a group naming the frame-pointer region, then the - * data region at `FP + delta`. Static (main/create): a single memory - * region at the fixed offset, named with the identifier. - */ -function buildPointer( - identifier: string, - allocation: Memory.Allocation, - frameSize: number | undefined, -): Format.Pointer { - const offset = Number(allocation.offset); - const length = Number(allocation.size); - - if (frameSize === undefined) { - // No call frame (main/create): static memory offset. - return { name: identifier, location: "memory", offset, length }; - } - - // Frame-relative: mem[ mem[FRAME_POINTER] + delta ]. - return { - group: [ - { - name: "frame", - location: "memory", - offset: Memory.regions.FRAME_POINTER, - length: 32, - }, - { - name: identifier, - location: "memory", - offset: { $sum: [{ $read: "frame" }, offset] }, - length, - }, - ], - }; -} - -/** - * Build a partial variable record for one SSA version: identifier + - * type + declaration always; pointer only when the version is - * memory-homed (present in the allocation map). - */ -function buildEntry( - ssa: Ir.Function.SsaVariable, - allocation: Memory.Allocation | undefined, - frameSize: number | undefined, - sourceId: string, -): VariableEntry { - const entry: VariableEntry = { identifier: ssa.name }; - - const type = convertToEthDebugType(ssa.type); - if (type) entry.type = type; - - if (ssa.loc) { - entry.declaration = { source: { id: sourceId }, range: ssa.loc }; - } - - if (allocation) { - entry.pointer = buildPointer(ssa.name, allocation, frameSize); - } - - return entry; -} - -/** - * Merge variable entries into an instruction's debug context as a - * flat `variables` sibling, appending to any existing `variables` - * (e.g. storage variables from irgen) and preserving all other keys. - * Storage entries take precedence over a same-identifier local. - */ -function withVariables( - debug: Ir.Instruction.Debug | undefined, - entries: VariableEntry[], -): Ir.Instruction.Debug { - if (entries.length === 0) return debug ?? {}; - - const context = (debug?.context ?? {}) as Record; - const existing = Array.isArray(context.variables) - ? (context.variables as VariableEntry[]) - : []; - - const have = new Set( - existing - .map((v) => v.identifier) - .filter((id): id is string => id !== undefined), - ); - const fresh = entries.filter( - (e) => e.identifier === undefined || !have.has(e.identifier), - ); - if (fresh.length === 0) return debug ?? {}; - - return { - context: { - ...context, - variables: [...existing, ...fresh], - } as Format.Program.Context, - }; -} - -/** Whether block `a` dominates-or-equals block `b`. */ -function dominatesBlock( - a: string, - b: string, - idom: Record, -): boolean { - let current: string | null = b; - while (current !== null) { - if (current === a) return true; - current = idom[current] ?? null; - } - return false; -} - -/** Whether def-site `d` dominates-or-equals position (block,index). */ -function defDominates( - d: DefSite, - block: string, - index: number, - idom: Record, -): boolean { - if (d.block === block) return d.index <= index; - return dominatesBlock(d.block, block, idom); -} - -/** Def site of every temp: parameters + phis at block entry (-1), - * instruction results at their index. */ -function collectDefSites(func: Ir.Function): Map { - const defs = new Map(); - for (const p of func.parameters) { - defs.set(p.tempId, { block: func.entry, index: -1 }); - } - for (const [blockId, block] of func.blocks) { - for (const phi of block.phis ?? []) { - defs.set(phi.dest, { block: blockId, index: -1 }); - } - block.instructions.forEach((inst, i) => { - if ("dest" in inst && typeof inst.dest === "string") { - defs.set(inst.dest, { block: blockId, index: i }); - } - }); - } - return defs; -} - -/** The source offset of an instruction/terminator, from its `code` - * context, if present. */ -function codeOffset( - debug: Ir.Instruction.Debug | undefined, -): number | undefined { - const code = ( - debug?.context as { code?: { range?: { offset?: unknown } } } | undefined - )?.code; - const offset = code?.range?.offset; - return offset == null ? undefined : Number(offset); -} - -/** - * Lexical scope of a source identifier, as bytecode-source intervals. - * `always` = a parameter (no scope end) that is in scope throughout - * the function. `intervals` = `[declStart, scopeEnd)` per lexical - * scope the name is declared in (shadowing → multiple). The start is - * the earliest declaration offset among the name's versions sharing - * that scope end; phi/intermediate versions (no `loc`) don't lower it. - */ -interface NameScope { - always: boolean; - intervals: Array<[start: number, end: number]>; -} - -function buildNameScopes( - byName: Map, -): Map { - const out = new Map(); - for (const [name, versions] of byName) { - let always = false; - const startByEnd = new Map(); - for (const v of versions) { - if (v.scopeEnd === undefined) { - always = true; - continue; - } - if (!v.loc) continue; // no declaration offset → don't set start - const start = Number(v.loc.offset); - const prev = startByEnd.get(v.scopeEnd); - startByEnd.set( - v.scopeEnd, - prev === undefined ? start : Math.min(prev, start), - ); - } - out.set(name, { - always, - intervals: [...startByEnd].map(([end, start]) => [start, end]), - }); - } - return out; -} - -/** Whether the source identifier is lexically in scope at `offset`. - * Unknown offset ⇒ don't drop (treated as in scope). */ -function nameInScope(scope: NameScope, offset: number | undefined): boolean { - if (scope.always) return true; - if (offset === undefined) return true; - return scope.intervals.some(([s, e]) => offset >= s && offset < e); -} - -/** - * Among candidate versions, the current one by dominance — the latest - * (deepest-dominating) def that dominates-or-equals (block,index). - * Returns undefined if none is available yet (in scope, not located). - */ -function dominatingVersion( - candidates: Ir.Function.SsaVariable[], - tempOf: Map, - defs: Map, - block: string, - index: number, - idom: Record, -): { ssa: Ir.Function.SsaVariable; temp: string } | undefined { - let best: - | { ssa: Ir.Function.SsaVariable; temp: string; def: DefSite } - | undefined; - for (const ssa of candidates) { - const temp = tempOf.get(ssa)!; - const def = defs.get(temp); - if (!def || !defDominates(def, block, index, idom)) continue; - if (!best) { - best = { ssa, temp, def }; - continue; - } - const later = - def.block === best.def.block - ? def.index > best.def.index - : dominatesBlock(best.def.block, def.block, idom); - if (later) best = { ssa, temp, def }; - } - return best; -} - -/** - * Build the snapshot at (block,index) with source `offset`. LIST axis: - * every lexically-in-scope variable is listed (name + type). POINTER - * axis: a pointer is attached only where the value is located (its - * current version's def dominates and it is memory-homed); otherwise - * the record is type-only ("in scope, no value"). - */ -function snapshotAt( - byName: Map, - scopes: Map, - tempOf: Map, - defs: Map, - block: string, - index: number, - offset: number | undefined, - idom: Record, - allocations: Record, - frameSize: number | undefined, - sourceId: string, -): VariableEntry[] { - const entries: VariableEntry[] = []; - for (const [name, versions] of byName) { - // LIST axis: is the identifier lexically in scope here? - if (!nameInScope(scopes.get(name)!, offset)) continue; - - // POINTER axis: the current version by dominance (if located). - const located = dominatingVersion( - versions, - tempOf, - defs, - block, - index, - idom, - ); - // Representative for name/type: the located version, else the - // highest-version (type-only "in scope, no value"). - const repr = - located?.ssa ?? - versions.reduce((a, b) => (b.version > a.version ? b : a)); - const allocation = located ? allocations[located.temp] : undefined; - entries.push(buildEntry(repr, allocation, frameSize, sourceId)); - } - return entries; -} - -/** - * Stamp every instruction AND terminator of a function with its - * variable snapshot (in-scope list + pointer-where-located). - */ -function enrichFunction( - func: Ir.Function, - module: Ir.Module, - memory: Memory.Function.Info, - sourceId: string, -): void { - if (!func.ssaVariables || func.ssaVariables.size === 0) return; - - const { allocations, frameSize } = memory; - - const byName = new Map(); - const tempOf = new Map(); - for (const [temp, ssa] of func.ssaVariables) { - tempOf.set(ssa, temp); - const list = byName.get(ssa.name); - if (list) list.push(ssa); - else byName.set(ssa.name, [ssa]); - } - if (byName.size === 0) return; - - const scopes = buildNameScopes(byName); - const defs = collectDefSites(func); - const idom = new Ir.Analysis.Statistics.Analyzer().analyze({ - ...module, - main: func, - }).dominatorTree; - - // Carry the last known source offset forward (function-wide) so - // synthesized ops without a `code` context inherit a nearby scope. - let lastOffset: number | undefined; - const stamp = ( - debug: Ir.Instruction.Debug | undefined, - blockId: string, - index: number, - ): Ir.Instruction.Debug => { - const offset = codeOffset(debug) ?? lastOffset; - if (offset !== undefined) lastOffset = offset; - const entries = snapshotAt( - byName, - scopes, - tempOf, - defs, - blockId, - index, - offset, - idom, - allocations, - frameSize, - sourceId, - ); - // withVariables returns the (possibly unchanged) debug, never - // undefined, so it is safe to assign back to a required field. - return withVariables(debug, entries); - }; - - for (const [blockId, block] of func.blocks) { - block.instructions.forEach((inst, i) => { - inst.operationDebug = stamp(inst.operationDebug, blockId, i); - }); - // The terminator's position is after all instructions in the block. - if (block.terminator) { - block.terminator.operationDebug = stamp( - block.terminator.operationDebug, - blockId, - block.instructions.length, - ); - } - } -} - -/** - * Enrich a module's instructions with per-instruction guaranteed- - * known local-variable snapshots. Mutates in place; safe before - * generation. - */ -export function enrich( - module: Ir.Module, - memory: Memory.Module.Info, -): Ir.Module { - const sourceId = module.sourceId; - - const targets: { func: Ir.Function; mem?: Memory.Function.Info }[] = [ - { func: module.main, mem: memory.main }, - ...(module.create ? [{ func: module.create, mem: memory.create }] : []), - ...[...module.functions].map(([name, func]) => ({ - func, - mem: memory.functions[name], - })), - ]; - - for (const { func, mem } of targets) { - if (!mem) continue; - enrichFunction(func, module, mem, sourceId); - } - - return module; -} diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index af962bc96d..9334d11a1d 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -24,21 +24,6 @@ function codeContext( return undefined; } -/** - * Extract the in-scope `variables` list from an instruction/terminator - * debug, if present, so a call's caller JUMP still lists the locals - * that are in scope at the call (they must not vanish at the call). - */ -function variablesContext( - debug: { context?: Format.Program.Context } | undefined, -): Format.Program.Context.Variables["variables"] | undefined { - const ctx = debug?.context as Record | undefined; - if (ctx && Array.isArray(ctx.variables) && ctx.variables.length > 0) { - return ctx.variables as Format.Program.Context.Variables["variables"]; - } - return undefined; -} - /** * Generate code for a block terminator */ @@ -282,12 +267,10 @@ export function generateCallTerminator( // flat alongside the invoke, so the caller JUMP maps back to the // call expression. invoke and code are disjoint keys. const callSiteCode = codeContext(debug); - const inScopeVars = variablesContext(debug); const invokeContext = { context: { ...invoke, ...(callSiteCode ? { code: callSiteCode } : {}), - ...(inScopeVars ? { variables: inScopeVars } : {}), } as Format.Program.Context, }; diff --git a/packages/bugc/src/evmgen/pass.ts b/packages/bugc/src/evmgen/pass.ts index 94b3a992a9..9b6039e55d 100644 --- a/packages/bugc/src/evmgen/pass.ts +++ b/packages/bugc/src/evmgen/pass.ts @@ -9,7 +9,6 @@ import { Error as EvmgenError, ErrorCode } from "#evmgen/errors"; import { buildProgram } from "#evmgen/program-builder"; import { Layout, Liveness, Memory } from "#evmgen/analysis"; -import { enrich as enrichLocalVariables } from "./debug/local-variables.js"; /** * Output produced by the EVM generation pass @@ -58,11 +57,6 @@ const pass: Pass<{ ); } - // Attach local-variable debug info (memory-homed locals) to - // the IR before generation, so it propagates onto the emitted - // instructions. Emission-only; no-op where nothing is spilled. - enrichLocalVariables(ir, memoryResult.value); - // Analyze block layout const blockResult = Layout.Module.perform(ir); if (!blockResult.success) { diff --git a/packages/bugc/src/ir/spec/function.ts b/packages/bugc/src/ir/spec/function.ts index d89aaeee4e..93d72d8c8e 100644 --- a/packages/bugc/src/ir/spec/function.ts +++ b/packages/bugc/src/ir/spec/function.ts @@ -52,13 +52,5 @@ export namespace Function { version: number; /** Source location of declaration */ loc?: Ast.SourceLocation; - /** - * Source offset at which the variable's enclosing lexical scope - * ends. Together with `loc`, gives the lexical extent over which - * the variable is in scope: `[loc.offset, scopeEnd)`. Used to - * list a variable (name + type) across its scope, independently - * of whether its value is currently located. - */ - scopeEnd?: number; } } diff --git a/packages/bugc/src/irgen/generate/process.ts b/packages/bugc/src/irgen/generate/process.ts index 7dac10a29f..96efbfec54 100644 --- a/packages/bugc/src/irgen/generate/process.ts +++ b/packages/bugc/src/irgen/generate/process.ts @@ -374,17 +374,6 @@ export namespace Process { const state: State = yield { type: "peek" }; const currentMetadata = state.function.ssaMetadata || new Map(); - // The variable's lexical scope ends where its declaring scope's - // source range ends. `scopeId` is `scope__`; the - // index is the scope's position on the stack. - const indexMatch = /^scope_(\d+)_/.exec(scopeId); - const scopeRange = indexMatch - ? state.scopes.stack[Number(indexMatch[1])]?.range - : undefined; - const scopeEnd = scopeRange - ? Number(scopeRange.offset) + Number(scopeRange.length) - : undefined; - const newMetadata = new Map(currentMetadata); newMetadata.set(tempId, { name, @@ -392,7 +381,6 @@ export namespace Process { type, version, loc, - scopeEnd, }); // Update function with new metadata diff --git a/packages/bugc/src/irgen/generate/state.ts b/packages/bugc/src/irgen/generate/state.ts index c4900b836d..a08356071d 100644 --- a/packages/bugc/src/irgen/generate/state.ts +++ b/packages/bugc/src/irgen/generate/state.ts @@ -167,7 +167,6 @@ export namespace State { export interface Scope { readonly ssaVars: Map; // SSA variable tracking readonly usedNames: Map; // For handling shadowing - readonly range?: Ast.SourceLocation; // Lexical extent of this scope } /** @@ -190,12 +189,9 @@ export namespace State { (read) => (state) => read(state.scopes), ); - export const push = (range?: Ast.SourceLocation) => + export const push = () => update((scopes) => ({ - stack: [ - ...scopes.stack, - { ssaVars: new Map(), usedNames: new Map(), range }, - ], + stack: [...scopes.stack, { ssaVars: new Map(), usedNames: new Map() }], })); export const pop = () => diff --git a/packages/bugc/src/irgen/generate/statements/block.ts b/packages/bugc/src/irgen/generate/statements/block.ts index 710b25f531..762b72a522 100644 --- a/packages/bugc/src/irgen/generate/statements/block.ts +++ b/packages/bugc/src/irgen/generate/statements/block.ts @@ -8,7 +8,7 @@ export const makeBuildBlock = ( buildStatement: (stmt: Ast.Statement) => Process, ) => function* buildBlock(block: Ast.Block): Process { - yield* Process.Variables.enterScope(block.loc ?? undefined); + yield* Process.Variables.enterScope(); for (const item of block.items) { if (Ast.isStatement(item)) { diff --git a/packages/bugc/src/irgen/generate/statements/declare.ts b/packages/bugc/src/irgen/generate/statements/declare.ts index f95e0abb37..fc011901a6 100644 --- a/packages/bugc/src/irgen/generate/statements/declare.ts +++ b/packages/bugc/src/irgen/generate/statements/declare.ts @@ -102,7 +102,6 @@ function* buildVariableDeclaration( decl.name, irType, allocTemp, - decl.loc ?? undefined, ); // If there's an initializer, store the value in memory @@ -184,11 +183,7 @@ function* buildVariableDeclaration( const value = yield* buildExpression(decl.initializer, { kind: "rvalue", }); - const ssaVar = yield* Process.Variables.declare( - decl.name, - irType, - decl.loc ?? undefined, - ); + const ssaVar = yield* Process.Variables.declare(decl.name, irType); // Generate assignment to the new SSA temp if (value.kind === "temp") { @@ -212,11 +207,7 @@ function* buildVariableDeclaration( } } else { // No initializer - declare with default value - const ssaVar = yield* Process.Variables.declare( - decl.name, - irType, - decl.loc ?? undefined, - ); + const ssaVar = yield* Process.Variables.declare(decl.name, irType); yield* Process.Instructions.emit({ kind: "const", value: 0n, From 3b33b534de14107164c9bb44ffc7b92a1025ea1c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 16 Jul 2026 17:36:35 -0400 Subject: [PATCH 2/2] Revert "pointers: add scalar-first type-directed value decoder (#269)" This reverts commit 4429ae848377ad1ba9946c7253baae7431c12aaa. --- packages/pointers/package.json | 4 - packages/pointers/src/decode.test.ts | 135 --------------------------- packages/pointers/src/decode.ts | 125 ------------------------- packages/pointers/src/index.ts | 1 - 4 files changed, 265 deletions(-) delete mode 100644 packages/pointers/src/decode.test.ts delete mode 100644 packages/pointers/src/decode.ts diff --git a/packages/pointers/package.json b/packages/pointers/package.json index a1ce744dfd..7de1fa7a25 100644 --- a/packages/pointers/package.json +++ b/packages/pointers/package.json @@ -14,10 +14,6 @@ "types": "./src/data.ts", "default": "./dist/src/data.js" }, - "#decode": { - "types": "./src/decode.ts", - "default": "./dist/src/decode.js" - }, "#evaluate": { "types": "./src/evaluate.ts", "default": "./dist/src/evaluate.js" diff --git a/packages/pointers/src/decode.test.ts b/packages/pointers/src/decode.test.ts deleted file mode 100644 index 1d2c055930..0000000000 --- a/packages/pointers/src/decode.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { expect, describe, it } from "vitest"; - -import { Data } from "./data.js"; -import { decodeValue } from "./decode.js"; - -// Build a right-aligned 32-byte word from a hex value (numbers/addresses are -// stored right-aligned in an EVM word). -const word = (hexNo0x: string): Data => - Data.fromHex(`0x${hexNo0x.padStart(64, "0")}`); - -describe("decodeValue", () => { - describe("uint", () => { - const type = { kind: "uint" as const, bits: 256 }; - - it("decodes to decimal", () => { - expect(decodeValue(word("02"), type)).toBe("2"); - expect(decodeValue(word("ff"), type)).toBe("255"); - expect(decodeValue(word("0100"), type)).toBe("256"); - }); - - it("decodes zero", () => { - expect(decodeValue(word("00"), type)).toBe("0"); - }); - - it("decodes a large 256-bit value", () => { - expect(decodeValue(Data.fromHex(`0x${"ff".repeat(32)}`), type)).toBe( - (2n ** 256n - 1n).toString(10), - ); - }); - - it("decodes a tightly-stored uint8", () => { - expect(decodeValue(new Data([0xff]), { kind: "uint", bits: 8 })).toBe( - "255", - ); - }); - }); - - describe("int", () => { - const type = { kind: "int" as const, bits: 256 }; - - it("decodes positive values", () => { - expect(decodeValue(word("05"), type)).toBe("5"); - }); - - it("decodes -1 from a sign-extended full word", () => { - expect(decodeValue(Data.fromHex(`0x${"ff".repeat(32)}`), type)).toBe( - "-1", - ); - }); - - it("decodes a tightly-stored negative int8", () => { - expect(decodeValue(new Data([0xfb]), { kind: "int", bits: 8 })).toBe( - "-5", - ); - }); - - it("decodes int8 minimum", () => { - expect(decodeValue(new Data([0x80]), { kind: "int", bits: 8 })).toBe( - "-128", - ); - }); - - it("decodes zero", () => { - expect(decodeValue(word("00"), type)).toBe("0"); - }); - }); - - describe("bool", () => { - const type = { kind: "bool" as const }; - - it("decodes false and true", () => { - expect(decodeValue(word("00"), type)).toBe("false"); - expect(decodeValue(word("01"), type)).toBe("true"); - }); - - it("treats any nonzero as true", () => { - expect(decodeValue(word("05"), type)).toBe("true"); - }); - }); - - describe("address", () => { - const type = { kind: "address" as const }; - - it("decodes to an EIP-55 checksummed address", () => { - // canonical EIP-55 test vector - expect( - decodeValue(word("5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"), type), - ).toBe("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"); - }); - - it("keeps the low 20 bytes of a wider word", () => { - // leading nonzero bytes above the 20-byte address must be dropped - const padded = Data.fromHex( - `0xdeadbeef${"5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}`, - ); - expect(decodeValue(padded, type)).toBe( - "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", - ); - }); - }); - - describe("bytesN", () => { - it("decodes left-aligned static bytes4", () => { - const leftAligned = Data.fromHex(`0xdeadbeef${"00".repeat(28)}`); - expect(decodeValue(leftAligned, { kind: "bytes", size: 4 })).toBe( - "0xdeadbeef", - ); - }); - - it("falls back to raw hex for dynamic bytes (no size)", () => { - const data = Data.fromHex("0xdeadbeef"); - expect(decodeValue(data, { kind: "bytes" })).toBe("0xdeadbeef"); - }); - }); - - describe("fallbacks (deferred / unhandled shapes)", () => { - const data = word("2a"); - - it("falls back to raw hex for string", () => { - expect(decodeValue(data, { kind: "string" })).toBe(data.toHex()); - }); - - it("falls back to raw hex for an unresolved { id } reference", () => { - expect(decodeValue(data, { id: 5 })).toBe(data.toHex()); - }); - - it("falls back to raw hex for a missing type", () => { - expect(decodeValue(data, undefined)).toBe(data.toHex()); - }); - - it("falls back to raw hex for aggregate kinds", () => { - expect(decodeValue(data, { kind: "array" } as never)).toBe(data.toHex()); - }); - }); -}); diff --git a/packages/pointers/src/decode.ts b/packages/pointers/src/decode.ts deleted file mode 100644 index ed6f419294..0000000000 --- a/packages/pointers/src/decode.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { keccak256 } from "ethereum-cryptography/keccak"; - -import { Data } from "#data"; - -// Type is imported for its shape only; @ethdebug/format is a type-only -// (dev) dependency here, so we discriminate on `kind` structurally rather -// than pulling in the package's runtime type guards. -import type { Type } from "@ethdebug/format"; - -/** - * Decode a resolved value region's bytes into a readable, human-facing - * string, directed by the variable's static type. - * - * This is the scalar-first slice of the value-rendering `reduce` layer: it - * turns raw bytes + type into a value a person can read (`uint256` -> `2`, - * not `0x00...02`). It is a pure, framework-agnostic, byte-testable function - * — no machine state, no location logic (the pointer layer already erased - * that). - * - * Scalar kinds handled: `uint`, `int`, `bool`, `address`, static `bytes`. - * Everything else — dynamic `bytes`/`string`, arrays, structs, mappings, - * `fixed`/`ufixed`, `enum`, unresolved `{ id }` type references, or a missing - * type — falls back to raw hex so callers never break on an unhandled shape. - */ -export function decodeValue( - data: Data, - type: Type.Specifier | undefined, -): string { - // No type, or a bare `{ id }` reference we can't resolve here -> raw hex. - if (!type || typeof type !== "object" || !("kind" in type)) { - return data.toHex(); - } - - switch (type.kind) { - case "uint": - return decodeUint(data); - - case "bool": - return decodeBool(data); - - case "address": - return decodeAddress(data); - - case "int": - return decodeInt(data); - - case "bytes": - // `size` present -> static `bytes`; absent -> dynamic (deferred). - return "size" in type && typeof type.size === "number" - ? decodeBytesN(data, type.size) - : data.toHex(); - - default: - // string, arrays, structs, mappings, fixed/ufixed, enum, contract, ... - return data.toHex(); - } -} - -function decodeUint(data: Data): string { - return data.asUint().toString(10); -} - -function decodeBool(data: Data): string { - return data.asUint() === 0n ? "false" : "true"; -} - -/** - * Two's-complement decode over the region's *actual* byte width. Signed - * values are sign-extended to the region width by the compiler, so - * interpreting over `data.length` bytes is correct whether the value is - * stored tightly (`int8` in 1 byte) or in a full sign-extended word - * (`int8` -1 as `0xff..ff`). - */ -function decodeInt(data: Data): string { - const width = data.length; - if (width === 0) { - return "0"; - } - - const raw = data.asUint(); - const bits = BigInt(width * 8); - const signBit = 1n << (bits - 1n); - - const value = raw >= signBit ? raw - (1n << bits) : raw; - return value.toString(10); -} - -/** - * Address = the low 20 bytes, rendered as an EIP-55 checksummed `0x` string. - * `resizeTo(20)` keeps the least-significant 20 bytes of a wider word (and - * left-pads a narrower one). - */ -function decodeAddress(data: Data): string { - const hex = data.resizeTo(20).toHex().slice(2); - return toChecksumAddress(hex); -} - -/** - * Static `bytes` is left-aligned (high-order) in its word, so the value is - * the first `size` bytes of the region. - */ -function decodeBytesN(data: Data, size: number): string { - return Data.fromBytes(data.slice(0, size)).toHex(); -} - -/** - * EIP-55 checksum: hash the lowercase hex (ASCII, no `0x`), then uppercase - * each hex letter whose corresponding hash nibble is >= 8. - */ -function toChecksumAddress(lowerHex: string): string { - const hash = keccak256(new TextEncoder().encode(lowerHex)); - - let out = "0x"; - for (let i = 0; i < lowerHex.length; i++) { - const char = lowerHex[i]; - if (char >= "a" && char <= "f") { - const hashByte = hash[i >> 1]; - const nibble = i % 2 === 0 ? hashByte >> 4 : hashByte & 0x0f; - out += nibble >= 8 ? char.toUpperCase() : char; - } else { - out += char; - } - } - return out; -} diff --git a/packages/pointers/src/index.ts b/packages/pointers/src/index.ts index 80e9161be0..5475fbbf88 100644 --- a/packages/pointers/src/index.ts +++ b/packages/pointers/src/index.ts @@ -3,4 +3,3 @@ export { Cursor } from "#cursor"; export type { Machine } from "#machine"; export { Data } from "#data"; -export { decodeValue } from "#decode";