Conversation
Completed brainstorming session. Design includes: - Three stdlib modules (systems_rate, systems_conversion, systems_leak) for flow type encapsulation with remaining-chain pattern for sequential debiting - Two-stage parser (lexer + AST) for the systems text format - Module-based translator producing datamodel with correct priority ordering - Round-trip writer that reconstructs .txt from module structure - ALLOCATE BY PRIORITY as a first-class engine builtin via AST rewrite - Layout engine extension for module diagram generation - 7 implementation phases with acceptance criteria
Three new stdlib modules for the systems format flow types: - systems_rate: moves min(requested, min(available, dest_capacity)) - systems_leak: moves min(floor(available * rate), dest_capacity) - systems_conversion: drains source, adds floor(src * rate) to dest Each module is a pure-auxiliary XMILE model with input variables (available, rate/requested, dest_capacity) and output variables (actual/outflow, remaining, waste). Regenerated stdlib.gen.rs brings MODEL_NAMES from 6 to 9 entries. Includes 12 unit tests covering all specified test cases from the design plan, verifying numeric behavior via the incremental compiler and VM pipeline.
Defines the intermediate representation for the systems format parser: Expr (Int/Float/Ref/Inf/BinOp/Paren), FlowType (Rate/Conversion/Leak), SystemsStock, SystemsFlow, and SystemsModel. Module structure follows the MDL reader pattern with mod.rs + submodule files. Parser has a stub implementation that will be filled in the next commit.
Two-stage parser for the systems format (.txt files): 1. Lexer (lexer.rs): line-oriented tokenizer that classifies lines as comments, stock-only declarations, or flow lines. Parses stock refs (plain, parameterized, infinite), explicit flow type prefixes (Rate/Conversion/Leak, case-insensitive), and formula expressions via recursive descent (left-to-right, no operator precedence). 2. Parser (parser.rs): builds SystemsModel IR from lexer output with stock deduplication (later non-default values update defaults, conflicting non-defaults error), implicit flow type detection (Float -> Conversion, everything else -> Rate), and declaration order preservation for sequential debiting priority. 40 tests covering all acceptance criteria (AC1.1-AC1.11, AC7.2) plus all five example files from third_party/systems/examples/.
Implements Expr::to_equation_string() which converts the systems format AST to equation strings suitable for Equation::Scalar. The key feature is preserving left-to-right evaluation semantics by parenthesizing the left operand of a BinOp when it has lower precedence than the outer operator (e.g., a + b parsed as left operand of * emits "(a + b) * c"). References are canonicalized (lowercased, spaces to underscores), Inf maps to "inf()", and floats always include a decimal point.
… and chaining Translates SystemsModel IR into datamodel::Project by instantiating stdlib modules (systems_rate, systems_leak, systems_conversion) for each flow, creating actual transfer flows and waste flows, and wiring stocks with inflow/outflow lists. Multi-outflow stocks produce chained modules where each module's available input references the previous module's remaining output. Flows are processed in reversed declaration order so the last-declared flow gets highest priority. The open_systems() entry point in compat.rs provides the top-level parse-and-translate API with a default 10-round simulation.
Parse, translate, and compile all valid example files (hiring.txt, links.txt, maximums.txt, projects.txt, extended_syntax.txt) through the incremental pipeline. For hiring.txt, additionally verify that Candidates is infinite, rate/conversion/leak modules are selected correctly, and waste flows exist for conversion flows.
Python script reads example .txt files from third_party/systems/examples/, runs them through the Python systems package, and writes expected CSV output to test/systems-format/ for use by Rust integration tests.
Extract ensure_results into shared test_helpers module so both simulate.rs and simulate_systems.rs can use the same comparison logic. Add simulate_systems.rs with 5 integration tests that parse systems format files, translate to datamodel, and run both the interpreter and VM paths against expected CSV output from the Python systems package. Fix several translator bugs discovered by the integration tests: - systems_rate stdlib module: use MIN(dest_capacity, MAX(0, ...)) to match Python's Rate.calculate semantics (allows negative capacity for reverse flows, clamps negative requested to 0) - Deferred dest_capacity creation: account for destination stock outflows (capacity = max - stock + outflows) so simultaneous SD evaluation matches Python's sequential source-drain semantics - Rate formula reference rewriting: when a rate formula references a stock drained by earlier flows (in reversed declaration order), substitute the effective post-outflow value to match Python's intermediate state visibility - Module output chaining: create intermediate aux variables to bridge module outputs to module inputs, since ModuleReference bindings require outer model variables as sources
The translator uses rewrite_expr_to_equation() (translate.rs) to combine reference rewriting with equation string emission in one pass, which also correctly preserves dot-separated module reference strings (e.g. "module.remaining") without re-canonicalization. Expr::rewrite_refs was never called after that design was settled, leaving a parallel but subtly different implementation that would diverge from translate.rs over time. Remove it and its unused HashMap import.
The doc comment referenced `Expr::rewrite_refs` which no longer exists. Update it to reference `Expr::to_equation_string` instead, which is the actual method it contrasts with.
Add writer.rs that reconstructs the .txt format from a translated datamodel::Project by inspecting systems module structure, stripping synthesized variables (modules, waste flows, helper auxes, transfer flows, effective vars), and recovering original declaration order via chain walking. Key design decisions: - Reverse-rewrites _effective and _remaining references back to original stock names in rate expressions - Topological sort of flow groups based on _effective dependency analysis ensures the translator's drained_stocks logic produces identical rewrite decisions during round-trip - Explicit flow type syntax (Rate/Conversion/Leak) always emitted to avoid ambiguity from implicit type detection Also adds round-trip integration tests verifying that parse -> translate -> write -> parse -> translate -> simulate produces matching output for all 5 fixture files, plus a compat API test through open_systems/to_systems.
ALLOCATE BY PRIORITY(request, priority, size, width, supply) is a Vensim function that allocates a shared supply among competing requesters based on priority rankings. Previously, MDL import converted this to the XMILE ALLOCATE() form with complex argument reordering. Now the engine handles it natively. The implementation adds AllocateByPriority as a first-class BuiltinFn variant that flows through the full compilation pipeline (Expr0 -> Expr1 -> Expr2 -> Expr3 -> bytecode). At runtime (both VM and interpreter), it constructs rectangular priority profiles (ptype=1, ppriority=priority[i], pwidth=width, pextra=0) and delegates to the existing allocate_available algorithm. The MDL xmile_compat translator now emits allocate_by_priority() directly instead of rewriting to the complex ALLOCATE() form, and the MDL writer recognizes both the native and legacy forms for round-trip fidelity.
Add a writer test for the native XMILE allocate_by_priority(...) form (AC5.4), which was already handled by recognize_allocate but lacked test coverage. Update the engine CLAUDE.md to list AllocateByPriority alongside AllocateAvailable in the descriptions of builtins.rs, compiler/mod.rs, bytecode.rs, and vm.rs.
Add module_width (55.0) and module_height (45.0) fields matching existing rendering constants. Include validation clamps following the pattern for other dimension fields.
Remove the 9 module exclusion sites in layout/mod.rs that previously skipped Variable::Module during dependency graph construction, positioning, label placement, and validation. Modules now participate as first-class layout elements. Key changes: - LayoutEngine::new includes modules in display_names and UID seeding - apply_layout_positions maps Module positions from SFDP results - create_connectors includes modules in model_var_idents - apply_optimal_label_placement optimizes module label sides - apply_feedback_loop_curvature includes modules in uid_to_ident - validate_view_completeness checks for ViewElement::Module - compute_metadata records module UIDs, includes modules in all_idents, and builds dep_graph edges from module reference bindings - recalculate_bounds tracks module element extents - New create_missing_module_elements follows the aux pattern - Renamed unit test to test_generate_layout_includes_module_elements_and_connectors
…ation Extend verify_layout helper to include ViewElement::Module in the view_names set, coordinate positivity checks, and viewbox containment checks. Add test_systems_format_layout_with_modules which loads the hiring.txt systems format fixture, translates it, generates a layout, and verifies: - Every Variable::Module has a corresponding ViewElement::Module - All modules have finite coordinates from SFDP positioning - Full verify_layout validation passes including module elements
Systems format is a new parser/translator/writer subsystem in simlin-engine that handles line-oriented .txt stock-and-flow models. Layout engine now generates view elements for modules. New stdlib modules (systems_rate, systems_leak, systems_conversion) support the systems format flow types. ALLOCATE BY PRIORITY was also added as a new engine builtin during this branch.
Three issues addressed from code review: - Critical: rewrite_expr_to_equation in systems/translate.rs did not apply the needs_parens logic from Expr::to_equation_string. When a rate formula contained mixed-precedence operators (e.g. A + D * 2, which left-to-right parses as (A + D) * 2) and the source stock reference was rewritten to a chained remaining variable, the emitted equation omitted the required parentheses. Fix: introduce a local needs_parens_in_rewrite helper that applies the same precedence check, and promote BinOp::precedence to pub(super) so translate.rs can call it. Added a regression test verifying the parenthesized output. - Important: CLAUDE.md described tests/test_helpers.rs as providing open_systems_fixture() which does not exist. Updated the description to accurately reflect that the file provides ensure_results for CSV result comparison. - Minor: systems_rate.stmx used MAX(0, MIN(requested, available)) without explaining why MAX(0) is present. Added an XML comment documenting that this is a deliberate defensive guard against negative requested values and that it deviates from the original specification.
Include phase-by-phase implementation plans and test requirements document used during development of the systems format feature.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7dafe2b2ab
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if modules.is_empty() { | ||
| return Ok(String::new()); |
There was a problem hiding this comment.
Serialize stock-only models instead of returning empty output
When a translated systems project has no systems_* modules (for example, an input containing only stock declarations like A(10) or [Pool]), this early return emits an empty string and drops every stock declaration. That makes parse→translate→write lossy for a valid syntax class the parser supports (FlowNoRate/stock-only lines), and users will lose model content on export.
Useful? React with 👍 / 👎.
| let stock_idx: HashMap<String, usize> = stocks | ||
| .iter() | ||
| .enumerate() | ||
| .map(|(i, s)| (s.ident.clone(), i)) | ||
| .collect(); |
There was a problem hiding this comment.
Detect canonical stock-name collisions during translation
Translation canonicalizes stock names (e.g. A and a both become a) but builds this lookup map with collect() and no collision check, so later entries silently overwrite earlier ones. In mixed-case/whitespace-equivalent inputs this miswires inflow/outflow attachment and can yield duplicate variable idents with incorrect simulation behavior instead of a deterministic import error.
Useful? React with 👍 / 👎.
Code ReviewThis is a thorough, well-structured addition of systems format support. The parser, translator, stdlib module approach, ALLOCATE BY PRIORITY builtin, and layout changes are all implemented consistently across the full pipeline (AST lowering, codegen, VM, interpreter, compiler hoisting, patch, symbolic, MDL writer, pretty printer). FindingsNo blocking bugs found. The code is correct. [P3] Writer The Overall Correctness VerdictCorrect. The patch is free of bugs that would break existing code or tests. The ALLOCATE BY PRIORITY additions are consistent across all compilation stages (AST, codegen, VM, interpreter, symbolic, patch). The systems format pipeline (lexer -> parser -> translator -> writer) is well-tested with integration tests and round-trip verification. The layout engine module support additions follow existing patterns for other element types. No security, performance, or correctness issues found. |
Code ReviewReviewed 54 changed files across 19 commits covering: systems format parser/lexer/translator/writer, ALLOCATE BY PRIORITY as native engine builtin, module layout support, and stdlib modules. Areas Reviewed in Detail
FindingsNo bugs found. The implementation is thorough, well-tested, and correct. Verdict: CorrectThe patch introduces no regressions to existing code or tests. The new functionality is well-structured with comprehensive test coverage across unit tests, integration tests, and round-trip tests. |
The translator used a single _effective variable per stock that subtracted ALL outflows, but cross-stock rate references should only see the outflows processed so far in reversed declaration order. For example, in a model with A>B@1, C>D@A, A>E@8, the flow C>D should see A after only A>E drains it (a-a_to_e), not after both outflows (a-a_to_e-a_to_b). Replace the monolithic _effective approach with incremental drain variables (_drained_N) that accumulate outflows as they are processed. Pre-compute the drain state at each flow's processing point and share it between rate and capacity equation generation. Also fix an inaccurate comment in the lexer about flow arrow whitespace, add a FlowNoRate parser test, and document why conversion remaining is 0.
Code ReviewThis is a well-structured PR that adds systems format support through a clean translation-to-stdlib-modules approach, plus a native After thorough review of all 54 changed files, I found no blocking bugs. The implementation is consistent across all compilation paths (codegen, VM, interpreter), the view stack management is correct, and the test coverage is comprehensive. Minor observations (non-blocking)
Overall correctness: CORRECT The patch is well-tested (5 simulation integration tests against Python reference CSVs, 6 round-trip tests, ALLOCATE BY PRIORITY cross-validation between VM and interpreter), and all code paths are consistently implemented. The stdlib module approach avoids engine-level special-casing, and the writer's chain-walking + topological sort correctly recovers declaration order for round-trips. |
Fix three writer issues found during code review: - Stock-only models (no flows/modules) now emit stock declarations instead of silently producing empty output. - find_stock_max uses exact identifier matching to prevent prefix collisions (e.g., stock "a" matching within " - ab"). - Rate equations containing inf() are normalized to bare "inf" to match the systems parser's expected syntax.
Code Review54 files changed, ~6000 lines — systems format parser/translator/writer, ALLOCATE BY PRIORITY builtin, module diagram support. This is a well-structured PR with comprehensive test coverage (5 integration tests matching Python reference CSVs, 6 round-trip tests, VM/interpreter allocation tests, 207 layout tests). The stdlib module approach for flow types is elegant — it avoids special-case engine code while letting simlin's existing topological sort handle evaluation order. After thorough review of the parser, lexer, translator, writer, VM, interpreter, codegen, layout, and supporting changes, I found no blocking bugs. A few observations below: Observations (non-blocking)1. [P3] else if let Some(pos) = token.find("_drained_") {
let stock_name = &token[..pos];
2. [P3] Size parameter intentionally unused in ALLOCATE BY PRIORITY — The VerdictOverall correctness: Correct. The patch is well-tested, the translation logic is sound, and the round-trip tests provide strong confidence that parse→translate→write→re-parse→simulate produces consistent results. No bugs that would break existing code or tests. 🤖 Generated with Claude Code |
The dest_capacity equation was counting ALL outflows from the destination stock, but in the Python sequential model only outflows already processed (higher flow_idx in reversed declaration order) have freed capacity. Unprocessed outflows inflated the capacity estimate, allowing excessive transfers into capped stocks. Fix by building a flow-ident-to-index map and filtering destination outflows to only include those with higher flow_idx than the current flow. Also reject Leak/Conversion flows from infinite sources with a clear error, matching the Python systems package's IllegalSourceStock behavior.
Code Review[P2] Negative
|
The writer's find_stock_max used find() to locate the " - {stock}"
separator in capacity equations, but the translator always appends this
separator at the end. When the max expression itself references the
destination stock (e.g., max = A - B producing "a - b - b"), the first
match was inside the max expression, truncating it to just "a".
Fix by using rfind() to match the last occurrence, which is always the
translator-generated separator.
Code ReviewI reviewed the full PR (54 files, ~8700 lines) covering:
FindingsNo bugs found that meet the review criteria. Key areas verified:
Overall Correctness VerdictCorrect. The patch is well-structured with comprehensive test coverage including integration tests against Python reference CSVs, round-trip tests, and VM/interpreter equivalence checks. No bugs or blocking issues found. |
The Leak/Conversion from infinite source validation only checked source_ref.is_infinite (the current line's bracket syntax), missing cases where the stock was declared infinite on an earlier line. Fix by checking the resolved stock's is_infinite state from the deduplicated stock table after ensure_stock has processed all declarations.
Review cycle summaryFive iterations of automated code review (codex + claude) identified and resolved several correctness issues in the systems format translator and writer: Cross-stock drain semantics (P1): The translator used a monolithic Destination capacity ordering (P1): The Writer robustness (3 fixes): Stock-only models no longer produce empty output. Input validation: Leak/Conversion flows from infinite sources are now rejected (matching Python's All fixes follow TDD: failing test first, then minimal code change. Total: 5 review iterations, 4 fix commits, all 2800+ engine tests passing. |
Code Review: engine: add systems format supportSummaryThis is a large, well-structured PR that adds native support for Will Larson's FindingsNo bugs found that meet the flagging criteria. The code is thoroughly tested with unit tests, integration tests against Python reference CSVs, and round-trip tests. The Key observations on correctness:
Overall Correctness VerdictCorrect. The patch is free of bugs and blocking issues. Existing code and tests will not break. The implementation is consistent with the Python |
Test model verifying destination capacity ordering: stock b has capacity 5 and drains to c, while a drains to b at rate 10. This exercises the priority ordering logic when destination stocks have limited capacity.
Code ReviewLarge, well-structured PR adding systems format support and ALLOCATE BY PRIORITY as a native builtin. Reviewed the parser pipeline (lexer → parser → AST → translator), the writer/round-trip logic, the ALLOCATE BY PRIORITY implementation across all compilation stages (builtins, AST lowering, codegen, VM, interpreter), stdlib module semantics, and layout changes for module rendering. FindingsNo bugs found. Notes
VerdictCorrect. The patch is well-tested (5 simulation integration tests against Python reference CSVs, 6 round-trip tests, ALLOCATE BY PRIORITY unit tests) and the implementation follows established patterns throughout the codebase. No existing code or tests should break. |
Three correctness fixes from code review: 1. Translator: multiple flows between the same source and destination now produce disambiguated identifiers (e.g. a_outflows_b, a_outflows_b_2). Previously both would get "a_outflows_b", causing the second flow's module and transfer variables to overwrite the first. 2. Writer: per-flow topological sort replaces per-group sort. When flows from different sources need interleaving for correct drain semantics (e.g. A>X, B>Y, A>Z where A>X sees drained B but A>Z sees raw B), the writer now correctly interleaves them instead of grouping all same-source flows together. 3. Writer: O(n^2) cycle fallback in topological sort replaced with HashSet-based O(n) lookup. Also adds a clarifying comment about the ignored _size parameter in AllocateByPriority codegen.
Code Review[P2] Parser uses case-sensitive stock dedup but translator canonicalizes to lowercaseFile: The parser's Overall Correctness Verdict: CorrectThe patch is well-structured and thoroughly tested. The parser, translator, writer, ALLOCATE BY PRIORITY builtin, layout changes, and compiler pipeline additions are all internally consistent. The one finding above is a P2 edge case requiring inconsistent casing of stock names in the same file — unlikely in practice and not blocking. The 5 simulation integration tests against Python reference CSVs, 6 round-trip tests, and comprehensive unit tests provide strong confidence in correctness. |
Two fixes from the second review iteration: 1. stdlib systems_conversion module: when dest_capacity is finite, the module now only consumes the source units needed to fill the capacity (max_src_change = floor(dest_capacity / rate)), matching the Python reference. Previously it drained the entire source stock regardless of the cap, producing wrong waste and remaining values. 2. Parser validation: reject Leak/Conversion from stocks with infinite initial value (e.g. A(inf)), not just bracket-declared infinite stocks ([A]). The Python reference raises IllegalSourceStock for any source whose initial value is infinite. Also deduplicates the pair-count suffix logic in the translator by pre-computing flow_suffixes once and reusing in both the main loop and the drain state computation.
Code Review[P2]
|
Review Cycle SummaryThree iterations of automated code review (Codex + Claude) identified and resolved the following issues: Iteration 1 addressed two correctness bugs and two code quality improvements:
Iteration 2 fixed a simulation semantics bug and a validation gap:
Iteration 3 produced no actionable feedback from either reviewer. Codex raised theoretical edge cases (case-sensitive identifiers, cyclic dependency ordering) that either conflict with the engine's core design invariants or represent inherently unsolvable constraint conflicts. |
Summary
.txtsystems format files, producing aSystemsModelIR with stocks, flows (Rate/Conversion/Leak), and left-to-right formula expressionsSystemsModeltodatamodel::Projectusing stdlib module instantiation (systems_rate,systems_leak,systems_conversion), with multi-outflow chaining, waste flow generation, and order-dependent rate rewriting for sequential-drain semantics.txtformat from datamodel via module inspection, chain walking for declaration order recovery, and topological sort for flow group orderingsystemspackage reference CSVsVariable::Modulein diagram generation with SFDP positioning54 files changed, 6075 insertions, 206 deletions across 19 commits.
Test plan
docs/test-plans/2026-03-18-systems-format.md