Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 60 additions & 41 deletions sjsonnet/src/sjsonnet/Materializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import upickle.core.{ArrVisitor, ObjVisitor, Visitor}
* guards against accidental TailCall leakage with a clear internal-error diagnostic.
*
* Match ordering: all dispatch points ([[apply0]], [[materializeRecursiveChild]],
* [[materializeChild]]) use a unified match that places [[Val.Str]] first, followed by [[Val.Obj]],
* [[Val.Num]], [[Val.Arr]], and other leaf types. This ordering mirrors the original single-method
* design and ensures the most common leaf type (strings) is matched without first testing for
* container types.
* [[materializeChild]]) use a unified dispatch that routes via `valTag` (a byte field on each
* [[Val]] subclass) through a JVM tableswitch for O(1) type resolution. The tag values 0-7
* correspond to Str, Num, True, False, Null, Arr, Obj, Func respectively. Rare types
* (Materializable, TailCall) fall through to a pattern match in the default branch.
*/
abstract class Materializer {
def storePos(pos: Position): Unit
Expand Down Expand Up @@ -110,9 +110,10 @@ abstract class Materializer {
depth: Int,
ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): T = {
storePos(xs.pos)
val av = visitor.visitArray(xs.length, -1)
val len = xs.length
val av = visitor.visitArray(len, -1)
var i = 0
while (i < xs.length) {
while (i < len) {
val childVal = xs.value(i)
av.visitValue(
materializeRecursiveChild(childVal, av.subVisitor.asInstanceOf[Visitor[T, T]], depth, ctx),
Expand All @@ -127,41 +128,59 @@ abstract class Materializer {
childVal: Val,
childVisitor: Visitor[T, T],
depth: Int,
ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): T = childVal match {
case Val.Str(pos, s) => storePos(pos); childVisitor.visitString(s, -1)
case obj: Val.Obj =>
val nextDepth = depth + 1
if (nextDepth < ctx.recursiveDepthLimit)
materializeRecursiveObj(obj, childVisitor, nextDepth, ctx)
else
materializeStackless(childVal, childVisitor, ctx)
case Val.Num(pos, _) => storePos(pos); childVisitor.visitFloat64(childVal.asDouble, -1)
case xs: Val.Arr =>
val nextDepth = depth + 1
if (nextDepth < ctx.recursiveDepthLimit)
materializeRecursiveArr(xs, childVisitor, nextDepth, ctx)
else
materializeStackless(childVal, childVisitor, ctx)
case Val.True(pos) => storePos(pos); childVisitor.visitTrue(-1)
case Val.False(pos) => storePos(pos); childVisitor.visitFalse(-1)
case Val.Null(pos) => storePos(pos); childVisitor.visitNull(-1)
case mat: Materializer.Materializable => storePos(childVal.pos); mat.materialize(childVisitor)
case s: Val.Func =>
Error.fail(
"Couldn't manifest function with params [" + s.params.names.mkString(",") + "]",
childVal.pos
)
case tc: TailCall =>
Error.fail(
"Internal error: TailCall sentinel leaked into materialization. " +
"This indicates a bug in the TCO protocol — a TailCall was not resolved before " +
"reaching the Materializer.",
tc.pos
)
case vv: Val =>
Error.fail("Unknown value type " + vv.prettyName, vv.pos)
case null =>
Error.fail("Unknown value type " + childVal)
ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): T = {
if (childVal == null) Error.fail("Unknown value type " + childVal)
// Use tableswitch dispatch via @switch on valTag for O(1) type routing.
// This replaces up to 7 sequential instanceof checks with a single
// byte comparison + jump table, benefiting materialization-heavy workloads.
val vt: Int = childVal.valTag.toInt
(vt: @scala.annotation.switch) match {
case 0 => // TAG_STR
val s = childVal.asInstanceOf[Val.Str]
storePos(s.pos); childVisitor.visitString(s.str, -1)
case 1 => // TAG_NUM
storePos(childVal.pos); childVisitor.visitFloat64(childVal.asDouble, -1)
case 2 => // TAG_TRUE
storePos(childVal.pos); childVisitor.visitTrue(-1)
case 3 => // TAG_FALSE
storePos(childVal.pos); childVisitor.visitFalse(-1)
case 4 => // TAG_NULL
storePos(childVal.pos); childVisitor.visitNull(-1)
case 5 => // TAG_ARR
val xs = childVal.asInstanceOf[Val.Arr]
val nextDepth = depth + 1
if (nextDepth < ctx.recursiveDepthLimit)
materializeRecursiveArr(xs, childVisitor, nextDepth, ctx)
else
materializeStackless(childVal, childVisitor, ctx)
case 6 => // TAG_OBJ
val obj = childVal.asInstanceOf[Val.Obj]
val nextDepth = depth + 1
if (nextDepth < ctx.recursiveDepthLimit)
materializeRecursiveObj(obj, childVisitor, nextDepth, ctx)
else
materializeStackless(childVal, childVisitor, ctx)
case 7 => // TAG_FUNC
val s = childVal.asInstanceOf[Val.Func]
Error.fail(
"Couldn't manifest function with params [" + s.params.names.mkString(",") + "]",
childVal.pos
)
case _ =>
childVal match {
case mat: Materializer.Materializable =>
storePos(childVal.pos); mat.materialize(childVisitor)
case tc: TailCall =>
Error.fail(
"Internal error: TailCall sentinel leaked into materialization. " +
"This indicates a bug in the TCO protocol — a TailCall was not resolved before " +
"reaching the Materializer.",
tc.pos
)
case vv: Val =>
Error.fail("Unknown value type " + vv.prettyName, vv.pos)
}
}
}

// Iterative materialization for deep nesting. Used as a fallback when recursive depth exceeds
Expand Down
23 changes: 23 additions & 0 deletions sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ final class LazyApply2(
sealed abstract class Val extends Eval {
final def value: Val = this

/** Runtime type tag for O(1) dispatch in Materializer (tableswitch). */
private[sjsonnet] def valTag: Byte

def pos: Position
def prettyName: String

Expand Down Expand Up @@ -175,6 +178,17 @@ object Val {
private[sjsonnet] final val DOUBLE_MAX_SAFE_INTEGER = (1L << 53) - 1
private[sjsonnet] final val DOUBLE_MIN_SAFE_INTEGER = -((1L << 53) - 1)

// Runtime type tags for O(1) dispatch in Materializer (tableswitch).
// Values 0-7 form a contiguous range enabling JVM tableswitch bytecode.
private[sjsonnet] final val TAG_STR: Byte = 0
private[sjsonnet] final val TAG_NUM: Byte = 1
private[sjsonnet] final val TAG_TRUE: Byte = 2
private[sjsonnet] final val TAG_FALSE: Byte = 3
private[sjsonnet] final val TAG_NULL: Byte = 4
private[sjsonnet] final val TAG_ARR: Byte = 5
private[sjsonnet] final val TAG_OBJ: Byte = 6
private[sjsonnet] final val TAG_FUNC: Byte = 7

abstract class Literal extends Val with Expr {
final override private[sjsonnet] def tag = ExprTags.`Val.Literal`
}
Expand All @@ -186,16 +200,20 @@ object Val {

final case class True(var pos: Position) extends Bool {
def prettyName = "boolean"
private[sjsonnet] def valTag: Byte = TAG_TRUE
}
final case class False(var pos: Position) extends Bool {
def prettyName = "boolean"
private[sjsonnet] def valTag: Byte = TAG_FALSE
}
final case class Null(var pos: Position) extends Literal {
def prettyName = "null"
private[sjsonnet] def valTag: Byte = TAG_NULL
}
final case class Str(var pos: Position, str: String) extends Literal {
def prettyName = "string"
override def asString: String = str
private[sjsonnet] def valTag: Byte = TAG_STR
}
final case class Num(var pos: Position, private val num: Double) extends Literal {
if (num.isInfinite) {
Expand Down Expand Up @@ -235,10 +253,12 @@ object Val {
}
num
}
private[sjsonnet] def valTag: Byte = TAG_NUM
}

final case class Arr(var pos: Position, private val arr: Array[? <: Eval]) extends Literal {
def prettyName = "array"
private[sjsonnet] def valTag: Byte = TAG_ARR

override def asArr: Arr = this
def length: Int = arr.length
Expand Down Expand Up @@ -364,6 +384,7 @@ object Val {
private val excludedKeys: java.util.Set[String] = null)
extends Literal
with Expr.ObjBody {
private[sjsonnet] def valTag: Byte = TAG_OBJ
private var asserting: Boolean = false

def getSuper: Obj = `super`
Expand Down Expand Up @@ -767,6 +788,7 @@ object Val {
extends Val
with Expr {
final override private[sjsonnet] def tag = ExprTags.`Val.Func`
private[sjsonnet] def valTag: Byte = TAG_FUNC

def evalRhs(scope: ValScope, ev: EvalScope, fs: FileScope, pos: Position): Val

Expand Down Expand Up @@ -1185,6 +1207,7 @@ final class TailCall(
val namedNames: Array[String],
val callSiteExpr: Expr)
extends Val {
private[sjsonnet] def valTag: Byte = -1
def pos: Position = callSiteExpr.pos
def prettyName = "tailcall"
def exprErrorString: String = callSiteExpr.exprErrorString
Expand Down
1 change: 1 addition & 0 deletions sjsonnet/test/src/sjsonnet/CustomValTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object CustomValTests extends TestSuite {
extends Val.Literal
with Materializer.Materializable {
override def prettyName: String = "Important string"
private[sjsonnet] def valTag: Byte = -1
def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T = {
visitor.visitString(str + "!".repeat(importance), -1)
}
Expand Down