From 8dc940e66d3fa20632f2f4021e39e2a0c5990458 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Fri, 3 Jul 2026 17:30:03 -0400 Subject: [PATCH] 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, but there isn't always a meaningful target to record — e.g. at the first instruction of an inlined body, where the JUMP has been elided. The callee identity (identifier, declaration, type) stays meaningful; the target does not. Same pattern as return.data being optional. - Schema: drop `target` from InternalCall.required; expand the description; add a worked example for the no-target case. - TS types: mark `target` optional; relax the guard. - bugc: guard target access in patchInvokeInContext (+ tests assert target is defined before dereferencing). Wave 1 of the transform-context → main split (#31): transform-schema- independent. Forward-references to `transform:["inline"]` in the prose/ example are deferred to the wave that lands the transform schema. --- .../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 | 7 +++++ .../context/function/invoke.schema.yaml | 28 ++++++++++++++++++- 6 files changed, 44 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 d27b6fd61..4460a0d2f 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 b9ec6fc4f..77d31df7a 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..617a2b5d8 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -51,6 +51,13 @@ 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. +