Skip to content

engine: same-instance XMILE <connect> (from=bridge.output to=bridge.input) has no settled meaning; binds nothing + BadModuleInputSrc warning since V9a-2 #1048

Description

@bpowers

Summary

An XMILE <connect> recorded on a module instance whose from is inside that same instance's namespace and whose to names one of that instance's own ports has no settled meaning, in the spec or in the engine:

<module name="bridge" model_name="leaf">
  <connect to="bridge.input" from="bridge.output"/>
</module>
  • Before PR engine: one owner per compiler decision, LTM as a first-class consumer #1040 the engine accepted this at load and then panicked in Vm::new (vm.rs key_to_idx[&child_key], "no entry found for key"; simlin simulate aborted with exit 134). The instance's compilation identity (module_input_set, which ignored src) counted input as a bound port while its wiring (build_module_inputs, which skipped a src inside the namespace) wrote nothing: two owners of the bound-port rule that disagreed.
  • Since PR engine: one owner per compiler decision, LTM as a first-class consumer #1040 (commit bea5d79, design-doc divergence V9a-2) bound_port is the one owner: the model compiles, the reference binds nothing (input keeps its sub-model default of 2, so bridge·output = 3), and model_module_wiring_diagnostics emits a BadModuleInputSrc Warning naming the source ("... is inside the module's own namespace, so the wiring binds no input of model 'leaf' ...").

The current behaviour is a defensible placeholder (loud, not silently wrong), but it is not a rule anyone chose: nobody has established what the shape means. This issue is to settle the meaning and then either keep warn-and-bind-nothing as the documented rule or implement the meaning.

What the XMILE spec says (docs/reference/xmile-v1.0.html)

Section 4.7.1 "Modules", "Submodel input assignment": 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, 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)." Footnote 18 defines "re-use" (independently developed modules in different models, or the same module file used twice) and adds nothing about self-connection.

