Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Rumoca Dev Guide cover

This book explains how the Rumoca compiler works and how to contribute to it. It is written for humans first: the chapters tell the story of a model moving through the pipeline, then point you at the code and the specs that own each piece.

Rumoca is a Modelica compiler written in Rust. It compiles Modelica models to differential-algebraic equations (DAE), simulates them with pluggable solver backends, and generates code for symbolic, compiled, and packaged targets. The same compiler runs natively, in the rumoca-lsp language server, and in WebAssembly in the browser.

How to Use This Book

  • New to the codebase? Read the How the Compiler Works part in order — it follows a model from source text to simulation results.
  • Fixing a bug or adding a feature? Start with Getting Started, then read the chapter for the pipeline stage you are touching, then read the spec that owns it.
  • Looking up a rule? Skip this book and go straight to spec/.

This Book Explains; the Specs Decide

Normative architecture and contribution rules live in spec/, and AGENTS.md is the routing index from task to spec. This book deliberately does not restate spec rules — it gives you the mental model that makes the specs easy to read, and links to them. When this book and a spec disagree, the spec wins (and a docs fix is welcome).

Philosophy in One Paragraph

Rumoca’s core scope ends at DAE generation (SPEC_0031): the compiler builds a stable, deterministic, solver-agnostic DAE, and everything downstream — solvers, runtimes, viewers, bindings, code generation targets — is a replaceable extension layered on that contract. Most architectural decisions in the codebase trace back to defending that boundary.

Prerequisites

You should be comfortable with Rust, basic compiler concepts (parsing, ASTs, type checking), and Git/GitHub workflows. Familiarity with Modelica helps but is not required — the user guide and the chapters here explain the concepts where they matter.

Pipeline Overview

A model moves through the compiler in a fixed sequence of phases, each owned by a rumoca-phase-* crate, each producing or refining one of four intermediate representations:

Modelica source (.mo)
   │  parse                 rumoca-phase-parse
   ▼
  AST ───────────────────── rumoca-ir-ast
   │  resolve               rumoca-phase-resolve
   │  typecheck             rumoca-phase-typecheck
   │  instantiate           rumoca-phase-instantiate
   │  flatten               rumoca-phase-flatten
   ▼
  Flat ──────────────────── rumoca-ir-flat
   │  DAE lowering          rumoca-phase-dae
   ▼
  DAE ───────────────────── rumoca-ir-dae      ◄── the stable contract
   │  structural prep +     rumoca-phase-structural,
   │  solve lowering        rumoca-phase-solve
   ▼
  Solve ─────────────────── rumoca-ir-solve
   │
   ├── simulate             rumoca-sim, rumoca-solver-*
   └── generate code        rumoca-phase-codegen + targets

The stage contracts — what each IR may contain, what each phase may and may not do — are normative in SPEC_0007. The narrative below is the mental model.

The Story of a Compile

  1. Parse turns source text into an AST that faithfully represents the text — comments, spans, syntax structure — with no semantic knowledge.
  2. Resolve builds the scope tree and assigns every definition a stable DefId. From this point on, compiler identity is DefId-keyed, never string-keyed.
  3. Typecheck checks the resolved tree and evaluates structural parameters (the ones that determine array sizes and loop ranges).
  4. Instantiate applies modifications and builds the instance tree for the requested model — Tank tank1(area = 2.0) becomes a concrete instance with area bound.
  5. Flatten walks the instance tree into a single flat model: one list of variables with fully qualified names, one list of equations, connect sets expanded into equality and flow-sum equations. Arrays stay symbolic; der, pre, sample are still present as expressions.
  6. DAE lowering eliminates Modelica-specific operators and produces the MLS Appendix B canonical form: pure functions over the variable vector, with events, relations, and clocks as explicit metadata. reinit becomes guarded update equations; pre becomes explicit __pre__.* slots; assert/terminate become event actions.
  7. Structural preparation (on the way to simulation) matches equations to unknowns, sorts into block lower-triangular (BLT) order, tears algebraic loops, performs index reduction with dummy derivatives, and selects states.
  8. Solve lowering converts the prepared system into a register-machine representation — scalar programs and tensor program nodes — that execution backends consume directly.

You can watch every step on a real model:

rumoca compile Model.mo --emit ast-mo     # or flat-mo, dae-mo, *-json
rumoca compile Model.mo --emit solve-json
rumoca compile Model.mo --inspect structure
rumoca compile Model.mo --target sympy -o /tmp/out -v   # phase timing lines

Try It Here

This block runs the same pipeline in your browser; Show DAE displays the lowered system (stage 6) for whatever you type:

model Mixer "Two tanks exchanging fluid"
  parameter Real k = 0.4 "Exchange coefficient";
  Real h1(start = 1.0);
  Real h2(start = 0.0);
  Real q "Exchange flow";
equation
  q = k * (h1 - h2);
  der(h1) = -q;
  der(h2) = q;
  annotation(experiment(StopTime = 10.0));
end Mixer;

Where the Boundaries Bite

Three boundary rules explain most review feedback on pipeline changes:

  • Codegen targets the lowest IR it needs — no lower. A formatter reads AST; FMI export and symbolic backends read DAE; numeric kernels read Solve. Reaching down for convenience couples a backend to representation details it should not know.
  • The DAE is lean. Mass matrices, Jacobians, BLT orderings, tearing choices — anything that is a solver work product — belongs in structural analysis results or Solve artifacts, never stored inside the DAE.
  • Phases fail loudly in their own stage. If source information (a span, a name structure, a type) is lost at a phase boundary, the fix is to preserve it at that boundary — not to reconstruct it downstream by parsing strings.

The Four IRs

Each IR is a serializable data structure crate (rumoca-ir-*) with a schema version. This chapter says what each one is for; the binding contracts live in SPEC_0007.

AST (rumoca-ir-ast)

The parser’s output: concrete syntax with comments and spans. It represents text, not semantics — no name resolution, no types. Formatters, pretty-printers, and doc generators work here because they need the original syntax. Every node carries a Span, and that provenance must survive any AST merging.

Dump it: rumoca compile Model.mo --emit ast-mo (or ast-json).

Flat (rumoca-ir-flat)

The instantiated, modified, flattened model: variables and equations with fully qualified names, no unresolved references, no pending modification chains. Two things are deliberately not done yet:

  • Arrays stay symbolic. Scalarization happens later, with shape metadata, only for backends that need it.
  • Modelica operators survive. der(), pre(), sample(), initial() are still expression nodes; their semantic lowering is the DAE phase’s job.

Flat is the right level for flat-Modelica export and for structural transformations that preserve Modelica expression form.

Dump it: --emit flat-mo / flat-json.

DAE (rumoca-ir-dae)

The stable contract of the whole project: the MLS Appendix B canonical DAE. Pure functions over the variable vector v := [p; t; ẋ; x; y; z; m; pre(z); pre(m)], partitioned by kind:

IDFunctionRole
B.1afx(v, c) = 0Continuous DAE residual
B.1bfz(v, c) = 0Discrete real update
B.1cfm(v, c) = 0Discrete-valued update
B.1dfc(relation(v))Event conditions

By the time a model is DAE, no source temporal operator survives — not in any partition. pre has become explicit __pre__.* parameter slots; reinit has become guarded update equations; sample has become event and clock metadata; assert/terminate have become event actions. A validation pass (appendix_b_validation) enforces this positively rather than relying on downstream code to cope.

The DAE is also deliberately lean: it represents Modelica semantics and source identity, never solver work products. If you are tempted to cache a Jacobian, a BLT ordering, or a scalarized variant inside the DAE — that belongs in structural analysis results or Solve artifacts.

Dump it: --emit dae-mo / dae-json. The dae-mo form is what the Show DAE buttons in both guides render.

Solve (rumoca-ir-solve)

A register-machine representation of the DAE functions: ComputeBlock graphs mixing ScalarProgramBlocks (flat register programs, one scalar output each) with tensor program nodes (MatMul, LinSolve, …) that carry explicit shape/layout metadata and a fallible scalar fallback. Solve adds no new mathematics — it changes format so execution backends (interpreter, Cranelift JIT, MLIR, CUDA, generated C/Rust) can consume the system directly, choosing scalar expansion or native tensor kernels.

Dump it: --emit solve-json (there is no Modelica rendering of Solve).

Schema Versions

Serialized DAE and Solve payloads carry a mandatory root schema_version; deserializers reject unsupported versions. The policy is in IR Schema Versioning.

Front End: Parse to Flat

The front end takes source text to the Flat IR. Five phases, each a crate.

Parse (rumoca-phase-parse)

Produces the AST: syntax structure, comments, spans. The parser assigns each file a stable source identity (Span.source derives from the source name, not an insertion index), which is what lets diagnostics, the LSP, and the formatter agree about locations across sessions.

The parser is also the foundation of the editor experience — it is fast and error-tolerant enough to run on every keystroke in rumoca-lsp and the browser playground.

Resolve (rumoca-phase-resolve)

Builds the scope tree and assigns every definition a DefId — a stable, structural identity for classes, components, and variables. Name lookup walks the scope tree (SPEC_0002); the DefId design and its invariants are SPEC_0001.

The rule that shapes everything downstream: after resolution, identity is DefId, never a rendered name string. Hashing or comparing flattened name strings ("a.b.c") inside semantic code means structure was lost too early; carry the DefId instead. Textual path parsing is allowed only at true source/protocol/config/display boundaries.

Typecheck (rumoca-phase-typecheck)

Checks the resolved tree and evaluates structural parameters — the values that determine array dimensions and for-equation ranges, which must be known before instantiation can size anything. Type errors carry spans and phase-local error codes.

Instantiate (rumoca-phase-instantiate)

Builds the instance tree for the requested model: applies modification chains (Tank tank1(area = 2.0, h(start = 1.0))), handles extends, redeclarations, and conditional components, producing an InstanceOverlay/InstancedTree. Instantiation and flattening are deliberately separate phases — the production path runs instanced typechecking between them, and no cross-phase shortcuts are allowed.

Flatten (rumoca-phase-flatten)

Traverses the instance overlay into flat::Model: one variable list with fully qualified names, one equation list, connect sets expanded into potential-equality and flow-sum equations, for-equations unrolled. What flattening does not do is equally important: arrays stay symbolic, function bodies stay structured, and Modelica operators (der, pre, sample) survive untouched for the DAE phase.

Seeing It

rumoca compile Model.mo --emit ast-mo    # what the parser saw
rumoca compile Model.mo --emit flat-mo   # what flattening produced

Comparing flat-mo against your mental model of the hierarchy is the fastest way to debug modification and connection handling.

DAE Lowering and Structural Analysis

DAE Lowering (rumoca-phase-dae)

DAE lowering eliminates the Modelica-specific surface and produces the MLS Appendix B canonical system (see The Four IRs). The transformations with the most moving parts:

  • pre() elimination. Every pre(x) becomes an explicit __pre__.* parameter slot; the runtime writes those slots at event entry. This must hold in all partitions (f_x, f_z, f_m, f_c) — pre exists only in AST and Flat.
  • when/reinit lowering. When-clauses become event conditions plus guarded discrete update equations; reinit(x, e) becomes a guarded state update over current/pre slots.
  • Relation extraction. Every relational expression that can generate an event gets a relation/condition variable; conditions.relations is the single owner of that surface. Synthetic numeric roots (from abs, sign) live separately in events.synthetic_root_conditions.
  • sample/clocks. Scheduled events are explicit DAE metadata, not expressions the runtime has to discover.
  • assert/terminate. Lowered to guarded events.event_actions with source spans, keeping the compute graphs pure.

A positive validation gate (appendix_b_validation) rejects any surviving source temporal operator — the contract is enforced, not assumed.

Structural Analysis (rumoca-phase-structural)

Between the DAE and a runnable system sits structural preparation:

  1. Matching. Assign each equation to the unknown it will compute (maximum bipartite matching). Failure here is the structurally singular system diagnostic users see, which names unmatched equations and unknowns.
  2. Sorting (BLT). Order the matched system into block lower-triangular form: a sequence of scalar assignments and strongly connected components (simultaneous blocks).
  3. Tearing. Within coupled blocks, choose tearing variables so a small nonlinear core is iterated while the rest is evaluated explicitly.
  4. Index reduction. Higher-index DAEs are reduced with the Mattsson–Söderlind dummy-derivative method: constraint equations are differentiated and some derivative-defined states are demoted to dummy variables. State selection decides which candidates remain integrator states.
  5. Scalarization (when a backend requires it) expands symbolic arrays using shape metadata — never by parsing display strings.

The analysis products (matching, BLT blocks, tearing choices, state selection reports) are returned as separate artifacts — they are inputs to Solve lowering and to --inspect structure, and deliberately not stored in the DAE.

Watching It Work

rumoca compile Model.mo --inspect structure   # matching, BLT, SCCs, tearing
rumoca sim Model.mo --inspect eval            # values + derivatives at a point
rumoca sim Model.mo --inspect jacobian --at "x=1@0"

These are the same tools the user guide teaches for debugging models — as a compiler developer you will mostly use them to verify that a lowering or matching change did what you intended on a specific model.

Solve IR and Execution

Solve Lowering (rumoca-phase-solve)

Solve lowering converts the structurally prepared DAE into the Solve IR: a register-machine format that execution backends consume directly. The MLS B.1 functions become ComputeBlock graphs mixing two kinds of nodes:

NodeContents
ScalarProgramBlockFlat register programs (Vec<LinearOp>), one scalar output per program
Tensor program nodes (ComputeNode::MatMul, LinSolve, AffineStencil, …)Tensor kernels with explicit shape/layout metadata and a scalar fallback

Solve adds no mathematics; it changes format. Keeping tensor structure explicit above the scalar layer is what lets a backend choose between scalar expansion (embedded C) and native kernels (BLAS/faer, CUDA, MLIR linalg) without re-deriving structure.

ComputeNode::AffineStencil represents a source-proven DAE for-equation subdomain. It carries the preserved source iteration domain plus affine LoadY, LoadP, and Const operand strides. This gives PDE-style method-of-lines systems a native GPU/codegen path while preserving exact scalar fallback behavior.

SolveProblem is the base lowered problem. Expensive or non-canonical products (mass-matrix form, output projections) are separate artifacts requested by the backends that need them.

Reverse-mode AD, steady-state objectives, lift/drag projections, and optimizer sensitivities are artifact/runtime layers over Solve. They are not fields in the base SolveProblem.

Execution Adapters

Execution adapters wrap toolchains and runtime APIs around generated or lowered code. They must not own semantics — no DAE lowering, no structural rewrites, no template policy:

CrateRole
rumoca-exec-craneliftIn-process JIT via Cranelift
rumoca-exec-mlirMLIR-based compilation path
rumoca-exec-wasmWASM execution backend

The generated-code targets (rust-solve, c-solve, embedded-c, cuda-c, cuda-nvrtc-solve-jit, fmi2/fmi3) consume Solve through the codegen engine instead — see Code Generation Engine.

Adding a New Backend

The pathway for a new execution backend (the same one a future WebGPU/WGSL backend would take):

  1. Decide the consumption model: a codegen target (templates rendering kernels, like cuda-c) or an execution adapter (an API wrapper, like rumoca-exec-cranelift) — or both, like the NVRTC JIT.
  2. Consume Solve IR. If the backend needs tensor structure, use the tensor program nodes; every valid node has a fallible scalar fallback, so a backend can start scalar-only, propagate malformed tensor metadata, and specialize incrementally.
  3. Declare capabilities honestly in the target manifest — readiness level and per-feature support columns are what rumoca targets reports.
  4. Keep language/toolchain specifics in the target’s target.toml and templates, never in phase logic (SPEC_0029).

Diagnostics and Spans

Good diagnostics are a feature of every phase, not a layer bolted on at the end. The normative rules are SPEC_0008; this page is the working model.

Phase-Local Errors

Each phase defines its own error enum with phase-specific error codes (EM001 duplicate class, ED013 unsupported algorithm lowering, …), defined next to the code that emits them. There is no central error enum: errors evolve with their phase, ownership is obvious, and code ranges stay consistent per phase.

When you add a failure path, add it to the owning phase’s enum with a span and a code — do not widen a generic error or stringify early.

Spans and Source Identity

Span.source is a stable identity derived from the source name, assigned by the parser. Spans must be carried from source data through every diagnostic-producing IR. Fallback spans are acceptable only when no source exists; if source data exists and a diagnostic lacks a span, the correct fix is to preserve the span at the phase boundary where it was lost — not to synthesize one downstream.

This is why diagnostics work identically across the CLI (rich terminal rendering), VS Code (Problems panel via LSP ranges), and the browser editors (Monaco markers): they all consume the same span-carrying diagnostics, only the presentation differs.

Failing Early and Loudly

The repository’s defensive-coding posture: catch errors in the phase that owns the invariant, with a typed error — never weaken a check because a downstream consumer can “cope”. Validation gates (such as the DAE’s appendix_b_validation) are positive enforcement of contracts, and tests assert the specific expected error, not just “some error”.

Tracing

Phases use the tracing crate for structured diagnostics during development. Debugging knobs are documented CLI flags — Rumoca has a zero-RUMOCA_*-environment-variable policy (SPEC_0018), so a debugging affordance worth keeping becomes a flag (like --inspect and -v), not an env var.

Simulation Runtime

The simulation runtime turns a compiled model into trajectories. It is organized around shared solver interfaces: generic simulation policy lives in the shared runtime layer, and solver backends are thin adapters.

Layering

CrateRole
rumoca-simSimulation orchestration over the compiled model
rumoca-solverShared solver API, result types, report payloads
rumoca-solver-rk45Explicit Runge–Kutta-style backend (rk-like)
rumoca-solver-diffsolImplicit backends via diffsol (bdf, esdirk34, trbdf2)
rumoca-input, rumoca-input-keyboard, rumoca-input-gamepadInteractive input devices
rumoca-signal-frameSignal payload types
rumoca-transport-udp, rumoca-transport-websocketExternal coupling and viewer transport
rumoca-webBrowser viewer assets

Shared Responsibilities

These belong in the shared runtime/solver API layer, never duplicated in a backend:

  • event schedules and root handling (zero-crossing location, event iteration, __pre__.* slot updates at event entry),
  • input routing and zero-order-hold behavior,
  • result collection and the report payload,
  • termination (terminate) and assertion handling,
  • pacing: as_fast_as_possible, realtime, and lockstep modes.

If two solvers need the same code, it moves into the shared layer. Solver backends implement the integration method and consume resolved input values — they must not know about keyboards, gamepads, or transports.

Events at Runtime

The DAE hands the runtime explicit metadata: relations (event condition surfaces), scheduled events (sample), event actions (assert, terminate), and the pre-slot bindings. The runtime’s job is mechanical: detect or schedule the event, advance to the event instant, write pre slots, apply discrete updates, and restart integration. No backend rediscovers event structure from expressions.

Failure Diagnostics

When a simulation produces a non-finite value, the runtime re-runs with NaN tracing to name the offending variables — the user-facing behavior documented in the handbook’s troubleshooting chapter. Keep that path working when touching solver internals; it is the difference between a useful bug report and “solver error”.

Code Generation Engine

rumoca-phase-codegen renders text from IRs through Jinja templates. A target is a directory: a target.toml manifest plus templates. Built-in targets are bundled into the binary; users can supply their own directory or a raw template (see the user guide’s Custom Targets chapter for the user-facing workflow).

Ownership Rules

  • The target owns its IR choice. A manifest declares which stage it consumes (ast/flat/dae/solve); individual templates may not silently switch IRs. Targets consume the lowest IR they need — no lower.
  • The engine renders; it does not decide semantics. Scalarization, structural analysis, and lowering happen in compiler phases; templates receive prepared data.
  • No language special-casing in Rust. Phase code must not branch on “is this C/CUDA/Python”. Language-specific behavior is expressed in target.toml metadata and the templates themselves (SPEC_0029).

Capability Declarations

rumoca targets prints, for every built-in target, the IR it consumes, its generation mode (symbolic / compiled / source-transform / packaged), deployment class, readiness level (0 experimental … 2 validated), and per-feature support columns (scalar, matmul, linsolve, sparse, dynamic control flow, affine stencils, events, forward/reverse AD). These come from the target manifests — keep them honest when extending a target; the table is the user-facing contract.

Solve-level tensor nodes, including ComputeNode::AffineStencil, arrive at templates with scalar fallback behavior already proven. Stencil-aware targets such as wgsl-solve should render the native tensor node directly; scalar targets should use the fallback programs and must not infer stencil structure by scanning flattened row text.

Template Runtime Tests

Targets whose output is executable are covered by opt-in template-runtime regression tests (cargo xtask verify template-runtimes), which actually run the generated code. Adding a target with runnable output should come with such a test.

Adding a Target

  1. Start from a worked example: examples/codegen/standalone_web/ is a complete custom bundle; the built-in target directories show the full manifest vocabulary.
  2. Declare the IR stage and capabilities in target.toml.
  3. Write templates against the serialized IR (--emit <stage>-json shows the exact shape).
  4. Wire a runtime regression test if the output executes.

WASM, LSP, and Editors

The same compiler serves three editor surfaces: the rumoca-lsp language server (native, bundled with the VS Code extension), the WASM bindings (browser playground and the live examples in these books), and the CLI’s terminal diagnostics. They share rumoca-tool-lsp for language smarts, so completion and diagnostics behave identically everywhere.

Crates and Directories

LocationRole
rumoca-tool-lspLanguage-server logic: diagnostics, completion, hover, semantic tokens, definitions, code actions
rumoca-tool-fmt, rumoca-tool-lintFormatter and linter (CLI + LSP + WASM)
rumoca-bind-wasmwasm-bindgen API: compile, simulate, simulation sessions, source-root management, LSP functions
rumoca-bind-pythonPython bindings (pip install rumoca)
packages/rumocanpm/WASM package builder; generated packages land under packages/rumoca/dist/
packages/rumoca-webHand-written browser runtime, WebGPU/diffsol drivers, visualization code, and vendored web dependencies
packages/vscodeVS Code extension (TypeScript)
packages/playgroundBrowser playground (Monaco workbench over the WASM package)
docs/user-guide/live/The books’ live-example harness (mini Monaco editors over the same WASM package)

The WASM API Surface

rumoca-bind-wasm exposes the pipeline to JavaScript:

  • compile(source, model) → DAE JSON; render_target(...) renders a DAE-level target (the books’ Show DAE uses the dae-modelica target).
  • simulate_model(source, model, t_end, dt, solver, parameter_overrides_json) → result payload with names/allData/nStates plus request/timing metadata. Passing t_end = 0, dt = 0, solver = "" defers to the model’s experiment annotation. parameter_overrides_json is a JSON object whose keys are tunable parameter names and whose values are finite numbers.
  • model_parameter_metadata(source, model_name) → tunable parameter metadata used by the shared scenario GUI before writing [parameters] overrides.
  • WasmSimulationSession — interactive simulation with set_input, advance_to, and get.
  • lsp_diagnostics / lsp_completion / lsp_hover / lsp_semantic_tokens … — thin wrappers over rumoca-tool-lsp used by the playground workers and the books’ editors.
  • Source-root management (load_source_roots, parsed-document caches, bundled archives) so browser sessions can host package trees.

Build and Test

cargo xtask playground build  # wasm-pack build into packages/rumoca/dist/<profile>/
cargo xtask playground test   # CI gate: build + browser smoke tests (Playwright)
cargo xtask vscode test       # VS Code extension gate
cargo xtask docs serve        # local books with live examples and WASM package

The Pages deployment copies the WASM package, the playground, and both books into one artifact — see Docs and Pages.

One Language Definition

