@@ -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
31+ * [[materializeRecursiveObj ]]/[[materializeRecursiveArr ]] (recursive mode) or
32+ * [[materializeIterative ]] (iterative fallback). Passing a container will fall through to the
33+ * catch-all branch and throw an error.
34+ */
35+ @ inline private def materializeLeaf [T ](v : Val , visitor : Visitor [T , T ])(implicit
36+ 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,236 @@ 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 ]].
67+ */
68+ def apply0 [T ](v : Val , visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = {
69+ val ctx = Materializer .MaterializeContext (evaluator)
70+ v match {
71+ case obj : Val .Obj => materializeRecursiveObj(obj, visitor, 0 , ctx)
72+ case xs : Val .Arr => materializeRecursiveArr(xs, visitor, 0 , ctx)
73+ case _ => materializeLeaf(v, visitor)
74+ }
75+ }
76+
77+ @ inline private def materializeRecursiveObj [T ](
78+ obj : Val .Obj ,
79+ visitor : Visitor [T , T ],
80+ depth : Int ,
81+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): T = {
82+ storePos(obj.pos)
83+ obj.triggerAllAsserts(ctx.brokenAssertionLogic)
84+ val keys =
85+ if (ctx.sort) obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
86+ else obj.visibleKeyNames
87+ val ov = visitor.visitObject(keys.length, jsonableKeys = true , - 1 )
88+ var i = 0
89+ var prevKey : String = null
90+ while (i < keys.length) {
91+ val key = keys(i)
92+ val childVal = obj.value(key, ctx.emptyPos)
93+ storePos(childVal)
94+ if (ctx.sort) {
95+ if (prevKey != null && Util .compareStringsByCodepoint(key, prevKey) <= 0 )
96+ Error .fail(
97+ s """ Internal error: Unexpected key " $key" after " $prevKey" in sorted object materialization """ ,
98+ childVal.pos
99+ )
100+ prevKey = key
101+ }
102+ ov.visitKeyValue(ov.visitKey(- 1 ).visitString(key, - 1 ))
103+ val sub = ov.subVisitor.asInstanceOf [Visitor [T , T ]]
104+ ov.visitValue(materializeRecursiveChild(childVal, sub, depth, ctx), - 1 )
105+ i += 1
106+ }
107+ ov.visitEnd(- 1 )
108+ }
109+
110+ @ inline private def materializeRecursiveArr [T ](
111+ xs : Val .Arr ,
112+ visitor : Visitor [T , T ],
113+ depth : Int ,
114+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): T = {
115+ storePos(xs.pos)
116+ val av = visitor.visitArray(xs.length, - 1 )
117+ var i = 0
118+ while (i < xs.length) {
119+ val childVal = xs.value(i)
120+ av.visitValue(
121+ materializeRecursiveChild(childVal, av.subVisitor.asInstanceOf [Visitor [T , T ]], depth, ctx),
122+ - 1
123+ )
124+ i += 1
125+ }
126+ av.visitEnd(- 1 )
127+ }
128+
129+ @ inline private def materializeRecursiveChild [T ](
130+ childVal : Val ,
131+ childVisitor : Visitor [T , T ],
132+ depth : Int ,
133+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): T = childVal match {
134+ case obj : Val .Obj =>
135+ val nextDepth = depth + 1
136+ if (nextDepth < ctx.recursiveDepthLimit)
137+ materializeRecursiveObj(obj, childVisitor, nextDepth, ctx)
138+ else
139+ materializeStackless(childVal, childVisitor, ctx)
140+ case xs : Val .Arr =>
141+ val nextDepth = depth + 1
142+ if (nextDepth < ctx.recursiveDepthLimit)
143+ materializeRecursiveArr(xs, childVisitor, nextDepth, ctx)
144+ else
145+ materializeStackless(childVal, childVisitor, ctx)
146+ case _ =>
147+ materializeLeaf(childVal, childVisitor)
148+ }
149+
150+ // Iterative materialization for deep nesting. Used as a fallback when recursive depth exceeds
151+ // the recursive depth limit. Uses an explicit ArrayDeque stack to avoid StackOverflowError.
152+ private def materializeStackless [T ](
153+ v : Val ,
154+ visitor : Visitor [T , T ],
155+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): T = {
156+ try {
157+ val stack = new java.util.ArrayDeque [Materializer .MaterializeFrame ](
158+ ctx.recursiveDepthLimit << 2
159+ )
160+
161+ // Push the initial container frame
162+ v match {
163+ case obj : Val .Obj => pushObjFrame(obj, visitor, stack, ctx)
164+ case xs : Val .Arr => pushArrFrame(xs, visitor, stack, ctx)
165+ case _ => () // unreachable
166+ }
167+
168+ while (true ) {
169+ stack.peekFirst() match {
170+ case frame : Materializer .MaterializeObjFrame [T @ unchecked] =>
171+ val keys = frame.keys
172+ val ov = frame.objVisitor
173+ if (frame.index < keys.length) {
174+ val key = keys(frame.index)
175+ val childVal = frame.obj.value(key, ctx.emptyPos)
176+ storePos(childVal)
177+
178+ if (frame.sort) {
179+ if (
180+ frame.prevKey != null && Util .compareStringsByCodepoint(key, frame.prevKey) <= 0
181+ )
182+ Error .fail(
183+ s """ Internal error: Unexpected key " $key" after " ${frame.prevKey}" in sorted object materialization """ ,
184+ childVal.pos
185+ )
186+ frame.prevKey = key
187+ }
188+
189+ ov.visitKeyValue(ov.visitKey(- 1 ).visitString(key, - 1 ))
190+ frame.index += 1
191+
192+ val sub = ov.subVisitor.asInstanceOf [Visitor [T , T ]]
193+ materializeChild(childVal, sub, ov, stack, ctx)
194+ } else {
195+ val result = ov.visitEnd(- 1 )
196+ stack.removeFirst()
197+ if (stack.isEmpty) return result
198+ feedResult(stack.peekFirst(), result)
199+ }
200+
201+ case frame : Materializer .MaterializeArrFrame [T @ unchecked] =>
202+ val arr = frame.arr
203+ val av = frame.arrVisitor
204+ if (frame.index < arr.length) {
205+ val childVal = arr.value(frame.index)
206+ frame.index += 1
207+
208+ val sub = av.subVisitor.asInstanceOf [Visitor [T , T ]]
209+ materializeChild(childVal, sub, av, stack, ctx)
210+ } else {
211+ val result = av.visitEnd(- 1 )
212+ stack.removeFirst()
213+ if (stack.isEmpty) return result
214+ feedResult(stack.peekFirst(), result)
215+ }
216+ }
217+ }
218+
219+ null .asInstanceOf [T ] // unreachable — while(true) exits via return
220+ } catch {
221+ case _ : StackOverflowError =>
222+ Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
223+ case _ : OutOfMemoryError =>
224+ Error .fail(" Out of memory while materializing, possibly due to recursive value" , v.pos)
225+ }
226+ }
227+
228+ // Materialize a child value in iterative mode. Single match dispatches leaf values directly
229+ // and pushes a new stack frame for containers. Avoids redundant isInstanceOf pre-checks.
230+ @ inline private def materializeChild [T ](
231+ childVal : Val ,
232+ childVisitor : Visitor [T , T ],
233+ parentVisitor : upickle.core.ObjArrVisitor [T , T ],
234+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
235+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): Unit = {
236+ childVal match {
237+ case obj : Val .Obj =>
238+ pushObjFrame(obj, childVisitor, stack, ctx)
239+ case xs : Val .Arr =>
240+ pushArrFrame(xs, childVisitor, stack, ctx)
241+ case _ =>
242+ parentVisitor.visitValue(materializeLeaf(childVal, childVisitor), - 1 )
243+ }
244+ }
245+
246+ @ inline private def pushObjFrame [T ](
247+ obj : Val .Obj ,
248+ visitor : Visitor [T , T ],
249+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
250+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): Unit = {
251+ checkDepth(obj.pos, stack.size, ctx.maxDepth)
252+ storePos(obj.pos)
253+ obj.triggerAllAsserts(ctx.brokenAssertionLogic)
254+ val keyNames =
255+ if (ctx.sort) obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
256+ else obj.visibleKeyNames
257+ val objVisitor = visitor.visitObject(keyNames.length, jsonableKeys = true , - 1 )
258+ stack.push(
259+ new Materializer .MaterializeObjFrame [T ](objVisitor, keyNames, obj, ctx.sort, 0 , null )
260+ )
261+ }
262+
263+ @ inline private def pushArrFrame [T ](
264+ xs : Val .Arr ,
265+ visitor : Visitor [T , T ],
266+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
267+ ctx : Materializer .MaterializeContext )(implicit evaluator : EvalScope ): Unit = {
268+ checkDepth(xs.pos, stack.size, ctx.maxDepth)
269+ storePos(xs.pos)
270+ val arrVisitor = visitor.visitArray(xs.length, - 1 )
271+ stack.push(new Materializer .MaterializeArrFrame [T ](arrVisitor, xs, 0 ))
272+ }
273+
274+ // Feed a completed child result into the parent frame's visitor.
275+ @ inline private def feedResult [T ](parentFrame : Materializer .MaterializeFrame , result : T ): Unit =
276+ parentFrame match {
277+ case f : Materializer .MaterializeObjFrame [T @ unchecked] =>
278+ f.objVisitor.visitValue(result, - 1 )
279+ case f : Materializer .MaterializeArrFrame [T @ unchecked] =>
280+ f.arrVisitor.visitValue(result, - 1 )
281+ }
282+
283+ @ inline private def checkDepth (pos : Position , stackSize : Int , maxDepth : Int )(implicit
284+ ev : EvalErrorScope ): Unit =
285+ if (stackSize >= maxDepth)
286+ Error .fail(
287+ " Stackoverflow while materializing, possibly due to recursive value" ,
288+ pos
289+ )
290+
85291 def reverse (pos : Position , v : ujson.Value ): Val = v match {
86292 case ujson.True => Val .True (pos)
87293 case ujson.False => Val .False (pos)
@@ -156,6 +362,48 @@ object Materializer extends Materializer {
156362 final val emptyStringArray = new Array [String ](0 )
157363 final val emptyLazyArray = new Array [Eval ](0 )
158364
365+ /**
366+ * Immutable snapshot of all settings needed during a single materialization pass. Created once
367+ * per top-level call and threaded through recursive/iterative helpers, avoiding repeated field
368+ * lookups on the [[Settings ]] object on every frame.
369+ */
370+ private [sjsonnet] final class MaterializeContext (
371+ val sort : Boolean ,
372+ val brokenAssertionLogic : Boolean ,
373+ val emptyPos : Position ,
374+ val recursiveDepthLimit : Int ,
375+ val maxDepth : Int )
376+
377+ private [sjsonnet] object MaterializeContext {
378+ def apply (ev : EvalScope ): MaterializeContext = new MaterializeContext (
379+ sort = ! ev.settings.preserveOrder,
380+ brokenAssertionLogic = ev.settings.brokenAssertionLogic,
381+ emptyPos = ev.emptyMaterializeFileScopePos,
382+ recursiveDepthLimit = ev.settings.materializeRecursiveDepthLimit,
383+ maxDepth = ev.settings.maxMaterializeDepth
384+ )
385+ }
386+
387+ /** Common parent for stack frames used in iterative materialization. */
388+ private [sjsonnet] sealed trait MaterializeFrame
389+
390+ /** Stack frame for in-progress object materialization. */
391+ private [sjsonnet] final class MaterializeObjFrame [T ](
392+ val objVisitor : ObjVisitor [T , T ],
393+ val keys : Array [String ],
394+ val obj : Val .Obj ,
395+ val sort : Boolean ,
396+ var index : Int ,
397+ var prevKey : String )
398+ extends MaterializeFrame
399+
400+ /** Stack frame for in-progress array materialization. */
401+ private [sjsonnet] final class MaterializeArrFrame [T ](
402+ val arrVisitor : ArrVisitor [T , T ],
403+ val arr : Val .Arr ,
404+ var index : Int )
405+ extends MaterializeFrame
406+
159407 /**
160408 * Trait for providing custom materialization logic to the Materializer.
161409 * @since 1.0.0
0 commit comments