Skip to content

fix(calendar): replace add-mo panic with ILO-R009 on out-of-range#561

Merged
danieljohnmorris merged 2 commits into
mainfrom
fix/add-mo-range
May 21, 2026
Merged

fix(calendar): replace add-mo panic with ILO-R009 on out-of-range#561
danieljohnmorris merged 2 commits into
mainfrom
fix/add-mo-range

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

add-mo was crashing the interpreter with a Rust panic in debug builds (and silently producing wrong dates in release) when called with extreme month offsets like add-mo 0 2147483647. The bug landed with #495 (calendar arithmetic). The runtime should surface ILO-R009 cleanly so agents can recover, not panic — and never silently return the wrong date in release.

Root cause

src/interpreter/mod.rs::add_months_snap did its year arithmetic in i32:

let total = date.year() * 12 + (date.month() as i32 - 1) + months;

With months = i32::MAX, 1970 * 12 + 2147483647 overflows i32. Debug panics with attempt to add with overflow; release silently wraps to a valid (but wrong) NaiveDate, so the from_ymd_opt guard never tripped and the documented "result out of calendar range" error was unreachable.

The dedicated regression test add_mo_result_out_of_calendar_range was passing in release (silent wrap landed in chrono's range by luck) and crashing in debug.

Repro

$ cargo test --features cranelift add_mo_result_out_of_calendar_range
thread 'main' panicked at src/interpreter/mod.rs:2009:21:
attempt to add with overflow

After the fix:

$ cargo test --features cranelift add_mo_result_out_of_calendar_range
test add_mo_result_out_of_calendar_range ... ok

What's in the diff

  • fix(calendar): widen add-mo month arithmetic to i64 — compute total months in i64, narrow to i32 via try_from when constructing the date. Overflow now propagates cleanly to the existing None → ILO-R009 arm.
  • test(calendar): pin add-mo overflow boundaries + large-but-valid offsets — adds symmetric i32::MIN negative case, a 1000-year-forward "still valid" guard so the fix doesn't over-reject, and an examples/add-mo-out-of-range.ilo exercising the same path through the examples_engines harness.

VM and Cranelift route through the tree-bridge for add-mo, so a single fix in the interpreter covers all three engines.

Test plan

  • cargo test --features cranelift --test regression_calendar_arithmetic — 36/36 in debug
  • cargo test --release --features cranelift --test regression_calendar_arithmetic — 36/36 in release
  • cargo fmt --check clean
  • Example runs end-to-end via ilo run and the examples_engines harness

Follow-ups

None directly tied to this fix. Pre-existing clippy + JIT test failures on main (the OP_TAILCALL match-arm warnings and various vm::jit_cranelift::tests cases) are not from this change and will need separate attention.

The internal `date.year() * 12 + months` calculation in add_months_snap
was i32 and overflowed when `months` approached i32::MAX or i32::MIN.
In debug builds this panicked with 'attempt to add with overflow'; in
release it silently wrapped to a valid (but wrong) NaiveDate, so the
calendar-range guard never tripped.

Widen the intermediate to i64 and narrow back via i32::try_from when
constructing the chrono date. Out-of-range results now surface as the
documented ILO-R009 'add-mo: result out of calendar range' via the
existing None arm.
The existing add_mo_result_out_of_calendar_range test only exercised
the positive i32::MAX path. Add:

- add_mo_result_out_of_calendar_range_negative: symmetric i32::MIN
  case so an asymmetric fix can't slip through.
- add_mo_large_but_valid_offset: 1000-year (12000-month) forward jump
  to guard against the overflow fix being too aggressive and
  rejecting plausible long offsets.

Plus an examples/add-mo-out-of-range.ilo demonstrating the long-offset
case across engines via the examples_engines harness.
@danieljohnmorris danieljohnmorris merged commit 9f01ce1 into main May 21, 2026
1 of 4 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/add-mo-range branch May 21, 2026 15:47
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1828 2 1826 0
View the top 2 failed test(s) by shortest run time
ilo::vm::compile_cranelift::tests::aot_sequential_cross_function_calls
Stack Traces | 2.96s run time
thread 'vm::compile_cranelift::tests::aot_sequential_cross_function_calls' (32699) panicked at src/vm/compile_cranelift.rs:5170:9:
assertion `left == right` failed: dbl(5)=10, triple(10)=30
  left: "nil"
 right: "30"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ilo::vm::compile_cranelift::tests::aot_pipe_chain
Stack Traces | 3.03s run time
thread 'vm::compile_cranelift::tests::aot_pipe_chain' (32698) panicked at src/vm/compile_cranelift.rs:5188:9:
assertion `left == right` failed: 4*5+3=23
  left: "nil"
 right: "23"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant