Status: reference document for designing and implementing Vensim macro support in
simlin-engine. The engine currently parses :MACRO: blocks but discards them
(src/simlin-engine/src/mdl/convert/mod.rs:248). This document collects what a
Vensim macro is, how it is written, and -- most importantly -- its precise
runtime semantics, so an implementation design can be grounded in fact rather
than guesswork.
Authoritative sources used (cited inline throughout):
- Vensim documentation, "Macros": https://www.vensim.com/documentation/macros.html
- Vensim documentation, "Defining Macros": https://www.vensim.com/documentation/22145.html
- Vensim documentation, "Using Macros": https://www.vensim.com/documentation/22150.html
- Vensim documentation, "Function and Language Changes": https://www.vensim.com/documentation/function_changes.html
- Vensim documentation, version 5.8 release notes: https://www.vensim.com/documentation/version_5_8___-___.html
- XMILE v1.0 OASIS specification (local copy:
docs/reference/xmile-v1.0.html, saved from https://docs.oasis-open.org/xmile/xmile/v1.0/xmile-v1.0.html) -- sections 2.2.1, 2.10, 3.6/3.6.1/3.6.2, and 4.8/4.8.1/4.8.2/4.8.3. - Local repository: the six
test/test-models/tests/macro_*fixtures, the C-LEARN hero model, the engine MDL parser (src/simlin-engine/src/mdl/), the XMILE serde layer (src/simlin-engine/src/xmile/mod.rs). xmutil, Bob Eberlein's C++ Vensim-to-XMILE converter (https://github.com/bobeberlein/xmutil). File references of the formxmutil:Vensim/VYacc.y:99are paths under itssrc/directory; the line numbers are approximate, since that tree moves.
A note on terminology: throughout, "the model" or "the whole-model" means the top-level model that invokes a macro, as distinguished from the contents of the macro definition itself.
A Vensim macro is a named, parameterized fragment of model structure -- one or more equations, optionally including stocks, that is defined once and then "called like a function" from anywhere in the model. Per the Vensim "Macros" overview, macros exist so that modelers can "repeat model structures without retyping equations" (https://www.vensim.com/documentation/macros.html).
Conceptually a macro is closer to a textual / structural template that is expanded inline at each call site than to a true runtime function:
- "When the macro is encountered, variables and equations will be made up so that causal tracing will continue to function properly" (https://www.vensim.com/documentation/22150.html). That is, calling a macro materializes new model variables -- it does not perform an opaque function call.
- "When Vensim expands the macro to create the detailed equations it does the expansion recursively" for nested macros (https://www.vensim.com/documentation/22145.html).
- The XMILE spec describes the same idea from the encoding side: macros with variables "are, in fact, independent models that run to completion each time they are invoked" (XMILE 1.0 section 3.6.1).
How a macro differs from a regular variable: a regular variable has exactly one value (or one arrayed value) in the model namespace. A macro has no value of its own; it is invoked, and each invocation produces a fresh set of expanded variables that do have values.
How a macro differs from a built-in function: a built-in function (e.g. MIN,
ABS) is implemented by the engine and is stateless from the model's point of
view. A macro is defined in the model's own language and can carry state
(stocks). In fact several Vensim "functions" are themselves macros under the
hood: DELAY1, DELAY1I, DELAY3, DELAY3I, DELAYP, FORECAST, NPV,
NPVE, SMOOTH, SMOOTH3, SMOOTH3I, and TREND are all "defined as Macros"
(https://www.vensim.com/documentation/macros.html). This is the key reason a
macro can introduce a hidden stock: the same mechanism that lets SMOOTH
introduce a hidden level is the macro mechanism.
Why modelers use them: reuse of structure, and the ability to package a behavior (a custom delay, a custom smoothing, a custom NPV variant) as a callable unit. Why the Vensim docs are ambivalent about them: macros "are also dangerous in that they can allow dynamics to 'creep into' a model," and the documentation explicitly recommends using them "sparingly, if at all" (https://www.vensim.com/documentation/macros.html). The use of output arguments in particular is called out as "discouraged" (https://www.vensim.com/documentation/22145.html).
Caveat on Vensim editions: defining macros, the $ model-variable escape, and
nested/recursive macro expansion are Pro/DSS features. PLE and PLE Plus cannot
define macros, and cannot even display the hidden variables that built-in
macro-functions create
(https://www.vensim.com/documentation/macros.html,
https://www.vensim.com/documentation/function_changes.html).
A macro definition is introduced by a :MACRO: line and closed by a
:END OF MACRO: line. The header has the form
(https://www.vensim.com/documentation/22145.html):
:MACRO: macroname(inarg1, inarg2, inarg3 : outarg1, outarg2)
-
macroname-- "any valid unquoted Vensim name": it must start with a letter and may contain letters, numbers, spaces, underbars (_) and dollar signs ($). Because Vensim names may contain spaces, a macro can be namedEXPRESSION MACROorRAMP FROM TO(both appear in the local fixtures and the C-LEARN model). A macro name may shadow a built-in function name -- the C-LEARN model defines a macro literally namedSSHAPE, which is also a Vensim built-in; the engine's parser already accommodates this by accepting either aSymbolor aFunctiontoken for the macro name (src/simlin-engine/src/mdl/parser.rs:521-523). -
Input argument list -- "any number" of arguments, each "any valid unquoted name." Arguments are positional. "The arguments cannot be subscripted" in the definition, "but you can use subscripted values when calling the macro" (https://www.vensim.com/documentation/22145.html). Input arguments are separated by commas.
-
The
:separator and output argument list -- the:character (when present) separates the input argument list (before the colon) from the output argument list (after the colon). Output arguments "are variables that are defined inside of the macro, in addition to the output of the macro itself. These are optional and must be separated from input arguments by a colon. Their use is discouraged. When no outputs are specified, omit the colon" (https://www.vensim.com/documentation/22145.html).Concretely, the
add3example from the Vensim "Using Macros" page declares two extra outputs::MACRO: add3(val1, val2, val3 : minval, maxval) add3 = val1 + val2 + val3 ~ val1 ~ | minval = min(val1, min(val2, val3)) ~ val1 ~ | maxval = max(val1, max(val2, val3)) ~ val1 ~ | :END OF MACRO:(https://www.vensim.com/documentation/22150.html -- the page's own example has typos, repeating
val2; the corrected intent is shown above.)
Even with no output list, every macro has exactly one "output of the macro itself": the variable whose name equals the macro name. "The macroname and output arguments each must have a single equation inside of the macro description" (https://www.vensim.com/documentation/22145.html).
So in:
:MACRO: EXPRESSION MACRO(input, parameter)
EXPRESSION MACRO = input * parameter
~ input
~ tests basic macro containing no stocks and having no output
|
:END OF MACRO:
(from test/test-models/tests/macro_expression/test_macro_expression.mdl) the
equation EXPRESSION MACRO = input * parameter defines the macro's return
value. When there is no output list, the macro name is the output -- that is
what "When no outputs are specified, omit the colon" means: there is still an
output, it is just the macro-named variable and nothing else.
When there is an output list, the macro still has its macro-named output
plus each named output variable. In the add3 example above, add3 is the
primary output and minval/maxval are additional outputs that the caller can
also retrieve (see section 4.5).
- Input and output argument names are "any valid unquoted name" (https://www.vensim.com/documentation/22145.html).
- Argument names are local to the macro (see section 5.4). The Vensim docs
note you "can use, for example,
L1in as many macros as you like" (https://www.vensim.com/documentation/22150.html). - Arguments are bound positionally at the call site (section 5.2).
- Argument names cannot carry subscripts in the header (https://www.vensim.com/documentation/22145.html).
-
A macro definition is a top-level block in the
.mdlfile's equation section, peer to ordinary variable equations and group (****) markers. The engine models this directly: the MDL reader producesMdlItem::Macroas a sibling ofMdlItem::EquationandMdlItem::Group(src/simlin-engine/src/mdl/ast.rs:498-506). -
Ordering rule. "The macro definition must occur before the macro is referenced. This is the only equation ordering rule in the Vensim modeling language. If you do not define a macro before you use it, you will receive a syntax error message" (https://www.vensim.com/documentation/22145.html, and restated at https://www.vensim.com/documentation/macros.html). This is significant: Vensim equations are otherwise order-independent, but macro definitions must lexically precede their uses.
The local fixture
test/test-models/tests/macro_trailing_definition/is a deliberate counter-example: it places the:MACRO:block after themacro output = EXPRESSION MACRO(...)call that uses it. Its README still calls it a valid test and it has anoutput.tab, which means the third-party tools that produced the fixture (and the SD community's expectations) tolerate a trailing definition even though the Vensim manual forbids it. Treat the "definition before use" rule as Vensim's stated contract, but be aware real-world files and other tools may not enforce it, and the engine should decide deliberately whether to be strict or lenient. -
Multiple macros. A file may define any number of macros. The fixtures
macro_multi_macros/(two independent macros) andmacro_cross_reference/(one macro calls another) both exercise this. Each:MACRO: ... :END OF MACRO:is a separate block. -
Scope. A macro definition introduces its own private namespace. Names defined inside one macro do not leak into the model or into other macros (see section 5.4). The macro name itself, however, becomes visible in the model namespace so it can be called.
:END OF MACRO: closes the most recently opened :MACRO: block. There is no
nesting of :MACRO: ... :END OF MACRO: blocks themselves -- nesting of macros
is achieved by calling one macro from inside another (section 5.5), not by
lexically embedding one definition inside another. The engine treats an
unmatched :END OF MACRO: and an end-of-file inside an open macro as errors
(ReaderError::UnmatchedMacroEnd, ReaderError::EofInsideMacro in
src/simlin-engine/src/mdl/reader.rs:27-30).
Between :MACRO: and :END OF MACRO: the body is a sequence of ordinary
Vensim equations, written in the normal name = rhs ~ units ~ comment |
format. From the Vensim docs and the local fixtures, the body may contain:
-
Auxiliaries / intermediate variables. "Variables can also be defined internal to the macro description" beyond the required macro-named and output-named equations (https://www.vensim.com/documentation/22145.html). The XMILE spec adds that auxiliaries inside a macro are "useful to simplify the equation into smaller, meaningful pieces" (XMILE 1.0 section 4.8.2). The
macro_multi_expression/fixture has a body with anintermediatevariable; C-LEARN'sRAMP FROM TOmacro body has six internal auxiliaries (linear,linear ramp,exp ramp,slope,rate,interval). -
Constants. A body equation can be a literal constant.
-
Stocks (
INTEG). A macro body may containINTEG(...)equations. Themacro_stock/fixture's macro isEXPRESSION MACRO = INTEG(input, parameter). C-LEARN'sSAMPLE UNTILmacro containsSAMPLE UNTIL = INTEG(..., initval). The XMILE spec's SMOOTH1 macro example is built from a stock and a flow (XMILE 1.0 section 4.8.2). Per-invocation state of these stocks is the single most important semantic point and is covered in section 5.1. -
Lookups / table functions. The XMILE spec does not prohibit them, and the body is "all of the syntax of XMILE" (XMILE 1.0 section 3.6.1) / Vensim. None of the local fixtures exercises a lookup inside a macro, so there is no local ground-truth output for this case -- flag it as something to test against Vensim directly if the implementation needs to support it.
-
Subscripted / arrayed variables. Vensim is explicit and restrictive here: "There is no support for Subscripts within Macro definitions. When a Macro is called, the variables should be used with the same subscripts they appear with normally. When the macro equation is expanded, the created variables inherit the Subscripts from the left hand side of the equation. Each equation created during the expansion of the Macro has the same subscripts on the left hand side" (https://www.vensim.com/documentation/macros.html). In other words, the macro body is written as if scalar; subscripts are supplied entirely from the call-site LHS and propagated onto every expanded equation. XMILE, by contrast, permits arrays inside macros and even shows a recursive
FINDmacro indexing into a 1-D array argument withA[i](XMILE 1.0 section 4.8.1) -- so the two formats differ on array handling inside macros. See section 5.6. -
References to simulation control. The macro body cannot name
TIME STEP,INITIAL TIME,FINAL TIME, orTIMEdirectly as ordinary identifiers (those would be treated as local, undefined names). It reaches them via the$escape (section 3.1). C-LEARN'sSAMPLE UNTILmacro usesTIME STEP$;RAMP FROM TOusesTime$andTIME STEP$;INITusesINITIAL(x).
By default, no -- the macro body is a "self-contained world that can be accessed only through the macro call itself" (https://www.vensim.com/documentation/22145.html). There is exactly one documented exception:
"The one exception to the local nature of macro variables is that you may specify a model variable by following its name with a dollar sign
$. For example you can useTIME STEP$inside of a macro to refer to the model variableTIME STEP. Only unsubscripted variables may be referred to in this manner." (https://www.vensim.com/documentation/22145.html)
So Time$, TIME STEP$, INITIAL TIME$, etc. inside a macro body refer to
the model's (the caller's) corresponding variable. This is a Pro/DSS-only
feature (https://www.vensim.com/documentation/function_changes.html). The $
escape is restricted to unsubscripted model variables.
This $-suffixed reference is a real, load-bearing piece of syntax that the
engine's MDL lexer/parser must handle for the C-LEARN hero model -- e.g.
SAMPLE UNTIL = INTEG( (1-STEP(1,lastTime))*(input-SAMPLE UNTIL)/TIME STEP$, initval). The implementation must decide how a macro-body $ reference maps
onto whatever representation a macro becomes (e.g. an implicit module input
wired to the caller's time_step).
Units in a macro body equation may be either ordinary model units (People,
$, etc.) or the name of an input argument. "If the units are the name of an
input variable, the units from the input will be substituted wherever the macro
is invoked" (https://www.vensim.com/documentation/22145.html). A name like
Time$ may also be used as a units specifier. Note in every local fixture the
macro-named equation's units are written as the name of the first input
argument (~ input), which is exactly this "units = input argument name"
convention -- the macro's output unit is whatever unit the actual first
argument had at the call site.
Once defined, a macro "can be used any number of times. To use it, call it just like a function" (https://www.vensim.com/documentation/22150.html). The call appears on the right-hand side of an ordinary equation:
macro output = EXPRESSION MACRO(macro input, macro parameter)
(from test/test-models/tests/macro_expression/test_macro_expression.mdl). The
syntax is MACRONAME(arg1, arg2, ...) -- syntactically indistinguishable from a
function call or a lookup invocation. The engine's MDL parser does not even
recognize it as a macro call at parse time: a multi-argument call on a Symbol
token is parsed as Expr::App with CallKind::Symbol and described in the AST
as "unknown function (could be macro or error)"
(src/simlin-engine/src/mdl/ast.rs:97,106,127). Resolution of "is this name a
macro?" therefore has to happen after parsing, against the table of collected
macro definitions.
Per the Vensim docs the call is "just like a function" and the local + C-LEARN fixtures show:
- Simple identifiers --
EXPRESSION MACRO(macro input, macro parameter)(allmacro_*fixtures). - Subscripted variables --
SMOOTH(tadum[house], 20)in the Vensim "Using Macros" page (https://www.vensim.com/documentation/22150.html). Vensim explicitly says "you can use subscripted values when calling the macro" (https://www.vensim.com/documentation/22145.html). - Numeric literals -- the
20inSMOOTH(tadum[house], 20)and the literals passed to C-LEARN's built-inDELAY/STEP/RAMPmacro-functions. - Arbitrary sub-expressions -- C-LEARN's macro-function calls receive
compound expressions (e.g.
STEP(1, lastTime)is itself nested inside the macro body, but at the model level functions-implemented-as-macros such asSMOOTH/DELAY3routinely receive full expressions as arguments). The MDL grammar both inxmutiland in the engine parses macro-call arguments as a generalexprlist(xmutil:Vensim/VYacc.y:99,src/simlin-engine/src/mdl/parser.rs:528), so syntactically any expression is accepted.
The engine's AST documentation flags the tension: the grammar "parses macro
arguments as exprlist, which allows arbitrary expressions. In valid macros,
these should be simple variable references"
(src/simlin-engine/src/mdl/ast.rs:478-482). That comment is about the
definition header (where args must be plain names); at the call site,
expression-valued arguments are normal and expected (think SMOOTH(a + b, t)).
This is an important asymmetry: header argument list = plain names only;
call-site argument list = arbitrary expressions.
Yes. The Vensim "Using Macros" page's first example is
diddle[house] = daddle + SMOOTH(tadum[house], 20) -- the SMOOTH macro call
is one operand of a + (https://www.vensim.com/documentation/22150.html). Since
SMOOTH is itself a macro, this is direct evidence that a macro invocation can
appear as a sub-expression of a larger RHS, not only as the entire RHS.
The local fixtures, by contrast, only ever use a macro call as the entire
RHS of macro output = MACRONAME(...). So the engine's existing fixtures do
not cover the nested-in-expression case, even though Vensim clearly supports it.
Yes -- "you can use it any number of times" (https://www.vensim.com/documentation/22150.html). Each use is an independent expansion (section 5.1). The local fixtures invoke each macro only once; multiple-invocation behavior is the headline correctness concern for any implementation and is not directly pinned by a local fixture.
When a macro declares output arguments, the call site uses the same :
separator to bind them. From the Vensim "Using Macros" page
(https://www.vensim.com/documentation/22150.html):
total reserve = add3(water reserve lakes, water reserve rivers,
water reserve reservoir : min reserve, max reserve)
~ m*m*m
~ |
is water low flag = if then else(min reserve < LOW WARNING LEVEL, 1, 0)
~ DMNL
~ |
Here:
- The LHS variable
total reservereceives the macro's primary output (add3). - The names after the
:in the call (min reserve,max reserve) are bound by the caller to the macro's output arguments (minval,maxval). They become real model variables and can be referenced in subsequent equations (is water low flag = ... min reserve ...).
So the output-list mechanism at the call site is effectively multiple-value return by named binding: the input list yields the macro-named output assigned to the LHS, and the colon-separated output list yields additional model variables named by the caller.
Both xmutil and the engine's current MDL parser handle only the no-output
form of the macro header and call. Specifically:
-
xmutil's grammar rule for a macro header isVPTT_macro VPTT_symbol '(' exprlist ')'(xmutil:Vensim/VYacc.y:99), andexprlistonly permits,and;as separators (VYacc.y:203-207). There is no production that consumes a:between an input list and an output list.xmutiltherefore supports only macros whose sole output is the macro-named variable. -
The engine's MDL parser does the same:
parse_expr_list(src/simlin-engine/src/mdl/parser.rs:1361-1399) continues only onCommaorSemicolonand stops at aColon. The macro-header parse then callsexpect(TokenKind::RParen, "')'")(src/simlin-engine/src/mdl/parser.rs:530), so a header like:MACRO: add3(a, b : c)would fail with an "expected ')'" error at the:. (The reader's raw-token scan tracks parentheses and would read through a:--src/simlin-engine/src/mdl/reader.rs:211-227-- but the structured parser sub-step rejects it.)The engine's lexer does have a
Colontoken (src/simlin-engine/src/mdl/lexer.rs:44), so adding output-list support is a parser change, not a lexer change.
Implication: macros with explicit output lists are (a) discouraged by Vensim, (b) absent from every local fixture, and (c) unparseable by both converters today. An implementation must decide whether to support them at all; if it does, parser work is required on both the header and the call-site grammar.
This is the section that most directly governs the implementation design.
Each invocation of a macro that contains a stock gets its own independent
stock state, and that state persists for the whole simulation. This is stated
unambiguously by the XMILE spec and demonstrated by the local macro_stock
fixture.
XMILE 1.0 section 4.8.2, verbatim:
"Note that any stocks that are defined within a macro MUST have their own instances for each use of that macro and these instances MUST persist across the length of the simulation. That is to say, if this SMOOTH1 function is used five times in a model, there must also be five copies of the stock Smooth_of_Input, one for each use. Use of flows and auxiliaries do not require this..."
And XMILE 1.0 section 3.6.1: macros with variables "are, in fact, independent models that run to completion each time they are invoked."
The Vensim side corroborates this: the whole point of SMOOTH, DELAY3, etc.
being macros is that every call site of SMOOTH(...) gets its own hidden level
-- two SMOOTH calls in a model do not share a smoothing stock. The
hash-delimited expanded variable names (section 6 / section 7) exist precisely
so that each call site's expanded stock is a distinct, uniquely-named model
variable.
The local fixture test/test-models/tests/macro_stock/ pins the runtime
behavior of a single stock-bearing macro invocation. Its macro is:
:MACRO: EXPRESSION MACRO(input, parameter)
EXPRESSION MACRO = INTEG(input, parameter)
:END OF MACRO:
invoked as macro output = EXPRESSION MACRO(macro input, macro parameter) with
macro input = 5, macro parameter = 1.1, TIME STEP = 1, time 0..10. The
expected output.tab shows macro output taking values
1.1, 6.1, 11.1, 16.1, 21.1, ... -- i.e. a stock initialized to parameter
(= 1.1) and integrating input (= 5) per unit time. So the macro's
INTEG(input, parameter) becomes, at that call site, a genuine stock with
initial value = the bound parameter argument and inflow = the bound input
argument, and macro output reads that stock. (Note for implementers: the
fixture's expected macro output at t=1 is 6.1, i.e. the Euler update
1.1 + 5*1 -- the value of the macro at time t is the stock value at time
t, with the first integration step already applied at t=1.)
Flows and auxiliaries inside a macro do not need per-instance persistence in the same sense -- they are recomputed each step like any auxiliary -- but they are still materialized per call site (each call site gets its own copy of every internal variable; see section 7).
-
By position. The Nth actual argument at the call site binds to the Nth formal parameter in the header. Vensim and XMILE both treat the parameter list as ordered: XMILE requires "one
<parm>property for each macro parameter and they must appear in the expected calling order (i.e., the order of the actual parameters)" (XMILE 1.0 section 4.8). -
Evaluated in the caller's context. Because macro expansion materializes the macro body as real model variables wired to the call-site arguments, an argument expression is evaluated in the caller's namespace, on the caller's schedule. The XMILE framing -- the macro is "an independent model" with the arguments as its inputs -- means each argument is, in effect, an input port fed by the caller's expression. For a stockless macro this is just inline substitution; for a stock-bearing macro the argument feeds the macro's internal structure each time step.
-
Argument arity is fixed per definition. "Variable numbers of arguments are not supported, but the same macro MAY be defined multiple times with a different number of arguments" (XMILE 1.0 section 3.6.1). XMILE additionally allows
<parm default="...">so trailing parameters can be omitted at the call site (XMILE 1.0 section 4.8); Vensim's:MACRO:syntax as documented has no default-value mechanism.
- The macro's primary output is the value of the variable whose name equals the
macro name. At a call site
lhs = MACRONAME(args),lhstakes that value. Inxmutil's XMILE output the macro's<eqn>is literally just the macro name (xmutil:Xmile/XMILEGenerator.cpp:60-63), and the macro's<variables>block contains the equation that actually defines that macro-named variable -- the<eqn>says "the output is the variable called<macroname>," and<variables>says how that variable is computed. - For stockless macros the output is a pure function of the (possibly
expression-valued) arguments at the current time step (
macro_expression,macro_multi_expressionfixtures). - For stock-bearing macros the output is whatever the macro-named equation
computes -- which may itself be the stock (
macro_stock: the macro-named variable is theINTEG), or may be an auxiliary that reads an internal stock (the XMILESMOOTH1example:<eqn>Smooth_of_Input</eqn>returns the internal stock). - Additional named outputs (section 4.5) are returned by the caller binding
them to caller-named variables via the
:list.
Every name defined inside a macro -- parameters, the macro-named variable,
internal auxiliaries, internal stocks, named outputs -- is local to that
macro. "The variable names inside of a macro relate to nothing outside the
macro or in other macros"
(https://www.vensim.com/documentation/22150.html). A model variable named
population and a macro-internal variable named population are unrelated
(https://www.vensim.com/documentation/22150.html). XMILE states the same: "the
names of any variables (including parameter identifiers) defined within a macro
are local to that macro alone and will not conflict with any names within
either the whole-model or other macros" (XMILE 1.0 section 3.6.1).
xmutil implements this with an explicit separate symbol table per macro: on
:MACRO: it pushes a fresh SymbolNameSpace and on :END OF MACRO: it pops
back to the model's namespace; the macro name alone is registered into the
model's (main) namespace so it can be called
(xmutil:Vensim/VensimParse.cpp:679-694). The only
escape from the local namespace is the $-suffixed model-variable reference
(section 3.1).
-
Because a macro expands into real model variables, those expanded variables participate in the model's normal dependency graph and topological sort. They are initialized and stepped exactly like any other variable -- the macro boundary is not a scheduling barrier once expanded. (The one definition-time ordering rule -- "definition before use" -- is a parse-time rule, not a runtime one; see section 2.4.)
-
Nested macros. "Macro definitions can be nested, that is the definition of a macro can involve another macro. In this case when Vensim expands the macro to create the detailed equations it does the expansion recursively" (https://www.vensim.com/documentation/22145.html). The local fixture
test/test-models/tests/macro_cross_reference/exercises exactly this:EXPRESSION MACRO's body isEXPRESSION MACRO = SECOND MACRO(input, parameter), i.e. one macro calls another. Itsoutput.tabexpectsmacro output = 4.54545formacro input=5, macro parameter=1.1(= 5/1.1, sinceSECOND MACROdivides) -- confirming the nested call expands and evaluates correctly. The version-5.8 release notes give the canonical use: "a DELAY9 macro can be constructed from 3 DELAY3 calls" (https://www.vensim.com/documentation/version_5_8___-___.html). Because the body-equation calls another macro, definition order matters here too:SECOND MACROis defined beforeEXPRESSION MACROin the cross-reference fixture.
Vensim and XMILE diverge here, and the implementation should be explicit about which model it follows.
-
Vensim. No subscripts in the macro definition. At the call site you pass normally-subscripted variables; on expansion, "the created variables inherit the Subscripts from the left hand side of the equation" and "each equation created during the expansion of the Macro has the same subscripts on the left hand side" (https://www.vensim.com/documentation/macros.html). So a call
diddle[house] = ... SMOOTH(tadum[house], 20)expands to an internal smoothing stock that is also subscripted byhouse-- the array dimension rides in from the call-site LHS and is stamped onto every expanded equation. The macro body is authored as if scalar. The$-escape, by contrast, only works for unsubscripted model variables (https://www.vensim.com/documentation/22145.html). -
XMILE. Macros may declare and use arrays internally. The spec's recursive
FINDmacro takes a 1-D array parameterAand indexes it asA[i], usesSIZE(A), etc. (XMILE 1.0 section 4.8.1). So XMILE macros are not restricted to scalar bodies.
None of the local macro_* fixtures exercises subscripts at all -- they are
all scalar. There is therefore no local ground-truth output.tab for arrayed
macro expansion; this case must be validated against Vensim itself if needed.
-
Vensim. The Vensim
:MACRO:documentation describes nesting (one macro calling another) and recursive expansion of that nesting (https://www.vensim.com/documentation/22145.html), and "macro definitions can contain calls to other macros previously defined, or built in" (https://www.vensim.com/documentation/macros.html). The phrase "previously defined" plus the strict "definition before use" rule means self-reference (a macro calling itself by name) is not expressible in standard Vensim.mdl, because the macro's own name is not yet defined within its own body. The documented Vensim feature is mutual reference among distinct, previously-defined macros, not classical recursion. (No local fixture or Vensim doc page demonstrates a self-recursive:MACRO:.) -
XMILE. XMILE explicitly supports recursive macros. "Macros can be recursive, so a slightly more complicate[d] macro would call itself:
FACT(x): IF x <= 1 THEN 1 ELSE x*FACT(x - 1)" (XMILE 1.0 section 3.6.1). Recursion can even be mutual/nested: a recursive macro can call another recursive macro (theFACTSUMexample, XMILE 1.0 section 4.8.1). The spec notes its recursion examples all use tail recursion deliberately, "because implementers are free to map macros into any internal form, especially for the sake of efficiency. The use of tail recursion whenever possible allows implementers to convert the recursion into a simple loop" (XMILE 1.0 section 4.8.1). -
The
recursive_macrosflag. XMILE requires a model that uses recursive macros to declare it. The<uses_macros>tag (which itself must be listed under<options>when the file uses macros at all) has two REQUIRED attributes:recursive_macros="true|false"("Has macros which are recursive (directly or indirectly)") andoption_filters="true|false"("Defines option filters") (XMILE 1.0 section 2.2.1, and section 4.8: "OPTIONALLY, they can also be recursive... In this case, the recursive_macros option must be set to true in the<uses_macros>tag"). This is therecursive_macrosfield that the engine's XMILE serde already models:Feature::UsesMacros { recursive_macros: Option<bool>, option_filters: Option<bool> }(src/simlin-engine/src/xmile/mod.rs:510-513). It is purely a declaration / capability flag in the file header -- it tells a reader "this file contains macros that recurse, so don't assume macro expansion terminates by simple inlining." It does not itself change macro semantics; it is a hint that naive finite inline-expansion is unsafe.
See section 3.2 for the in-body rule (units may name an input argument, whose
actual unit is substituted at each call). At the call site, the LHS variable
that receives the macro output carries its own units declaration like any
equation (e.g. total reserve = add3(...) ~ m*m*m ~ | in the Vensim "Using
Macros" example). XMILE keeps <units> on each variable inside the macro's
<variables> block (the macro_stock.xmile fixture shows <units>input</units>
on the macro's stock -- again the "units = name of an input parameter"
convention, carried through verbatim by xmutil).
XMILE v1.0 does specify macros (despite a common belief that it does not). The local copy of the OASIS spec covers them in sections 2.10 (pointer), 3.6 / 3.6.1 / 3.6.2 (rationale), and 4.8 / 4.8.1 / 4.8.2 / 4.8.3 (the encoding).
"Macros live outside of all other blocks, at the same level as the
<model>tag, and MAY be the only thing in a file other than its header." (XMILE 1.0 section 4.8)
A macro is a <macro> element. Its REQUIRED properties/attributes:
name="..."-- the macro name, a valid XMILE identifier.<eqn>-- a valid XMILE expression (in aCDATAsection if needed). For a stockless macro this is the macro's actual formula (e.g.<eqn>LN(x)/LN(base)</eqn>); for a macro with variables this is typically just the name of the variable to return (e.g.<eqn>Smooth_of_Input</eqn>).
OPTIONAL properties/attributes (XMILE 1.0 section 4.8):
<parm>-- one per formal parameter, in calling order. Holds the parameter's local name (a valid XMILE identifier). May carry adefault="..."attribute (a valid XMILE expression that can refer to earlier parameters); once one parameter has a default, every later parameter must too. The spec strongly recommends<parm>tags appear before<eqn>.<format>-- text describing the proper call format (e.g.LOG(<value>, <base>)).<doc>-- text describing the macro's purpose, optionally HTML.<sim_specs>-- the macro may run with its own simulation specs (only<start>,<stop>,<dt>, andmethod; all butmethodmay be expressions referring to parameters). Must only appear together with a<variables>block. When<sim_specs>is present the macro's default DT is 1 and default integration method is Euler. Absent<sim_specs>, the macro uses the invoking model's DT and method.<variables>-- a variable block, exactly as for<model>(stocks, flows, auxes, etc.). Absent for a pure-expression macro.<views>containing exactly one<view>-- only with<variables>, "exists only to facilitate editing macros."namespace="..."-- a single XMILE namespace for the macro.
XMILE also says macros MAY include submodels, and MAY be recursive (with the
recursive_macros option flag set; section 5.7).
Stockless / expression macro (XMILE 1.0 section 4.8.1):
<macro name="LOG">
<parm>x</parm>
<parm>base</parm>
<eqn>LN(x)/LN(base)</eqn>
<format><![CDATA[LOG(<value>, <base>)]]></format>
<doc><![CDATA[Finds the base-<base> logarithm of <value>.]]></doc>
</macro>Recursive macro (XMILE 1.0 section 4.8.1):
<macro name="FACT">
<parm>x</parm>
<eqn>IF x <= 1 THEN 1 ELSE FACT(x - 1)</eqn>
</macro>Macro with variables -- a hand-rolled first-order smooth (XMILE 1.0 section 4.8.2):
<macro name="SMOOTH1">
<parm>input</parm>
<parm>averaging_time</parm>
<parm default="input">initial</parm>
<eqn>Smooth_of_Input</eqn>
<variables>
<stock name="Smooth_of_Input">
<eqn>initial</eqn>
<inflow>change_in_smooth</inflow>
</stock>
<flow name="change_in_smooth">
<eqn>(input - Smooth_of_Input)/averaging_time</eqn>
</flow>
</variables>
</macro>Macro with variables and simulation specs -- an iterative factorial (XMILE 1.0 section 4.8.3):
<macro name="FACT">
<parm>x</parm>
<eqn>Fact</eqn>
<sim_specs>
<start>1</start>
<stop>x</stop> <!-- stop time is parameterized -->
</sim_specs> <!-- DT defaults to 1 -->
<variables>
<stock name="Fact">
<eqn>1</eqn>
<inflow>change_in_fact</inflow>
</stock>
<flow name="change_in_fact">
<eqn>Fact*TIME</eqn>
</flow>
</variables>
</macro>"In equations, the macro name is used as a function" (XMILE 1.0 section 3.6.1)
-- i.e. LOG(256, 2), SMOOTH1(demand, 5). This matches the Vensim call
syntax and matches what xmutil emits: the macro_expression.xmile fixture's
model variable is <eqn>EXPRESSION_MACRO(macro_input, macro_parameter)</eqn>.
The XMILE spec does not define a :-style multiple-output call syntax; XMILE's
optional-parameter story is <parm default="...">, not Vensim's output list.
A file that uses any macros must list <uses_macros/> under the header's
<options> tag, and <uses_macros> carries the two REQUIRED attributes
recursive_macros and option_filters (XMILE 1.0 sections 2.2.1, 4.8; see
section 5.7). XMILE also defines a standard macro library distribution
mechanism: vendors/organizations can publish versioned macro files (e.g.
http://systemdynamics.org/xmile/macros/standard-1.0.xml) and a model pulls
them in via <includes><include resource="..."/></includes> inside <header>
(XMILE 1.0 sections 2.10, 2.11). "A vendor may create a common library of
macros with specific functionality used by all whole-models produced by that
vendor's software" (XMILE 1.0 section 2.10).
xmile::Projecthas#[serde(rename = "macro")] pub macros: Vec<Macro>(src/simlin-engine/src/xmile/mod.rs:75-76,244) -- so the<macro>elements are collected at the project level, as the spec requires.xmile::Macrois a stub: the struct body is literally// TODO(src/simlin-engine/src/xmile/mod.rs:334-338). Noname, no<parm>, no<eqn>, no<variables>. So XMILE macro contents are currently parsed into an empty struct and effectively dropped, even though the count of macros is preserved.Feature::UsesMacros { recursive_macros: Option<bool>, option_filters: Option<bool> }(src/simlin-engine/src/xmile/mod.rs:510-513) models the header<uses_macros>declaration faithfully.
So on the XMILE side, implementing macros means (at minimum) fleshing out
xmile::Macro to carry name, parms, eqn, variables, and optionally
sim_specs/views/namespace, and then giving it meaning in the
datamodel/compiler.
xmutil is Bob Eberlein's C++ Vensim-to-XMILE converter
(https://github.com/bobeberlein/xmutil). It is the tool that produced the
.xmile and .stmx files in the local fixtures, so its mapping is the shape
those fixtures have, and the shape the engine's XMILE reader has to accept.
What a model computes is settled by Vensim's own output, not by xmutil.
Reading its source, the mapping is:
- The Vensim lexer recognizes
:MACRO:and:END OF MACRO:as dedicated tokensVPTT_macro/VPTT_end_of_macro(xmutil:Vensim/VensimLex.cpp:427-438). - The grammar rule is
macrostart: VPTT_macro { vpyy_macro_start(); } VPTT_symbol '(' exprlist ')' { vpyy_macro_expression($3, $5); }andmacroend: VPTT_end_of_macro { vpyy_macro_end(); }(xmutil:Vensim/VYacc.y:98-104). Note again: the header parses(' exprlist ')'with no:output-list production -- soxmutilsupports only the macro-name-is-the-output form. VensimParse::MacroStart()pushes a new localSymbolNameSpacefor the macro's interior;VensimParse::MacroExpression(name, margs)constructs aMacroFunctionregistered against the main (model) namespace -- so the macro name is callable from the model -- while its body variables live in the local namespace;VensimParse::MacroEnd()restores the model namespace (xmutil:Vensim/VensimParse.cpp:679-694).- A
MacroFunction(subclass ofFunction) holds: the macro name, anExpressionList* mArgs(the formal parameters), a privateSymbolNameSpace* pSymbolNameSpace(the local namespace), and a vector ofEqUnitPair(each body equation + its units) (xmutil:Function/Function.h:106-135). Body equations encountered between:MACRO:and:END OF MACRO:are added to the active macro rather than to a model group --AddFullEqchecks!mInMacrobefore assigning a variable to a group (xmutil:Vensim/VensimParse.cpp:181). The completed list ofMacroFunctions is handed to theModel(xmutil:Vensim/VensimParse.cpp:387,Model.h:58-62,127). - Because a
MacroFunctionis aFunction, a call to it inside an equation is parsed like any function call -- there is no special call-site syntax.
The whole-model translation runs first, then "macros are presented as separate
models." For each MacroFunction
(xmutil:Xmile/XMILEGenerator.cpp:55-78):
- Create a
<macro>element withname="<macro name>"(original Vensim casing, spaces preserved -- e.g.name="EXPRESSION MACRO"). - Emit an
<eqn>whose text is just the macro name. The source comment is explicit: "in vensim the equation is always just the name of the macro" (XMILEGenerator.cpp:60-63). This is XMILE's "return the variable named<macroname>" convention -- and it works because the macro body always contains an equation that defines a variable with the macro's name. - For each formal parameter in
mArgs, emit a<parm>whose text is the parameter name (XMILEGenerator.cpp:64-74). Parameter order is preserved. - Emit the macro body by calling
generateModelAsSectors(macro, ..., mf-> NameSpace(), false)-- i.e. the macro's local namespace is rendered as a<variables>block (plus a<views>) inside the<macro>element, using the same machinery that renders the main model (XMILEGenerator.cpp:76). The comment notes it is "not really a sector -- only a root module here." <macro>elements are appended to the document root, as siblings of<model>(XMILEGenerator.cpp:77).
MacroFunction::ComputableName() returns SpaceToUnderBar(name)
(xmutil:Function/Function.cpp:21-23), so when the macro
is called in a model equation, xmutil emits the underscored name -- e.g.
the Vensim call EXPRESSION MACRO(macro input, macro parameter) becomes the
XMILE equation EXPRESSION_MACRO(macro_input, macro_parameter) (exactly what
test_macro_expression.xmile contains). The <macro name="..."> attribute
keeps the spaced name, but call sites use the underscored form.
test_macro_expression.mdl ->
test_macro_expression.xmile (verified by reading both files):
:MACRO: EXPRESSION MACRO(input, parameter)
EXPRESSION MACRO = input * parameter
~ input
~ tests basic macro containing no stocks and having no output
|
:END OF MACRO:
becomes
<macro name="EXPRESSION MACRO">
<eqn>EXPRESSION MACRO</eqn>
<parm>input</parm>
<parm>parameter</parm>
<variables>
<aux name="EXPRESSION MACRO">
<doc> tests basic macro containing no stocks and having no output</doc>
<eqn>input*parameter</eqn>
<units>input</units>
</aux>
</variables>
<views>...</views>
</macro>and the model-level call macro output = EXPRESSION MACRO(macro input, macro parameter) becomes <aux name="macro output"><eqn>EXPRESSION_MACRO(macro_input, macro_parameter)</eqn></aux>.
For the stock-bearing test_macro_stock.mdl, the macro body
EXPRESSION MACRO = INTEG(input, parameter) becomes a <stock> plus a
synthetic <flow> inside the macro's <variables>:
<macro name="EXPRESSION MACRO">
<eqn>EXPRESSION MACRO</eqn>
<parm>input</parm>
<parm>parameter</parm>
<variables>
<flow name="input"/>
<stock name="EXPRESSION MACRO">
<doc> tests basic macro containing a stock but no output</doc>
<inflow>input</inflow>
<eqn>parameter</eqn>
<units>input</units>
</stock>
</variables>
<views>...</views>
</macro>Note xmutil's INTEG(input, parameter) -> stock translation reuses the first
INTEG argument's name (input) as the inflow name -- here input is also a
parameter name, so the macro ends up with a <flow name="input"/> shadowing the
<parm>input</parm>. That is a quirk of xmutil's INTEG handling, not a
property of macros per se, but it is what the fixture contains and an
implementation reading these .xmile files must cope with it.
Multiple macros (test_macro_multi_macros.mdl) produce multiple sibling
<macro> elements. Cross-referencing macros (macro_cross_reference) produce
two <macro> elements where one macro's <variables> equation calls the other
by its underscored name.
- It does not expand/inline macros into the model. It carries the macro
through to XMILE as a
<macro>element and leaves call sites asMACRONAME(args)function calls. Expansion/instantiation is left to whatever consumes the XMILE. - It does not support the
:-separated output list (section 4.6 / 7.1). - It does not carry
<sim_specs>on the macro (Vensim:MACRO:syntax has no per-macro sim specs to begin with; that is an XMILE-only feature).
The engine's pure-Rust MDL parser (src/simlin-engine/src/mdl/) parses
macros into a MacroDef { name, args: Vec<Expr>, equations: Vec<FullEquation>, loc }
(src/simlin-engine/src/mdl/ast.rs:483-493), assembled by the reader's
MacroState machine (src/simlin-engine/src/mdl/reader.rs:59-70,300-330). But
the AST-to-datamodel conversion discards it: MdlItem::Macro(_) is matched
and ignored (src/simlin-engine/src/mdl/convert/mod.rs:248, with a test
test_mdl_group_variables_after_macro at convert/mod.rs:616 that only checks
the macro doesn't corrupt group membership). The engine's own MDL parser
CLAUDE.md lists this as a known gap: "Macro expansion/inlining (parsing
complete, conversion not implemented). C-LEARN model requires this to
simulate" (src/simlin-engine/src/mdl/CLAUDE.md). So the implementation task,
on the Vensim-import side, is conversion, not parsing.
This section lays out -- neutrally -- the evidence bearing on one candidate representation: modeling a Vensim macro as a reusable sub-model (module), instantiated once per call site, with the macro's input arguments wired as module inputs and its output(s) read by auxiliary variables. It does not pick a design; it catalogs which facts make that mapping clean and which make it hard.
-
The macro body already is a little model. XMILE says so directly: macros with variables "are, in fact, independent models that run to completion each time they are invoked" (XMILE 1.0 section 3.6.1), and
xmutilliterally renders the macro body with the same code path it uses for the main model (generateModelAsSectors,XMILEGenerator.cpp:76). A module/sub-model is the natural home for "a variable block that looks like a model." -
Per-invocation state is exactly module-instance semantics. The XMILE requirement that "any stocks that are defined within a macro MUST have their own instances for each use of that macro and these instances MUST persist across the length of the simulation" (XMILE 1.0 section 4.8.2) is precisely what a module instance gives you: each instantiation has its own stock storage that persists for the run. The
macro_stockfixture's expected trajectory confirms the runtime behavior a module instance would have to reproduce. -
Locality maps onto a module namespace. Macro-internal names are local and cannot conflict with the model or other macros (https://www.vensim.com/documentation/22150.html, XMILE 1.0 section 3.6.1) -- the same isolation a module/sub-model namespace provides.
xmutilalready enforces this with a per-macro symbol table (VensimParse.cpp:679-694). -
Positional input binding maps onto module inputs. Arguments bind by position (XMILE 1.0 section 4.8); a module with an ordered input port list is the same shape. The macro's primary output (the macro-named variable) is a designated output of the module that the call-site LHS reads.
-
Definition/parsing is done. Both
xmutiland the engine's native parser already capture the macro's name, ordered parameter list, and body equations (MacroDef/MacroFunction). The XMILE serde already collects<macro>elements intoproject.macros. The hard part left is instantiation + wiring, not lexing. -
The
recursive_macrosflag already has a home. The engine's XMILE serde modelsFeature::UsesMacros { recursive_macros, option_filters }(xmile/mod.rs:510-513), so a design can detect "this file claims recursive macros" up front and choose a strategy.
-
Multiple call sites of the same macro. A module typically has a single declared instance with a name; a macro can be called arbitrarily many times (https://www.vensim.com/documentation/22150.html), each needing its own instance with its own stock storage and its own expanded variable names. Vensim's own answer is name-mangling: expanded variables are wrapped in
#...#and prefixed by the call-site LHS variable name -- the post-5.8 form is#lhs>macroname>macrovar#(e.g.#smoothed income>smooth#) (https://www.vensim.com/documentation/macros.html, https://www.vensim.com/documentation/version_5_8___-___.html). Any module-instantiation design has to synthesize one instance (and a unique instance name) per call site, and decide how those instances are named and surfaced. -
Macros nested inside larger expressions. Vensim allows
diddle = daddle + SMOOTH(tadum, 20)-- the macro call is a sub-expression (https://www.vensim.com/documentation/22150.html). A module instance produces a variable, not an expression value, so a nested call has to be lifted: the call site must be rewritten to introduce a synthesized variable that reads the instance's output, and the original expression must reference that synthesized variable. (This is the same shape as the engine's existing builtins handling, e.g.PREVIOUS/INITdesugaring through synthesized helper auxes -- seesimlin-engine'sbuiltins_visitor.rs-- so there is precedent, but it is non-trivial work.) The local fixtures never exercise the nested case, so there is no ground-truth output for it. -
Expression-valued arguments. Call-site arguments can be arbitrary expressions (
SMOOTH(a + b, t); section 4.2). A module input port is fed by a variable or an expression-input; the design must decide whether to (a) pass an expression directly as a module input, or (b) synthesize a helper aux per non-trivial argument and wire that. Either is workable; it must be chosen. Note the asymmetry: definition-header parameters are plain names, but call-site arguments are full expressions. -
Multiple outputs (the
:output list). Vensim's discouraged-but-legal output list (add3(... : minval, maxval)) returns extra named variables to the caller (section 4.5). Neither converter parses it today (section 4.6). A module can have multiple outputs, so the representation is fine, but: the parser must be extended (header and call site), and the call-site binding semantics ("the caller names these; they become model variables") must be implemented. Given Vensim discourages it and no fixture uses it, an implementation could legitimately defer or reject this -- but that is a decision to make explicitly. -
Per-instance stock state vs. how the engine instantiates modules. The design has to confirm that the engine's module mechanism actually gives each instance independent, persistent stock storage (the XMILE MUST in section 4.8.2) and independent initialization. If module instances currently share any storage or are deduplicated, that breaks macros with stocks.
-
The
$model-variable escape. Macro bodies reachTime$,TIME STEP$, etc. -- references out of the macro to the caller's control variables (section 3.1), and this is load-bearing for the C-LEARN hero model. A module is normally a closed namespace fed only through its input ports. The design must decide how a$reference becomes an (implicit) module input wired to the caller'stime/time_step/etc. -- and remember it is restricted to unsubscripted model variables. -
Subscript inheritance from the call-site LHS. Vensim macro bodies are authored as scalar; on expansion every created variable inherits the subscripts of the call-site LHS (https://www.vensim.com/documentation/macros.html). A module instantiation design must replicate "stamp the call-site LHS's dimensions onto the whole instance" -- which is a different operation from the usual "module has its own fixed dimensions." (XMILE diverges here -- it allows arrays in the macro body -- so the design must also decide which semantics to honor for XMILE-sourced vs. Vensim-sourced macros.) No local fixture covers arrayed macros, so this path is unverified by the test corpus.
-
Recursion. XMILE permits self-recursive and mutually-recursive macros (section 5.7); a per-call-site inline instantiation strategy does not terminate for a genuinely recursive macro. The
recursive_macrosflag is the warning sign. Vensim's standard:MACRO:syntax cannot express direct self-recursion ("definition before use" + the macro name not being defined in its own body), so for the Vensim-import path this may be a non-issue in practice -- but an XMILE-import path, or a strict reading, has to decide what to do (support bounded recursion, reject, etc.). XMILE's own hint is that tail-recursive macros can be turned into loops (section 4.8.1). -
Definition-order rule. Vensim's "definition must precede use" is the only ordering rule in the language (https://www.vensim.com/documentation/22145.html), yet the local
macro_trailing_definitionfixture deliberately violates it and is still treated as a valid, simulating model. The implementation must decide whether to enforce Vensim's stated rule (and error on trailing definitions) or be lenient (collect all macro definitions before resolving any call, regardless of order). The engine's parser already collects all top-level items before conversion, so leniency is the lower-effort path -- but it is a conscious deviation from Vensim's documented contract. -
Mismatch between XMILE
<macro>and a module. An XMILE<macro>can carry its own<sim_specs>(its ownstart/stop/dt/method, possibly parameterized by arguments) and "run to completion each time it is invoked" (XMILE 1.0 sections 3.6.1, 4.8, 4.8.3). That is not how a normal sub-model/ module behaves -- a module steps in lockstep with its parent. A macro with its own sim specs is closer to "call a whole nested simulation per time step." The local Vensim fixtures never hit this (Vensim:MACRO:has no per-macro sim specs), but an XMILE-import path or a faithful XMILE-export would have to confront it. For the Vensim hero use case this can likely be ignored; for full XMILE-macro support it cannot.
| Macro feature | Pinned by a local fixture? |
|---|---|
| Stockless macro, single call, scalar | Yes -- macro_expression, macro_multi_expression |
| Stock-bearing macro, single call, scalar | Yes -- macro_stock (full 11-step trajectory) |
| Multiple distinct macros in one file | Yes -- macro_multi_macros, macro_cross_reference |
| One macro calling another (nesting) | Yes -- macro_cross_reference |
| Macro defined after use | Yes -- macro_trailing_definition (and treated as valid) |
| Same macro invoked at multiple call sites | No |
| Macro call nested inside a larger expression | No |
| Expression-valued call arguments | No (fixtures pass only plain identifiers) |
:-separated output list (multiple outputs) |
No (and unparseable today) |
| Subscripted / arrayed macros | No |
$ model-variable escape (Time$, TIME STEP$) |
Not in macro_* fixtures, but used by the C-LEARN hero model |
| Recursive macro | No |
Per-macro <sim_specs> (XMILE) |
No |
The C-LEARN hero model (test/xmutil_test_models/C-LEARN v77 for Vensim.mdl)
defines four macros -- SAMPLE UNTIL (contains a stock and uses TIME STEP$),
SSHAPE (stockless, two body equations, shadows a built-in name),
RAMP FROM TO (stockless, five parameters, six internal auxes, uses Time$
and TIME STEP$), and INIT (stockless, wraps INITIAL) -- so it stresses
multi-equation bodies, the $ escape, a stock-in-macro, and a builtin-shadowing
name, but (as written) it invokes each macro in a relatively simple way. It is
the realistic target; the table above shows which of its features have
isolated fixture coverage and which do not.
-
Lookups inside a macro body. Neither the Vensim macro pages nor the local fixtures explicitly demonstrate a graphical-function/lookup defined inside a
:MACRO:body. XMILE's "all of the syntax of XMILE" implies it is allowed, but there is no ground-truth output. Should be tested against Vensim directly if needed. -
Exact Vensim behavior on a trailing macro definition. The Vensim manual says it is a syntax error; the
macro_trailing_definitionfixture (produced with "Vensim DSS 6.3E for Mac") nonetheless ships an expectedoutput.tab. Whether that fixture's.mdlactually loaded cleanly in that Vensim build, or was hand-constructed, is not determinable from the files alone. -
Whether direct self-recursion is ever accepted by Vensim DSS. The docs describe nesting of previously defined macros and recursive expansion of that nesting, and the strict ordering rule makes literal self-reference inexpressible -- but the Vensim documentation never says "self-recursive macros are rejected" in so many words. XMILE explicitly allows recursion; Vensim's position is, at best, implied.
-
How Vensim resolves a macro name that shadows a built-in at a call site. C-LEARN names a macro
SSHAPE, which is also a Vensim built-in. The engine's parser disambiguates the definition header (accepts aFunctiontoken as the macro name). What is not pinned by an authoritative source is the precedence rule at the call site once such a macro is in scope -- does the user macro shadow the built-in for the rest of the file? The "definition before use" rule and the per-file macro table strongly imply yes, but it is not stated outright. -
Argument-evaluation timing for stock-bearing macros across DT. The XMILE/Vensim model ("the macro is an independent model fed by the arguments") and the
macro_stockfixture together imply the argument expressions are evaluated in the caller's context every step and fed into the macro's internal integration. The exact ordering relative to the caller's own stock updates within a single Euler/RK step is not spelled out in the macro documentation pages; it follows from the general XMILE/Vensim integration semantics, but a careful implementation should verify it against themacro_stocknumbers and, ideally, a multi-step RK fixture. -
XMILE
<macro>with its own<sim_specs>-- runtime semantics in practice. The spec defines the encoding (sections 4.8, 4.8.3) and says such a macro "runs to completion each time it is invoked," but real-world XMILE files exercising this are scarce, and the local corpus has none.