diff --git a/sjsonnet/src/sjsonnet/BaseCharRenderer.scala b/sjsonnet/src/sjsonnet/BaseCharRenderer.scala index c858dde5..4fe51ed8 100644 --- a/sjsonnet/src/sjsonnet/BaseCharRenderer.scala +++ b/sjsonnet/src/sjsonnet/BaseCharRenderer.scala @@ -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) { diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index f0767b8e..dda1c037 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -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 => @@ -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 @@ -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, _) =>