Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/simlin-engine/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ Integration tests are consolidated into the single `tests/integration` harness (
- **`src/array_tests.rs`** - Array-specific tests (feature-gated)
- **`src/per_element_gf_tests.rs`** - Per-element graphical-function (arrayed GF) layout invariant: a per-element GF table must land at the element's *declared dimension index* (row-major), not its `Equation::Arrayed` `elems` Vec position. Uses deliberately NON-alphabetical declared orders (where position != dim-index) so a positional mis-map is observable; covers the scalar `Lookup` path, the `LookupArray` (VECTOR SELECT) path, the MDL-importer-sort twin, sparse + multi-dim, the salsa dependency-table path (`extract_tables_from_source_var`), and a compile-time/perf structural guard that the reorder is materialized at compile time and the VM opcode carries no element name. The LTM-polarity twin lives in the `ltm` tests module (`test_per_element_gf_link_polarity_fixed_index_non_sorted_order`).
- **`src/lookup_only_tests.rs`** - End-to-end tests for the standalone-lookup-only **static table** contract (#606): a lookup-only holder (scalar, A2A, arrayed) produces NO saved series; a consumer that calls it (`LOOKUP(g, x)`) reads the table, two calls at different arguments are independent, a bare reference is a compile error, and the legacy `"0+0"` form is still accepted. The pure `is_lookup_only`/`var_is_lookup_only` predicate units live in `src/variable.rs`.
- **`src/round_builtin_tests.rs`** - End-to-end contract for the `ROUND(x)` builtin (a Simlin extension: round-half-to-even, Python `round()` / IEEE roundTiesToEven -- see the `BuiltinFn::Round` comment for the Stella/Vensim external-tool caveats). Drives the SHARED case table `test_common::ROUND_TIES_TO_EVEN_CASES` through the full parse -> compile -> VM pipeline; the same table drives the VM `apply()` unit test (`vm::tests`) and the wasm parity test (`wasmgen::lower_tests`), so the three backends are pinned against identical rows. Also covers elementwise arrays, ROUND under a reducer, NaN/infinity, case-insensitivity, arity errors, units preservation, and the patch print-reparse round trip.
- **`src/keyword_ident_tests.rs`** - End-to-end contract for a variable whose canonical name IS an equation-language keyword (GH #976). Pins the reachability premise the issue left unverified -- both the XMILE and MDL readers admit `if`/`mod`/`nan`/... as a variable name and the model compiles with zero diagnostics -- and then the consequence: a `patch` rename of an UNRELATED variable reprints every dependent equation, and the reprint must keep the quotes (bare, it re-parses as the keyword and the persisted datamodel no longer compiles). Both rename directions are covered, since renaming TO a keyword reprints the new name at every reference.
- **`src/json_proptest.rs`**, **`src/json_sdai_proptest.rs`** - Property-based tests
- **`src/unit_checking_test.rs`** - Unit checking regression tests
Expand Down
2 changes: 2 additions & 0 deletions src/simlin-engine/src/ast/expr1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ impl Expr1 {
"pulse" => check_arity!(Pulse, 2, 3),
"quantum" => check_arity!(Quantum, 2),
"ramp" => check_arity!(Ramp, 2, 3),
"round" => check_arity!(Round, 1),
"safediv" => check_arity!(SafeDiv, 2, 3),
"sign" => check_arity!(Sign, 1),
"sin" => check_arity!(Sin, 1),
Expand Down Expand Up @@ -367,6 +368,7 @@ impl Expr1 {
BuiltinFn::Int(a) => BuiltinFn::Int(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Ln(a) => BuiltinFn::Ln(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Log10(a) => BuiltinFn::Log10(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Round(a) => BuiltinFn::Round(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Sign(a) => BuiltinFn::Sign(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Sin(a) => BuiltinFn::Sin(Box::new(a.constify_dimensions(scope))),
BuiltinFn::Sqrt(a) => BuiltinFn::Sqrt(Box::new(a.constify_dimensions(scope))),
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/ast/expr2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ impl Expr2 {
loc,
),
Abs(e) => Abs(Box::new(Expr2::from(*e, ctx)?)),
Round(e) => Round(Box::new(Expr2::from(*e, ctx)?)),
Arccos(e) => Arccos(Box::new(Expr2::from(*e, ctx)?)),
Arcsin(e) => Arcsin(Box::new(Expr2::from(*e, ctx)?)),
Arctan(e) => Arctan(Box::new(Expr2::from(*e, ctx)?)),
Expand Down
5 changes: 5 additions & 0 deletions src/simlin-engine/src/ast/expr3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ impl Expr3 {
| Int(e)
| Ln(e)
| Log10(e)
| Round(e)
| Sign(e)
| Sin(e)
| Sqrt(e)
Expand Down Expand Up @@ -842,6 +843,10 @@ impl<'a> Pass1Context<'a> {
let (new_e, has_a2a) = self.transform_inner(*e);
(Int(Box::new(new_e)), has_a2a)
}
Round(e) => {
let (new_e, has_a2a) = self.transform_inner(*e);
(Round(Box::new(new_e)), has_a2a)
}
Ln(e) => {
let (new_e, has_a2a) = self.transform_inner(*e);
(Ln(Box::new(new_e)), has_a2a)
Expand Down
26 changes: 24 additions & 2 deletions src/simlin-engine/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ pub enum BuiltinFn<Expr> {
Pulse(Box<Expr>, Box<Expr>, Option<Box<Expr>>),
Quantum(Box<Expr>, Box<Expr>),
Ramp(Box<Expr>, Box<Expr>, Option<Box<Expr>>),
// ROUND(x): nearest integer, exact .5 ties to the EVEN neighbor
// (Python round() / IEEE roundTiesToEven). The XMILE v1.0 spec defines no
// ROUND builtin (its function catalog stops at INT, which footnote 7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe the XMILE catalog accurately

The corrected citation in round_builtin_tests.rs now says only that the catalog contains no ROUND entry, but this separate comment still claims the catalog “stops at INT.” Section 3.5.1 continues after INT with LN, LOG10, MAX, and other functions, so replace this with the narrower verified claim that the catalog lists INT but not ROUND; otherwise the code retains another false external-spec premise.

AGENTS.md reference: AGENTS.md:L103-L109

Useful? React with 👍 / 👎.

// mandates as floor). Python's semantics are the REQUIREMENT here, a
// product decision -- ties-to-even regardless of what other tools do.
// Disclosure, not a caveat to revisit: Stella also defines ROUND ("rounds
// expression to its nearest integer value", isee Stella docs, tie rule
// unspecified), so a Stella-authored model calling ROUND imports and
// simulates under these semantics instead of failing with UnknownBuiltin;
// whether Stella agrees at exact .5 ties is unverified, and matching
// Stella is explicitly a non-goal (if that ever changes, verify its tie
// rule against ground-truth output first). Vensim defines no ROUND at
// all, and the MDL writer records an ExportWarning for it
// (mdl/writer.rs).
Round(Box<Expr>),
SafeDiv(Box<Expr>, Box<Expr>, Option<Box<Expr>>),
Sign(Box<Expr>),
Sshape(Box<Expr>, Box<Expr>, Box<Expr>),
Expand Down Expand Up @@ -136,6 +151,7 @@ impl<Expr> BuiltinFn<Expr> {
Pulse(_, _, _) => "pulse",
Quantum(_, _) => "quantum",
Ramp(_, _, _) => "ramp",
Round(_) => "round",
SafeDiv(_, _, _) => "safediv",
Sign(_) => "sign",
Sshape(_, _, _) => "sshape",
Expand Down Expand Up @@ -216,6 +232,7 @@ impl<Expr> BuiltinFn<Expr> {
Box::new(f(*b)?),
c.map(|c| f(*c)).transpose()?.map(Box::new),
),
Round(a) => Round(Box::new(f(*a)?)),
SafeDiv(a, b, c) => SafeDiv(
Box::new(f(*a)?),
Box::new(f(*b)?),
Expand Down Expand Up @@ -303,6 +320,7 @@ impl<Expr> BuiltinFn<Expr> {
| Pulse(_, _, _)
| Quantum(_, _)
| Ramp(_, _, _)
| Round(_)
| SafeDiv(_, _, _)
| Sign(_)
| Sshape(_, _, _)
Expand Down Expand Up @@ -341,8 +359,8 @@ impl<Expr> BuiltinFn<Expr> {
f(b);
}
Abs(a) | Arccos(a) | Arcsin(a) | Arctan(a) | Cos(a) | Exp(a) | Int(a) | Ln(a)
| Log10(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) | Size(a) | Stddev(a) | Sum(a)
| Init(a) => f(a),
| Log10(a) | Round(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) | Size(a) | Stddev(a)
| Sum(a) | Init(a) => f(a),
Previous(a, b) => {
f(a);
f(b);
Expand Down Expand Up @@ -486,6 +504,7 @@ pub fn is_builtin_fn(name: &str) -> bool {
| "pulse"
| "quantum"
| "ramp"
| "round"
| "safediv"
| "sign"
| "sin"
Expand Down Expand Up @@ -550,6 +569,7 @@ where
| BuiltinFn::Int(a)
| BuiltinFn::Ln(a)
| BuiltinFn::Log10(a)
| BuiltinFn::Round(a)
| BuiltinFn::Sign(a)
| BuiltinFn::Sin(a)
| BuiltinFn::Sqrt(a)
Expand Down Expand Up @@ -657,6 +677,7 @@ fn every_builtin_variant_names_itself_and_is_recognized() {
Builtin::Pulse(b(), b(), None),
Builtin::Quantum(b(), b()),
Builtin::Ramp(b(), b(), None),
Builtin::Round(b()),
Builtin::SafeDiv(b(), b(), None),
Builtin::Sign(b()),
Builtin::Sshape(b(), b(), b()),
Expand Down Expand Up @@ -706,6 +727,7 @@ fn every_builtin_variant_names_itself_and_is_recognized() {
| Builtin::Pulse(..)
| Builtin::Quantum(..)
| Builtin::Ramp(..)
| Builtin::Round(..)
| Builtin::SafeDiv(..)
| Builtin::Sign(..)
| Builtin::Sshape(..)
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ pub(crate) enum BuiltinId {
Pulse,
Quantum,
Ramp,
Round,
SafeDiv,
Sign,
Sin,
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/compiler/array_operand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ fn materialize_view_operands(
| Arctan(_)
| Cos(_)
| Exp(_)
| Round(_)
| Inf
| Int(_)
| IsModuleInput(_, _)
Expand Down
4 changes: 3 additions & 1 deletion src/simlin-engine/src/compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,7 @@ impl<'module> Compiler<'module> {
| BuiltinFn::Int(a)
| BuiltinFn::Ln(a)
| BuiltinFn::Log10(a)
| BuiltinFn::Round(a)
| BuiltinFn::Sign(a)
| BuiltinFn::Sin(a)
| BuiltinFn::Sqrt(a)
Expand Down Expand Up @@ -1491,6 +1492,7 @@ impl<'module> Compiler<'module> {
BuiltinFn::Exp(_) => BuiltinId::Exp,
BuiltinFn::Inf => BuiltinId::Inf,
BuiltinFn::Int(_) => BuiltinId::Int,
BuiltinFn::Round(_) => BuiltinId::Round,
BuiltinFn::IsModuleInput(_, _) => unreachable!(),
BuiltinFn::Ln(_) => BuiltinId::Ln,
BuiltinFn::Log10(_) => BuiltinId::Log10,
Expand Down Expand Up @@ -1948,7 +1950,7 @@ impl<'module> Compiler<'module> {
self.collect_iter_source_views_impl(b, views, seen);
}
Abs(a) | Arccos(a) | Arcsin(a) | Arctan(a) | Cos(a) | Exp(a) | Int(a) | Ln(a)
| Log10(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) => {
| Log10(a) | Round(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) => {
self.collect_iter_source_views_impl(a, views, seen);
}
Max(a, opt_b) | Min(a, opt_b) => {
Expand Down
2 changes: 2 additions & 0 deletions src/simlin-engine/src/compiler/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ impl Context<'_> {
Int(e) => Int(Box::new(self.lower_pass0(e))),
Ln(e) => Ln(Box::new(self.lower_pass0(e))),
Log10(e) => Log10(Box::new(self.lower_pass0(e))),
Round(e) => Round(Box::new(self.lower_pass0(e))),
Sign(e) => Sign(Box::new(self.lower_pass0(e))),
Sin(e) => Sin(Box::new(self.lower_pass0(e))),
Sqrt(e) => Sqrt(Box::new(self.lower_pass0(e))),
Expand Down Expand Up @@ -2250,6 +2251,7 @@ impl Context<'_> {
BFn::Exp(a) => BuiltinFn::Exp(Box::new(self.lower_from_expr3(a)?)),
BFn::Inf => BuiltinFn::Inf,
BFn::Int(a) => BuiltinFn::Int(Box::new(self.lower_from_expr3(a)?)),
BFn::Round(a) => BuiltinFn::Round(Box::new(self.lower_from_expr3(a)?)),
BFn::IsModuleInput(id, loc) => BuiltinFn::IsModuleInput(id.clone(), *loc),
BFn::Ln(a) => BuiltinFn::Ln(Box::new(self.lower_from_expr3(a)?)),
BFn::Log10(a) => BuiltinFn::Log10(Box::new(self.lower_from_expr3(a)?)),
Expand Down
2 changes: 1 addition & 1 deletion src/simlin-engine/src/compiler/invariance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ where

// Pure scalar builtins of invariant arguments.
Abs(a) | Arccos(a) | Arcsin(a) | Arctan(a) | Cos(a) | Exp(a) | Int(a) | Ln(a)
| Log10(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) => all(&[a]),
| Log10(a) | Round(a) | Sign(a) | Sin(a) | Sqrt(a) | Tan(a) => all(&[a]),
Max(a, b) | Min(a, b) => {
expr_is_invariant(a, classify_ref)
&& b.as_ref()
Expand Down
2 changes: 2 additions & 0 deletions src/simlin-engine/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,7 @@ fn collect_expr_array_views(expr: &Expr, out: &mut Vec<ArrayView>) {
| BuiltinFn::Int(e)
| BuiltinFn::Ln(e)
| BuiltinFn::Log10(e)
| BuiltinFn::Round(e)
| BuiltinFn::Sign(e)
| BuiltinFn::Sin(e)
| BuiltinFn::Sqrt(e)
Expand Down Expand Up @@ -3026,6 +3027,7 @@ fn extract_temp_sizes_from_builtin(builtin: &BuiltinFn, temp_sizes_map: &mut Has
| BuiltinFn::Int(expr)
| BuiltinFn::Ln(expr)
| BuiltinFn::Log10(expr)
| BuiltinFn::Round(expr)
| BuiltinFn::Sign(expr)
| BuiltinFn::Sin(expr)
| BuiltinFn::Size(expr)
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/compiler/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub fn pretty(expr: &Expr) -> String {
BuiltinFn::Exp(l) => format!("exp({})", pretty(l)),
BuiltinFn::Inf => "\u{221e}".to_string(),
BuiltinFn::Int(l) => format!("int({})", pretty(l)),
BuiltinFn::Round(l) => format!("round({})", pretty(l)),
BuiltinFn::IsModuleInput(ident, _loc) => format!("isModuleInput({ident})"),
BuiltinFn::Ln(l) => format!("ln({})", pretty(l)),
BuiltinFn::Log10(l) => format!("log10({})", pretty(l)),
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/db/assemble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ fn collect_expr_refs(exprs: &[crate::compiler::Expr], out: &mut HashSet<Ident<Ca
| BuiltinFn::Int(a)
| BuiltinFn::Ln(a)
| BuiltinFn::Log10(a)
| BuiltinFn::Round(a)
| BuiltinFn::Sign(a)
| BuiltinFn::Sin(a)
| BuiltinFn::Sqrt(a)
Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/db/ltm/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ fn expr_contains_pass1_decomposition_site(expr: &crate::ast::Expr2) -> bool {
| BuiltinFn::Pulse(_, _, _)
| BuiltinFn::Quantum(_, _)
| BuiltinFn::Ramp(_, _, _)
| BuiltinFn::Round(_)
| BuiltinFn::SafeDiv(_, _, _)
| BuiltinFn::Sign(_)
| BuiltinFn::Sshape(_, _, _)
Expand Down
2 changes: 2 additions & 0 deletions src/simlin-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub mod rapidhash;
mod results;
#[cfg(test)]
mod rk_integration_tests;
#[cfg(test)]
mod round_builtin_tests;
pub mod serde;
#[path = "stdlib.gen.rs"]
mod stdlib;
Expand Down
8 changes: 5 additions & 3 deletions src/simlin-engine/src/ltm/polarity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,16 @@ fn analyze_builtin_polarity(
arg_polarity.compose(lookup_table_polarity(table_expr, variables))
}
// Non-decreasing single-arg builtins: propagate inner polarity.
// Int (floor) is a step function with discontinuities, but is still
// non-decreasing, which is sufficient for polarity propagation.
// Int (floor) and Round (nearest, ties to even) are step functions
// with discontinuities, but are still non-decreasing, which is
// sufficient for polarity propagation.
BuiltinFn::Exp(inner)
| BuiltinFn::Ln(inner)
| BuiltinFn::Log10(inner)
| BuiltinFn::Sqrt(inner)
| BuiltinFn::Arctan(inner)
| BuiltinFn::Int(inner) => {
| BuiltinFn::Int(inner)
| BuiltinFn::Round(inner) => {
analyze_expr_polarity_with_context(inner, from_var, current_polarity, variables)
}
// Max/Min (scalar two-arg form): non-decreasing in each argument
Expand Down
4 changes: 3 additions & 1 deletion src/simlin-engine/src/ltm/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,14 +1633,16 @@ fn test_builtin_polarity_monotone_increasing() {
let x_expr = || Box::new(Expr2::Var(x_var.clone(), None, Loc::default()));
let empty_vars = HashMap::new();

// Exp(x), Ln(x), Log10(x), Sqrt(x), Arctan(x), Int(x) all propagate polarity
// Every arm of polarity.rs's non-decreasing single-arg builtin group:
// Exp, Ln, Log10, Sqrt, Arctan, Int, and Round all propagate polarity.
let monotone_fns: Vec<(&str, BuiltinFn<Expr2>)> = vec![
("Exp", BuiltinFn::Exp(x_expr())),
("Ln", BuiltinFn::Ln(x_expr())),
("Log10", BuiltinFn::Log10(x_expr())),
("Sqrt", BuiltinFn::Sqrt(x_expr())),
("Arctan", BuiltinFn::Arctan(x_expr())),
("Int", BuiltinFn::Int(x_expr())),
("Round", BuiltinFn::Round(x_expr())),
];

for (name, builtin) in monotone_fns {
Expand Down
Loading
Loading