Skip to content

engine: XMILE 1.0 section 4.7.1 bare <connect to="Input"> (the spec's own module-input spelling; Stella 10.0.6 writer) binds no port -- BadModuleInputDst warning, empty_equation refusal; only Stella's module-qualified to= wires #1053

Description

@bpowers

Summary

XMILE 1.0 section 4.7.1 spells a module input connect with a BARE to naming the module's own input, and its own example is <connect to="Input" from=".Root_Model_Output"/> recorded on <module name="Sub_Model">. The engine only wires a to in Stella's module-qualified form (to="Sub_Model.Input"): the spec's spelling binds nothing, is reported as BadModuleInputDst with a message that is false on its face ("input wiring target 'input' does not name an input of model 'sub_model'" -- it does), and when the port carries Stella's {Enter equation for use when not hooked up to other models} placeholder (the shape Stella writes for an input port) the whole model is refused with empty_equation / not_simulatable.

module_wiring_tests::bare_dst_warns pins the opposite of the spec: it asserts a bare dst is "the editor-bug shape" and MUST warn.

What the spec says (docs/reference/xmile-v1.0.html, section 4.7.1 "Modules")

Exact sentences (tags stripped):

Submodel input assignment: <connect to="..." from="..."/> where to contains the name of the module input within this submodel or one its submodels (in which case, it is qualified with the submodel's name) that is assigned and from contains the qualified name of the submodel output that is being assigned to that submodel input. To support arbitrary re-use of submodels[18], the inputs of a submodel within a submodel must be allowed to appear here as the connections must be specified at the lowest common ancestor (LCA) of the submodel hierarchy (keep in mind also that the module itself always appears one level above the submodel it refers to).

So to is either (a) the bare name of an input of THIS module's submodel, or (b) qualified with a NESTED submodel's name when the input belongs to a submodel inside it. The section's example (same-file and separate-file variants, identical wiring):

<module name="Sub_Model">
   <connect to="Input" from=".Root_Model_Output"/>
   <connect to=".Root_Model_Input" from="Sub_Model.Output"/>
</module>
<aux name="Root_Model_Output" access="output"/>
<aux name="Root_Model_Input" access="input"/>
...
<model name="Sub_Model">
   <variables>
      <aux name="Input" access="input"/>
      <aux name="Output" access="output"/>

to="Input" is the module's own input, bare. Nothing in 4.7.1 describes a to qualified with the module's OWN name (to="Sub_Model.Input"); that is the spelling Stella Architect 1.4 and Stella Professional 1.9.4 write (see corpus evidence below), not the spec's. (The second connect, to=".Root_Model_Input", is a module OUTPUT connect -- that is GH #1048's sibling shape, not this issue.)

What the engine does today

Every reading of a reference's destination goes through one function, port_of (src/simlin-engine/src/db/assemble.rs):

pub(crate) fn port_of<'a>(module_var_prefix: &str, dst: &'a str) -> Option<&'a str> {
    dst.strip_prefix(module_var_prefix)
}

