@@ -2,11 +2,19 @@ package sjsonnet
22
33import sjsonnet .Expr .{FieldName , Member , ObjBody }
44import sjsonnet .Expr .Member .Visibility
5- import upickle .core .Visitor
5+ import upickle .core .{ ArrVisitor , ObjVisitor , Visitor }
66
77/**
88 * Serializes the given [[Val ]] out to the given [[upickle.core.Visitor ]], which can transform it
9- * into [[ujson.Value ]]s or directly serialize it to `String`s
9+ * into [[ujson.Value ]]s or directly serialize it to `String`s.
10+ *
11+ * TCO boundary: all [[Val ]] values entering materialization — whether from object field evaluation
12+ * (`Val.Obj.value`), array element forcing (`Val.Arr.value`), or top-level evaluation — must not
13+ * contain unresolved [[TailCall ]] sentinels. This invariant is maintained by the evaluator: object
14+ * field `invoke` calls `visitExpr` (not `visitExprWithTailCallSupport`), and `Val.Func.apply*`
15+ * resolves TailCalls when called with `TailstrictModeDisabled`. A defensive check in
16+ * `materializeLeaf` guards against accidental TailCall leakage with a clear internal-error
17+ * diagnostic.
1018 */
1119abstract class Materializer {
1220 def storePos (pos : Position ): Unit
@@ -17,64 +25,200 @@ abstract class Materializer {
1725 apply0(v, new sjsonnet.Renderer ()).toString
1826 }
1927
20- def apply0 [T ](v : Val , visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = try {
28+ /**
29+ * Materialize a leaf value (non-container) to the given visitor. Returns the visitor result or
30+ * `null` if the value is a container (Obj/Arr) that needs stack-based processing.
31+ */
32+ private def materializeLeaf [T ](
33+ v : Val ,
34+ visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = {
2135 v match {
22- case Val .Str (pos, s) => storePos(pos); visitor.visitString(s, - 1 )
23- case obj : Val .Obj =>
24- storePos(obj.pos)
25- obj.triggerAllAsserts(evaluator.settings.brokenAssertionLogic)
26- val objVisitor = visitor.visitObject(obj.visibleKeyNames.length, jsonableKeys = true , - 1 )
27- val sort = ! evaluator.settings.preserveOrder
28- var prevKey : String = null
29- obj.foreachElement(sort, evaluator.emptyMaterializeFileScopePos) { (k, v) =>
30- storePos(v)
31- objVisitor.visitKeyValue(objVisitor.visitKey(- 1 ).visitString(k, - 1 ))
32- objVisitor.visitValue(
33- apply0(v, objVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]),
34- - 1
35- )
36- if (sort) {
37- if (prevKey != null && Util .compareStringsByCodepoint(k, prevKey) <= 0 )
38- Error .fail(
39- s """ Internal error: Unexpected key " $k" after " $prevKey" in sorted object materialization """ ,
40- v.pos
41- )
42- prevKey = k
43- }
44- }
45- objVisitor.visitEnd(- 1 )
46- case Val .Num (pos, _) => storePos(pos); visitor.visitFloat64(v.asDouble, - 1 )
47- case xs : Val .Arr =>
48- storePos(xs.pos)
49- val arrVisitor = visitor.visitArray(xs.length, - 1 )
50- var i = 0
51- while (i < xs.length) {
52- val sub = arrVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]
53- arrVisitor.visitValue(apply0(xs.value(i), sub), - 1 )
54- i += 1
55- }
56- arrVisitor.visitEnd(- 1 )
57- case Val .True (pos) => storePos(pos); visitor.visitTrue(- 1 )
58- case Val .False (pos) => storePos(pos); visitor.visitFalse(- 1 )
59- case Val .Null (pos) => storePos(pos); visitor.visitNull(- 1 )
60- case s : Val .Func =>
36+ case Val .Str (pos, s) => storePos(pos); visitor.visitString(s, - 1 )
37+ case Val .Num (pos, _) => storePos(pos); visitor.visitFloat64(v.asDouble, - 1 )
38+ case Val .True (pos) => storePos(pos); visitor.visitTrue(- 1 )
39+ case Val .False (pos) => storePos(pos); visitor.visitFalse(- 1 )
40+ case Val .Null (pos) => storePos(pos); visitor.visitNull(- 1 )
41+ case mat : Materializer .Materializable => storePos(v.pos); mat.materialize(visitor)
42+ case s : Val .Func =>
6143 Error .fail(
6244 " Couldn't manifest function with params [" + s.params.names.mkString(" ," ) + " ]" ,
6345 v.pos
6446 )
65- case mat : Materializer .Materializable => storePos(v.pos); mat.materialize(visitor)
66- case vv : Val =>
47+ case tc : TailCall =>
48+ Error .fail(
49+ " Internal error: TailCall sentinel leaked into materialization. " +
50+ " This indicates a bug in the TCO protocol — a TailCall was not resolved before " +
51+ " reaching the Materializer." ,
52+ tc.pos
53+ )
54+ case vv : Val =>
6755 Error .fail(" Unknown value type " + vv.prettyName, vv.pos)
6856 case null =>
6957 Error .fail(" Unknown value type " + v)
7058 }
71- } catch {
72- case _ : StackOverflowError =>
73- Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
74- case _ : OutOfMemoryError =>
75- Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
7659 }
7760
61+ /**
62+ * Iterative materialization using an explicit stack. Replaces the previous recursive
63+ * implementation to eliminate StackOverflowError on deeply nested JSON structures.
64+ *
65+ * The stack holds frames representing in-progress container (object/array) materializations. Each
66+ * iteration either:
67+ * 1. Processes the next child of the current container, pushing a new frame if the child is
68+ * itself a container.
69+ * 2. Completes the current container (visitEnd) and pops the frame, feeding the result back to
70+ * the parent frame via visitValue.
71+ *
72+ * A depth limit guards against infinite recursion from self-referential values (e.g.
73+ * `local L = [L]; L`), throwing a descriptive error instead of OOM.
74+ */
75+ def apply0 [T ](v : Val , visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = {
76+ import Materializer .{MaterializeObjFrame , MaterializeArrFrame }
77+
78+ // --- fast path for non-container values (avoids stack allocation) ---
79+ v match {
80+ case _ : Val .Obj | _ : Val .Arr => // fall through to iterative path
81+ case _ => return materializeLeaf(v, visitor)
82+ }
83+
84+ try {
85+ val maxDepth = evaluator.settings.maxMaterializeDepth
86+ val stack = new java.util.ArrayDeque [Materializer .MaterializeFrame ](16 )
87+ var pendingVal : Val = v
88+ var pendingVisitor : Visitor [T , T ] = visitor
89+ var leafResult : T = null .asInstanceOf [T ]
90+ var hasLeafResult = false
91+
92+ while (true ) {
93+ // Step 1: If we have a pending value to process, handle it
94+ if (pendingVal != null ) {
95+ val currentVal = pendingVal
96+ val currentVisitor = pendingVisitor
97+ pendingVal = null
98+ pendingVisitor = null
99+
100+ currentVal match {
101+ case obj : Val .Obj =>
102+ checkDepth(obj.pos, stack.size, maxDepth)
103+ storePos(obj.pos)
104+ obj.triggerAllAsserts(evaluator.settings.brokenAssertionLogic)
105+ val sort = ! evaluator.settings.preserveOrder
106+ val keyNames = if (sort) {
107+ obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
108+ } else {
109+ obj.visibleKeyNames
110+ }
111+ val objVisitor =
112+ currentVisitor.visitObject(keyNames.length, jsonableKeys = true , - 1 )
113+ stack.push(new MaterializeObjFrame [T ](objVisitor, keyNames, obj, sort, 0 , null ))
114+
115+ case xs : Val .Arr =>
116+ checkDepth(xs.pos, stack.size, maxDepth)
117+ storePos(xs.pos)
118+ val arrVisitor = currentVisitor.visitArray(xs.length, - 1 )
119+ stack.push(new MaterializeArrFrame [T ](arrVisitor, xs, 0 ))
120+
121+ case _ =>
122+ leafResult = materializeLeaf(currentVal, currentVisitor)
123+ hasLeafResult = true
124+ }
125+ }
126+
127+ // Step 2+3: Process the current top-of-stack frame (feeding any pending leaf result first)
128+ if (stack.isEmpty) {
129+ if (hasLeafResult) return leafResult
130+ Error .fail(" Internal error in iterative materializer" )
131+ }
132+
133+ stack.peekFirst() match {
134+ case frame : MaterializeObjFrame [T @ unchecked] =>
135+ if (hasLeafResult) {
136+ frame.objVisitor.visitValue(leafResult, - 1 )
137+ hasLeafResult = false
138+ leafResult = null .asInstanceOf [T ]
139+ }
140+ if (frame.index < frame.keys.length) {
141+ val key = frame.keys(frame.index)
142+ val value = frame.obj.value(key, evaluator.emptyMaterializeFileScopePos)
143+ storePos(value)
144+
145+ if (frame.sort) {
146+ if (
147+ frame.prevKey != null && Util .compareStringsByCodepoint(key, frame.prevKey) <= 0
148+ )
149+ Error .fail(
150+ s """ Internal error: Unexpected key " $key" after " ${frame.prevKey}" in sorted object materialization """ ,
151+ value.pos
152+ )
153+ frame.prevKey = key
154+ }
155+
156+ frame.objVisitor.visitKeyValue(frame.objVisitor.visitKey(- 1 ).visitString(key, - 1 ))
157+ frame.index += 1
158+
159+ value match {
160+ case _ : Val .Obj | _ : Val .Arr =>
161+ pendingVal = value
162+ pendingVisitor = frame.objVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]
163+ case _ =>
164+ val sub = frame.objVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]
165+ val childResult = materializeLeaf(value, sub)
166+ frame.objVisitor.visitValue(childResult, - 1 )
167+ }
168+ } else {
169+ val result = frame.objVisitor.visitEnd(- 1 )
170+ stack.pop()
171+ if (stack.isEmpty) return result
172+ leafResult = result
173+ hasLeafResult = true
174+ }
175+
176+ case frame : MaterializeArrFrame [T @ unchecked] =>
177+ if (hasLeafResult) {
178+ frame.arrVisitor.visitValue(leafResult, - 1 )
179+ hasLeafResult = false
180+ leafResult = null .asInstanceOf [T ]
181+ }
182+ if (frame.index < frame.arr.length) {
183+ val childVal = frame.arr.value(frame.index)
184+ frame.index += 1
185+
186+ childVal match {
187+ case _ : Val .Obj | _ : Val .Arr =>
188+ pendingVal = childVal
189+ pendingVisitor = frame.arrVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]
190+ case _ =>
191+ val sub = frame.arrVisitor.subVisitor.asInstanceOf [Visitor [T , T ]]
192+ val childResult = materializeLeaf(childVal, sub)
193+ frame.arrVisitor.visitValue(childResult, - 1 )
194+ }
195+ } else {
196+ val result = frame.arrVisitor.visitEnd(- 1 )
197+ stack.pop()
198+ if (stack.isEmpty) return result
199+ leafResult = result
200+ hasLeafResult = true
201+ }
202+ }
203+ }
204+
205+ null .asInstanceOf [T ] // unreachable
206+ } catch {
207+ case _ : StackOverflowError =>
208+ Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
209+ case _ : OutOfMemoryError =>
210+ Error .fail(" Out of memory while materializing, possibly due to recursive value" , v.pos)
211+ }
212+ }
213+
214+ private def checkDepth (pos : Position , stackSize : Int , maxDepth : Int )(implicit
215+ ev : EvalErrorScope ): Unit =
216+ if (stackSize >= maxDepth)
217+ Error .fail(
218+ " Stackoverflow while materializing, possibly due to recursive value" ,
219+ pos
220+ )
221+
78222 def reverse (pos : Position , v : ujson.Value ): Val = v match {
79223 case ujson.True => Val .True (pos)
80224 case ujson.False => Val .False (pos)
@@ -149,6 +293,26 @@ object Materializer extends Materializer {
149293 final val emptyStringArray = new Array [String ](0 )
150294 final val emptyLazyArray = new Array [Eval ](0 )
151295
296+ /** Common parent for stack frames used in iterative materialization. */
297+ private [sjsonnet] sealed trait MaterializeFrame
298+
299+ /** Stack frame for in-progress object materialization. */
300+ private [sjsonnet] final class MaterializeObjFrame [T ](
301+ val objVisitor : ObjVisitor [T , T ],
302+ val keys : Array [String ],
303+ val obj : Val .Obj ,
304+ val sort : Boolean ,
305+ var index : Int ,
306+ var prevKey : String )
307+ extends MaterializeFrame
308+
309+ /** Stack frame for in-progress array materialization. */
310+ private [sjsonnet] final class MaterializeArrFrame [T ](
311+ val arrVisitor : ArrVisitor [T , T ],
312+ val arr : Val .Arr ,
313+ var index : Int )
314+ extends MaterializeFrame
315+
152316 /**
153317 * Trait for providing custom materialization logic to the Materializer.
154318 * @since 1.0.0
0 commit comments