big optimization round
This commit is contained in:
parent
029fde2883
commit
38c1b3c209
@ -204,4 +204,18 @@ Flows allow easy transforming of any [Iterable]. See how the standard Lyng libra
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Iterable]: Iterable.md
|
[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.
|
||||||
|
|||||||
119
docs/perf_guide.md
Normal file
119
docs/perf_guide.md
Normal file
@ -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.
|
||||||
@ -142,6 +142,13 @@ publishing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure JVM test stdout is visible and runs are single-threaded for stable timings
|
||||||
|
tasks.withType<org.gradle.api.tasks.testing.Test> {
|
||||||
|
testLogging {
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
maxParallelForks = 1
|
||||||
|
}
|
||||||
|
|
||||||
//mavenPublishing {
|
//mavenPublishing {
|
||||||
// publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
// publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
|
||||||
|
actual object ArgBuilderProvider {
|
||||||
|
private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
|
||||||
|
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
|
||||||
|
}
|
||||||
|
actual fun acquire(): ArgsBuilder = tl.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AndroidArgsBuilder : ArgsBuilder {
|
||||||
|
private val buf: ArrayList<Obj> = 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<Obj>) {
|
||||||
|
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 */ }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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<Obj>)
|
||||||
|
/** 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()
|
||||||
|
}
|
||||||
@ -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<Obj> = ArrayList(initialCapacity)
|
||||||
@ -24,31 +24,95 @@ import net.sergeych.lyng.obj.ObjList
|
|||||||
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
|
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
|
||||||
|
|
||||||
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
|
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
|
||||||
// If ARG_BUILDER is enabled, try to reuse a pre-sized ArrayList and do bulk-adds
|
// Small-arity fast path (no splats) to reduce allocations
|
||||||
val list: MutableList<Obj> = if (PerfFlags.ARG_BUILDER) ArrayList(this.size) else mutableListOf()
|
if (PerfFlags.ARG_BUILDER) {
|
||||||
|
var hasSplat = false
|
||||||
for (x in this) {
|
var count = 0
|
||||||
val value = x.value.execute(scope)
|
for (pa in this) {
|
||||||
if (x.isSplat) {
|
if (pa.isSplat) { hasSplat = true; break }
|
||||||
when {
|
count++
|
||||||
value is ObjList -> {
|
if (count > 3) break
|
||||||
// Bulk add elements from an ObjList
|
}
|
||||||
list.addAll(value.list)
|
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)
|
||||||
}
|
}
|
||||||
|
3 -> {
|
||||||
value.isInstanceOf(ObjIterable) -> {
|
val a0 = this.elementAt(0).value.execute(scope)
|
||||||
// Convert to list once and bulk add
|
val a1 = this.elementAt(1).value.execute(scope)
|
||||||
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
|
val a2 = this.elementAt(2).value.execute(scope)
|
||||||
list.addAll(i)
|
Arguments(listOf(a0, a1, a2), tailBlockMode)
|
||||||
}
|
}
|
||||||
|
4 -> {
|
||||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
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 {
|
if (quick != null) return quick
|
||||||
list.add(value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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<Obj> = 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<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
|
data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
|
||||||
@ -57,7 +121,14 @@ import net.sergeych.lyng.obj.ObjList
|
|||||||
|
|
||||||
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
|
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
|
||||||
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -1,26 +1,33 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime-togglable perf flags for micro-benchmarking and A/B comparisons on the JVM.
|
* Runtime-togglable perf flags for micro-benchmarking and A/B comparisons.
|
||||||
* Keep as `var` so tests can flip them.
|
* Initialized from platform-specific defaults via PerfDefaults expect/actual.
|
||||||
|
* Keep as `var` so tests can flip them at runtime.
|
||||||
*/
|
*/
|
||||||
object PerfFlags {
|
object PerfFlags {
|
||||||
// Enable PIC inside LocalVarRef (runtime cache of name->slot per frame)
|
// 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
|
// 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
|
// 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.
|
// Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only.
|
||||||
var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER
|
||||||
// Enable pooling of Scope frames for calls (planned; JVM-only)
|
// Enable pooling of Scope frames for calls (may be JVM-only optimization)
|
||||||
var SCOPE_POOL: Boolean = false
|
var SCOPE_POOL: Boolean = PerfDefaults.SCOPE_POOL
|
||||||
|
|
||||||
// Step 2: PICs for fields and methods
|
// Step 2: PICs for fields and methods
|
||||||
var FIELD_PIC: Boolean = true
|
var FIELD_PIC: Boolean = PerfDefaults.FIELD_PIC
|
||||||
var METHOD_PIC: Boolean = true
|
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
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
40
lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt
Normal file
40
lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfStats.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,8 +22,8 @@ import net.sergeych.lyng.pacman.ImportManager
|
|||||||
import net.sergeych.lyng.pacman.ImportProvider
|
import net.sergeych.lyng.pacman.ImportProvider
|
||||||
|
|
||||||
// Simple per-frame id generator for perf caches (not thread-safe, fine for scripts)
|
// 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++ }
|
object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
||||||
private fun nextFrameId(): Long = FrameIdGen.nextId()
|
fun nextFrameId(): Long = FrameIdGen.nextId()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
|
* 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
|
* - [ClosureScope] - scope used to apply a closure to some thisObj scope
|
||||||
*/
|
*/
|
||||||
open class Scope(
|
open class Scope(
|
||||||
val parent: Scope?,
|
var parent: Scope?,
|
||||||
val args: Arguments = Arguments.EMPTY,
|
var args: Arguments = Arguments.EMPTY,
|
||||||
var pos: Pos = Pos.builtIn,
|
var pos: Pos = Pos.builtIn,
|
||||||
var thisObj: Obj = ObjVoid,
|
var thisObj: Obj = ObjVoid,
|
||||||
var skipScopeCreation: Boolean = false,
|
var skipScopeCreation: Boolean = false,
|
||||||
) {
|
) {
|
||||||
// Unique id per scope frame for PICs; cheap to compare and stable for the frame lifetime.
|
// Unique id per scope frame for PICs; regenerated on each borrow from the pool.
|
||||||
val frameId: Long = nextFrameId()
|
var frameId: Long = nextFrameId()
|
||||||
|
|
||||||
// Fast-path storage for local variables/arguments accessed by slot index.
|
// Fast-path storage for local variables/arguments accessed by slot index.
|
||||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||||
@ -165,12 +165,48 @@ open class Scope(
|
|||||||
return idx
|
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`.
|
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||||
*/
|
*/
|
||||||
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
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 <R> 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`.
|
* 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.
|
* The child scope inherits the current scope's properties such as position and the existing `thisObj` if no new `thisObj` is provided.
|
||||||
|
|||||||
35
lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt
Normal file
35
lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt
Normal file
@ -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<Scope>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -265,7 +265,12 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): 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 =
|
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
||||||
callOn(
|
callOn(
|
||||||
|
|||||||
@ -24,6 +24,11 @@ import net.sergeych.lyng.*
|
|||||||
*/
|
*/
|
||||||
sealed interface ObjRef {
|
sealed interface ObjRef {
|
||||||
suspend fun get(scope: Scope): ObjRecord
|
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) {
|
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
throw ScriptError(pos, "can't assign value")
|
throw ScriptError(pos, "can't assign value")
|
||||||
}
|
}
|
||||||
@ -51,7 +56,7 @@ enum class BinOp {
|
|||||||
/** R-value reference for unary operations. */
|
/** R-value reference for unary operations. */
|
||||||
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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) {
|
val r = when (op) {
|
||||||
UnaryOp.NOT -> v.logicalNot(scope)
|
UnaryOp.NOT -> v.logicalNot(scope)
|
||||||
UnaryOp.NEGATE -> v.negate(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. */
|
/** R-value reference for binary operations. */
|
||||||
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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
|
||||||
val b = right.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)
|
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
||||||
if (net.sergeych.lyng.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
|
BinOp.NEQ -> if (a.value != b.value) ObjTrue else ObjFalse
|
||||||
else -> null
|
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
|
// Fast integer ops when both operands are ObjInt
|
||||||
if (a is ObjInt && b is 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
|
BinOp.GTE -> if (av >= bv) ObjTrue else ObjFalse
|
||||||
else -> null
|
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 {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val x = target.get(scope).value
|
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) {
|
val inPlace: Obj? = when (op) {
|
||||||
BinOp.PLUS -> x.plusAssign(scope, y)
|
BinOp.PLUS -> x.plusAssign(scope, y)
|
||||||
BinOp.MINUS -> x.minusAssign(scope, y)
|
BinOp.MINUS -> x.minusAssign(scope, y)
|
||||||
@ -196,8 +207,8 @@ class IncDecRef(
|
|||||||
/** Elvis operator reference: a ?: b */
|
/** Elvis operator reference: a ?: b */
|
||||||
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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
|
||||||
val r = if (a != ObjNull) a else right.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
|
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 */
|
/** Logical OR with short-circuit: a || b */
|
||||||
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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
|
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 (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
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 */
|
/** Logical AND with short-circuit: a && b */
|
||||||
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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
|
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 (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||||
if (a is ObjBool && b is ObjBool) {
|
if (a is ObjBool && b is ObjBool) {
|
||||||
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
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
|
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 {
|
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 (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||||
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
||||||
val (key, ver) = receiverKeyAndVersion(base)
|
val (key, ver) = receiverKeyAndVersion(base)
|
||||||
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope) }
|
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
|
||||||
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) return g(base, scope) }
|
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
|
// Slow path
|
||||||
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicMiss++
|
||||||
val rec = base.readField(scope, name)
|
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
|
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
|
||||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
|
when (base) {
|
||||||
when (obj) {
|
is ObjClass -> {
|
||||||
is ObjInstance -> {
|
val clsScope = base.classScope
|
||||||
val instScope = obj.instanceScope
|
val capturedIdx = clsScope?.getSlotIndexOf(name)
|
||||||
val idx = instScope.getSlotIndexOf(name)
|
if (clsScope != null && capturedIdx != null) {
|
||||||
if (idx != null) {
|
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
|
||||||
val r = instScope.getSlotRecord(idx)
|
val scope0 = (obj as ObjClass).classScope!!
|
||||||
if (!r.visibility.isPublic)
|
val r0 = scope0.getSlotRecord(capturedIdx)
|
||||||
|
if (!r0.visibility.isPublic)
|
||||||
sc.raiseError(ObjAccessException(sc, "can't access non-public field $name"))
|
sc.raiseError(ObjAccessException(sc, "can't access non-public field $name"))
|
||||||
r
|
r0
|
||||||
} else obj.readField(sc, name)
|
}
|
||||||
|
} else {
|
||||||
|
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
|
||||||
}
|
}
|
||||||
is ObjClass -> {
|
}
|
||||||
val clsScope = obj.classScope
|
else -> {
|
||||||
if (clsScope != null) {
|
// For instances and other types, fall back to name-based lookup per access (slot index may differ per instance)
|
||||||
val idx = clsScope.getSlotIndexOf(name)
|
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rec
|
return rec
|
||||||
@ -305,39 +317,38 @@ class FieldRef(
|
|||||||
}
|
}
|
||||||
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
||||||
val (key, ver) = receiverKeyAndVersion(base)
|
val (key, ver) = receiverKeyAndVersion(base)
|
||||||
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) return s(base, scope, newValue) }
|
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
|
||||||
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) return s(base, scope, newValue) }
|
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
|
// Slow path
|
||||||
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetMiss++
|
||||||
base.writeField(scope, name, newValue)
|
base.writeField(scope, name, newValue)
|
||||||
// Install move-to-front with a handle-aware setter
|
// Install move-to-front with a handle-aware setter
|
||||||
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
|
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
|
||||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v ->
|
when (base) {
|
||||||
when (obj) {
|
is ObjClass -> {
|
||||||
is ObjInstance -> {
|
val clsScope = base.classScope
|
||||||
val instScope = obj.instanceScope
|
val capturedIdx = clsScope?.getSlotIndexOf(name)
|
||||||
val idx = instScope.getSlotIndexOf(name)
|
if (clsScope != null && capturedIdx != null) {
|
||||||
if (idx != null) {
|
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v ->
|
||||||
val r = instScope.getSlotRecord(idx)
|
val scope0 = (obj as ObjClass).classScope!!
|
||||||
if (!r.visibility.isPublic)
|
val r0 = scope0.getSlotRecord(capturedIdx)
|
||||||
sc.raiseError(ObjAccessException(sc, "can't assign to non-public field $name"))
|
if (!r0.isMutable)
|
||||||
if (!r.isMutable)
|
|
||||||
sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name"))
|
sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name"))
|
||||||
if (r.value.assign(sc, v) == null) r.value = v
|
if (r0.value.assign(sc, v) == null) r0.value = v
|
||||||
} else obj.writeField(sc, name, v)
|
}
|
||||||
|
} else {
|
||||||
|
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
|
||||||
}
|
}
|
||||||
is ObjClass -> {
|
}
|
||||||
val clsScope = obj.classScope
|
else -> {
|
||||||
if (clsScope != null) {
|
// For instances and other types, fall back to generic write (instance slot indices may differ per instance)
|
||||||
val idx = clsScope.getSlotIndexOf(name)
|
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -361,9 +372,9 @@ class IndexRef(
|
|||||||
private val isOptional: Boolean,
|
private val isOptional: Boolean,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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 (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
|
return base.getAt(scope, idx).asMutable
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,10 +406,16 @@ class CallRef(
|
|||||||
private val isOptionalInvoke: Boolean,
|
private val isOptionalInvoke: Boolean,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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
|
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
|
||||||
val callArgs = args.toArguments(scope, tailBlock)
|
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
|
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
|
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 {
|
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
|
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
||||||
val callArgs = args.toArguments(scope, tailBlock)
|
val callArgs = args.toArguments(scope, tailBlock)
|
||||||
if (net.sergeych.lyng.PerfFlags.METHOD_PIC) {
|
if (net.sergeych.lyng.PerfFlags.METHOD_PIC) {
|
||||||
val (key, ver) = receiverKeyAndVersion(base)
|
val (key, ver) = receiverKeyAndVersion(base)
|
||||||
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) return inv(base, scope, callArgs).asReadonly }
|
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
|
||||||
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) return inv(base, scope, callArgs).asReadonly }
|
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
|
// Slow path
|
||||||
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicMiss++
|
||||||
val result = base.invokeInstanceMethod(scope, name, callArgs)
|
val result = base.invokeInstanceMethod(scope, name, callArgs)
|
||||||
// Install move-to-front with a handle-aware invoker
|
// Install move-to-front with a handle-aware invoker
|
||||||
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
|
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
|
||||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
when (base) {
|
||||||
when (obj) {
|
is ObjInstance -> {
|
||||||
is ObjInstance -> {
|
// Prefer resolved class member to avoid per-call lookup on hit
|
||||||
val instScope = obj.instanceScope
|
val member = base.objClass.getInstanceMemberOrNull(name)
|
||||||
val rec = instScope.get(name)
|
if (member != null) {
|
||||||
if (rec != null) {
|
val visibility = member.visibility
|
||||||
if (!rec.visibility.isPublic)
|
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"))
|
sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name"))
|
||||||
rec.value.invoke(instScope, obj, a)
|
callable.invoke(inst.instanceScope, inst, a)
|
||||||
} else obj.invokeInstanceMethod(sc, name, 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
|
is ObjClass -> {
|
||||||
if (clsScope != null) {
|
val clsScope = base.classScope
|
||||||
val rec = clsScope.get(name)
|
val rec = clsScope?.get(name)
|
||||||
if (rec != null) {
|
if (rec != null) {
|
||||||
rec.value.invoke(sc, obj, a)
|
val callable = rec.value
|
||||||
} else obj.invokeInstanceMethod(sc, name, a)
|
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) }
|
||||||
} else obj.invokeInstanceMethod(sc, name, 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
|
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 {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
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'")
|
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||||
}
|
}
|
||||||
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
if (slot >= 0) return scope.getSlotRecord(slot)
|
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'")
|
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,10 +628,13 @@ class FastLocalVarRef(
|
|||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null
|
val ownerValid = isOwnerValidFor(scope)
|
||||||
val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
|
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)
|
return actualOwner.getSlotRecord(slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,10 +656,11 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
|||||||
for (e in entries) {
|
for (e in entries) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is ListEntry.Element -> {
|
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 -> {
|
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) {
|
when (elements) {
|
||||||
is ObjList -> list.addAll(elements.list)
|
is ObjList -> list.addAll(elements.list)
|
||||||
else -> scope.raiseError("Spread element must be list")
|
else -> scope.raiseError("Spread element must be list")
|
||||||
@ -633,8 +681,8 @@ class RangeRef(
|
|||||||
private val isEndInclusive: Boolean
|
private val isEndInclusive: Boolean
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
val l = left?.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?.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
|
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -646,7 +694,7 @@ class AssignRef(
|
|||||||
private val atPos: Pos,
|
private val atPos: Pos,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
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)
|
val rec = target.get(scope)
|
||||||
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
||||||
if (rec.value.assign(scope, v) == null) {
|
if (rec.value.assign(scope, v) == null) {
|
||||||
|
|||||||
@ -3332,7 +3332,7 @@ class ScriptTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// @Test
|
///@Test
|
||||||
fun testMinimumOptimization() = runTest {
|
fun testMinimumOptimization() = runTest {
|
||||||
for (i in 1..200) {
|
for (i in 1..200) {
|
||||||
bm {
|
bm {
|
||||||
|
|||||||
20
lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt
Normal file
20
lynglib/src/jsMain/kotlin/net/sergeych/lyng/ArgBuilderJs.kt
Normal file
@ -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<Obj> = ArrayList()
|
||||||
|
|
||||||
|
override fun reset(expectedSize: Int) {
|
||||||
|
buf = ArrayList(expectedSize.coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(v: Obj) { buf.add(v) }
|
||||||
|
override fun addAll(vs: List<Obj>) { if (vs.isNotEmpty()) buf.addAll(vs) }
|
||||||
|
override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode)
|
||||||
|
override fun release() { /* no-op */ }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
|
||||||
|
actual object ArgBuilderProvider {
|
||||||
|
private val tl = object : ThreadLocal<JvmArgsBuilder>() {
|
||||||
|
override fun initialValue(): JvmArgsBuilder = JvmArgsBuilder()
|
||||||
|
}
|
||||||
|
actual fun acquire(): ArgsBuilder = tl.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class JvmArgsBuilder : ArgsBuilder {
|
||||||
|
private val buf: ArrayList<Obj> = 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<Obj>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -52,4 +52,49 @@ class CallBenchmarkTest {
|
|||||||
assertEquals(expected, r1)
|
assertEquals(expected, r1)
|
||||||
assertEquals(expected, r2)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt
Normal file
57
lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt
Normal file
53
lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt
Normal file
55
lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt
Normal file
76
lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt
Normal file
61
lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt
Normal file
50
lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt
Normal file
81
lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,10 @@ class PicBenchmarkTest {
|
|||||||
val t3 = System.nanoTime()
|
val t3 = System.nanoTime()
|
||||||
println("[DEBUG_LOG] [BENCH] Field PIC=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
println("[DEBUG_LOG] [BENCH] Field PIC=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||||
assertEquals(iterations.toLong(), r2)
|
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
|
@Test
|
||||||
|
|||||||
105
lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt
Normal file
105
lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt
Normal file
47
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest.kt
Normal file
@ -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<Any?> = (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
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt
Normal file
140
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions3.kt
Normal file
@ -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<Any?> = (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt
Normal file
137
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions4.kt
Normal file
@ -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<Any?> = (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)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt
Normal file
81
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_Additions5.kt
Normal file
@ -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<Throwable> { 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
|
||||||
|
}
|
||||||
|
}
|
||||||
120
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt
Normal file
120
lynglib/src/jvmTest/kotlin/ScriptSubsetJvmTest_additions.kt
Normal file
@ -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<Any?> = (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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Obj> = ArrayList()
|
||||||
|
|
||||||
|
override fun reset(expectedSize: Int) {
|
||||||
|
buf = ArrayList(expectedSize.coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(v: Obj) { buf.add(v) }
|
||||||
|
override fun addAll(vs: List<Obj>) { if (vs.isNotEmpty()) buf.addAll(vs) }
|
||||||
|
override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode)
|
||||||
|
override fun release() { /* no-op */ }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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<Obj> = ArrayList()
|
||||||
|
|
||||||
|
override fun reset(expectedSize: Int) {
|
||||||
|
buf = ArrayList(expectedSize.coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(v: Obj) { buf.add(v) }
|
||||||
|
override fun addAll(vs: List<Obj>) { if (vs.isNotEmpty()) buf.addAll(vs) }
|
||||||
|
override fun build(tailBlockMode: Boolean): Arguments = Arguments(buf.toList(), tailBlockMode)
|
||||||
|
override fun release() { /* no-op */ }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user