Skip to content

fix: error thunk propagation and std.round(-0) sign#1044

Open
He-Pin wants to merge 2 commits into
databricks:masterfrom
He-Pin:fix/round-negative-zero-yaml-help
Open

fix: error thunk propagation and std.round(-0) sign#1044
He-Pin wants to merge 2 commits into
databricks:masterfrom
He-Pin:fix/round-negative-zero-yaml-help

Conversation

@He-Pin

@He-Pin He-Pin commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Motivation

Two bugs found via adversarial testing against go-jsonnet and jrsonnet:

  1. Error thunk false "Infinite recursion": When an error thunk was forced inside a binary operator, sjsonnet incorrectly reported "Infinite recursion detected (self-referential thunk)" instead of propagating the original error. Root cause: tryEvalCatch caught the sjsonnet.Error (which is NonFatal), returned null, and the main dispatch re-evaluated the thunk which still had its evaluating flag set to true.

  2. std.round(-0) returned 0: std.round(-0) returned positive zero instead of negative zero. Both go-jsonnet and jrsonnet correctly return -0. Root cause: x >= 0 is true for -0.0 (IEEE 754: -0 == +0), so it took math.floor(-0 + 0.5) = 0.0 (positive zero).

Expression sjsonnet master go-jsonnet jrsonnet this PR
local a = error "fail"; local b = a + 1; b Infinite recursion detected fail fail fail
std.round(-0) 0 -0 -0 -0
std.round(0) 0 0 0 0

Modification

Error thunk fix (Val.scala):

  • Wrap LazyExpr.value's computation in try/finally to guarantee the evaluating flag is reset even when the expression throws. This allows the main dispatch path to properly re-evaluate the thunk and propagate the original error.

std.round fix (MathModule.scala):

  • Add else if (x == 0) x early return before the positive/negative branches. Since 0.0 == -0.0 is true in IEEE 754, this catches both +0 and -0 and returns x preserving the original sign bit.

Result

  • All 8 affected operator patterns (+, -, *, /, string concat, !, >, array concat) now correctly propagate the original error.
  • True self-referential thunks (local a = a + 1; a) still correctly detected.
  • std.round(-0) returns -0, std.round(0) returns 0 — both signs preserved.
  • std.atan2(std.round(0), -1) > 0 is true; std.atan2(std.round(-0), -1) < 0 is true.
  • All tests pass on JVM 2.12, 2.13, 3.3.

@He-Pin He-Pin marked this pull request as ready for review June 25, 2026 10:59
Motivation:
Two bugs found via adversarial testing against go-jsonnet and jrsonnet:

1. When an error thunk was forced inside a binary operator, sjsonnet
   incorrectly reported "Infinite recursion detected (self-referential
   thunk)" instead of propagating the original error message. Root cause:
   tryEvalCatch caught the sjsonnet.Error (NonFatal), returned null, and
   the main dispatch re-evaluated the thunk which still had its
   `evaluating` flag set to true.

2. std.round(-0) returned 0 instead of -0, losing the IEEE 754 negative
   zero sign. Both go-jsonnet and jrsonnet correctly return -0.

Modification:
1. Wrap LazyExpr.value's computation in try/finally to guarantee the
   `evaluating` flag is reset even when the expression throws an error.
   This allows the main dispatch path to properly re-evaluate the
   thunk and propagate the original error.

2. Change `x >= 0` to `x > 0` in std.round so that -0.0 takes the
   negative branch (math.ceil preserves -0.0 sign per IEEE 754).

Result:
- `local a = error "fail"; local b = a + 1; b` now correctly reports
  "fail" instead of "Infinite recursion" (all 8 operator patterns fixed).
- True self-referential thunks (`local a = a + 1; a`) still correctly
  detected as infinite recursion.
- `std.round(-0)` returns -0, matching go-jsonnet and jrsonnet.
- All tests pass on JVM 2.12, 2.13, 3.3.
@He-Pin He-Pin force-pushed the fix/round-negative-zero-yaml-help branch from fe61a64 to 75c6cb9 Compare June 25, 2026 12:47
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