@@ -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,211 @@ 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 )
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 =>
38+ case Val .Str (pos, s) => storePos(pos); visitor.visitString(s, - 1 )
39+ case Val .Num (pos, _) => storePos(pos); visitor.visitFloat64(v.asDouble, - 1 )
40+ case Val .True (pos) => storePos(pos); visitor.visitTrue(- 1 )
41+ case Val .False (pos) => storePos(pos); visitor.visitFalse(- 1 )
42+ case Val .Null (pos) => storePos(pos); visitor.visitNull(- 1 )
43+ case mat : Materializer .Materializable => storePos(v.pos); mat.materialize(visitor)
44+ case s : Val .Func =>
6145 Error .fail(
6246 " Couldn't manifest function with params [" + s.params.names.mkString(" ," ) + " ]" ,
6347 v.pos
6448 )
65- case mat : Materializer .Materializable => storePos(v.pos); mat.materialize(visitor)
66- case vv : Val =>
49+ case tc : TailCall =>
50+ Error .fail(
51+ " Internal error: TailCall sentinel leaked into materialization. " +
52+ " This indicates a bug in the TCO protocol — a TailCall was not resolved before " +
53+ " reaching the Materializer." ,
54+ tc.pos
55+ )
56+ case vv : Val =>
6757 Error .fail(" Unknown value type " + vv.prettyName, vv.pos)
6858 case null =>
6959 Error .fail(" Unknown value type " + v)
7060 }
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)
7661 }
7762
63+ /**
64+ * Iterative materialization using an explicit stack. Replaces the previous recursive
65+ * implementation to eliminate StackOverflowError on deeply nested JSON structures.
66+ *
67+ * The stack holds frames representing in-progress container (object/array) materializations. Each
68+ * iteration either:
69+ * 1. Processes the next child of the current container, pushing a new frame if the child is
70+ * itself a container.
71+ * 2. Completes the current container (visitEnd) and pops the frame, feeding the result back to
72+ * the parent frame via visitValue.
73+ *
74+ * A depth limit guards against infinite recursion from self-referential values (e.g.
75+ * `local L = [L]; L`), throwing a descriptive error instead of OOM.
76+ */
77+ def apply0 [T ](v : Val , visitor : Visitor [T , T ])(implicit evaluator : EvalScope ): T = v match {
78+ case obj : Val .Obj => materializeContainer(obj, visitor)
79+ case xs : Val .Arr => materializeContainer(xs, visitor)
80+ case _ => materializeLeaf(v, visitor)
81+ }
82+
83+ // Iterative materialization for container values (Obj/Arr). Separated from apply0 to keep
84+ // the fast-path method small and inlineable by the JIT.
85+ //
86+ // Design: the loop processes one child per iteration using pattern matching on the sealed trait
87+ // MaterializeFrame. Helper methods (pushObjFrame, pushArrFrame, materializeChild, feedResult)
88+ // are kept small so the JIT can unconditionally inline them (< 35 bytes each), giving the same
89+ // effect as manual inlining while keeping the main loop compact enough for C2 deep optimization.
90+ private def materializeContainer [T ](v : Val , visitor : Visitor [T , T ])(implicit
91+ evaluator : EvalScope ): T = {
92+ try {
93+ val maxDepth = evaluator.settings.maxMaterializeDepth
94+ val sort = ! evaluator.settings.preserveOrder
95+ val brokenAssertionLogic = evaluator.settings.brokenAssertionLogic
96+ val emptyPos = evaluator.emptyMaterializeFileScopePos
97+ val stack = new java.util.ArrayDeque [Materializer .MaterializeFrame ](8 )
98+
99+ // Push the initial container frame
100+ v match {
101+ case obj : Val .Obj => pushObjFrame(obj, visitor, stack, maxDepth, sort, brokenAssertionLogic)
102+ case xs : Val .Arr => pushArrFrame(xs, visitor, stack, maxDepth)
103+ case _ => () // unreachable — apply0 guarantees Obj or Arr
104+ }
105+
106+ while (true ) {
107+ stack.peekFirst() match {
108+ case frame : Materializer .MaterializeObjFrame [T @ unchecked] =>
109+ val keys = frame.keys
110+ val ov = frame.objVisitor
111+ if (frame.index < keys.length) {
112+ val key = keys(frame.index)
113+ val childVal = frame.obj.value(key, emptyPos)
114+ storePos(childVal)
115+
116+ if (frame.sort) {
117+ if (
118+ frame.prevKey != null && Util .compareStringsByCodepoint(key, frame.prevKey) <= 0
119+ )
120+ Error .fail(
121+ s """ Internal error: Unexpected key " $key" after " ${frame.prevKey}" in sorted object materialization """ ,
122+ childVal.pos
123+ )
124+ frame.prevKey = key
125+ }
126+
127+ ov.visitKeyValue(ov.visitKey(- 1 ).visitString(key, - 1 ))
128+ frame.index += 1
129+
130+ val sub = ov.subVisitor.asInstanceOf [Visitor [T , T ]]
131+ materializeChild(childVal, sub, ov, stack, maxDepth, sort, brokenAssertionLogic)
132+ } else {
133+ val result = ov.visitEnd(- 1 )
134+ stack.removeFirst()
135+ if (stack.isEmpty) return result
136+ feedResult(stack.peekFirst(), result)
137+ }
138+
139+ case frame : Materializer .MaterializeArrFrame [T @ unchecked] =>
140+ val arr = frame.arr
141+ val av = frame.arrVisitor
142+ if (frame.index < arr.length) {
143+ val childVal = arr.value(frame.index)
144+ frame.index += 1
145+
146+ val sub = av.subVisitor.asInstanceOf [Visitor [T , T ]]
147+ materializeChild(childVal, sub, av, stack, maxDepth, sort, brokenAssertionLogic)
148+ } else {
149+ val result = av.visitEnd(- 1 )
150+ stack.removeFirst()
151+ if (stack.isEmpty) return result
152+ feedResult(stack.peekFirst(), result)
153+ }
154+ }
155+ }
156+
157+ null .asInstanceOf [T ] // unreachable — while(true) exits via return
158+ } catch {
159+ case _ : StackOverflowError =>
160+ Error .fail(" Stackoverflow while materializing, possibly due to recursive value" , v.pos)
161+ case _ : OutOfMemoryError =>
162+ Error .fail(" Out of memory while materializing, possibly due to recursive value" , v.pos)
163+ }
164+ }
165+
166+ // Materialize a child value: leaf fast-path avoids a full pattern match for the common case
167+ // (strings, numbers, booleans, null). Only containers (Obj/Arr) push a new frame.
168+ private def materializeChild [T ](
169+ childVal : Val ,
170+ childVisitor : Visitor [T , T ],
171+ parentVisitor : upickle.core.ObjArrVisitor [T , T ],
172+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
173+ maxDepth : Int ,
174+ sort : Boolean ,
175+ brokenAssertionLogic : Boolean )(implicit evaluator : EvalScope ): Unit = {
176+ if (! childVal.isInstanceOf [Val .Obj ] && ! childVal.isInstanceOf [Val .Arr ]) {
177+ parentVisitor.visitValue(materializeLeaf(childVal, childVisitor), - 1 )
178+ } else
179+ childVal match {
180+ case obj : Val .Obj =>
181+ pushObjFrame(obj, childVisitor, stack, maxDepth, sort, brokenAssertionLogic)
182+ case xs : Val .Arr =>
183+ pushArrFrame(xs, childVisitor, stack, maxDepth)
184+ case _ => () // unreachable — guarded by isInstanceOf checks above
185+ }
186+ }
187+
188+ private def pushObjFrame [T ](
189+ obj : Val .Obj ,
190+ visitor : Visitor [T , T ],
191+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
192+ maxDepth : Int ,
193+ sort : Boolean ,
194+ brokenAssertionLogic : Boolean )(implicit evaluator : EvalScope ): Unit = {
195+ checkDepth(obj.pos, stack.size, maxDepth)
196+ storePos(obj.pos)
197+ obj.triggerAllAsserts(brokenAssertionLogic)
198+ val keyNames =
199+ if (sort) obj.visibleKeyNames.sorted(Util .CodepointStringOrdering )
200+ else obj.visibleKeyNames
201+ val objVisitor = visitor.visitObject(keyNames.length, jsonableKeys = true , - 1 )
202+ stack.push(new Materializer .MaterializeObjFrame [T ](objVisitor, keyNames, obj, sort, 0 , null ))
203+ }
204+
205+ private def pushArrFrame [T ](
206+ xs : Val .Arr ,
207+ visitor : Visitor [T , T ],
208+ stack : java.util.ArrayDeque [Materializer .MaterializeFrame ],
209+ maxDepth : Int )(implicit evaluator : EvalScope ): Unit = {
210+ checkDepth(xs.pos, stack.size, maxDepth)
211+ storePos(xs.pos)
212+ val arrVisitor = visitor.visitArray(xs.length, - 1 )
213+ stack.push(new Materializer .MaterializeArrFrame [T ](arrVisitor, xs, 0 ))
214+ }
215+
216+ // Feed a completed child result into the parent frame's visitor.
217+ private def feedResult [T ](parentFrame : Materializer .MaterializeFrame , result : T ): Unit =
218+ parentFrame match {
219+ case f : Materializer .MaterializeObjFrame [T @ unchecked] =>
220+ f.objVisitor.visitValue(result, - 1 )
221+ case f : Materializer .MaterializeArrFrame [T @ unchecked] =>
222+ f.arrVisitor.visitValue(result, - 1 )
223+ }
224+
225+ private def checkDepth (pos : Position , stackSize : Int , maxDepth : Int )(implicit
226+ ev : EvalErrorScope ): Unit =
227+ if (stackSize >= maxDepth)
228+ Error .fail(
229+ " Stackoverflow while materializing, possibly due to recursive value" ,
230+ pos
231+ )
232+
78233 def reverse (pos : Position , v : ujson.Value ): Val = v match {
79234 case ujson.True => Val .True (pos)
80235 case ujson.False => Val .False (pos)
@@ -149,6 +304,26 @@ object Materializer extends Materializer {
149304 final val emptyStringArray = new Array [String ](0 )
150305 final val emptyLazyArray = new Array [Eval ](0 )
151306
307+ /** Common parent for stack frames used in iterative materialization. */
308+ private [sjsonnet] sealed trait MaterializeFrame
309+
310+ /** Stack frame for in-progress object materialization. */
311+ private [sjsonnet] final class MaterializeObjFrame [T ](
312+ val objVisitor : ObjVisitor [T , T ],
313+ val keys : Array [String ],
314+ val obj : Val .Obj ,
315+ val sort : Boolean ,
316+ var index : Int ,
317+ var prevKey : String )
318+ extends MaterializeFrame
319+
320+ /** Stack frame for in-progress array materialization. */
321+ private [sjsonnet] final class MaterializeArrFrame [T ](
322+ val arrVisitor : ArrVisitor [T , T ],
323+ val arr : Val .Arr ,
324+ var index : Int )
325+ extends MaterializeFrame
326+
152327 /**
153328 * Trait for providing custom materialization logic to the Materializer.
154329 * @since 1.0.0
0 commit comments