Monaco’s Modelica language definition (tokenizer, comments, brackets) lives in packages/rumoca-web/runtime/modelica_language.js and is imported by both the playground and the books’ live harness. Edit it there only — the books load it dynamically from the deployed package layout.

Getting Started

Clone and Build

Install the Rust toolchain pinned by rust-toolchain.toml, then:

git clone https://github.com/CogniPilot/rumoca
cd rumoca
cargo build --workspace

Install the Developer CLI

The repository uses an xtask developer CLI (the cargo-xtask convention) for every verification, packaging, and maintenance workflow. Install the standalone launcher and the repo hooks once:

cargo xtask repo cli install     # `xtask` on PATH + shell completions
cargo xtask repo hooks install   # git hooks

After that, xtask ... works from anywhere in the workspace (the cargo xtask ... form always works too).

Fetch Modelica Dependencies

cargo xtask repo modelica-deps ensure

Downloads the pinned MSL and CMM versions into target/, which the examples, tests, and committed VS Code settings expect.

Sanity Check

cargo xtask verify quick

This runs the same verification surface as GitHub CI except the slow full-MSL parity gate. It expects the local prerequisites CI installs: cargo-llvm-cov, Node/npm, and the wasm Rust target/tooling. For the narrow loops you will actually iterate with, see Testing and Quality Gates.

Editor Setup

Open the repository root in VS Code with the Rumoca Modelica extension. For compiler development, enable rumoca.useSystemServer and put your locally built rumoca-lsp on PATH so the editor exercises your changes. Launch the extension from source in packages/vscode when working on the extension itself.

Find Your Bearings

  1. Read Pipeline Overview if you have not yet.
  2. Read Where the Rules Live — five minutes that will save your first review round.
  3. Pick the chapter for the area you are changing; it links the owning spec.

Where the Rules Live: Specs

Rumoca keeps one source of truth per rule: the specs in spec/. AGENTS.md is a thin routing index from “what you are touching” to “which spec to read” — it contains no rules itself, and neither does this book.

The Map

If you are touching…Read
Compiler pipeline / any IR / any phaseSPEC_0007
Crate dependencies, foundation types, re-exportsSPEC_0029
Modelica semantics (anything MLS-affecting)SPEC_0022
Name lookup, scopes, DefIdSPEC_0001, SPEC_0002
Diagnostics, spans, error codes, tracingSPEC_0008
Tool config (rumoca-tool-*, env-var policy)SPEC_0018
Function length, nesting, file size, determinismSPEC_0021
Development workflow, bug triage, root-cause proofSPEC_0032
Opening a PRSPEC_0025
Scope/philosophy questions (“should this live in the compiler?”)SPEC_0031

Start at spec/README.md for the full index with statuses; SPEC_0000 explains how specs themselves work.

Working With Specs

  • Active specs (ACCEPTED/REFERENCE) are mandatory; archived ones are history.
  • If a change you want violates a spec, propose the spec change first — architecture tests enforce important boundaries, and bypassing a test is never the move.
  • If you cannot find the spec for what you are about to change, stop and ask: the rule either exists somewhere you have not looked, or it needs to be written before the code.
  • Do not duplicate spec content into other documents (including this one); one source of truth per rule is what keeps the spec set trustworthy.

House Norms Worth Knowing Early

These recur in review and each traces to a spec:

  • Prefer the correct long-term fix over a short-term hack; fix root causes in the owning phase rather than weakening checks downstream.
  • No clippy allow attributes; address the lint.
  • No behavior-changing RUMOCA_* environment variables — knobs are documented CLI flags or config keys.
  • Debugging is tracing-based, not eprintln! behind env vars.
  • Deterministic collections and complexity limits per SPEC_0021.

Testing and Quality Gates

The Verification Surface

cargo xtask verify is the umbrella for everything CI runs:

CommandScope
cargo xtask verify quickFull CI surface except the slow full-MSL parity gate
cargo xtask verify fullEverything, including full-MSL parity
cargo xtask verify lintFormatting + clippy
cargo xtask verify workspaceWorkspace build/tests
cargo xtask verify docsDocumentation build (rustdoc + mdBook books)
cargo xtask verify msl-parityMSL parity gate on its own
cargo xtask verify template-runtimesOpt-in execution tests for generated target code

Editor surfaces have their own gates:

cargo xtask vscode test      # extension compile + tests
cargo xtask playground test  # wasm build + browser smoke tests

verify quick/full include the coverage, VS Code, and wasm gates, so they need the same prerequisites CI installs: cargo-llvm-cov, Node/npm, and the wasm Rust target/tooling.

During Development

Plain Cargo works for tight loops:

cargo test -p rumoca-phase-dae
cargo test -p rumoca-phase-structural some_test_name

When testing failure paths, assert the specific phase error you expect — the codebase’s expect-vs-error discipline exists so a passing test means the right thing failed for the right reason.

The MSL Quality Gate

The strongest regression net is the Modelica Standard Library gate: CI compiles and simulates a large MSL model population and compares against recorded baselines, blocking silent regressions in compile success, simulation success, and trace parity. Details, baseline policy, and promotion workflow: MSL Quality Gate.

For compiler changes that could affect MSL behavior, run the parity gate (or at minimum verify quick plus a targeted MSL model) before opening the PR — SPEC_0025 defines what evidence a PR needs.

Coverage

cargo xtask coverage report

CI enforces a coverage gate; locally you need cargo-llvm-cov.

Architecture Tests

Dependency boundaries from SPEC_0029 are enforced by tests. If one fails on your change, the answer is a design conversation (possibly a spec change) — not loosening the test.

Pull Requests

The PR process — required verification, metrics, MSL evidence, and done criteria — is normative in SPEC_0025. Read it before opening your first PR. The short practical version:

Before Opening

  • cargo xtask verify quick passes locally (use verify full when your change can affect MSL behavior).
  • The change follows the owning spec; if it required bending one, the spec change is part of the discussion, not an afterthought.
  • Diagnostics added or changed carry spans and phase-local error codes.
  • Tests assert specific expected errors, not just failure.

Commit Hygiene

  • Commits are signed off (git commit -s) — the DCO sign-off is required.
  • Write commit messages about why, not just what.

Review Flow

PRs run the full CI matrix: lint, workspace tests, coverage gate, MSL quality gate, docs build, VS Code extension gate, and the WASM gate. The MSL gate posts a comparison comment on the PR so reviewers see compile/simulate/parity movement at a glance.

Upstream-first: when a bug traces to an earlier phase, fix it there rather than compensating downstream — reviewers will ask for the root-cause fix (see Diagnostics and Spans for the same principle applied to error handling).

Issues and Discussion

Bugs and feature requests: https://github.com/CogniPilot/rumoca/issues. A minimal .mo reproduction (the playground makes these easy to verify) turns a vague report into a fixable one.

Crate Map

The workspace is deliberately granular — phases, IRs, tools, backends, and bindings are separate crates with enforced dependency edges. The normative boundary rules are SPEC_0029; this page is the orientation map.

Families

FamilyCratesRole
Foundationrumoca-core, rumoca-contracts, rumoca-codec, rumoca-codec-flatbuffersShared low-level types, contracts, serialization
IRsrumoca-ir-ast, rumoca-ir-flat, rumoca-ir-dae, rumoca-ir-solveThe four stage data structures
Phasesrumoca-phase-parse, -resolve, -typecheck, -instantiate, -flatten, -dae, -structural, -solve, -codegenOne transformation each
Facaderumoca-compileSession/compilation API the tools use
Evaluatorsrumoca-eval-ast, -eval-flat, -eval-dae, -eval-solveStage-appropriate evaluation
Runtimerumoca-sim, rumoca-solver, rumoca-solver-rk45, rumoca-solver-diffsol, rumoca-workerSimulation orchestration and solver backends
Optimizationrumoca-optTraining and optimization over differentiable Solve runtime
Execution adaptersrumoca-exec-cranelift, rumoca-exec-mlir, rumoca-exec-wasmJIT/compiled execution over Solve
Interactive I/Orumoca-input, rumoca-input-keyboard, rumoca-input-gamepad, rumoca-signal-frame, rumoca-transport-udp, rumoca-transport-websocket, rumoca-webDevices, signals, transports, viewer
Toolsrumoca-tool-fmt, rumoca-tool-lint, rumoca-tool-lspFormatter, linter, language server logic
Bindingsrumoca (CLI), rumoca-bind-wasm, rumoca-bind-pythonUser-facing entry points
Testing/devrumoca-test-msl, xtaskMSL gates, developer CLI

