@@ -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,43 +25,18 @@ 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. Callers must ensure that
30+ * container values (Obj/Arr) are never passed to this method — they are handled by the iterative
31+ * stack-based loop in [[materializeContainer ]]. Passing a container will fall through to the
32+ * catch-all branch and throw an error.
33+ */
34+ private def materializeLeaf [T ](
35+ v : Val ,
36+ visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = {
2137 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 )
38+ case Val .Str (pos, s) => storePos(pos); visitor.visitString(s, - 1 )
39+ case Val .Num (pos, _) => storePos(pos); visitor.visitFloat64(v.asDouble, - 1 )
5740 case Val .True (pos) => storePos(pos); visitor.visitTrue(- 1 )
5841 case Val .False (pos) => storePos(pos); visitor.visitFalse(- 1 )
5942 case Val .Null (pos) => storePos(pos); visitor.visitNull(- 1 )
@@ -75,13 +58,235 @@ abstract class Materializer {
7558 case null =>
7659 Error .fail(" Unknown value type " + v)
7760 }
78- } catch {
79- case _ : StackOverflowError =>
80- Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
81- case _ : OutOfMemoryError =>
82- Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
8361 }
8462
63+ /**
64+ * Hybrid materialization: uses JVM stack recursion for shallow nesting (zero heap allocation,
65+ * JIT-friendly) and automatically switches to an explicit stack-based iterative loop when the
66+ * recursion depth exceeds [[Settings.materializeRecursiveDepthLimit ]]. This gives optimal
67+ * performance for the 99.9% common case while still handling arbitrarily deep structures (e.g.
68+ * those built via TCO) without StackOverflowError.
69+ */
70+ def apply0 [T ](v : Val , visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = v match {
71+ case obj : Val .Obj => materializeRecursive(obj, visitor, 0 )
72+ case xs : Val .Arr => materializeRecursive(xs, visitor, 0 )
73+ case _ => materializeLeaf(v, visitor)
74+ }
75+
76+ // Recursive materialization for shallow nesting. Each container consumes one JVM stack frame.
77+ // When depth reaches settings.materializeRecursiveDepthLimit, switches to the iterative materializeContainer to
78+ // avoid StackOverflowError. The method is kept small to encourage JIT inlining.
79+ private def materializeRecursive [T ](v : Val , visitor : Visitor [T , T ], depth : Int )(implicit
80+ evaluator : EvalScope ): T = {
81+ val sort = ! evaluator.settings.preserveOrder
82+ val brokenAssertionLogic = evaluator.settings.brokenAssertionLogic
83+ val emptyPos = evaluator.emptyMaterializeFileScopePos
84+ v match {
85+ case obj : Val .Obj =>
86+ storePos(obj.pos)
87+ obj.triggerAllAsserts(brokenAssertionLogic)
88+ val keys =
89+ if (sort) obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
90+ else obj.visibleKeyNames
91+ val ov = visitor.visitObject(keys.length, jsonableKeys = true , - 1 )
92+ var i = 0
93+ var prevKey : String = null
94+ while (i < keys.length) {
95+ val key = keys(i)
96+ val childVal = obj.value(key, emptyPos)
97+ storePos(childVal)
98+ if (sort) {
99+ if (prevKey != null && Util .compareStringsByCodepoint(key, prevKey) <= 0 )
100+ Error .fail(
101+ s """ Internal error: Unexpected key " $key" after " $prevKey" in sorted object materialization """ ,
102+ childVal.pos
103+ )
104+ prevKey = key
105+ }
106+ ov.visitKeyValue(ov.visitKey(- 1 ).visitString(key, - 1 ))
107+ val sub = ov.subVisitor.asInstanceOf [Visitor [T , T ]]
108+ ov.visitValue(materializeRecursiveChild(childVal, sub, depth), - 1 )
109+ i += 1
110+ }
111+ ov.visitEnd(- 1 )
112+ case xs : Val .Arr =>
113+ storePos(xs.pos)
114+ val av = visitor.visitArray(xs.length, - 1 )
115+ var i = 0
116+ while (i < xs.length) {
117+ val childVal = xs.value(i)
118+ val sub = av.subVisitor.asInstanceOf [Visitor [T , T ]]
119+ av.visitValue(materializeRecursiveChild(childVal, sub, depth), - 1 )
120+ i += 1
121+ }
122+ av.visitEnd(- 1 )
123+ case _ =>
124+ materializeLeaf(v, visitor)
125+ }
126+ }
127+
128+ // Materialize a child value during recursive mode. Leaf values are handled directly;
129+ // container children either recurse (if depth < limit) or switch to iterative mode.
130+ private def materializeRecursiveChild [T ](childVal : Val , childVisitor : Visitor [T , T ], depth : Int )(
131+ implicit evaluator : EvalScope ): T = {
132+ if (! childVal.isInstanceOf [Val .Obj ] && ! childVal.isInstanceOf [Val .Arr ]) {
133+ materializeLeaf(childVal, childVisitor)
134+ } else {
135+ val nextDepth = depth + 1
136+ if (nextDepth < evaluator.settings.materializeRecursiveDepthLimit)
137+ materializeRecursive(childVal, childVisitor, nextDepth)
138+ else
139+ materializeContainer(childVal, childVisitor)
140+ }
141+ }
142+
143+ // Iterative materialization for deep nesting. Used as a fallback when recursive depth exceeds
144+ // settings.materializeRecursiveDepthLimit. Uses an explicit ArrayDeque stack to avoid StackOverflowError.
145+ private def materializeContainer [T ](v : Val , visitor : Visitor [T , T ])(implicit
146+ evaluator : EvalScope ): T = {
147+ try {
148+ val maxDepth = evaluator.settings.maxMaterializeDepth
149+ val sort = ! evaluator.settings.preserveOrder
150+ val brokenAssertionLogic = evaluator.settings.brokenAssertionLogic
151+ val emptyPos = evaluator.emptyMaterializeFileScopePos
152+ val stack = new java.util.ArrayDeque [Materializer .MaterializeFrame ](
153+ evaluator.settings.materializeRecursiveDepthLimit << 2
154+ )
155+
156+ // Push the initial container frame
157+ v match {
158+ case obj : Val .Obj => pushObjFrame(obj, visitor, stack, maxDepth, sort, brokenAssertionLogic)
159+ case xs : Val .Arr => pushArrFrame(xs, visitor, stack, maxDepth)
160+ case _ => () // unreachable
161+ }
162+
163+ while (true ) {
164+ stack.peekFirst() match {
165+ case frame : Materializer .MaterializeObjFrame [T @ unchecked] =>
166+ val keys = frame.keys
167+ val ov = frame.objVisitor
168+ if (frame.index < keys.length) {
169+ val key = keys(frame.index)
170+ val childVal = frame.obj.value(key, emptyPos)
171+ storePos(childVal)
172+
173+ if (frame.sort) {
174+ if (
175+ frame.prevKey != null && Util .compareStringsByCodepoint(key, frame.prevKey) <= 0
176+ )
177+ Error .fail(
178+ s """ Internal error: Unexpected key " $key" after " ${frame.prevKey}" in sorted object materialization """ ,
179+ childVal.pos
180+ )
181+ frame.prevKey = key
182+ }
183+
184+ ov.visitKeyValue(ov.visitKey(- 1 ).visitString(key, - 1 ))
185+ frame.index += 1
186+
187+ val sub = ov.subVisitor.asInstanceOf [Visitor [T , T ]]
188+ materializeChild(childVal, sub, ov, stack, maxDepth, sort, brokenAssertionLogic)
189+ } else {
190+ val result = ov.visitEnd(- 1 )
191+ stack.removeFirst()
192+ if (stack.isEmpty) return result
193+ feedResult(stack.peekFirst(), result)
194+ }
195+
196+ case frame : Materializer .MaterializeArrFrame [T @ unchecked] =>
197+ val arr = frame.arr
198+ val av = frame.arrVisitor
199+ if (frame.index < arr.length) {
200+ val childVal = arr.value(frame.index)
201+ frame.index += 1
202+
203+ val sub = av.subVisitor.asInstanceOf [Visitor [T , T ]]
204+ materializeChild(childVal, sub, av, stack, maxDepth, sort, brokenAssertionLogic)
205+ } else {
206+ val result = av.visitEnd(- 1 )
207+ stack.removeFirst()
208+ if (stack.isEmpty) return result
209+ feedResult(stack.peekFirst(), result)
210+ }
211+ }
212+ }
213+
214+ null .asInstanceOf [T ] // unreachable — while(true) exits via return
215+ } catch {
216+ case _ : StackOverflowError =>
217+ Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
218+ case _ : OutOfMemoryError =>
219+ Error .fail(" Out of memory while materializing, possibly due to recursive value" , v.pos)
220+ }
221+ }
222+
223+ // Materialize a child value in iterative mode: leaf fast-path avoids a full pattern match for
224+ // the common case (strings, numbers, booleans, null). Only containers push a new frame.
225+ private def materializeChild [T ](
226+ childVal : Val ,
227+ childVisitor : Visitor [T , T ],
228+ parentVisitor : upickle.core.ObjArrVisitor [T , T ],
229+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
230+ maxDepth : Int ,
231+ sort : Boolean ,
232+ brokenAssertionLogic : Boolean )(implicit evaluator : EvalScope ): Unit = {
233+ if (! childVal.isInstanceOf [Val .Obj ] && ! childVal.isInstanceOf [Val .Arr ]) {
234+ parentVisitor.visitValue(materializeLeaf(childVal, childVisitor), - 1 )
235+ } else
236+ childVal match {
237+ case obj : Val .Obj =>
238+ pushObjFrame(obj, childVisitor, stack, maxDepth, sort, brokenAssertionLogic)
239+ case xs : Val .Arr =>
240+ pushArrFrame(xs, childVisitor, stack, maxDepth)
241+ case _ => () // unreachable — guarded by isInstanceOf checks above
242+ }
243+ }
244+
245+ private def pushObjFrame [T ](
246+ obj : Val .Obj ,
247+ visitor : Visitor [T , T ],
248+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
249+ maxDepth : Int ,
250+ sort : Boolean ,
251+ brokenAssertionLogic : Boolean )(implicit evaluator : EvalScope ): Unit = {
252+ checkDepth(obj.pos, stack.size, maxDepth)
253+ storePos(obj.pos)
254+ obj.triggerAllAsserts(brokenAssertionLogic)
255+ val keyNames =
256+ if (sort) obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
257+ else obj.visibleKeyNames
258+ val objVisitor = visitor.visitObject(keyNames.length, jsonableKeys = true , - 1 )
259+ stack.push(new Materializer .MaterializeObjFrame [T ](objVisitor, keyNames, obj, sort, 0 , null ))
260+ }
261+
262+ private def pushArrFrame [T ](
263+ xs : Val .Arr ,
264+ visitor : Visitor [T , T ],
265+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
266+ maxDepth : Int )(implicit evaluator : EvalScope ): Unit = {
267+ checkDepth(xs.pos, stack.size, maxDepth)
268+ storePos(xs.pos)
269+ val arrVisitor = visitor.visitArray(xs.length, - 1 )
270+ stack.push(new Materializer .MaterializeArrFrame [T ](arrVisitor, xs, 0 ))
271+ }
272+
273+ // Feed a completed child result into the parent frame's visitor.
274+ private def feedResult [T ](parentFrame : Materializer .MaterializeFrame , result : T ): Unit =
275+ parentFrame match {
276+ case f : Materializer .MaterializeObjFrame [T @ unchecked] =>
277+ f.objVisitor.visitValue(result, - 1 )
278+ case f : Materializer .MaterializeArrFrame [T @ unchecked] =>
279+ f.arrVisitor.visitValue(result, - 1 )
280+ }
281+
282+ private def checkDepth (pos : Position , stackSize : Int , maxDepth : Int )(implicit
283+ ev : EvalErrorScope ): Unit =
284+ if (stackSize >= maxDepth)
285+ Error .fail(
286+ " Stackoverflow while materializing, possibly due to recursive value" ,
287+ pos
288+ )
289+
85290 def reverse (pos : Position , v : ujson.Value ): Val = v match {
86291 case ujson.True => Val .True (pos)
87292 case ujson.False => Val .False (pos)
@@ -156,6 +361,26 @@ object Materializer extends Materializer {
156361 final val emptyStringArray = new Array [String ](0 )
157362 final val emptyLazyArray = new Array [Eval ](0 )
158363
364+ /** Common parent for stack frames used in iterative materialization. */
365+ private [sjsonnet] sealed trait MaterializeFrame
366+
367+ /** Stack frame for in-progress object materialization. */
368+ private [sjsonnet] final class MaterializeObjFrame [T ](
369+ val objVisitor : ObjVisitor [T , T ],
370+ val keys : Array [String ],
371+ val obj : Val .Obj ,
372+ val sort : Boolean ,
373+ var index : Int ,
374+ var prevKey : String )
375+ extends MaterializeFrame
376+
377+ /** Stack frame for in-progress array materialization. */
378+ private [sjsonnet] final class MaterializeArrFrame [T ](
379+ val arrVisitor : ArrVisitor [T , T ],
380+ val arr : Val .Arr ,
381+ var index : Int )
382+ extends MaterializeFrame
383+
159384 /**
160385 * Trait for providing custom materialization logic to the Materializer.
161386 * @since 1.0.0
0 commit comments