Helper returns synthesize helpers_<name>: the member slot is the helper name

- The synthesized-name pattern is two-part: container helpers, member the helper's own name — no _return suffix
- Parameter synthesis (helpers_<name>_<param>) is unchanged
- A contract fixture helper with an inline-record return pins the spelling on both emissions
This commit is contained in:
Chris Tate
2026-07-23 15:42:40 -05:00
3 changed files with 23 additions and 2 deletions
+4 -1
View File
@@ -593,7 +593,10 @@ export function buildSidecar(input: SidecarBuildInput): SidecarBuildResult {
if (fn.returns.k === "void") {
throw new SidecarError(`helper '${fn.name}' returns void — a contract helper returns a value the host can read`, fn.loc);
}
const returns = projector.shapeRef(fn.returns, `helpers_${fn.name}`, "return", fn.loc);
// Helper-return synthesized names are two-part like everything else:
// container 'helpers', member the helper's name — `helpers_<name>`,
// never a '_return' suffix (the ratified spelling).
const returns = projector.shapeRef(fn.returns, "helpers", fn.name, fn.loc);
if (helpers.some((h) => h.name === fn.name)) {
throw new SidecarError(`helper '${fn.name}' is declared twice`, fn.loc);
}
+12 -1
View File
@@ -115,7 +115,7 @@ describe.each(EMISSIONS)("contract sidecar, %s emission", (emission) => {
// The type table: declaration order everywhere, exactly the reachable
// set, synthesized entries anchored at their containing declaration.
expect(doc.types.structs.map((s) => s.name)).toEqual(["Waypoint", "Shift", "Model", "Msg_blob_tag", "Msg_nudge"]);
expect(doc.types.structs.map((s) => s.name)).toEqual(["Waypoint", "Shift", "Model", "Msg_blob_tag", "Msg_nudge", "helpers_extent"]);
expect(doc.types.enums.map((e) => e.name)).toEqual(["Zone"]);
expect(doc.types.unions.map((u) => u.name)).toEqual(["Route"]);
expect(doc.types.enums[0]!.members).toEqual(["west", "north", "east"]);
@@ -135,6 +135,16 @@ describe.each(EMISSIONS)("contract sidecar, %s emission", (emission) => {
{ name: "status", type: { kind: "f64" } },
],
});
// Helper-return synthesized names are the same two-part pattern:
// container 'helpers', member the helper name — no '_return' suffix.
expect(doc.types.structs[5]).toEqual({
name: "helpers_extent",
synthesized: true,
fields: [
{ name: "span", type: { kind: "f64" } },
{ name: "first", type: { kind: "bytes" } },
],
});
expect(doc.types.unions[0]!.arms).toEqual([
{ name: "warp", payload: { kind: "node", name: "Waypoint" } },
{ name: "step", payload: { kind: "value", name: "Shift" } },
@@ -169,6 +179,7 @@ describe.each(EMISSIONS)("contract sidecar, %s emission", (emission) => {
{ name: "waypointsOf", params: [], returns: { kind: "slice", elem: { kind: "node", name: "Waypoint" } }, arena: true },
{ name: "headline", params: [], returns: { kind: "bytes" }, arena: true },
{ name: "waypointCount", params: [], returns: { kind: "f64" }, arena: false },
{ name: "extent", params: [], returns: { kind: "value", name: "helpers_extent" }, arena: true },
]);
// Helpers are bindable surface: the unbound list may name one.
expect(doc.model_unbound).toEqual(["title", "waypointCount"]);
+7
View File
@@ -95,6 +95,13 @@ export function waypointCount(m: Model): number {
return m.waypoints.length;
}
// An inline-record return: the synthesized name is two-part like every
// other (`<Container>_<member>`) — container `helpers`, member the helper
// name — so this tables `helpers_extent`, never a `_return` suffix.
export function extent(m: Model): { span: number; first: string } {
return { span: m.speed, first: m.title };
}
// The ratified exported-const conventions (under an embedder's profile a
// generated facade emits these; nothing here is host code).
export const modelUnbound = ["title", "waypointCount"];