Compare commits

...

38 Commits

Author SHA1 Message Date
db4f7d0973 Finalize bytecode-only lambdas and frame binding 2026-02-12 17:47:05 +03:00
a481371349 Step 27E: add CLI bytecode fallback reporting 2026-02-12 00:15:35 +03:00
51319fa8b7 Step 27D: expose lambda fallback reporter 2026-02-12 00:03:48 +03:00
86ac7eee54 Step 27C: report lambda bytecode fallbacks 2026-02-12 00:02:09 +03:00
18278794d6 Step 27B: bytecode ::class 2026-02-11 23:10:32 +03:00
ac680ceb6c Step 27A: remove EVAL_STMT 2026-02-11 21:15:43 +03:00
6efdfc1964 Step 26C: remove forceScopeSlots 2026-02-11 21:02:48 +03:00
b9af80a1b2 Step 26B: drop containsValueFnRef 2026-02-11 20:53:15 +03:00
bde32ca7b5 Step 26A: bytecode lambda callables 2026-02-11 20:50:56 +03:00
99ca15d20f Step 24E: frame capture refs 2026-02-11 20:33:58 +03:00
c14c7d43d9 Step 24D: bytecode closure scope 2026-02-11 11:36:19 +03:00
1271f347bd Step 25D: module decl disasm coverage 2026-02-10 09:31:21 +03:00
4e08339756 Step 25C: bytecode class declarations 2026-02-10 08:30:10 +03:00
b32a937636 Step 25B: bytecode function declarations 2026-02-10 08:13:58 +03:00
4db0a7fbab Step 25A: bytecode enum declarations 2026-02-10 08:04:28 +03:00
8b196c7b0c Step 24E: isolate interpreter-only capture logic 2026-02-10 07:58:29 +03:00
54ca886753 Step 24D: update plan 2026-02-10 07:56:24 +03:00
abbebec153 Step 24D: bytecode-only closure path for lambdas 2026-02-10 07:56:08 +03:00
c066dc7150 Step 24C: remove scope mirroring in bytecode runtime 2026-02-10 04:46:42 +03:00
6c0b86f6e6 Step 24B: frame-slot captures for bytecode lambdas 2026-02-10 04:33:04 +03:00
8f1c660f4e Step 24A: bytecode capture tables for lambdas 2026-02-10 04:19:52 +03:00
3ce9029162 Mark step 25 complete 2026-02-09 22:01:02 +03:00
0caa9849cf Step 25: emit DECL_EXEC for declarations 2026-02-09 22:00:46 +03:00
8314127fdb Update bytecode migration plan for interpreter removal 2026-02-09 20:44:45 +03:00
dab0b9f165 Step 24: remove assign scope slot 2026-02-09 20:42:02 +03:00
6aa23e8ef3 Step 23: frame-based delegated locals 2026-02-09 19:57:59 +03:00
c035b4c34c Step 22: delegated locals in bytecode 2026-02-09 12:34:23 +03:00
9d508e219f Step 21: union mismatch bytecode throw 2026-02-09 12:13:33 +03:00
b49f291bff Step 20: bytecode NopStatement 2026-02-09 12:04:51 +03:00
ae88898f58 Step 16: remove ToBoolStatement 2026-02-09 11:42:51 +03:00
565dbf98ed Step 19: union member access 2026-02-09 11:38:04 +03:00
5305ced89f Step 18: delegated member access in bytecode 2026-02-09 11:00:29 +03:00
b2d5897aa8 Step 17: callable property calls 2026-02-09 10:50:53 +03:00
0b94b46d40 Step 15: class-scope ?= in bytecode 2026-02-09 10:34:36 +03:00
541738646f Enable fast local refs behind compiler flag 2026-02-09 10:28:32 +03:00
f0dc0d2396 Support qualified this refs in bytecode 2026-02-09 10:16:04 +03:00
7850d5fbde Cover optional index pre-inc short-circuit 2026-02-09 10:07:32 +03:00
cde7cf2caf Add optional inc/dec bytecode tests 2026-02-09 10:06:39 +03:00
32 changed files with 3158 additions and 1022 deletions

View File

@ -43,6 +43,98 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
- [x] Fix post-inc return value for object slots stored in scope frames. - [x] Fix post-inc return value for object slots stored in scope frames.
- [x] Handle optional receivers for member assign-ops and inc/dec without evaluating operands on null. - [x] Handle optional receivers for member assign-ops and inc/dec without evaluating operands on null.
- [x] Support class-scope and index optional inc/dec paths in bytecode. - [x] Support class-scope and index optional inc/dec paths in bytecode.
- [x] Step 13: Qualified `this` value refs in bytecode.
- [x] Compile `QualifiedThisRef` (`this@Type`) via `LOAD_THIS_VARIANT`.
- [x] Add a JVM test that evaluates `this@Type` as a value inside nested classes.
- [x] Step 14: Fast local ref reads in bytecode.
- [x] Support `FastLocalVarRef` reads with the same slot resolution as `LocalVarRef`.
- [x] If `BoundLocalVarRef` is still emitted, map it to a direct slot read instead of failing.
- [x] Add a JVM test that exercises fast-local reads in a bytecode-compiled function.
- [x] Step 15: Class-scope `?=` in bytecode.
- [x] Handle `C.x ?= v` and `C?.x ?= v` for class-scope members without falling back.
- [x] Add a JVM test for class-scope `?=` on static vars.
- [x] Step 16: Remove dead `ToBoolStatement`.
- [x] Confirm no parser/compiler paths construct `ToBoolStatement` and delete it plus interpreter hooks.
- [x] Keep JVM tests green after removal.
- [x] Step 17: Callable property calls in bytecode.
- [x] Support `CallRef` where the target is a `FieldRef` (e.g., `(obj.fn)()`), keeping compile-time resolution.
- [x] Add a JVM test for a callable property call compiled to bytecode.
- [x] Step 18: Delegated member access in bytecode.
- [x] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly.
- [x] Add JVM coverage for delegated member get/set/call in bytecode.
- [x] Step 19: Unknown receiver member access in bytecode.
- [x] Reject Object/unknown receiver member calls without explicit cast or Dynamic.
- [x] Add union-member dispatch with ordered type checks and runtime mismatch error.
- [x] Add JVM tests for unknown receiver and union member access.
- [x] Step 20: Bytecode support for `NopStatement`.
- [x] Allow `NopStatement` in `containsUnsupportedForBytecode`.
- [x] Emit `ObjVoid` directly in bytecode for `NopStatement` in statement/value contexts.
- [x] Add a JVM test that exercises a code path returning `NopStatement` in bytecode (e.g., static class member decl in class body).
- [x] Step 21: Union mismatch path in bytecode.
- [x] Replace `UnionTypeMismatchStatement` branch with a bytecode-compilable throw path (no custom `StatementRef` that blocks bytecode).
- [x] Add a JVM test that forces the union mismatch at runtime and asserts the error message.
- [x] Step 22: Delegated local slots in bytecode.
- [x] Support reads/writes/assign-ops/inc/dec for delegated locals (`LocalSlotRef.isDelegated`) in `BytecodeCompiler`.
- [x] Remove `containsDelegatedRefs` guard once delegated locals are bytecode-safe.
- [x] Add JVM tests that use delegated locals inside bytecode-compiled functions.
- [x] Step 23: Refactor delegated locals to keep delegate objects in frame slots.
- [x] Add bytecode ops to bind/get/set delegated locals without scope storage.
- [x] Store delegated locals in frame slots and compile get/set/assign ops with new ops.
- [x] Preserve reflection facade by syncing delegated locals into scope only when needed.
- [x] Step 24: Remove `ASSIGN_SCOPE_SLOT` now that delegated locals are always frame-backed.
- [x] Force delegated locals into local slots (even module) and avoid scope-slot resolution.
- [x] Drop opcode/runtime support for `ASSIGN_SCOPE_SLOT`.
## Frame-Only Execution (new, before interpreter removal)
- [x] Step 24A: Bytecode capture tables for lambdas (compiler only).
- [x] Emit per-lambda capture tables containing (ownerFrameKind, ownerSlotId).
- [x] Create captures only when detected; do not allocate scope slots.
- [x] Add disassembler output for capture tables.
- [x] JVM tests must be green before commit.
- [x] Step 24B: Frame-slot captures in bytecode runtime.
- [x] Build lambdas from bytecode + capture table (no capture names).
- [x] Read captured values via `FrameSlotRef` only.
- [x] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter.
- [x] JVM tests must be green before commit.
- [x] Step 24C: Remove scope local mirroring in bytecode execution.
- [x] Remove/disable any bytecode runtime code that writes locals into Scope for execution.
- [x] Keep Scope creation only for reflection/Kotlin interop paths.
- [x] JVM tests must be green before commit.
- [x] Step 24D: Eliminate `ClosureScope` usage on bytecode execution paths.
- [x] Avoid `ClosureScope` in bytecode-related call paths (Block/Lambda/ObjDynamic/ObjProperty).
- [x] Keep interpreter path using `ClosureScope` until interpreter removal.
- [x] JVM tests must be green before commit.
- [x] Step 24E: Isolate interpreter-only capture logic.
- [x] Mark `resolveCaptureRecord` paths as interpreter-only.
- [x] Guard or delete any bytecode path that tries to sync captures into scopes.
- [x] JVM tests must be green before commit.
## Interpreter Removal (next)
- [ ] Step 25: Replace Statement-based declaration calls in bytecode.
- [x] Add bytecode const/op for enum declarations (no `Statement` objects in constants).
- [x] Add bytecode const/op for class declarations (no `Statement` objects in constants).
- [x] Add bytecode const/op for function declarations (no `Statement` objects in constants).
- [x] Replace `emitStatementCall` usage for `EnumDeclStatement`.
- [x] Replace `emitStatementCall` usage for `ClassDeclStatement`.
- [x] Replace `emitStatementCall` usage for `FunctionDeclStatement`.
- [x] Add JVM disasm coverage to ensure module init has no `CALL_SLOT` to `Callable@...` for declarations.
- [ ] Step 26: Bytecode-backed lambdas (remove `ValueFnRef` runtime execution).
- [x] Compile lambda bodies to bytecode and emit an opcode to create a callable from bytecode + capture plan.
- [x] Remove `containsValueFnRef` helper now that lambdas are bytecode-backed.
- [x] Remove `forceScopeSlots` branches once no bytecode paths depend on scope slots.
- [x] Add JVM tests for captured locals and delegated locals inside lambdas on the bytecode path.
- [x] Step 27: Remove interpreter opcodes and constants from bytecode runtime.
- [x] Delete `BytecodeConst.ValueFn`, `CmdMakeValueFn`, and `MAKE_VALUE_FN`.
- [x] Delete `BytecodeConst.StatementVal`, `CmdEvalStmt`, and `EVAL_STMT`.
- [x] Add bytecode-backed `::class` via `ClassOperatorRef` + `GET_OBJ_CLASS` to avoid ValueFn for class operator.
- [x] Add a bytecode fallback reporter hook for lambdas to locate remaining non-bytecode cases.
- [x] Remove `emitStatementCall`/`emitStatementEval` once unused.
- [ ] Step 28: Scope as facade only.
- [x] Audit bytecode execution paths for `Statement.execute` usage and remove remaining calls.
- [x] Keep scope sync only for reflection/Kotlin interop, not for execution.
- [x] Replace bytecode entry seeding from Scope with frame-only arg/local binding.
## Notes ## Notes

