Skip to content

engine: freshly imported/created period-named variables may fail to simulate at HEAD (XMILE-name + JSON ident boundaries skip name-context canonicalization, unlike the #690 serde fix) #702

@bpowers

Description

@bpowers

Problem (needs empirical reproduction)

The fix for #690 adds a protobuf-deserialize ident migration so a 2022-era stored blob carrying a raw-. canonical ident (e.g. goal_1.5_for_temperature) is repaired at load time. During design work for that fix, a code-trace of HEAD surfaced a broader, apparently pre-existing problem that #690's protobuf-only fix deliberately does not cover: HEAD's other ident-entry boundaries appear to mishandle freshly created / imported period-named variables today, independent of any 2022 blob.

This is code-traced, not yet empirically reproduced. It needs a reproduction before it is treated as confirmed (see the recipe below). It is filed honestly as a suspected defect with a concrete trace.

The conceptual root cause

canonicalize (src/simlin-engine/src/common.rs) applies equation-reference semantics to name/ident-field contexts. In an equation, an unquoted . means a module path, so common.rs:690 rewrites it to · (U+00B7 MIDDLE DOT, the module separator); a . inside a quoted identifier is a literal, so common.rs:681 rewrites it to (U+2024 ONE DOT LEADER, the LITERAL_PERIOD_SENTINEL, the #559 path). But a variable's name attribute / ident field is a context where a . can only ever be literal -- there is no module-path meaning. Feeding an unquoted display-name ident through the equation-context canonicalize therefore maps its literal . to the · module separator, and downstream module-splitting logic (src/simlin-engine/src/model.rs:396-403) then tears the ident into a phantom submodule path.

Entry path 1: XMILE import (fresh model)

Entry path 2: TS editor (create / rename in the live app)

  • The TypeScript canonicalize in @simlin/core (see ts: two divergent canonicalize implementations (@simlin/core incomplete vs @simlin/engine Rust-faithful) #624 -- two divergent TS canonicalize implementations) reportedly produces raw-dot canonical idents (e.g. goal_1.5_for_temperature) for period-named variables.
  • That ident enters the engine via the JSON datamodel path: src/simlin-engine/src/json.rs (e.g. :791, ident: stock.name) carries it through unchanged.
  • It then hits the same .· re-canonicalization at the salsa boundary, reproducing the path-1 mismatch.
  • Net: creating or renaming a period-named variable in the live editor may break simulation at HEAD.

Mismatch mechanism (the load-bearing detail)

Three visually-near-identical characters are in play (cf. #565):

char codepoint meaning produced for
· U+00B7 MIDDLE DOT AST/module-path separator an unquoted . (common.rs:690) -- the path the name attribute / JSON ident wrongly takes
U+2024 ONE DOT LEADER literal-period sentinel a quoted inner . (common.rs:681, #559) -- the path a quoted equation referrer takes
. ASCII U+002E raw period the TS @simlin/core ident (#624) and 2022 stored blobs (#690)

The variable's own key lands on U+00B7 (and gets module-split), while a quoted referrer resolves to U+2024 -- so they never match.

Why it matters

Components affected

  • src/simlin-engine/src/xmile/variables.rs:671 (Aux) and :289 (Stock) -- stores the raw unquoted display name as the ident.
  • src/simlin-engine/src/json.rs (~:791) -- carries the TS-produced raw-dot ident through the JSON datamodel path.
  • src/simlin-engine/src/db/sync.rs (~:288, ~:616) -- the salsa-boundary canonicalize that re-maps the unquoted . to ·.
  • src/simlin-engine/src/common.rs:681 / :690 -- the quoted (, U+2024) vs unquoted (·, U+00B7) . mapping; the source of the context-blindness.
  • src/simlin-engine/src/model.rs:396-403 -- the ·-split that turns the corrupted ident into a phantom submodule path.
  • @simlin/core src/core/canonicalize.ts -- emits raw-dot idents (the path-2 trigger; tracked structurally by ts: two divergent canonicalize implementations (@simlin/core incomplete vs @simlin/engine Rust-faithful) #624).

Reproduction recipe (do this first to confirm)

  1. XMILE import: construct an XMILE model with a quoted-period-named aux (name attribute Goal 1.5 for Temperature) and a second variable whose equation references it via the quoted form "Goal 1.5 for Temperature". open_xmile it, run compile_project_incremental, and check for DoesNotExist on the referrer plus a module-split on the period-named variable. Add a no-period control with the same structure that should compile and run.
  2. Editor / JSON: create (or rename a variable to) a period-named variable in the live editor (or drive the equivalent JSON datamodel through json.rs), add a referrer, and check the same simulate-time failure.

Per the TDD / 95%-coverage discipline, land the failing fixtures first.

Suggested direction (do NOT implement without design discussion)

Apply name-context canonicalization -- treat a . in an ident/name field as a literal (the U+2024 LITERAL_PERIOD_SENTINEL), never as the · module separator -- at all ident-entry boundaries, consistent with the protobuf-deserialize migration being added for #690:

  • The XMILE reader (xmile/variables.rs) should canonicalize the name attribute in name-context (literal-period sentinel) rather than storing the raw display name to be re-canonicalized in equation-context.
  • The JSON reader (json.rs) should do the same for incoming idents.
  • More fundamentally, canonicalize likely needs context awareness (a name-context vs equation-context mode), so a single function does not apply module-path semantics to a field where a . can only be literal. The engine: 2022-era models with quoted literal-period idents silently fail to simulate (no load-time canonical-data migration) #690 serde migration, this XMILE-name boundary, and the JSON boundary should all share that name-context treatment so the three entry points agree.

Relationship to existing tracked items (all DISTINCT)

Context

Surfaced during design work for the #690 fix (a protobuf-deserialize ident migration in src/simlin-engine/src/serde.rs). A codebase investigation of HEAD code-traced (but did not empirically reproduce) these two additional ident-entry boundaries that #690's protobuf-only fix does not cover. File:line anchors validated against HEAD: xmile/variables.rs:671/:289 (ident: name.clone()), common.rs:681 (U+2024 quoted) / :690 (U+00B7 unquoted), model.rs:396-403 (phantom submodule split). The not-yet-reproduced caveat is intentional; the first task is to confirm via the recipe above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions