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