View File

@ -27,7 +27,9 @@ import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.LyngVersion import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
@ -156,6 +158,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
val version by option("-v", "--version", help = "Print version and exit").flag() val version by option("-v", "--version", help = "Print version and exit").flag()
val benchmark by option("--benchmark", help = "Run JVM microbenchmarks and exit").flag() val benchmark by option("--benchmark", help = "Run JVM microbenchmarks and exit").flag()
val bytecodeFallbacks by option("--bytecode-fallbacks", help = "Report lambdas that fall back to interpreter").flag()
val script by argument(help = "one or more scripts to execute").optional() val script by argument(help = "one or more scripts to execute").optional()
val execute: String? by option( val execute: String? by option(
"-x", "--execute", help = """ "-x", "--execute", help = """
@ -198,7 +201,15 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
launcher { launcher {
// there is no script name, it is a first argument instead: // there is no script name, it is a first argument instead:
processErrors { processErrors {
baseScope.eval(execute!!) val reporter = bytecodeFallbackReporter(bytecodeFallbacks)
val script = Compiler.compileWithResolution(
Source("<eval>", execute!!),
baseScope.currentImportProvider,
seedScope = baseScope,
bytecodeFallbackReporter = reporter
)
script.execute(baseScope)
flushBytecodeFallbacks(reporter)
} }
} }
} }
@ -209,7 +220,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
echoFormattedHelp() echoFormattedHelp()
} else { } else {
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList())) baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
launcher { executeFile(script!!) } launcher { executeFile(script!!, bytecodeFallbacks) }
} }
} }
} }
@ -217,14 +228,14 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
} }
} }
fun executeFileWithArgs(fileName: String, args: List<String>) { fun executeFileWithArgs(fileName: String, args: List<String>, reportBytecodeFallbacks: Boolean = false) {
runBlocking { runBlocking {
baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList())) baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
executeFile(fileName) executeFile(fileName, reportBytecodeFallbacks)
} }
} }
suspend fun executeFile(fileName: String) { suspend fun executeFile(fileName: String, reportBytecodeFallbacks: Boolean = false) {
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource -> var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
fileSource.buffer().use { bs -> fileSource.buffer().use { bs ->
bs.readUtf8() bs.readUtf8()
@ -236,7 +247,16 @@ suspend fun executeFile(fileName: String) {
text = text.substring(pos + 1) text = text.substring(pos + 1)
} }
processErrors { processErrors {
baseScopeDefer.await().eval(Source(fileName, text)) val scope = baseScopeDefer.await()
val reporter = bytecodeFallbackReporter(reportBytecodeFallbacks)
val script = Compiler.compileWithResolution(
Source(fileName, text),
scope.currentImportProvider,
seedScope = scope,
bytecodeFallbackReporter = reporter
)
script.execute(scope)
flushBytecodeFallbacks(reporter)
} }
} }
@ -248,3 +268,22 @@ suspend fun processErrors(block: suspend () -> Unit) {
println("\nError executing the script:\n$e\n") println("\nError executing the script:\n$e\n")
} }
} }
private fun bytecodeFallbackReporter(enabled: Boolean): ((Pos, String) -> Unit)? {
if (!enabled) return null
val reports = ArrayList<String>()
val reporter: (Pos, String) -> Unit = { pos, msg ->
reports.add("$pos: $msg")
}
return object : (Pos, String) -> Unit by reporter {
override fun invoke(pos: Pos, msg: String) = reporter(pos, msg)
override fun toString(): String = reports.joinToString("\n")
}
}
private fun flushBytecodeFallbacks(reporter: ((Pos, String) -> Unit)?) {
val text = reporter?.toString().orEmpty()
if (text.isBlank()) return
println("Bytecode lambda fallbacks:")
println(text)
}

View File

