Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ files at the root: an entry point lives inside the root it starts.
construct the pipeline. 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 required. Written
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
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.
Expand All @@ -141,7 +143,8 @@ files at the root: an entry point lives inside the root it starts.

**"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: C11.` into the generated `.c`, and `captureEmissionFacts` carries
` * Requires: C++20.` into a generated `.cpp` that uses a designated initializer
(`cpp-designated-initializer`, recorded where `CodeGenWalker` emits it), 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
Expand Down
4 changes: 2 additions & 2 deletions scripts/__tests__/passes-hold-no-mutable-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ const INSTRUMENTATION_ROOT = "src/instrumentation";
const INSTRUMENTATION_HOLDERS = [
"src/instrumentation/AdrProvenance.ts:58",
"src/instrumentation/AdrProvenance.ts:59",
"src/instrumentation/ToolchainRequirements.ts:56",
"src/instrumentation/ToolchainRequirements.ts:67",
"src/instrumentation/ToolchainRequirements.ts:65",
"src/instrumentation/ToolchainRequirements.ts:78",
];

/**
Expand Down
38 changes: 36 additions & 2 deletions src/TRANSPILE/2-Plan/EmissionPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import type IEmissionFacts from "../../types/IEmissionFacts";
import type IEmissionPlan from "../../types/IEmissionPlan";
import type IPlannedBlock from "../../types/IPlannedBlock";
import type IRecordedRequirement from "../../types/IRecordedRequirement";
import type TRequirementKey from "../../types/TRequirementKey";
import SYSTEM_INCLUDE_TARGETS from "./SYSTEM_INCLUDE_TARGETS";
import CTypeIncludes from "./CTypeIncludes";
Expand Down Expand Up @@ -106,6 +107,39 @@ class EmissionPlan {
return Object.freeze(plan);
}

/**
* Every `IEmissionPlan` field that holds a planned block, nullable or
* optional. A block added to the plan and not listed here fails to compile,
* instead of rendering with its cost missing from the banner -- #1143's
* failure.
*/
private static readonly PLANNED_BLOCKS: {
readonly [K in keyof IEmissionPlan as NonNullable<
IEmissionPlan[K]
> extends IPlannedBlock
? K
: never]-?: true;
} = {
floatStaticAssert: true,
irqWrappers: 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.
*/
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 }));
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
});
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/**
* Which system headers the implementation file emits, already deduplicated
* against what its own source includes.
Expand Down Expand Up @@ -151,7 +185,7 @@ class EmissionPlan {
return {
keyword: facts.cppMode ? "static_assert" : "_Static_assert",
requirements: [facts.cppMode ? "float-assert-cpp11" : "float-assert-c11"],
sites: facts.floatAssertSites,
sites: [...facts.floatAssertSites],
};
}

Expand All @@ -163,7 +197,7 @@ class EmissionPlan {
return {
keyword: null,
requirements: IRQ_WRAPPER_REQUIREMENTS,
sites: facts.irqWrapperSites,
sites: [...facts.irqWrapperSites],
};
}
}
Expand Down
50 changes: 50 additions & 0 deletions src/TRANSPILE/2-Plan/__tests__/EmissionPlan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,56 @@ describe("EmissionPlan (2.2 Plan)", () => {
});
});

describe("requirements -- read off the blocks, never copied (#1928)", () => {
it("lists every planned block's keys with that block's sites", () => {
const site = { sourcePath: "a.cnx", line: 3 };
const plan = EmissionPlan.build(
facts({
needsFloatStaticAssert: true,
needsIrqWrappers: true,
floatAssertSites: [site],
}),
);

expect(EmissionPlan.requirements(plan)).toEqual([
{ key: "float-assert-c11", sites: [site] },
{ key: "critical-arm-gnu", sites: [] },
{ key: "critical-arduino", sites: [] },
{ key: "critical-avr-libc", sites: [] },
{ key: "critical-cmsis-fallback", sites: [] },
]);
});

// The facts' site lists are 1.x-era accumulators that can still grow; a
// site noted after the decision must not join a block it did not cause.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
it("keeps the sites as decided when the facts' lists grow later", () => {
const early = { sourcePath: "a.cnx", line: 3 };
const irqWrapperSites = [early];
const floatAssertSites = [early];
const plan = EmissionPlan.build(
facts({
needsIrqWrappers: true,
irqWrapperSites,
needsFloatStaticAssert: true,
floatAssertSites,
}),
);

irqWrapperSites.push({ sourcePath: "a.cnx", line: 9 });
floatAssertSites.push({ sourcePath: "a.cnx", line: 9 });

for (const entry of EmissionPlan.requirements(plan)) {
expect(entry.sites).toEqual([early]);
}
});

it("is empty when the plan has no block that costs anything", () => {
expect(EmissionPlan.requirements(EmissionPlan.build(NOTHING))).toEqual(
[],
);
});
});

describe("ISR typedef", () => {
it.each([
[false, true],
Expand Down
17 changes: 12 additions & 5 deletions src/TRANSPILE/3-Render/codegen/CodeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import PassByValueAnalyzer from "../../2-Plan/PassByValueAnalyzer";
// 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";
import StructDefault from "../../../utils/StructDefault";
Expand Down Expand Up @@ -232,14 +234,19 @@ export default class CodeGenerator implements IOrchestrator {
}

/**
* Issue #1143: Snapshot the toolchain requirements recorded during the last
* generate() call.
* Issue #1143: what the last generate() call's output requires.
*
* Must be read before the next file's this.state.reset(), which clears the
* recording map.
* 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.
*
* Must be read before the next file's this.state.reset(), which clears both.
*/
getToolchainRequirements(): readonly IRecordedRequirement[] {
return ToolchainRequirements.collect();
return RequirementAggregator.union([
ToolchainRequirements.collect(),
EmissionPlan.requirements(this.state.emissionPlan()),
]);
}

/**
Expand Down
35 changes: 3 additions & 32 deletions src/TRANSPILE/CodeGenWalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1650,11 +1650,9 @@ class CodeGenWalker {
this.captureEmissionFacts(sourceIncludeTargets),
);

// The plan decided WHICH toolchain capabilities these helpers need and at
// which sites; registering them is part of that decision, not part of
// formatting. `addGeneratedHelpers` used to do it while pushing the text,
// which made the renderer a writer of state something downstream reads.
CodeGenWalker.registerPlannedToolchain(plan);
// #1928: the banner and the file result read the planned blocks'
// requirements off the plan itself, so nothing copies them anywhere.
this.host.state.emissionPlanOrNull = plan;

// 2.3 Render: format the plan. These two decide nothing.
this.addAutoIncludes(output, plan);
Expand Down Expand Up @@ -1887,9 +1885,6 @@ class CodeGenWalker {
return match ? match[1] : null;
}

/**
* Add generated helpers (static asserts, IRQ wrappers, typedefs, etc.).
*/
/**
* Print the deferred blocks and helpers the plan decided on.
*
Expand All @@ -1898,31 +1893,7 @@ class CodeGenWalker {
* the two cannot disagree -- #1143 kept them in step by computing both from
* one ternary at this site; the plan keeps them in step by making them two
* fields of one record, and the ternary is gone from here.
*
* Requirements are still recorded into `CodeGenState` rather than read off
* the plan by the banner, because the banner also carries requirements this
* plan does not yet own (the mode baseline, C++ initializer forms, atomics).
* Recording a decision someone else made is transcription, not derivation.
*/
/**
* Register the toolchain capabilities the plan's helper blocks need.
*
* Separated from `addGeneratedHelpers` so that rendering a block and
* declaring what the block requires are not the same act: the plan already
* holds both the keys and the sites, and a renderer that writes them is a
* renderer making state visible downstream. Order is unchanged -- this runs
* immediately after the plan is built, and nothing between reads a toolchain
* requirement.
*/
private static registerPlannedToolchain(plan: IEmissionPlan): void {
for (const block of [plan.floatStaticAssert, plan.irqWrappers]) {
if (block === null) continue;
for (const key of block.requirements) {
ToolchainRequirements.record(key, block.sites);
}
}
}