The Rules That Matter Daily

  • Foundation types live in low-level crates; phases depend forward through explicit IR contracts.
  • Tools and bindings use the rumoca-compile facade instead of reaching into phase internals.
  • Backend/runtime crates stay thin around the shared solver and execution APIs; shared policy lives in the shared layer.
  • Target-language specifics live in target.toml + templates, never in phase logic.

Architecture tests enforce the important edges. If a dependency you want violates a spec, update the spec first — see Where the Rules Live.

Docs and Pages

Site Layout

GitHub Pages serves one artifact assembled by the WASM build job in CI:

https://cognipilot.github.io/rumoca/            ← playground (packages/playground)
https://cognipilot.github.io/rumoca/user-guide/ ← mdBook, docs/user-guide
https://cognipilot.github.io/rumoca/dev-guide/  ← mdBook, docs/dev-guide
https://cognipilot.github.io/rumoca/pkg/<subdir>/ ← rumoca-bind-wasm package
https://cognipilot.github.io/rumoca/src/        ← playground JS modules

The deploy job publishes whatever the WASM build job staged into gh-pages/; any new public static content must be copied there in that CI step (.github/workflows/ci.yml, “Prepare GitHub Pages content”).

Books

Both books are mdBook projects (docs/user-guide, docs/dev-guide).

cargo xtask docs build       # build both books
cargo xtask docs serve       # build and host both books locally
cargo xtask verify docs        # the docs CI gate

Repository Web Layout

LocationRole
packages/rumocanpm/WASM package entrypoint. Its build writes generated dist/<profile>-<variant> packages; checked-in sources stay small.
packages/rumoca-webSingle source for hand-written browser runtime and visualization JavaScript/CSS, plus npm-managed browser dependencies.
crates/rumoca-webRust crate that embeds the web package for native reports/viewer shells; crates/rumoca-web/web points at packages/rumoca-web so Cargo packaging and npm packaging use the same sources.
packages/playgroundStatic playground app and browser smoke tests; it is a product package, not a reusable JavaScript library.
packages/vscodeVS Code extension, including offline webview assets built from the same packages/rumoca-web source package.
docs/user-guide/livemdBook live-example harness. docs/dev-guide/live is a symlink to it.
infra/Installer, Nix, and runtime dependency integration; not npm/Cargo package source.

Do not check generated/minified vendor JavaScript into the tree. Browser dependencies are normal npm dependencies and are staged into ignored/generated outputs by package-local builds or cargo xtask commands.

Live Examples

Fenced blocks annotated modelica,interactive become editable, runnable mini editors backed by the WASM package. The harness is docs/user-guide/live/rumoca-live.js (+ .css), wired into both books via additional-js/additional-css in their book.tomls — docs/dev-guide/live is a symlink to the user-guide copy so there is a single source.

How it works:

  • Editors are Monaco from staged local vendor assets, using the shared Modelica language definition packages/rumoca-web/runtime/modelica_language.js, with a plain-textarea fallback when those assets are absent.
  • Language services (completion, hover, diagnostics-as-markers) call the lsp_* functions of rumoca-bind-wasm directly on the main thread — book examples are small, so no worker is needed.
  • Simulate calls the shared packages/rumoca-web/runtime/rumoca_runtime.js interface, which loads the parsed source-root cache and routes explicit BDF requests through the lazy diffsol addon. Results render as an inline SVG plot. Show DAE uses the same runtime to render the dae-modelica target.
  • Scenario blocks can point at a staged rumoca-scenario.toml with // rumoca-live-scenario: .... The book build copies repository examples into repo-examples/ and writes a visible rumoca-workspace.toml for the staged examples. When the pinned MSL/CMM libraries are present under target/, it writes a parsed source-root cache plus manifest so browser runs honor the effective workspace roots without loading raw library source files at runtime.
  • Package discovery probes <site>/pkg/<subdir>/ (deployed layout) and <repo>/packages/rumoca/dist/<subdir>/ (local repo-root serve), overridable with window.RUMOCA_LIVE_PKG_BASE. The WASM download happens lazily on first interaction. cargo xtask docs serve builds the missing local package before serving unless --skip-wasm-build is passed.
  • Visualizations: a viz-radial fence annotation adds a built-in animated cross-section for 1-D array states; a following js,rumoca-viz fence becomes an editable visualization script that receives { payload, times, names, data, container, api } (see the turkey and 2-D wave pages in the user guide for worked examples).

Authoring a live example:

```modelica,interactive
model M ... end M;
```

Keep embedded models validated — simulate them with the native CLI before committing, and give them an experiment annotation so browser runs have sensible defaults.

Local testing with the WASM parts active: run cargo xtask docs serve and open the printed guide URL. A manual browser smoke test covering the live widgets lives at packages/playground/tests/book_live_smoke.mjs.

Updating the Pages Artifact

cargo xtask docs build runs inside the WASM CI job for both books after cargo xtask repo modelica-deps ensure; broken book builds therefore block the Pages deployment rather than shipping. The symlinked live/ assets are copied into each book’s output with hashed filenames automatically.

Scenario Config and VS Code

Scenario and tool configuration are covered by spec/SPEC_0018_TOOL_CONFIG.md.

Rumoca now prefers colocated rumoca-scenario.toml files for runnable scenarios. A rumoca-scenario.toml file uses TOML content, lives next to the example it controls, and describes one task: simulation, code generation, or another explicit tool action.

VS Code should run rumoca-scenario.toml scenarios directly. The play action belongs on the scenario, not on a .mo file that requires guessing the model, source roots, solver, and viewer.

Workspace Modelica paths are visible rumoca-workspace.toml files. Keep repo-committable dependency paths there for examples. Scenario-specific paths belong in the rumoca-scenario.toml scenario.

Workspace and Scenario Roadmap

This roadmap tracks the migration to one browser/editor model shared by VS Code, the playground, and mdBook live examples. The policy source of truth is spec/SPEC_0018_TOOL_CONFIG.md; update that spec before treating any roadmap item below as a permanent rule.

Target Shape

Rumoca has two user-visible concepts:

  • Workspace: the open file tree and performance context, like a VS Code workspace. It owns files, folders, active documents, mounted libraries, parsed source-root caches, and editor session state.
  • Scenario: one runnable TOML file, rumoca-scenario.toml or rumoca-scenario.<profile>.toml. It owns model selection, task, source roots for that run, solver settings, codegen settings, plots, viewer settings, input routing, and visualization script paths.

The GUI never owns configuration truth. It renders the selected scenario TOML and writes back to that same scenario.

Workspace Settings

Workspace configuration lives in visible rumoca-workspace.toml files. Rumoca does not use hidden project directories for generated caches, results, or local editor state.

rumoca-workspace.toml is for workspace context only:

  • global and scoped source roots
  • mounted libraries or package archives
  • parsed source-root cache policy
  • default scenario on open
  • browser/book preload bundles
  • editor layout defaults when they are portable

It must not contain model-specific solver, plot, codegen, or viewer settings. Those stay in scenario files.

Nested workspace files cascade from parent to child. The nearest file may add scoped source roots or override scalar workspace preferences. Scenario-local source_roots are applied after matching workspace roots.

Effective source roots for a scenario are:

  1. parent workspace roots
  2. nested workspace roots
  3. workspace scoped roots matching the scenario path
  4. scenario source_roots
  5. host-local overrides, only for local machine paths

Shared Surfaces

All three hosts should use the same shared behavior:

  • VS Code reads real files from disk and calls the LSP.
  • The playground reads an in-memory workspace and calls the WASM worker.
  • The Rust book opens a small generated workspace per live example and calls the same WASM/runtime helpers.

