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