From b54fbfd85ee376d43107c80a17eb3aa70c5966a7 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 24 Jun 2026 12:58:51 +0800 Subject: [PATCH] fix: self-referential thunks produce clean error instead of StackOverflowError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: `local x = x; x` caused an uncontrolled java.lang.StackOverflowError with a raw Java stack trace. C++ jsonnet, go-jsonnet, and jrsonnet all produce clean, controlled error messages for self-referential thunks. Modification: - Val.scala: Add `evaluating` boolean flag to LazyExpr. When value is first requested, set the flag to true. If value is requested again while the flag is true, it's a cycle — throw a clean error: "Infinite recursion detected (self-referential thunk)". Result: Self-referential thunks now produce a clean sjsonnet.Error instead of crashing the JVM. Cross-implementation comparison: | Expression | cpp-jsonnet | go-jsonnet | jrsonnet | sjsonnet (before) | sjsonnet (after) | |---|---|---|---|---|---| | local x = x; x | "max stack frames" | "max stack frames" | "infinite recursion" | StackOverflowError 💥 | "Infinite recursion detected" ✅ | | local f() = f(); f() | "max stack frames" | "max stack frames" | "stack overflow" | "Max stack frames" ✅ | "Max stack frames" ✅ | | local x = 1+2; x | 3 | 3 | 3 | 3 ✅ | 3 ✅ | --- sjsonnet/src/sjsonnet/Val.scala | 7 ++++++- .../new_test_suite/error.self_referential_thunk.jsonnet | 1 + .../error.self_referential_thunk.jsonnet.golden | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet.golden diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index b5d1ae62..a547efd5 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -71,13 +71,18 @@ final class LazyExpr( private var scope: ValScope, private var ev: Evaluator) extends Lazy { + private var evaluating: Boolean = false def value: Val = { if (ev == null) exprOrVal.asInstanceOf[Val] - else { + else if (evaluating) { + Error.fail("Infinite recursion detected (self-referential thunk)") + } else { + evaluating = true val r = ev.visitExpr(exprOrVal.asInstanceOf[Expr])(scope) exprOrVal = r // cache result scope = null.asInstanceOf[sjsonnet.ValScope] // allow GC ev = null // sentinel: marks as computed + evaluating = false r } } diff --git a/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet b/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet new file mode 100644 index 00000000..f566a092 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet @@ -0,0 +1 @@ +local x = x; x diff --git a/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet.golden new file mode 100644 index 00000000..eb6d47c8 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.self_referential_thunk.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: Infinite recursion detected (self-referential thunk) + at [].(error.self_referential_thunk.jsonnet:1:7)