@ -250,6 +250,212 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
} }
} }
/**
* Assign arguments directly into frame slots using [paramSlotPlan] without creating scope locals.
* Still allows default expressions to evaluate by exposing FrameSlotRef facades in [scope].
*/
suspend fun assignToFrame(
scope: Scope,
arguments: Arguments = scope.args,
paramSlotPlan: Map<String, Int>,
frame: FrameAccess,
slotOffset: Int = 0,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public,
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
) {
fun slotFor(name: String): Int {
val full = paramSlotPlan[name] ?: scope.raiseIllegalState("parameter slot for '$name' is missing")
val slot = full - slotOffset
if (slot < 0) scope.raiseIllegalState("parameter slot for '$name' is out of range")
return slot
}
fun ensureScopeRef(a: Item, slot: Int, recordType: ObjRecord.Type) {
if (scope.getLocalRecordDirect(a.name) != null) return
scope.addItem(
a.name,
(a.accessType ?: defaultAccessType).isMutable,
FrameSlotRef(frame, slot),
a.visibility ?: defaultVisibility,
recordType = recordType,
declaringClass = declaringClass,
isTransient = a.isTransient
)
}
fun setFrameValue(slot: Int, value: Obj) {
when (value) {
is net.sergeych.lyng.obj.ObjInt -> frame.setInt(slot, value.value)
is net.sergeych.lyng.obj.ObjReal -> frame.setReal(slot, value.value)
is net.sergeych.lyng.obj.ObjBool -> frame.setBool(slot, value.value)
else -> frame.setObj(slot, value)
}
}
fun assign(a: Item, value: Obj) {
val recordType = if (declaringClass != null && a.accessType != null) {
ObjRecord.Type.ConstructorField
} else {
ObjRecord.Type.Argument
}
val slot = slotFor(a.name)
setFrameValue(slot, value.byValueCopy())
ensureScopeRef(a, slot, recordType)
}
suspend fun missingValue(a: Item, error: String): Obj {
return a.defaultValue?.execute(scope)
?: if (a.type.isNullable) ObjNull else scope.raiseIllegalArgument(error)
}
// Fast path for simple positional-only calls with no ellipsis and no defaults
if (arguments.named.isEmpty() && !arguments.tailBlockMode) {
var hasComplex = false
for (p in params) {
if (p.isEllipsis || p.defaultValue != null) {
hasComplex = true
break
}
}
if (!hasComplex) {
if (arguments.list.size > params.size)
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
if (arguments.list.size < params.size) {
for (i in arguments.list.size until params.size) {
val a = params[i]
if (!a.type.isNullable) {
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
}
}
}
for (i in params.indices) {
val a = params[i]
val value = if (i < arguments.list.size) arguments.list[i] else ObjNull
assign(a, value)
}
return
}
}
// Prepare positional args and parameter count, handle tail-block binding
val callArgs: List<Obj>
val paramsSize: Int
if (arguments.tailBlockMode) {
val lastParam = params.last()
if (arguments.named.containsKey(lastParam.name))
scope.raiseIllegalArgument("trailing block cannot be used when the last parameter is already assigned by a named argument")
paramsSize = params.size - 1
assign(lastParam, arguments.list.last())
callArgs = arguments.list.dropLast(1)
} else {
paramsSize = params.size
callArgs = arguments.list
}
val coveredByPositional = BooleanArray(paramsSize)
run {
var headRequired = 0
var tailRequired = 0
val ellipsisIdx = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIdx >= 0) {
for (i in 0 until ellipsisIdx) if (!params[i].isEllipsis && params[i].defaultValue == null) headRequired++
for (i in paramsSize - 1 downTo ellipsisIdx + 1) if (params[i].defaultValue == null) tailRequired++
} else {
for (i in 0 until paramsSize) if (params[i].defaultValue == null) headRequired++
}
val P = callArgs.size
if (ellipsisIdx < 0) {
val k = minOf(P, paramsSize)
for (i in 0 until k) coveredByPositional[i] = true
} else {
val headTake = minOf(P, headRequired)
for (i in 0 until headTake) coveredByPositional[i] = true
val remaining = P - headTake
val tailTake = minOf(remaining, tailRequired)
var j = paramsSize - 1
var taken = 0
while (j > ellipsisIdx && taken < tailTake) {
coveredByPositional[j] = true
j--
taken++
}
}
}
val assignedByName = BooleanArray(paramsSize)
val namedValues = arrayOfNulls<Obj>(paramsSize)
if (arguments.named.isNotEmpty()) {
for ((k, v) in arguments.named) {
val idx = params.subList(0, paramsSize).indexOfFirst { it.name == k }
if (idx < 0) scope.raiseIllegalArgument("unknown parameter '$k'")
if (params[idx].isEllipsis) scope.raiseIllegalArgument("ellipsis (variadic) parameter cannot be assigned by name: '$k'")
if (coveredByPositional[idx]) scope.raiseIllegalArgument("argument '$k' is already set by positional argument")
if (assignedByName[idx]) scope.raiseIllegalArgument("argument '$k' is already set")
assignedByName[idx] = true
namedValues[idx] = v
}
}
suspend fun processHead(index: Int, headPos: Int): Pair<Int, Int> {
var i = index
var hp = headPos
while (i < paramsSize) {
val a = params[i]
if (a.isEllipsis) break
if (assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (hp < callArgs.size) callArgs[hp++]
else missingValue(a, "too few arguments for the call (missing ${a.name})")
assign(a, value)
}
i++
}
return i to hp
}
suspend fun processTail(startExclusive: Int, tailStart: Int, headPosBound: Int): Int {
var i = paramsSize - 1
var tp = tailStart
while (i > startExclusive) {
val a = params[i]
if (a.isEllipsis) break
if (i < assignedByName.size && assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (tp >= headPosBound) callArgs[tp--]
else missingValue(a, "too few arguments for the call")
assign(a, value)
}
i--
}
return tp
}
fun processEllipsis(index: Int, headPos: Int, tailPos: Int) {
val a = params[index]
val from = headPos
val to = tailPos
val l = if (from > to) ObjList()
else ObjList(callArgs.subList(from, to + 1).toMutableList())
assign(a, l)
}
val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIndex >= 0) {
val (_, headConsumedTo) = processHead(0, 0)
val tailConsumedFrom = processTail(ellipsisIndex, callArgs.size - 1, headConsumedTo)
processEllipsis(ellipsisIndex, headConsumedTo, tailConsumedFrom)
} else {
val (_, headConsumedTo) = processHead(0, 0)
if (headConsumedTo != callArgs.size)
scope.raiseIllegalArgument("too many arguments for the call")
}
}
/** /**
* Single argument declaration descriptor. * Single argument declaration descriptor.
* *

View File

@ -31,22 +31,33 @@ class BlockStatement(
val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos) val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos)
if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan) if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan)
if (captureSlots.isNotEmpty()) { if (captureSlots.isNotEmpty()) {
val applyScope = scope as? ApplyScope val captureRecords = scope.captureRecords
for (capture in captureSlots) { if (captureRecords != null) {
val rec = if (applyScope != null) { for (i in captureSlots.indices) {
applyScope.resolveCaptureRecord(capture.name) val capture = captureSlots[i]
?: applyScope.callScope.resolveCaptureRecord(capture.name) val rec = captureRecords.getOrNull(i)
} else { ?: scope.raiseSymbolNotFound("capture ${capture.name} not found")
scope.resolveCaptureRecord(capture.name) target.updateSlotFor(capture.name, rec)
} }
if (rec == null) { } else {
if (scope.getSlotIndexOf(capture.name) == null && scope.getLocalRecordDirect(capture.name) == null) { val applyScope = scope as? ApplyScope
continue for (capture in captureSlots) {
// Interpreter-only capture resolution; bytecode paths must use captureRecords instead.
val rec = if (applyScope != null) {
applyScope.resolveCaptureRecord(capture.name)
?: applyScope.callScope.resolveCaptureRecord(capture.name)
} else {
scope.resolveCaptureRecord(capture.name)
} }
(applyScope?.callScope ?: scope) if (rec == null) {
.raiseSymbolNotFound("symbol ${capture.name} not found") if (scope.getSlotIndexOf(capture.name) == null && scope.getLocalRecordDirect(capture.name) == null) {
continue
}
(applyScope?.callScope ?: scope)
.raiseSymbolNotFound("symbol ${capture.name} not found")
}
target.updateSlotFor(capture.name, rec)
} }
target.updateSlotFor(capture.name, rec)
} }
} }
return block.execute(target) return block.execute(target)

View File

@ -0,0 +1,19 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng
interface BytecodeCallable

View File

@ -17,20 +17,147 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjException
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjInstanceClass
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjRecord
data class ClassDeclBaseSpec(
val name: String,
val args: List<ParsedArgument>?
)
data class ClassDeclSpec(
val declaredName: String?,
val className: String,
val typeName: String,
val startPos: Pos,
val isExtern: Boolean,
val isAbstract: Boolean,
val isObject: Boolean,
val isAnonymous: Boolean,
val baseSpecs: List<ClassDeclBaseSpec>,
val constructorArgs: ArgsDeclaration?,
val constructorFieldIds: Map<String, Int>?,
val bodyInit: Statement?,
val initScope: List<Statement>,
)
internal suspend fun executeClassDecl(scope: Scope, spec: ClassDeclSpec): Obj {
if (spec.isObject) {
val parentClasses = spec.baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name] ?: throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}")
(rec.value as? ObjClass) ?: throw ScriptError(spec.startPos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray())
newClass.isAnonymous = spec.isAnonymous
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
}
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
classScope.addConst("object", newClass)
spec.bodyInit?.execute(classScope)
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
if (spec.declaredName != null) {
scope.addItem(spec.declaredName, false, instance)
}
return instance
}
if (spec.isExtern) {
val rec = scope[spec.className]
val existing = rec?.value as? ObjClass
val resolved = if (existing != null) {
existing
} else if (spec.className.contains('.')) {
scope.resolveQualifiedIdentifier(spec.className) as? ObjClass
} else {
null
}
val stub = resolved ?: ObjInstanceClass(spec.className).apply { this.isAbstract = true }
spec.declaredName?.let { scope.addItem(it, false, stub) }
return stub
}
val parentClasses = spec.baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name]
val cls = rec?.value as? ObjClass
if (cls != null) return@map cls
if (baseSpec.name == "Exception") return@map ObjException.Root
if (rec == null) throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}")
throw ScriptError(spec.startPos, "${baseSpec.name} is not a class")
}
val constructorCode = object : Statement() {
override val pos: Pos = spec.startPos
override suspend fun execute(scope: Scope): Obj {
val instance = scope.thisObj as ObjInstance
return instance
}
}
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
it.isAbstract = spec.isAbstract
it.instanceConstructor = constructorCode
it.constructorMeta = spec.constructorArgs
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
spec.constructorArgs?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name,
ObjNull,
isMutable = p.accessType == AccessType.Var,
visibility = p.visibility ?: Visibility.Public,
declaringClass = it,
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
fieldId = spec.constructorFieldIds?.get(p.name)
)
}
}
}
spec.declaredName?.let { scope.addItem(it, false, newClass) }
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
spec.bodyInit?.execute(classScope)
if (spec.initScope.isNotEmpty()) {
for (s in spec.initScope) {
s.execute(classScope)
}
}
newClass.checkAbstractSatisfaction(spec.startPos)
return newClass
}
class ClassDeclStatement( class ClassDeclStatement(
private val delegate: Statement, val spec: ClassDeclSpec,
private val startPos: Pos,
val declaredName: String?,
) : Statement() { ) : Statement() {
override val pos: Pos = startPos override val pos: Pos = spec.startPos
val declaredName: String? get() = spec.declaredName
val typeName: String get() = spec.typeName
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope) return executeClassDecl(scope, spec)
} }
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
val target = scope.parent ?: scope val target = scope.parent ?: scope
return delegate.execute(target) return executeClassDecl(target, spec)
} }
} }

View File

@ -84,6 +84,32 @@ class ClosureScope(
} }
} }
/**
* Bytecode-oriented closure scope that keeps the call scope parent chain for stack traces
* while carrying the lexical closure for `this` variants and module resolution.
* Unlike [ClosureScope], it does not override name lookup.
*/
class BytecodeClosureScope(
val callScope: Scope,
val closureScope: Scope,
private val preferredThisType: String? = null
) :
Scope(callScope, callScope.args, thisObj = closureScope.thisObj) {
init {
val desired = preferredThisType?.let { typeName ->
callScope.thisVariants.firstOrNull { it.objClass.className == typeName }
}
val primaryThis = closureScope.thisObj
val merged = ArrayList<Obj>(callScope.thisVariants.size + closureScope.thisVariants.size + 1)
desired?.let { merged.add(it) }
merged.addAll(callScope.thisVariants)
merged.addAll(closureScope.thisVariants)
setThisVariants(primaryThis, merged)
this.currentClassCtx = closureScope.currentClassCtx ?: callScope.currentClassCtx
}
}
class ApplyScope(val callScope: Scope, val applied: Scope) : class ApplyScope(val callScope: Scope, val applied: Scope) :
Scope(callScope, thisObj = applied.thisObj) { Scope(callScope, thisObj = applied.thisObj) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
interface DeclExecutable {
suspend fun execute(scope: Scope): Obj
}
class StatementDeclExecutable(private val delegate: Statement) : DeclExecutable {
override suspend fun execute(scope: Scope): Obj = delegate.execute(scope)
}

View File

@ -28,6 +28,8 @@ class DelegatedVarDeclStatement(
val visibility: Visibility, val visibility: Visibility,
val initializer: Statement, val initializer: Statement,
val isTransient: Boolean, val isTransient: Boolean,
val slotIndex: Int?,
val scopeId: Int?,
private val startPos: Pos, private val startPos: Pos,
) : Statement() { ) : Statement() {
override val pos: Pos = startPos override val pos: Pos = startPos

View File

@ -17,19 +17,34 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjEnumClass
import net.sergeych.lyng.obj.ObjRecord
class EnumDeclStatement( class EnumDeclStatement(
private val delegate: Statement, val declaredName: String,
val qualifiedName: String,
val entries: List<String>,
val lifted: Boolean,
private val startPos: Pos, private val startPos: Pos,
) : Statement() { ) : Statement() {
override val pos: Pos = startPos override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope) val enumClass = ObjEnumClass.createSimpleEnum(qualifiedName, entries)
scope.addItem(declaredName, false, enumClass, recordType = ObjRecord.Type.Enum)
if (lifted) {
for (entry in entries) {
val rec = enumClass.getInstanceMemberOrNull(entry, includeAbstract = false, includeStatic = true)
if (rec != null) {
scope.addItem(entry, false, rec.value)
}
}
}
return enumClass
} }
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
val target = scope.parent ?: scope val target = scope.parent ?: scope
return delegate.execute(target) return execute(target)
} }
} }

View File

