Skip to content

Release Rumoca 0.10.0 with checked Solve, GALEC, and FMI 3 pipelines - #340

Open
jgoppert wants to merge 194 commits into
mainfrom
msl-trace-parity-50
Open

Release Rumoca 0.10.0 with checked Solve, GALEC, and FMI 3 pipelines#340
jgoppert wants to merge 194 commits into
mainfrom
msl-trace-parity-50

Conversation

@jgoppert

Copy link
Copy Markdown
Member

Summary

This release candidate advances Rumoca to 0.10.0 and completes the checked compiler path needed by current Modelica flight-control consumers.

The branch is a linear 31-commit series directly on public main. It replaces intermediate development history with semantic commits covering the IR, Solve construction and evaluation, generated targets, runtime behavior, regression evidence, and the final version update.

Major user-visible outcomes:

  • valid-by-construction DAE, Solve, and target aggregates with checked ownership
  • tensor-native structural, Solve, and code-generation paths
  • exact algebraic refresh scheduling and dependency certification
  • GALEC/eFMI generation with shared generated kernels
  • FMI 3 source export with native Boolean and tensor variables, exact parameter starts, checked pure calls, compact tensor operations, and initialization assertions
  • corrected nested function correlation and conditional array-update semantics
  • runtime event, initialization, projection, and rollback hardening
  • MSL trace-parity evidence and bounded worker orchestration
  • Rumoca package, Python metadata, and VS Code metadata advanced consistently to 0.10.0

Specification alignment

The implementation was reviewed against:

  • SPEC_0007 and SPEC_0040 for pipeline and stage ownership
  • SPEC_0036 and SPEC_0043 for valid-by-construction aggregates
  • SPEC_0032 for compact tensor and range-preserving semantics
  • SPEC_0029 and SPEC_0041 for crate and helper ownership
  • SPEC_0034 and SPEC_0042 for GALEC and eFMI export
  • SPEC_0022 for Modelica language behavior
  • SPEC_0021 for complexity and deterministic construction
  • SPEC_0033 for first-divergence triage and evidence
  • SPEC_0025 for branch, risk, and verification reporting

RDD2 integration evidence

The consolidated compiler exported the west-pinned RDD2 Plant as FMI 3 source, compiled the generated C under strict C99 warnings-as-errors, executed a 32-second co-simulation run, preserved invalid-parameter assertion failure, and passed the landing-contact runner.

Generated RDD2 Guidance and Navigation artifacts also compiled with their shared kernel translation unit under strict warnings. Focused runtime probes produced finite, status-zero guidance outputs with discriminating position-mode thrust and accepted the nominal GPS and optical-flow corrections in Navigation.

These receipts establish the compiler capability used by the downstream flight branch. They do not replace downstream firmware, FastDyn, or hardware qualification.

Risk and design notes

This is a large compiler-architecture change. The principal correctness risk is a mismatch between checked producer ownership and a later scalar or generated-code view. The series addresses that risk with construction-time certificates, earliest-layer fixes, strict target rejection, generated-runtime discriminators, and adversarial model probes.

The principal maintenance risk is the breadth of target and runtime surfaces. Shared typed programs and shared generated kernels reduce duplicated lowering, while target-specific checks remain fail-closed.

No superseded internal compatibility reader, optional legacy eFMI path, or silent capability fallback is introduced.

Verification

Completed on the final source content before the version-only 0.10.0 metadata commit:

  • verify quick: green
  • MSL parity: green with 113 strict-high models, zero bad comparisons, and 122 successful simulations
  • architecture and workspace gates: green
  • focused Solve, GALEC, codegen, FMI 3, generated-C, Guidance, Navigation, and RDD2 Plant gates: green
  • cargo xtask verify docs: green after correcting the two rustdoc links found by the broad run
  • git diff --check: green
  • linear-history, sign-off, ignored dev directory, and attribution scans: green

The broad full run reached the documentation stage after its earlier build, test, and coverage work; it stopped on the two rustdoc links. The focused documentation rerun passed after the fix. Per the flight-release schedule, the complete full command was not restarted from zero on commit 030fc05; CI is the final aggregate gate for this PR.

History and release

  • base: 5bafcd9
  • head: 030fc05
  • 31 linear commits, no merge commits
  • every commit has one Signed-off-by trailer
  • no assistant attribution or co-author trailer in the branch messages
  • dev remains ignored and untracked
  • the 0.10.0 tag will be created only after this PR passes and is merged

@jgoppert

Copy link
Copy Markdown
Member Author

Release hardening update at bdd2a981:

  • invalidates recognized prior --emit files and built-in target products before semantic compilation;
  • a failed compile can no longer leave a stale DAE JSON, ProductionCode tree, or .efmu at the named output paths;
  • validates the directory and archive before either is removed, preserving the existing refusal to delete foreign data;
  • adds success-then-failure CLI regressions for dae-json and galec-production.

Focused receipts: both new regressions pass; foreign-product protection passes; packaging transaction tests 13/13 pass; cargo clippy -p rumoca --all-targets --all-features -- -D warnings passes; formatting and diff checks pass.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

MSL Quality Gate Summary

Generated by cargo xtask repo msl pr-comment from CI result artifacts.

Scope MSL OMC Compile Balance Initial Balance Simulation
full v4.1.0 a96aa1a-cmake 267/255 (Δ-278/-277) 255/255 (Δ-277/-277) 255/255 (Δ-277/-277) 149/566 (Δ-21/0)

Deltas compare numerator/denominator against the resolved MSL quality baseline.
Baseline: quality gate v1, commit 08fac54, MSL v4.1.0, OMC OpenModelica 1.27.0~dev.beta.3.
Trace agreement vs baseline: high+near Δ-19, deviation Δ-16.

CI Gate Snapshot

These are the baseline-relative MSL stats checked by CI; IC progress is contextual unless simulation successes regress, and speed metrics are informational only.

Gate area Current (Δ vs baseline)
Structural floors parse 566 (0), flat 487 (-78), DAE 267 (-278), solve 248 (-133)
Balance floors balance 255/255 (-277), init 255/255 (-277)
Runtime gate IC progress 163/566 (-76), sim 149/566 (-21)
Trace floors acceptable 131/566 (-19), no-severe 131/566 (-18), compared 131 (-35)
CI gate details
CI gate metric Current Baseline Δ
Parse floor 566/566 566/566 0
Flatten floor 487/566 565/566 -78
DAE/compile floor 267/566 545/566 -278
Solve-IR floor 248/566 381/566 -133
Balance floor 255/255 532/532 -277
Initial balance floor 255/255 532/532 -277
IC progress 163/566 239/566 -76
Simulation floor 149/566 170/566 -21
Partial models ceiling 13 13 0
Unbalanced models ceiling 0 0 0
Trace acceptable floor 131/566 150/566 -19
Trace no-severe floor 131/566 149/566 -18
Trace compared floor (drop ≤2) 131 166 -35

Package Pass Rates