packages/rumoca-web owns shared browser UI and transforms:

  • default scenario editor GUI for rumoca-scenario.toml and rumoca-scenario.<profile>.toml
  • synchronized GUI/raw TOML mode switching for the same scenario file
  • scenario settings GUI
  • results viewer settings GUI
  • shared Modelica language setup
  • runtime helpers for parsed source-root caches and simulation
  • host-neutral request/response shapes

Host packages only adapt transport and file access.

Implementation Phases

Current status:

  • Phase 1 is implemented for the browser command surface: playground code uses workspace/scenario request names, and Rust WASM export names are treated as an internal ABI boundary in the worker.
  • Phase 2 has an initial Rust implementation for visible rumoca-workspace.toml cascading, ordered source-root merging, and LSP source-root loading. LSP reload paths now use the focused document/scenario path when loading cascading workspace config, so child workspace files and scoped source roots participate in completions, diagnostics, and simulation preparation. The same Rust workspace-config semantics are now available to browser hosts through a WASM/worker command that evaluates visible rumoca-workspace.toml files from an in-memory workspace map for a focused path. Review of that API exposure found no discrete correctness issues. The playground now passes visible workspace config files through scenario requests and uses the effective source-root command when building simulation settings/defaults for the active document. Inherited workspace roots stay out of editable scenario overrides, and simulation execution now loads the resolved effective source-root contents before dispatching the solver. The Rust book live-example harness now loads staged rumoca-workspace.toml files through the same WASM workspace-config API, and generated book scenario TOMLs keep inherited dependency roots out of editable scenario settings. The live book harness now validates that repo examples resolve effective workspace roots before simulation/DAE compilation, reports missing staged workspace or source-root cache state explicitly, and surfaces the resolved root count in the widget status.
  • Phase 3 is implemented for the planned shared scenario surfaces: playground scenario settings go through the shared WASM scenario API instead of browser-local TOML rendering. The Rust book live settings path now also uses the same WASM scenario config round-trip API instead of a browser-local TOML parser/patcher. VS Code scenario creation now renders TOML through a shared Rust/LSP scenario config command instead of hand-built TOML strings. The LSP/VS Code command surface now has full scenario config load/save primitives for the shared scenario GUI, and open scenario files are saved through the VS Code editor buffer so raw TOML state cannot go stale. VS Code now contributes the shared scenario GUI as the default custom editor for rumoca-scenario.toml and rumoca-scenario.<profile>.toml, with a raw TOML escape hatch back to the same file. The custom editor parses the live TOML buffer so unsaved raw edits and parse errors are reflected immediately when switching back to the GUI, and scenario creation/settings commands now route scenario files into that shared editor instead of an older side-panel settings path. The scenario GUI refreshes from live text when its custom editor becomes active, keeping GUI/raw TOML switching synchronized without requiring an intermediate save. The playground now opens rumoca-scenario.toml/rumoca-scenario.<profile>.toml in the shared scenario GUI by default, uses the worker-backed full scenario config load/save commands, and keeps raw TOML edits synchronized against the same in-memory workspace file. The Rust book live settings panel now mounts the same shared scenario GUI document as VS Code and the playground, with the book acting only as a thin host for WASM load/save and raw TOML fallback. The shared GUI now exposes a task-aware run action, with VS Code, playground, and mdBook hosts all saving the scenario TOML before dispatching their existing scenario execution path. The shared GUI can now display resolved workspace source roots as read-only context while keeping scenario-local source_roots as the only editable scenario field. The live book Monaco editor follows mdBook theme changes and forces existing editors to repaint when switching Light/Rust/Coal/Navy/Ayu. The shared scenario GUI is now being upgraded from a generic TOML leaf editor into the sole typed scenario authoring surface across VS Code, playground, and mdBook: finite choices use dropdowns, simulation values use validated numeric controls, plot panels use repeated structured controls instead of JSON blobs, top-level sections have readable labels, summaries, and visible disclosure affordances, advanced input routing is hidden behind a summary disclosure, and normal saves validate data before writing TOML through the shared Rust scenario renderer. Structured signal routes now round-trip as typed TOML values instead of JSON strings, so input-enabled native and browser runs share the same signal mapping shape. Inline mdBook examples without a scenario file now synthesize an in-memory rumoca-scenario.toml before opening Settings, so the same GUI configures simulation, plot panels, and 3D viewer script paths. 3D plot panels expose viewer script paths as typed controls while script bodies stay in files or host-managed assets, and the browser harness now renders scenario-configured plot/viewer panels instead of treating sidecar JavaScript as a hardcoded viewer path. The canonical file names are now rumoca-workspace.toml, rumoca-scenario.toml, rumoca-scenario.<profile>.toml, rumoca-result.<timestamp>.<model>.json, and rumoca-cache/; old scenario/workspace names and hidden project-result directories are being removed rather than aliased. VS Code source-file toolbar actions now create/open a scenario file with sensible simulation defaults and an output directory instead of prompting for task/model/name internals up front. Playground editor toolbar actions now use the same shared scenario command path: the lightning action creates/opens a codegen scenario file, the run action dispatches from the active scenario task, scenario runs compile the configured [model].file rather than editor chrome text, and generated code respects [codegen].output_dir. Scenario-local source roots now use a structured add/remove list in the shared GUI, with host-backed browsing where the host can provide it, and confusing plot default knobs are hidden from the main form in favor of concrete plot-panel configuration. Input routing is now treated as a scenario-level simulation capability rather than an interactive scenario type: the shared GUI has an explicit input-enable switch plus typed local, keyboard key/integrator, gamepad axis/button/integrator, and model-input mapping rows, while viewer surfaces remain separate. The quadrotor interactive example now demonstrates that split: realtime simulation plus input routing in the scenario, with the external 3D scene as presentation rather than a distinct scenario kind. The Rust book live widget now has the browser side of that path as well: input-enabled/realtime scenarios load the shared web scheduled simulation, compile a WASM session with staged workspace/source-root context, route keyboard/gamepad/local inputs into Modelica inputs, and run the scenario’s 3D scene inline instead of falling back to batch plots or a native-only viewer. Interactive capture, HUD/camera controls, stop handling, and state reset now live in the shared web runtime so mdBook, VS Code, and the playground consume the same control semantics instead of rebuilding them per host. Simulation speed and user input are separate scenario concepts: [sim].mode controls pacing, [input] opts into the input-capable session/viewer path, and viewer mode only chooses presentation. The playground interactive-result editor now uses the same runtime failure reporting path as the shared runtime: source-root loading, scene initialization, startup rendering, and animation-frame errors update the tab status and flow into the shared Errors panel instead of leaving .input editor tabs stuck at “compiling session”.
  • Phase 5 has a packaging/runtime baseline: xtask owns Rust orchestration and web asset freshness checks, while npm-owned package scripts may run npm, refresh stale web dependencies, and rebuild shared web assets before copying them into VS Code. The browser WASM package and GitHub Pages staging include the worker runtime dependencies (rumoca_runtime.js and modelica_language.js), and playground branding uses the shared staged brand asset.
  • Phase 6 has started in browser code: old browser-only config and generated hidden side-data terminology have been removed from the playground surface. Browser simulations with workspace sources keep RK/auto on the workspace-source WASM path, while BDF syncs those workspace sources into the runtime session before using the lazy diffsol addon path. Playground codegen target selection now reads/writes [codegen].target through shared scenario commands instead of persisting a duplicate browser editor-state setting, and the same shared scenario command surface is now available through LSP/WASM for hosts that need model-scoped codegen configuration. Codegen scenario settings no longer save through simulation-preset commands: codegen target and scenario-local source roots are task-aware scenario edits, and simulate and codegen scenarios for the same model no longer collide in the shared scenario config store. VS Code/shared settings helper APIs now use scenario names, and the remaining browser dependency bundle generated by packages/rumoca-web has been renamed away from project terminology. Playground editor-state now uses an explicit session-state allowlist, so old hidden sim, codegen-template, and selected-model values are not persisted as a second scenario configuration store. Browser contract tests now pin removal of .rum handling and generated/project side channels. Current progress is no longer considered 100% because the generic scenario form exposed too much raw TOML shape to users; remaining work is to finish typed coverage for codegen and viewer/input routing, then rerun final cross-host done-criteria verification.

Phase 1: Name the API

  • Add rumoca-workspace.toml to SPEC_0018.
  • Define workspace cascade and source-root merge semantics there.
  • Define that scenario files are the only run/config truth.
  • Define that opening a scenario file defaults to the shared scenario GUI, with a raw TOML toggle synchronized against the same file content.
  • Rename browser/LSP command concepts away from project-specific config where possible:
    • workspace.* for file tree, model discovery, and source-root cache work
    • scenario.* for reading/writing scenario TOML and derived run settings

Phase 2: Workspace-Driven Source Roots

  • Implement parent-to-child loading for rumoca-workspace.toml.
  • Merge the nearest workspace file with its parents for the active document or selected scenario.
  • Use the merged workspace source roots for VS Code completion, hover, diagnostics, go-to-definition, and model discovery.
  • Apply the same effective workspace source roots in the playground and Rust book through WASM.
  • Rebuild or invalidate parsed source-root caches when the nearest effective workspace context changes.

Phase 3: Centralize Scenario Logic

  • Keep TOML parsing/rendering in Rust shared APIs (rumoca-compile through LSP and WASM bindings).
  • Keep GUI rendering and host-neutral form transforms in packages/rumoca-web.
  • Remove browser-only TOML parsers and config renderers from the playground.
  • Make the playground apply writes returned by the WASM scenario API instead of constructing scenario text itself.
  • Make the GUI and raw TOML view round-trip through the same scenario file: valid GUI edits render TOML; raw TOML edits reparse into the GUI; parse errors stay visible without losing raw text.

Phase 4: Introduce Workspace Settings

  • Add a Rust WorkspaceConfig parser for rumoca-workspace.toml.
  • Support parent-to-child cascading and scoped source roots.
  • Expose the same effective workspace context through LSP and WASM.
  • Store generated parsed source-root caches under visible rumoca-cache/.

Phase 5: Make Hosts Thin

  • VS Code maps filesystem events and webview messages to shared scenario and workspace commands.
  • VS Code contributes a custom editor for scenario files that opens the shared GUI by default and can toggle to raw TOML.
  • The playground maps in-memory files and workers to the same commands.
  • The Rust book builds a small workspace for each live example, including the selected scenario, model files, visualization assets, and prebuilt source-root caches.

Phase 6: Delete Old Surfaces

  • Remove .rum handling.
  • Remove project-specific config concepts from browser code.
  • Remove separate playground settings state that duplicates scenario TOML.
  • Remove hidden generated cache/result/session directories. Persisted simulation results are standalone result JSON files written next to the scenario TOML by default, or under the scenario’s configured output directory.

Done Criteria

  • A scenario that works in VS Code works unchanged in the playground and Rust book when the same workspace files are available.
  • The settings GUI can be closed and reopened without losing fidelity because rumoca-scenario.toml remains the source of truth.
  • Source-root caches are preloaded per workspace/scenario context so editing, autocomplete, diagnostics, and simulation startup stay responsive.
  • VS Code autocomplete uses the merged nearest rumoca-workspace.toml context plus scenario-local roots, matching playground and Rust book behavior.
  • Clicking a scenario opens the shared GUI by default, and toggling raw TOML edits the same file without drift.
  • Browser, book, and VS Code tests exercise the same scenario/workspace command shapes instead of three separate implementations.

MSL Quality Gate

Rumoca’s main MSL baseline is the MSL 4.1.0 root-example set selected by Modelica.*.Examples.*. Helper packages under Examples such as Utilities, BaseClasses, Internal, and Interfaces are excluded.

Run the gate with:

cargo xtask verify msl-parity

The raw test command is:

cargo test --release --package rumoca-test-msl --features msl-full-test \
  --test msl_tests balance_pipeline::balance_pipeline_core::test_msl_all -- --nocapture

The gate writes the current run to:

  • target/msl/results/msl_quality_current.json
  • target/msl/results/msl_package_pass_rates.md
  • target/msl/results/msl_package_trace_accuracy.md
  • target/msl/results/mls_contract_coverage.md
  • target/msl/results/omc_simulation_reference.json

Local full runs also generate OMC compile/flatten reference data in target/msl/results/omc_reference.json unless RUMOCA_MSL_SKIP_OMC_COMPILE_REFERENCE=1 is set. CI sets that flag because cold GitHub runners repeatedly reload MSL for the compile reference; the CI gate still checks Rumoca stage counts and OMC simulation trace parity.

CI compares the current run against the resolved MSL quality baseline. cargo xtask verify msl-parity downloads the latest promoted msl_quality_baseline.json from the stable msl-quality-baseline GitHub release asset when available, caches it under target/msl/baselines/, and falls back to crates/rumoca-test-msl/tests/msl_tests/msl_quality_baseline.json for offline runs. The stage checks are cumulative over the fixed root-example denominator: parse/IR-AST, flatten/IR-flat, DAE/IR-DAE, solve/IR-Solve, initial-condition solve, and simulation. Increasing an early-stage pass count is always treated as an improvement; the gate fails when any cumulative stage count drops below the committed baseline for the same target set.

msl_quality_current.json also records release review metadata:

  • omc_version records the OpenModelica build used for OMC trace parity; the quality gate compares the upstream release version and tolerates distro package rebuild suffix drift.
  • mls_contract_coverage groups per-model stage, Solve-IR, balance, simulation, and error-code counts by MLS contract category (ARR, CONN_STRM, FUNC, EQN_ALG_SIM, CLK_SM, DECL_TYPE, PKG, OTHER). The same data is written as mls_contract_coverage.{json,md,txt} so release reviews can inspect category coverage without manually querying the quality snapshot JSON.

On pull requests, CI also generates target/msl/results/msl_pr_comment.md with cargo xtask repo msl pr-comment and publishes it as a sticky PR comment. The comment embeds the package pass-rate, MLS contract coverage, and OMC trace-accuracy markdown tables so reviewers can inspect the MSL gate without downloading artifacts first. Its top summary also shows deltas against the resolved MSL quality baseline. Forked pull requests receive the uploaded artifacts from the read-only CI run, then a separate workflow_run publisher comments from the artifact using repository write permissions.

When a full main CI run improves every ratcheted metric without regressions, the MSL Baseline Ratchet workflow publishes the new baseline to that release asset. Do not promote focused subsets or one-off explicit target files as the baseline. Promotion requires a full-run snapshot with non-empty omc_version metadata.

Focused debugging runs can use RUMOCA_MSL_SIM_MATCH, RUMOCA_MSL_SIM_LIMIT, RUMOCA_MSL_SIM_TARGETS_FILE, or RUMOCA_MSL_TARGET_SCOPE=committed-targets, but those runs are not baseline updates.

For commit-to-commit regression diffs, run both worktrees with the same focused target JSON, then generate machine-readable buckets with:

cargo xtask repo msl parity-manifest \
  --rumoca-results-file <worktree>/target/msl/results/msl_results.json \
  --omc-simulation-reference-file <worktree>/target/msl/results/omc_simulation_reference.json \
  --output-file <worktree>/target/msl/results/parity_fail_manifest.json

Compare msl_quality_current.json, parity_fail_manifest.json, and the per-model [sim_*] log lines before inspecting emitted IR artifacts.

OMC reference pool and compile-speed comparison

cargo xtask repo msl omc-simulation-reference generates the OMC simulation baseline (omc_simulation_reference.json) that the trace gate compares rumoca against, and emits the rumoca-vs-OMC compile-speed report. It runs a pool of persistent omc --interactive=zmq worker sessions (the OMC analogue of the rumoca warm worker): each worker loads the MSL once, pulls per-model jobs, and is killed + respawned (with its whole process group, so hung simulation grandchildren are reaped) on a per-model timeout.

