Skip to content

fix: preserve TCO through error values#818

Open
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:fix/tco-error-value-directional-tests
Open

fix: preserve TCO through error values#818
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:fix/tco-error-value-directional-tests

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin commented May 1, 2026

Rebased onto current master at 7eb796f (chore(deps): bump actions/cache from 4.3.0 to 5.0.5 (#819)).

Motivation

error value evaluates value immediately before throwing. That value is a tail position for auto-TCO and explicit tailstrict, but resolving a TailCall inside the error expression would re-enter the trampoline from the current function body and can bring back recursive stack growth for deep error f(n - 1) chains.

error value also must not be treated as an unconditional non-recursive exit. A function like local f() = error f() has no base case; auto-TCOing it would turn the existing max-stack diagnostic into an infinite trampoline loop.

Implementation

  • Evaluate tail-position error value with tail-call support and attach a delayed error-value continuation to the returned TailCall.
  • Store delayed result continuations as fixed-slot state: optional &&/|| validation plus optional error value throw.
  • Avoid linked continuation nodes and per-wrapper check objects on deep recursive paths; the trampoline combines a small final container in place, which is more JIT- and GC-friendly.
  • Preserve continuation order:
    • inner error value preempts any outer boolean check or outer error throw
    • inner boolean validation runs before an outer error throw
    • redundant outer boolean validations collapse after the innermost boolean check
  • Update auto-TCO exit analysis so Expr.Error recursively inspects its value expression instead of always counting as a non-recursive exit.

Tests

New golden tests cover:

  • auto-TCO and explicit tailstrict through error value
  • lazy unused arguments through a 10000-deep recursive error f(n - 1, unused) chain
  • boolean-check-before-outer-error ordering
  • inner-error-before-outer-bool ordering
  • nested boolean continuation ordering
  • a non-tail lhs negative case
  • an error value expression whose non-recursive exit is inside the error value
  • a no-exit error f() case that must keep the max-stack diagnostic

Verification

  • ./mill --no-server 'sjsonnet.jvm[3.3.7].compile'
  • ./mill --no-server 'sjsonnet.jvm[3.3.7].test.testOnly' sjsonnet.FileTests
  • ./mill --no-server 'sjsonnet.jvm[3.3.7].test.testOnly' sjsonnet.TailCallOptimizationTests
  • ./mill --no-server __.checkFormat
  • git diff --check

@He-Pin He-Pin marked this pull request as draft May 1, 2026 04:36
@He-Pin He-Pin force-pushed the fix/tco-error-value-directional-tests branch from ba255b1 to 115eba9 Compare May 1, 2026 04:44
Motivation:
`error value` evaluates `value` immediately before throwing, so a tail-position self call there should not force nested trampoline resolution inside the current function body. The previous auto-TCO marker treated that value as tail position, but the evaluator still used normal `visitExpr`, leaving deep `error f(n - 1)` chains stack-sensitive. At the same time, `error value` must not be treated as an unconditional non-recursive exit: `local f() = error f()` has no base case and should keep the existing max-stack diagnostic instead of becoming an infinite trampoline loop.

Modification:
Add delayed TailCall result state for `error value` and fold it together with the existing delayed boolean validation. The result state is fixed-slot (`&&`/`||` validation plus optional error throw) rather than linked continuation nodes, so the trampoline preserves ordering without accumulating per-wrapper objects on deep recursive paths. Update auto-TCO exit analysis so `Expr.Error` recursively inspects its value expression instead of always counting as a non-recursive exit.

Result:
Auto-TCO and explicit `tailstrict` now remain stack-safe through `error value`, lazy arguments remain lazy, and nested boolean/error continuation ordering matches expression evaluation order. Directional golden tests cover the new error path, continuation ordering, a deep non-tail negative case, an `error value` expression with an internal base case, and a no-exit `error f()` case that must not be auto-TCOd.

Risk:
The main risk is continuation ordering and exit analysis: a boolean check inside an error message must run before the outer error throw, an inner error must preempt an outer boolean check, and no-exit recursive error expressions must not be converted into unbounded trampoline loops. The new tests lock down these orders and the no-exit guard. This is still scoped to the current auto-TCO protocol; broader proper-tail-call lowering remains a separate follow-up.
@He-Pin He-Pin force-pushed the fix/tco-error-value-directional-tests branch from 115eba9 to 6f1f2ee Compare May 2, 2026 17:39
@He-Pin He-Pin marked this pull request as ready for review May 2, 2026 17:43
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