private addGeneratedHelpers(output: string[], plan: IEmissionPlan): void {
const floatAssert = plan.floatStaticAssert;
if (floatAssert !== null) {
Expand Down
31 changes: 28 additions & 3 deletions src/TRANSPILE/TranspileState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type ITypeBindingDeps from "../types/ITypeBindingDeps";
import type IProgram from "../types/IProgram";
import type ITypingContext from "../types/ITypingContext";
import type IDeclarationPlan from "../types/IDeclarationPlan";
import type IEmissionPlan from "../types/IEmissionPlan";
import type IFunctionSignature from "../types/IFunctionSignature";
import invariant from "../utils/invariant";
import emittedLocalName from "../utils/emittedLocalName";
Expand Down Expand Up @@ -140,12 +141,12 @@ class TranspileState {
*/
requireInclude(header: TIncludeHeader, line: number | null = null): void {
// Issue #1143: three of these "headers" are really deferred code-emission
// requests. Record where they were asked for, so the emitter that finally
// produces the block can attribute its requirement to a .cnx line. No
// requests. Record where they were asked for, so the 2.2 plan block that
// decides the emission carries them as its requirements' sites (#1928). No
// requirement is recorded here -- the code does not exist yet, and
// recording a requirement for text that may never be emitted is exactly
// the mistake that made #1141's guard fire on files without the construct.
// Only the two headers that have a claiming emitter. "isr" was noted here
// Only the two headers whose sites a plan block carries. "isr" was noted here
// and never read: ToolchainRequirements.takeDeferredSites is called for
// float_static_assert and irq_wrappers alone, and the ISR typedef carries
// no requirement. Keeping
Expand Down Expand Up @@ -309,6 +310,14 @@ class TranspileState {
* silently-wrong default.
*/
declarationPlanOrNull: IDeclarationPlan | null = null;
/**
* 2.2 Plan's emission decisions for the file being generated.
*
* Held for the rest of the file's turn so the requirements its planned
* blocks carry are read off the plan, never copied into
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
* `ToolchainRequirements` (#1928). Null until the declarations have rendered.
*/
emissionPlanOrNull: IEmissionPlan | null = null;
/** ADR-013: Track function parameter const-ness for call-site validation */
functionSignatures: Map<string, IFunctionSignature> = new Map();
/** Callback field types: "Struct.field" -> callbackTypeName */
Expand Down Expand Up @@ -363,6 +372,21 @@ class TranspileState {
);
return plan;
}
/**
* 2.2 Plan's emission decisions, asserted present.
*
* Same reason as `declarationPlan()`: answering "no planned block costs
* anything" before the plan exists would drop the interrupt-masking and C11
* lines from the banner without failing.
*/
emissionPlan(): IEmissionPlan {
const plan = this.emissionPlanOrNull;
invariant(
plan !== null,
"2.2 Plan builds the emission plan before its requirements are read",
);
return plan;
}
/**
* Compute unmodified parameters for all functions on-demand.
* Returns a map of function name -> Set of parameter names NOT modified.
Expand Down Expand Up @@ -1219,6 +1243,7 @@ class TranspileState {
this.callbackTypeReferences = new Set();
this.publicCallbackTypeReferences = new Set();
this.declarationPlanOrNull = null;
this.emissionPlanOrNull = null;
this.usedClampOps = new Set();
this.usedSafeDivOps = new Set();
this.usedCastHelpers = new Set();
Expand Down
8 changes: 8 additions & 0 deletions src/TRANSPILE/__tests__/CodeGenWalker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17715,3 +17715,11 @@ describe("CodeGenWalker", () => {
});
});
});

describe("CodeGenerator.getToolchainRequirements (#1928)", () => {
it("refuses to answer before 2.2 has built the emission plan", () => {
expect(() => new CodeGenerator().getToolchainRequirements()).toThrow(
"2.2 Plan builds the emission plan before its requirements are read",
);
});
});
6 changes: 3 additions & 3 deletions src/TRANSPILE/__tests__/TranspileState.includes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* `CodeGenState.test.ts` while the members were statics; the behavior they
* pin is unchanged, and the negative controls came with them -- the include
* funnel must raise ONE flag rather than blanket them, and only the two
* headers with a claiming emitter record a deferred site.
* headers whose sites a plan block carries record a deferred site.
*/
import { describe, it, expect, beforeEach } from "vitest";
import TranspileState from "../TranspileState";
Expand Down Expand Up @@ -49,8 +49,8 @@ describe("TranspileState", () => {
expect(state.needsIrqWrappers).toBe(false);
});

// #1143: only the two headers with a claiming emitter are recorded as
// deferred sites. "isr" is deliberately NOT one --
// #1143: only the two headers whose sites a plan block carries are
// recorded as deferred sites. "isr" is deliberately NOT one --
// ToolchainRequirements.takeDeferredSites is called for
// float_static_assert and irq_wrappers alone.
//
Expand Down
24 changes: 13 additions & 11 deletions src/cli/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,17 +1018,19 @@ class Transpiler {
this.headerEmissionFactsByPath.set(sourcePath, headerFacts);
}

// Issue #1143: read after header-facts CAPTURE, and before the next
// file's TranspileState.reset() clears the recording map. This covers a
// requirement that capturing a header's facts triggers (e.g. through
// HeaderEmissionCapture.convertToHeaderSymbols) -- it does NOT cover one the 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: TranspileState.requireToolchain
// has no caller under output/headers/, so no render path 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 -- 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.
const requirements = this.codeGenerator.getToolchainRequirements();

return this.buildSuccessResult(
Expand Down
Loading
Loading