ConcernBehavior
Pool sizeOne worker per physical core, minus headroom on large hosts (--workers 0 = auto); each worker pinned to a core.
Per-model timeout--batch-timeout-seconds (wall, compile+simulate). Kept equal to the rumoca per-model budget so timing is fair.
CachingResults are reused while the OMC version and MSL source are unchanged (cache_key in the JSON). --force re-runs everything.
ScopeAll targets by default (so models that later pass rumoca already have a baseline); --rumoca-sim-ok-only is the CI fast subset.
Subsetting--model-regex '<re>' scopes a run to matching models — the fast path for local iteration.

Compile-speed artifacts

Restricted to models where the OMC and rumoca traces agree (high/near band), so only matching results are timed:

  • msl_speed_comparison.json — the single data contract. Its _about block defines every metric (OMC compile = timeTotal - timeSimulation; speedup = omc_compile / rumoca_compile, >1 = rumoca faster; scaling binned by scalar_equations, the flattened system size — not states, which are 0 for most MSL examples).
  • msl_speed_scaling.html — a self-contained local scatter plot (one point per model, x = scalar equations, y = compile seconds, rumoca vs OMC) rendered with the same embedded uPlot backend as plot-compare. Open it in a browser.

The plot is rendered two ways from that one JSON:

  • Local: omc-simulation-reference writes msl_speed_scaling.html (uPlot).
  • PR comment: cargo xtask repo msl pr-comment reads the JSON and renders the table plus a mermaid xychart. GitHub cannot execute JS, so the PR plot is mermaid, not the uPlot viewer — and it is produced only by pr-comment, not on every OMC run.

Fast local subset

# Scope to a regex; reuses cached OMC + existing rumoca traces, then writes
# msl_speed_comparison.json + msl_speed_scaling.html for just that subset.
cargo xtask repo msl omc-simulation-reference \
  --model-regex 'Mechanics\.Translational\.Examples'

MSL Baseline Promotion Analysis, 2026-06-05

This note compares the recent full MSL quality artifacts from PRs #191, #192, #198, and #199 against the committed baseline. PR #202 is excluded from the promotion decision because its MSL job was cancelled before producing msl_quality_current.json; the uploaded artifact only contains the placeholder PR comment.

Quality Snapshot

All four comparable PR artifacts are full-scope MSL 4.1.0 runs using OpenModelica 1.26.7 and report the same quality counts. That repeated result is strong enough to promote the quality baseline from a successful full run.

MetricBaselineRecent full runsDelta
Parse5665660
Flatten556562+6
DAE / compiled414477+63
IR-Solve336387+51
Balanced401464+63
Initial balanced401464+63
Initial-condition solve OK182224+42
Simulation OK132153+21

The trace snapshot also improves on the gate-relevant model counts:

Trace metricBaselineRecent full runsDelta
Models compared123143+20
High agreement6589+24
Minor agreement1620+4
Deviation agreement4234-8
Models with any bad channel4644-2
Bad channels1047678-369

The severe-channel total rises from 35 to 151 because the current run compares 20 more models and 1363 more channels. The model-level gate signal is still better: acceptable trace models increase from 81 to 109, and models without a severe channel increase from 117 to 131.

Speed Snapshot

The rendered PR comments include comparable aggregate speed tables for 108 trace-agreeing models:

PRTotal throughputTotal medianCompile throughputCompile medianSim throughputSim median
#1913.495.965.806.820.181.22
#1923.255.515.055.960.191.57
#1983.175.304.805.640.191.65
#1993.315.585.025.740.191.47

These values support a stable median-based speed gate more than a mean-based gate. Mean speedup is more sensitive to outlier models and tiny OMC simulation times, while the existing spec defines a median tolerance for system and wall runtime ratios.

The 2026-06-05 artifacts cannot directly promote runtime speed baselines because the generated msl_quality_current.json snapshots did not serialize runtime_ratio_stats. The gate now preserves those stats from the OMC parity input and checks both system and wall medians once a promoted baseline includes them. A follow-up full MSL run after that change can safely promote the speed baseline from the machine-readable snapshot.

Recommendation

Promote the MSL quality baseline from the successful #198 full MSL artifact. Do not synthesize speed baseline values from rendered markdown. Instead, use the next full MSL snapshot generated after runtime ratio stats are serialized into msl_quality_current.json to promote speed medians through the normal baseline promotion command.

Strict Eval Rollout

This page records the roll-forward plan for removing silent default-to-zero evaluation from production compiler/runtime paths. Normative phase and crate rules remain in spec/; this guide explains the migration sequence.

Goal

Production evaluation should return an error when an expression, variable reference, runtime slot, table, or backend input is missing or unsupported. Defaulting to zero is allowed only at explicitly named caller boundaries, such as a solver initial-guess policy.

The first targets are:

  • rumoca-eval-dae internal helpers that currently default unhandled expressions or missing bindings to zero.
  • rumoca-eval-solve runtime reads that currently default missing state, parameter, seed, or register slots to zero.
  • Backend interpreter paths that mirror the permissive Solve-IR behavior.

Roll Forward

The migration is intentionally not compatibility-preserving. New production evaluator APIs should expose errors directly; callers that still need a fallback must choose an explicit policy at the boundary.

  1. Convert exported evaluator APIs to return Result<T, EvalError> or a runtime layout error.
  2. Replace implicit defaults with explicit caller policies such as EvalGuess::SolverInitialGuess.
  3. Add source guards that prevent the known default-zero fallback surface from growing while each helper is converted.
  4. Run the MSL quality gates after each behavioral slice and promote baselines only through the normal reviewed promotion flow.
  5. Remove each permissive fallback as its caller has an explicit error or initial-guess policy.

Acceptance Checks

  • No production evaluator helper silently substitutes 0.0 for an unsupported expression or missing binding.
  • Solve/runtime APIs validate vector dimensions before residual or Jacobian evaluation.
  • Any remaining default-to-zero behavior is named as an initial-guess or recovery policy at the caller boundary.
  • MSL trace and balance gates stay active while strict behavior rolls through the evaluator stack.

Test Expectations

Each converted evaluator branch should add a negative test that proves the new error is reported. MSL parity should be run before promoting any baseline after behavior changes.

IR Schema Versioning

Rumoca’s serialized IRs are compatibility contracts, not debug dumps. DAE and Solve JSON must carry an explicit schema_version, and deserializers reject unsupported versions instead of guessing.

The policy is:

  • Keep the same schema version only for additive fields that have a semantic default and are annotated with #[serde(default)].
  • Bump the schema version for renamed fields, removed fields, changed enum tags, changed units, changed indexing conventions, or changed interpretation of an existing field.
  • Do not add silent in-crate migrators to Deserialize. Migration should be an explicit tool or phase so stale fixtures and artifacts fail visibly.
  • Commit or update golden fixtures with every intentional schema-shape change.

Worked example:

#![allow(unused)]
fn main() {
pub const DAE_SCHEMA_VERSION: u16 = 1;

#[derive(Deserialize, Serialize)]
pub struct DaeClockPartition {
    pub schedules: Vec<ClockSchedule>,

    // Same-version additive change: old artifacts deserialize as no triggers.
    #[serde(default)]
    pub triggered_conditions: Vec<Expression>,
}
}

If schedules is renamed to periodic_schedules, the change is incompatible:

#![allow(unused)]
fn main() {
pub const DAE_SCHEMA_VERSION: u16 = 2;

#[derive(Deserialize)]
struct DaeWire {
    schema_version: u16,
    periodic_schedules: Vec<ClockSchedule>,
}

impl<'de> Deserialize<'de> for Dae {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let wire = DaeWire::deserialize(deserializer)?;
        if wire.schema_version != DAE_SCHEMA_VERSION {
            return Err(serde::de::Error::custom("unsupported DAE schema_version"));
        }
        Ok(Self::from_wire(wire))
    }
}
}

Review checklist:

  • Update the relevant *_SCHEMA_VERSION constant for incompatible changes.
  • Keep old versions rejected by the primary IR deserializer.
  • Add JSON and bincode round-trip coverage for the changed shape.
  • Update committed goldens so reviewers can see the exact wire-format delta.