MSL Package n Ast Flat Dae Solve IC Sim
Overall 566 100% 86% 47% 44% 29% 23%
Per-package pass rates
MSL Package n Ast Flat Dae Solve IC Sim
Blocks 32 100% 91% 41% 41% 28% 25%
Clocked 76 100% 100% 57% 57% 54% 54%
ComplexBlocks 2 100% 100% 50% 50% 50% 50%
Electrical.Analog 65 100% 100% 91% 82% 54% 37%
Electrical.Batteries 8 100% 75% 13% 13% 13% 13%
Electrical.Digital 23 100% 96% 0% 0% 0% 0%
Electrical.Machines 43 100% 93% 84% 84% 14% 9%
Electrical.Polyphase 5 100% 100% 80% 80% 0% 0%
Electrical.PowerConverters 59 100% 100% 86% 68% 66% 42%
Electrical.QuasiStatic.Machines 1 100% 100% 0% 0% 0% 0%
Electrical.QuasiStatic.Polyphase 4 100% 75% 0% 0% 0% 0%
Electrical.QuasiStatic.SinglePhase 6 100% 100% 0% 0% 0% 0%
Electrical.Spice3 14 100% 93% 7% 7% 7% 7%
Fluid 23 100% 0% 0% 0% 0% 0%
Magnetic.FluxTubes 20 100% 100% 45% 45% 5% 5%
Magnetic.FundamentalWave 27 100% 100% 0% 0% 0% 0%
Magnetic.QuasiStatic.FluxTubes 9 100% 100% 0% 0% 0% 0%
Magnetic.QuasiStatic.FundamentalWave 21 100% 19% 10% 0% 0% 0%
Math.FastFourierTransform 2 100% 0% 0% 0% 0% 0%
Math.Nonlinear 1 100% 100% 0% 0% 0% 0%
Math.Random 1 100% 100% 0% 0% 0% 0%
Mechanics.MultiBody 42 100% 93% 33% 33% 14% 10%
Mechanics.Rotational 17 100% 76% 41% 41% 24% 24%
Mechanics.Translational 16 100% 88% 75% 75% 56% 50%
Media 22 100% 32% 0% 0% 0% 0%
Media.Incompressible 1 100% 100% 0% 0% 0% 0%
StateGraph 7 100% 100% 0% 0% 0% 0%
Thermal.FluidHeatFlow 12 100% 100% 83% 83% 67% 58%
Thermal.HeatTransfer 4 100% 100% 75% 75% 50% 50%
Utilities 3 100% 33% 33% 33% 0% 0%
MLS Contract Coverage
MLS Category n Compile Solve IR Balance Sim Phases Errors
ARR 20 75% 35% 35% 20% Flatten:1, Success:7, ToDae:8, Typecheck:4 ED013:1, ED019:6, ED020:1, EF025:1, EL005:3, ET004:4
CLK_SM 64 100% 58% 58% 58% Success:37, ToDae:27 ED008:3, ED009:3, ED010:11, ED018:4, ED019:5, ED020:1
CONN_STRM 335 88% 54% 59% 29% Flatten:12, Instantiate:11, Resolve:2, Success:197, ToDae:98, Typecheck:15 ED001:20, ED008:13, ED010:3, ED013:22, ED019:38, ED020:2, EF004:4, EF020:1, EF023:1, EF024:3, EF025:1, EI007:1, EI012:8, EI027:2, EL005:64, EMSL_TIMEOUT_MODEL_ATTEMPT:3, ER130:2, ET000:14, EX001:1, EX002:10
DECL_TYPE 3 67% 0% 0% 0% Flatten:1, ToDae:2 ED019:2, EF015:1
EQN_ALG_SIM 3 33% 0% 0% 0% Flatten:2, ToDae:1 ED001:1, EF004:1, EF023:1
FUNC 50 78% 0% 0% 0% Flatten:11, ToDae:39 ED019:39, EF016:1, EF019:8, EF025:2
OTHER 91 78% 26% 29% 12% Flatten:20, Success:26, ToDae:45 ED001:24, ED008:14, ED019:7, EF004:1, EF023:15, EF024:4, EL005:10, EX002:3
Trace Accuracy vs OMC
MSL Package n Compared Trace High Near Channel OK No Severe
Blocks 32 25% 25% 25% 0% 100% 25%
Clocked 76 54% 54% 54% 0% 100% 54%
ComplexBlocks 2 50% 50% 50% 0% 100% 50%
Electrical.Analog 65 37% 37% 37% 0% 100% 37%
Electrical.Batteries 8 13% 13% 13% 0% 100% 13%
Electrical.Digital 23 0% 0% 0% 0% 0% 0%
Electrical.Machines 43 9% 9% 9% 0% 100% 9%
Electrical.Polyphase 5 0% 0% 0% 0% 0% 0%
Electrical.PowerConverters 59 42% 42% 42% 0% 100% 42%
Electrical.QuasiStatic.Machines 1 0% 0% 0% 0% 0% 0%
Electrical.QuasiStatic.Polyphase 4 0% 0% 0% 0% 0% 0%
Electrical.QuasiStatic.SinglePhase 6 0% 0% 0% 0% 0% 0%
Electrical.Spice3 14 7% 7% 7% 0% 100% 7%
Fluid 23 0% 0% 0% 0% 0% 0%
Magnetic.FluxTubes 20 5% 5% 5% 0% 100% 5%
Magnetic.FundamentalWave 27 0% 0% 0% 0% 0% 0%
Magnetic.QuasiStatic.FluxTubes 9 0% 0% 0% 0% 0% 0%
Magnetic.QuasiStatic.FundamentalWave 21 0% 0% 0% 0% 0% 0%
Math.FastFourierTransform 2 0% 0% 0% 0% 0% 0%
Math.Nonlinear 1 0% 0% 0% 0% 0% 0%
Math.Random 1 0% 0% 0% 0% 0% 0%
Mechanics.MultiBody 42 10% 10% 10% 0% 100% 10%
Mechanics.Rotational 17 24% 24% 24% 0% 100% 24%
Mechanics.Translational 16 50% 50% 50% 0% 100% 50%
Media 22 0% 0% 0% 0% 0% 0%
Media.Incompressible 1 0% 0% 0% 0% 0% 0%
StateGraph 7 0% 0% 0% 0% 0% 0%
Thermal.FluidHeatFlow 12 58% 58% 58% 0% 100% 58%
Thermal.HeatTransfer 4 50% 50% 50% 0% 100% 50%
Utilities 3 0% 0% 0% 0% 0% 0%
Overall 566 23% 23% 23% 0% 100% 23%

Speed vs OMC

Compilation and simulation are reported separately for the 131 trace-agreeing models with valid timings on both tools. Speedup = OMC / Rumoca (>1 means rumoca faster).

Aggregate speed summary
Phase Models Rumoca total (s) OMC total (s) Throughput speedup (×) Median per-model speedup (×)
Total 131 115.6 612.6 5.30 26.87
Compilation 131 33.8 530.5 15.69 35.19
Simulation 131 81.8 82.1 1.00 3.00
Speed breakdown
Where rumoca's time goes
Scalar eqns Models front→DAE Solve-IR + JIT integration
1–9 33 0.063 0.001 0.0008
10–24 16 0.071 0.002 0.0028
25–49 11 0.080 0.011 0.0094
50–99 32 0.096 0.020 0.0442
100–249 27 0.213 0.066 0.1570
250+ 12 0.529 0.285 1.1852
Rumoca stage Total seconds Share of rumoca total
front→DAE 22.1 19%
Solve-IR + JIT 11.7 10%
integration 81.8 71%
Speed by system size
Table 1 — Total (model → simulated results)

Rumoca = front-end compile + Solve-IR/JIT build + integration; OMC = timeTotal.

Scalar eqns Models Rumoca (s) OMC (s) Speedup (×)
1–9 33 0.0656 3.3013 49.52
10–24 16 0.0912 3.4491 38.82
25–49 11 0.0951 3.4952 36.11
50–99 32 0.1860 4.0362 22.39
100–249 27 0.4716 5.0298 9.83
250+ 12 2.8132 6.9210 3.25

Per-model speedup: min 0.34, median 26.87, max 85.01.

Table 2 — Compilation (build-to-runnable)

Rumoca = front-end compile + Solve-IR/JIT build; OMC = timeTotal - timeSimulation.

Scalar eqns Models Rumoca (s) OMC (s) Speedup (×)
1–9 33 0.0638 3.2853 50.93
10–24 16 0.0774 3.4007 43.91
25–49 11 0.0857 3.4415 40.73
50–99 32 0.1158 3.7758 33.33
100–249 27 0.2797 4.7085 16.96
250+ 12 0.8765 6.3189 7.49

Per-model speedup: min 1.85, median 35.19, max 86.17.

Table 3 — Simulation (integration only)

Rumoca = sim_run_seconds; OMC = timeSimulation.

Scalar eqns Models Rumoca (s) OMC (s) Speedup (×)
1–9 33 0.0008 0.0149 16.81
10–24 16 0.0028 0.0228 8.30
25–49 11 0.0094 0.0363 3.84
50–99 32 0.0442 0.0677 2.16
100–249 27 0.1570 0.1369 0.92
250+ 12 1.1852 0.2079 0.34

Per-model speedup: min 0.01, median 3.00, max 38.35.

Top 10 slowest models
Top 10 slowest rumoca compilation models

Trace-agreeing models ranked by rumoca time.

Model Scalar eqns Rumoca compile (s) OMC compile (s) Speedup (×)
Modelica.Mechanics.MultiBody.Examples.Elementary.SpringWithMass 968 3.7993 7.0467 1.85
Modelica.Mechanics.MultiBody.Examples.Elementary.InitSpringConstant 1275 2.3718 7.7246 3.26
Modelica.Electrical.Machines.Examples.DCMachines.DCPM_withLosses 393 2.2384 5.4249 2.42
Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum 934 1.7291 7.0612 4.08
Modelica.Mechanics.MultiBody.Examples.Elementary.PointGravityWithPointMasses 1113 1.4887 7.0548 4.74
Modelica.Thermal.HeatTransfer.Examples.TwoMasses 20 0.9347 3.3559 3.59
Modelica.Electrical.PowerConverters.Examples.ACDC.RectifierCenterTap2mPulse.ThyristorCenterTap2mPulse_R 450 0.8843 6.3076 7.13
Modelica.Electrical.Machines.Examples.DCMachines.DCPM_Cooling 272 0.8688 6.8094 7.84
Modelica.Electrical.PowerConverters.Examples.ACDC.RectifierCenterTapmPulse.ThyristorCenterTapmPulse_RLV 346 0.7089 5.6833 8.02
Modelica.Electrical.PowerConverters.Examples.ACDC.RectifierCenterTapmPulse.ThyristorCenterTapmPulse_RL 340 0.6857 5.4005 7.88
Top 10 slowest rumoca simulation models

Trace-agreeing models ranked by rumoca time.

Model Scalar eqns Rumoca simulation (s) OMC simulation (s) Speedup (×)
Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum 934 19.2480 0.1071 0.01
Modelica.Electrical.Machines.Examples.DCMachines.DCPM_Cooling 272 16.9343 42.1419 2.49
Modelica.Mechanics.MultiBody.Examples.Elementary.SpringWithMass 968 14.0891 0.1014 0.01
Modelica.Electrical.PowerConverters.Examples.ACDC.Rectifier1Pulse.Thyristor1Pulse_R_Characteristic 104 5.8818 4.6002 0.78
Modelica.Mechanics.MultiBody.Examples.Elementary.PointGravityWithPointMasses 1113 3.8317 0.1452 0.04
Modelica.Electrical.Machines.Examples.DCMachines.DCPM_withLosses 393 2.0300 5.4268 2.67
Modelica.Mechanics.MultiBody.Examples.Elementary.InitSpringConstant 1275 1.4853 0.1802 0.12
Modelica.Electrical.Batteries.Examples.SuperCapDischargeCharge 77 1.1367 3.1046 2.73
Modelica.Blocks.Examples.DemonstrateSignalExtrema 18 1.0347 2.3822 2.30
Modelica.Electrical.Machines.Examples.DCMachines.DCPM_Start 200 0.9710 2.4841 2.56

Additional Artifacts

  • msl_compatibility_report.md

Establish the repository specifications, contract fixtures, CI workflows, documentation, and developer commands that govern strict compiler phase ownership and reproducible verification.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Introduce the common compiler primitives, reference representations, syntax IR, parsing behavior, and AST evaluation needed by the typed lowering pipeline.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Resolve scopes and stable identities, validate semantic references, and type-check expressions, components, and functions before structural lowering.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Expand component arrays, apply type and connection overrides, and represent instantiated models with explicit flat clocks, expressions, and equations.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Compute phase-constant values and lower function bodies, component connections, dimensions, and injected constants into deterministic flat model structures.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Lower equations, algorithms, event relations, aliases, and post-processing passes into the complete checked flat model consumed by structural analysis.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Build incidence ownership, eliminate structural aliases, scalarize solvable domains, and establish same-tick and causal-definition invariants.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Apply index reduction, differentiation, initialization planning, event ownership, and equation reconstruction while preserving structural proofs.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Add the valid DAE aggregate, expression vocabulary, discrete state, event metadata, and validation surfaces required by execution and Solve lowering.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Derive structural dependencies, expression ownership, function result shapes, and construction inventories before emitting checked DAE programs.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Build equations, algorithms, function bodies, variables, clocks, discrete values, and model-event transactions as validated DAE-owned products.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Convert typed equations, bindings, conditions, algorithms, start values, runtime precomputation, and scalar metadata into executable DAE form.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Remove the superseded GALEC generator surface and introduce the grammar and parsing ownership used by the checked production-code pipeline.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Execute DAE expressions, statements, numerical programs, projections, and initialization values through validated runtime access paths.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Introduce the Solve model, typed scalar and tensor operations, structural dependency patterns, refresh ownership, and wire validation required by solver backends.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Translate derivative right-hand sides, array values, compact tensor operations, scalar expressions, and continuous assignments into checked Solve programs.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Preserve typed function calls, statements, clocks, event actions, discrete updates, and call-scoped effects in the executable Solve representation.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Complete Solve emission, source mapping, initialization projections, assignment certification, diagnostics, and lowering-specific regression coverage.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Construct solver memory layouts, automatic differentiation, stencil evaluation, initial values, implicit equations, runtime assignments, and model-level validation.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Prepare and execute typed Solve programs, scalarize compute blocks, derive causal refresh plans, and expose checked runtime evaluation interfaces.

