diff --git a/sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala b/sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala index 547625d93..e498b46f8 100644 --- a/sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala +++ b/sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala @@ -6,7 +6,9 @@ import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val} object NativeGzip extends AbstractFunctionModule { def name = "gzip" - val functions: Seq[(String, Val.Builtin)] = Seq( + val functionNames: Array[String] = Array("gzip") + + lazy val functions: Seq[(String, Val.Builtin)] = Seq( "gzip" -> new Val.Builtin1("gzip", "v") { override def evalRhs(v: Eval, ev: EvalScope, pos: Position): Val = v.value match { case Val.Str(_, value) => Val.Str(pos, Platform.gzipString(value)) diff --git a/sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala b/sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala index 011bc2b56..7566c4697 100644 --- a/sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala +++ b/sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala @@ -6,7 +6,9 @@ import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val} object NativeXz extends AbstractFunctionModule { def name = "xz" - val functions: Seq[(String, Val.Builtin)] = Seq( + val functionNames: Array[String] = Array("xz") + + lazy val functions: Seq[(String, Val.Builtin)] = Seq( "xz" -> new Val.Builtin2( "xz", "v", diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 730f29395..1cce15e9a 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -635,6 +635,20 @@ object Val { def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = v } + /** + * A member whose value is created lazily on first access. Used by stdlib to defer builtin + * function instantiation until actually needed, reducing startup overhead. + */ + final class LazyConstMember(add2: Boolean, visibility2: Visibility, private var init: () => Val) + extends Member(add2, visibility2, cached = true, deprecatedSkipAsserts = true) { + private var _val: Val = _ + def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = { + var v = _val + if (v eq null) { v = init(); _val = v; init = null } + v + } + } + def mk(pos: Position, members: (String, Obj.Member)*): Obj = { val m = Util.preSizedJavaLinkedHashMap[String, Obj.Member](members.length) for ((k, v) <- members) m.put(k, v) diff --git a/sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala b/sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala index 3931de422..9aeb91892 100644 --- a/sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala +++ b/sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala @@ -8,12 +8,31 @@ import sjsonnet.Val abstract class AbstractFunctionModule extends FunctionModule { /** - * module's functions + * Module function names — cheap string array for lazy registration. No Val.Func objects created. */ - val functions: Seq[(String, Val.Func)] + def functionNames: Array[String] /** - * the hodler of the module object + * module's functions — override as lazy val in concrete modules to defer Val.Builtin creation. + */ + def functions: Seq[(String, Val.Func)] + + /** + * Unsynchronized lazy function lookup map — single-threaded, no synchronization needed. + */ + private var _functionMap: Map[String, Val.Func] = _ + + /** + * Get a function by name, triggering module initialization if needed. + */ + def getFunction(name: String): Val.Func = { + var m = _functionMap + if (m eq null) { m = functions.toMap; _functionMap = m } + m(name) + } + + /** + * the holder of the module object */ override final lazy val module: Val.Obj = moduleFromFunctions(functions: _*) } diff --git a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala index 1f1344213..3b2e19860 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala @@ -459,7 +459,35 @@ object ArrayModule extends AbstractFunctionModule { Val.Arr(pos, chars) } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "minArray", + "maxArray", + "all", + "any", + "count", + "filter", + "map", + "mapWithIndex", + "find", + "flattenArrays", + "flattenDeepArray", + "reverse", + "member", + "range", + "foldl", + "foldr", + "flatMap", + "filterMap", + "repeat", + "makeArray", + "contains", + "remove", + "removeAt", + "sum", + "avg" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(MinArray), builtin(MaxArray), builtin(All), diff --git a/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala b/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala index 8fa00f73b..3646a7f84 100644 --- a/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala @@ -13,7 +13,18 @@ object EncodingModule extends AbstractFunctionModule { Val.Str(pos, Platform.md5(s.value.asString)) } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "md5", + "base64", + "base64Decode", + "base64DecodeBytes", + "sha1", + "sha256", + "sha512", + "sha3" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(MD5), builtin("base64", "input") { (_, _, input: Val) => input match { diff --git a/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala b/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala index 9424f6315..7a3930236 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala @@ -209,7 +209,25 @@ object ManifestModule extends AbstractFunctionModule { } } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "manifestJson", + "manifestJsonMinified", + "manifestJsonEx", + "parseJson", + "parseYaml", + "manifestTomlEx", + "lines", + "deepJoin", + "manifestToml", + "manifestYamlDoc", + "manifestYamlStream", + "manifestIni", + "manifestPython", + "manifestPythonVars", + "manifestXmlJsonml" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(ManifestJson), builtin(ManifestJsonMinified), builtin(ManifestJsonEx), diff --git a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala index 9c71d6126..1e3170e97 100644 --- a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala @@ -6,7 +6,44 @@ import sjsonnet.functions.AbstractFunctionModule object MathModule extends AbstractFunctionModule { def name = "math" - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "sqrt", + "max", + "min", + "mod", + "modulo", + "clamp", + "pow", + "floor", + "round", + "ceil", + "abs", + "sign", + "sin", + "cos", + "tan", + "isEven", + "isInteger", + "isOdd", + "isDecimal", + "asin", + "acos", + "atan", + "atan2", + "hypot", + "deg2rad", + "rad2deg", + "log", + "log2", + "log10", + "exp", + "mantissa", + "exponent", + "xor", + "xnor" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin("sqrt", "x") { (pos, ev, x: Double) => math.sqrt(x) }, diff --git a/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala b/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala index df06fe2c0..0711501fa 100644 --- a/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala +++ b/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala @@ -43,7 +43,15 @@ object NativeRegex extends AbstractFunctionModule { } } - val functions: Seq[(String, Val.Builtin)] = Seq( + val functionNames: Array[String] = Array( + "regexPartialMatch", + "regexFullMatch", + "regexGlobalReplace", + "regexReplace", + "regexQuoteMeta" + ) + + lazy val functions: Seq[(String, Val.Builtin)] = Seq( "regexPartialMatch" -> new Val.Builtin2("regexPartialMatch", "pattern", "str") { override def evalRhs(pattern: Eval, str: Eval, ev: EvalScope, pos: Position): Val = { regexPartialMatch(pos, pattern.value.asString, str.value.asString) diff --git a/sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala b/sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala index ea4b3e356..17025b418 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala @@ -194,7 +194,25 @@ object ObjectModule extends AbstractFunctionModule { Val.Arr(pos, result) } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "objectHas", + "objectHasAll", + "objectHasEx", + "objectFields", + "objectFieldsAll", + "objectFieldsEx", + "objectValues", + "objectValuesAll", + "get", + "mapWithKey", + "objectKeysValues", + "objectKeysValuesAll", + "objectRemoveKey", + "mergePatch", + "prune" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(ObjectHas), builtin(ObjectHasAll), builtin("objectHasEx", "o", "k", "inc_hidden") { diff --git a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala index e78b9fe7f..937ec08d0 100644 --- a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala @@ -205,7 +205,18 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, chars) } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "set", + "slice", + "uniq", + "sort", + "setUnion", + "setInter", + "setDiff", + "setMember" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(Set_), builtin("slice", "indexable", "index", "end", "step") { (pos, ev, indexable: Val, index: Option[Int], _end: Option[Int], _step: Option[Int]) => diff --git a/sjsonnet/src/sjsonnet/stdlib/StdLibModule.scala b/sjsonnet/src/sjsonnet/stdlib/StdLibModule.scala index 378fbc846..d81dc4168 100644 --- a/sjsonnet/src/sjsonnet/stdlib/StdLibModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/StdLibModule.scala @@ -2,10 +2,13 @@ package sjsonnet.stdlib import sjsonnet._ import sjsonnet.Expr.Member.Visibility -import sjsonnet.functions.FunctionModule +import sjsonnet.functions.{AbstractFunctionModule, FunctionModule} /** - * Main standard library module that combines all the individual stdlib modules + * Main standard library module that combines all the individual stdlib modules. + * + * Uses lazy initialization: only function names (cheap string arrays) are registered at startup. + * The actual Val.Builtin objects are created on first access, per-module granularity. */ final class StdLibModule( private val nativeFunctions: Map[String, Val.Func] = Map.empty, @@ -21,39 +24,78 @@ final class StdLibModule( nativeFunctions.getOrElse(name.value.asString, Val.Null(pos)) } - // All functions including native and additional functions - val functions: Map[String, Val.Func] = allModuleFunctions ++ - additionalStdFunctions + - ("native" -> nativeFunction) + - ("trace" -> traceFunction) + - ("extVar" -> extVarFunction) - - val module: Val.Obj = Val.Obj.mk( - null, - functions.size + additionalStdMembers.size, - functions.view.map { case (k, v) => - ( - k, - new Val.Obj.ConstMember(false, Visibility.Hidden, v) - ) - }, - additionalStdMembers - ) + val module: Val.Obj = { + // Estimate total size: module functions + additional std functions + native/trace/extVar + pi/thisFile + val totalSize = + sharedLazyMembers.size + additionalStdFunctions.size + 3 + additionalStdMembers.size + val entries = Util.preSizedJavaLinkedHashMap[String, Val.Obj.Member](totalSize) + + // Shared lazy members — created once in companion object, reused across all instances. + // After first StdLibModule evaluation, LazyConstMember._val is populated and subsequent + // instances get the cached values directly (single null-check fast path). + entries.putAll(sharedLazyMembers) + + // Additional std functions (eager — typically empty or small) + for ((k, v) <- additionalStdFunctions) + entries.put(k, new Val.Obj.ConstMember(false, Visibility.Hidden, v)) + + // Core functions (eager — always needed, only 3) + entries.put("native", new Val.Obj.ConstMember(false, Visibility.Hidden, nativeFunction)) + entries.put("trace", new Val.Obj.ConstMember(false, Visibility.Hidden, traceFunction)) + entries.put("extVar", new Val.Obj.ConstMember(false, Visibility.Hidden, extVarFunction)) + + // Non-function members + for ((k, v) <- additionalStdMembers) entries.put(k, v) + + new Val.Obj(null, entries, false, null, null) + } } object StdLibModule { - // Combine all functions from all modules - private val allModuleFunctions: Map[String, Val.Func] = ( - ArrayModule.functions ++ - StringModule.functions ++ - ObjectModule.functions ++ - MathModule.functions ++ - TypeModule.functions ++ - EncodingModule.functions ++ - ManifestModule.functions ++ - SetModule.functions ++ - NativeRegex.functions - ).toMap + // All stdlib modules — referenced but NOT initialized (functions are lazy val) + private val modules: Array[AbstractFunctionModule] = Array( + ArrayModule, + StringModule, + ObjectModule, + MathModule, + TypeModule, + EncodingModule, + ManifestModule, + SetModule, + NativeRegex + ) + + // Build name→module index using only cheap functionNames arrays (no Val.Builtin created) + private val nameToModule: java.util.LinkedHashMap[String, AbstractFunctionModule] = { + val m = new java.util.LinkedHashMap[String, AbstractFunctionModule](256) + var i = 0 + while (i < modules.length) { + val mod = modules(i) + val names = mod.functionNames + var j = 0 + while (j < names.length) { + m.put(names(j), mod) + j += 1 + } + i += 1 + } + m + } + + // Shared LazyConstMember objects — created once at class loading, reused across all + // StdLibModule instances. After first evaluation, each member caches its resolved Val.Func, + // so subsequent instances pay only a null-check per member access (no closure or Map lookup). + private val sharedLazyMembers: java.util.LinkedHashMap[String, Val.Obj.Member] = { + val m = new java.util.LinkedHashMap[String, Val.Obj.Member](256) + val iter = nameToModule.entrySet().iterator() + while (iter.hasNext) { + val e = iter.next() + val n = e.getKey + val mod = e.getValue + m.put(n, new Val.Obj.LazyConstMember(false, Visibility.Hidden, () => mod.getFunction(n))) + } + m + } // Core std library functions that belong directly in StdLibModule private val traceFunction = new Val.Builtin2("trace", "str", "rest") { diff --git a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala index f4a7c9762..4827bf288 100644 --- a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala @@ -444,7 +444,43 @@ object StringModule extends AbstractFunctionModule { Val.Arr(pos, chars) } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "toString", + "length", + "codepoint", + "substr", + "startsWith", + "endsWith", + "char", + "strReplace", + "stripChars", + "lstripChars", + "rstripChars", + "join", + "split", + "splitLimit", + "splitLimitR", + "stringChars", + "parseInt", + "parseOctal", + "parseHex", + "asciiUpper", + "asciiLower", + "encodeUTF8", + "decodeUTF8", + "format", + "findSubstr", + "isEmpty", + "trim", + "equalsIgnoreCase", + "escapeStringJson", + "escapeStringPython", + "escapeStringXML", + "escapeStringBash", + "escapeStringDollars" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(ToString), builtin(Length), builtin(Codepoint), diff --git a/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala b/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala index 9da70bbc5..8cf260c0c 100644 --- a/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala @@ -60,7 +60,21 @@ object TypeModule extends AbstractFunctionModule { } } - val functions: Seq[(String, Val.Func)] = Seq( + val functionNames: Array[String] = Array( + "assertEqual", + "isString", + "isBoolean", + "isNumber", + "isObject", + "isArray", + "isFunction", + "isNull", + "type", + "equals", + "primitiveEquals" + ) + + lazy val functions: Seq[(String, Val.Func)] = Seq( builtin(AssertEqual), builtin(IsString), builtin(IsBoolean),