That LCA rule is the natural place a from spelled through an instance arises (<connect to="Sub.Inner.x" from="Sub.y"/> recorded at Sub's parent). Section 4.7.1 never mentions a connect whose from and to are both inside the SAME instance, so its meaning is unverified. Also unverified: whether Stella, Vensim, or any other XMILE writer ever emits this shape, and what it does with one on read. No corpus model contains it (the reviewer re-scanned 247 files / 40 <connect> elements; the only instance-qualified from values in the corpus are the cross-instance shape below).

Candidate readings, none verified:

  1. Malformed: a submodel cannot be its own input's source. Then today's rule (warn, bind nothing) is right and should be documented as the rule, with the citation.
  2. An algebraic identity at the module boundary (input := output inside the instance). For r3 that is a zero-lag cycle (output = input + 1, input = output), so the correct outcome would be the ordinary CircularDependency refusal rather than a warning plus a simulation on the port's default; with a stock on the path it would simulate. Then bound_port should bind the port with the src resolved inside the instance and let the cycle check decide.
  3. A writer-specific encoding of something else (a self-alias, an output export). Only a Stella/Vensim check can tell.

Sibling shapes with a from inside the instance's namespace (settle together)

The warning's gate is port_of(dst).is_some() && src.starts_with(prefix). Two neighbouring shapes are deliberately NOT given the BadModuleInputSrc warning and belong to the same reading of section 4.7.1:

  • to is another instance's port: <connect to="lynxes.hare_density" from="hares.hare_density"/> recorded under hares. This is how Stella (Stella Professional 1.9.4 in the corpus) records a module-to-module connect on the SOURCE instance; five corpus models carry it (test/modules_hares_and_foxes/modules_hares_and_foxes.stmx, test/modules2/modules2.xmile, test/ai-information/WithModulesAndArrays.stmx, test/modules_with_complex_idents/modules_with_complex_idents.stmx, test/test-models/samples/bpowers-hares_and_lynxes_modules/). port_of(dst) is None, bound_port binds nothing, and the dst arm reports it as BadModuleInputDst; the matching connect recorded under lynxes is the one that binds. Pinned by module_wiring_tests::a_cross_instance_reference_on_the_source_instance_is_a_dst_report_only.
  • to is a parent-scope variable: the spec's own example in section 4.7.1 records <connect to=".Root_Model_Input" from="Sub_Model.Output"/> on module Sub_Model, a module OUTPUT feeding the root model, spelled as a connect on the source instance. By code reading (not run): canonicalize(".Root_Model_Input") is ·root_model_input, port_of("sub_model·", ..) is None, so the dst arm warns BadModuleInputDst ("does not name an input of model ...") on the spec's own example. Whether the reader should recognise output-connects, and whether Stella's source-side records are the same idea, is part of the same question.

Related but distinct (may deserve its own issue): the spec example's input connect is <connect to="Input" from=".Root_Model_Output"/> with a BARE to. Section 4.7.1 says an unqualified to names the module's own input and qualification is for nested submodels, while module_wiring_tests::bare_dst_warns pins that a bare dst warns BadModuleInputDst as "the editor-bug shape". Stella qualifies (to="hares.area"), so the corpus never exercises the spec's form.

Why it matters

  • Correctness: under reading 2 the engine simulates on the port's default with only a warning, a wrong number for a modeller who meant the binding. Under reading 1 the behaviour is right but rests on an "unverified" comment, so the next change to bound_port can move it without anyone noticing.
  • The bound-port rule is one owner by design (V9a-2). Whatever the answer is, it belongs in bound_port's rustdoc and src/simlin-engine/CLAUDE.md as a standing rule with the spec citation, per the repo's rule that claims about other tools are checked facts or are marked unverified.

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
  • src/simlin-engine/src/xmile/model.rs: impl From<Module> for datamodel::Module (canonicalizes from/to verbatim; neither qualifies nor classifies a connect)
  • Pins: assemble_tests::internal_module_reference_is_not_a_bound_input, assemble_tests::an_xmile_internal_module_reference_compiles_and_binds_nothing, module_wiring_tests::internal_src_warns_that_it_binds_nothing, module_wiring_tests::a_cross_instance_reference_on_the_source_instance_is_a_dst_report_only
  • Docs: src/simlin-engine/CLAUDE.md (module wiring bullets), docs/design-plans/2026-08-25-compiler-unification.md (V9a-2)

Possible approaches

  1. Check the writers: open r3 in Stella (and a Vensim equivalent if one exists) and record what each does. If neither accepts the shape, adopt reading 1, keep warn-and-bind-nothing, and rewrite the bound_port rustdoc and the CLAUDE.md bullet from "unverified" to the settled rule with the citation.
  2. If a writer gives it a meaning, implement it: bound_port returns the port with the src resolved inside the instance, and the cycle check / simulation decides. r3 and r3b are the fixtures (both become CircularDependency under reading 2: r3b is input = output = input * gain + 1).
  3. Either way, decide the two sibling shapes in the same pass so the diagnostic's three arms (own port, other instance's port, parent variable) each cite the section-4.7.1 sentence they implement.

Repro

r3_internal_module_reference.xmile (the review's probe):

<?xml version="1.0" encoding="utf-8"?>
<xmile version="1.0" xmlns="http://docs.oasis-open.org/xmile/ns/XMILE/v1.0" xmlns:simlin="https://simlin.com/XMILE/v1.0"><header><name>probe</name><vendor>rv</vendor><product version="1.0">rv</product><options namespace="std"></options></header><sim_specs method="Euler" time_units="Month"><start>0</start><stop>6</stop><dt>1</dt></sim_specs><model name="main"><variables><module name="bridge" model_name="leaf"><connect to="bridge.input" from="bridge.output"/></module><aux name="reader"><eqn>bridge.output</eqn></aux></variables></model><model name="leaf"><variables><aux name="input"><eqn>2</eqn></aux><aux name="output"><eqn>input + 1</eqn></aux></variables></model></xmile>

Today: compiles; simlin simulate prints one BadModuleInputSrc warning; bridge·output = 3 for all t. Before PR #1040: abort, exit 134.

r3b_internal_and_bound.xmile adds <aux name="source"><eqn>5</eqn></aux>, a second reference <connect to="bridge.gain" from="source"/>, and leaf2 with gain and output = input * gain + 1: same outcome, one warning, gain binds.

Context

Identified during PR #1040 (branch compiler-unification-v2) by its V9a adversarial review, finding D2; moved from the panic to its current warn-and-bind-nothing form in commit bea5d79 ("engine: LTM reads typed values; one module-instance owner") and recorded as design-doc divergence V9a-2. The review's own words: "whether a same-instance connect means anything is unverified in the spec".

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