Signed-off-by: James Goppert <james.goppert@gmail.com>
James Goppert and others added 29 commits August 26, 2026 13:00
Deleting SolveRoundingMode (a9f1fe0) changed a user-visible serde dump:
--emit solve-json no longer carries "rounding". On
Vehicles.Rdd2.NavigationEstimator the dump goes from 2,347,860 bytes
with 1704 "rounding": "nearest_ties_to_even" entries to 2,234,140 with
none, and deleting exactly those entries from the old dump reproduces
the new one byte for byte, so the change is precisely that and nothing
else. No consumer in this repository or its docs reads the field, and
--target output is byte-identical, but an IR dump is a surface a
downstream tool can be reading, so state it rather than let it surprise
someone.

Record the schema-bundle sharing in the same entry: galec-production no
longer holds a schemas/ directory, which a downstream that vendored that
path would notice, while the containers it emits are unchanged.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
The AssetBundle doc pointed at TargetSource::asset_files; the type is
TargetBundle, so rustdoc could not resolve the link and the
Documentation job would have carried the warning.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
A shared-call node is built from the memo the earlier nodes left, so a
nested call in a later node's own argument tree could take an earlier
node's result temporary. Nothing recorded that. The lowerer was told which
calls were shared only after every node had been built, so a node under
construction recorded nothing at all, and what it did record was keyed by
the call owner, which collapses when two nodes materialize one owner under
two different activations. No node-to-node edge reached the scheduler, the
greedy worklist was free to emit the reader first, and it did: the emitted
DoStep read a result temporary at a point where nothing had written it. The
strict profile reports that as a definite-assignment defect, and where the
slot happened to hold the previous tick's value the numbers were merely
wrong rather than obviously so.

Key the record by the memo entry instead of by the call owner, and write it
as each node completes rather than after all of them. A node's entries are
the difference between the memo it inherited and the memo it leaves, so
nothing inside the materialization has to cooperate and no entry a nested
argument lowering produced can be missed. Everything that takes an entry
now records the node that wrote THAT entry: a consumer group as before, and
now a node under construction as well, which is the edge that was missing.
On top of that each node declares a read of every node whose memo it
started from, so an inherited entry it takes without a recorded hit is
still ordered. Where those edges close a cycle the schedule fails to order
and re-lowers without sharing, which is the existing fail-safe.

Three further findings from the same review:

A node trial that fails to lower now declines the sharing instead of
aborting the compile. Sharing is never a reason to refuse a model.

A discrete-value owner's `Always` branch is no longer scanned when the
owner also has a conditional branch. `compose_discrete_value_branches`
emits it as the `else_body` of the when-chain in that case, so it runs on
strictly fewer ticks than the domain does and a call it names must not be
hoisted anywhere unguarded.

The shared-call plan moves out of `clocked_assignments` and the memo it
carries moves out of `lower`, which puts every file back under the
SPEC_0021 size threshold. Both plan steps now take one context struct, so
the argument-count waiver is gone rather than documented.

Fixture 17 is the reproducer, built on the shape that fails: `outr(ia)`
inside an `if`, `ia` from a call shared at domain entry, and `alt(v)`
reading a `v` the `if` writes. It pins the hand-computed closed form, which
shares no code with either executing leg, and compiles the emitted unit
under the strict profile. The trap needs a node whose arguments are
rewritten later in the same tick, which `simulate_dae` refuses as a
same-tick discrete loop, so this one fixture is gated on the two executing
legs as `LimitSmoke` is.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A call written once in the source was re-evaluated at every lowered
definition that used it: the RDD2 Bezier trajectory planner performed one
tuple call sixteen times per tick. Repeated calls are now materialized once
into a scheduled node, under the activation guard or conditional branch they
already ran under, so the call executes on exactly the ticks it executed on
before. Nodes declare reads of the nodes whose memo entries they take, so a
node is ordered after the node whose temporary it reads.

Planner: 247,287 to 30,154 executed Cortex-M7 instructions, 6,648 to 3,995
generated C lines, flash 30,050 to 14,860 bytes, with trajectory outputs
identical to the previous compiler.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
The dae-modelica emitter rendered string literals with the tojson filter.
JSON spells BEL and VT as backslash-u escapes, and the Modelica grammar has
no backslash-u escape at all, so a model whose description or unit carried
either character was emitted in a form the parser that produced it could not
read back. The correct escaper already existed and is what every other string
rendering path uses; the emitter now routes through it as a modelica_string
filter.

Covered by an emit and re-parse round trip over a description carrying bell,
vertical tab and tab.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A GALEC function leaves its outputs in its own context region and the
caller copies each one out. On the Bezier planner that read-back was 74
of the 192 kernel copy calls a tick executes, moving values that already
had a home.

The C emission now places such an output at the caller's slot. The
callee's region holds no storage for it; the callee's entry declares a
typed pointer at the destination, the same shape an arena slot already
uses, and the caller emits no read-back. GALEC keeps its value semantics
throughout: no out parameter is invented, no statement changes, and the
`.alg` prints the same bytes.

The permission is minted in one expression and carries the whole proof:
one call site so the address is unique, a destination that is a plain
member of the calling function's own region with the output's exact
shape, no actual that can reach it, and a syntactic witness that the
callee writes every element. Everything else declines.

Planner, one dostep on Cortex-M7 -Os: 30,154 executed instructions to
28,530; 192 kernel copy calls to 146; scratch 2,564 B to 2,468 B; the
floating-point mix and the driver's printed line unchanged.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The plan decides emitted storage, so its sets and maps now iterate in one
order on every run; the generated planner and estimator are byte-identical
across repeated runs. Ordering also satisfies the string-hashing gate,
which refuses a derived Hash over string-shaped fields.

The three-leg harness gains the fixture the placement's whole risk lives
in. `stage` calls two identical blends: one assigns its result to a slot
no operand names, the other assigns it back over the argument it read.
Both bodies read element 1 on every iteration after writing element 1 on
the first, so a placement taken on the aliasing call would feed elements
2 and 3 a value the model never computes. The generated C, the GALEC
oracle and the Modelica reference agree on every tick, and the emitted
unit is checked for the alias declaration on one call and the read-back
on the other.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A callee writes its placed output through a pointer at a moment neither
the arm overlay, nor the value arena, nor the marshalling rewrite can
see, so a destination has to be a slot none of the three claimed. The
first attempt forced that by withholding the destination from all three.
That inverted the trade on the artifact that flies: the RDD2 estimator's
scratch grew 20,572 B to 23,044 B, buying copies with storage those
mechanisms were saving.

Which slots own their storage outright is an output of a projection, not
an input to one, so the projection now runs twice where anything is
placed: the first pass places nothing and the plan reads its answer, the
second emits against it. Nothing is withheld from anything, so every
input the callers' own layout is derived from is identical between the
passes; that the chosen slot is still a plain member is checked, not
assumed, and a mismatch fails the projection.

The generated estimator is byte-identical to what it was before this
mechanism existed: every candidate there is a slot the arena was already
sharing, and all of them decline. The planner keeps its win at
28,614 executed instructions from 30,154, 146 kernel copy calls a tick
from 192, and scratch 2,492 B from 2,564 B.

A function whose header says outputs may be placed now only says it when
one is, so a unit with no placement carries no text about the mechanism.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Each shape the witness admits performs exactly one write to the output, so
a statement that writes it twice is doing something the coverage argument
does not cover: a whole assignment beside a subscripted one, or a second
target of the same multi-assignment. Both are now refused rather than
reasoned about.

Two references also differ when one is reached through a typed pointer and
the other is not, which is what `same_reference` means by identical
storage spelling; the arena flag joins the two it already compared.

Neither changes the planner or the estimator, which is the point: the
witness was already tight on both, and this closes the shapes a future
model could bring.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The middle link of a placement chain names its output nowhere. Its one
write was a read-back the inner placement dropped, and every read belongs
to the caller, so the pointer it declared was a line no path reached: two
of them in the Bezier planner, each an `unused variable` error under the
strict assurance profile and, either way, code a reviewer has to account
for and cannot.

The emission now asks whether the body still spells the output before
declaring the pointer, using the same rule the template prints by: a
result whose read-back is dropped is not a mention. The placement itself
is unaffected, because a link that names its output nowhere contributes
nothing but the pass-through it already performs.

Planner: 4,009 C lines from 4,019, and the emitted unit has no unused
declaration left. Every other measure is unchanged.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The gate decided deadness by counting bare identifiers over the whole
workspace and reporting only names whose total count was exactly one. Any
`pub` item whose bare name collided with any other identifier anywhere was
therefore exempt, and the hole was live: `PhaseResult::is_success` was excused
by `InstantiationOutcome::is_success`, `OptModel::set_parameter_value` by the
sim one, `Rk45Session::set_inputs` by `SimulationSession::set_inputs`, and
`rumoca_phase_parse::parse_file` by roughly fifteen `syn::parse_file` calls in
the test suites. None of the five had a caller.

Resolve instead by declaration identity, `(crate, module path, name)`: two
crates that both name something `helpers` must not vouch for each other. A
name is recorded from the constructs that can actually reach a declaration
(paths, `use` trees, method calls, macro token streams) rather than from a
bare ident visit, so a struct field named like its accessor no longer keeps
the accessor alive. `spec/` and `docs/` leave the usage corpus, because a
mnemonic in a specification table is prose about the code and not a caller of
it, and an item an FFI attribute hands to a non-Rust caller is exempt by
construction since no Rust reference can resolve to it.

Disposing of what the audit finds at this commit: of eighteen reported items,
sixteen are deleted and two stay, recorded as contracts SPEC_0041 names as
crate-owned. The exemption dies with the row that grants it. Two further
deletions came out of that work by hand: `Session::all_class_names_cached`,
whose only caller was the deleted snapshot wrapper, and
`PhaseResult::needs_inner`, which the resolver still misses because a local
binding of that name is written in the same crate. Codegen is byte-identical.

MLS N/A: repository verification gate.

Signed-off-by: James Goppert <james.goppert@gmail.com>
SPEC_0008 states that a warning MUST NOT be minted in an `E` range, with
`ES001`/`ES002` recorded as the only sanctioned drift, and AST constant
evaluation was minting `ET006` and `ET007` through `emit_warning`. The gate
missed both twice over: it recognized two call spellings, `structural_warning`
and `CommonDiagnostic::warning`, and it read only the files a hand-kept
registry list named, which does not name this phase. A warning minted in a
phase nobody remembered to list is exactly the violation the property exists
to catch, so the list was the wrong instrument.

Rename the two to `WT006`/`WT007`, give `WT0xx` its own row in the SPEC_0008
range table alongside the `WT003` variability warning already shipping there,
and update the three typecheck assertions that name them.

Then widen the gate so it cannot go blind the same way again:

* the mnemonic is read from any call whose name ends in `warning(`, so a
  helper the workspace adds tomorrow is recognized the moment it is named for
  what it does, and the two narrow readers collapse into one;
* the corpus is every crate's `src` tree rather than a list of files, read
  through the totality scan's production filter so a comment about a mint and
  a `#[cfg(test)]` fixture that performs one answer nothing about shipped
  code. That filter gains a policy for keeping string-literal text, with the
  punctuation that shapes an item removed from it so a brace in a format
  string still cannot move the item scan;
* the scan is held to still see: canaries assert that each recognized
  spelling is still read, and that the workspace scan still finds mnemonics
  the shipped tree really mints. A text scan that quietly matches nothing
  reports a clean workspace while a violation ships, which is worse than
  having no scan at all;
* a mnemonic-named constant must hold the mnemonic it names, since the scan
  reads the name and the compiler ships the value;
* the double-mint check runs over every registry rather than instantiate
  alone, with `EI001`'s two presentation variants recorded as the one
  deliberate sharing.

Restoring `ET006`/`ET007` makes the new gate fail and names the file. Codegen
is byte-identical.

MLS N/A: repository verification gate.

Signed-off-by: James Goppert <james.goppert@gmail.com>
`uses_scratch` decides whether a body declares its region alias, and it
was set while PROJECTING a reference rather than while printing one. A
call target whose result the placement delivers directly is still
projected, because the checked Algorithm Code still spells the assignment
and `model.alg` still prints it, but the read-back that named it is gone.
In a thin wrapper that read-back is the only emitted use of the region, so
the alias became a declaration nothing reads: an `unused variable` error
under the profile every GALEC C product is preflighted with, and an
unreachable declaration a structural-coverage argument has to account for
either way.

The projection now asks the same question about a call target that the
emission already asks about a callee's own output, through one shared
predicate. The RDD2 artifacts never showed it because their regions all
hold other live members.

The wrapper shape is a permanent three-leg fixture now, and it is
load-bearing: with the suppression mutated out it fails on the harness's
own strict compile.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The liveness walk answered "this body names the output" for every branch
guarded by a signal check, for every output. That was written as the safe
direction and it was, until the walk started deciding whether to DECLARE a
destination pointer: a wrong "named" now emits a declaration no path
reaches, which the assurance profile reports as an unused variable.

A signal check tests signals, and its only expression is the optional
`or expr` fallback, so the walk reads that and neither guess is needed.
Nothing in the GALEC phase constructs a signal check today, so no emitted
artifact moves.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A callee that reads its own output before writing it reads persistent
storage either way, but not the SAME storage: without a placement it reads
what its own previous activation left there, and with one it reads the
caller's destination, which the caller may have written in between. The
theorem's value-preservation argument depends on that never happening and
never said so.

