From b3fc3a1d9faa10681931a586b467eba44e16e5bb Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 04:43:26 -0400 Subject: [PATCH 1/2] bugc: add debug contexts to all remaining unmapped bytecodes Thread remark/code contexts through all compiler-generated instructions that previously lacked debug info: - Free memory pointer initialization (remark) - Return value spill after call continuation (call expr source range) - STOP guard between main and user functions (remark) - Function prologue MSTORE for param storage (thread existing remark) - Function prologue return PC save sequence (thread existing remark) - Deployment wrapper CODECOPY+RETURN (remark) All 82 instructions across runtime and create programs now carry debug contexts (previously 22 were unmapped). --- packages/bugc/src/evmgen/generation/block.ts | 42 +++++++++++++------ .../bugc/src/evmgen/generation/function.ts | 19 +++++++-- packages/bugc/src/evmgen/generation/module.ts | 32 ++++++++++---- 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index 6b0c2189d..26b93b4b7 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -104,6 +104,7 @@ export function generate( predBlock.terminator.dest ) { const destId = predBlock.terminator.dest; + const spillDebug = predBlock.terminator.operationDebug; result = result.then(annotateTop(destId)).then((s) => { const allocation = s.memory.allocations[destId]; if (!allocation) return s; @@ -112,7 +113,11 @@ export function generate( ...s, instructions: [ ...s.instructions, - { mnemonic: "DUP1" as const, opcode: 0x80 }, + { + mnemonic: "DUP1" as const, + opcode: 0x80, + debug: spillDebug, + }, { mnemonic: "PUSH2" as const, opcode: 0x61, @@ -120,8 +125,13 @@ export function generate( (allocation.offset >> 8) & 0xff, allocation.offset & 0xff, ], + debug: spillDebug, + }, + { + mnemonic: "MSTORE" as const, + opcode: 0x52, + debug: spillDebug, }, - { mnemonic: "MSTORE" as const, opcode: 0x52 }, ], }; }); @@ -212,14 +222,22 @@ function initializeMemory( ): Transition { const { PUSHn, MSTORE } = operations; - return ( - pipe() - // Push the static offset value (the value to store) - .then(PUSHn(BigInt(nextStaticOffset)), { as: "value" }) - // Push the free memory pointer location (0x40) (the offset) - .then(PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER)), { as: "offset" }) - // Store the initial free pointer (expects [value, offset] on stack) - .then(MSTORE()) - .done() - ); + const debug = { + context: { + remark: "initialize free memory pointer", + }, + }; + + return pipe() + .then(PUSHn(BigInt(nextStaticOffset), { debug }), { + as: "value", + }) + .then( + PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER), { + debug, + }), + { as: "offset" }, + ) + .then(MSTORE({ debug })) + .done(); } diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 70b31d8aa..0cd7ac335 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -104,7 +104,11 @@ function generatePrologue( ...currentState, instructions: [ ...currentState.instructions, - { mnemonic: "MSTORE", opcode: 0x52 }, + { + mnemonic: "MSTORE", + opcode: 0x52, + debug: prologueDebug, + }, ], }; } @@ -130,13 +134,22 @@ function generatePrologue( immediates: [0x60], debug: savePcDebug, }, - { mnemonic: "MLOAD", opcode: 0x51 }, + { + mnemonic: "MLOAD", + opcode: 0x51, + debug: savePcDebug, + }, { mnemonic: "PUSH2", opcode: 0x61, immediates: [highByte, lowByte], + debug: savePcDebug, + }, + { + mnemonic: "MSTORE", + opcode: 0x52, + debug: savePcDebug, }, - { mnemonic: "MSTORE", opcode: 0x52 }, ], }; } diff --git a/packages/bugc/src/evmgen/generation/module.ts b/packages/bugc/src/evmgen/generation/module.ts index 92c306f8a..2e588a65e 100644 --- a/packages/bugc/src/evmgen/generation/module.ts +++ b/packages/bugc/src/evmgen/generation/module.ts @@ -107,7 +107,17 @@ export function generate( // STOP (the isLastBlock optimization). const stopGuard: Evm.Instruction[] = patchedFunctions.length > 0 - ? [{ mnemonic: "STOP" as const, opcode: 0x00 }] + ? [ + { + mnemonic: "STOP" as const, + opcode: 0x00, + debug: { + context: { + remark: "guard: prevent fall-through into functions", + }, + }, + }, + ] : []; const stopGuardBytes: number[] = patchedFunctions.length > 0 ? [0x00] : []; @@ -243,13 +253,19 @@ function buildDeploymentInstructions( function deploymentTransition(runtimeOffset: bigint, runtimeLength: bigint) { const { PUSHn, CODECOPY, RETURN } = operations; + const debug = { + context: { + remark: "deployment: copy runtime bytecode and return", + }, + }; + return pipe() - .then(PUSHn(runtimeLength), { as: "size" }) - .then(PUSHn(runtimeOffset), { as: "offset" }) - .then(PUSHn(0n), { as: "destOffset" }) - .then(CODECOPY()) - .then(PUSHn(runtimeLength), { as: "size" }) - .then(PUSHn(0n), { as: "offset" }) - .then(RETURN()) + .then(PUSHn(runtimeLength, { debug }), { as: "size" }) + .then(PUSHn(runtimeOffset, { debug }), { as: "offset" }) + .then(PUSHn(0n, { debug }), { as: "destOffset" }) + .then(CODECOPY({ debug })) + .then(PUSHn(runtimeLength, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(RETURN({ debug })) .done(); } From eea5ef76bea236ca7361da15e76d0887ed9c9bf6 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 04:58:13 -0400 Subject: [PATCH 2/2] bugc: add code contexts with source ranges to compiler-generated instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add source location info (loc, sourceId) to Ir.Function so EVM codegen can build code contexts for compiler-generated instructions. Instructions that map to a source location now use gather contexts combining both a remark (for debugger tooling) and a code context (for source highlighting): - Free memory pointer init → code block / create block range - Function prologue (param stores, return PC save) → function decl range - STOP guard → code block range Deployment wrapper remains remark-only (no corresponding source). Return value spill already had correct source mapping (call expr). --- packages/bugc/src/evmgen/generation/block.ts | 39 ++++++++++---- .../bugc/src/evmgen/generation/function.ts | 54 +++++++++++++++---- packages/bugc/src/evmgen/generation/module.ts | 28 ++++++++-- packages/bugc/src/ir/spec/function.ts | 4 ++ packages/bugc/src/irgen/generate/function.ts | 4 ++ 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index 26b93b4b7..5fea7a285 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -2,6 +2,7 @@ * Block-level code generation */ +import type * as Ast from "#ast"; import type * as Format from "@ethdebug/format"; import * as Ir from "#ir"; import type { Stack } from "#evm"; @@ -47,9 +48,13 @@ export function generate( // Initialize memory for first block if (isFirstBlock) { - // Always initialize the free memory pointer for consistency - // This ensures dynamic allocations start after static ones - result = result.then(initializeMemory(state.memory.nextStaticOffset)); + const sourceInfo = + func?.sourceId && func?.loc + ? { sourceId: func.sourceId, loc: func.loc } + : undefined; + result = result.then( + initializeMemory(state.memory.nextStaticOffset, sourceInfo), + ); } // Set JUMPDEST for non-first blocks @@ -215,18 +220,34 @@ function generatePhi( /** * Initialize the free memory pointer at runtime - * Sets the value at 0x40 to the next available memory location after static allocations + * Sets the value at 0x40 to the next available memory location + * after static allocations */ function initializeMemory( nextStaticOffset: number, + sourceInfo?: { sourceId: string; loc: Ast.SourceLocation }, ): Transition { const { PUSHn, MSTORE } = operations; - const debug = { - context: { - remark: "initialize free memory pointer", - }, - }; + const debug = sourceInfo + ? { + context: { + gather: [ + { remark: "initialize free memory pointer" }, + { + code: { + source: { id: sourceInfo.sourceId }, + range: sourceInfo.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: "initialize free memory pointer", + } as Format.Program.Context, + }; return pipe() .then(PUSHn(BigInt(nextStaticOffset), { debug }), { diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 0cd7ac335..9f397e7bc 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -71,11 +71,28 @@ function generatePrologue( // Return PC is already in memory at 0x60 (stored by caller) // Pop and store each arg from argN down to arg0 - const prologueDebug = { - context: { - remark: `prologue: store ${params.length} parameter(s) to memory`, - }, - }; + const prologueDebug = + func.sourceId && func.loc + ? { + context: { + gather: [ + { + remark: `prologue: store ${params.length} parameter(s) to memory`, + }, + { + code: { + source: { id: func.sourceId }, + range: func.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: `prologue: store ${params.length} parameter(s) to memory`, + } as Format.Program.Context, + }; for (let i = params.length - 1; i >= 0; i--) { const param = params[i]; @@ -117,11 +134,28 @@ function generatePrologue( // so nested function calls don't clobber it. const savedPcOffset = currentState.memory.savedReturnPcOffset; if (savedPcOffset !== undefined) { - const savePcDebug = { - context: { - remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, - }, - }; + const savePcDebug = + func.sourceId && func.loc + ? { + context: { + gather: [ + { + remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, + }, + { + code: { + source: { id: func.sourceId }, + range: func.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, + } as Format.Program.Context, + }; const highByte = (savedPcOffset >> 8) & 0xff; const lowByte = savedPcOffset & 0xff; currentState = { diff --git a/packages/bugc/src/evmgen/generation/module.ts b/packages/bugc/src/evmgen/generation/module.ts index 2e588a65e..a0e26b229 100644 --- a/packages/bugc/src/evmgen/generation/module.ts +++ b/packages/bugc/src/evmgen/generation/module.ts @@ -105,17 +105,35 @@ export function generate( // Insert STOP between main and user functions to prevent // fall-through when the main function's last block omits // STOP (the isLastBlock optimization). + const stopGuardDebug = + module.main.sourceId && module.main.loc + ? { + context: { + gather: [ + { + remark: "guard: prevent fall-through into functions", + }, + { + code: { + source: { id: module.main.sourceId }, + range: module.main.loc, + }, + }, + ], + }, + } + : { + context: { + remark: "guard: prevent fall-through into functions", + }, + }; const stopGuard: Evm.Instruction[] = patchedFunctions.length > 0 ? [ { mnemonic: "STOP" as const, opcode: 0x00, - debug: { - context: { - remark: "guard: prevent fall-through into functions", - }, - }, + debug: stopGuardDebug, }, ] : []; diff --git a/packages/bugc/src/ir/spec/function.ts b/packages/bugc/src/ir/spec/function.ts index df58c6ac8..49be241c5 100644 --- a/packages/bugc/src/ir/spec/function.ts +++ b/packages/bugc/src/ir/spec/function.ts @@ -17,6 +17,10 @@ export interface Function { blocks: Map; /** SSA variable metadata mapping temp IDs to original variables */ ssaVariables?: Map; + /** Source location of the function body */ + loc?: Ast.SourceLocation; + /** Source identifier for debug info */ + sourceId?: string; } export namespace Function { diff --git a/packages/bugc/src/irgen/generate/function.ts b/packages/bugc/src/irgen/generate/function.ts index 7e39aacfd..59e152768 100644 --- a/packages/bugc/src/irgen/generate/function.ts +++ b/packages/bugc/src/irgen/generate/function.ts @@ -46,12 +46,16 @@ export function* buildFunction( // Collect SSA variable metadata const ssaVariables = yield* Process.Functions.collectSsaMetadata(); + const module_ = yield* Process.Modules.current(); + const function_: Ir.Function = { name, parameters: params, entry: "entry", blocks, ssaVariables: ssaVariables.size > 0 ? ssaVariables : undefined, + loc: body.loc ?? undefined, + sourceId: module_.sourceId, }; return function_;