@ -55,3 +55,16 @@ class FrameSlotRef(
} }
} }
} }
class RecordSlotRef(
private val record: ObjRecord,
) : net.sergeych.lyng.obj.Obj() {
fun read(): Obj {
val direct = record.value
return if (direct is FrameSlotRef) direct.read() else direct
}
fun write(value: Obj) {
record.value = value
}
}

View File

@ -17,19 +17,247 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjExtensionMethodCallable
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjUnset
import net.sergeych.lyng.obj.ObjVoid
class FunctionClosureBox(
var closure: Scope? = null,
var captureContext: Scope? = null,
)
data class FunctionDeclSpec(
val name: String,
val visibility: Visibility,
val isAbstract: Boolean,
val isClosed: Boolean,
val isOverride: Boolean,
val isStatic: Boolean,
val isTransient: Boolean,
val isDelegated: Boolean,
val delegateExpression: Statement?,
val extTypeName: String?,
val extensionWrapperName: String?,
val memberMethodId: Int?,
val actualExtern: Boolean,
val parentIsClassBody: Boolean,
val externCallSignature: CallSignature?,
val annotation: (suspend (Scope, ObjString, Statement) -> Statement)?,
val fnBody: Statement,
val closureBox: FunctionClosureBox,
val captureSlots: List<CaptureSlot>,
val startPos: Pos,
)
internal suspend fun executeFunctionDecl(scope: Scope, spec: FunctionDeclSpec): Obj {
if (spec.actualExtern && spec.extTypeName == null && !spec.parentIsClassBody) {
val existing = scope.get(spec.name)
if (existing != null) {
scope.addItem(
spec.name,
false,
existing.value,
spec.visibility,
callSignature = existing.callSignature
)
return existing.value
}
}
if (spec.isDelegated) {
val delegateExpr = spec.delegateExpression ?: scope.raiseError("delegated function missing delegate")
val accessType = ObjString("Callable")
val initValue = delegateExpr.execute(scope)
val finalDelegate = try {
initValue.invokeInstanceMethod(scope, "bind", Arguments(ObjString(spec.name), accessType, scope.thisObj))
} catch (e: Exception) {
initValue
}
if (spec.extTypeName != null) {
val type = scope[spec.extTypeName]?.value ?: scope.raiseSymbolNotFound("class ${spec.extTypeName} not found")
if (type !is ObjClass) scope.raiseClassCastError("${spec.extTypeName} is not the class instance")
scope.addExtension(
type,
spec.name,
ObjRecord(ObjUnset, isMutable = false, visibility = spec.visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply {
delegate = finalDelegate
}
)
return ObjVoid
}
val th = scope.thisObj
if (spec.isStatic) {
(th as ObjClass).createClassField(
spec.name,
ObjUnset,
false,
spec.visibility,
null,
spec.startPos,
isTransient = spec.isTransient,
type = ObjRecord.Type.Delegated
).apply {
delegate = finalDelegate
}
scope.addItem(
spec.name,
false,
ObjUnset,
spec.visibility,
recordType = ObjRecord.Type.Delegated,
isTransient = spec.isTransient
).apply {
delegate = finalDelegate
}
} else if (th is ObjClass) {
val cls: ObjClass = th
val storageName = "${cls.className}::${spec.name}"
cls.createField(
spec.name,
ObjUnset,
false,
spec.visibility,
null,
spec.startPos,
declaringClass = cls,
isAbstract = spec.isAbstract,
isClosed = spec.isClosed,
isOverride = spec.isOverride,
isTransient = spec.isTransient,
type = ObjRecord.Type.Delegated,
methodId = spec.memberMethodId
)
cls.instanceInitializers += object : Statement() {
override val pos: Pos = spec.startPos
override suspend fun execute(scp: Scope): Obj {
val accessType2 = ObjString("Callable")
val initValue2 = delegateExpr.execute(scp)
val finalDelegate2 = try {
initValue2.invokeInstanceMethod(scp, "bind", Arguments(ObjString(spec.name), accessType2, scp.thisObj))
} catch (e: Exception) {
initValue2
}
scp.addItem(
storageName,
false,
ObjUnset,
spec.visibility,
null,
recordType = ObjRecord.Type.Delegated,
isAbstract = spec.isAbstract,
isClosed = spec.isClosed,
isOverride = spec.isOverride,
isTransient = spec.isTransient
).apply {
delegate = finalDelegate2
}
return ObjVoid
}
}
} else {
scope.addItem(
spec.name,
false,
ObjUnset,
spec.visibility,
recordType = ObjRecord.Type.Delegated,
isTransient = spec.isTransient
).apply {
delegate = finalDelegate
}
}
return ObjVoid
}
if (spec.isStatic || !spec.parentIsClassBody) {
spec.closureBox.closure = scope
}
if (spec.parentIsClassBody && spec.captureSlots.isNotEmpty()) {
spec.closureBox.captureContext = scope
}
val annotatedFnBody = spec.annotation?.invoke(scope, ObjString(spec.name), spec.fnBody) ?: spec.fnBody
val compiledFnBody = annotatedFnBody
spec.extTypeName?.let { typeName ->
val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance")
val stmt = object : Statement() {
override val pos: Pos = spec.startPos
override suspend fun execute(scope: Scope): Obj {
val result = (scope.thisObj as? ObjInstance)?.let { i ->
val execScope = if (compiledFnBody is net.sergeych.lyng.bytecode.BytecodeStatement) {
scope.applyClosureForBytecode(i.instanceScope).also {
it.args = scope.args
}
} else {
ClosureScope(scope, i.instanceScope)
}
compiledFnBody.execute(execScope)
} ?: compiledFnBody.execute(scope.thisObj.autoInstanceScope(scope))
return result
}
}
scope.addExtension(type, spec.name, ObjRecord(stmt, isMutable = false, visibility = spec.visibility, declaringClass = null))
val wrapperName = spec.extensionWrapperName ?: extensionCallableName(typeName, spec.name)
val wrapper = ObjExtensionMethodCallable(spec.name, stmt)
scope.addItem(wrapperName, false, wrapper, spec.visibility, recordType = ObjRecord.Type.Fun)
} ?: run {
val th = scope.thisObj
if (!spec.isStatic && th is ObjClass) {
val cls: ObjClass = th
cls.addFn(
spec.name,
isMutable = true,
visibility = spec.visibility,
isAbstract = spec.isAbstract,
isClosed = spec.isClosed,
isOverride = spec.isOverride,
pos = spec.startPos,
methodId = spec.memberMethodId
) {
val savedCtx = this.currentClassCtx
this.currentClassCtx = cls
try {
(thisObj as? ObjInstance)?.let { i ->
val execScope = i.instanceScope.createChildScope(
pos = this.pos,
args = this.args,
newThisObj = i
)
execScope.currentClassCtx = cls
compiledFnBody.execute(execScope)
} ?: compiledFnBody.execute(thisObj.autoInstanceScope(this))
} finally {
this.currentClassCtx = savedCtx
}
}
scope.addItem(spec.name, false, compiledFnBody, spec.visibility, callSignature = spec.externCallSignature)
compiledFnBody
} else {
scope.addItem(spec.name, false, compiledFnBody, spec.visibility, callSignature = spec.externCallSignature)
}
}
return annotatedFnBody
}
class FunctionDeclStatement( class FunctionDeclStatement(
private val delegate: Statement, val spec: FunctionDeclSpec,
private val startPos: Pos,
) : Statement() { ) : Statement() {
override val pos: Pos = startPos override val pos: Pos = spec.startPos
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope) return executeFunctionDecl(scope, spec)
} }
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
val target = scope.parent ?: scope val target = scope.parent ?: scope
return delegate.execute(target) return executeFunctionDecl(target, spec)
} }
} }

View File