The front end already refuses such a body with ED019, so no artifact
moves. The witness checks it anyway: a proof that leans on another phase's
diagnostic without naming it stops holding the moment that phase changes,
and silently.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The destination pointer adopts its destination's final extents, which is
sound for the one difference the bound equalization can introduce: a wider
LEADING extent moves no element, because it is not a stride. A different
rank or element type is different address arithmetic, and the pointer
would have carried it silently into the artifact.

Nothing emits that today. A future pass that did now meets a diagnostic
instead, beside the membership check that was already there.

Signed-off-by: James Goppert <james.goppert@gmail.com>
An adversarial review found the refusal unreachable by mutation and
argued it redundant: a descendant's formal can alias an ancestor slot
only if that slot was passed at the very call whose result lands there,
which the by-name operand check already refuses. The argument is
believable, and it is also an inductive claim about everything a formal
can alias across the whole call graph, which is the reasoning this
calling convention exists to avoid having to get right.

The refusal stays, and what it costs is written down beside it: 20
read-back copies a tick on the Bezier planner, about 0.7 percent of the
step. Retire it against evidence, not against the argument.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Real comparisons signal, so a guard is not a pure test: it writes
ErrorSignalStatus. Two measured optimizations are blocked on the claim that
evaluating such a guard N times and once produce the same word, and nobody had
established it.

State it as SPEC_0034 GAL-040 rather than assume it. The word is cleared exactly
once on entry to each method, every other emitted write is a bitwise OR of a
constant mask, and nothing reads it, so inside one method the final word depends
on the SET of raising evaluations, never on their count or order. The rule also
names what breaks it: a signal check catches, clearing the bits it tests, so the
monotone argument holds only between a method's reset and its first check, and
an optimization may never make a raise reachable that the source form did not.

Enforce it two ways, per D1 and D2 of the architecture decision record.
rumoca_ir_galec::signal_effect classifies every expression, condition and
statement over exhaustive matches, so a construct added to the AST cannot be
silently treated as safe; RepeatableSignalEffect is a branded token whose field
is private to that module and whose only constructor is a proof. The
classification is a claim about emitted C, which no Rust type constrains, so
galec_c_error_signal.rs scans both the template source and rendered C and
accepts only a reset, an OR with a decimal-literal mask, a declaration, or an
address handed to a helper the same scan covers. The template scan is the one
that catches a kernel no fixture exercises: injecting `*status = UINT32_C(4)`
into the unrendered compare_lt kernel fails it while the rendered scan stays
green.

SPEC_0034 stood at 2,491 of its 2,500-word budget, so the new rule needs room.
Move the two lookup tables that are catalog material by SPEC_0000 section 3, the
variable-classification table and the checked-construction-scope table, into
SPEC_0042 section 5 alongside the GAL-040 rationale, and cite them as normative
by reference exactly as the language traps already are. SPEC_0034 ends at 2,472
words, below where it started.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
An array actual that is not a whole-aggregate copy is staged element by
element, and when the element value came from a conditional every element
carries the same test. On Planning.Bezier.WaypointTrajectoryPlanner twelve such
nests evaluated the plan-acceptance guard 269 times per tick, at nineteen
Cortex-M7 instructions each; the guard mentions no iterator, so 257 of those
evaluations recomputed bits that could not have changed.

fuse_guarded_tensor_loop does not reach this: it PAIRS an already-hoisted
prefix guard with a matching body guard, and here nothing was hoisted because
the conditional's arms are entirely index-dependent. Bind the condition to a
Boolean ahead of the nest instead, and let the nest test the Boolean.

The rewrite is gated on a token, not a predicate. HoistableGuard::prove
discharges four obligations before it will mint one: the condition is an
expression and not a signal check, it is worth binding, it mentions no iterator
and no name the body assigns, and it holds no call. It then requires GAL-040's
RepeatableSignalEffect on top. The nest is refused outright if any body
statement classifies Consume or Opaque, and if any axis could run zero times:
hoisting out of a possibly-empty loop would raise on an execution that raised
nothing, which is the one clause that makes the status word identical rather
than merely equivalent. The token owns the expression it was minted against and
is consumed by the rewrite, so there is no second look and no assertion.

Measured on the planner from mm-base, driver output byte-identical: guard
evaluations 322 to 62 per tick, and 269 to 0 inside staging nests; executed
instructions on the guard lines 6,179 to 1,272, and 43,927 against 48,076 over
the whole model translation unit. Flash grows 392 bytes.
Vehicles.Rdd2.NavigationEstimator is byte-identical: its NaN guards are
index-dependent, so the rewrite correctly declines there.

Fixture 18 is the evidence for the contract, not for the speed. Its staged
guard is `zeroGain / zeroGain > 1.0`, which raises NAN whenever it runs, so the
per-tick ErrorSignalStatus word records whether the guard ran at all that tick.
The nest sits inside `if ticks > 2.5`, so the pinned word [0,0,4,4,4] would
catch a guard lifted out of THAT. Ablating the rewrite and re-running the
fixture leaves every status and value assertion passing and fails only the
shape assertion, which is the measurement the contract predicts.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
Argument-staging loop nests re-evaluated a loop-invariant guard once per
element: the Bezier trajectory planner evaluated its plan-acceptance guard
269 times per tick inside staging nests alone. Binding the guard to a Boolean
ahead of the nest is only sound if repeating a guard is unobservable, which
nobody had established, so the accumulation contract is now written down as
SPEC_0034 GAL-040 and enforced by a branded permission plus a scan of both
the kernel template and the rendered C.

Guard evaluations per tick fall from 322 to 62, and the instructions those
lines execute from 6,179 to 1,272, with outputs identical.
Two workspace lint budgets, both tripped by the last two changes: the
leading-extent comparison nested a conditional inside a closure inside a
call, and routing an assignment's target through the call-target rule
pushed the statement projection past its line budget. Both are now named
functions saying what they decide. No emitted artifact moves.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A callee wrote its array output into its own scratch region and the caller
copied the result out, so every call paid a full read-back traversal. A
callee's output slot may now live in a slot the caller already owns, reached
through a typed pointer the callee declares, and the read-back is not emitted
at all. The C signature is unchanged and no out parameter is introduced: out
pointers were removed here once before because a callee writing a caller
object during its body corrupts an operand that aliases it, and that reason
still stands.

Placement is minted only by a proof discharging single call site, caller
ownership, disjointness from every operand, a total write, no self read,
injectivity, reentrant addressing, and chain resolution. Declining costs one
copy and nothing else.

Planner: 192 to 146 kernel copy calls a tick, State 4,360 to 4,288 bytes.
Controller: overlay groups 6 to 5, State 1,632 to 1,588 bytes. Outputs
identical on both, and the flight estimator is byte-identical.

Signed-off-by: James Goppert <james.goppert@gmail.com>
The compiler had no way to say how much of a model's structure the emitted C
should keep. `GalecOptions` carried one field, every caller built it with
`::default()`, and `lower_algorithm_code` hardcoded that default, so no CLI
flag could reach the projection even in principle.

Add the two axes, and keep them two, because they differ in kind and not in
degree. Inlining changes CALL structure and is information-preserving: an
inlined contraction is still a contraction, its shape and index sets arrive
intact, and no floating-point operation is reordered. Scalarization changes
DATA structure and is information-destroying: index sets, symmetry, bandedness
and tensor identity are gone the moment a contraction becomes N statements, and
only a recognizer guessing at emitted shapes can pretend to get them back.

So `--inline-policy none|annotated|cost-model|all` defaults to `cost-model` and
never taints the artifact, while `--scalarize-policy never|cost-model|all`
defaults to `never` and taints at every other setting. A single dial would have
made the one combination this compiler is uniquely able to emit, inlined code
that keeps its tensors, unaskable. `--emission-policy reviewable|balanced|flat`
survives as shorthand over the pair and is never the only way to name a point.

Expansion is not implemented. Asking for it is refused by name rather than
silently ignored: a no-op there would emit structured code under a header
claiming a tradeoff the compiler never made.

`--inline-policy none --scalarize-policy never` is today's compiler exactly.
Verified on Planning.Bezier.WaypointTrajectoryPlanner,
Vehicles.Rdd2.GuidanceController (embedded-c-galec) and
Vehicles.Rdd2.NavigationEstimator (galec-production): all 65 generated files
byte-identical, masking only the container UUIDs, the generation timestamp and
the SHA-1s derived from them, which differ run to run at HEAD as well. At this
commit the default differs from that baseline by the disclosure header and
nothing else.

Every emitted block that collapsed or expanded anything states both axes and
whether it is still eligible for the certification path. Nothing is emitted
when nothing was collapsed or expanded: the absence of that notice IS the
statement that the emitted functions are the model's functions and the emitted
tensor operations are the model's tensor operations.

Two shapes fell out of the threading rather than being sought. The five facts
every block-lowering step reads (the DAE view, its causal definitions, the
classified variables, the `pre()` names, and now the policy) became
`BlockLowering`, because a step holding four of them cannot build an
`ExpressionLowerer` and every one of these steps builds one; four functions
that would otherwise have crossed the argument limit now take fewer arguments
than before. `dependent_assignment` moved next to its only caller, and the
clap-facing emission selectors moved out of `cli.rs`, both to stay under the
SPEC_0021 file-size threshold.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
An `if`/`elseif` chain in a clocked algorithm reaches the checked DAE twice
over: as one guarded definition per branch, and as one n-ary conditional per
value that reads what the chain assigned. The guarded shared-call node
already evaluated a repeated call once for the definitions, but the
conditional's branch evaluated it again, because a memo entry is admitted
only where the guard facts it was emitted under hold again and a guarded node
states its guard as an emitted `if` rather than as a fact.

Where the node's guard IS one branch's selection, the fact does exist and it
is exactly that branch. The DAE builds a branch guard and a conditional's
branch condition from the identical expression identities, so the proof is a
literal-set comparison and not a decision procedure: the guard has to be a
conjunction of predicates over those expressions plus this domain's own
clock, and its positive and negative literals have to be exactly `c_k` and
`not c_0 .. not c_(k-1)`. The node's entries are then published a second time
under that branch's activation key, and `dominates` admits them inside the
branch and nowhere else. Each alias is attributed to the node that wrote the
temporaries, so a consumer taking one still declares the schedule edge that
keeps the node ahead of it.

Declining is free and three cases take it: an entry that already carries a
guard fact of its own, a branch whose own conditions evaluate the same call
owner, since the branch key is pushed for the condition too and the condition
runs before its branch is selected, and a key some earlier node already
published.

`Ekf2.DifferentialStep` emitted its one source `predictStep` tuple call at
three call sites and now emits one: 64,282 to 26,139 executed Cortex-M7
instructions per predict dostep at -Os on the corrected instrument, FLOP
equivalent 6,552 to 2,184, block state unchanged at 29,048 bytes. Outputs are
bit-identical over a differential driver covering all four branches of the
block. The flight estimator and the Bezier planner emit byte-identical C.

Signed-off-by: James Goppert <james.goppert@gmail.com>
Fixture 22 is the shape the branch alias opens: an `if`/`elseif` chain whose
three definitions share one guarded node, and one value that reads what the
chain assigned. Only one value reads it, so nothing in that branch looks
repeated and fixture 16's conditional-branch node does not fire; what removes
the second evaluation is the proof that the node's guard IS this branch's
selection.

The shared function raises NAN on every call, so the compared
`ErrorSignalStatus` word is a per-tick record of the ticks it ran on: the
branch is unselected on ticks 1 and 2, and temporaries taken where the guard
had not held would either raise NAN there or hand back a stale slot. All
three legs agree tick for tick, the emitted unit compiles under the strict
profile, and the DoStep is pinned to one call site standing inside its guard.
Without the compiler change that count is two.

Signed-off-by: James Goppert <james.goppert@gmail.com>
A tuple call written once was still evaluated three times in the Ekf2
covariance block: twice through guarded state definitions and once more inside
a conditional branch that the same guard selects. The guard and the branch
condition are the same expression identities, so a node emitted under the
guard runs on exactly the ticks the branch is reached, and the branch may be
served from it. The alias is published only when the guard normalises to the
branch's own selection, and declines otherwise.

Ekf2 predict step: three call sites to one, 64,282 to 26,139 executed
Cortex-M7 instructions, flop-equivalent exactly one third, outputs bit
identical over all four branch selections. The flight estimator and the
trajectory planner emit byte-identical C.
`annotation(Inline = true)`, `Inline = false` and `LateInline = true` are MLS
§18.3 and are the spec-grounded way a model author states, per function,
whether a call should disappear. The parser read them into an untyped
`Vec<Expression>` and every later phase dropped them; a repository-wide search
for `"Inline"` in `crates/*/src/` returned nothing.

Carry the request from the declaration that wrote it: `InlineAnnotation` on
`rumoca_core::Function`, filled where `derivative` annotations are already
read, through `FunctionSignature::with_inline` onto the checked DAE function,
and out through `FunctionView::inline`. Reading it late is not an option: by
the time a backend decides, the annotation expressions are gone, and a compiler
that has to guess the author's intent is a compiler that ignores it.

Substituting a body is then a decision with an order of authority.
`Inline = false` refuses at every setting, because a refusal a compiler flag
can override is not a refusal. Legality refuses next: a callee already being
expanded at this site would expand forever, and a callee returning a tensor
would be lowered one element at a time, which is scalarization by another route
and is what the scalarization axis declines. Only then does `--inline-policy`
decide, from `annotated` upward.

