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
21 changes: 21 additions & 0 deletions sjsonnet/src/sjsonnet/BaseCharRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,27 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output](
out
}

/**
* Fast path for [[Val.AsciiSafeStr]]: the string is statically known to contain only chars in
* 0x20-0x7E, excluding `"` and `\`. That means no JSON escaping is ever required — not even under
* `escapeUnicode`, since every char is <= 0x7E. Emit `"` + raw chars + `"` with a single bulk
* `getChars`, skipping the per-call `CharSWAR.hasEscapeChar` scan that [[visitNonNullString]]
* would otherwise perform. Mirrors the no-escape ASCII fast path, minus the scan.
*/
def visitAsciiSafeString(s: String, index: Int): T = {
flushBuffer()
val len = s.length
elemBuilder.ensureLength(len + 2)
elemBuilder.appendUnsafe('"')
val cbArr = elemBuilder.arr
val pos = elemBuilder.getLength
s.getChars(0, len, cbArr, pos)
elemBuilder.length = pos + len
elemBuilder.appendUnsafe('"')
flushCharBuilder()
out
}

final def renderIndent(): Unit = {
if (indent == -1) ()
else if (indentCache != null && depth < BaseCharRenderer.MaxCachedDepth) {
Expand Down
24 changes: 19 additions & 5 deletions sjsonnet/src/sjsonnet/Materializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,24 @@ abstract class Materializer {
* JIT-friendly) and automatically switches to an explicit stack-based iterative loop when the
* recursion depth exceeds [[Settings.materializeRecursiveDepthLimit]].
*/
/**
* Visit a string value, routing [[Val.AsciiSafeStr]] through the renderer's escape-free fast path
* when the visitor is a char renderer. Falls back to plain `visitString` for the ujson.Value AST
* path and for strings that may require escaping.
*/
@inline private def visitStr[T](s: Val.Str, visitor: Visitor[T, T]): T = {
storePos(s.pos)
visitor match {
case cr: BaseCharRenderer[T @unchecked] if s.isInstanceOf[Val.AsciiSafeStr] =>
cr.visitAsciiSafeString(s.str, -1)
case _ => visitor.visitString(s.str, -1)
}
}

def apply0[T](v: Val, visitor: Visitor[T, T])(implicit evaluator: EvalScope): T = try {
v match {
case Val.Str(pos, s) => storePos(pos); visitor.visitString(s, -1)
case obj: Val.Obj =>
case s: Val.Str => visitStr(s, visitor)
case obj: Val.Obj =>
materializeRecursiveObj(obj, visitor, 0, Materializer.MaterializeContext(evaluator))
case Val.Num(pos, _) => storePos(pos); visitor.visitFloat64(v.asDouble, -1)
case xs: Val.Arr =>
Expand Down Expand Up @@ -285,7 +299,7 @@ abstract class Materializer {
(vt: @scala.annotation.switch) match {
case 0 => // TAG_STR
val s = childVal.asInstanceOf[Val.Str]
storePos(s.pos); childVisitor.visitString(s.str, -1)
visitStr(s, childVisitor)
case 1 => // TAG_NUM
storePos(childVal.pos); childVisitor.visitFloat64(childVal.asDouble, -1)
case 2 => // TAG_TRUE
Expand Down Expand Up @@ -436,8 +450,8 @@ abstract class Materializer {
stack: java.util.ArrayDeque[Materializer.MaterializeFrame],
ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): Unit = {
childVal match {
case Val.Str(pos, s) =>
storePos(pos); parentVisitor.visitValue(childVisitor.visitString(s, -1), -1)
case s: Val.Str =>
parentVisitor.visitValue(visitStr(s, childVisitor), -1)
case obj: Val.Obj =>
pushObjFrame(obj, childVisitor, stack, ctx)
case Val.Num(pos, _) =>
Expand Down
Loading