@ -59,6 +59,8 @@ open class Scope(
// Enabled by default for child scopes; module/class scopes can ignore it. // Enabled by default for child scopes; module/class scopes can ignore it.
private val slots: MutableList<ObjRecord> = mutableListOf() private val slots: MutableList<ObjRecord> = mutableListOf()
private val nameToSlot: MutableMap<String, Int> = mutableMapOf() private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
internal var captureRecords: List<ObjRecord>? = null
internal var captureNames: List<String>? = null
/** /**
* Auxiliary per-frame map of local bindings (locals declared in this frame). * Auxiliary per-frame map of local bindings (locals declared in this frame).
* This helps resolving locals across suspension when slot ownership isn't * This helps resolving locals across suspension when slot ownership isn't
@ -169,6 +171,9 @@ open class Scope(
} }
internal fun resolveCaptureRecord(name: String): ObjRecord? { internal fun resolveCaptureRecord(name: String): ObjRecord? {
if (captureRecords != null) {
raiseIllegalState("resolveCaptureRecord is interpreter-only; bytecode captures use captureRecords")
}
return chainLookupIgnoreClosure(name, followClosure = true, caller = currentClassCtx) return chainLookupIgnoreClosure(name, followClosure = true, caller = currentClassCtx)
} }
@ -831,6 +836,10 @@ open class Scope(
open fun applyClosure(closure: Scope, preferredThisType: String? = null): Scope = open fun applyClosure(closure: Scope, preferredThisType: String? = null): Scope =
ClosureScope(this, closure, preferredThisType) ClosureScope(this, closure, preferredThisType)
internal fun applyClosureForBytecode(closure: Scope, preferredThisType: String? = null): Scope {
return BytecodeClosureScope(this, closure, preferredThisType)
}
/** /**
* Resolve and evaluate a qualified identifier exactly as compiled code would. * Resolve and evaluate a qualified identifier exactly as compiled code would.
* For input like `A.B.C`, it builds the same ObjRef chain the compiler emits: * For input like `A.B.C`, it builds the same ObjRef chain the compiler emits:

View File

@ -84,7 +84,7 @@ class Script(
seedModuleSlots(moduleTarget) seedModuleSlots(moduleTarget)
} }
moduleBytecode?.let { fn -> moduleBytecode?.let { fn ->
return CmdVm().execute(fn, scope, scope.args.list) return CmdVm().execute(fn, scope, scope.args)
} }
var lastResult: Obj = ObjVoid var lastResult: Obj = ObjVoid
for (s in statements) { for (s in statements) {

View File

@ -16,6 +16,7 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ArgsDeclaration
import net.sergeych.lyng.Pos import net.sergeych.lyng.Pos
import net.sergeych.lyng.Visibility import net.sergeych.lyng.Visibility
import net.sergeych.lyng.obj.ListLiteralRef import net.sergeych.lyng.obj.ListLiteralRef
@ -30,11 +31,32 @@ sealed class BytecodeConst {
data class StringVal(val value: String) : BytecodeConst() data class StringVal(val value: String) : BytecodeConst()
data class PosVal(val pos: Pos) : BytecodeConst() data class PosVal(val pos: Pos) : BytecodeConst()
data class ObjRef(val value: Obj) : BytecodeConst() data class ObjRef(val value: Obj) : BytecodeConst()
data class Ref(val value: net.sergeych.lyng.obj.ObjRef) : BytecodeConst()
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
data class ListLiteralPlan(val spreads: List<Boolean>) : BytecodeConst() data class ListLiteralPlan(val spreads: List<Boolean>) : BytecodeConst()
data class ValueFn(val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord) : BytecodeConst() data class LambdaFn(
val fn: CmdFunction,
val captureTableId: Int?,
val captureNames: List<String>,
val paramSlotPlan: Map<String, Int>,
val argsDeclaration: ArgsDeclaration?,
val preferredThisType: String?,
val wrapAsExtensionCallable: Boolean,
val returnLabels: Set<String>,
val pos: Pos,
) : BytecodeConst()
data class EnumDecl(
val declaredName: String,
val qualifiedName: String,
val entries: List<String>,
val lifted: Boolean,
) : BytecodeConst()
data class FunctionDecl(
val spec: net.sergeych.lyng.FunctionDeclSpec,
) : BytecodeConst()
data class ClassDecl(
val spec: net.sergeych.lyng.ClassDeclSpec,
) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst() data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst()
data class CaptureTable(val entries: List<BytecodeCaptureEntry>) : BytecodeConst()
data class ExtensionPropertyDecl( data class ExtensionPropertyDecl(
val extTypeName: String, val extTypeName: String,
val property: ObjProperty, val property: ObjProperty,

View File

@ -40,9 +40,23 @@ class BytecodeFrame(
slotTypes[slot] = type.code slotTypes[slot] = type.code
} }
override fun getObj(slot: Int): Obj = objSlots[slot] ?: ObjNull override fun getObj(slot: Int): Obj {
val value = objSlots[slot] ?: return ObjNull
return when (value) {
is net.sergeych.lyng.FrameSlotRef -> value.read()
is net.sergeych.lyng.RecordSlotRef -> value.read()
else -> value
}
}
internal fun getRawObj(slot: Int): Obj? = objSlots[slot]
override fun setObj(slot: Int, value: Obj) { override fun setObj(slot: Int, value: Obj) {
objSlots[slot] = value when (val current = objSlots[slot]) {
is net.sergeych.lyng.FrameSlotRef -> current.write(value)
is net.sergeych.lyng.RecordSlotRef -> current.write(value)
else -> objSlots[slot] = value
}
slotTypes[slot] = SlotType.OBJ.code slotTypes[slot] = SlotType.OBJ.code
} }

View File

@ -20,6 +20,7 @@ package net.sergeych.lyng.bytecode
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ValueFnRef
class BytecodeStatement private constructor( class BytecodeStatement private constructor(
val original: Statement, val original: Statement,
@ -29,7 +30,7 @@ class BytecodeStatement private constructor(
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
scope.pos = pos scope.pos = pos
return CmdVm().execute(function, scope, scope.args.list) return CmdVm().execute(function, scope, scope.args)
} }
internal fun bytecodeFunction(): CmdFunction = function internal fun bytecodeFunction(): CmdFunction = function
@ -43,6 +44,8 @@ class BytecodeStatement private constructor(
rangeLocalNames: Set<String> = emptySet(), rangeLocalNames: Set<String> = emptySet(),
allowedScopeNames: Set<String>? = null, allowedScopeNames: Set<String>? = null,
moduleScopeId: Int? = null, moduleScopeId: Int? = null,
forcedLocalSlots: Map<String, Int> = emptyMap(),
forcedLocalScopeId: Int? = null,
slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(), slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
knownNameObjClass: Map<String, ObjClass> = emptyMap(), knownNameObjClass: Map<String, ObjClass> = emptyMap(),
knownObjectNames: Set<String> = emptySet(), knownObjectNames: Set<String> = emptySet(),
@ -50,6 +53,7 @@ class BytecodeStatement private constructor(
enumEntriesByName: Map<String, List<String>> = emptyMap(), enumEntriesByName: Map<String, List<String>> = emptyMap(),
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(), callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(), callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
): Statement { ): Statement {
if (statement is BytecodeStatement) return statement if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement) val hasUnsupported = containsUnsupportedStatement(statement)
@ -67,13 +71,16 @@ class BytecodeStatement private constructor(
rangeLocalNames = rangeLocalNames, rangeLocalNames = rangeLocalNames,
allowedScopeNames = allowedScopeNames, allowedScopeNames = allowedScopeNames,
moduleScopeId = moduleScopeId, moduleScopeId = moduleScopeId,
forcedLocalSlots = forcedLocalSlots,
forcedLocalScopeId = forcedLocalScopeId,
slotTypeByScopeId = slotTypeByScopeId, slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownNameObjClass, knownNameObjClass = knownNameObjClass,
knownObjectNames = knownObjectNames, knownObjectNames = knownObjectNames,
classFieldTypesByName = classFieldTypesByName, classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName, enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) )
val compiled = compiler.compileStatement(nameHint, statement) val compiled = compiler.compileStatement(nameHint, statement)
val fn = compiled ?: throw BytecodeCompileException( val fn = compiled ?: throw BytecodeCompileException(
@ -132,6 +139,7 @@ class BytecodeStatement private constructor(
target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false
is net.sergeych.lyng.ThrowStatement -> is net.sergeych.lyng.ThrowStatement ->
containsUnsupportedStatement(target.throwExpr) containsUnsupportedStatement(target.throwExpr)
is net.sergeych.lyng.NopStatement -> false
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> false is net.sergeych.lyng.ExtensionPropertyDeclStatement -> false
is net.sergeych.lyng.ClassDeclStatement -> false is net.sergeych.lyng.ClassDeclStatement -> false
is net.sergeych.lyng.FunctionDeclStatement -> false is net.sergeych.lyng.FunctionDeclStatement -> false
@ -208,6 +216,7 @@ class BytecodeStatement private constructor(
stmt.label, stmt.label,
stmt.canBreak, stmt.canBreak,
stmt.loopSlotPlan, stmt.loopSlotPlan,
stmt.loopScopeId,
stmt.pos stmt.pos
) )
} }

View File

@ -68,7 +68,9 @@ class CmdBuilder {
scopeSlotNames: Array<String?> = emptyArray(), scopeSlotNames: Array<String?> = emptyArray(),
scopeSlotIsModule: BooleanArray = BooleanArray(0), scopeSlotIsModule: BooleanArray = BooleanArray(0),
localSlotNames: Array<String?> = emptyArray(), localSlotNames: Array<String?> = emptyArray(),
localSlotMutables: BooleanArray = BooleanArray(0) localSlotMutables: BooleanArray = BooleanArray(0),
localSlotDelegated: BooleanArray = BooleanArray(0),
localSlotCaptures: BooleanArray = BooleanArray(0)
): CmdFunction { ): CmdFunction {
val scopeSlotCount = scopeSlotIndices.size val scopeSlotCount = scopeSlotIndices.size
require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) { require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) {
@ -78,6 +80,8 @@ class CmdBuilder {
"scope slot module mapping size mismatch" "scope slot module mapping size mismatch"
} }
require(localSlotNames.size == localSlotMutables.size) { "local slot metadata size mismatch" } require(localSlotNames.size == localSlotMutables.size) { "local slot metadata size mismatch" }
require(localSlotNames.size == localSlotDelegated.size) { "local slot delegation size mismatch" }
require(localSlotNames.size == localSlotCaptures.size) { "local slot capture size mismatch" }
val labelIps = mutableMapOf<Label, Int>() val labelIps = mutableMapOf<Label, Int>()
for ((label, idx) in labelPositions) { for ((label, idx) in labelPositions) {
labelIps[label] = idx labelIps[label] = idx
@ -111,6 +115,8 @@ class CmdBuilder {
scopeSlotIsModule = if (scopeSlotIsModule.isEmpty()) BooleanArray(scopeSlotCount) else scopeSlotIsModule, scopeSlotIsModule = if (scopeSlotIsModule.isEmpty()) BooleanArray(scopeSlotCount) else scopeSlotIsModule,
localSlotNames = localSlotNames, localSlotNames = localSlotNames,
localSlotMutables = localSlotMutables, localSlotMutables = localSlotMutables,
localSlotDelegated = localSlotDelegated,
localSlotCaptures = localSlotCaptures,
constants = constPool.toList(), constants = constPool.toList(),
cmds = cmds.toTypedArray(), cmds = cmds.toTypedArray(),
posByIp = posByInstr.toTypedArray() posByIp = posByInstr.toTypedArray()
@ -123,7 +129,7 @@ class CmdBuilder {
Opcode.CLEAR_PENDING_THROWABLE, Opcode.RETHROW_PENDING -> emptyList() Opcode.CLEAR_PENDING_THROWABLE, Opcode.RETHROW_PENDING -> emptyList()
Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ,
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
Opcode.OBJ_TO_BOOL, Opcode.OBJ_TO_BOOL, Opcode.GET_OBJ_CLASS,
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT,
Opcode.ASSERT_IS -> Opcode.ASSERT_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
@ -135,19 +141,27 @@ class CmdBuilder {
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->
listOf(OperandKind.SLOT, OperandKind.ADDR) listOf(OperandKind.SLOT, OperandKind.ADDR)
Opcode.DELEGATED_GET_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT)
Opcode.DELEGATED_SET_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT)
Opcode.BIND_DELEGATE_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.CONST, OperandKind.SLOT)
Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR -> Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR ->
listOf(OperandKind.ADDR, OperandKind.SLOT) listOf(OperandKind.ADDR, OperandKind.SLOT)
Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR -> Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR ->
listOf(OperandKind.SLOT, OperandKind.ADDR) listOf(OperandKind.SLOT, OperandKind.ADDR)
Opcode.CONST_NULL -> Opcode.CONST_NULL ->
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, Opcode.MAKE_VALUE_FN -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
Opcode.MAKE_LAMBDA_FN ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
listOf(OperandKind.CONST) listOf(OperandKind.CONST)
Opcode.PUSH_TRY -> Opcode.PUSH_TRY ->
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP) listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE, Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE,
Opcode.DECL_ENUM, Opcode.DECL_FUNCTION, Opcode.DECL_CLASS,
Opcode.ASSIGN_DESTRUCTURE -> Opcode.ASSIGN_DESTRUCTURE ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT,
@ -240,9 +254,10 @@ class CmdBuilder {
Opcode.CONST_REAL -> CmdConstReal(operands[0], operands[1]) Opcode.CONST_REAL -> CmdConstReal(operands[0], operands[1])
Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1]) Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1])
Opcode.CONST_NULL -> CmdConstNull(operands[0]) Opcode.CONST_NULL -> CmdConstNull(operands[0])
Opcode.MAKE_VALUE_FN -> CmdMakeValueFn(operands[0], operands[1]) Opcode.MAKE_LAMBDA_FN -> CmdMakeLambda(operands[0], operands[1])
Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1]) Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1])
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1]) Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
Opcode.GET_OBJ_CLASS -> CmdGetObjClass(operands[0], operands[1])
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
Opcode.LOAD_THIS -> CmdLoadThis(operands[0]) Opcode.LOAD_THIS -> CmdLoadThis(operands[0])
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1]) Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
@ -254,6 +269,9 @@ class CmdBuilder {
Opcode.THROW -> CmdThrow(operands[0], operands[1]) Opcode.THROW -> CmdThrow(operands[0], operands[1])
Opcode.RETHROW_PENDING -> CmdRethrowPending() Opcode.RETHROW_PENDING -> CmdRethrowPending()
Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1]) Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1])
Opcode.DELEGATED_GET_LOCAL -> CmdDelegatedGetLocal(operands[0], operands[1], operands[2])
Opcode.DELEGATED_SET_LOCAL -> CmdDelegatedSetLocal(operands[0], operands[1], operands[2])
Opcode.BIND_DELEGATE_LOCAL -> CmdBindDelegateLocal(operands[0], operands[1], operands[2], operands[3])
Opcode.LOAD_OBJ_ADDR -> CmdLoadObjAddr(operands[0], operands[1]) Opcode.LOAD_OBJ_ADDR -> CmdLoadObjAddr(operands[0], operands[1])
Opcode.STORE_OBJ_ADDR -> CmdStoreObjAddr(operands[0], operands[1]) Opcode.STORE_OBJ_ADDR -> CmdStoreObjAddr(operands[0], operands[1])
Opcode.LOAD_INT_ADDR -> CmdLoadIntAddr(operands[0], operands[1]) Opcode.LOAD_INT_ADDR -> CmdLoadIntAddr(operands[0], operands[1])
@ -397,6 +415,9 @@ class CmdBuilder {
Opcode.DECL_LOCAL -> CmdDeclLocal(operands[0], operands[1]) Opcode.DECL_LOCAL -> CmdDeclLocal(operands[0], operands[1])
Opcode.DECL_DELEGATED -> CmdDeclDelegated(operands[0], operands[1]) Opcode.DECL_DELEGATED -> CmdDeclDelegated(operands[0], operands[1])
Opcode.DECL_DESTRUCTURE -> CmdDeclDestructure(operands[0], operands[1]) Opcode.DECL_DESTRUCTURE -> CmdDeclDestructure(operands[0], operands[1])
Opcode.DECL_ENUM -> CmdDeclEnum(operands[0], operands[1])
Opcode.DECL_FUNCTION -> CmdDeclFunction(operands[0], operands[1])
Opcode.DECL_CLASS -> CmdDeclClass(operands[0], operands[1])
Opcode.DECL_EXT_PROPERTY -> CmdDeclExtProperty(operands[0], operands[1]) Opcode.DECL_EXT_PROPERTY -> CmdDeclExtProperty(operands[0], operands[1])
Opcode.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3]) Opcode.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3])
Opcode.ASSIGN_DESTRUCTURE -> CmdAssignDestructure(operands[0], operands[1]) Opcode.ASSIGN_DESTRUCTURE -> CmdAssignDestructure(operands[0], operands[1])

View File

@ -50,6 +50,23 @@ object CmdDisassembler {
} }
out.append('\n') out.append('\n')
} }
val captureConsts = fn.constants.withIndex().mapNotNull { (idx, constVal) ->
val table = constVal as? BytecodeConst.CaptureTable ?: return@mapNotNull null
idx to table
}
if (captureConsts.isNotEmpty()) {
out.append("consts:\n")
for ((idx, table) in captureConsts) {
val entries = if (table.entries.isEmpty()) {
"[]"
} else {
table.entries.joinToString(prefix = "[", postfix = "]") { entry ->
"${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}@s${entry.slotIndex}"
}
}
out.append("k").append(idx).append(" CAPTURE_TABLE ").append(entries).append('\n')
}
}
return out.toString() return out.toString()
} }
@ -69,9 +86,10 @@ object CmdDisassembler {
is CmdLoadThis -> Opcode.LOAD_THIS to intArrayOf(cmd.dst) is CmdLoadThis -> Opcode.LOAD_THIS to intArrayOf(cmd.dst)
is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, cmd.dst) is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, cmd.dst)
is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst) is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst)
is CmdMakeValueFn -> Opcode.MAKE_VALUE_FN to intArrayOf(cmd.id, cmd.dst) is CmdMakeLambda -> Opcode.MAKE_LAMBDA_FN to intArrayOf(cmd.id, cmd.dst)
is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst) is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst)
is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst) is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst)
is CmdGetObjClass -> Opcode.GET_OBJ_CLASS to intArrayOf(cmd.src, cmd.dst)
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot) is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
@ -84,6 +102,9 @@ object CmdDisassembler {
cmd.dst cmd.dst
) )
is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot) is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot)
is CmdDelegatedGetLocal -> Opcode.DELEGATED_GET_LOCAL to intArrayOf(cmd.delegateSlot, cmd.nameId, cmd.dst)
is CmdDelegatedSetLocal -> Opcode.DELEGATED_SET_LOCAL to intArrayOf(cmd.delegateSlot, cmd.nameId, cmd.valueSlot)
is CmdBindDelegateLocal -> Opcode.BIND_DELEGATE_LOCAL to intArrayOf(cmd.delegateSlot, cmd.nameId, cmd.accessId, cmd.dst)
is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot) is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot)
is CmdLoadIntAddr -> Opcode.LOAD_INT_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) is CmdLoadIntAddr -> Opcode.LOAD_INT_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
@ -191,6 +212,9 @@ object CmdDisassembler {
is CmdDeclLocal -> Opcode.DECL_LOCAL to intArrayOf(cmd.constId, cmd.slot) is CmdDeclLocal -> Opcode.DECL_LOCAL to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclDelegated -> Opcode.DECL_DELEGATED to intArrayOf(cmd.constId, cmd.slot) is CmdDeclDelegated -> Opcode.DECL_DELEGATED to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclDestructure -> Opcode.DECL_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot) is CmdDeclDestructure -> Opcode.DECL_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclEnum -> Opcode.DECL_ENUM to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclFunction -> Opcode.DECL_FUNCTION to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclClass -> Opcode.DECL_CLASS to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclExtProperty -> Opcode.DECL_EXT_PROPERTY to intArrayOf(cmd.constId, cmd.slot) is CmdDeclExtProperty -> Opcode.DECL_EXT_PROPERTY to intArrayOf(cmd.constId, cmd.slot)
is CmdCallDirect -> Opcode.CALL_DIRECT to intArrayOf(cmd.id, cmd.argBase, cmd.argCount, cmd.dst) is CmdCallDirect -> Opcode.CALL_DIRECT to intArrayOf(cmd.id, cmd.argBase, cmd.argCount, cmd.dst)
is CmdAssignDestructure -> Opcode.ASSIGN_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot) is CmdAssignDestructure -> Opcode.ASSIGN_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot)
@ -231,7 +255,7 @@ object CmdDisassembler {
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.OBJ_TO_BOOL, Opcode.ASSERT_IS -> Opcode.OBJ_TO_BOOL, Opcode.GET_OBJ_CLASS, Opcode.ASSERT_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW -> Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
@ -243,19 +267,27 @@ object CmdDisassembler {
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->
listOf(OperandKind.SLOT, OperandKind.ADDR) listOf(OperandKind.SLOT, OperandKind.ADDR)
Opcode.DELEGATED_GET_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT)
Opcode.DELEGATED_SET_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT)
Opcode.BIND_DELEGATE_LOCAL ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.CONST, OperandKind.SLOT)
Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR -> Opcode.LOAD_OBJ_ADDR, Opcode.LOAD_INT_ADDR, Opcode.LOAD_REAL_ADDR, Opcode.LOAD_BOOL_ADDR ->
listOf(OperandKind.ADDR, OperandKind.SLOT) listOf(OperandKind.ADDR, OperandKind.SLOT)
Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR -> Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR ->
listOf(OperandKind.SLOT, OperandKind.ADDR) listOf(OperandKind.SLOT, OperandKind.ADDR)
Opcode.CONST_NULL -> Opcode.CONST_NULL ->
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL, Opcode.MAKE_VALUE_FN -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
Opcode.MAKE_LAMBDA_FN ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
listOf(OperandKind.CONST) listOf(OperandKind.CONST)
Opcode.PUSH_TRY -> Opcode.PUSH_TRY ->
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP) listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE, Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE,
Opcode.DECL_ENUM, Opcode.DECL_FUNCTION, Opcode.DECL_CLASS,
Opcode.ASSIGN_DESTRUCTURE -> Opcode.ASSIGN_DESTRUCTURE ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT,

View File

@ -28,6 +28,8 @@ data class CmdFunction(
val scopeSlotIsModule: BooleanArray, val scopeSlotIsModule: BooleanArray,
val localSlotNames: Array<String?>, val localSlotNames: Array<String?>,
val localSlotMutables: BooleanArray, val localSlotMutables: BooleanArray,
val localSlotDelegated: BooleanArray,
val localSlotCaptures: BooleanArray,
val constants: List<BytecodeConst>, val constants: List<BytecodeConst>,
val cmds: Array<Cmd>, val cmds: Array<Cmd>,
val posByIp: Array<net.sergeych.lyng.Pos?>, val posByIp: Array<net.sergeych.lyng.Pos?>,
@ -37,10 +39,31 @@ data class CmdFunction(
require(scopeSlotNames.size == scopeSlotCount) { "scopeSlotNames size mismatch" } require(scopeSlotNames.size == scopeSlotCount) { "scopeSlotNames size mismatch" }
require(scopeSlotIsModule.size == scopeSlotCount) { "scopeSlotIsModule size mismatch" } require(scopeSlotIsModule.size == scopeSlotCount) { "scopeSlotIsModule size mismatch" }
require(localSlotNames.size == localSlotMutables.size) { "localSlot metadata size mismatch" } require(localSlotNames.size == localSlotMutables.size) { "localSlot metadata size mismatch" }
require(localSlotNames.size == localSlotDelegated.size) { "localSlot delegation size mismatch" }
require(localSlotNames.size == localSlotCaptures.size) { "localSlot capture size mismatch" }
require(localSlotNames.size <= localCount) { "localSlotNames exceed localCount" } require(localSlotNames.size <= localCount) { "localSlotNames exceed localCount" }
require(addrCount >= 0) { "addrCount must be non-negative" } require(addrCount >= 0) { "addrCount must be non-negative" }
if (posByIp.isNotEmpty()) { if (posByIp.isNotEmpty()) {
require(posByIp.size == cmds.size) { "posByIp size mismatch" } require(posByIp.size == cmds.size) { "posByIp size mismatch" }
} }
} }
fun localSlotPlanByName(): Map<String, Int> {
val result = LinkedHashMap<String, Int>()
for (i in localSlotNames.indices) {
val name = localSlotNames[i] ?: continue
val existing = result[name]
if (existing == null) {
result[name] = i
continue
}
val existingIsCapture = localSlotCaptures.getOrNull(existing) == true
val currentIsCapture = localSlotCaptures.getOrNull(i) == true
if (existingIsCapture && !currentIsCapture) {
result[name] = i
}
}
return result
}
} }

View File

@ -0,0 +1,35 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
enum class CaptureOwnerFrameKind { MODULE, LOCAL }
data class LambdaCaptureEntry(
val ownerKind: CaptureOwnerFrameKind,
val ownerScopeId: Int,
val ownerSlotId: Int,
val ownerName: String,
val ownerIsMutable: Boolean,
val ownerIsDelegated: Boolean,
)
data class BytecodeCaptureEntry(
val ownerKind: CaptureOwnerFrameKind,
val ownerScopeId: Int,
val ownerSlotId: Int,
val slotIndex: Int,
)

View File

@ -31,7 +31,6 @@ enum class Opcode(val code: Int) {
RANGE_INT_BOUNDS(0x0B), RANGE_INT_BOUNDS(0x0B),
MAKE_RANGE(0x0C), MAKE_RANGE(0x0C),
LOAD_THIS(0x0D), LOAD_THIS(0x0D),
MAKE_VALUE_FN(0x0E),
LOAD_THIS_VARIANT(0x0F), LOAD_THIS_VARIANT(0x0F),
INT_TO_REAL(0x10), INT_TO_REAL(0x10),
@ -42,6 +41,8 @@ enum class Opcode(val code: Int) {
CHECK_IS(0x15), CHECK_IS(0x15),
ASSERT_IS(0x16), ASSERT_IS(0x16),
MAKE_QUALIFIED_VIEW(0x17), MAKE_QUALIFIED_VIEW(0x17),
MAKE_LAMBDA_FN(0x18),
GET_OBJ_CLASS(0x19),
ADD_INT(0x20), ADD_INT(0x20),
SUB_INT(0x21), SUB_INT(0x21),
@ -158,9 +159,15 @@ enum class Opcode(val code: Int) {
STORE_BOOL_ADDR(0xB9), STORE_BOOL_ADDR(0xB9),
THROW(0xBB), THROW(0xBB),
RETHROW_PENDING(0xBC), RETHROW_PENDING(0xBC),
DECL_ENUM(0xBE),
ITER_PUSH(0xBF), ITER_PUSH(0xBF),
ITER_POP(0xC0), ITER_POP(0xC0),
ITER_CANCEL(0xC1), ITER_CANCEL(0xC1),
DELEGATED_GET_LOCAL(0xC2),
DELEGATED_SET_LOCAL(0xC3),
BIND_DELEGATE_LOCAL(0xC4),
DECL_FUNCTION(0xC5),
DECL_CLASS(0xC6),
; ;
companion object { companion object {

View File

@ -0,0 +1,33 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.ArgsDeclaration
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.bytecode.CmdFunction
class LambdaFnRef(
valueFn: suspend (Scope) -> ObjRecord,
val bytecodeFn: CmdFunction?,
val paramSlotPlan: Map<String, Int>,
val argsDeclaration: ArgsDeclaration?,
val preferredThisType: String?,
val wrapAsExtensionCallable: Boolean,
val returnLabels: Set<String>,
val pos: Pos,
) : ValueFnRef(valueFn)

View File

@ -18,7 +18,6 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ClosureScope
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
@ -63,7 +62,7 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
* with method invocation which is implemented separately in [invokeInstanceMethod] below. * with method invocation which is implemented separately in [invokeInstanceMethod] below.
*/ */
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
return readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))?.let { return readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))?.let {
if (writeCallback != null) if (writeCallback != null)
it.asMutable it.asMutable
@ -83,26 +82,26 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
args: Arguments, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)? onNotFoundResult: (suspend () -> Obj?)?
): Obj { ): Obj {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name)))) val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
return over?.invoke(scope, scope.thisObj, args) return over?.invoke(scope, scope.thisObj, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
} }
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
writeCallback?.execute(execBase.createChildScope(Arguments(ObjString(name), newValue))) writeCallback?.execute(execBase.createChildScope(Arguments(ObjString(name), newValue)))
?: super.writeField(scope, name, newValue) ?: super.writeField(scope, name, newValue)
} }
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
return readCallback?.execute(execBase.createChildScope(Arguments(index))) return readCallback?.execute(execBase.createChildScope(Arguments(index)))
?: super.getAt(scope, index) ?: super.getAt(scope, index)
} }
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val execBase = builderScope?.let { scope.applyClosure(it) } ?: scope
writeCallback?.execute(execBase.createChildScope(Arguments(index, newValue))) writeCallback?.execute(execBase.createChildScope(Arguments(index, newValue)))
?: super.putAt(scope, index, newValue) ?: super.putAt(scope, index, newValue)
} }