with module_var_prefix = module_input_prefix(instance) = "{instance}·". A bare dst has no such prefix, so port_of is None; bound_port therefore returns None; build_module_inputs (the instance's lowered wiring) and module_input_set (its compilation identity, which selects the isModuleInput(port) branch in the compiled child) both project from bound_port, so the port is neither counted nor written. The XMILE reader (impl From<Module> for datamodel::Module, src/simlin-engine/src/xmile/model.rs) stores canonicalize(to) / canonicalize(from) verbatim -- it neither qualifies a bare to nor classifies a connect -- and db::sync copies references onto the SourceVariable unchanged. model_module_wiring_diagnostics (src/simlin-engine/src/db/diagnostic.rs) then reports !port.is_some_and(|port| child_vars.contains_key(port)) as BadModuleInputDst.

Reproduced with the section-4.7.1 example given equations (working-tree debug build, branch compiler-unification, 2026-09-03; fixtures in the review scratchpad, reproduced inline below):

fixture to= Input eqn result
spec bare, fallback "Input" 0 exit 0; sub_model·input=0, sub_model·output=0, reader=0; warning bad_module_input_dst: module 'sub_model' input wiring target 'input' does not name an input of model 'sub_model'
spec bare, Stella placeholder "Input" {Enter equation for use when not hooked up to other models} exit 1; same warning + error in model 'Sub_Model' variable 'Input': empty_equation -- input + error compiling model 'main': SimulationError{not_simulatable: failed to compile fragments for variables: input, input}
Stella-qualified "Sub_Model.Input" 0 exit 0; sub_model·input=7, sub_model·output=14, reader=14; no warning

Fixture (the fallback row; the other two are one attribute / one equation away):

<?xml version="1.0" encoding="utf-8"?>
<xmile version="1.0" xmlns="http://docs.oasis-open.org/xmile/ns/XMILE/v1.0">
<header><name>spec_4_7_1</name><vendor>simlin</vendor><product version="1.0">simlin</product></header>
<sim_specs method="Euler"><start>1</start><stop>2</stop><dt>1</dt></sim_specs>
<model><variables>
<module name="Sub_Model">
  <connect to="Input" from=".Root_Model_Output"/>
</module>
<aux name="Root_Model_Output" access="output"><eqn>7</eqn></aux>
<aux name="reader"><eqn>Sub_Model.Output</eqn></aux>
</variables></model>
<model name="Sub_Model"><variables>
<aux name="Input" access="input"><eqn>0</eqn></aux>
<aux name="Output" access="output"><eqn>Input * 2</eqn></aux>
</variables></model>
</xmile>

The same on a real model: test/test-models/samples/bpowers-hares_and_lynxes_modules/model.xmile with each module's own connects rewritten to the bare form (to="area", to="lynxes" on hares; to="hare_density", to="size_of_one_time_lynx_harvest" on lynxes -- exactly the module blocks Stella 10.0.6 wrote, see below) is refused: four empty_equation errors (every input port carries the placeholder comment), four bad_module_input_dst warnings for the bare rows, not_simulatable, exit 1, no output. The unmodified qualified file runs and matches output.csv.

Corpus evidence (what the writers do)

  • Stella 10.0.6 wrote the spec's bare form. test/test-models/samples/bpowers-hares_and_lynxes_modules/model_legacy.stmx (<product version="10.0.6" lang="en">STELLA</product>, <vendor>isee systems, inc.</vendor>) records <connect to="area" from=".area" /> and <connect to="lynxes" from="lynxes.lynxes" /> on <module name="hares">, and <connect to="hare_density" from="hares.hare_density" />, <connect to="size_of_one_time_lynx_harvest" from=".size_of_one_time_lynx_harvest" /> on <module name="lynxes">. Its output.csv in the same directory is Stella's run of that model (README: model and output both "Stella 10.0.6 for Win", 8/28/15).
  • Stella Architect 1.4 and Stella Professional 1.9.4 write the module-qualified form. model.stmx (Stella Architect 1.4) and model.xmile (Stella Professional 1.9.4) in the same directory carry to="hares.area", to="lynxes.hare_density", etc. Every other <connect> in test/ is from Stella Professional/Enterprise and is qualified (test/modules_hares_and_foxes/, test/modules2/, test/modules_with_complex_idents/, test/ai-information/WithModulesAndArrays.stmx, test/land_model/). The only other bare to is <connect2 to="z_in_module" .../> in WithModulesAndArrays.stmx, an isee connect2 row the reader drops.
  • Why the bare form has never surfaced: model_legacy.stmx is referenced by no test, and the current reader cannot read its variables anyway -- it is the pre-1.0 draft format (level="3", xmlns="http://www.systemdynamics.org/XMILE", <module>/<aux> directly under <model> with no <variables> wrapper; xmile::Model.variables is Option<Variables>), so simlin simulate on it prints only time dt initial_time final_time. The 1.0-conformant bare form is therefore exercised by nothing in the repo.
  • Unverified: whether current Stella (Architect/Professional) READS the bare form. The README history is suggestive -- model.stmx was authored in Stella 10.0.6 (bare) and the checked-in model.stmx is a Stella Architect 1.4 re-save with the qualified form, so Architect 1.4 presumably read the bare file and rewrote it -- but that is an inference from file headers, not a checked behaviour. Vensim does not write <connect> at all in this corpus.

Where the current rule came from

PR #807 (commit 40c85bf, "Module editing: fix dangling/cyclic/mis-wired reference bugs found by adversarial audit") found the editor persisting a bare port name as dst (so every editor-wired input silently failed to connect), added qualifyDst/unqualifyDst in src/diagram/module-wiring.ts, added the BadModuleInputDst diagnostic, and stated the design choice:

dst is qualified in the editor, not bare. The canonical datamodel/XMILE representation is module·port; making the editor conform (rather than making the engine accept bare dst) keeps a single representation across import/export/edit.

"The canonical XMILE representation is module·port" is the premise that section 4.7.1 contradicts: module.port is Stella Professional's spelling, not the spec's. The single-internal-representation goal is sound; it was stated as a fact about XMILE rather than a fact about one writer, which is the class of error CLAUDE.md "Claims About Other Tools" describes -- reviewers checked the code against the claim, and the claim survived.

Why it matters

  • Conformance / correctness: a 1.0-conformant file using the spec's own spelling either simulates on port defaults with a misleading warning (the diagnostic asserts input is not an input of sub_model when it is) or, in the common Stella shape (placeholder-comment input ports), is refused outright.
  • The pin is inverted: bare_dst_warns guards the editor-bug shape and, as a side effect, forbids the spec's form. A future "make the engine accept the spec's example" change trips a test whose comment says the shape is a bug.
  • Documentation: port_of's rustdoc, src/simlin-engine/CLAUDE.md (module wiring bullets, "A reference's port is port_of(dst)"), src/diagram/module-wiring.ts's header comment, and PR Module editing: fix dangling/cyclic/mis-wired reference bugs found by adversarial audit #807 all state the qualified form as THE representation with no spec citation.

Is it a one-site change?

Accepting the bare form in port_of alone (return Some(dst) when dst contains no ·) does make identity (module_input_set), wiring (build_module_inputs) and the diagnostic agree by construction, because all three read port_of/bound_port. But it is NOT a one-site change overall:

  1. src/simlin-engine/src/patch.rs recognises only the {module}· prefix. The module self-rename reprefix (apply_rename_variable, "If the renamed variable is itself a module instance ... {old}·{port}") and retarget_parent_module_dst (port rename in the child model) both strip_prefix("{module}·") and skip a row that lacks it. A bare row that port_of now binds would be silently unwired by the first port rename -- the exact "wrong numbers, no error" class Module editing: fix dangling/cyclic/mis-wired reference bugs found by adversarial audit #807 fixed for the qualified form.
  2. The datamodel contract is stated as {module}·{port} in several places (patch.rs rustdoc, canonicalize_module_references, module-wiring.ts, CLAUDE.md), and the JSON/MCP/pysimlin upsert_module path already stores a bare dst verbatim (pinned by the patch.rs upsert test asserting references[0].dst == "input_var"), so a bare row can also enter without an XMILE file.

Two coherent designs:

  • (A) Bare is a valid internal spelling. port_of accepts it; patch.rs's two rename sites learn to match a bare dst (dst == old_port for a module whose target is the renamed model); bare_dst_warns flips to "a bare dst naming a real child input binds and is clean"; the "editor-bug shape" guard becomes the dangling-port check (dangling_dst_port_warns), which already exists. Simplest engine change; widens the internal contract.
  • (B) Qualify at the XMILE boundary; internal contract unchanged. impl From<Module> for datamodel::Module (xmile/model.rs) rewrites a to with no · to {module}·{to}; port_of, bound_port, patch.rs and bare_dst_warns stay as they are (the warning then genuinely means "a row the patch/JSON path left unqualified"). One reader site, but the reader must also decide to="a.b": if a is this module's own name it is Stella's spelling (bind b, today's behaviour); otherwise it is the spec's NESTED-submodel spelling ("qualified with the submodel's name"), which is LCA wiring the engine does not support (build_module_inputs binds only the instance's own ports) and should keep warning -- with a message that says so instead of "does not name an input". The XMILE writer question is separate: Simlin writes hares.area today (matches Stella Architect/Professional); whether to emit the spec's bare form instead depends on the unverified "does current Stella read bare" question above, so the writer should not change until that is checked.

Either way port_of's rustdoc and the CLAUDE.md bullets should carry the section-4.7.1 citation and name Stella's qualified form as a writer convention the engine also accepts, not as the XMILE representation.

Pins

  1. The spec's example through open_xmile (the fixture above, in module_wiring_tests / assemble_tests style): input_sets(.., "sub_model") contains input; no BadModuleInputDst; sub_model·output = 14 and reader = 14; and the placeholder-comment variant compiles (the bound port's fallback equation is never compiled). Today: 0 / 0 / warning, and refusal for the placeholder variant.
  2. Stella 10.0.6's spelling against Stella's own output: model.xmile's body with model_legacy.stmx's module blocks (the bare rows) run through corpus_tests! against test/test-models/samples/bpowers-hares_and_lynxes_modules/output.csv -- the only bare-to ground truth in the repo, currently unreachable because model_legacy.stmx is pre-1.0 draft XMILE.
  3. bare_dst_warns rewritten to the chosen contract (flipped under A; under B kept for a hand-built datamodel row and paired with a reader test showing the XMILE bare form arrives qualified), with a comment that cites 4.7.1 rather than "the editor-bug shape".
  4. The nested-submodel spelling (to="Inner.x" on a module whose model contains module Inner) pinned as a warning whose message names LCA wiring as unsupported, so the diagnostic's three arms (own port bare, own port qualified, nested port) each cite the sentence they implement.

Components

  • src/simlin-engine/src/db/assemble.rs: module_input_prefix, port_of, bound_port, build_module_inputs, module_input_set
  • src/simlin-engine/src/db/diagnostic.rs: model_module_wiring_diagnostics (and its "does not name an input" message)
  • src/simlin-engine/src/xmile/model.rs: impl From<Module> for datamodel::Module (reader), impl From<datamodel::Module> for Module (writer emits to_source_repr() of the stored dst)
  • src/simlin-engine/src/patch.rs: apply_rename_variable module reprefix, retarget_parent_module_dst, canonicalize_module_references
  • src/simlin-engine/src/db/module_wiring_tests.rs: bare_dst_warns, qualified_dst_to_real_port_is_clean, dangling_dst_port_warns
  • src/diagram/module-wiring.ts: qualifyDst/unqualifyDst header comment
  • Docs: src/simlin-engine/CLAUDE.md module wiring bullets; docs/design-plans/2026-08-25-compiler-unification.md V9a-2 (states "no corpus model carries an internal reference"; unaffected, but the same paragraph is where the settled bound-port rule is described)

Relationship to GH #1048

#1048 is the same-instance SOURCE question (<connect to="bridge.input" from="bridge.output"/>) and the two sibling shapes with a from inside the instance (cross-instance connect recorded on the source instance; module OUTPUT connect to=".Root_Model_Input"). This issue is the DESTINATION spelling of an ordinary input connect and is independent of #1048's answer: whichever reading #1048 settles on, the spec's to="Input" must bind Sub_Model's Input. The two should be fixed in one pass over model_module_wiring_diagnostics so each arm cites its 4.7.1 sentence, and #1048's "Related but distinct" paragraph points here.

Context

Identified while filing GH #1048 during the V9a adversarial review of PR #1040 (branch compiler-unification-v2); the #1048 agent flagged the bare to as a separate question and did not file it. The premise handed to this investigation ("Stella always qualifies the to, so the corpus never exercises the bare form") is corrected above: Stella 10.0.6 wrote the bare form and the corpus holds that file with Stella's output, but no test reads it and the reader cannot parse its draft-era container.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions