Compare commits
38 Commits
473a5dd6ed
...
db4f7d0973
| Author | SHA1 | Date | |
|---|---|---|---|
| db4f7d0973 | |||
| a481371349 | |||
| 51319fa8b7 | |||
| 86ac7eee54 | |||
| 18278794d6 | |||
| ac680ceb6c | |||
| 6efdfc1964 | |||
| b9af80a1b2 | |||
| bde32ca7b5 | |||
| 99ca15d20f | |||
| c14c7d43d9 | |||
| 1271f347bd | |||
| 4e08339756 | |||
| b32a937636 | |||
| 4db0a7fbab | |||
| 8b196c7b0c | |||
| 54ca886753 | |||
| abbebec153 | |||
| c066dc7150 | |||
| 6c0b86f6e6 | |||
| 8f1c660f4e | |||
| 3ce9029162 | |||
| 0caa9849cf | |||
| 8314127fdb | |||
| dab0b9f165 | |||
| 6aa23e8ef3 | |||
| c035b4c34c | |||
| 9d508e219f | |||
| b49f291bff | |||
| ae88898f58 | |||
| 565dbf98ed | |||
| 5305ced89f | |||
| b2d5897aa8 | |||
| 0b94b46d40 | |||
| 541738646f | |||
| f0dc0d2396 | |||
| 7850d5fbde | |||
| cde7cf2caf |
@ -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] 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] 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
|
||||
|
||||
|
||||
@ -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.option
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.LyngVersion
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
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 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 execute: String? by option(
|
||||
"-x", "--execute", help = """
|
||||
@ -198,7 +201,15 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
||||
launcher {
|
||||
// there is no script name, it is a first argument instead:
|
||||
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()
|
||||
} else {
|
||||
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 {
|
||||
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 ->
|
||||
fileSource.buffer().use { bs ->
|
||||
bs.readUtf8()
|
||||
@ -236,7 +247,16 @@ suspend fun executeFile(fileName: String) {
|
||||
text = text.substring(pos + 1)
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -31,8 +31,18 @@ class BlockStatement(
|
||||
val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos)
|
||||
if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan)
|
||||
if (captureSlots.isNotEmpty()) {
|
||||
val captureRecords = scope.captureRecords
|
||||
if (captureRecords != null) {
|
||||
for (i in captureSlots.indices) {
|
||||
val capture = captureSlots[i]
|
||||
val rec = captureRecords.getOrNull(i)
|
||||
?: scope.raiseSymbolNotFound("capture ${capture.name} not found")
|
||||
target.updateSlotFor(capture.name, rec)
|
||||
}
|
||||
} else {
|
||||
val applyScope = scope as? ApplyScope
|
||||
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)
|
||||
@ -49,6 +59,7 @@ class BlockStatement(
|
||||
target.updateSlotFor(capture.name, rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
return block.execute(target)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -17,20 +17,147 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
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(
|
||||
private val delegate: Statement,
|
||||
private val startPos: Pos,
|
||||
val declaredName: String?,
|
||||
val spec: ClassDeclSpec,
|
||||
) : 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 {
|
||||
return delegate.execute(scope)
|
||||
return executeClassDecl(scope, spec)
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val target = scope.parent ?: scope
|
||||
return delegate.execute(target)
|
||||
return executeClassDecl(target, spec)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) :
|
||||
Scope(callScope, thisObj = applied.thisObj) {
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
@ -28,6 +28,8 @@ class DelegatedVarDeclStatement(
|
||||
val visibility: Visibility,
|
||||
val initializer: Statement,
|
||||
val isTransient: Boolean,
|
||||
val slotIndex: Int?,
|
||||
val scopeId: Int?,
|
||||
private val startPos: Pos,
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
|
||||
@ -17,19 +17,34 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjEnumClass
|
||||
import net.sergeych.lyng.obj.ObjRecord
|
||||
|
||||
class EnumDeclStatement(
|
||||
private val delegate: Statement,
|
||||
val declaredName: String,
|
||||
val qualifiedName: String,
|
||||
val entries: List<String>,
|
||||
val lifted: Boolean,
|
||||
private val startPos: Pos,
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
|
||||
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 {
|
||||
val target = scope.parent ?: scope
|
||||
return delegate.execute(target)
|
||||
return execute(target)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,19 +17,247 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
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(
|
||||
private val delegate: Statement,
|
||||
private val startPos: Pos,
|
||||
val spec: FunctionDeclSpec,
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
override val pos: Pos = spec.startPos
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return delegate.execute(scope)
|
||||
return executeFunctionDecl(scope, spec)
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val target = scope.parent ?: scope
|
||||
return delegate.execute(target)
|
||||
return executeFunctionDecl(target, spec)
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ open class Scope(
|
||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||
private val slots: MutableList<ObjRecord> = mutableListOf()
|
||||
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).
|
||||
* This helps resolving locals across suspension when slot ownership isn't
|
||||
@ -169,6 +171,9 @@ open class Scope(
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -831,6 +836,10 @@ open class Scope(
|
||||
open fun applyClosure(closure: Scope, preferredThisType: String? = null): Scope =
|
||||
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.
|
||||
* For input like `A.B.C`, it builds the same ObjRef chain the compiler emits:
|
||||
|
||||
@ -84,7 +84,7 @@ class Script(
|
||||
seedModuleSlots(moduleTarget)
|
||||
}
|
||||
moduleBytecode?.let { fn ->
|
||||
return CmdVm().execute(fn, scope, scope.args.list)
|
||||
return CmdVm().execute(fn, scope, scope.args)
|
||||
}
|
||||
var lastResult: Obj = ObjVoid
|
||||
for (s in statements) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@
|
||||
|
||||
package net.sergeych.lyng.bytecode
|
||||
|
||||
import net.sergeych.lyng.ArgsDeclaration
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Visibility
|
||||
import net.sergeych.lyng.obj.ListLiteralRef
|
||||
@ -30,11 +31,32 @@ sealed class BytecodeConst {
|
||||
data class StringVal(val value: String) : BytecodeConst()
|
||||
data class PosVal(val pos: Pos) : 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 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 CaptureTable(val entries: List<BytecodeCaptureEntry>) : BytecodeConst()
|
||||
data class ExtensionPropertyDecl(
|
||||
val extTypeName: String,
|
||||
val property: ObjProperty,
|
||||
|
||||
@ -40,9 +40,23 @@ class BytecodeFrame(
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ package net.sergeych.lyng.bytecode
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ValueFnRef
|
||||
|
||||
class BytecodeStatement private constructor(
|
||||
val original: Statement,
|
||||
@ -29,7 +30,7 @@ class BytecodeStatement private constructor(
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
scope.pos = pos
|
||||
return CmdVm().execute(function, scope, scope.args.list)
|
||||
return CmdVm().execute(function, scope, scope.args)
|
||||
}
|
||||
|
||||
internal fun bytecodeFunction(): CmdFunction = function
|
||||
@ -43,6 +44,8 @@ class BytecodeStatement private constructor(
|
||||
rangeLocalNames: Set<String> = emptySet(),
|
||||
allowedScopeNames: Set<String>? = null,
|
||||
moduleScopeId: Int? = null,
|
||||
forcedLocalSlots: Map<String, Int> = emptyMap(),
|
||||
forcedLocalScopeId: Int? = null,
|
||||
slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
knownNameObjClass: Map<String, ObjClass> = emptyMap(),
|
||||
knownObjectNames: Set<String> = emptySet(),
|
||||
@ -50,6 +53,7 @@ class BytecodeStatement private constructor(
|
||||
enumEntriesByName: Map<String, List<String>> = emptyMap(),
|
||||
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
||||
): Statement {
|
||||
if (statement is BytecodeStatement) return statement
|
||||
val hasUnsupported = containsUnsupportedStatement(statement)
|
||||
@ -67,13 +71,16 @@ class BytecodeStatement private constructor(
|
||||
rangeLocalNames = rangeLocalNames,
|
||||
allowedScopeNames = allowedScopeNames,
|
||||
moduleScopeId = moduleScopeId,
|
||||
forcedLocalSlots = forcedLocalSlots,
|
||||
forcedLocalScopeId = forcedLocalScopeId,
|
||||
slotTypeByScopeId = slotTypeByScopeId,
|
||||
knownNameObjClass = knownNameObjClass,
|
||||
knownObjectNames = knownObjectNames,
|
||||
classFieldTypesByName = classFieldTypesByName,
|
||||
enumEntriesByName = enumEntriesByName,
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
)
|
||||
val compiled = compiler.compileStatement(nameHint, statement)
|
||||
val fn = compiled ?: throw BytecodeCompileException(
|
||||
@ -132,6 +139,7 @@ class BytecodeStatement private constructor(
|
||||
target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false
|
||||
is net.sergeych.lyng.ThrowStatement ->
|
||||
containsUnsupportedStatement(target.throwExpr)
|
||||
is net.sergeych.lyng.NopStatement -> false
|
||||
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> false
|
||||
is net.sergeych.lyng.ClassDeclStatement -> false
|
||||
is net.sergeych.lyng.FunctionDeclStatement -> false
|
||||
@ -208,6 +216,7 @@ class BytecodeStatement private constructor(
|
||||
stmt.label,
|
||||
stmt.canBreak,
|
||||
stmt.loopSlotPlan,
|
||||
stmt.loopScopeId,
|
||||
stmt.pos
|
||||
)
|
||||
}
|
||||
|
||||
@ -68,7 +68,9 @@ class CmdBuilder {
|
||||
scopeSlotNames: Array<String?> = emptyArray(),
|
||||
scopeSlotIsModule: BooleanArray = BooleanArray(0),
|
||||
localSlotNames: Array<String?> = emptyArray(),
|
||||
localSlotMutables: BooleanArray = BooleanArray(0)
|
||||
localSlotMutables: BooleanArray = BooleanArray(0),
|
||||
localSlotDelegated: BooleanArray = BooleanArray(0),
|
||||
localSlotCaptures: BooleanArray = BooleanArray(0)
|
||||
): CmdFunction {
|
||||
val scopeSlotCount = scopeSlotIndices.size
|
||||
require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) {
|
||||
@ -78,6 +80,8 @@ class CmdBuilder {
|
||||
"scope slot module mapping 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>()
|
||||
for ((label, idx) in labelPositions) {
|
||||
labelIps[label] = idx
|
||||
@ -111,6 +115,8 @@ class CmdBuilder {
|
||||
scopeSlotIsModule = if (scopeSlotIsModule.isEmpty()) BooleanArray(scopeSlotCount) else scopeSlotIsModule,
|
||||
localSlotNames = localSlotNames,
|
||||
localSlotMutables = localSlotMutables,
|
||||
localSlotDelegated = localSlotDelegated,
|
||||
localSlotCaptures = localSlotCaptures,
|
||||
constants = constPool.toList(),
|
||||
cmds = cmds.toTypedArray(),
|
||||
posByIp = posByInstr.toTypedArray()
|
||||
@ -123,7 +129,7 @@ class CmdBuilder {
|
||||
Opcode.CLEAR_PENDING_THROWABLE, Opcode.RETHROW_PENDING -> emptyList()
|
||||
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.OBJ_TO_BOOL,
|
||||
Opcode.OBJ_TO_BOOL, Opcode.GET_OBJ_CLASS,
|
||||
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT,
|
||||
Opcode.ASSERT_IS ->
|
||||
listOf(OperandKind.SLOT, OperandKind.SLOT)
|
||||
@ -135,19 +141,27 @@ class CmdBuilder {
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||
Opcode.RESOLVE_SCOPE_SLOT ->
|
||||
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 ->
|
||||
listOf(OperandKind.ADDR, OperandKind.SLOT)
|
||||
Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR ->
|
||||
listOf(OperandKind.SLOT, OperandKind.ADDR)
|
||||
Opcode.CONST_NULL ->
|
||||
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)
|
||||
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
||||
listOf(OperandKind.CONST)
|
||||
Opcode.PUSH_TRY ->
|
||||
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
|
||||
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 ->
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||
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_BOOL -> CmdConstBool(operands[0], operands[1])
|
||||
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.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.LOAD_THIS -> CmdLoadThis(operands[0])
|
||||
Opcode.LOAD_THIS_VARIANT -> CmdLoadThisVariant(operands[0], operands[1])
|
||||
@ -254,6 +269,9 @@ class CmdBuilder {
|
||||
Opcode.THROW -> CmdThrow(operands[0], operands[1])
|
||||
Opcode.RETHROW_PENDING -> CmdRethrowPending()
|
||||
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.STORE_OBJ_ADDR -> CmdStoreObjAddr(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_DELEGATED -> CmdDeclDelegated(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.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3])
|
||||
Opcode.ASSIGN_DESTRUCTURE -> CmdAssignDestructure(operands[0], operands[1])
|
||||
|
||||
@ -50,6 +50,23 @@ object CmdDisassembler {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
@ -69,9 +86,10 @@ object CmdDisassembler {
|
||||
is CmdLoadThis -> Opcode.LOAD_THIS to intArrayOf(cmd.dst)
|
||||
is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, 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 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 CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
|
||||
is CmdMakeQualifiedView -> Opcode.MAKE_QUALIFIED_VIEW to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
|
||||
@ -84,6 +102,9 @@ object CmdDisassembler {
|
||||
cmd.dst
|
||||
)
|
||||
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 CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot)
|
||||
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 CmdDeclDelegated -> Opcode.DECL_DELEGATED 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 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)
|
||||
@ -231,7 +255,7 @@ object CmdDisassembler {
|
||||
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 ->
|
||||
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)
|
||||
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
|
||||
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
||||
@ -243,19 +267,27 @@ object CmdDisassembler {
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||
Opcode.RESOLVE_SCOPE_SLOT ->
|
||||
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 ->
|
||||
listOf(OperandKind.ADDR, OperandKind.SLOT)
|
||||
Opcode.STORE_OBJ_ADDR, Opcode.STORE_INT_ADDR, Opcode.STORE_REAL_ADDR, Opcode.STORE_BOOL_ADDR ->
|
||||
listOf(OperandKind.SLOT, OperandKind.ADDR)
|
||||
Opcode.CONST_NULL ->
|
||||
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)
|
||||
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
||||
listOf(OperandKind.CONST)
|
||||
Opcode.PUSH_TRY ->
|
||||
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
|
||||
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 ->
|
||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT,
|
||||
|
||||
@ -28,6 +28,8 @@ data class CmdFunction(
|
||||
val scopeSlotIsModule: BooleanArray,
|
||||
val localSlotNames: Array<String?>,
|
||||
val localSlotMutables: BooleanArray,
|
||||
val localSlotDelegated: BooleanArray,
|
||||
val localSlotCaptures: BooleanArray,
|
||||
val constants: List<BytecodeConst>,
|
||||
val cmds: Array<Cmd>,
|
||||
val posByIp: Array<net.sergeych.lyng.Pos?>,
|
||||
@ -37,10 +39,31 @@ data class CmdFunction(
|
||||
require(scopeSlotNames.size == scopeSlotCount) { "scopeSlotNames size mismatch" }
|
||||
require(scopeSlotIsModule.size == scopeSlotCount) { "scopeSlotIsModule 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(addrCount >= 0) { "addrCount must be non-negative" }
|
||||
if (posByIp.isNotEmpty()) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
)
|
||||
@ -31,7 +31,6 @@ enum class Opcode(val code: Int) {
|
||||
RANGE_INT_BOUNDS(0x0B),
|
||||
MAKE_RANGE(0x0C),
|
||||
LOAD_THIS(0x0D),
|
||||
MAKE_VALUE_FN(0x0E),
|
||||
LOAD_THIS_VARIANT(0x0F),
|
||||
|
||||
INT_TO_REAL(0x10),
|
||||
@ -42,6 +41,8 @@ enum class Opcode(val code: Int) {
|
||||
CHECK_IS(0x15),
|
||||
ASSERT_IS(0x16),
|
||||
MAKE_QUALIFIED_VIEW(0x17),
|
||||
MAKE_LAMBDA_FN(0x18),
|
||||
GET_OBJ_CLASS(0x19),
|
||||
|
||||
ADD_INT(0x20),
|
||||
SUB_INT(0x21),
|
||||
@ -158,9 +159,15 @@ enum class Opcode(val code: Int) {
|
||||
STORE_BOOL_ADDR(0xB9),
|
||||
THROW(0xBB),
|
||||
RETHROW_PENDING(0xBC),
|
||||
DECL_ENUM(0xBE),
|
||||
ITER_PUSH(0xBF),
|
||||
ITER_POP(0xC0),
|
||||
ITER_CANCEL(0xC1),
|
||||
DELEGATED_GET_LOCAL(0xC2),
|
||||
DELEGATED_SET_LOCAL(0xC3),
|
||||
BIND_DELEGATE_LOCAL(0xC4),
|
||||
DECL_FUNCTION(0xC5),
|
||||
DECL_CLASS(0xC6),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@ -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)
|
||||
@ -18,7 +18,6 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.ClosureScope
|
||||
import net.sergeych.lyng.Scope
|
||||
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.
|
||||
*/
|
||||
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 {
|
||||
if (writeCallback != null)
|
||||
it.asMutable
|
||||
@ -83,26 +82,26 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
args: Arguments,
|
||||
onNotFoundResult: (suspend () -> 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))))
|
||||
return over?.invoke(scope, scope.thisObj, args)
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
}
|
||||
|
||||
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)))
|
||||
?: super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
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)))
|
||||
?: super.getAt(scope, index)
|
||||
}
|
||||
|
||||
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)))
|
||||
?: super.putAt(scope, index, newValue)
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.ClosureScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Statement
|
||||
|
||||
@ -35,9 +34,9 @@ class ObjProperty(
|
||||
suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj {
|
||||
val g = getter ?: scope.raiseError("property $name has no getter")
|
||||
// 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 execScope = ClosureScope(scope, instanceScope).createChildScope(newThisObj = instance)
|
||||
val execScope = scope.applyClosure(instanceScope).createChildScope(newThisObj = instance)
|
||||
execScope.currentClassCtx = declaringClass
|
||||
return g.execute(execScope)
|
||||
}
|
||||
@ -45,9 +44,9 @@ class ObjProperty(
|
||||
suspend fun callSetter(scope: Scope, instance: Obj, value: Obj, declaringClass: ObjClass? = null) {
|
||||
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
|
||||
// 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 execScope = ClosureScope(scope, instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
|
||||
val execScope = scope.applyClosure(instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
|
||||
execScope.currentClassCtx = declaringClass
|
||||
s.execute(execScope)
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.FrameSlotRef
|
||||
import net.sergeych.lyng.RecordSlotRef
|
||||
|
||||
/**
|
||||
* A reference to a value with optional write-back path.
|
||||
@ -43,7 +45,12 @@ sealed interface ObjRef {
|
||||
if (rec.receiver != null && rec.declaringClass != null) {
|
||||
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) {
|
||||
throw ScriptError(pos, "can't assign value")
|
||||
@ -65,12 +72,19 @@ sealed interface ObjRef {
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
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. */
|
||||
enum class UnaryOp { NOT, NEGATE, BITNOT }
|
||||
|
||||
@ -1971,6 +1985,7 @@ class BoundLocalVarRef(
|
||||
private val slot: Int,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
internal fun slotIndex(): Int = slot
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
|
||||
@ -99,6 +99,7 @@ class ForInStatement(
|
||||
val label: String?,
|
||||
val canBreak: Boolean,
|
||||
val loopSlotPlan: Map<String, Int>,
|
||||
val loopScopeId: Int,
|
||||
override val pos: Pos,
|
||||
) : Statement() {
|
||||
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(
|
||||
val ref: net.sergeych.lyng.obj.ObjRef,
|
||||
override val pos: Pos
|
||||
|
||||
@ -16,8 +16,22 @@
|
||||
*/
|
||||
|
||||
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.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.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BytecodeRecentOpsTest {
|
||||
|
||||
@ -88,4 +102,300 @@ class BytecodeRecentOpsTest {
|
||||
""".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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,12 @@ extern class List<T> : Array<T> {
|
||||
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> {
|
||||
}
|
||||
|
||||
@ -270,8 +276,10 @@ fun Iterable<T>.shuffled(): List<T> {
|
||||
*/
|
||||
fun Iterable<Iterable<T>>.flatten(): List<T> {
|
||||
var result: List<T> = List()
|
||||
forEach { i ->
|
||||
i.forEach { result += it }
|
||||
for (i in this) {
|
||||
for (item in i) {
|
||||
result += item
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user