View File

@ -18,7 +18,6 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ClosureScope
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
@ -35,9 +34,9 @@ class ObjProperty(
suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj { suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj {
val g = getter ?: scope.raiseError("property $name has no getter") val g = getter ?: scope.raiseError("property $name has no getter")
// Execute getter in a child scope of the instance with 'this' properly set // Execute getter in a child scope of the instance with 'this' properly set
// Use ClosureScope to match extension function behavior (access to instance scope + call scope) // Match extension function behavior (access to instance scope + call scope).
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope) val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
val execScope = ClosureScope(scope, instanceScope).createChildScope(newThisObj = instance) val execScope = scope.applyClosure(instanceScope).createChildScope(newThisObj = instance)
execScope.currentClassCtx = declaringClass execScope.currentClassCtx = declaringClass
return g.execute(execScope) return g.execute(execScope)
} }
@ -45,9 +44,9 @@ class ObjProperty(
suspend fun callSetter(scope: Scope, instance: Obj, value: Obj, declaringClass: ObjClass? = null) { suspend fun callSetter(scope: Scope, instance: Obj, value: Obj, declaringClass: ObjClass? = null) {
val s = setter ?: scope.raiseError("property $name has no setter") val s = setter ?: scope.raiseError("property $name has no setter")
// Execute setter in a child scope of the instance with 'this' properly set and the value as an argument // Execute setter in a child scope of the instance with 'this' properly set and the value as an argument
// Use ClosureScope to match extension function behavior // Match extension function behavior (access to instance scope + call scope).
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope) val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
val execScope = ClosureScope(scope, instanceScope).createChildScope(args = Arguments(value), newThisObj = instance) val execScope = scope.applyClosure(instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
execScope.currentClassCtx = declaringClass execScope.currentClassCtx = declaringClass
s.execute(execScope) s.execute(execScope)
} }

View File

@ -20,6 +20,8 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.FrameSlotRef
import net.sergeych.lyng.RecordSlotRef
/** /**
* A reference to a value with optional write-back path. * A reference to a value with optional write-back path.
@ -43,7 +45,12 @@ sealed interface ObjRef {
if (rec.receiver != null && rec.declaringClass != null) { if (rec.receiver != null && rec.declaringClass != null) {
return rec.receiver!!.resolveRecord(scope, rec, "unknown", rec.declaringClass).value return rec.receiver!!.resolveRecord(scope, rec, "unknown", rec.declaringClass).value
} }
return rec.value val value = rec.value
return when (value) {
is FrameSlotRef -> value.read()
is RecordSlotRef -> value.read()
else -> 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")
@ -65,12 +72,19 @@ sealed interface ObjRef {
} }
/** Runtime-computed read-only reference backed by a lambda. */ /** Runtime-computed read-only reference backed by a lambda. */
class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef { open class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef {
internal fun valueFn(): suspend (Scope) -> ObjRecord = fn internal fun valueFn(): suspend (Scope) -> ObjRecord = fn
override suspend fun get(scope: Scope): ObjRecord = fn(scope) override suspend fun get(scope: Scope): ObjRecord = fn(scope)
} }
/** Compile-time supported ::class operator reference. */
class ClassOperatorRef(val target: ObjRef, val pos: Pos) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
return target.evalValue(scope).objClass.asReadonly
}
}
/** Unary operations supported by ObjRef. */ /** Unary operations supported by ObjRef. */
enum class UnaryOp { NOT, NEGATE, BITNOT } enum class UnaryOp { NOT, NEGATE, BITNOT }
@ -1971,6 +1985,7 @@ class BoundLocalVarRef(
private val slot: Int, private val slot: Int,
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
internal fun slotIndex(): Int = slot
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos scope.pos = atPos
val rec = scope.getSlotRecord(slot) val rec = scope.getSlotRecord(slot)

View File

@ -99,6 +99,7 @@ class ForInStatement(
val label: String?, val label: String?,
val canBreak: Boolean, val canBreak: Boolean,
val loopSlotPlan: Map<String, Int>, val loopSlotPlan: Map<String, Int>,
val loopScopeId: Int,
override val pos: Pos, override val pos: Pos,
) : Statement() { ) : Statement() {
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
@ -439,15 +440,6 @@ class ThrowStatement(
} }
} }
class ToBoolStatement(
val expr: Statement,
override val pos: Pos,
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
return if (expr.execute(scope).toBool()) net.sergeych.lyng.obj.ObjTrue else net.sergeych.lyng.obj.ObjFalse
}
}
class ExpressionStatement( class ExpressionStatement(
val ref: net.sergeych.lyng.obj.ObjRef, val ref: net.sergeych.lyng.obj.ObjRef,
override val pos: Pos override val pos: Pos

View File

@ -16,8 +16,22 @@
*/ */
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.eval import net.sergeych.lyng.eval
import net.sergeych.lyng.toSource
import net.sergeych.lyng.bytecode.CmdDisassembler
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.obj.toInt
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class BytecodeRecentOpsTest { class BytecodeRecentOpsTest {
@ -88,4 +102,300 @@ class BytecodeRecentOpsTest {
""".trimIndent() """.trimIndent()
) )
} }
@Test
fun optionalIndexIncDecSkipsOnNullReceiver() = runTest {
eval(
"""
var count = 0
fun idx() { count = count + 1; return 1 }
var a: List<Int>? = null
val r = a?[idx()]++
assertEquals(null, r)
assertEquals(0, count)
""".trimIndent()
)
}
@Test
fun optionalIndexIncDecUpdatesOnNonNullReceiver() = runTest {
eval(
"""
var a = [1, 2, 3]
val r = a?[1]++
assertEquals(2, r)
assertEquals(3, a[1])
""".trimIndent()
)
}
@Test
fun optionalIndexPreIncSkipsOnNullReceiver() = runTest {
eval(
"""
var count = 0
fun idx() { count = count + 1; return 1 }
var a: List<Int>? = null
val r = ++a?[idx()]
assertEquals(null, r)
assertEquals(0, count)
""".trimIndent()
)
}
@Test
fun optionalClassScopeIncDec() = runTest {
eval(
"""
class C { static var x = 1 }
val r = C?.x++
assertEquals(1, r)
assertEquals(2, C.x)
""".trimIndent()
)
}
@Test
fun classScopeIfNullAssign() = runTest {
eval(
"""
class C { static var x: Object? = null }
C.x ?= 7
assertEquals(7, C.x)
C.x ?= 9
assertEquals(7, C.x)
""".trimIndent()
)
}
@Test
fun callablePropertyCall() = runTest {
eval(
"""
class C { var f = { x -> x + 1 } }
val c = C()
val r = (c.f)(2)
assertEquals(3, r)
""".trimIndent()
)
}
@Test
fun lambdaCapturesLocalByReference() = runTest {
eval(
"""
fun make() {
var base = 3
val f = { x -> x + base }
base = 7
return f(1)
}
assertEquals(8, make())
""".trimIndent()
)
}
@Test
fun lambdaCapturesDelegatedLocal() = runTest {
eval(
"""
class BoxDelegate(var v) : Delegate {
override fun getValue(thisRef: Object, name: String): Object = v
override fun setValue(thisRef: Object, name: String, value: Object) { v = value }
}
fun make() {
var x by BoxDelegate(1)
val f = { y -> x += y; return x }
return f(2)
}
assertEquals(3, make())
""".trimIndent()
)
}
@Test
fun delegatedMemberAccessAndCall() = runTest {
eval(
"""
class ConstDelegate(val v) : Delegate {
override fun getValue(thisRef: Object, name: String): Object = v
}
class ActionDelegate : Delegate {
override fun invoke(thisRef: Object, name: String, args...) {
val list: List = args as List
"Called %s with %d args: %s"(name, list.size, list.toString())
}
}
class C {
val a by ConstDelegate(7)
fun greet by ActionDelegate()
}
val c = C()
assertEquals(7, c.a)
assertEquals("Called greet with 2 args: [hi,world]", c.greet("hi", "world"))
""".trimIndent()
)
}
@Test
fun delegatedLocalAssignAndIncDec() = runTest {
eval(
"""
class BoxDelegate(var v) : Delegate {
override fun getValue(thisRef: Object, name: String): Object = v
override fun setValue(thisRef: Object, name: String, value: Object) { v = value }
}
fun calc() {
var x by BoxDelegate(1)
x += 2
x++
return x
}
assertEquals(4, calc())
""".trimIndent()
)
}
@Test
fun delegatedLocalDisasmUsesDelegateOps() = runTest {
val script = """
class BoxDelegate(var v) : Delegate {
override fun getValue(thisRef: Object, name: String): Object = v
override fun setValue(thisRef: Object, name: String, value: Object) { v = value }
}
fun calc() {
var x by BoxDelegate(1)
x += 2
x++
return x
}
""".trimIndent()
val compiled = Compiler.compile(script.toSource(), Script.defaultImportManager)
val scope = Script.defaultImportManager.newModuleAt(Pos.builtIn)
compiled.execute(scope)
val disasm = scope.disassembleSymbol("calc")
assertTrue(disasm.contains("DELEGATED_GET_LOCAL"), disasm)
assertTrue(disasm.contains("DELEGATED_SET_LOCAL"), disasm)
assertTrue(disasm.contains("DECL_DELEGATED"), disasm)
}
@Test
fun moduleDeclsAvoidCallableCallSlots() = runTest {
val script = """
class A {}
fun f() { 1 }
enum E { one }
""".trimIndent()
val compiled = Compiler.compile(script.toSource(), Script.defaultImportManager)
val field = Script::class.java.getDeclaredField("moduleBytecode")
field.isAccessible = true
val moduleFn = field.get(compiled) as? CmdFunction
assertNotNull(moduleFn, "module bytecode missing")
val disasm = CmdDisassembler.disassemble(moduleFn)
assertTrue(!disasm.contains("CALL_SLOT"), disasm)
assertTrue(!disasm.contains("Callable@"), disasm)
assertTrue(disasm.contains("DECL_CLASS"), disasm)
assertTrue(disasm.contains("DECL_FUNCTION"), disasm)
assertTrue(disasm.contains("DECL_ENUM"), disasm)
}
@Test
fun unionMemberDispatchSubtype() = runTest {
eval(
"""
class A { fun who() = "A" }
class B : A { override fun who() = "B" }
fun pick(x: A | B) { x.who() }
assertEquals("B", pick(B()))
""".trimIndent()
)
}
@Test
fun staticMemberDeclNopStatement() = runTest {
eval(
"""
class C {
static fun ping() { 7 }
}
assertEquals(7, C.ping())
""".trimIndent()
)
}
@Test
fun unionMemberDispatchMismatch() = runTest {
val err = assertFailsWith<ExecutionError> {
eval(
"""
class A { fun who() = "A" }
class B { fun who() = "B" }
val x: A | B = 1
x.who()
""".trimIndent()
)
}
assertTrue(err.message?.contains("value is not A | B") == true)
}
@Test
fun objectReceiverMemberError() = runTest {
val failed = try {
eval("fun bad(x) { x.missing() }")
false
} catch (_: ScriptError) {
true
}
assertTrue(failed)
}
@Test
fun unionMissingMemberError() = runTest {
val failed = try {
eval(
"""
class A { fun who() = "A" }
class B { fun other() = "B" }
fun pick(x: A | B) { x.who() }
""".trimIndent()
)
false
} catch (_: ScriptError) {
true
}
assertTrue(failed)
}
@Test
fun qualifiedThisValueRef() = runTest {
eval(
"""
class T(val v) {
fun get() {
this@T.v
}
}
assertEquals(7, T(7).get())
""".trimIndent()
)
}
@Test
fun fastLocalVarRefRead() = runTest {
val code = """
fun addOne(x) {
val y = x + 1
y
}
addOne(1)
""".trimIndent()
val script = Compiler.compileWithResolution(
Source("<fast-local>", code),
Script.defaultImportManager,
useBytecodeStatements = true,
useFastLocalRefs = true
)
val result = script.execute(Script.defaultImportManager.newStdScope())
assertEquals(2, result.toInt())
}
} }

View File

@ -31,6 +31,12 @@ extern class List<T> : Array<T> {
fun add(value: T, more...): Void fun add(value: T, more...): Void
} }
extern class RingBuffer<T> : Iterable<T> {
val size: Int
fun first(): T
fun add(value: T): Void
}
extern class Set<T> : Collection<T> { extern class Set<T> : Collection<T> {
} }
@ -270,8 +276,10 @@ fun Iterable<T>.shuffled(): List<T> {
*/ */
fun Iterable<Iterable<T>>.flatten(): List<T> { fun Iterable<Iterable<T>>.flatten(): List<T> {
var result: List<T> = List() var result: List<T> = List()
forEach { i -> for (i in this) {
i.forEach { result += it } for (item in i) {
result += item
}
} }
result result
} }