The tensor refusal is measured, not assumed. Substituting aggregate results
took the RDD2 trajectory planner's emitted translation unit from 4,033 lines to
136,464 and its compile from one second to thirty-one, because each element of
each result re-lowered the callee's defining expression. It is also why
inlining does not yet close the working-memory gap against a flat generator:
the chain holding the flight estimator's residual scratch live is a chain of
matrix-returning callees, and every one of them is refused here. Collapsing
those needs an inliner that splices the callee's statements, loop nests
included, which keeps the tensors AND removes the boundary.

Declining is free, so it is free at every step. The substitution runs on a
clone of the lowerer: a body that cannot be substituted leaves no temporary, no
cached value, no emitted statement and no recorded call behind, and the call
the ABI can always represent is emitted instead.

Traceability survives the transform rather than being traded for it. The
selected output is lowered through its own function-value definition instead of
through the bare right-hand side, so the emitted statement carries the CALLEE's
span rather than the call site's: without this, inlining `MathUtilities.clip`
removed `clip.mo` from the guidance controller's trace legend entirely. The
local that holds the value is named after the whole chain of callees it was
substituted through, the callee value it realizes, and the call it replaced, so
a temporary that used to read `rumoca.tmp.<ns>.<callee>.<result>.call<n>` does
not become a counter.

`--inline-policy none --scalarize-policy never` stays byte-identical to a build
with no dial: all 65 generated files across the two flight controllers and the
flight estimator, under the same mask for container UUIDs and timestamps.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
This is the cost model's first rule and the only one that needs no tuning. A
callee with one call site has nothing to duplicate: the body moves rather than
being copied, so there is no flash to trade, and what it removes is
unconditional, namely the argument staging and result read-back at that
boundary and one edge of the call chain that the storage overlay refuses to let
a caller and a callee share. The rule is not a heuristic; it is the statement
that a boundary crossed once buys nothing.

The census is counted once per projection over the expression arena and keyed
by the construction-issued call OWNER, never by the expression: one source call
to a four-result function reaches lowering as many scalar projections of one
owner, and counting projections would make every multi-result call look
duplicated when it is not. It covers the whole DAE rather than one block, which
is the conservative direction.

Two corrections to the surface land with it.

The flag defaults are now `--inline-policy none` and `--scalarize-policy
never`, so passing no flag emits what a compiler with no dial emits. The cost
model acts under `--emission-policy balanced` or an explicit `--inline-policy
cost-model`, never silently: a structural decision is taken deliberately or not
at all. Verified again on the two flight controllers and the flight estimator:
all 65 generated files byte-identical to a build without the dial, with no
flag, with `--emission-policy reviewable`, and with the two axes named
explicitly.

An eFMI container target now refuses a policy that would reshape it. A
container ships AlgorithmCode as the reviewable semantic reference and
ProductionCode as its optimized C, and this compiler renders both from one
projection, so a policy that collapses call structure would collapse it in the
reference too. Refusing names what is being protected; silently dropping the
flag would hand back an artifact that does not match the request. The
transform's end-state home is the GALEC-to-Solve refinement, where the two
representations can legitimately diverge, and both the refusal and the module
header record that as relocation debt rather than a settled location.

Every emitted block that collapsed anything now states its budget in the units
the settings act on: emitted statements, emitted functions, and working memory
against the floor any sound overlay of the same regions could reach.

MEASURED, and the headline is a negative result worth having. On the trajectory
planner the rule fires nowhere at all (58 emitted functions, 48,039 executed
instructions per tick, 15,012 B of ARM -Os .text, and 2,492 B of scratch, all
unchanged); on the guidance controller and the flight estimator it collapses
four and eight functions and moves working memory by ZERO bytes (604 B and
20,572 B, identical), while the estimator's .text GROWS 820 bytes.

The reason is structural, not a tuning failure. This projection substitutes
EXPRESSIONS, so a callee returning a tensor would be lowered one element at a
time, which is scalarization by another route and is refused. The chain that
holds the estimator's 20,572 B of scratch live is a chain of matrix-returning
callees, so every member of it is refused, and inlining as implemented recovers
none of the working-memory gap against a flat generator. Closing that gap needs
an inliner that splices the callee's STATEMENTS, loop nests included, which
keeps the tensors AND removes the boundary. Pushing the existing mechanism
harder goes the wrong way: `--inline-policy all` on the planner doubles the
executed instructions to 94,824 and nearly triples .text to 43,546 B while
still freeing no memory.

Outputs are unchanged at every setting. The planner driver prints
`t=1.200 seg=1 pos=2.4054 -0.2177 2.0000 vel=3.5856 acc=-2.7648 jerk=-15.8400
snap=40.7999 yaw=0.1944` byte for byte under `none`, `cost-model` and `all`.

Traceability is checked rather than asserted: every anchor the structured
artifact carries survives substituting every legal call, except the spans of
the call expressions that no longer exist, and each of those losses is checked
to name a model line that writes one of the substituted calls. Naming those
call sites in the artifact is the inline-chain breadcrumb D6 asks for; the
GALEC statement carries a span and no annotation channel, so it is not emitted
yet and is owed.

Signed-off-by: James Goppert <jgoppert@cognipilot.com>
Function inlining and array scalarization differ in kind: inlining changes
call structure and preserves every tensor fact, scalarization destroys index
sets and structure and can only be undone by a recognizer. They are therefore
independent flags, --inline-policy (none, annotated, cost-model, all) and
--scalarize-policy (never, and refused otherwise), with the certifiable
defaults emitting today's exact bytes and every block disclosing the policy it
was built under. Modelica's standard Inline and LateInline annotations are
honored, with Inline=false absolute.

Measured: expression substitution cannot recover the call-chain working
memory, because a tensor-returning callee would be lowered element by element,
which is scalarization by another route and is refused. Closing that axis
needs a statement-splicing inliner in the GALEC to Solve refinement, where the
transform's home is recorded.
dougdew64 added a commit to dougdew64/rumoca that referenced this pull request Sep 4, 2026
…ng the fix

Doug ruled: do not fix the initialization defect in the fork, wait for the release.
That was in the conversation only, so a fresh session reading the Current work box
would have found a located defect with a scoped fix and proposed it again. The box now
carries the ruling and the reason it is not caution — 0.10.0 already contains
initial_pins.rs, whose doc describes the exact formulation this project derived, and
has deleted the der-to-zero arm.

Also corrects the box's implicit premise. Upstream is not abandoned; it is mid-release,
which reads identically from outside: main last moved 2026-07-29 and outreach went
unanswered, while PR CogniPilot#340 (194 commits, 2,377 files) was updated 2026-08-27. The work
waits on git fetch, not on a person.

DECISIONS.md records the CLAUDE.md backup hazard, because the instinct behind it was
sound and will recur. A copy in a parent directory is auto-loaded as authority rather
than sitting inertly beside the original, and repos/ is not a git repo so nothing
tracked it. Measured cost from ad1856b..HEAD: nothing superseded, but one rule added
2026-09-02 was invisible for a session — answer through HRW when HRW can show it —
which is that rule's own documented failure mode, silent. A backup of a tracked file
is redundant with git; the pre-maintenance text was read out of ad1856b. Use a tag.

Fast gate: 874 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant