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
167 changes: 149 additions & 18 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,70 @@ class Evaluator(
newScope
}

val builder = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](fields.length)
// Lazily allocate builder only when we have more than 8 fields,
// using flat arrays (single-field or inline arrays) for small objects
var builder: java.util.LinkedHashMap[String, Val.Obj.Member] = null
// Track inline fields: for 1 field use singleKey/singleMember, for 2-8 use arrays
var singleKey: String = null
var singleMember: Val.Obj.Member = null
var inlineKeys: Array[String] = null
var inlineMembers: Array[Val.Obj.Member] = null
var fieldCount = 0
val maxInlineFields = 8

// Shared field-tracking logic: manages singleKey → inlineKeys → builder transitions.
// Handles duplicate key detection at each tier.
def trackField(k: String, v: Val.Obj.Member, offset: Position): Unit = {
if (fieldCount == 0) {
singleKey = k
singleMember = v
} else if (fieldCount == 1) {
// Moving from single-field to multi-field: allocate inline arrays
inlineKeys = new Array[String](math.min(fields.length, maxInlineFields))
inlineMembers = new Array[Val.Obj.Member](inlineKeys.length)
inlineKeys(0) = singleKey
inlineMembers(0) = singleMember
if (singleKey.equals(k)) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
inlineKeys(1) = k
inlineMembers(1) = v
singleKey = null
singleMember = null
} else if (fieldCount <= maxInlineFields && inlineKeys != null) {
// Check for duplicates in inline array
var di = 0
while (di < fieldCount) {
if (inlineKeys(di).equals(k)) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
di += 1
}
if (fieldCount < inlineKeys.length) {
inlineKeys(fieldCount) = k
inlineMembers(fieldCount) = v
} else {
// Overflow: move all inline fields into LinkedHashMap builder
builder = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](fields.length)
var mi = 0
while (mi < fieldCount) {
builder.put(inlineKeys(mi), inlineMembers(mi))
mi += 1
}
inlineKeys = null
inlineMembers = null
builder.put(k, v)
}
} else {
// Already using builder
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
}
fieldCount += 1
}

fields.foreach {
case Member.Field(offset, fieldName, plus, null, sep, rhs) =>
val k = visitFieldName(fieldName, offset)
Expand All @@ -1062,10 +1125,7 @@ class Evaluator(
finally decrementStackDepth()
}
}
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
trackField(k, v, offset)
}
case Member.Field(offset, fieldName, false, argSpec, sep, rhs) =>
val k = visitFieldName(fieldName, offset)
Expand All @@ -1078,27 +1138,98 @@ class Evaluator(
finally decrementStackDepth()
}
}
val previousValue = builder.put(k, v)
if (previousValue != null) {
Error.fail(s"Duplicate key $k in evaluated object.", offset)
}
trackField(k, v, offset)
}
case _ =>
Error.fail("This case should never be hit", objPos)
}
val valueCache = if (sup == null) {
Val.Obj.getEmptyValueCacheForObjWithoutSuper(fields.length)
Val.Obj.getEmptyValueCacheForObjWithoutSuper(fieldCount)
} else {
new java.util.HashMap[Any, Val]()
}
cachedObj = new Val.Obj(
objPos,
builder,
false,
if (asserts != null) triggerAsserts else null,
sup,
valueCache
)
cachedObj = if (fieldCount == 1 && singleKey != null) {
// Single-field object: store key and member inline, avoid LinkedHashMap allocation entirely
new Val.Obj(
objPos,
null,
false,
if (asserts != null) triggerAsserts else null,
sup,
valueCache,
singleFieldKey = singleKey,
singleFieldMember = singleMember
)
} else if (inlineKeys != null && fieldCount >= 2) {
// Multi-field inline object: use flat arrays instead of LinkedHashMap
val finalKeys =
if (fieldCount == inlineKeys.length) inlineKeys
else java.util.Arrays.copyOf(inlineKeys, fieldCount)
val finalMembers =
if (fieldCount == inlineMembers.length) inlineMembers
else java.util.Arrays.copyOf(inlineMembers, fieldCount)
new Val.Obj(
objPos,
null,
false,
if (asserts != null) triggerAsserts else null,
sup,
valueCache,
inlineFieldKeys = finalKeys,
inlineFieldMembers = finalMembers
)
} else {
new Val.Obj(
objPos,
if (builder != null) builder
else Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](0),
false,
if (asserts != null) triggerAsserts else null,
sup,
valueCache
)
}
// Cache sorted field order on MemberList for inline objects.
// Only safe when all field names are Fixed (string literals) — dynamic field names
// (FieldName.Dyn) can evaluate to different keys across invocations, so the cached
// sorted order would be invalid. For dynamic fields, compute per-object instead.
if (cachedObj.canDirectIterate && sup == null) {
val allFieldsFixed = {
val fs = e.fields; var i = 0; var ok = true
while (i < fs.length && ok) { ok = fs(i).fieldName.isInstanceOf[FieldName.Fixed]; i += 1 }
ok
}
if (allFieldsFixed) {
// Static field names: cache on MemberList, shared across all instances
var sortedOrder = e._cachedSortedOrder
if (sortedOrder == null) {
val ik = cachedObj.inlineKeys
val im = cachedObj.inlineMembers
if (ik != null) {
sortedOrder = Materializer.computeSortedInlineOrder(ik, im)
} else {
val sfm = cachedObj.singleMem
sortedOrder =
if (sfm != null && sfm.visibility != Expr.Member.Visibility.Hidden) Array(0)
else Array.emptyIntArray
}
e._cachedSortedOrder = sortedOrder
}
cachedObj._sortedInlineOrder = sortedOrder
} else {
// Dynamic field names: compute per-object, no shared cache
val ik = cachedObj.inlineKeys
val im = cachedObj.inlineMembers
if (ik != null) {
cachedObj._sortedInlineOrder = Materializer.computeSortedInlineOrder(ik, im)
} else {
val sfm = cachedObj.singleMem
cachedObj._sortedInlineOrder =
if (sfm != null && sfm.visibility != Expr.Member.Visibility.Hidden) Array(0)
else Array.emptyIntArray
}
}
}
cachedObj
}

Expand Down
7 changes: 7 additions & 0 deletions sjsonnet/src/sjsonnet/Expr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,13 @@ object Expr {
asserts: Array[Member.AssertStmt])
extends ObjBody {
final override private[sjsonnet] def tag = ExprTags.`ObjBody.MemberList`

/**
* Cached sorted field order for inline objects. Computed once, shared across all Val.Obj
* instances created from this MemberList.
*/
@volatile var _cachedSortedOrder: Array[Int] = null

override def toString: String =
s"MemberList($pos, ${arrStr(binds)}, ${arrStr(fields)}, ${arrStr(asserts)})"
}
Expand Down
Loading