diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a5af8f397..da577ec57 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -127,9 +127,9 @@ There are no bare files at either level: an entry point lives inside the root it transpiler, so nothing in `cnx/`, its tests included, imports them. Each is entered through its own `index.ts`: the command-line tool and the library are separate concerns and do not share a starting point. - **Instrumentation** -- `instrumentation/`. Records facts about the RUN, never about the - program: where an ADR's rule fired, which toolchain features a run's Render emitters - required (a 2.2 planned block's features are 2.2's decision, read off the plan, #1928; - that Render still authors the rest, against the Ownership table, is tracked by #1997). Written + program: where an ADR's rule fired, and where each toolchain requirement was asked for + (2.2 decides every requirement key, read off the plan, #1928 and #1997; Render reports + only which forms it emitted, and the sites travel beside the plan's keys). Written from any layer, read once by the host, delivered on the run's result. It authors no fact the program has, so it is not a state container, and it decides nothing, so it is not a pass. @@ -148,15 +148,15 @@ There are no bare files at either level: an entry point lives inside the root it **"Read once at the end" is not the test -- "nothing branches on it" is**, and the distinction matters because the ledger IS read back. `CodeGenWalker.buildBanner` puts ` * Requires: C++20.` into a generated `.cpp` that uses a designated initializer - (`cpp-designated-initializer`, recorded where `CodeGenWalker` emits it), and `captureEmissionFacts` carries + (`cpp-designated-initializer`, decided by 2.2 from the form Render reports, #1997), and `captureEmissionFacts` carries `takeDeferredSites(...)` into `IEmissionFacts`. Checked, because a rule stated loosely - reads as violated: `EmissionPlan` branches on `facts.needsFloatStaticAssert` and - `facts.needsIrqWrappers` -- both render state -- and the sites travel beside them as + reads as violated: `EmissionPlan` branches on render state -- the `needs*` and + `emitted*` facts and `debugMode`, never a site -- and the sites travel beside them as attribution that no decision consults. So no generated CODE differs because of instrumentation; a comment and a diagnostic's file:line do. `scripts/__tests__/passes-hold-no-mutable-state.test.ts` scans this root **and** the - layers, with its four holders listed by name. It used not to scan here at all, which + layers, with its three holders listed by name. It used not to scan here at all, which made the exemption geographic: the four mutable statics #1452 relocated into this root left the guard unable to fail on the state that card was about. A list is a thing a reviewer can disagree with; a directory boundary is not. diff --git a/scripts/__tests__/passes-hold-no-mutable-state.test.ts b/scripts/__tests__/passes-hold-no-mutable-state.test.ts index d24efcb08..7fcf3f483 100644 --- a/scripts/__tests__/passes-hold-no-mutable-state.test.ts +++ b/scripts/__tests__/passes-hold-no-mutable-state.test.ts @@ -72,7 +72,7 @@ const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const PASS_ROOTS = ["src/cnx/PARSE", "src/cnx/TRANSPILE", "src/cnx/WRITE"]; /** - * Scanned alongside the pass roots, with its four holders listed below. + * Scanned alongside the pass roots, with its three holders listed below. * * The `instrumentation/` root did not exist when this file was written, and #1452 * created it -- so the four mutable statics this card RELOCATED there sat @@ -100,8 +100,7 @@ const INSTRUMENTATION_ROOT = "src/cnx/instrumentation"; const INSTRUMENTATION_HOLDERS = [ "src/cnx/instrumentation/AdrProvenance.ts:58", "src/cnx/instrumentation/AdrProvenance.ts:59", - "src/cnx/instrumentation/ToolchainRequirements.ts:65", - "src/cnx/instrumentation/ToolchainRequirements.ts:78", + "src/cnx/instrumentation/ToolchainRequirements.ts:49", ]; /** diff --git a/src/cli/Transpiler.ts b/src/cli/Transpiler.ts index 8310b2cb2..c74add041 100644 --- a/src/cli/Transpiler.ts +++ b/src/cli/Transpiler.ts @@ -1018,19 +1018,12 @@ class Transpiler { this.headerEmissionFactsByPath.set(sourcePath, headerFacts); } - // Issue #1143: the file's one requirement list -- Render's records plus - // what the emission plan's blocks carry (#1928). Read after header-facts - // CAPTURE, and before the next file's TranspileState.reset() clears both. - // This covers a requirement that capturing a header's facts triggers - // (e.g. through HeaderEmissionCapture.convertToHeaderSymbols) -- it does - // NOT cover one the header RENDER might trigger, since #1323 moved - // rendering to Stage 5.5, after every file's requirements have already - // been read here and reset() has run N times. Currently unreachable - // rather than wrong: nothing under TRANSPILE/3-Render/headers/ calls - // ToolchainRequirements, so no header render records one -- but this - // read does not guarantee that stays true, and #1143 is precisely the - // bug class where an ordering assumption like that broke under a later - // refactor. + // Issue #1143: the file's one requirement list, a view of the emission + // plan, which decides every key (#1928, #1997). EmissionPlan.build froze + // that plan inside generate(), so which line reads it here no longer + // matters: the boundary is the plan build. A form emitted after it would + // lose its cost from the banner and from this result alike, so + // TranspileState's note* methods refuse such a write instead. const requirements = this.codeGenerator.getToolchainRequirements(); return this.buildSuccessResult( @@ -1133,9 +1126,9 @@ class Transpiler { // // The comment here used to claim it stopped a run that plans nothing from // reporting the previous run's cost. It cannot: every reader runs after - // `generate()`'s own `reset()` -- `collect()` from `buildBanner` inside - // `generate()` and from `_transpileFile` immediately after it, - // `takeDeferredSites()` from inside `generate()` -- so a run that plans no + // `generate()`'s own `reset()` -- `takeDeferredSites()` from inside + // `generate()`, whose sites the plan carries to `buildBanner` and to + // `_transpileFile` immediately after it -- so a run that plans no // file never reads the ledger at all. Verified by deleting this line: 7438 // unit tests and 1263 fixtures stay green, which is why no regression test // could be written for it. diff --git a/src/cli/__tests__/Transpiler.requirementSites.test.ts b/src/cli/__tests__/Transpiler.requirementSites.test.ts new file mode 100644 index 000000000..47ed57bab --- /dev/null +++ b/src/cli/__tests__/Transpiler.requirementSites.test.ts @@ -0,0 +1,35 @@ +/** + * #1997: the atomic requirement's site travels from where Render notes the + * LDREX/STREX loop to the result through `ToolchainRequirements`. No fixture + * can see it -- the banner prints no sites -- so this is the one check that + * fails if the note and the take stop meeting. + */ +import { describe, it, expect } from "vitest"; +import Transpiler from "../Transpiler"; +import MockFileSystem from "./MockFileSystem"; + +describe("Transpiler -- requirement sites (#1997)", () => { + it("attributes atomic-ldrex-cmsis to the file that emitted the loop", async () => { + const fs = new MockFileSystem(); + fs.addFile( + "/project/src/main.cnx", + "atomic u32 n <- 0;\nvoid bump() { n +<- 1; }\n", + ); + const transpiler = new Transpiler( + { + input: "/project/src/main.cnx", + outDir: "/project/build", + noCache: true, + target: "teensy41", + }, + fs, + ); + + const result = await transpiler.transpile({ kind: "files" }); + + expect(result.requirements).toContainEqual({ + key: "atomic-ldrex-cmsis", + sites: [{ sourcePath: "/project/src/main.cnx", line: null }], + }); + }); +}); diff --git a/src/cli/__tests__/cnx/TRANSPILE/CodeGenWalker.test.ts b/src/cli/__tests__/cnx/TRANSPILE/CodeGenWalker.test.ts index b2b7801a9..9dbe5816f 100644 --- a/src/cli/__tests__/cnx/TRANSPILE/CodeGenWalker.test.ts +++ b/src/cli/__tests__/cnx/TRANSPILE/CodeGenWalker.test.ts @@ -215,6 +215,33 @@ describe("CodeGenWalker", () => { // Debug mode affects helper generation expect(code).toContain("cnx_clamp_add_u8"); + // #1997: 2.2's one panic decision drives both the helper Render emits + // and the cost the banner reports. + expect(code).toContain("abort();"); + expect(code).toContain("hosted libc"); + }); + + it("clamps without a hosted-libc cost outside debug mode", () => { + const source = ` + clamp u8 val; + void main() { + val +<- 1; + } + `; + const { tree, program } = CNextSourceParser.parse(source); + const generator = new CodeGenWalker(new CodeGenerator()); + const symbols = TSymbolInfoAdapter.convert(declareAndResolve(tree)); + + const code = generateWithProgram(generator, tree, program, { + cppMode: false, + symbolInfo: symbols, + sourcePath: "test.cnx", + debugMode: false, + }); + + expect(code).toContain("cnx_clamp_add_u8"); + expect(code).not.toContain("abort();"); + expect(code).not.toContain("hosted libc"); }); it("should generate C++ output when cppMode is enabled", () => { @@ -304,7 +331,6 @@ describe("CodeGenWalker", () => { expect(input.knownStructs).toBeInstanceOf(Set); expect(input.callbackTypes).toBeInstanceOf(Map); expect(input.callbackFieldTypes).toBeInstanceOf(Map); - expect(typeof input.debugMode).toBe("boolean"); }); }); diff --git a/src/cnx/TRANSPILE/2-Plan/EmissionPlan.ts b/src/cnx/TRANSPILE/2-Plan/EmissionPlan.ts index e741be2ee..8de122f71 100644 --- a/src/cnx/TRANSPILE/2-Plan/EmissionPlan.ts +++ b/src/cnx/TRANSPILE/2-Plan/EmissionPlan.ts @@ -100,8 +100,10 @@ class EmissionPlan { facts.needsISR && !HeaderOwnership.ownsDeclarations(facts.selfIncludeAdded), clampOps: [...facts.clampOps], + panicOverflowHelpers: EmissionPlan.decidePanicOverflowHelpers(facts), safeDivOps: [...facts.safeDivOps], castHelpers: [...facts.castHelpers].sort((a, b) => a.localeCompare(b)), + formRequirements: EmissionPlan.decideFormRequirements(facts), }; return Object.freeze(plan); @@ -122,22 +124,79 @@ class EmissionPlan { } = { floatStaticAssert: true, irqWrappers: true, + panicOverflowHelpers: true, }; /** - * The requirements the plan's blocks carry, each with the sites that asked - * for its block. A view of decisions already in the plan, not a new one. + * Every requirement the plan decided: the forms' and the blocks', each with + * the sites that asked for it. A view of decisions already in the plan, not + * a new one. Copied on the way out: `Object.freeze` is shallow, the banner + * and `ITranspilerResult` both read this list, and the IRQ block's four keys + * share one `sites` array. */ static requirements(plan: IEmissionPlan): readonly IRecordedRequirement[] { const fields = Object.keys( EmissionPlan.PLANNED_BLOCKS, ) as (keyof typeof EmissionPlan.PLANNED_BLOCKS)[]; - return fields.flatMap((field) => { - const block = plan[field] ?? null; - return block === null - ? [] - : block.requirements.map((key) => ({ key, sites: block.sites })); - }); + return [ + ...plan.formRequirements.map((entry) => ({ + key: entry.key, + sites: [...entry.sites], + })), + ...fields.flatMap((field) => { + const block = plan[field] ?? null; + return block === null + ? [] + : block.requirements.map((key) => ({ key, sites: [...block.sites] })); + }), + ]; + } + + /** + * What the forms Render emitted cost, beside the planned blocks (#1997). + * + * Render reports the form -- it wrote a designated initializer -- and this + * decides the cost: C99 and free in C, C++20 in C++. Each branch reads an + * `emitted*` fact; the LDREX sites ride along as attribution and decide + * nothing (README §1: no decision consults instrumentation). + */ + private static decideFormRequirements( + facts: IEmissionFacts, + ): readonly IRecordedRequirement[] { + const decided: IRecordedRequirement[] = [ + { key: facts.cppMode ? "baseline-cpp" : "baseline-c", sites: [] }, + ]; + if (facts.cppMode && facts.emittedDesignatedInitializer) { + decided.push({ key: "cpp-designated-initializer", sites: [] }); + } + if (facts.cppMode && facts.emittedCompoundLiteral) { + decided.push({ key: "cpp-compound-literal", sites: [] }); + } + if (facts.emittedLdrexStrexLoop) { + decided.push({ + key: "atomic-ldrex-cmsis", + sites: [...facts.ldrexStrexSites], + }); + } + return decided; + } + + /** + * Whether the overflow helpers panic, with what that costs (#1997). + * + * Render reads this block to choose the panic templates, so the hosted-libc + * key and the `` it pays for are one decision rather than two + * readings of `debugMode` that happen to agree. + */ + private static decidePanicOverflowHelpers( + facts: IEmissionFacts, + ): IPlannedBlock | null { + if (!facts.debugMode || facts.clampOps.size === 0) return null; + return { + keyword: null, + requirements: ["overflow-panic-hosted-libc"], + sites: [], + }; } /** diff --git a/src/cnx/TRANSPILE/2-Plan/__tests__/EmissionPlan.test.ts b/src/cnx/TRANSPILE/2-Plan/__tests__/EmissionPlan.test.ts index 44904db88..f83cc43d7 100644 --- a/src/cnx/TRANSPILE/2-Plan/__tests__/EmissionPlan.test.ts +++ b/src/cnx/TRANSPILE/2-Plan/__tests__/EmissionPlan.test.ts @@ -26,6 +26,11 @@ const NOTHING: IEmissionFacts = { safeDivOps: new Set(), floatAssertSites: [], irqWrapperSites: [], + debugMode: false, + emittedDesignatedInitializer: false, + emittedCompoundLiteral: false, + emittedLdrexStrexLoop: false, + ldrexStrexSites: [], }; const facts = (over: Partial): IEmissionFacts => ({ @@ -167,6 +172,7 @@ describe("EmissionPlan (2.2 Plan)", () => { ); expect(EmissionPlan.requirements(plan)).toEqual([ + { key: "baseline-c", sites: [] }, { key: "float-assert-c11", sites: [site] }, { key: "critical-arm-gnu", sites: [] }, { key: "critical-arduino", sites: [] }, @@ -181,29 +187,152 @@ describe("EmissionPlan (2.2 Plan)", () => { // not cause. it("keeps the sites as decided when the facts' lists grow later", () => { const early = { sourcePath: "a.cnx", line: 3 }; + const late = { sourcePath: "a.cnx", line: 9 }; const irqWrapperSites = [early]; const floatAssertSites = [early]; + const ldrexStrexSites = [early]; const plan = EmissionPlan.build( facts({ needsIrqWrappers: true, irqWrapperSites, needsFloatStaticAssert: true, floatAssertSites, + emittedLdrexStrexLoop: true, + ldrexStrexSites, }), ); - irqWrapperSites.push({ sourcePath: "a.cnx", line: 9 }); - floatAssertSites.push({ sourcePath: "a.cnx", line: 9 }); + irqWrapperSites.push(late); + floatAssertSites.push(late); + ldrexStrexSites.push(late); - for (const entry of EmissionPlan.requirements(plan)) { - expect(entry.sites).toEqual([early]); - } + expect(EmissionPlan.requirements(plan)).toEqual([ + { key: "baseline-c", sites: [] }, + { key: "atomic-ldrex-cmsis", sites: [early] }, + { key: "float-assert-c11", sites: [early] }, + { key: "critical-arm-gnu", sites: [early] }, + { key: "critical-arduino", sites: [early] }, + { key: "critical-avr-libc", sites: [early] }, + { key: "critical-cmsis-fallback", sites: [early] }, + ]); }); - it("is empty when the plan has no block that costs anything", () => { - expect(EmissionPlan.requirements(EmissionPlan.build(NOTHING))).toEqual( - [], + // The banner and ITranspilerResult both read this list, and freezing the + // plan is shallow; one reader editing it must not change what the next + // one sees, nor one IRQ key's sites another's. + it("hands out copies, so one reader cannot edit what the next sees", () => { + const site = { sourcePath: "a.cnx", line: 3 }; + const forged = { sourcePath: "forged.cnx", line: 99 }; + const plan = EmissionPlan.build( + facts({ + needsIrqWrappers: true, + irqWrapperSites: [site], + emittedLdrexStrexLoop: true, + ldrexStrexSites: [site], + }), ); + type Site = { sourcePath: string; line: number | null }; + const sitesIn = ( + list: ReturnType, + key: string, + ): Site[] => { + const entry = list.find((candidate) => candidate.key === key); + if (entry === undefined) throw new Error(`no ${key} requirement`); + return entry.sites as Site[]; + }; + const sitesOf = (key: string) => + sitesIn(EmissionPlan.requirements(plan), key); + + sitesOf("atomic-ldrex-cmsis").push(forged); + const first = EmissionPlan.requirements(plan); + sitesIn(first, "critical-arm-gnu").push(forged); + + expect(sitesOf("atomic-ldrex-cmsis")).toEqual([site]); + expect(sitesIn(first, "critical-arduino")).toEqual([site]); + expect(sitesOf("critical-arm-gnu")).toEqual([site]); + }); + + it("is the mode baseline alone when nothing else costs anything", () => { + expect(EmissionPlan.requirements(EmissionPlan.build(NOTHING))).toEqual([ + { key: "baseline-c", sites: [] }, + ]); + }); + }); + + // #1997: Render reports the form it emitted; the plan decides its cost. + describe("form requirements -- decided here, not by Render (#1997)", () => { + const keys = (over: Partial): string[] => + EmissionPlan.build(facts(over)).formRequirements.map((r) => r.key); + + it.each([ + [false, ["baseline-c"]], + [true, ["baseline-cpp"]], + ] as const)("cppMode=%s decides the baseline %j", (cppMode, expected) => { + expect(keys({ cppMode })).toEqual(expected); + }); + + it.each([ + ["emittedDesignatedInitializer", "cpp-designated-initializer"], + ["emittedCompoundLiteral", "cpp-compound-literal"], + ] as const)("%s costs %s in C++ only", (fact, key) => { + expect(keys({ cppMode: true, [fact]: true })).toEqual([ + "baseline-cpp", + key, + ]); + // Control: the same form is C99 in C, so the baseline already covers it. + expect(keys({ cppMode: false, [fact]: true })).toEqual(["baseline-c"]); + }); + + it("an LDREX/STREX loop costs CMSIS, attributed to its sites", () => { + const site = { sourcePath: "a.cnx", line: null }; + const plan = EmissionPlan.build( + facts({ emittedLdrexStrexLoop: true, ldrexStrexSites: [site] }), + ); + + expect(plan.formRequirements).toEqual([ + { key: "baseline-c", sites: [] }, + { key: "atomic-ldrex-cmsis", sites: [site] }, + ]); + }); + + // README §1: no decision consults instrumentation. Losing the sites loses + // attribution, never the key; sites without the form decide nothing. + it("decides the LDREX key from the form, never from its sites", () => { + expect(keys({ emittedLdrexStrexLoop: true })).toEqual([ + "baseline-c", + "atomic-ldrex-cmsis", + ]); + expect( + keys({ ldrexStrexSites: [{ sourcePath: "a.cnx", line: null }] }), + ).toEqual(["baseline-c"]); + }); + }); + + // #1997: one decision -- Render picks the panic templates from this block, + // and the block carries what they cost. + describe("panicking overflow helpers", () => { + const clampOps = new Set(["add_u8"]); + + it("cost a hosted libc, as a planned block", () => { + const plan = EmissionPlan.build(facts({ debugMode: true, clampOps })); + + expect(plan.panicOverflowHelpers).toEqual({ + keyword: null, + requirements: ["overflow-panic-hosted-libc"], + sites: [], + }); + expect(EmissionPlan.requirements(plan).map((r) => r.key)).toEqual([ + "baseline-c", + "overflow-panic-hosted-libc", + ]); + expect(plan.formRequirements.map((r) => r.key)).toEqual(["baseline-c"]); + }); + + it.each([ + ["clamping helpers outside debug mode", { debugMode: false, clampOps }], + ["debug mode with no helper emitted", { debugMode: true }], + ] as const)("are absent for %s", (_label, over) => { + expect(EmissionPlan.build(facts(over)).panicOverflowHelpers).toBeNull(); }); }); diff --git a/src/cnx/TRANSPILE/3-Render/codegen/CodeGenerator.ts b/src/cnx/TRANSPILE/3-Render/codegen/CodeGenerator.ts index 6b7e73e76..ac2d73a45 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/CodeGenerator.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/CodeGenerator.ts @@ -73,8 +73,6 @@ import PassByValueAnalyzer from "../../2-Plan/PassByValueAnalyzer"; // Extracted resolvers that use CodeGenState // Issue #797: Centralized C-style name generation import type IRecordedRequirement from "../../../types/IRecordedRequirement"; -import ToolchainRequirements from "../../../instrumentation/ToolchainRequirements"; -import RequirementAggregator from "../../../utils/RequirementAggregator"; import EmissionPlan from "../../2-Plan/EmissionPlan"; import TranspileState from "../../TranspileState"; import StructDefaultInitializer from "./helpers/StructDefaultInitializer"; @@ -112,7 +110,6 @@ export default class CodeGenerator implements IOrchestrator { knownStructs: this.state.symbols?.knownStructs ?? new Set(), callbackTypes: this.state.callbackTypes, callbackFieldTypes: this.state.callbackFieldTypes, - debugMode: this.state.debugMode, program: this.state.program, }; } @@ -157,11 +154,10 @@ export default class CodeGenerator implements IOrchestrator { this.state.emittedCTypes.add(effect.cType); break; - // Toolchain requirement effects (Issue #1143) - case "requires": - ToolchainRequirements.record(effect.key, [ - { sourcePath: this.state.sourcePath ?? "", line: effect.line }, - ]); + // Emitted-form effects (Issue #1143): the form is a fact for the + // plan, which decides what it costs (#1997). + case "ldrex-strex-loop": + this.state.noteLdrexStrexLoop(); break; // Helper function effects @@ -236,17 +232,15 @@ export default class CodeGenerator implements IOrchestrator { /** * Issue #1143: what the last generate() call's output requires. * - * Two sources, each the owner of its facts: what Render recorded at the - * sites that emitted text, and what 2.2's planned blocks carry, read off the - * plan (#1928). The banner and the file result both read this one list. + * Read off the emission plan, which decides every key (#1928, #1997); + * Render records none. The banner and the file result both read this one + * list. * - * Must be read before the next file's this.state.reset(), which clears both. + * Must be read before the next file's this.state.reset(), which clears the + * plan. */ getToolchainRequirements(): readonly IRecordedRequirement[] { - return RequirementAggregator.union([ - ToolchainRequirements.collect(), - EmissionPlan.requirements(this.state.emissionPlan()), - ]); + return EmissionPlan.requirements(this.state.emissionPlan()); } /** diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/IGeneratorInput.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/IGeneratorInput.ts index 31e622e08..69a30b7eb 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/IGeneratorInput.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/IGeneratorInput.ts @@ -33,9 +33,6 @@ interface IGeneratorInput { /** Callback types used as struct field types: "StructName.fieldName" -> callback type name */ readonly callbackFieldTypes: ReadonlyMap; - /** Debug mode - affects overflow helper generation */ - readonly debugMode: boolean; - /** The program's declarations, by C name -- a struct's fields (#1535) */ readonly program: IProgram | null; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/TGeneratorEffect.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/TGeneratorEffect.ts index 25b6c0ee8..5995f2641 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/TGeneratorEffect.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/TGeneratorEffect.ts @@ -8,7 +8,6 @@ */ import TParameterInfo from "../../../../types/TParameterInfo"; import TIncludeHeader from "../../../../types/TIncludeHeader"; -import type TRequirementKey from "../../../../types/TRequirementKey"; type TGeneratorEffect = // === Include Effects === @@ -21,10 +20,11 @@ type TGeneratorEffect = // decides which header that costs (`CTypeIncludes`). | { type: "c-type"; cType: string } - // === Toolchain Requirement Effects (Issue #1143) === - // Emitted by the branch that produced requirement-bearing text, so no - // consumer has to infer which branch ran. - | { type: "requires"; key: TRequirementKey; line: number | null } + // === Emitted-form Effects (Issue #1143, #1997) === + // Emitted by the branch that produced the LDREX/STREX loop, so no consumer + // has to infer which branch ran. It names the form, not its cost: 2.2 Plan + // decides the requirement. + | { type: "ldrex-strex-loop" } // === Helper Function Effects === | { type: "helper"; operation: string; cnxType: string } // Overflow clamp helper diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/BitmapGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/BitmapGenerator.test.ts index f24843a97..2d667ed3f 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/BitmapGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/BitmapGenerator.test.ts @@ -33,7 +33,6 @@ function createMockInput( constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as unknown as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/EnumGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/EnumGenerator.test.ts index b47b4a1d3..00529334d 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/EnumGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/declarationGenerators/__tests__/EnumGenerator.test.ts @@ -30,7 +30,6 @@ function createMockInput( constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as unknown as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts index a77c20030..0323c3bf6 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts @@ -87,7 +87,6 @@ function createMockInput( constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, ...input, } as unknown as IGeneratorInput; diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts index 51116380d..cb523f406 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts @@ -33,7 +33,6 @@ function createMockInput(): IGeneratorInput { constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as unknown as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts index fa6703dfb..12e47ea30 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts @@ -74,7 +74,6 @@ function createMockInput(overrides?: { constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/AtomicGenerator.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/AtomicGenerator.ts index 61a3ea24a..2c8d58edb 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/AtomicGenerator.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/AtomicGenerator.ts @@ -104,13 +104,13 @@ function generateLdrexStrexLoop( const strex = STREX_MAP[typeInfo.baseType]; const cType = TYPE_MAP[typeInfo.baseType]; - // Mark that we need CMSIS headers, and record what this branch costs. - // Issue #1143: recorded here, in the branch that emits __LDREX*/__STREX*, - // so the requirement cannot be attributed to a file that took the other - // branch below. + // Mark that we need CMSIS headers, and report the form this branch emits. + // Issue #1143: reported here, in the branch that emits __LDREX*/__STREX*, + // so its cost cannot be attributed to a file that took the other branch. + // 2.2 decides that cost (#1997). effects.push( { type: "include", header: "cmsis" }, - { type: "requires", key: "atomic-ldrex-cmsis", line: null }, + { type: "ldrex-strex-loop" }, { type: "c-type", cType }, ); diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/CriticalGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/CriticalGenerator.test.ts index 69d6de0cc..86b0e9b10 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/CriticalGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/CriticalGenerator.test.ts @@ -33,7 +33,6 @@ function createMockInput(): IGeneratorInput { constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as unknown as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/SwitchGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/SwitchGenerator.test.ts index dcc34fe65..7916a3419 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/SwitchGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/statements/__tests__/SwitchGenerator.test.ts @@ -64,7 +64,6 @@ function createMockInput(options?: { constValues: new Map(), callbackTypes: new Map(), callbackFieldTypes: new Map(), - debugMode: false, program: null, } as unknown as IGeneratorInput; } diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/support/HelperGenerator.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/support/HelperGenerator.ts index 178922817..28723c444 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/support/HelperGenerator.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/support/HelperGenerator.ts @@ -13,7 +13,6 @@ import OverflowHelperTemplates from "./OverflowHelperTemplates"; import SaturatingCast from "../../helpers/SaturatingCast"; import CppModeHelper from "../../helpers/CppModeHelper"; import ReservedCnxName from "../../../../../utils/ReservedCnxName"; -import ToolchainRequirements from "../../../../../instrumentation/ToolchainRequirements"; /** * Generate a safe arithmetic helper function (div or mod). @@ -39,10 +38,13 @@ const generateSafeArithmeticHelper = ( /** * Generate all needed overflow helper functions * ADR-044: Overflow helper functions with clamping or panic behavior + * + * @param panic - 2.2's decision (`plan.panicOverflowHelpers`), not a re-read + * of debug mode: the block that decides it carries its hosted-libc cost */ const generateOverflowHelpers = ( usedClampOps: ReadonlySet, - debugMode: boolean, + panic: boolean, ): string[] => { if (usedClampOps.size === 0) { return []; @@ -50,11 +52,7 @@ const generateOverflowHelpers = ( const lines: string[] = []; - if (debugMode) { - // Issue #1143: this branch, and only this branch, pulls in a hosted libc. - // Release-mode clamp helpers are freestanding-safe, so the requirement is - // recorded here rather than wherever a clamp op happens to be registered. - ToolchainRequirements.record("overflow-panic-hosted-libc"); + if (panic) { lines.push( "// ADR-044: Debug overflow helper functions (panic on overflow)", "#include ", @@ -85,7 +83,7 @@ const generateOverflowHelpers = ( for (const op of sortedOps) { const [operation, cnxType] = op.split("_"); - const helper = debugMode + const helper = panic ? OverflowHelperTemplates.generatePanicHelper(operation, cnxType) : OverflowHelperTemplates.generateClampHelper(operation, cnxType); if (helper) { diff --git a/src/cnx/TRANSPILE/3-Render/codegen/generators/support/__tests__/HelperGenerator.test.ts b/src/cnx/TRANSPILE/3-Render/codegen/generators/support/__tests__/HelperGenerator.test.ts index 976cec673..d6422661d 100644 --- a/src/cnx/TRANSPILE/3-Render/codegen/generators/support/__tests__/HelperGenerator.test.ts +++ b/src/cnx/TRANSPILE/3-Render/codegen/generators/support/__tests__/HelperGenerator.test.ts @@ -1,26 +1,9 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import helperGenerators from "../HelperGenerator"; -import ToolchainRequirements from "../../../../../../instrumentation/ToolchainRequirements"; const { generateOverflowHelpers, generateSafeDivHelpers, generateCastHelpers } = helperGenerators; -/** - * Issue #1143: the debug branch records `overflow-panic-hosted-libc`, so these - * template functions are no longer pure. Vitest's per-file isolation hides the - * leak today, which makes it a trap rather than a failure -- the same reason - * CLAUDE.md already requires this for analyzer tests. - * - * #1452: this was `CodeGenState.reset()`, which cleared the requirement maps as - * part of clearing everything. Those maps moved to `ToolchainRequirements`, so - * the teardown names the owner directly -- it was briefly an empty body under - * this same docblock, which reads as "handled" while the write at - * `HelperGenerator.ts` records into a `private static` Map nothing clears. - */ -afterEach(() => { - ToolchainRequirements.reset(); -}); - describe("HelperGenerator - generateOverflowHelpers", () => { describe("empty input", () => { it("returns empty array for empty set", () => { diff --git a/src/cnx/TRANSPILE/CodeGenWalker.ts b/src/cnx/TRANSPILE/CodeGenWalker.ts index 207eec1a6..e23a5719c 100644 --- a/src/cnx/TRANSPILE/CodeGenWalker.ts +++ b/src/cnx/TRANSPILE/CodeGenWalker.ts @@ -1597,13 +1597,6 @@ class CodeGenWalker { ): string { const output: string[] = []; - // Issue #1143: every file carries its mode's baseline. Recorded here rather - // than assumed by consumers, so "what does this file need?" has exactly one - // answer source even for the trivial case. - ToolchainRequirements.record( - this.host.state.cppMode ? "baseline-cpp" : "baseline-c", - ); - // 2.2 Plan's fact, recorded where it is KNOWN rather than recovered from // text. `captureEmissionFacts` used to regex `#include` lines back out of // the rendered `output` array, so a fact the parse tree carries was @@ -1883,6 +1876,13 @@ class CodeGenWalker { "float_static_assert", ), irqWrapperSites: ToolchainRequirements.takeDeferredSites("irq_wrappers"), + debugMode: this.host.state.debugMode, + emittedDesignatedInitializer: + this.host.state.emittedDesignatedInitializer, + emittedCompoundLiteral: this.host.state.emittedCompoundLiteral, + emittedLdrexStrexLoop: this.host.state.emittedLdrexStrexLoop, + ldrexStrexSites: + ToolchainRequirements.takeDeferredSites("ldrex_strex_loop"), }; } @@ -1927,7 +1927,7 @@ class CodeGenWalker { ); } - const helpers = this.generateOverflowHelpers(plan.clampOps); + const helpers = this.generateOverflowHelpers(plan); if (helpers.length > 0) { output.push(...helpers); } @@ -3225,14 +3225,11 @@ class CodeGenWalker { } // For C-Next/C structs, generate designated initializer. - // Issue #1143: `.field = value` is C99 in C mode (baseline, free) but - // C++20 in C++ mode -- GCC and Clang accept it earlier as an extension, - // which is how this repo's own -std=c++14 harness compiles the output. - // The text is identical in both modes, so the mode has to be recorded - // here; no probe over the output could recover it. - if (this.host.state.cppMode) { - ToolchainRequirements.record("cpp-designated-initializer"); - } + // #1997: reported as the form emitted, in both modes. What it costs (C99 in + // C, C++20 in C++) is 2.2's decision, made from this fact and the mode. + // The text is identical in both modes, so no probe over the output could + // recover it; the fact has to come from here. + this.host.state.noteDesignatedInitializer(); const fieldInits = fields.map((f) => `.${f.fieldName} = ${f.value}`); return this.formatStructInitializer(typeName, castType, fieldInits); @@ -3266,11 +3263,9 @@ class CodeGenWalker { return initializer; } - // Issue #1143: a compound literal is C99, but is not ISO C++ at any - // version -- GCC and Clang accept it as an extension. - if (this.host.state.cppMode) { - ToolchainRequirements.record("cpp-compound-literal"); - } + // #1997: reported as the form emitted. A compound literal is C99, but is + // not ISO C++ at any version, and 2.2 decides what that costs. + this.host.state.noteCompoundLiteral(); return `(${castType})${initializer}`; } @@ -4820,12 +4815,13 @@ class CodeGenWalker { * Takes the ops from the PLAN, not from `CodeGenState`. Reading the state * here while the plan also carried them was the fact in two places with the * renderer using the other one -- the duplicate path this pass exists to - * remove, reintroduced by the pass itself. + * remove, reintroduced by the pass itself. The panic-or-clamp flavor comes + * off the plan too (#1997): the block that decides it carries its cost. */ - private generateOverflowHelpers(clampOps: readonly string[]): string[] { + private generateOverflowHelpers(plan: IEmissionPlan): string[] { return helperGenerateOverflowHelpers( - new Set(clampOps), - this.host.state.debugMode, + new Set(plan.clampOps), + plan.panicOverflowHelpers !== null, ); } diff --git a/src/cnx/TRANSPILE/TranspileState.ts b/src/cnx/TRANSPILE/TranspileState.ts index 72ad8b412..c9fe5aa5b 100644 --- a/src/cnx/TRANSPILE/TranspileState.ts +++ b/src/cnx/TRANSPILE/TranspileState.ts @@ -111,6 +111,16 @@ class TranspileState { needsFloatStaticAssert: boolean = false; /** Issue #473: IRQ wrappers for critical sections */ needsIrqWrappers: boolean = false; + /** + * #1997: the forms Render emitted, in both modes; 2.2 decides what each + * costs. Written only through the `note*` methods below, which refuse a + * write once the emission plan is built: the plan has already read these, + * so a late one would drop its cost from the banner and the file result + * without failing. + */ + private designatedInitializerEmitted: boolean = false; + private compoundLiteralEmitted: boolean = false; + private ldrexStrexLoopEmitted: boolean = false; /** Track which overflow helper types/operations are needed: "add_u8", etc. */ usedClampOps: Set = new Set(); /** Track which safe division helpers are needed: "div_u32", "mod_i16" */ @@ -118,20 +128,67 @@ class TranspileState { /** #1668: single-evaluation saturating casts, as `"f32_u8"` keys */ usedCastHelpers: Set = new Set(); + /** #1997: Render emitted a `.field = value` initializer. */ + get emittedDesignatedInitializer(): boolean { + return this.designatedInitializerEmitted; + } + + /** #1997: Render emitted a compound literal. */ + get emittedCompoundLiteral(): boolean { + return this.compoundLiteralEmitted; + } + + /** #1997: Render emitted an LDREX/STREX loop. */ + get emittedLdrexStrexLoop(): boolean { + return this.ldrexStrexLoopEmitted; + } + + noteDesignatedInitializer(): void { + this.assertFormBeforePlan("designated initializer"); + this.designatedInitializerEmitted = true; + } + + noteCompoundLiteral(): void { + this.assertFormBeforePlan("compound literal"); + this.compoundLiteralEmitted = true; + } + + /** + * The form decides the requirement; the site is attribution only, so the + * plan keeps the key even if no site arrives. + */ + noteLdrexStrexLoop(): void { + this.assertFormBeforePlan("LDREX/STREX loop"); + this.ldrexStrexLoopEmitted = true; + ToolchainRequirements.noteDeferredSite( + "ldrex_strex_loop", + this.sourcePath ?? "", + null, + ); + } + + private assertFormBeforePlan(form: string): void { + invariant( + this.emissionPlanOrNull === null, + `a ${form} was emitted after 2.2 Plan built the emission plan, so its cost would be lost`, + ); + } + /** * THE sink for include and deferred-emission requests. * * Every transport lands here -- generator effects via * `CodeGenerator.applyEffects`, the `requireInclude` callbacks injected into - * the static helpers, and direct calls from assignment handlers. One sink for - * the same reason `ToolchainRequirements.record` is one: the question "does - * this file need ?" gets exactly one recorded answer, so changing - * how that answer is REPRESENTED is one edit rather than one per writer. + * the static helpers, and direct calls from assignment handlers. One sink so + * that the question "does this file need ?" gets exactly one + * recorded answer, so changing how that answer is REPRESENTED is one edit + * rather than one per writer. * * #1452 moved the requirement half to `instrumentation/`, so the two no - * longer sit beside each other. They are still one decision made once -- this - * method is the only caller of `noteDeferredSite`, and the deferral keys below - * are the only ones it can produce. + * longer sit beside each other. They are still one decision made once -- + * this class is the only caller of `noteDeferredSite`: this method for the + * two deferral keys below, and `noteLdrexStrexLoop` for `ldrex_strex_loop` + * (#1997). Those three are the only keys it can produce. * It was a private method on `CodeGenerator` until #1449, which is why five * sites in `StringHandlers` set `needsString` raw instead -- a handler could * not reach the funnel, so it wrote the flag. That made the include decision @@ -1246,7 +1303,7 @@ class TranspileState { this.generatedStructInits = new Set(); this.exportedRegisterBlocks = []; - // Issue #1143, #1452: the per-file requirement maps moved to + // Issue #1143, #1452: the per-file requirement sites live in // src/cnx/instrumentation/ToolchainRequirements, which owns its own clearing. ToolchainRequirements.reset(); @@ -1296,6 +1353,9 @@ class TranspileState { this.needsCMSIS = false; this.needsLimits = false; this.needsIrqWrappers = false; + this.designatedInitializerEmitted = false; + this.compoundLiteralEmitted = false; + this.ldrexStrexLoopEmitted = false; this.emittedCallbackTypedefs = new Set(); this.pendingCallbackTypedefs = []; this.currentFunctionReturnType = null; diff --git a/src/cnx/TRANSPILE/__tests__/TranspileState.includes.test.ts b/src/cnx/TRANSPILE/__tests__/TranspileState.includes.test.ts index 9180c3bd6..37f1951a6 100644 --- a/src/cnx/TRANSPILE/__tests__/TranspileState.includes.test.ts +++ b/src/cnx/TRANSPILE/__tests__/TranspileState.includes.test.ts @@ -10,6 +10,8 @@ import { describe, it, expect, beforeEach } from "vitest"; import TranspileState from "../TranspileState"; import ToolchainRequirements from "../../instrumentation/ToolchainRequirements"; +import type IEmissionPlan from "../../types/IEmissionPlan"; +import type TDeferredSiteKey from "../../types/TDeferredSiteKey"; describe("TranspileState", () => { let state = new TranspileState(); @@ -57,17 +59,24 @@ describe("TranspileState", () => { // #1452 moved the sink to instrumentation/, so the observation is made // there. The subject is still this funnel: requireInclude decides which // headers defer, and that decision is what these four rows pin. + const DEFERRED_KEYS: readonly TDeferredSiteKey[] = [ + "irq_wrappers", + "float_static_assert", + "ldrex_strex_loop", + ]; it.each([ - ["float_static_assert", true], - ["irq_wrappers", true], - ["isr", false], - ["string", false], - ] as const)("%s deferred-site recorded: %s", (header, recorded) => { + ["float_static_assert", ["float_static_assert"]], + ["irq_wrappers", ["irq_wrappers"]], + ["isr", []], + ["string", []], + ] as const)("%s defers a site under: %j", (header, deferred) => { state.requireInclude(header, 42); - expect(ToolchainRequirements.takeDeferredSites(header).length > 0).toBe( - recorded, - ); + expect( + DEFERRED_KEYS.filter( + (key) => ToolchainRequirements.takeDeferredSites(key).length > 0, + ), + ).toEqual(deferred); }); // #1452: the sink's third argument. `requireInclude` briefly took @@ -294,3 +303,61 @@ describe("TranspileState", () => { }); }); }); + +// #1997: Render reports the forms it emitted; 2.2 reads them once, at the +// plan build. A form noted after that would vanish from the banner and the +// file result without failing, so the write is refused instead. +const FORMS = [ + [ + "designated initializer", + (s: TranspileState) => s.noteDesignatedInitializer(), + (s: TranspileState) => s.emittedDesignatedInitializer, + ], + [ + "compound literal", + (s: TranspileState) => s.noteCompoundLiteral(), + (s: TranspileState) => s.emittedCompoundLiteral, + ], + [ + "LDREX/STREX loop", + (s: TranspileState) => s.noteLdrexStrexLoop(), + (s: TranspileState) => s.emittedLdrexStrexLoop, + ], +] as const; + +describe("TranspileState -- emitted forms", () => { + let state = new TranspileState(); + + beforeEach(() => { + state = new TranspileState(); + ToolchainRequirements.reset(); + }); + + it.each(FORMS)( + "records a %s before the plan, and refuses one after", + (form, note, emitted) => { + expect(emitted(state)).toBe(false); + note(state); + expect(emitted(state)).toBe(true); + + state.emissionPlanOrNull = {} as IEmissionPlan; + expect(() => note(state)).toThrow(`a ${form} was emitted after 2.2 Plan`); + }, + ); + + // #1143's leak shape: a form one file emitted must not cost the next file. + it.each(FORMS)("reset clears a noted %s", (_form, note, emitted) => { + note(state); + state.reset(); + expect(emitted(state)).toBe(false); + }); + + it("notes the LDREX/STREX site for attribution", () => { + state.sourcePath = "a.cnx"; + state.noteLdrexStrexLoop(); + + expect(ToolchainRequirements.takeDeferredSites("ldrex_strex_loop")).toEqual( + [{ sourcePath: "a.cnx", line: null }], + ); + }); +}); diff --git a/src/cnx/instrumentation/ToolchainRequirements.ts b/src/cnx/instrumentation/ToolchainRequirements.ts index 5119bc022..10148002c 100644 --- a/src/cnx/instrumentation/ToolchainRequirements.ts +++ b/src/cnx/instrumentation/ToolchainRequirements.ts @@ -1,103 +1,60 @@ /** - * Issue #1143: what the generated output for one file costs the reader's - * toolchain, recorded by each 2.3 Render emitter at the moment it produces the - * text. + * Issue #1143: where each toolchain requirement of one file was asked for. * - * This holds Render's records only. The requirements 2.2's planned blocks - * carry (float asserts, IRQ wrappers) are read off the emission plan, never - * recorded here (#1928). The file's one list is - * `CodeGenerator.getToolchainRequirements()`, which unions the two; consumers - * read that, and nothing re-derives it. That Render still authors these keys - * at all, where §1's Ownership table gives toolchain requirements to 2.2, is - * tracked by #1997, not settled as the design. + * Sites only, never keys. 2.2 Plan decides every requirement key -- the + * planned blocks' own (#1928) and what each form Render emits costs (#1997) -- + * and carries these sites beside them as attribution. Render reports the forms + * it emitted; it records no requirement, which is §1's Ownership table: + * toolchain requirements are 2.2's. The file's one list is + * `CodeGenerator.getToolchainRequirements()`, read off the plan. * * PR #1141 failed by re-deriving -- its `#error` guard keyed on * `usedClampOps.size > 0` while the emission it described keyed on the template * family, so 96 snapshots shipped a guard for a builtin they did not contain. - * Recording at the emission site is what makes a consumer unable to disagree - * with what was emitted. + * Each requirement is now decided once, from the same facts its emission reads. * * ## Why this is instrumentation (#1452) * - * A toolchain requirement is a fact ABOUT the run's output, not a fact the run - * computes in order to decide anything. Both consumers are self-report: the - * banner comment on the `.c`/`.cpp`, so someone handed a generated file can see - * what it needs without the `.cnx` or this repository, and - * `ITranspilerResult.requirements`. No branch anywhere reads a recorded - * requirement to choose what to emit -- the `needs*` flags decide that, and the - * sites travel alongside as attribution. + * A site is a fact ABOUT the run's output, not a fact the run computes in order + * to decide anything. Both consumers are self-report: the banner comment on the + * `.c`/`.cpp`, and `ITranspilerResult.requirements`. No branch anywhere reads a + * site to choose what to emit -- the `needs*` and `emitted*` facts decide that, + * and the sites travel alongside as attribution. * * ## What this does NOT need, and AdrProvenance does * * [[AdrProvenance]] sits here under a stated exemption from box 4: it is * genuinely cross-pass, written at 17 `record` sites spanning 2.1 Analyze and * 2.3 Render and read once at the end of the run. This module claims no such - * exemption, because it is not cross-pass state at all. Measured on - * `a1b3e92b`, every write is inside 2.3 Render: + * exemption, because it is not cross-pass state at all. Every write is inside + * 2.3 Render's walk -- `TranspileState.requireInclude` and + * `TranspileState.noteLdrexStrexLoop` -- and every read is `takeDeferredSites` in + * `captureEmissionFacts`, inside the same `generate()` call for the same file. * - * | Write site | Pass | - * | ------------------------------------- | ------------- | - * | `CodeGenWalker` (3 sites) | 2.3 Render | - * | `CodeGenerator.applyEffects` | 2.3 Render | - * | `HelperGenerator` | 2.3 Render | - * | `TranspileState.requireInclude` | 2.3 Render | - * - * and every read -- `takeDeferredSites` at plan-build, `collect` for the banner - * and for the result -- happens inside the same `generate()` call for the same - * file. Every `requireInclude` caller is Render's walk; the one 2.2 caller - * this once excepted (`TypeRegistrationEngine`, which passed `"string"`, not a - * deferred key) was deleted with the per-file type registry (#1668, C8). - * - * That makes the move out of `CodeGenState` a correction rather than a - * relocation: the accumulation was per-file and single-pass the whole time, and - * only the container it sat in was cross-pass. Do not read its presence in this - * root as a precedent for parking cross-pass state here to get out from under - * `passes-hold-no-mutable-state.test.ts` -- the table above is the claim, and it - * is the thing to re-measure if a write site is ever added from an earlier pass. + * Do not read its presence in this root as a precedent for parking cross-pass + * state here to get out from under `passes-hold-no-mutable-state.test.ts` -- + * the paragraph above is the claim, and it is the thing to re-measure if a + * write site is ever added from an earlier pass. */ import RequirementSites from "../utils/RequirementSites"; -import type IRecordedRequirement from "../types/IRecordedRequirement"; import type IRequirementSite from "../types/IRequirementSite"; -import type TRequirementKey from "../types/TRequirementKey"; +import type TDeferredSiteKey from "../types/TDeferredSiteKey"; class ToolchainRequirements { - /** Requirements the emitted text actually carries, with every site. */ - private static recorded: Map = new Map(); - - /** - * Source sites for emissions DEFERRED to assembleGeneratedOutput -- IRQ - * wrappers and float asserts. Keyed by the request that triggered - * the deferral ("irq_wrappers", "float_static_assert"). - * - * Kept separate from `recorded` because at request time the code has not been - * emitted yet, so there is nothing to record a requirement for. - * `captureEmissionFacts` takes these sites into `IEmissionFacts`, and the - * plan block that decides the emission carries them from there (#1928); they - * never come back to `recorded`. - */ - private static deferred: Map = new Map(); - /** - * THE recording sink for toolchain requirements. - * - * Every requirement, from every transport -- generator effects, the include - * funnel, and direct calls from static helpers -- lands here. Call it from - * the branch that emits the text, never from a caller that infers which - * branch ran. + * Sites that asked for a plan-decided emission, keyed by the request. + * `captureEmissionFacts` takes these into `IEmissionFacts`, and the plan + * carries them as its requirements' sites (#1928, #1997). */ - static record( - key: TRequirementKey, - sites: readonly IRequirementSite[] = [], - ): void { - RequirementSites.accumulate(this.recorded, key, sites); - } + private static deferred: Map = + new Map(); /** - * Note where a deferred emission was requested, so the emitter can attribute - * it once it actually produces the block. + * Note where a plan-decided emission was requested, so the plan can + * attribute its requirement. */ static noteDeferredSite( - requestKey: string, + requestKey: TDeferredSiteKey, sourcePath: string, line: number | null, ): void { @@ -107,32 +64,17 @@ class ToolchainRequirements { } /** Sites recorded for a deferred emission. */ - static takeDeferredSites(requestKey: string): readonly IRequirementSite[] { + static takeDeferredSites( + requestKey: TDeferredSiteKey, + ): readonly IRequirementSite[] { return this.deferred.get(requestKey) ?? []; } /** - * Snapshot what Render recorded for this file. - * - * Not the file's whole list: the planned blocks' requirements live on the - * emission plan. `CodeGenerator.getToolchainRequirements()` is the one list, - * and its only reader of this (#1928). - * - * Must be read before the next file's reset, which clears the recording map. - */ - static collect(): readonly IRecordedRequirement[] { - return Array.from(this.recorded.entries()).map(([key, sites]) => ({ - key, - sites: [...sites], - })); - } - - /** - * Both maps MUST be cleared per file: leaking them makes a project report - * attribute one file's CMSIS or C11 cost to every later file. + * MUST be cleared per file: leaking it makes a project report attribute one + * file's CMSIS or C11 cost to every later file. */ static reset(): void { - this.recorded = new Map(); this.deferred = new Map(); } } diff --git a/src/cnx/instrumentation/__tests__/ToolchainRequirements.test.ts b/src/cnx/instrumentation/__tests__/ToolchainRequirements.test.ts index 3eef5a18e..7fcec230b 100644 --- a/src/cnx/instrumentation/__tests__/ToolchainRequirements.test.ts +++ b/src/cnx/instrumentation/__tests__/ToolchainRequirements.test.ts @@ -5,7 +5,8 @@ * was the banner line in 96 fixture snapshots, which can only fail on the * requirements a fixture happens to incur. These pin the four properties the * consumers depend on and a fixture cannot reach: dedup, the caller-supplied - * source path, the defensive copy, and a reset that clears BOTH maps. + * source path, and the reset. #1997 removed the key map, `record` and + * `collect`: 2.2 Plan decides every key. */ import { describe, it, expect, beforeEach } from "vitest"; import ToolchainRequirements from "../ToolchainRequirements"; @@ -15,94 +16,6 @@ describe("ToolchainRequirements (#1143, moved by #1452)", () => { ToolchainRequirements.reset(); }); - describe("record", () => { - it("reports a key recorded with no site at all", () => { - // HelperGenerator and the two C++ initializer sites record bare keys. - ToolchainRequirements.record("cpp-compound-literal"); - - expect(ToolchainRequirements.collect()).toEqual([ - { key: "cpp-compound-literal", sites: [] }, - ]); - }); - - it("accumulates distinct sites under one key", () => { - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 9 }, - ]); - - const [requirement] = ToolchainRequirements.collect(); - expect(requirement.sites).toEqual([ - { sourcePath: "a.cnx", line: 1 }, - { sourcePath: "a.cnx", line: 9 }, - ]); - }); - - it("does not record the same site twice", () => { - // Two emitters can incur one requirement at one line; the banner must - // not name it twice. - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - - expect(ToolchainRequirements.collect()[0].sites).toHaveLength(1); - }); - - it("keeps the same line in different files apart", () => { - // Negative control for the dedup above: a body comparing only `line` - // passes every row so far and fails this one. - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "b.cnx", line: 1 }, - ]); - - expect(ToolchainRequirements.collect()[0].sites).toHaveLength(2); - }); - - it("keeps separate keys separate", () => { - // Negative control: a sink that merged everything into one bucket passes - // all of the above. - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - ToolchainRequirements.record("cpp-designated-initializer", [ - { sourcePath: "a.cnx", line: 2 }, - ]); - - expect( - ToolchainRequirements.collect().map((requirement) => requirement.key), - ).toEqual(["cpp-compound-literal", "cpp-designated-initializer"]); - }); - }); - - describe("collect", () => { - it("hands out a copy, so a consumer cannot edit the record", () => { - // Two consumers read this -- the banner and ITranspilerResult -- and the - // banner formats what it is given. Returning the live array would let - // the first reader change what the second one sees. - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); - - const first = ToolchainRequirements.collect(); - (first[0].sites as { sourcePath: string; line: number }[]).push({ - sourcePath: "forged.cnx", - line: 99, - }); - - expect(ToolchainRequirements.collect()[0].sites).toEqual([ - { sourcePath: "a.cnx", line: 1 }, - ]); - }); - }); - describe("deferred sites", () => { it("attributes a site to the path the caller passed", () => { // #1452: this read `CodeGenState.sourcePath` off a global. The caller @@ -139,19 +52,13 @@ describe("ToolchainRequirements (#1143, moved by #1452)", () => { }); describe("reset", () => { - it("clears BOTH maps", () => { - // #1143's leak: a reset that cleared one map attributed one file's CMSIS - // or C11 cost to every later file. Asserting both in one test is - // deliberate -- split, a reset that forgot the deferred map would still - // show one green row. - ToolchainRequirements.record("cpp-compound-literal", [ - { sourcePath: "a.cnx", line: 1 }, - ]); + it("clears the deferred sites", () => { + // #1143's leak: a reset that missed a map attributed one file's CMSIS or + // C11 cost to every later file. Since #1997 the sites are the only map. ToolchainRequirements.noteDeferredSite("irq_wrappers", "a.cnx", 1); ToolchainRequirements.reset(); - expect(ToolchainRequirements.collect()).toEqual([]); expect(ToolchainRequirements.takeDeferredSites("irq_wrappers")).toEqual( [], ); diff --git a/src/cnx/types/IEmissionFacts.ts b/src/cnx/types/IEmissionFacts.ts index 93c105cd4..b9aae2cc7 100644 --- a/src/cnx/types/IEmissionFacts.ts +++ b/src/cnx/types/IEmissionFacts.ts @@ -77,6 +77,21 @@ interface IEmissionFacts { /** Sites that asked for the IRQ wrappers, for attribution. */ readonly irqWrapperSites: readonly IRequirementSite[]; + + /** ADR-044 debug mode, from which 2.2 decides whether clamp helpers panic. */ + readonly debugMode: boolean; + + /** #1997: Render emitted a `.field = value` initializer, in either mode. */ + readonly emittedDesignatedInitializer: boolean; + + /** #1997: Render emitted a compound literal, in either mode. */ + readonly emittedCompoundLiteral: boolean; + + /** #1997: Render emitted an LDREX/STREX loop. */ + readonly emittedLdrexStrexLoop: boolean; + + /** Where Render emitted LDREX/STREX loops, for attribution only. */ + readonly ldrexStrexSites: readonly IRequirementSite[]; } export default IEmissionFacts; diff --git a/src/cnx/types/IEmissionPlan.ts b/src/cnx/types/IEmissionPlan.ts index 6d2e170c3..c298cb82a 100644 --- a/src/cnx/types/IEmissionPlan.ts +++ b/src/cnx/types/IEmissionPlan.ts @@ -1,4 +1,5 @@ import type IPlannedBlock from "./IPlannedBlock"; +import type IRecordedRequirement from "./IRecordedRequirement"; /** * 2.2 Plan's artifact: what C should exist for ONE file. @@ -65,11 +66,26 @@ interface IEmissionPlan { /** ADR-044 overflow-clamp helper keys (`"add_u32"`), in emission order. */ readonly clampOps: readonly string[]; + /** + * ADR-044 debug mode's panicking overflow helpers, or null when the helpers + * clamp or there are none. Render picks the panic templates from this, so + * the hosted-libc cost and the `` it pays for are one decision + * (#1997). + */ + readonly panicOverflowHelpers: IPlannedBlock | null; + /** ADR-051 safe-division helper keys (`"div_u32"`), in emission order. */ readonly safeDivOps: readonly string[]; /** #1668: single-evaluation saturating cast keys (`"f32_u8"`), in emission order. */ readonly castHelpers: readonly string[]; + + /** + * #1997: the requirements no planned block carries -- the mode baseline, the + * C++ initializer forms, LDREX/STREX atomics. + * Render reports which forms it emitted; what each costs is decided here. + */ + readonly formRequirements: readonly IRecordedRequirement[]; } export default IEmissionPlan; diff --git a/src/cnx/types/IFileResult.ts b/src/cnx/types/IFileResult.ts index 46e816577..c09fa8d94 100644 --- a/src/cnx/types/IFileResult.ts +++ b/src/cnx/types/IFileResult.ts @@ -40,8 +40,8 @@ interface IFileResult { /** * Issue #1143: Toolchain requirements this file's generated output actually - * carries: recorded by the emitters that produced the text, plus what the - * emission plan's blocks carry (#1928). + * carries. 2.2 Plan decides every key, from a planned block (#1928) or a + * form Render reported emitting (#1997); the sites are attribution. */ requirements?: readonly IRecordedRequirement[]; } diff --git a/src/cnx/types/IRecordedRequirement.ts b/src/cnx/types/IRecordedRequirement.ts index 315ab1c43..ed1fc8ece 100644 --- a/src/cnx/types/IRecordedRequirement.ts +++ b/src/cnx/types/IRecordedRequirement.ts @@ -5,9 +5,9 @@ import type TRequirementKey from "./TRequirementKey"; * Issue #1143: A requirement that generated output actually carries, together * with every source location that incurred it. * - * Recorded by the emitter at the moment the text is produced, or carried by the - * 2.2 plan block that decided the text (#1928), so a consumer can never - * disagree with what was emitted. + * Decided by 2.2 Plan, from the planned block that decided the text (#1928) or + * the form Render reported emitting (#1997), so a consumer can never disagree + * with what was emitted. The sites are attribution only. */ interface IRecordedRequirement { /** Registry key; look up the details in TOOLCHAIN_REQUIREMENTS. */ diff --git a/src/cnx/types/TDeferredSiteKey.ts b/src/cnx/types/TDeferredSiteKey.ts new file mode 100644 index 000000000..b3ba1d1f3 --- /dev/null +++ b/src/cnx/types/TDeferredSiteKey.ts @@ -0,0 +1,12 @@ +/** + * #1997: the requests whose sites `ToolchainRequirements` defers to the 2.2 + * plan. The note side (`TranspileState`) and the take side + * (`captureEmissionFacts`) name the same key, so a misspelling on either is a + * `tsc` error rather than a requirement that silently loses its sites. + */ +type TDeferredSiteKey = + | "irq_wrappers" + | "float_static_assert" + | "ldrex_strex_loop"; + +export default TDeferredSiteKey; diff --git a/src/cnx/utils/RequirementAggregator.ts b/src/cnx/utils/RequirementAggregator.ts index 03a88e727..5d63c5b47 100644 --- a/src/cnx/utils/RequirementAggregator.ts +++ b/src/cnx/utils/RequirementAggregator.ts @@ -24,8 +24,8 @@ class RequirementAggregator { /** * Union requirement lists by key, keeping every distinct site in the order - * first seen. One file's list is built this way too (#1928): what Render - * recorded, plus what the emission plan's blocks carry. + * first seen. Feeds `merge` only: one file's list is a view of its emission + * plan (#1997), which has one entry per key already. */ static union( lists: readonly (readonly IRecordedRequirement[])[], diff --git a/src/cnx/utils/RequirementSites.ts b/src/cnx/utils/RequirementSites.ts index 9d7f7d4df..81cc3dfd9 100644 --- a/src/cnx/utils/RequirementSites.ts +++ b/src/cnx/utils/RequirementSites.ts @@ -5,7 +5,8 @@ import type IRequirementSite from "../types/IRequirementSite"; * * "Is this the same site?" was written out three times -- in * `CodeGenState.requireToolchain`, in `CodeGenState.noteDeferredSite` (now - * `ToolchainRequirements.record` and `.noteDeferredSite`, #1452), and in + * `ToolchainRequirements.record` and `.noteDeferredSite`, #1452; `record` was + * deleted by #1997), and in * `RequirementAggregator.merge` (now `.union`, #1928). Adding a column to * `IRequirementSite`, or deciding that an unlocated site should merge with a * located one, would have been three edits that must agree. jscpd cannot see diff --git a/src/cnx/utils/__tests__/RequirementSites.test.ts b/src/cnx/utils/__tests__/RequirementSites.test.ts index 1d709d740..3e959e169 100644 --- a/src/cnx/utils/__tests__/RequirementSites.test.ts +++ b/src/cnx/utils/__tests__/RequirementSites.test.ts @@ -1,6 +1,6 @@ /** - * #1928: the one key-to-sites fold `ToolchainRequirements.record`, - * `.noteDeferredSite` and `RequirementAggregator.union` all share. + * #1928: the one key-to-sites fold `ToolchainRequirements.noteDeferredSite` + * and `RequirementAggregator.union` share (`record` too, until #1997). */ import { describe, it, expect } from "vitest"; import RequirementSites from "../RequirementSites"; diff --git a/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.c b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.c new file mode 100644 index 000000000..cd3ed05d9 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.c @@ -0,0 +1,23 @@ +/** + * Generated by C-Next Transpiler from: emitted-form-reset.test.cnx + * A safer C for embedded systems + */ + +// test-execution +// Issue #1997: Render reports each form it emits (here a designated +// initializer) as a per-file fact, and 2.2 Plan decides what it costs. The +// helper emits one; this file does not. If TranspileState.reset() kept the +// helper's fact, this file's C++ banner would claim "Requires: C++20." and the +// designated-initializer extension for code it does not contain -- the #1143 +// leak, moved from Render's requirement map to its form facts. +#include "lib/origin.h" + +#include + +int main(void) { + int32_t x = originX(); + if (x != 0) { + return 1U; + } + return 0U; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.cpp b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.cpp new file mode 100644 index 000000000..b387b8108 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.expected.cpp @@ -0,0 +1,23 @@ +/** + * Generated by C-Next Transpiler from: emitted-form-reset.test.cnx + * A safer C for embedded systems + */ + +// test-execution +// Issue #1997: Render reports each form it emits (here a designated +// initializer) as a per-file fact, and 2.2 Plan decides what it costs. The +// helper emits one; this file does not. If TranspileState.reset() kept the +// helper's fact, this file's C++ banner would claim "Requires: C++20." and the +// designated-initializer extension for code it does not contain -- the #1143 +// leak, moved from Render's requirement map to its form facts. +#include "lib/origin.hpp" + +#include + +int main(void) { + int32_t x = originX(); + if (x != 0) { + return 1U; + } + return 0U; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.c b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.c new file mode 100644 index 000000000..cd3ed05d9 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.c @@ -0,0 +1,23 @@ +/** + * Generated by C-Next Transpiler from: emitted-form-reset.test.cnx + * A safer C for embedded systems + */ + +// test-execution +// Issue #1997: Render reports each form it emits (here a designated +// initializer) as a per-file fact, and 2.2 Plan decides what it costs. The +// helper emits one; this file does not. If TranspileState.reset() kept the +// helper's fact, this file's C++ banner would claim "Requires: C++20." and the +// designated-initializer extension for code it does not contain -- the #1143 +// leak, moved from Render's requirement map to its form facts. +#include "lib/origin.h" + +#include + +int main(void) { + int32_t x = originX(); + if (x != 0) { + return 1U; + } + return 0U; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cnx b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cnx new file mode 100644 index 000000000..a11d549e0 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cnx @@ -0,0 +1,16 @@ +// test-execution +// Issue #1997: Render reports each form it emits (here a designated +// initializer) as a per-file fact, and 2.2 Plan decides what it costs. The +// helper emits one; this file does not. If TranspileState.reset() kept the +// helper's fact, this file's C++ banner would claim "Requires: C++20." and the +// designated-initializer extension for code it does not contain -- the #1143 +// leak, moved from Render's requirement map to its form facts. +#include "lib/origin.cnx" + +u32 main() { + i32 x <- originX(); + if (x != 0) { + return 1; + } + return 0; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cpp b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cpp new file mode 100644 index 000000000..b387b8108 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/emitted-form-reset.test.cpp @@ -0,0 +1,23 @@ +/** + * Generated by C-Next Transpiler from: emitted-form-reset.test.cnx + * A safer C for embedded systems + */ + +// test-execution +// Issue #1997: Render reports each form it emits (here a designated +// initializer) as a per-file fact, and 2.2 Plan decides what it costs. The +// helper emits one; this file does not. If TranspileState.reset() kept the +// helper's fact, this file's C++ banner would claim "Requires: C++20." and the +// designated-initializer extension for code it does not contain -- the #1143 +// leak, moved from Render's requirement map to its form facts. +#include "lib/origin.hpp" + +#include + +int main(void) { + int32_t x = originX(); + if (x != 0) { + return 1U; + } + return 0U; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/lib/origin.c b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.c new file mode 100644 index 000000000..19aebe8cd --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.c @@ -0,0 +1,16 @@ +/** + * Generated by C-Next Transpiler from: origin.cnx + * A safer C for embedded systems + */ + +#include "lib/origin.h" + +#include + +// Helper for issue #1997 -- the file that emits a designated initializer. +// It is generated before the entry that includes it, so its emitted form is +// what a missing per-file reset would carry into the entry. +int32_t originX(void) { + Point origin = { .x = 0, .y = 0 }; + return origin.x; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cnx b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cnx new file mode 100644 index 000000000..732395b46 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cnx @@ -0,0 +1,12 @@ +// Helper for issue #1997 -- the file that emits a designated initializer. +// It is generated before the entry that includes it, so its emitted form is +// what a missing per-file reset would carry into the entry. +struct Point { + i32 x; + i32 y; +} + +i32 originX() { + Point origin <- { x: 0, y: 0 }; + return origin.x; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cpp b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cpp new file mode 100644 index 000000000..12fbace35 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.cpp @@ -0,0 +1,18 @@ +/** + * Generated by C-Next Transpiler from: origin.cnx + * A safer C for embedded systems + * Requires: C++20. + * GNU/Clang extensions: designated initializers in C++. + */ + +#include "lib/origin.hpp" + +#include + +// Helper for issue #1997 -- the file that emits a designated initializer. +// It is generated before the entry that includes it, so its emitted form is +// what a missing per-file reset would carry into the entry. +int32_t originX(void) { + Point origin = { .x = 0, .y = 0 }; + return origin.x; +} diff --git a/tests/bugs/issue-1997-emitted-form-reset/lib/origin.h b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.h new file mode 100644 index 000000000..0c19d1ff5 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.h @@ -0,0 +1,28 @@ +#ifndef CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H +#define CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H + +/** + * Generated by C-Next Transpiler from: origin.cnx + * Header file for cross-language interoperability + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Struct definitions */ +typedef struct Point { + int32_t x; + int32_t y; +} Point; + +/* Function prototypes */ +int32_t originX(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H */ diff --git a/tests/bugs/issue-1997-emitted-form-reset/lib/origin.hpp b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.hpp new file mode 100644 index 000000000..0c19d1ff5 --- /dev/null +++ b/tests/bugs/issue-1997-emitted-form-reset/lib/origin.hpp @@ -0,0 +1,28 @@ +#ifndef CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H +#define CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H + +/** + * Generated by C-Next Transpiler from: origin.cnx + * Header file for cross-language interoperability + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Struct definitions */ +typedef struct Point { + int32_t x; + int32_t y; +} Point; + +/* Function prototypes */ +int32_t originX(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CNX_TESTS_BUGS_ISSUE_1997_EMITTED_FORM_RESET_LIB_ORIGIN_H */