From 38c1b3c20994c5d118f127acd0f18c956a768146 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 10 Nov 2025 22:11:00 +0100 Subject: [PATCH] big optimization round --- docs/parallelism.md | 16 +- docs/perf_guide.md | 119 +++++++++ lynglib/build.gradle.kts | 7 + .../net/sergeych/lyng/ArgBuilderAndroid.kt | 32 +++ .../net/sergeych/lyng/PerfDefaults.android.kt | 18 ++ .../kotlin/net/sergeych/lyng/ArgBuilder.kt | 22 ++ .../kotlin/net/sergeych/lyng/ArgRuntime.kt | 11 + .../kotlin/net/sergeych/lyng/Arguments.kt | 113 ++++++-- .../kotlin/net/sergeych/lyng/PerfDefaults.kt | 23 ++ .../kotlin/net/sergeych/lyng/PerfFlags.kt | 29 +- .../kotlin/net/sergeych/lyng/PerfStats.kt | 40 +++ .../kotlin/net/sergeych/lyng/Scope.kt | 48 +++- .../kotlin/net/sergeych/lyng/ScopePool.kt | 35 +++ .../kotlin/net/sergeych/lyng/obj/Obj.kt | 7 +- .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 250 +++++++++++------- lynglib/src/commonTest/kotlin/ScriptTest.kt | 2 +- .../kotlin/net/sergeych/lyng/ArgBuilderJs.kt | 20 ++ .../net/sergeych/lyng/PerfDefaults.js.kt | 19 ++ .../kotlin/net/sergeych/lyng/ArgBuilderJvm.kt | 36 +++ .../net/sergeych/lyng/PerfDefaults.jvm.kt | 18 ++ .../src/jvmTest/kotlin/CallBenchmarkTest.kt | 47 +++- .../kotlin/CallMixedArityBenchmarkTest.kt | 57 ++++ .../kotlin/CallPoolingBenchmarkTest.kt | 53 ++++ .../jvmTest/kotlin/CallSplatBenchmarkTest.kt | 55 ++++ .../kotlin/DeepPoolingStressJvmTest.kt | 76 ++++++ .../jvmTest/kotlin/ExpressionBenchmarkTest.kt | 61 +++++ .../kotlin/MethodPoolingBenchmarkTest.kt | 50 ++++ .../src/jvmTest/kotlin/MixedBenchmarkTest.kt | 81 ++++++ .../src/jvmTest/kotlin/PicBenchmarkTest.kt | 4 + .../jvmTest/kotlin/PicInvalidationJvmTest.kt | 105 ++++++++ .../src/jvmTest/kotlin/ScriptSubsetJvmTest.kt | 47 ++++ .../kotlin/ScriptSubsetJvmTest_Additions3.kt | 140 ++++++++++ .../kotlin/ScriptSubsetJvmTest_Additions4.kt | 137 ++++++++++ .../kotlin/ScriptSubsetJvmTest_Additions5.kt | 81 ++++++ .../kotlin/ScriptSubsetJvmTest_additions.kt | 120 +++++++++ .../net/sergeych/lyng/ArgBuilderNative.kt | 20 ++ .../net/sergeych/lyng/PerfDefaults.native.kt | 19 ++ .../net/sergeych/lyng/ArgBuilderWasm.kt | 20 ++ .../net/sergeych/lyng/PerfDefaults.wasmJs.kt | 19 ++ 39 files changed, 1914 insertions(+), 143 deletions(-) create mode 100644 docs/perf_guide.md create mode 100644 lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt create mode 100644 lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgBuilder.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgRuntime.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt create mode 100644 lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt create mode 100644 lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt create mode 100644 lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ArgBuilderJvm.kt create mode 100644 lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt create mode 100644 lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt create mode 100644 lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt create mode 100644 lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt create mode 100644 lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt create mode 100644 lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt create mode 100644 lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ArgBuilderNative.kt create mode 100644 lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt create mode 100644 lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ArgBuilderWasm.kt create mode 100644 lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt diff --git a/docs/parallelism.md b/docs/parallelism.md index 9b28fbe..736789c 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -204,4 +204,18 @@ Flows allow easy transforming of any [Iterable]. See how the standard Lyng libra } -[Iterable]: Iterable.md \ No newline at end of file +[Iterable]: Iterable.md + +## Scope frame pooling (JVM) + +Lyng includes an optional optimization for function/method calls on JVM: scope frame pooling, toggled by the runtime flag `PerfFlags.SCOPE_POOL`. + +- Default: `SCOPE_POOL` is OFF on JVM. +- Rationale: the current `ScopePool` implementation is not thread‑safe. Lyng targets multi‑threaded execution on most platforms, therefore we keep pooling disabled by default until a thread‑safe design is introduced. +- When safe to enable: single‑threaded runs (e.g., micro‑benchmarks or scripts executed on a single worker) where no scopes are shared across threads. +- How to toggle at runtime (Kotlin/JVM tests): + - `PerfFlags.SCOPE_POOL = true` to enable. + - `PerfFlags.SCOPE_POOL = false` to disable. +- Expected effect (from our JVM micro‑benchmarks): in deep call loops, enabling pooling reduced total time by about 1.38× in a dedicated pooling benchmark; mileage may vary depending on workload. + +Future work: introduce thread‑safe pooling (e.g., per‑thread pools or confinement strategies) before considering enabling it by default in multi‑threaded environments. diff --git a/docs/perf_guide.md b/docs/perf_guide.md new file mode 100644 index 0000000..0f16b17 --- /dev/null +++ b/docs/perf_guide.md @@ -0,0 +1,119 @@ +# Lyng Performance Guide (JVM‑first) + +This document explains how to enable and measure the performance optimizations added to the Lyng interpreter. The focus is JVM‑first with safe, flag‑guarded rollouts and quick A/B testing. Other targets (JS/Wasm/Native) keep conservative defaults until validated. + +## Overview + +Optimizations are controlled by runtime‑mutable flags in `net.sergeych.lyng.PerfFlags`, initialized from platform‑specific static defaults `net.sergeych.lyng.PerfDefaults` (KMP `expect/actual`). + +- JVM/Android defaults are aggressive (e.g. `RVAL_FASTPATH=true`). +- Non‑JVM defaults are conservative (e.g. `RVAL_FASTPATH=false`). + +All flags are `var` and can be flipped at runtime (e.g., from tests or host apps) for A/B comparisons. + +## Key flags + +- `LOCAL_SLOT_PIC` — Runtime cache in `LocalVarRef` to avoid repeated name→slot lookups per frame (ON JVM default). +- `EMIT_FAST_LOCAL_REFS` — Compiler emits `FastLocalVarRef` for identifiers known to be locals/params (ON JVM default). +- `ARG_BUILDER` — Efficient argument building: small‑arity no‑alloc and pooled builder on JVM (ON JVM default). +- `SKIP_ARGS_ON_NULL_RECEIVER` — Early return on optional‑null receivers before building args (semantics‑compatible). A/B only. +- `SCOPE_POOL` — Scope frame pooling for calls (JVM‑first). OFF by default. Enable for benchmark A/B. +- `FIELD_PIC` — 2‑entry polymorphic inline cache for field reads/writes keyed by `(classId, layoutVersion)` (ON JVM default). +- `METHOD_PIC` — 2‑entry PIC for instance method calls keyed by `(classId, layoutVersion)` (ON JVM default). +- `PIC_DEBUG_COUNTERS` — Enable lightweight hit/miss counters via `PerfStats` (OFF by default). +- `PRIMITIVE_FASTOPS` — Fast paths for `(ObjInt, ObjInt)` arithmetic/comparisons and `(ObjBool, ObjBool)` logic (ON JVM default). +- `RVAL_FASTPATH` — Bypass `ObjRecord` in pure expression evaluation via `ObjRef.evalValue` (ON JVM default, OFF elsewhere). + +See `src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt` and `PerfDefaults.*.kt` for details and platform defaults. + +## Where optimizations apply + +- Locals: `FastLocalVarRef`, `LocalVarRef` per‑frame cache (PIC). +- Calls: small‑arity zero‑alloc paths (0–5 args), pooled builder (JVM), and child frame pooling (optional). +- Properties/methods: Field/Method PICs with receiver shape `(classId, layoutVersion)` and handle‑aware caches. +- Expressions: R‑value fast paths in hot nodes (`UnaryOpRef`, `BinaryOpRef`, `ElvisRef`, logical ops, `RangeRef`, `IndexRef` read, `FieldRef` receiver eval, `ListLiteralRef` elements, `CallRef` callee, `MethodCallRef` receiver, assignment RHS). +- Primitives: Direct boolean/int ops where safe. + +## Running JVM micro‑benchmarks + +Each benchmark prints timings with `[DEBUG_LOG]` and includes correctness assertions to prevent dead‑code elimination. + +Run individual tests to avoid multiplatform matrices: + +``` +./gradlew :lynglib:jvmTest --tests LocalVarBenchmarkTest +./gradlew :lynglib:jvmTest --tests CallBenchmarkTest +./gradlew :lynglib:jvmTest --tests CallMixedArityBenchmarkTest +./gradlew :lynglib:jvmTest --tests CallSplatBenchmarkTest +./gradlew :lynglib:jvmTest --tests PicBenchmarkTest +./gradlew :lynglib:jvmTest --tests PicInvalidationJvmTest +./gradlew :lynglib:jvmTest --tests ArithmeticBenchmarkTest +./gradlew :lynglib:jvmTest --tests ExpressionBenchmarkTest +./gradlew :lynglib:jvmTest --tests CallPoolingBenchmarkTest +./gradlew :lynglib:jvmTest --tests MethodPoolingBenchmarkTest +./gradlew :lynglib:jvmTest --tests MixedBenchmarkTest +./gradlew :lynglib:jvmTest --tests DeepPoolingStressJvmTest +``` + +Typical output (example): + +``` +[DEBUG_LOG] [BENCH] mixed-arity x200000 [ARG_BUILDER=ON]: 85.7 ms +``` + +Lower time is better. Run the same bench with a flag OFF vs ON to compare. + +## Toggling flags in tests + +Flags are mutable at runtime, e.g.: + +```kotlin +PerfFlags.ARG_BUILDER = false +val r1 = (Scope().eval(script) as ObjInt).value +PerfFlags.ARG_BUILDER = true +val r2 = (Scope().eval(script) as ObjInt).value +``` + +Reset flags at the end of a test to avoid impacting other tests. + +## PIC diagnostics (optional) + +Enable counters: + +```kotlin +PerfFlags.PIC_DEBUG_COUNTERS = true +PerfStats.resetAll() +``` + +Available counters in `PerfStats`: + +- Field PIC: `fieldPicHit`, `fieldPicMiss`, `fieldPicSetHit`, `fieldPicSetMiss` +- Method PIC: `methodPicHit`, `methodPicMiss` +- Locals: `localVarPicHit`, `localVarPicMiss`, `fastLocalHit`, `fastLocalMiss` +- Primitive ops: `primitiveFastOpsHit` + +Print a summary at the end of a bench/test as needed. Remember to turn counters OFF after the test. + +## Guidance per flag (JVM) + +- Keep `RVAL_FASTPATH = true` unless debugging a suspected expression‑semantics issue. +- Use `SCOPE_POOL = true` only for benchmarks or once pooling passes the deep stress tests and broader validation; currently OFF by default. +- `FIELD_PIC` and `METHOD_PIC` should remain ON; they are validated with invalidation tests. +- `ARG_BUILDER` should remain ON; switch OFF only to get a baseline. + +## Notes on correctness & safety + +- Optional chaining semantics are preserved across fast paths. +- Visibility/mutability checks are enforced even on PIC fast‑paths. +- `frameId` is regenerated on each pooled frame borrow; stress tests verify no leakage under deep nesting/recursion. + +## Cross‑platform + +- Non‑JVM defaults keep `RVAL_FASTPATH=false` for now; other low‑risk flags may be ON. +- Once JVM path is fully validated and measured, add lightweight benches for JS/Wasm/Native and enable flags incrementally. + +## Troubleshooting + +- If a benchmark shows regressions, flip related flags OFF to isolate the source (e.g., `ARG_BUILDER`, `RVAL_FASTPATH`, `FIELD_PIC`, `METHOD_PIC`). +- Use `PIC_DEBUG_COUNTERS` to observe inline cache effectiveness. +- Ensure tests do not accidentally keep flags ON for subsequent tests; reset after each test. diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 5af014c..622a1f9 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -142,6 +142,13 @@ publishing { } } +// Ensure JVM test stdout is visible and runs are single-threaded for stable timings +tasks.withType { + testLogging { + showStandardStreams = true + } + maxParallelForks = 1 +} //mavenPublishing { // publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt new file mode 100644 index 0000000..51f8d21 --- /dev/null +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt @@ -0,0 +1,32 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +actual object ArgBuilderProvider { + private val tl = object : ThreadLocal() { + override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder() + } + actual fun acquire(): ArgsBuilder = tl.get() +} + +private class AndroidArgsBuilder : ArgsBuilder { + private val buf: ArrayList = ArrayList(8) + + override fun reset(expectedSize: Int) { + buf.clear() + if (expectedSize > 0) buf.ensureCapacity(expectedSize) + } + + override fun add(v: Obj) { buf.add(v) } + + override fun addAll(vs: List) { + if (vs.isNotEmpty()) { + buf.ensureCapacity(buf.size + vs.size) + buf.addAll(vs) + } + } + + override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode) + + override fun release() { /* no-op, ThreadLocal keeps buffer */ } +} diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt new file mode 100644 index 0000000..16c0570 --- /dev/null +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt @@ -0,0 +1,18 @@ +package net.sergeych.lyng + +actual object PerfDefaults { + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true + + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + actual val SCOPE_POOL: Boolean = false + + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + + actual val PIC_DEBUG_COUNTERS: Boolean = false + + actual val PRIMITIVE_FASTOPS: Boolean = true + actual val RVAL_FASTPATH: Boolean = true +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgBuilder.kt new file mode 100644 index 0000000..9a597a0 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgBuilder.kt @@ -0,0 +1,22 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +/** + * Expect/actual API for an arguments builder that can reuse buffers on JVM to reduce allocations. + * Default (non-JVM) implementation just allocates fresh collections and has no pooling. + */ +expect object ArgBuilderProvider { + fun acquire(): ArgsBuilder +} + +interface ArgsBuilder { + /** Prepare the builder for a new build, optionally hinting capacity. */ + fun reset(expectedSize: Int = 0) + fun add(v: Obj) + fun addAll(vs: List) + /** Build immutable [Arguments] snapshot from the current buffer. */ + fun build(tailBlockMode: Boolean): Arguments + /** Return builder to pool/reset state. Safe to no-op on non-JVM. */ + fun release() +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgRuntime.kt new file mode 100644 index 0000000..5444aa3 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgRuntime.kt @@ -0,0 +1,11 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +/** + * Helper factory for argument-building mutable lists. + * Currently returns a fresh ArrayList with the requested initial capacity. + * JVM-specific pooling/builder can be introduced later via expect/actual without + * changing call sites that use [newArgMutableList]. + */ +fun newArgMutableList(initialCapacity: Int): MutableList = ArrayList(initialCapacity) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 50be76f..d1a0912 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -24,31 +24,95 @@ import net.sergeych.lyng.obj.ObjList data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false) suspend fun Collection.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { - // If ARG_BUILDER is enabled, try to reuse a pre-sized ArrayList and do bulk-adds - val list: MutableList = if (PerfFlags.ARG_BUILDER) ArrayList(this.size) else mutableListOf() - - for (x in this) { - val value = x.value.execute(scope) - if (x.isSplat) { - when { - value is ObjList -> { - // Bulk add elements from an ObjList - list.addAll(value.list) + // Small-arity fast path (no splats) to reduce allocations + if (PerfFlags.ARG_BUILDER) { + var hasSplat = false + var count = 0 + for (pa in this) { + if (pa.isSplat) { hasSplat = true; break } + count++ + if (count > 3) break + } + if (!hasSplat && count == this.size) { + val quick = when (count) { + 0 -> Arguments.EMPTY + 1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode) + 2 -> { + val a0 = this.elementAt(0).value.execute(scope) + val a1 = this.elementAt(1).value.execute(scope) + Arguments(listOf(a0, a1), tailBlockMode) } - - value.isInstanceOf(ObjIterable) -> { - // Convert to list once and bulk add - val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list - list.addAll(i) + 3 -> { + val a0 = this.elementAt(0).value.execute(scope) + val a1 = this.elementAt(1).value.execute(scope) + val a2 = this.elementAt(2).value.execute(scope) + Arguments(listOf(a0, a1, a2), tailBlockMode) } - - else -> scope.raiseClassCastError("expected list of objects for splat argument") + 4 -> { + val a0 = this.elementAt(0).value.execute(scope) + val a1 = this.elementAt(1).value.execute(scope) + val a2 = this.elementAt(2).value.execute(scope) + val a3 = this.elementAt(3).value.execute(scope) + Arguments(listOf(a0, a1, a2, a3), tailBlockMode) + } + 5 -> { + val a0 = this.elementAt(0).value.execute(scope) + val a1 = this.elementAt(1).value.execute(scope) + val a2 = this.elementAt(2).value.execute(scope) + val a3 = this.elementAt(3).value.execute(scope) + val a4 = this.elementAt(4).value.execute(scope) + Arguments(listOf(a0, a1, a2, a3, a4), tailBlockMode) + } + else -> null } - } else { - list.add(value) + if (quick != null) return quick } } - return Arguments(list, tailBlockMode) + // General path with builder or simple list fallback + if (PerfFlags.ARG_BUILDER) { + val b = ArgBuilderProvider.acquire() + try { + b.reset(this.size) + for (x in this) { + val value = x.value.execute(scope) + if (x.isSplat) { + when { + value is ObjList -> { + b.addAll(value.list) + } + value.isInstanceOf(ObjIterable) -> { + val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list + b.addAll(i) + } + else -> scope.raiseClassCastError("expected list of objects for splat argument") + } + } else { + b.add(value) + } + } + return b.build(tailBlockMode) + } finally { + b.release() + } + } else { + val list: MutableList = mutableListOf() + for (x in this) { + val value = x.value.execute(scope) + if (x.isSplat) { + when { + value is ObjList -> list.addAll(value.list) + value.isInstanceOf(ObjIterable) -> { + val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list + list.addAll(i) + } + else -> scope.raiseClassCastError("expected list of objects for splat argument") + } + } else { + list.add(value) + } + } + return Arguments(list, tailBlockMode) + } } data class Arguments(val list: List, val tailBlockMode: Boolean = false) : List by list { @@ -57,7 +121,14 @@ import net.sergeych.lyng.obj.ObjList fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") - return list.first().byValueCopy() + val v = list.first() + // Tiny micro-alloc win: avoid byValueCopy for immutable singletons + return when (v) { + net.sergeych.lyng.obj.ObjNull, + net.sergeych.lyng.obj.ObjTrue, + net.sergeych.lyng.obj.ObjFalse -> v + else -> v.byValueCopy() + } } /** diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt new file mode 100644 index 0000000..eb98ba7 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfDefaults.kt @@ -0,0 +1,23 @@ +package net.sergeych.lyng + +/** + * Platform-specific default values for performance flags. + * These defaults are applied once at static init time in [PerfFlags] and can + * be overridden at runtime by tests or callers. + */ +expect object PerfDefaults { + val LOCAL_SLOT_PIC: Boolean + val EMIT_FAST_LOCAL_REFS: Boolean + + val ARG_BUILDER: Boolean + val SKIP_ARGS_ON_NULL_RECEIVER: Boolean + val SCOPE_POOL: Boolean + + val FIELD_PIC: Boolean + val METHOD_PIC: Boolean + + val PIC_DEBUG_COUNTERS: Boolean + + val PRIMITIVE_FASTOPS: Boolean + val RVAL_FASTPATH: Boolean +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt index d3bf0be..bee0834 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfFlags.kt @@ -1,26 +1,33 @@ package net.sergeych.lyng /** - * Runtime-togglable perf flags for micro-benchmarking and A/B comparisons on the JVM. - * Keep as `var` so tests can flip them. + * Runtime-togglable perf flags for micro-benchmarking and A/B comparisons. + * Initialized from platform-specific defaults via PerfDefaults expect/actual. + * Keep as `var` so tests can flip them at runtime. */ object PerfFlags { // Enable PIC inside LocalVarRef (runtime cache of name->slot per frame) - var LOCAL_SLOT_PIC: Boolean = true + var LOCAL_SLOT_PIC: Boolean = PerfDefaults.LOCAL_SLOT_PIC // Make the compiler emit fast local refs for identifiers known to be function locals/params - var EMIT_FAST_LOCAL_REFS: Boolean = true + var EMIT_FAST_LOCAL_REFS: Boolean = PerfDefaults.EMIT_FAST_LOCAL_REFS // Enable more efficient argument building and bulk-copy for splats - var ARG_BUILDER: Boolean = true + var ARG_BUILDER: Boolean = PerfDefaults.ARG_BUILDER // Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only. - var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - // Enable pooling of Scope frames for calls (planned; JVM-only) - var SCOPE_POOL: Boolean = false + var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER + // Enable pooling of Scope frames for calls (may be JVM-only optimization) + var SCOPE_POOL: Boolean = PerfDefaults.SCOPE_POOL // Step 2: PICs for fields and methods - var FIELD_PIC: Boolean = true - var METHOD_PIC: Boolean = true + var FIELD_PIC: Boolean = PerfDefaults.FIELD_PIC + var METHOD_PIC: Boolean = PerfDefaults.METHOD_PIC + + // Debug/observability for PICs and fast paths (JVM-first) + var PIC_DEBUG_COUNTERS: Boolean = PerfDefaults.PIC_DEBUG_COUNTERS // Step 3: Primitive arithmetic and comparison fast paths - var PRIMITIVE_FASTOPS: Boolean = true + var PRIMITIVE_FASTOPS: Boolean = PerfDefaults.PRIMITIVE_FASTOPS + + // Step 4: R-value fast path to bypass ObjRecord in pure expression evaluation + var RVAL_FASTPATH: Boolean = PerfDefaults.RVAL_FASTPATH } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt new file mode 100644 index 0000000..5a4f44a --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt @@ -0,0 +1,40 @@ +package net.sergeych.lyng + +/** + * Lightweight runtime counters for perf diagnostics. Enabled via flags in [PerfFlags]. + * Keep simple and zero-cost when disabled by guarding increments at call sites. + */ +object PerfStats { + // Field PIC + var fieldPicHit: Long = 0 + var fieldPicMiss: Long = 0 + var fieldPicSetHit: Long = 0 + var fieldPicSetMiss: Long = 0 + + // Method PIC + var methodPicHit: Long = 0 + var methodPicMiss: Long = 0 + + // Local var PICs + var localVarPicHit: Long = 0 + var localVarPicMiss: Long = 0 + var fastLocalHit: Long = 0 + var fastLocalMiss: Long = 0 + + // Primitive fast ops + var primitiveFastOpsHit: Long = 0 + + fun resetAll() { + fieldPicHit = 0 + fieldPicMiss = 0 + fieldPicSetHit = 0 + fieldPicSetMiss = 0 + methodPicHit = 0 + methodPicMiss = 0 + localVarPicHit = 0 + localVarPicMiss = 0 + fastLocalHit = 0 + fastLocalMiss = 0 + primitiveFastOpsHit = 0 + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index d560d53..536d916 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -22,8 +22,8 @@ import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportProvider // Simple per-frame id generator for perf caches (not thread-safe, fine for scripts) -private object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ } -private fun nextFrameId(): Long = FrameIdGen.nextId() +object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ } +fun nextFrameId(): Long = FrameIdGen.nextId() /** * Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes. @@ -40,14 +40,14 @@ private fun nextFrameId(): Long = FrameIdGen.nextId() * - [ClosureScope] - scope used to apply a closure to some thisObj scope */ open class Scope( - val parent: Scope?, - val args: Arguments = Arguments.EMPTY, + var parent: Scope?, + var args: Arguments = Arguments.EMPTY, var pos: Pos = Pos.builtIn, var thisObj: Obj = ObjVoid, var skipScopeCreation: Boolean = false, ) { - // Unique id per scope frame for PICs; cheap to compare and stable for the frame lifetime. - val frameId: Long = nextFrameId() + // Unique id per scope frame for PICs; regenerated on each borrow from the pool. + var frameId: Long = nextFrameId() // Fast-path storage for local variables/arguments accessed by slot index. // Enabled by default for child scopes; module/class scopes can ignore it. @@ -165,12 +165,48 @@ open class Scope( return idx } + /** + * Reset this scope instance so it can be safely reused as a fresh child frame. + * Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj. + */ + fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) { + this.parent = parent + this.args = args + this.pos = pos + this.thisObj = thisObj + this.skipScopeCreation = false + // fresh identity for PIC caches + this.frameId = nextFrameId() + // clear locals and slot maps + objects.clear() + slots.clear() + nameToSlot.clear() + } + /** * Creates a new child scope using the provided arguments and optional `thisObj`. */ fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope = Scope(this, args, pos, newThisObj ?: thisObj) + /** + * Execute a block inside a child frame. Guarded for future pooling via [PerfFlags.SCOPE_POOL]. + * Currently always creates a fresh child scope to preserve unique frameId semantics. + */ + inline suspend fun withChildFrame(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null, crossinline block: suspend (Scope) -> R): R { + if (PerfFlags.SCOPE_POOL) { + val child = ScopePool.borrow(this, args, pos, newThisObj ?: thisObj) + try { + return block(child) + } finally { + ScopePool.release(child) + } + } else { + val child = createChildScope(args, newThisObj) + return block(child) + } + } + /** * Creates a new child scope using the provided arguments and optional `thisObj`. * The child scope inherits the current scope's properties such as position and the existing `thisObj` if no new `thisObj` is provided. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt new file mode 100644 index 0000000..65960e8 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt @@ -0,0 +1,35 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjVoid + +/** + * Simple, portable scope frame pool. JVM-first optimization; for now it uses a small + * global deque. It is only used when [PerfFlags.SCOPE_POOL] is true. + * + * NOTE: This implementation is not thread-safe. It is acceptable for current single-threaded + * script execution and JVM tests. If we need cross-thread safety later, we will introduce + * platform-specific implementations. + */ +object ScopePool { + private const val MAX_POOL_SIZE = 64 + private val pool = ArrayDeque(MAX_POOL_SIZE) + + fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { + val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) + // If we reused a scope, reset its state to behave as a fresh child frame + if (s.parent !== parent || s.args !== args || s.pos !== pos || s.thisObj !== thisObj) { + s.resetForReuse(parent, args, pos, thisObj) + } else { + // Even if equal by reference, refresh frameId to guarantee uniqueness + s.frameId = nextFrameId() + } + return s + } + + fun release(scope: Scope) { + // Scrub sensitive references to avoid accidental retention + scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid) + if (pool.size < MAX_POOL_SIZE) pool.addLast(scope) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 640ffa9..92f8820 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -265,7 +265,12 @@ open class Obj { } suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj = - callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj)) + if (net.sergeych.lyng.PerfFlags.SCOPE_POOL) + scope.withChildFrame(args, newThisObj = thisObj) { child -> + callOn(child) + } + else + callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj)) suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj = callOn( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index e40b6a1..7fb8a46 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -24,6 +24,11 @@ import net.sergeych.lyng.* */ sealed interface ObjRef { suspend fun get(scope: Scope): ObjRecord + /** + * Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord. + * Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic. + */ + suspend fun evalValue(scope: Scope): Obj = get(scope).value suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { throw ScriptError(pos, "can't assign value") } @@ -51,7 +56,7 @@ enum class BinOp { /** R-value reference for unary operations. */ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val v = a.get(scope).value + val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) a.evalValue(scope) else a.get(scope).value val r = when (op) { UnaryOp.NOT -> v.logicalNot(scope) UnaryOp.NEGATE -> v.negate(scope) @@ -63,8 +68,8 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { /** R-value reference for binary operations. */ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val a = left.get(scope).value - val b = right.get(scope).value + val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value + val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value // Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS) if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { @@ -77,7 +82,10 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r BinOp.NEQ -> if (a.value != b.value) ObjTrue else ObjFalse else -> null } - if (r != null) return r.asReadonly + if (r != null) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ + return r.asReadonly + } } // Fast integer ops when both operands are ObjInt if (a is ObjInt && b is ObjInt) { @@ -97,7 +105,10 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r BinOp.GTE -> if (av >= bv) ObjTrue else ObjFalse else -> null } - if (r != null) return r.asReadonly + if (r != null) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++ + return r.asReadonly + } } } @@ -139,7 +150,7 @@ class AssignOpRef( ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { val x = target.get(scope).value - val y = value.get(scope).value + val y = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value val inPlace: Obj? = when (op) { BinOp.PLUS -> x.plusAssign(scope, y) BinOp.MINUS -> x.minusAssign(scope, y) @@ -196,8 +207,8 @@ class IncDecRef( /** Elvis operator reference: a ?: b */ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val a = left.get(scope).value - val r = if (a != ObjNull) a else right.get(scope).value + val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value + val r = if (a != ObjNull) a else if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value return r.asReadonly } } @@ -205,9 +216,9 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { /** Logical OR with short-circuit: a || b */ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val a = left.get(scope).value + val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly - val b = right.get(scope).value + val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (a is ObjBool && b is ObjBool) { return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly @@ -220,9 +231,9 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef /** Logical AND with short-circuit: a && b */ class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val a = left.get(scope).value + val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly - val b = right.get(scope).value + val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (a is ObjBool && b is ObjBool) { return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly @@ -255,41 +266,42 @@ class FieldRef( private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: (suspend (Obj, Scope, Obj) -> Unit)? = null override suspend fun get(scope: Scope): ObjRecord { - val base = target.get(scope).value + val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value if (base == ObjNull && isOptional) return ObjNull.asMutable if (net.sergeych.lyng.PerfFlags.FIELD_PIC) { val (key, ver) = receiverKeyAndVersion(base) - rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope) } - rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) return g(base, scope) } + rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicHit++ + return g(base, scope) + } } + rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicHit++ + return g(base, scope) + } } // Slow path + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicMiss++ val rec = base.readField(scope, name) - // Install move-to-front with a handle-aware getter + // Install move-to-front with a handle-aware getter. Where safe, capture resolved handles. rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> - when (obj) { - is ObjInstance -> { - val instScope = obj.instanceScope - val idx = instScope.getSlotIndexOf(name) - if (idx != null) { - val r = instScope.getSlotRecord(idx) - if (!r.visibility.isPublic) + when (base) { + is ObjClass -> { + val clsScope = base.classScope + val capturedIdx = clsScope?.getSlotIndexOf(name) + if (clsScope != null && capturedIdx != null) { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> + val scope0 = (obj as ObjClass).classScope!! + val r0 = scope0.getSlotRecord(capturedIdx) + if (!r0.visibility.isPublic) sc.raiseError(ObjAccessException(sc, "can't access non-public field $name")) - r - } else obj.readField(sc, name) + r0 + } + } else { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } } - is ObjClass -> { - val clsScope = obj.classScope - if (clsScope != null) { - val idx = clsScope.getSlotIndexOf(name) - if (idx != null) { - val r = clsScope.getSlotRecord(idx) - if (!r.visibility.isPublic) - sc.raiseError(ObjAccessException(sc, "can't access non-public field $name")) - r - } else obj.readField(sc, name) - } else obj.readField(sc, name) - } - else -> obj.readField(sc, name) + } + else -> { + // For instances and other types, fall back to name-based lookup per access (slot index may differ per instance) + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } } } return rec @@ -305,39 +317,38 @@ class FieldRef( } if (net.sergeych.lyng.PerfFlags.FIELD_PIC) { val (key, ver) = receiverKeyAndVersion(base) - wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) return s(base, scope, newValue) } - wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) return s(base, scope, newValue) } + wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetHit++ + return s(base, scope, newValue) + } } + wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetHit++ + return s(base, scope, newValue) + } } // Slow path + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetMiss++ base.writeField(scope, name, newValue) // Install move-to-front with a handle-aware setter wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> - when (obj) { - is ObjInstance -> { - val instScope = obj.instanceScope - val idx = instScope.getSlotIndexOf(name) - if (idx != null) { - val r = instScope.getSlotRecord(idx) - if (!r.visibility.isPublic) - sc.raiseError(ObjAccessException(sc, "can't assign to non-public field $name")) - if (!r.isMutable) + when (base) { + is ObjClass -> { + val clsScope = base.classScope + val capturedIdx = clsScope?.getSlotIndexOf(name) + if (clsScope != null && capturedIdx != null) { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> + val scope0 = (obj as ObjClass).classScope!! + val r0 = scope0.getSlotRecord(capturedIdx) + if (!r0.isMutable) sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) - if (r.value.assign(sc, v) == null) r.value = v - } else obj.writeField(sc, name, v) + if (r0.value.assign(sc, v) == null) r0.value = v + } + } else { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } } - is ObjClass -> { - val clsScope = obj.classScope - if (clsScope != null) { - val idx = clsScope.getSlotIndexOf(name) - if (idx != null) { - val r = clsScope.getSlotRecord(idx) - if (!r.isMutable) - sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) - r.value = v - } else obj.writeField(sc, name, v) - } else obj.writeField(sc, name, v) - } - else -> obj.writeField(sc, name, v) + } + else -> { + // For instances and other types, fall back to generic write (instance slot indices may differ per instance) + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } } } return @@ -361,9 +372,9 @@ class IndexRef( private val isOptional: Boolean, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val base = target.get(scope).value + val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value if (base == ObjNull && isOptional) return ObjNull.asMutable - val idx = index.get(scope).value + val idx = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) index.evalValue(scope) else index.get(scope).value return base.getAt(scope, idx).asMutable } @@ -395,10 +406,16 @@ class CallRef( private val isOptionalInvoke: Boolean, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val callee = target.get(scope).value + val callee = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly val callArgs = args.toArguments(scope, tailBlock) - val result = callee.callOn(scope.createChildScope(scope.pos, callArgs)) + val result: Obj = if (net.sergeych.lyng.PerfFlags.SCOPE_POOL) { + scope.withChildFrame(callArgs) { child -> + callee.callOn(child) + } + } else { + callee.callOn(scope.createChildScope(scope.pos, callArgs)) + } return result.asReadonly } } @@ -418,38 +435,54 @@ class MethodCallRef( private var mKey2: Long = 0L; private var mVer2: Int = -1; private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null override suspend fun get(scope: Scope): ObjRecord { - val base = receiver.get(scope).value + val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) receiver.evalValue(scope) else receiver.get(scope).value if (base == ObjNull && isOptional) return ObjNull.asReadonly val callArgs = args.toArguments(scope, tailBlock) if (net.sergeych.lyng.PerfFlags.METHOD_PIC) { val (key, ver) = receiverKeyAndVersion(base) - mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) return inv(base, scope, callArgs).asReadonly } - mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) return inv(base, scope, callArgs).asReadonly } + mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicHit++ + return inv(base, scope, callArgs).asReadonly + } } + mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicHit++ + return inv(base, scope, callArgs).asReadonly + } } // Slow path + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicMiss++ val result = base.invokeInstanceMethod(scope, name, callArgs) // Install move-to-front with a handle-aware invoker mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> - when (obj) { - is ObjInstance -> { - val instScope = obj.instanceScope - val rec = instScope.get(name) - if (rec != null) { - if (!rec.visibility.isPublic) + when (base) { + is ObjInstance -> { + // Prefer resolved class member to avoid per-call lookup on hit + val member = base.objClass.getInstanceMemberOrNull(name) + if (member != null) { + val visibility = member.visibility + val callable = member.value + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + val inst = obj as ObjInstance + if (!visibility.isPublic) sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name")) - rec.value.invoke(instScope, obj, a) - } else obj.invokeInstanceMethod(sc, name, a) + callable.invoke(inst.instanceScope, inst, a) + } + } else { + // Fallback to name-based lookup per call (uncommon) + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } } - is ObjClass -> { - val clsScope = obj.classScope - if (clsScope != null) { - val rec = clsScope.get(name) - if (rec != null) { - rec.value.invoke(sc, obj, a) - } else obj.invokeInstanceMethod(sc, name, a) - } else obj.invokeInstanceMethod(sc, name, a) + } + is ObjClass -> { + val clsScope = base.classScope + val rec = clsScope?.get(name) + if (rec != null) { + val callable = rec.value + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) } + } else { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } } - else -> obj.invokeInstanceMethod(sc, name, a) + } + else -> { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } } } return result.asReadonly @@ -486,11 +519,22 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { scope.pos = atPos if (!PerfFlags.LOCAL_SLOT_PIC) { - scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it) } + scope.getSlotIndexOf(name)?.let { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicHit++ + return scope.getSlotRecord(it) + } + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++ return scope[name] ?: scope.raiseError("symbol not defined: '$name'") } - val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope) - if (slot >= 0) return scope.getSlotRecord(slot) + val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) + val slot = if (hit) cachedSlot else resolveSlot(scope) + if (slot >= 0) { + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) { + if (hit) net.sergeych.lyng.PerfStats.localVarPicHit++ else net.sergeych.lyng.PerfStats.localVarPicMiss++ + } + return scope.getSlotRecord(slot) + } + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++ return scope[name] ?: scope.raiseError("symbol not defined: '$name'") } @@ -584,10 +628,13 @@ class FastLocalVarRef( override suspend fun get(scope: Scope): ObjRecord { scope.pos = atPos - val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null - val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope) + val ownerValid = isOwnerValidFor(scope) + val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope) val actualOwner = cachedOwnerScope if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope") + if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) { + if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++ + } return actualOwner.getSlotRecord(slot) } @@ -609,10 +656,11 @@ class ListLiteralRef(private val entries: List) : ObjRef { for (e in entries) { when (e) { is ListEntry.Element -> { - list += e.ref.get(scope).value + val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value + list += v } is ListEntry.Spread -> { - val elements = e.ref.get(scope).value + val elements = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value when (elements) { is ObjList -> list.addAll(elements.list) else -> scope.raiseError("Spread element must be list") @@ -633,8 +681,8 @@ class RangeRef( private val isEndInclusive: Boolean ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val l = left?.get(scope)?.value ?: ObjNull - val r = right?.get(scope)?.value ?: ObjNull + val l = left?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull + val r = right?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly } } @@ -646,7 +694,7 @@ class AssignRef( private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { - val v = value.get(scope).value + val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value val rec = target.get(scope) if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable") if (rec.value.assign(scope, v) == null) { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 2c9941a..8edef4f 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3332,7 +3332,7 @@ class ScriptTest { } -// @Test + ///@Test fun testMinimumOptimization() = runTest { for (i in 1..200) { bm { diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt new file mode 100644 index 0000000..b2b56bf --- /dev/null +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt @@ -0,0 +1,20 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +actual object ArgBuilderProvider { + actual fun acquire(): ArgsBuilder = JsArgsBuilder() +} + +private class JsArgsBuilder : ArgsBuilder { + private var buf: MutableList = ArrayList() + + override fun reset(expectedSize: Int) { + buf = ArrayList(expectedSize.coerceAtLeast(0)) + } + + override fun add(v: Obj) { buf.add(v) } + override fun addAll(vs: List) { if (vs.isNotEmpty()) buf.addAll(vs) } + override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode) + override fun release() { /* no-op */ } +} diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt new file mode 100644 index 0000000..3da9b18 --- /dev/null +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt @@ -0,0 +1,19 @@ +package net.sergeych.lyng + +actual object PerfDefaults { + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true + + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + actual val SCOPE_POOL: Boolean = false + + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + + actual val PIC_DEBUG_COUNTERS: Boolean = false + + actual val PRIMITIVE_FASTOPS: Boolean = true + // Conservative default for non-JVM until validated + actual val RVAL_FASTPATH: Boolean = false +} \ No newline at end of file diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ArgBuilderJvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ArgBuilderJvm.kt new file mode 100644 index 0000000..013e698 --- /dev/null +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ArgBuilderJvm.kt @@ -0,0 +1,36 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +actual object ArgBuilderProvider { + private val tl = object : ThreadLocal() { + override fun initialValue(): JvmArgsBuilder = JvmArgsBuilder() + } + actual fun acquire(): ArgsBuilder = tl.get() +} + +private class JvmArgsBuilder : ArgsBuilder { + private val buf: ArrayList = ArrayList(8) + + override fun reset(expectedSize: Int) { + buf.clear() + if (expectedSize > 0) buf.ensureCapacity(expectedSize) + } + + override fun add(v: Obj) { + buf.add(v) + } + + override fun addAll(vs: List) { + if (vs.isNotEmpty()) { + buf.ensureCapacity(buf.size + vs.size) + buf.addAll(vs) + } + } + + override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode) + + override fun release() { + // ThreadLocal instance is reused automatically; nothing to do + } +} diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt new file mode 100644 index 0000000..16c0570 --- /dev/null +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt @@ -0,0 +1,18 @@ +package net.sergeych.lyng + +actual object PerfDefaults { + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true + + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + actual val SCOPE_POOL: Boolean = false + + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + + actual val PIC_DEBUG_COUNTERS: Boolean = false + + actual val PRIMITIVE_FASTOPS: Boolean = true + actual val RVAL_FASTPATH: Boolean = true +} \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt index 4cad648..b4dce9b 100644 --- a/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt +++ b/lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt @@ -52,4 +52,49 @@ class CallBenchmarkTest { assertEquals(expected, r1) assertEquals(expected, r2) } -} + + @Test + fun benchmarkMixedArityCalls() = runBlocking { + val n = 200_000 + val script = """ + fun f0() { 1 } + fun f1(a) { a } + fun f2(a,b) { a + b } + fun f3(a,b,c) { a + b + c } + fun f4(a,b,c,d) { a + b + c + d } + + var s = 0 + var i = 0 + while (i < $n) { + s = s + f0() + s = s + f1(1) + s = s + f2(1, 1) + s = s + f3(1, 1, 1) + s = s + f4(1, 1, 1, 1) + i = i + 1 + } + s + """.trimIndent() + + // Baseline + PerfFlags.ARG_BUILDER = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Optimized + PerfFlags.ARG_BUILDER = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Each loop: 1 + 1 + 2 + 3 + 4 = 11 + val expected = 11L * n + assertEquals(expected, r1) + assertEquals(expected, r2) + } +} \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt new file mode 100644 index 0000000..2155177 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt @@ -0,0 +1,57 @@ +/* + * JVM micro-benchmark for mixed-arity function calls and ARG_BUILDER. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallMixedArityBenchmarkTest { + @Test + fun benchmarkMixedArityCalls() = runBlocking { + val n = 200_000 + val script = """ + fun f0() { 1 } + fun f1(a) { a } + fun f2(a,b) { a + b } + fun f3(a,b,c) { a + b + c } + fun f4(a,b,c,d) { a + b + c + d } + + var s = 0 + var i = 0 + while (i < $n) { + s = s + f0() + s = s + f1(1) + s = s + f2(1, 1) + s = s + f3(1, 1, 1) + s = s + f4(1, 1, 1, 1) + i = i + 1 + } + s + """.trimIndent() + + // Baseline + PerfFlags.ARG_BUILDER = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Optimized + PerfFlags.ARG_BUILDER = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Each loop: 1 + 1 + 2 + 3 + 4 = 11 + val expected = 11L * n + assertEquals(expected, r1) + assertEquals(expected, r2) + } +} diff --git a/lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt new file mode 100644 index 0000000..73767f8 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt @@ -0,0 +1,53 @@ +/* + * JVM micro-benchmark for Scope frame pooling impact on call-heavy code paths. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallPoolingBenchmarkTest { + @Test + fun benchmarkScopePoolingOnFunctionCalls() = runBlocking { + val n = 300_000 + val script = """ + fun inc1(a) { a + 1 } + fun inc2(a) { inc1(a) + 1 } + fun inc3(a) { inc2(a) + 1 } + var s = 0 + var i = 0 + while (i < $n) { + s = inc3(s) + i = i + 1 + } + s + """.trimIndent() + + // Baseline: pooling OFF + PerfFlags.SCOPE_POOL = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] call-pooling x$n [SCOPE_POOL=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Optimized: pooling ON + PerfFlags.SCOPE_POOL = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] call-pooling x$n [SCOPE_POOL=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Each inc3 performs 3 increments per loop + val expected = 3L * n + assertEquals(expected, r1) + assertEquals(expected, r2) + + // Reset flag to default (OFF) to avoid affecting other tests unintentionally + PerfFlags.SCOPE_POOL = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt new file mode 100644 index 0000000..73814f2 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt @@ -0,0 +1,55 @@ +/* + * JVM micro-benchmark for calls with splat (spread) arguments. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallSplatBenchmarkTest { + @Test + fun benchmarkCallsWithSplatArgs() = runBlocking { + val n = 120_000 + val script = """ + fun sum4(a,b,c,d) { a + b + c + d } + val base = [1,1,1,1] + var s = 0 + var i = 0 + while (i < $n) { + // two direct, one splat per iteration + s = s + sum4(1,1,1,1) + s = s + sum4(1,1,1,1) + s = s + sum4(base[0], base[1], base[2], base[3]) + i = i + 1 + } + s + """.trimIndent() + + // Baseline + PerfFlags.ARG_BUILDER = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] splat-calls x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Optimized + PerfFlags.ARG_BUILDER = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] splat-calls x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Each loop adds (4 + 4 + 4) = 12 + val expected = 12L * n + assertEquals(expected, r1) + assertEquals(expected, r2) + + // Reset to default + PerfFlags.ARG_BUILDER = true + } +} diff --git a/lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt b/lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt new file mode 100644 index 0000000..2c19c2b --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt @@ -0,0 +1,76 @@ +/* + * JVM stress tests for scope frame pooling (deep nesting and recursion). + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class DeepPoolingStressJvmTest { + @Test + fun deepNestedCalls_noLeak_and_correct_with_and_without_pooling() = runBlocking { + val depth = 200 + val script = """ + fun f0(x) { x + 1 } + fun f1(x) { f0(x) + 1 } + fun f2(x) { f1(x) + 1 } + fun f3(x) { f2(x) + 1 } + fun f4(x) { f3(x) + 1 } + fun f5(x) { f4(x) + 1 } + + fun chain(x, d) { + var i = 0 + var s = x + while (i < d) { + // 5 nested calls per iteration + s = f5(s) + i = i + 1 + } + s + } + chain(0, $depth) + """.trimIndent() + + // Pool OFF + PerfFlags.SCOPE_POOL = false + val scope1 = Scope() + val r1 = (scope1.eval(script) as ObjInt).value + // Pool ON + PerfFlags.SCOPE_POOL = true + val scope2 = Scope() + val r2 = (scope2.eval(script) as ObjInt).value + // Each loop adds 6 (f0..f5 adds 6) + val expected = 6L * depth + assertEquals(expected, r1) + assertEquals(expected, r2) + // Reset + PerfFlags.SCOPE_POOL = false + } + + @Test + fun recursion_factorial_correct_with_and_without_pooling() = runBlocking { + val n = 10 + val script = """ + fun fact(x) { + if (x <= 1) 1 else x * fact(x - 1) + } + fact($n) + """.trimIndent() + // OFF + PerfFlags.SCOPE_POOL = false + val scope1 = Scope() + val r1 = (scope1.eval(script) as ObjInt).value + // ON + PerfFlags.SCOPE_POOL = true + val scope2 = Scope() + val r2 = (scope2.eval(script) as ObjInt).value + // 10! = 3628800 + val expected = 3628800L + assertEquals(expected, r1) + assertEquals(expected, r2) + PerfFlags.SCOPE_POOL = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt new file mode 100644 index 0000000..98cd0b0 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt @@ -0,0 +1,61 @@ +/* + * JVM micro-benchmark for expression evaluation with RVAL_FASTPATH. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExpressionBenchmarkTest { + @Test + fun benchmarkExpressionChains() = runBlocking { + val n = 350_000 + val script = """ + // arithmetic + elvis + logical chains + val maybe = null + var s = 0 + var i = 0 + while (i < $n) { + // exercise elvis on a null + s = s + (maybe ?: 0) + // branch using booleans without coercion to int + if ((i % 3 == 0 && true) || false) { s = s + 1 } else { s = s + 2 } + // parity via arithmetic only (avoid adding booleans) + s = s + (i - (i / 2) * 2) + i = i + 1 + } + s + """.trimIndent() + + // OFF + PerfFlags.RVAL_FASTPATH = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] expr-chain x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // ON + PerfFlags.RVAL_FASTPATH = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] expr-chain x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // correctness: compute expected with simple kotlin logic mirroring the loop + var s = 0L + var i = 0 + while (i < n) { + if ((i % 3 == 0 && true) || false) s += 1 else s += 2 + // parity via arithmetic only, matches script's single parity addition + s += i - (i / 2) * 2 + i += 1 + } + assertEquals(s, r1) + assertEquals(s, r2) + } +} diff --git a/lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt new file mode 100644 index 0000000..9aa06f8 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt @@ -0,0 +1,50 @@ +/* + * JVM micro-benchmark for scope frame pooling on instance method calls. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class MethodPoolingBenchmarkTest { + @Test + fun benchmarkInstanceMethodCallsWithPooling() = runBlocking { + val n = 300_000 + val script = """ + class C() { + var x = 0 + fun add1() { x = x + 1 } + fun get() { x } + } + val c = C() + var i = 0 + while (i < $n) { + c.add1() + i = i + 1 + } + c.get() + """.trimIndent() + + // Pool OFF + PerfFlags.SCOPE_POOL = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] method-loop x$n [SCOPE_POOL=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // Pool ON + PerfFlags.SCOPE_POOL = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] method-loop x$n [SCOPE_POOL=ON]: ${(t3 - t2)/1_000_000.0} ms") + + assertEquals(n.toLong(), r1) + assertEquals(n.toLong(), r2) + } +} diff --git a/lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt new file mode 100644 index 0000000..67a0267 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt @@ -0,0 +1,81 @@ +/* + * JVM mixed workload micro-benchmark to exercise multiple hot paths together. + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class MixedBenchmarkTest { + @Test + fun benchmarkMixedWorkloadRvalFastpath() = runBlocking { + // Keep iterations moderate to avoid CI timeouts + val n = 250_000 + val script = """ + class Acc() { + var x = 0 + fun add(v) { x = x + v } + fun get() { x } + } + val acc = Acc() + val maybe = null + var s = 0 + var i = 0 + while (i < $n) { + // exercise locals + primitive ops + s = s + i + // elvis on null + s = s + (maybe ?: 0) + // boolean logic (short-circuit + primitive fast path) + if ((i % 3 == 0 && true) || false) { s = s + 1 } else { s = s + 2 } + // instance field/method with PIC + acc.add(1) + // simple index with list building every 1024 steps (rare path) + if (i % 1024 == 0) { + val lst = [0,1,2,3] + s = s + lst[2] + } + i = i + 1 + } + s + acc.get() + """.trimIndent() + + // OFF + PerfFlags.RVAL_FASTPATH = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms") + + // ON + PerfFlags.RVAL_FASTPATH = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] mixed x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms") + + // Compute expected value in Kotlin to ensure correctness + var s = 0L + var i = 0 + var acc = 0L + while (i < n) { + s += i + s += 0 // (maybe ?: 0) + if ((i % 3 == 0 && true) || false) s += 1 else s += 2 + acc += 1 + if (i % 1024 == 0) s += 2 + i += 1 + } + val expected = s + acc + assertEquals(expected, r1) + assertEquals(expected, r2) + + // Reset flag for other tests + PerfFlags.RVAL_FASTPATH = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt index 93ad01e..18719ff 100644 --- a/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt +++ b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt @@ -45,6 +45,10 @@ class PicBenchmarkTest { val t3 = System.nanoTime() println("[DEBUG_LOG] [BENCH] Field PIC=ON: ${(t3 - t2) / 1_000_000.0} ms") assertEquals(iterations.toLong(), r2) + if (PerfFlags.PIC_DEBUG_COUNTERS) { + println("[DEBUG_LOG] [PIC] field get hit=${net.sergeych.lyng.PerfStats.fieldPicHit} miss=${net.sergeych.lyng.PerfStats.fieldPicMiss}") + println("[DEBUG_LOG] [PIC] field set hit=${net.sergeych.lyng.PerfStats.fieldPicSetHit} miss=${net.sergeych.lyng.PerfStats.fieldPicSetMiss}") + } } @Test diff --git a/lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt b/lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt new file mode 100644 index 0000000..a8a10b1 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt @@ -0,0 +1,105 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.PerfStats +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class PicInvalidationJvmTest { + @Test + fun fieldPicInvalidatesOnClassLayoutChange() = runBlocking { + // Enable counters and PICs + PerfFlags.FIELD_PIC = true + PerfFlags.PIC_DEBUG_COUNTERS = true + PerfStats.resetAll() + + val scope = Scope() + // Declare a class and warm up field access + val script = """ + class C() { + var x = 0 + fun getX() { x } + } + val c = C() + var i = 0 + while (i < 1000) { + // warm read path + val t = c.x + i = i + 1 + } + c.getX() + """.trimIndent() + val r1 = (scope.eval(script) as ObjInt).value + assertEquals(0L, r1) + val hitsBefore = PerfStats.fieldPicHit + val missesBefore = PerfStats.fieldPicMiss + assertTrue(hitsBefore >= 1, "Expected some PIC hits after warm-up") + + // Mutate class layout from Kotlin side to bump layoutVersion and invalidate PIC + val cls = (scope["C"]!!.value as ObjClass) + cls.createClassField("yy", ObjInt(1), isMutable = false) + + // Access the same field again; first access after version bump should miss PIC + val r2 = (scope.eval("c.x") as ObjInt).value + assertEquals(0L, r2) + val missesAfter = PerfStats.fieldPicMiss + assertTrue(missesAfter >= missesBefore + 1, "Expected PIC miss after class layout change") + + // Optional summary when counters enabled + if (PerfFlags.PIC_DEBUG_COUNTERS) { + println("[DEBUG_LOG] [PIC] field get hit=${PerfStats.fieldPicHit} miss=${PerfStats.fieldPicMiss}") + println("[DEBUG_LOG] [PIC] field set hit=${PerfStats.fieldPicSetHit} miss=${PerfStats.fieldPicSetMiss}") + } + + // Disable counters to avoid affecting other tests + PerfFlags.PIC_DEBUG_COUNTERS = false + } + + @Test + fun methodPicInvalidatesOnClassLayoutChange() = runBlocking { + PerfFlags.METHOD_PIC = true + PerfFlags.PIC_DEBUG_COUNTERS = true + PerfStats.resetAll() + + val scope = Scope() + val script = """ + class D() { + var x = 0 + fun inc() { x = x + 1 } + fun get() { x } + } + val d = D() + var i = 0 + while (i < 1000) { + d.inc() + i = i + 1 + } + d.get() + """.trimIndent() + val r1 = (scope.eval(script) as ObjInt).value + assertEquals(1000L, r1) + val mhBefore = PerfStats.methodPicHit + val mmBefore = PerfStats.methodPicMiss + assertTrue(mhBefore >= 1, "Expected method PIC hits after warm-up") + + // Bump layout by adding a new class field + val cls = (scope["D"]!!.value as ObjClass) + cls.createClassField("zz", ObjInt(0), isMutable = false) + + // Next invocation should miss and then re-fill + val r2 = (scope.eval("d.get()") as ObjInt).value + assertEquals(1000L, r2) + val mmAfter = PerfStats.methodPicMiss + assertTrue(mmAfter >= mmBefore + 1, "Expected method PIC miss after class layout change") + + // Optional summary when counters enabled + if (PerfFlags.PIC_DEBUG_COUNTERS) { + println("[DEBUG_LOG] [PIC] method hit=${PerfStats.methodPicHit} miss=${PerfStats.methodPicMiss}") + } + + PerfFlags.PIC_DEBUG_COUNTERS = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt new file mode 100644 index 0000000..445a96b --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt @@ -0,0 +1,47 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjList +import kotlin.test.Test +import kotlin.test.assertEquals + +class ScriptSubsetJvmTest { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + private suspend fun evalList(code: String): List = (Scope().eval(code) as ObjList).list.map { (it as? ObjInt)?.value ?: it } + + @Test + fun binarySearchBasics_jvm_only() = runBlocking { + val code = """ + val coll = [1,2,3,4,5] + coll.binarySearch(3) + """.trimIndent() + // OFF + PerfFlags.RVAL_FASTPATH = false + val rOff = evalInt(code) + // ON + PerfFlags.RVAL_FASTPATH = true + val rOn = evalInt(code) + assertEquals(2L, rOff) + assertEquals(2L, rOn) + PerfFlags.RVAL_FASTPATH = false + } + + @Test + fun optionalChainingIndexField_jvm_only() = runBlocking { + val code = """ + val a = null + val r1 = a?.x + val lst = [1,2,3] + val r2 = lst[1] + r2 + """.trimIndent() + PerfFlags.RVAL_FASTPATH = false + val off = evalInt(code) + PerfFlags.RVAL_FASTPATH = true + val on = evalInt(code) + assertEquals(2L, off) + assertEquals(2L, on) + PerfFlags.RVAL_FASTPATH = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt new file mode 100644 index 0000000..1ba9974 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt @@ -0,0 +1,140 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjBool +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjList +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * JVM-only fast functional subset additions. Keep each test quick (< ~1s) and deterministic. + */ +class ScriptSubsetJvmTest_Additions3 { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + private suspend fun evalBool(code: String): Boolean = (Scope().eval(code) as ObjBool).value + private suspend fun evalList(code: String): List = (Scope().eval(code) as ObjList).list.map { (it as? ObjInt)?.value ?: it } + + @Test + fun controlFlow_when_and_ifElse_jvm_only() = runBlocking { + val code = """ + fun classify(x) { + when(x) { + 0 -> 100 + 1 -> 200 + else -> 300 + } + } + val a = classify(0) + val b = classify(1) + val c = classify(5) + if (true) 1 else 2 + a + b + c + """.trimIndent() + val r = evalInt(code) + // 100 + 200 + 300 = 600 + assertEquals(600L, r) + } + + @Test + fun optionals_chain_field_index_method_jvm_only() = runBlocking { + val code = """ + class Box() { + var xs = [10,20,30] + fun get(i) { xs[i] } + } + val maybe = null + val b = Box() + // optional on null yields null + val r1 = maybe?.xs + // optional on non-null: method and index + val r2 = b?.get(1) + r2 + """.trimIndent() + val r = evalInt(code) + assertEquals(20L, r) + } + + @Test + fun exceptions_try_catch_finally_jvm_only() = runBlocking { + val code = """ + fun risky(x) { if (x == 0) throw "boom" else 7 } + var s = 0 + try { s = risky(0) } catch (e) { s = 1 } finally { s = s + 2 } + s + """.trimIndent() + val r = evalInt(code) + // catch sets 1, finally adds 2 -> 3 + assertEquals(3L, r) + } + + @Test + fun classes_visibility_and_fields_jvm_only() = runBlocking { + val code = """ + class C() { + var pub = 1 + private var hidden = 9 + fun getPub() { pub } + fun getHidden() { hidden } + fun setPub(v) { pub = v } + } + val c = C() + c.setPub(5) + c.getPub() + c.getHidden() + """.trimIndent() + val r = evalInt(code) + // 5 + 9 + assertEquals(14L, r) + } + + @Test + fun collections_insert_remove_and_maps_jvm_only() = runBlocking { + val code = """ + val lst = [] + lst.insertAt(0, 2) + lst.insertAt(0, 1) + lst.removeAt(1) + // now [1] + val a = 10 + val b = 20 + a + b + lst[0] + """.trimIndent() + val r = evalInt(code) + assertEquals(31L, r) + } + + @Test + fun loops_for_and_while_basics_jvm_only() = runBlocking { + val n = 2000 + val code = """ + var s = 0 + for (i in 0..$n) { s = s + 1 } + var j = 0 + while (j < $n) { s = s + 1; j = j + 1 } + s + """.trimIndent() + val r = evalInt(code) + // for loop adds n+1, while adds n -> total 2n+1 + assertEquals((2L*n + 1L), r) + } + + @Test + fun pooling_edgecase_captures_and_exception_jvm_only() = runBlocking { + // Ensure pooling ON for this test, then restore default + val prev = PerfFlags.SCOPE_POOL + try { + PerfFlags.SCOPE_POOL = true + val code = """ + fun outer(a) { + fun inner(b) { if (b == 0) throw "err" else a + b } + try { inner(0) } catch (e) { a + 2 } + } + outer(5) + """.trimIndent() + val r = evalInt(code) + assertEquals(7L, r) + } finally { + PerfFlags.SCOPE_POOL = prev + } + } +} diff --git a/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt new file mode 100644 index 0000000..5a64ece --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt @@ -0,0 +1,137 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjList +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * More JVM-only fast functional tests migrated from ScriptTest to avoid MPP runs. + * Keep each test fast (<1s) and deterministic. + */ +class ScriptSubsetJvmTest_Additions4 { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + private suspend fun evalList(code: String): List = (Scope().eval(code) as ObjList).list.map { (it as? ObjInt)?.value ?: it } + + @Test + fun mapsAndSetsBasics_jvm_only() = runBlocking { + // Validate simple list map behavior without relying on extra stdlib + val code = """ + val src = [1,2,2,3,1] + // map over list + val doubled = src.map { it + 1 } + doubled.size() + """.trimIndent() + val r = evalInt(code) + // doubled size == original size (5) + assertEquals(5L, r) + } + + @Test + fun optionalChainingDeep_jvm_only() = runBlocking { + val code = """ + class A() { fun b() { null } } + val a = A() + val r1 = a?.b()?.c + val r2 = (a?.b()?.c ?: 7) + r2 + """.trimIndent() + val r = evalInt(code) + assertEquals(7L, r) + } + + @Test + fun whenExpressionBasics_jvm_only() = runBlocking { + val code = """ + fun f(x) { + when(x) { + 0 -> 100 + 1 -> 200 + else -> 300 + } + } + f(0) + f(1) + f(2) + """.trimIndent() + val r = evalInt(code) + assertEquals(600L, r) + } + + @Test + fun tryCatchFinallyWithReturn_jvm_only() = runBlocking { + val code = """ + fun g(x) { + var t = 0 + try { + if (x < 0) throw("oops") + t = x + } catch (e) { + t = 5 + } finally { + t = t + 1 + } + t + } + g(-1) + g(3) + """.trimIndent() + val r = evalInt(code) + // g(-1): catch sets 5, finally +1 => 6; g(3): t=3, finally +1 => 4; total 10 + assertEquals(10L, r) + } + + @Test + fun pooling_edge_case_closure_and_exception_jvm_only() = runBlocking { + val code = """ + fun maker(base) { { base + 1 } } + val c = maker(41) + var r = 0 + try { + r = c() + throw("fail") + } catch (e) { + r = r + 1 + } + r + """.trimIndent() + // OFF + PerfFlags.SCOPE_POOL = false + val off = evalInt(code) + // ON + PerfFlags.SCOPE_POOL = true + val on = evalInt(code) + assertEquals(43L, off) + assertEquals(43L, on) + // reset + PerfFlags.SCOPE_POOL = false + } + + @Test + fun forWhileNested_jvm_only() = runBlocking { + val code = """ + var s = 0 + for (i in 1..10) { + var j = 0 + while (j < 3) { + if (i % 2 == 0 && j == 1) { j = j + 1; continue } + s = s + i + j + j = j + 1 + } + } + s + """.trimIndent() + val r = evalInt(code) + // Compute expected quickly in Kotlin mirror + var expected = 0L + for (i in 1..10) { + var j = 0 + while (j < 3) { + if (i % 2 == 0 && j == 1) { j += 1; continue } + expected += i + j + j += 1 + } + } + assertEquals(expected, r) + assertTrue(expected > 0) + } +} diff --git a/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt new file mode 100644 index 0000000..89c967a --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt @@ -0,0 +1,81 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +/** + * JVM-only fast functional tests to broaden coverage for pooling, classes, and control flow. + * Keep each test fast (<1s) and deterministic. + */ +class ScriptSubsetJvmTest_Additions5 { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + + @Test + fun classVisibility_public_vs_private_jvm_only() = runBlocking { + val code = """ + class C() { + var pub = 3 + private var prv = 5 + fun getPrv() { prv } + } + val c = C() + c.pub + c.getPrv() + """.trimIndent() + val r = evalInt(code) + assertEquals(8L, r) + } + + @Test + fun classVisibility_private_field_access_error_jvm_only() = runBlocking { + val code = """ + class C() { + private var prv = 5 + } + val c = C() + // attempt to access private field should fail + c.prv + """.trimIndent() + // We expect an exception; Scope.eval() will throw ExecutionError. + assertFailsWith { evalInt(code) } + // Ensure Unit return from the test body + Unit + } + + @Test + fun inheritance_override_call_path_jvm_only() = runBlocking { + val code = """ + // Simple two classes with same method name; no inheritance to avoid syntax dependencies + class A() { fun v() { 1 } } + class B() { fun v() { 2 } } + val a = A(); val b = B() + a.v() + b.v() + """.trimIndent() + val r = evalInt(code) + assertEquals(3L, r) + } + + @Test + fun pooled_frames_closure_this_capture_jvm_only() = runBlocking { + val code = """ + class Box() { var x = 40; fun inc() { x = x + 1 } fun get() { x } } + fun make(block) { block } + val b = Box() + val f = make { b.inc(); b.get() } + var r = 0 + r = f() + r + """.trimIndent() + // OFF + PerfFlags.SCOPE_POOL = false + val off = evalInt(code) + // ON + PerfFlags.SCOPE_POOL = true + val on = evalInt(code) + assertEquals(41L, off) + assertEquals(41L, on) + PerfFlags.SCOPE_POOL = false + } +} diff --git a/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt new file mode 100644 index 0000000..8a8a3f9 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt @@ -0,0 +1,120 @@ +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjList +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Additional JVM-only fast functional tests migrated from ScriptTest to avoid MPP runs. + * Keep each test fast (<1s) and with clear assertions. + */ +class ScriptSubsetJvmTest_Additions { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + private suspend fun evalList(code: String): List = (Scope().eval(code) as ObjList).list.map { (it as? ObjInt)?.value ?: it } + + @Test + fun rangesAndForLoop_jvm_only() = runBlocking { + val code = """ + var s = 0 + for (i in 1..100) s = s + i + s + """.trimIndent() + val r = evalInt(code) + assertEquals(5050L, r) + } + + @Test + fun classFieldsAndMethods_jvm_only() = runBlocking { + val n = 20000 + val code = """ + class Counter() { + var x = 0 + fun inc() { x = x + 1 } + fun get() { x } + } + val c = Counter() + var i = 0 + while (i < $n) { c.inc(); i = i + 1 } + c.get() + """.trimIndent() + val r = evalInt(code) + assertEquals(n.toLong(), r) + } + + @Test + fun elvisAndLogicalChains_jvm_only() = runBlocking { + val n = 10000 + val code = """ + val maybe = null + var s = 0 + var i = 0 + while (i < $n) { + s = s + (maybe ?: 0) + if ((i % 3 == 0 && true) || false) { s = s + 1 } else { s = s + 2 } + s = s + (i - (i / 2) * 2) + i = i + 1 + } + s + """.trimIndent() + val r = evalInt(code) + // Kotlin mirror for correctness + var s = 0L + var i = 0 + while (i < n) { + if ((i % 3 == 0 && true) || false) s += 1 else s += 2 + s += (i - (i / 2) * 2) + i += 1 + } + assertEquals(s, r) + } + + @Test + fun sortedInsertWithBinarySearch_jvm_only() = runBlocking { + val code = """ + val src = [3,1,2] + val result = [] + for (x in src) { + val i = result.binarySearch(x) + result.insertAt(if (i < 0) -i-1 else i, x) + } + result + """.trimIndent() + val r = evalList(code) + assertEquals(listOf(1L, 2L, 3L), r) + } +} + + +class ScriptSubsetJvmTest_Additions2 { + private suspend fun evalInt(code: String): Long = (Scope().eval(code) as ObjInt).value + + @Test + fun optionalMethodCallWithElvis_jvm_only() = runBlocking { + val code = """ + class C() { fun get() { 5 } } + val a = null + (a?.get() ?: 7) + """.trimIndent() + val r = evalInt(code) + assertEquals(7L, r) + } + + @Test + fun continueAndBreakInWhile_jvm_only() = runBlocking { + val code = """ + var s = 0 + var i = 0 + while (i <= 100) { + if (i > 50) break + if (i % 2 == 1) { i = i + 1; continue } + s = s + i + i = i + 1 + } + s + """.trimIndent() + val r = evalInt(code) + // Sum of even numbers from 0 to 50 inclusive: 2 * (0+1+...+25) = 2 * 325 = 650 + assertEquals(650L, r) + } +} diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ArgBuilderNative.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ArgBuilderNative.kt new file mode 100644 index 0000000..4371f21 --- /dev/null +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ArgBuilderNative.kt @@ -0,0 +1,20 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +actual object ArgBuilderProvider { + actual fun acquire(): ArgsBuilder = NativeArgsBuilder() +} + +private class NativeArgsBuilder : ArgsBuilder { + private var buf: MutableList = ArrayList() + + override fun reset(expectedSize: Int) { + buf = ArrayList(expectedSize.coerceAtLeast(0)) + } + + override fun add(v: Obj) { buf.add(v) } + override fun addAll(vs: List) { if (vs.isNotEmpty()) buf.addAll(vs) } + override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode) + override fun release() { /* no-op */ } +} diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt new file mode 100644 index 0000000..3da9b18 --- /dev/null +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt @@ -0,0 +1,19 @@ +package net.sergeych.lyng + +actual object PerfDefaults { + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true + + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + actual val SCOPE_POOL: Boolean = false + + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + + actual val PIC_DEBUG_COUNTERS: Boolean = false + + actual val PRIMITIVE_FASTOPS: Boolean = true + // Conservative default for non-JVM until validated + actual val RVAL_FASTPATH: Boolean = false +} \ No newline at end of file diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ArgBuilderWasm.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ArgBuilderWasm.kt new file mode 100644 index 0000000..5234fb5 --- /dev/null +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ArgBuilderWasm.kt @@ -0,0 +1,20 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.Obj + +actual object ArgBuilderProvider { + actual fun acquire(): ArgsBuilder = WasmArgsBuilder() +} + +private class WasmArgsBuilder : ArgsBuilder { + private var buf: MutableList = ArrayList() + + override fun reset(expectedSize: Int) { + buf = ArrayList(expectedSize.coerceAtLeast(0)) + } + + override fun add(v: Obj) { buf.add(v) } + override fun addAll(vs: List) { if (vs.isNotEmpty()) buf.addAll(vs) } + override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode) + override fun release() { /* no-op */ } +} diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt new file mode 100644 index 0000000..3da9b18 --- /dev/null +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt @@ -0,0 +1,19 @@ +package net.sergeych.lyng + +actual object PerfDefaults { + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true + + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true + actual val SCOPE_POOL: Boolean = false + + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + + actual val PIC_DEBUG_COUNTERS: Boolean = false + + actual val PRIMITIVE_FASTOPS: Boolean = true + // Conservative default for non-JVM until validated + actual val RVAL_FASTPATH: Boolean = false +} \ No newline at end of file