Skip to content

engine: DELAY N / SMOOTH N require a compile-time-constant order -- non-constant/parameterized order -> UnknownBuiltin (blocks full simulation of thyroid-2008-d and any dynamic-order delay/smooth) #562

@bpowers

Description

@bpowers

Problem

The Simlin engine's DELAY N / SMOOTH N support requires a compile-time-constant order argument. src/simlin-engine/src/builtins_visitor.rs::rewrite_alias_module_call (lines 84-127) selects the concrete stdlib model to instantiate by reading the value of the order argument via parse_module_order_arg (lines 74-82), which only accepts an Expr0::Const:

fn parse_module_order_arg(expr: &Expr0) -> Option<u32> {
    if let Expr0::Const(_, n, _) = expr {
        let rounded = n.round();
        if (*n - rounded).abs() < 1e-9 && rounded >= 0.0 {
            return Some(rounded as u32);
        }
    }
    None
}

rewrite_alias_module_call then maps the constant order to a fixed stdlib model:

let Some(order) = parse_module_order_arg(&order_expr) else {
    return eqn_err!(UnknownBuiltin, loc.start, loc.end);
};
let rewritten_name = match (func.as_str(), order) {
    ("delayn", 1) => "delay1",
    ("delayn", 3) => "delay3",
    ("smthn", 1) => "smth1",
    ("smthn", 3) => "smth3",
    _ => return eqn_err!(UnknownBuiltin, loc.start, loc.end),
};

If the order is any non-constant expression -- a variable, a macro port/parameter, MAX(1, Delay Order), etc. -- parse_module_order_arg returns None, rewrite_alias_module_call cannot resolve the call to a stdlib model, and the call surfaces as UnknownBuiltin. (Even a constant order other than 1 or 3 hits the _ => UnknownBuiltin arm, but the primary gap reported here is the non-constant case.)

Scope: this is a pre-existing, orthogonal engine limitation -- NOT a macro bug

This is not a macro defect and was not introduced by the Vensim macro support epic (docs/implementation-plans/2026-05-13-macros/). The identical failure occurs for a plain main-model equation with no macros involved:

x = DELAY N(in, dt, init, order)   // `order` a non-constant variable -> UnknownBuiltin

It only surfaced during the Phase 7 #554-follow-up fix (the delayn macro-collision fix, commit 029cf97f): test/metasd/thyroid-dynamics/thyroid-2008-d.mdl defines :MACRO: DELAYN(Input,DelayTime,Init,Order) ... DELAYN = DELAY N(Input,DelayTime,Init,Order) and invokes it as DELAYN(TSH, tau, TSH, MAX(1, Delay Order)), passing a non-constant order. The macro template body therefore cannot resolve DELAY N to a stdlib model and emits an in-body UnknownBuiltin.

The macro epic's Phase 7 metasd expansion tier correctly tolerates this: the resulting UnknownBuiltin is confined to the macro body and is non-macro-attributable, so thyroid-2008-d PASSES the expansion tier (macros.AC6.4 is satisfied). This limitation gates no macro acceptance criterion. It only blocks full simulation of thyroid (and of any model using DELAY N / SMOOTH N with a dynamic/parameterized order), and no metasd macro model is currently simulation-tier-eligible anyway (that broader gap is tracked separately under #561). This issue records the deferred engine-capability gap so it is not silently lost.

Why it matters

  • Builtin coverage / correctness: DELAY N / SMOOTH N with a variable or parameterized order is valid Vensim and appears in real-world SD models (and naturally in macros that take the order as a parameter, e.g. thyroid's DELAYN). Such models cannot simulate at all in Simlin today; the call dead-ends at UnknownBuiltin.
  • Test corpus reach: blocks full simulation of test/metasd/thyroid-dynamics/thyroid-2008-d.mdl (and any similar model), reducing end-to-end numeric validation reach.
  • Confusing signal: the importer accepts DELAY N / SMOOTH N, but a non-constant order yields a generic UnknownBuiltin on a function the engine otherwise recognizes, rather than a clear "dynamic-order delay unsupported" diagnostic.

Components affected

  • src/simlin-engine/src/builtins_visitor.rs lines 74-82 (parse_module_order_arg) and lines 84-127 (rewrite_alias_module_call) -- the compile-time constant-order-to-stdlib-model selection
  • src/simlin-engine/src/compiler/, src/simlin-engine/src/bytecode.rs, src/simlin-engine/src/vm.rs -- if remediated via a runtime ring-buffer / order-parameterized N-th order delay/smooth instead of compile-time delay1/delay3 (smth1/smth3) selection
  • stdlib (src/simlin-engine/stdlib/*.stmx) -- if remediated via an order-parameterized stdlib model

Recommended remediation (not to perform now)

Support a dynamic / order-parameterized N-th order delay and smooth rather than compile-time selection between delay1/delay3 (smth1/smth3) from a constant order argument. Two candidate approaches:

  1. A runtime ring-buffer / N-cascade: VM-level state for an order-N cascade of exponential stages where N is read at runtime (related in spirit to the ring-buffer approach proposed for engine: DELAY FIXED ring-buffer semantics blocks 4 simulation tests #346, though engine: DELAY FIXED ring-buffer semantics blocks 4 simulation tests #346 is a fixed pipeline delay -- a distinct semantic, do not conflate).
  2. An order-parameterized stdlib model that accepts the order as an input port and internally generalizes the delay1/delay3 structure.

In either case, also emit a clearer diagnostic than bare UnknownBuiltin when a dynamic-order delay/smooth cannot be lowered.

Where this is documented in-tree (so it is not silently lost)

  • src/simlin-engine/tests/metasd_macros.rs -- thyroid's SimTier skip reason annotates this blocker
  • The issue_554_followup_thyroid_shape_* test pins thyroid's shape/expansion behavior with this limitation present

Context

Identified during Phase 7 Task 2 of the Vensim macro support epic (docs/implementation-plans/2026-05-13-macros/), specifically while landing the #554-follow-up delayn macro-collision fix (commit 029cf97f). Out of scope for the macro epic; tracked here as a deferred general-engine capability gap.

Related: #561 (metasd macro simulation-tier enablement umbrella -- this is one of the per-model non-macro blockers it defers to individual tracking; cross-reference, do not duplicate), #554 (the delayn/INIT recursion false-positive follow-up that surfaced this), #560 (a sibling unrelated builtin-coverage gap: stochastic builtins), #346 (DELAY FIXED ring-buffer -- a different builtin and a different defect: fixed-pipeline-delay approximation, not dynamic order; distinct, cross-referenced only to disambiguate).

Metadata

Metadata

Assignees

No one assigned

    Labels

    compatCompatibility (XMILE, Vensim, etc) problem or enhancementengineIssues with the rust-based simulation engine

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions