From eb0dc8c4cedcdea176b361188e4f971f4d0ad440 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 16 Apr 2026 04:00:53 -0400 Subject: [PATCH 1/2] format: make invoke.target optional for internal calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internal calls via JUMP normally carry a code pointer to the callee's entry point. When the compiler inlines a function, the JUMP is elided — there is no physical call instruction and no code target to point at. The callee identity (identifier, declaration, type) remains meaningful, but the target pointer does not. Same pattern as #211 (making return.data optional). Unblocks inlining: bugc can emit invoke contexts on inlined first instructions without fabricating a target pointer. - Schema: drop target from InternalCall.required, expand description, add worked example for inlined case - TS types: mark target optional; guard relaxed - Spec page: document optionality and point at transform + gather for inlining annotation - bugc: guard target access in patchInvokeInContext; tests assert target defined before dereferencing --- .../bugc/src/evmgen/call-contexts.test.ts | 6 ++-- .../bugc/src/evmgen/generation/function.ts | 2 ++ .../src/evmgen/optimizer-contexts.test.ts | 3 +- packages/format/src/types/program/context.ts | 5 ++-- .../spec/program/context/function/invoke.mdx | 10 +++++++ .../context/function/invoke.schema.yaml | 29 ++++++++++++++++++- 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts index c05f58708..c670d1660 100644 --- a/packages/bugc/src/evmgen/call-contexts.test.ts +++ b/packages/bugc/src/evmgen/call-contexts.test.ts @@ -97,7 +97,8 @@ code { expect(typeof invoke.declaration!.range!.length).toBe("number"); // Target should be a code pointer (not stack) - expect(Pointer.Region.isCode(call.target.pointer)).toBe(true); + expect(call.target).toBeDefined(); + expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true); // Caller JUMP should NOT have argument pointers // (args live on the callee JUMPDEST invoke context) @@ -156,7 +157,8 @@ code { expect(call.identifier).toBe("add"); // Target should be a code pointer - expect(Pointer.Region.isCode(call.target.pointer)).toBe(true); + expect(call.target).toBeDefined(); + expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true); // Should have argument pointers matching // function parameters diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 5b1944b1e..8e7230155 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -537,6 +537,8 @@ function patchInvokeInContext( const offset = functionRegistry[invoke.identifier]; if (offset === undefined) return; + if (!invoke.target) return; + const ptr = invoke.target.pointer; if (Format.Pointer.Region.isCode(ptr)) { ptr.offset = `0x${offset.toString(16)}`; diff --git a/packages/bugc/src/evmgen/optimizer-contexts.test.ts b/packages/bugc/src/evmgen/optimizer-contexts.test.ts index 4fe92325f..a4ebfac29 100644 --- a/packages/bugc/src/evmgen/optimizer-contexts.test.ts +++ b/packages/bugc/src/evmgen/optimizer-contexts.test.ts @@ -495,7 +495,8 @@ code { r = count(0, 5); }`; expect(Invocation.isInternalCall(invocation)).toBe(true); const internalCall = invocation as Format.Program.Context.Invoke.Invocation.InternalCall; - const invokeTarget = internalCall.target.pointer; + expect(internalCall.target).toBeDefined(); + const invokeTarget = internalCall.target!.pointer; expect(invokeTarget).toBeDefined(); expect( "offset" in invokeTarget ? invokeTarget.offset : undefined, diff --git a/packages/format/src/types/program/context.ts b/packages/format/src/types/program/context.ts index 104f27196..5c9bfb7c9 100644 --- a/packages/format/src/types/program/context.ts +++ b/packages/format/src/types/program/context.ts @@ -171,7 +171,7 @@ export namespace Context { export namespace Invocation { export interface InternalCall extends Function.Identity { jump: true; - target: Function.PointerRef; + target?: Function.PointerRef; arguments?: Function.PointerRef; } @@ -180,8 +180,7 @@ export namespace Context { !!value && "jump" in value && value.jump === true && - "target" in value && - Function.isPointerRef(value.target) && + (!("target" in value) || Function.isPointerRef(value.target)) && (!("arguments" in value) || Function.isPointerRef(value.arguments)); export interface ExternalCall extends Function.Identity { diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index b249a1a0a..0e2c0d63e 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -51,6 +51,16 @@ caller's JUMP has already consumed the destination from the stack, so pointer slot values reflect the post-JUMP layout. The target points to a code location and arguments are passed on the stack. +The `target` field is optional. It may be omitted when there is no +meaningful code pointer to record — most notably at the first +instruction of an inlined function body, where the inlining pass +has elided the JUMP that would normally carry the target. The +callee identity (`identifier`, `declaration`, `type`) remains +meaningful in this case; a separate transform annotation +(typically via [`gather`](/spec/program/context/gather)) can +indicate that the call was inlined rather than physically +invoked. + Date: Thu, 16 Apr 2026 05:10:42 -0400 Subject: [PATCH 2/2] format: prefer flat form for invoke + transform composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pair with #212's flat-form guidance: when an inlined body's first instruction carries both an invoke and a transform, those belong as sibling keys on a single context — gather isn't needed because `invoke` and `transform` don't collide. --- packages/web/spec/program/context/function/invoke.mdx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index 0e2c0d63e..af1c1da7a 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -56,10 +56,9 @@ meaningful code pointer to record — most notably at the first instruction of an inlined function body, where the inlining pass has elided the JUMP that would normally carry the target. The callee identity (`identifier`, `declaration`, `type`) remains -meaningful in this case; a separate transform annotation -(typically via [`gather`](/spec/program/context/gather)) can -indicate that the call was inlined rather than physically -invoked. +meaningful in this case; a sibling `transform: ["inline"]` key +on the same context indicates that the call was inlined rather +than physically invoked.