big optimization round
This commit is contained in:
1 parent
029fde2883
commit
38c1b3c209
39 files changed
+1914
-143
No files matched your search
@@ -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)
|
||||
|
||||
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
|
||||
val list: MutableList<Obj> = if (PerfFlags.ARG_BUILDER) ArrayList(this.size) else mutableListOf()
|
||||
|
||||
for (x in this) {
|
||||
val value = x.value.execute(scope)
|
||||
if (x.isSplat) {
|
||||
when {
|
||||
value is ObjList -> {
|
||||
// Bulk add elements from an ObjList
|
||||
list.addAll(value.list)
|
||||
// Small-arity fast path (no splats) to reduce allocations
|
||||
if (PerfFlags.ARG_BUILDER) {
|
||||
var hasSplat = false
|
||||
var count = 0
|
||||
for (pa in this) {
|
||||
if (pa.isSplat) { hasSplat = true; break }
|
||||
count++
|
||||
if (count > 3) break
|
||||
}
|
||||
if (!hasSplat && count == this.size) {
|
||||
val quick = when (count) {
|
||||
0 -> Arguments.EMPTY
|
||||
1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode)
|
||||
2 -> {
|
||||
val a0 = this.elementAt(0).value.execute(scope)
|
||||
val a1 = this.elementAt(1).value.execute(scope)
|
||||
Arguments(listOf(a0, a1), tailBlockMode)
|
||||
}
|
||||
|
||||
value.isInstanceOf(ObjIterable) -> {
|
||||
// Convert to list once and bulk add
|
||||
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
|
||||
list.addAll(i)
|
||||
3 -> {
|
||||
val a0 = this.elementAt(0).value.execute(scope)
|
||||
val a1 = this.elementAt(1).value.execute(scope)
|
||||
val a2 = this.elementAt(2).value.execute(scope)
|
||||
Arguments(listOf(a0, a1, a2), tailBlockMode)
|
||||
}
|
||||
|
||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||
4 -> {
|
||||
val a0 = this.elementAt(0).value.execute(scope)
|
||||
val a1 = this.elementAt(1).value.execute(scope)
|
||||
val a2 = this.elementAt(2).value.execute(scope)
|
||||
val a3 = this.elementAt(3).value.execute(scope)
|
||||
Arguments(listOf(a0, a1, a2, a3), tailBlockMode)
|
||||
}
|
||||
5 -> {
|
||||
val a0 = this.elementAt(0).value.execute(scope)
|
||||
val a1 = this.elementAt(1).value.execute(scope)
|
||||
val a2 = this.elementAt(2).value.execute(scope)
|
||||
val a3 = this.elementAt(3).value.execute(scope)
|
||||
val a4 = this.elementAt(4).value.execute(scope)
|
||||
Arguments(listOf(a0, a1, a2, a3, a4), tailBlockMode)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
list.add(value)
|
||||
if (quick != null) return quick
|
||||
}
|
||||
}
|
||||
return Arguments(list, tailBlockMode)
|
||||
// General path with builder or simple list fallback
|
||||
if (PerfFlags.ARG_BUILDER) {
|
||||
val b = ArgBuilderProvider.acquire()
|
||||
try {
|
||||
b.reset(this.size)
|
||||
for (x in this) {
|
||||
val value = x.value.execute(scope)
|
||||
if (x.isSplat) {
|
||||
when {
|
||||
value is ObjList -> {
|
||||
b.addAll(value.list)
|
||||
}
|
||||
value.isInstanceOf(ObjIterable) -> {
|
||||
val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
|
||||
b.addAll(i)
|
||||
}
|
||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||
}
|
||||
} else {
|
||||
b.add(value)
|
||||
}
|
||||
}
|
||||
return b.build(tailBlockMode)
|
||||
} finally {
|
||||
b.release()
|
||||
}
|
||||
} else {
|
||||
val list: MutableList<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 {
|
||||
@@ -57,7 +121,14 @@ import net.sergeych.lyng.obj.ObjList
|
||||
|
||||
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
|
||||
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
|
||||
return list.first().byValueCopy()
|
||||
val v = list.first()
|
||||
// Tiny micro-alloc win: avoid byValueCopy for immutable singletons
|
||||
return when (v) {
|
||||
net.sergeych.lyng.obj.ObjNull,
|
||||
net.sergeych.lyng.obj.ObjTrue,
|
||||
net.sergeych.lyng.obj.ObjFalse -> v
|
||||
else -> v.byValueCopy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* Runtime-togglable perf flags for micro-benchmarking and A/B comparisons on the JVM.
|
||||
* Keep as `var` so tests can flip them.
|
||||
* Runtime-togglable perf flags for micro-benchmarking and A/B comparisons.
|
||||
* Initialized from platform-specific defaults via PerfDefaults expect/actual.
|
||||
* Keep as `var` so tests can flip them at runtime.
|
||||
*/
|
||||
object PerfFlags {
|
||||
// Enable PIC inside LocalVarRef (runtime cache of name->slot per frame)
|
||||
var LOCAL_SLOT_PIC: Boolean = true
|
||||
var LOCAL_SLOT_PIC: Boolean = PerfDefaults.LOCAL_SLOT_PIC
|
||||
// Make the compiler emit fast local refs for identifiers known to be function locals/params
|
||||
var EMIT_FAST_LOCAL_REFS: Boolean = true
|
||||
var EMIT_FAST_LOCAL_REFS: Boolean = PerfDefaults.EMIT_FAST_LOCAL_REFS
|
||||
|
||||
// Enable more efficient argument building and bulk-copy for splats
|
||||
var ARG_BUILDER: Boolean = true
|
||||
var ARG_BUILDER: Boolean = PerfDefaults.ARG_BUILDER
|
||||
// Allow early-return in optional calls before building args (semantics-compatible). Present for A/B only.
|
||||
var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
|
||||
// Enable pooling of Scope frames for calls (planned; JVM-only)
|
||||
var SCOPE_POOL: Boolean = false
|
||||
var SKIP_ARGS_ON_NULL_RECEIVER: Boolean = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER
|
||||
// Enable pooling of Scope frames for calls (may be JVM-only optimization)
|
||||
var SCOPE_POOL: Boolean = PerfDefaults.SCOPE_POOL
|
||||
|
||||
// Step 2: PICs for fields and methods
|
||||
var FIELD_PIC: Boolean = true
|
||||
var METHOD_PIC: Boolean = true
|
||||
var FIELD_PIC: Boolean = PerfDefaults.FIELD_PIC
|
||||
var METHOD_PIC: Boolean = PerfDefaults.METHOD_PIC
|
||||
|
||||
// Debug/observability for PICs and fast paths (JVM-first)
|
||||
var PIC_DEBUG_COUNTERS: Boolean = PerfDefaults.PIC_DEBUG_COUNTERS
|
||||
|
||||
// Step 3: Primitive arithmetic and comparison fast paths
|
||||
var PRIMITIVE_FASTOPS: Boolean = true
|
||||
var PRIMITIVE_FASTOPS: Boolean = PerfDefaults.PRIMITIVE_FASTOPS
|
||||
|
||||
// Step 4: R-value fast path to bypass ObjRecord in pure expression evaluation
|
||||
var RVAL_FASTPATH: Boolean = PerfDefaults.RVAL_FASTPATH
|
||||
}
|
||||
@@ -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
|
||||
|
||||
// Simple per-frame id generator for perf caches (not thread-safe, fine for scripts)
|
||||
private object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
||||
private fun nextFrameId(): Long = FrameIdGen.nextId()
|
||||
object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
||||
fun nextFrameId(): Long = FrameIdGen.nextId()
|
||||
|
||||
/**
|
||||
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
|
||||
@@ -40,14 +40,14 @@ private fun nextFrameId(): Long = FrameIdGen.nextId()
|
||||
* - [ClosureScope] - scope used to apply a closure to some thisObj scope
|
||||
*/
|
||||
open class Scope(
|
||||
val parent: Scope?,
|
||||
val args: Arguments = Arguments.EMPTY,
|
||||
var parent: Scope?,
|
||||
var args: Arguments = Arguments.EMPTY,
|
||||
var pos: Pos = Pos.builtIn,
|
||||
var thisObj: Obj = ObjVoid,
|
||||
var skipScopeCreation: Boolean = false,
|
||||
) {
|
||||
// Unique id per scope frame for PICs; cheap to compare and stable for the frame lifetime.
|
||||
val frameId: Long = nextFrameId()
|
||||
// Unique id per scope frame for PICs; regenerated on each borrow from the pool.
|
||||
var frameId: Long = nextFrameId()
|
||||
|
||||
// Fast-path storage for local variables/arguments accessed by slot index.
|
||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||
@@ -165,12 +165,48 @@ open class Scope(
|
||||
return idx
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this scope instance so it can be safely reused as a fresh child frame.
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
*/
|
||||
fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) {
|
||||
this.parent = parent
|
||||
this.args = args
|
||||
this.pos = pos
|
||||
this.thisObj = thisObj
|
||||
this.skipScopeCreation = false
|
||||
// fresh identity for PIC caches
|
||||
this.frameId = nextFrameId()
|
||||
// clear locals and slot maps
|
||||
objects.clear()
|
||||
slots.clear()
|
||||
nameToSlot.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||
*/
|
||||
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||
|
||||
/**
|
||||
* Execute a block inside a child frame. Guarded for future pooling via [PerfFlags.SCOPE_POOL].
|
||||
* Currently always creates a fresh child scope to preserve unique frameId semantics.
|
||||
*/
|
||||
inline suspend fun <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`.
|
||||
* The child scope inherits the current scope's properties such as position and the existing `thisObj` if no new `thisObj` is provided.
|
||||
|
||||
@@ -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 =
|
||||
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
|
||||
if (net.sergeych.lyng.PerfFlags.SCOPE_POOL)
|
||||
scope.withChildFrame(args, newThisObj = thisObj) { child ->
|
||||
callOn(child)
|
||||
}
|
||||
else
|
||||
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj))
|
||||
|
||||
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
|
||||
callOn(
|
||||
|
||||
@@ -24,6 +24,11 @@ import net.sergeych.lyng.*
|
||||
*/
|
||||
sealed interface ObjRef {
|
||||
suspend fun get(scope: Scope): ObjRecord
|
||||
/**
|
||||
* Fast path for evaluating an expression to a raw Obj value without wrapping it into ObjRecord.
|
||||
* Default implementation calls [get] and returns its value. Nodes can override to avoid record traffic.
|
||||
*/
|
||||
suspend fun evalValue(scope: Scope): Obj = get(scope).value
|
||||
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
throw ScriptError(pos, "can't assign value")
|
||||
}
|
||||
@@ -51,7 +56,7 @@ enum class BinOp {
|
||||
/** R-value reference for unary operations. */
|
||||
class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val v = a.get(scope).value
|
||||
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) a.evalValue(scope) else a.get(scope).value
|
||||
val r = when (op) {
|
||||
UnaryOp.NOT -> v.logicalNot(scope)
|
||||
UnaryOp.NEGATE -> v.negate(scope)
|
||||
@@ -63,8 +68,8 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef {
|
||||
/** R-value reference for binary operations. */
|
||||
class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val b = right.get(scope).value
|
||||
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
||||
val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
||||
|
||||
// Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS)
|
||||
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
@@ -77,7 +82,10 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
||||
BinOp.NEQ -> if (a.value != b.value) ObjTrue else ObjFalse
|
||||
else -> null
|
||||
}
|
||||
if (r != null) return r.asReadonly
|
||||
if (r != null) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
// Fast integer ops when both operands are ObjInt
|
||||
if (a is ObjInt && b is ObjInt) {
|
||||
@@ -97,7 +105,10 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
||||
BinOp.GTE -> if (av >= bv) ObjTrue else ObjFalse
|
||||
else -> null
|
||||
}
|
||||
if (r != null) return r.asReadonly
|
||||
if (r != null) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.primitiveFastOpsHit++
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +150,7 @@ class AssignOpRef(
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val x = target.get(scope).value
|
||||
val y = value.get(scope).value
|
||||
val y = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
||||
val inPlace: Obj? = when (op) {
|
||||
BinOp.PLUS -> x.plusAssign(scope, y)
|
||||
BinOp.MINUS -> x.minusAssign(scope, y)
|
||||
@@ -196,8 +207,8 @@ class IncDecRef(
|
||||
/** Elvis operator reference: a ?: b */
|
||||
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val r = if (a != ObjNull) a else right.get(scope).value
|
||||
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
||||
val r = if (a != ObjNull) a else if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
||||
return r.asReadonly
|
||||
}
|
||||
}
|
||||
@@ -205,9 +216,9 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
/** Logical OR with short-circuit: a || b */
|
||||
class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
||||
if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly
|
||||
val b = right.get(scope).value
|
||||
val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
||||
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
if (a is ObjBool && b is ObjBool) {
|
||||
return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||
@@ -220,9 +231,9 @@ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef
|
||||
/** Logical AND with short-circuit: a && b */
|
||||
class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.get(scope).value
|
||||
val a = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) left.evalValue(scope) else left.get(scope).value
|
||||
if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly
|
||||
val b = right.get(scope).value
|
||||
val b = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) right.evalValue(scope) else right.get(scope).value
|
||||
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
if (a is ObjBool && b is ObjBool) {
|
||||
return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly
|
||||
@@ -255,41 +266,42 @@ class FieldRef(
|
||||
private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: (suspend (Obj, Scope, Obj) -> Unit)? = null
|
||||
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = target.get(scope).value
|
||||
val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value
|
||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
||||
val (key, ver) = receiverKeyAndVersion(base)
|
||||
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) return g(base, scope) }
|
||||
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) return g(base, scope) }
|
||||
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicHit++
|
||||
return g(base, scope)
|
||||
} }
|
||||
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicHit++
|
||||
return g(base, scope)
|
||||
} }
|
||||
// Slow path
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicMiss++
|
||||
val rec = base.readField(scope, name)
|
||||
// Install move-to-front with a handle-aware getter
|
||||
// Install move-to-front with a handle-aware getter. Where safe, capture resolved handles.
|
||||
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
|
||||
when (obj) {
|
||||
is ObjInstance -> {
|
||||
val instScope = obj.instanceScope
|
||||
val idx = instScope.getSlotIndexOf(name)
|
||||
if (idx != null) {
|
||||
val r = instScope.getSlotRecord(idx)
|
||||
if (!r.visibility.isPublic)
|
||||
when (base) {
|
||||
is ObjClass -> {
|
||||
val clsScope = base.classScope
|
||||
val capturedIdx = clsScope?.getSlotIndexOf(name)
|
||||
if (clsScope != null && capturedIdx != null) {
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
|
||||
val scope0 = (obj as ObjClass).classScope!!
|
||||
val r0 = scope0.getSlotRecord(capturedIdx)
|
||||
if (!r0.visibility.isPublic)
|
||||
sc.raiseError(ObjAccessException(sc, "can't access non-public field $name"))
|
||||
r
|
||||
} else obj.readField(sc, name)
|
||||
r0
|
||||
}
|
||||
} else {
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
|
||||
}
|
||||
is ObjClass -> {
|
||||
val clsScope = obj.classScope
|
||||
if (clsScope != null) {
|
||||
val idx = clsScope.getSlotIndexOf(name)
|
||||
if (idx != null) {
|
||||
val r = clsScope.getSlotRecord(idx)
|
||||
if (!r.visibility.isPublic)
|
||||
sc.raiseError(ObjAccessException(sc, "can't access non-public field $name"))
|
||||
r
|
||||
} else obj.readField(sc, name)
|
||||
} else obj.readField(sc, name)
|
||||
}
|
||||
else -> obj.readField(sc, name)
|
||||
}
|
||||
else -> {
|
||||
// For instances and other types, fall back to name-based lookup per access (slot index may differ per instance)
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
|
||||
}
|
||||
}
|
||||
return rec
|
||||
@@ -305,39 +317,38 @@ class FieldRef(
|
||||
}
|
||||
if (net.sergeych.lyng.PerfFlags.FIELD_PIC) {
|
||||
val (key, ver) = receiverKeyAndVersion(base)
|
||||
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) return s(base, scope, newValue) }
|
||||
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) return s(base, scope, newValue) }
|
||||
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetHit++
|
||||
return s(base, scope, newValue)
|
||||
} }
|
||||
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetHit++
|
||||
return s(base, scope, newValue)
|
||||
} }
|
||||
// Slow path
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.fieldPicSetMiss++
|
||||
base.writeField(scope, name, newValue)
|
||||
// Install move-to-front with a handle-aware setter
|
||||
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
|
||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v ->
|
||||
when (obj) {
|
||||
is ObjInstance -> {
|
||||
val instScope = obj.instanceScope
|
||||
val idx = instScope.getSlotIndexOf(name)
|
||||
if (idx != null) {
|
||||
val r = instScope.getSlotRecord(idx)
|
||||
if (!r.visibility.isPublic)
|
||||
sc.raiseError(ObjAccessException(sc, "can't assign to non-public field $name"))
|
||||
if (!r.isMutable)
|
||||
when (base) {
|
||||
is ObjClass -> {
|
||||
val clsScope = base.classScope
|
||||
val capturedIdx = clsScope?.getSlotIndexOf(name)
|
||||
if (clsScope != null && capturedIdx != null) {
|
||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v ->
|
||||
val scope0 = (obj as ObjClass).classScope!!
|
||||
val r0 = scope0.getSlotRecord(capturedIdx)
|
||||
if (!r0.isMutable)
|
||||
sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name"))
|
||||
if (r.value.assign(sc, v) == null) r.value = v
|
||||
} else obj.writeField(sc, name, v)
|
||||
if (r0.value.assign(sc, v) == null) r0.value = v
|
||||
}
|
||||
} else {
|
||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
|
||||
}
|
||||
is ObjClass -> {
|
||||
val clsScope = obj.classScope
|
||||
if (clsScope != null) {
|
||||
val idx = clsScope.getSlotIndexOf(name)
|
||||
if (idx != null) {
|
||||
val r = clsScope.getSlotRecord(idx)
|
||||
if (!r.isMutable)
|
||||
sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name"))
|
||||
r.value = v
|
||||
} else obj.writeField(sc, name, v)
|
||||
} else obj.writeField(sc, name, v)
|
||||
}
|
||||
else -> obj.writeField(sc, name, v)
|
||||
}
|
||||
else -> {
|
||||
// For instances and other types, fall back to generic write (instance slot indices may differ per instance)
|
||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -361,9 +372,9 @@ class IndexRef(
|
||||
private val isOptional: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = target.get(scope).value
|
||||
val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value
|
||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||
val idx = index.get(scope).value
|
||||
val idx = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) index.evalValue(scope) else index.get(scope).value
|
||||
return base.getAt(scope, idx).asMutable
|
||||
}
|
||||
|
||||
@@ -395,10 +406,16 @@ class CallRef(
|
||||
private val isOptionalInvoke: Boolean,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val callee = target.get(scope).value
|
||||
val callee = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) target.evalValue(scope) else target.get(scope).value
|
||||
if (callee == ObjNull && isOptionalInvoke) return ObjNull.asReadonly
|
||||
val callArgs = args.toArguments(scope, tailBlock)
|
||||
val result = callee.callOn(scope.createChildScope(scope.pos, callArgs))
|
||||
val result: Obj = if (net.sergeych.lyng.PerfFlags.SCOPE_POOL) {
|
||||
scope.withChildFrame(callArgs) { child ->
|
||||
callee.callOn(child)
|
||||
}
|
||||
} else {
|
||||
callee.callOn(scope.createChildScope(scope.pos, callArgs))
|
||||
}
|
||||
return result.asReadonly
|
||||
}
|
||||
}
|
||||
@@ -418,38 +435,54 @@ class MethodCallRef(
|
||||
private var mKey2: Long = 0L; private var mVer2: Int = -1; private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null
|
||||
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val base = receiver.get(scope).value
|
||||
val base = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) receiver.evalValue(scope) else receiver.get(scope).value
|
||||
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
||||
val callArgs = args.toArguments(scope, tailBlock)
|
||||
if (net.sergeych.lyng.PerfFlags.METHOD_PIC) {
|
||||
val (key, ver) = receiverKeyAndVersion(base)
|
||||
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) return inv(base, scope, callArgs).asReadonly }
|
||||
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) return inv(base, scope, callArgs).asReadonly }
|
||||
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicHit++
|
||||
return inv(base, scope, callArgs).asReadonly
|
||||
} }
|
||||
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicHit++
|
||||
return inv(base, scope, callArgs).asReadonly
|
||||
} }
|
||||
// Slow path
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.methodPicMiss++
|
||||
val result = base.invokeInstanceMethod(scope, name, callArgs)
|
||||
// Install move-to-front with a handle-aware invoker
|
||||
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||
when (obj) {
|
||||
is ObjInstance -> {
|
||||
val instScope = obj.instanceScope
|
||||
val rec = instScope.get(name)
|
||||
if (rec != null) {
|
||||
if (!rec.visibility.isPublic)
|
||||
when (base) {
|
||||
is ObjInstance -> {
|
||||
// Prefer resolved class member to avoid per-call lookup on hit
|
||||
val member = base.objClass.getInstanceMemberOrNull(name)
|
||||
if (member != null) {
|
||||
val visibility = member.visibility
|
||||
val callable = member.value
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||
val inst = obj as ObjInstance
|
||||
if (!visibility.isPublic)
|
||||
sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name"))
|
||||
rec.value.invoke(instScope, obj, a)
|
||||
} else obj.invokeInstanceMethod(sc, name, a)
|
||||
callable.invoke(inst.instanceScope, inst, a)
|
||||
}
|
||||
} else {
|
||||
// Fallback to name-based lookup per call (uncommon)
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||
}
|
||||
is ObjClass -> {
|
||||
val clsScope = obj.classScope
|
||||
if (clsScope != null) {
|
||||
val rec = clsScope.get(name)
|
||||
if (rec != null) {
|
||||
rec.value.invoke(sc, obj, a)
|
||||
} else obj.invokeInstanceMethod(sc, name, a)
|
||||
} else obj.invokeInstanceMethod(sc, name, a)
|
||||
}
|
||||
is ObjClass -> {
|
||||
val clsScope = base.classScope
|
||||
val rec = clsScope?.get(name)
|
||||
if (rec != null) {
|
||||
val callable = rec.value
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) }
|
||||
} else {
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||
}
|
||||
else -> obj.invokeInstanceMethod(sc, name, a)
|
||||
}
|
||||
else -> {
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||
}
|
||||
}
|
||||
return result.asReadonly
|
||||
@@ -486,11 +519,22 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it) }
|
||||
scope.getSlotIndexOf(name)?.let {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicHit++
|
||||
return scope.getSlotRecord(it)
|
||||
}
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
||||
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||
}
|
||||
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) return scope.getSlotRecord(slot)
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) {
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (hit) net.sergeych.lyng.PerfStats.localVarPicHit++ else net.sergeych.lyng.PerfStats.localVarPicMiss++
|
||||
}
|
||||
return scope.getSlotRecord(slot)
|
||||
}
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
||||
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
||||
}
|
||||
|
||||
@@ -584,10 +628,13 @@ class FastLocalVarRef(
|
||||
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null
|
||||
val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
val ownerValid = isOwnerValidFor(scope)
|
||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
|
||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++
|
||||
}
|
||||
return actualOwner.getSlotRecord(slot)
|
||||
}
|
||||
|
||||
@@ -609,10 +656,11 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
||||
for (e in entries) {
|
||||
when (e) {
|
||||
is ListEntry.Element -> {
|
||||
list += e.ref.get(scope).value
|
||||
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
||||
list += v
|
||||
}
|
||||
is ListEntry.Spread -> {
|
||||
val elements = e.ref.get(scope).value
|
||||
val elements = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
||||
when (elements) {
|
||||
is ObjList -> list.addAll(elements.list)
|
||||
else -> scope.raiseError("Spread element must be list")
|
||||
@@ -633,8 +681,8 @@ class RangeRef(
|
||||
private val isEndInclusive: Boolean
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val l = left?.get(scope)?.value ?: ObjNull
|
||||
val r = right?.get(scope)?.value ?: ObjNull
|
||||
val l = left?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
||||
val r = right?.let { if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) it.evalValue(scope) else it.get(scope).value } ?: ObjNull
|
||||
return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly
|
||||
}
|
||||
}
|
||||
@@ -646,7 +694,7 @@ class AssignRef(
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val v = value.get(scope).value
|
||||
val v = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
|
||||
val rec = target.get(scope)
|
||||
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
|
||||
if (rec.value.assign(scope, v) == null) {
|
||||
|
||||
Reference in new issue
Block a user