From 059e36678768f887ec8f3d96e24ec841cd56ad78 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 26 Jan 2026 01:09:02 +0300 Subject: [PATCH] Add bytecode slot metadata and compile-time mutability --- docs/BytecodeSpec.md | 12 + .../kotlin/net/sergeych/lyng/Compiler.kt | 124 +++++-- .../sergeych/lyng/bytecode/BytecodeBuilder.kt | 23 +- .../lyng/bytecode/BytecodeCompiler.kt | 145 ++++++++- .../lyng/bytecode/BytecodeDisassembler.kt | 4 +- .../lyng/bytecode/BytecodeFunction.kt | 7 + .../lyng/bytecode/BytecodeStatement.kt | 50 +++ .../net/sergeych/lyng/bytecode/BytecodeVm.kt | 302 +++++++++++++----- .../net/sergeych/lyng/bytecode/Opcode.kt | 5 + .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 2 + .../src/commonTest/kotlin/BytecodeVmTest.kt | 41 ++- 11 files changed, 577 insertions(+), 138 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt diff --git a/docs/BytecodeSpec.md b/docs/BytecodeSpec.md index e92dd74..9eccaa8 100644 --- a/docs/BytecodeSpec.md +++ b/docs/BytecodeSpec.md @@ -10,6 +10,7 @@ interpreter and fall back to the existing AST execution when needed. ### Frame metadata - localCount: number of local slots for this function (fixed at compile time). - argCount: number of arguments passed at call time. +- scopeSlotNames: optional debug names for scope slots (locals/params), aligned to slot mapping. - argBase = localCount. ### Slot layout @@ -25,6 +26,10 @@ slots[localCount .. localCount+argCount-1] arguments - param i => slot localCount + i - variadic extra => slot localCount + declaredParamCount + k +### Debug metadata (optional) +- scopeSlotNames: array sized scopeSlotCount, each entry nullable. +- Intended for disassembly/debug tooling; VM semantics do not depend on it. + ## 2) Slot ID Width Per frame, select: @@ -113,6 +118,13 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass. - DIV_REAL S, S -> S - NEG_REAL S -> S +### Arithmetic: OBJ +- ADD_OBJ S, S -> S +- SUB_OBJ S, S -> S +- MUL_OBJ S, S -> S +- DIV_OBJ S, S -> S +- MOD_OBJ S, S -> S + ### Bitwise: INT - AND_INT S, S -> S - OR_INT S, S -> S diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index cf812e1..975867d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -18,6 +18,7 @@ package net.sergeych.lyng import net.sergeych.lyng.Compiler.Companion.compile +import net.sergeych.lyng.bytecode.BytecodeStatement import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager @@ -44,8 +45,9 @@ class Compiler( private val currentLocalNames: MutableSet? get() = localNamesStack.lastOrNull() - private data class SlotPlan(val slots: MutableMap, var nextIndex: Int) - private data class SlotLocation(val slot: Int, val depth: Int) + private data class SlotEntry(val index: Int, val isMutable: Boolean, val isDelegated: Boolean) + private data class SlotPlan(val slots: MutableMap, var nextIndex: Int) + private data class SlotLocation(val slot: Int, val depth: Int, val isMutable: Boolean, val isDelegated: Boolean) private val slotPlanStack = mutableListOf() // Track declared local variables count per function for precise capacity hints @@ -62,19 +64,20 @@ class Compiler( } } - private fun declareLocalName(name: String) { + private fun declareLocalName(name: String, isMutable: Boolean, isDelegated: Boolean = false) { // Add to current function's local set; only count if it was newly added (avoid duplicates) val added = currentLocalNames?.add(name) == true if (added && localDeclCountStack.isNotEmpty()) { localDeclCountStack[localDeclCountStack.lastIndex] = currentLocalDeclCount + 1 } - declareSlotName(name) + declareSlotName(name, isMutable, isDelegated) } - private fun declareSlotName(name: String) { + private fun declareSlotName(name: String, isMutable: Boolean, isDelegated: Boolean) { + if (codeContexts.lastOrNull() is CodeContext.ClassBody) return val plan = slotPlanStack.lastOrNull() ?: return if (plan.slots.containsKey(name)) return - plan.slots[name] = plan.nextIndex + plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated) plan.nextIndex += 1 } @@ -87,14 +90,38 @@ class Compiler( idx++ } } - return SlotPlan(map, idx) + val entries = mutableMapOf() + for ((name, index) in map) { + entries[name] = SlotEntry(index, isMutable = false, isDelegated = false) + } + return SlotPlan(entries, idx) + } + + private fun markDelegatedSlot(name: String) { + val plan = slotPlanStack.lastOrNull() ?: return + val entry = plan.slots[name] ?: return + if (!entry.isDelegated) { + plan.slots[name] = entry.copy(isDelegated = true) + } + } + + private fun slotPlanIndices(plan: SlotPlan): Map { + if (plan.slots.isEmpty()) return emptyMap() + val result = LinkedHashMap(plan.slots.size) + for ((name, entry) in plan.slots) { + result[name] = entry.index + } + return result } private fun lookupSlotLocation(name: String): SlotLocation? { for (i in slotPlanStack.indices.reversed()) { val slot = slotPlanStack[i].slots[name] ?: continue val depth = slotPlanStack.size - 1 - i - return SlotLocation(slot, depth) + if (codeContexts.any { it is CodeContext.ClassBody } && depth > 1) { + return null + } + return SlotLocation(slot.index, depth, slot.isMutable, slot.isDelegated) } return null } @@ -339,6 +366,12 @@ class Compiler( private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null private var isTransientFlag: Boolean = false private var lastLabel: String? = null + private val useBytecodeStatements: Boolean = true + + private fun wrapBytecode(stmt: Statement): Statement { + if (!useBytecodeStatements) return stmt + return BytecodeStatement.wrap(stmt, "stmt@${stmt.pos}", allowLocalSlots = true) + } private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { lastAnnotation = null @@ -348,16 +381,16 @@ class Compiler( val t = cc.next() return when (t.type) { Token.Type.ID, Token.Type.OBJECT -> { - parseKeywordStatement(t) + parseKeywordStatement(t)?.let { wrapBytecode(it) } ?: run { cc.previous() - parseExpression() + parseExpression()?.let { wrapBytecode(it) } } } Token.Type.PLUS2, Token.Type.MINUS2 -> { cc.previous() - parseExpression() + parseExpression()?.let { wrapBytecode(it) } } Token.Type.ATLABEL -> { @@ -389,9 +422,9 @@ class Compiler( Token.Type.LBRACE -> { cc.previous() if (braceMeansLambda) - parseExpression() + parseExpression()?.let { wrapBytecode(it) } else - parseBlock() + wrapBytecode(parseBlock()) } Token.Type.RBRACE, Token.Type.RBRACKET -> { @@ -404,7 +437,7 @@ class Compiler( else -> { // could be expression cc.previous() - parseExpression() + parseExpression()?.let { wrapBytecode(it) } } } } @@ -800,7 +833,7 @@ class Compiler( } label?.let { cc.labels.remove(it) } - val paramSlotPlanSnapshot = if (paramSlotPlan.slots.isEmpty()) emptyMap() else paramSlotPlan.slots.toMap() + val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) return ValueFnRef { closureScope -> val stmt = object : Statement() { override val pos: Pos = body.pos @@ -1424,7 +1457,14 @@ class Compiler( val slotLoc = lookupSlotLocation(t.value) val inClassCtx = codeContexts.any { it is CodeContext.ClassBody } when { - slotLoc != null -> LocalSlotRef(t.value, slotLoc.slot, slotLoc.depth, t.pos) + slotLoc != null -> LocalSlotRef( + t.value, + slotLoc.slot, + slotLoc.depth, + slotLoc.isMutable, + slotLoc.isDelegated, + t.pos + ) PerfFlags.EMIT_FAST_LOCAL_REFS && (currentLocalNames?.contains(t.value) == true) -> FastLocalVarRef(t.value, t.pos) inClassCtx -> ImplicitThisMemberRef(t.value, t.pos) @@ -1798,7 +1838,7 @@ class Compiler( private suspend fun parseWhenStatement(): Statement { // has a value, when(value) ? var t = cc.nextNonWhitespace() - return if (t.type == Token.Type.LPAREN) { + val stmt = if (t.type == Token.Type.LPAREN) { // when(value) val value = parseStatement() ?: throw ScriptError(cc.currentPos(), "when(value) expected") cc.skipTokenOfType(Token.Type.RPAREN) @@ -1919,13 +1959,14 @@ class Compiler( // when { cond -> ... } TODO("when without object is not yet implemented") } + return wrapBytecode(stmt) } private suspend fun parseThrowStatement(start: Pos): Statement { val throwStatement = parseStatement() ?: throw ScriptError(cc.currentPos(), "throw object expected") // Important: bind the created statement to the position of the `throw` keyword so that // any raised error reports the correct source location. - return object : Statement() { + val stmt = object : Statement() { override val pos: Pos = start override suspend fun execute(scope: Scope): Obj { var errorObject = throwStatement.execute(scope) @@ -1953,6 +1994,7 @@ class Compiler( return ObjVoid } } + return wrapBytecode(stmt) } private data class CatchBlockData( @@ -2185,8 +2227,14 @@ class Compiler( miniSink?.onEnterClass(node) } val bodyStart = nextBody.pos - val st = withLocalNames(emptySet()) { - parseScript() + val classSlotPlan = SlotPlan(mutableMapOf(), 0) + slotPlanStack.add(classSlotPlan) + val st = try { + withLocalNames(emptySet()) { + parseScript() + } + } finally { + slotPlanStack.removeLast() } val rbTok = cc.next() if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in object body") @@ -2324,8 +2372,14 @@ class Compiler( } // parse body val bodyStart = next.pos - val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) { - parseScript() + val classSlotPlan = SlotPlan(mutableMapOf(), 0) + slotPlanStack.add(classSlotPlan) + val st = try { + withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) { + parseScript() + } + } finally { + slotPlanStack.removeLast() } val rbTok = cc.next() if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body") @@ -2480,7 +2534,7 @@ class Compiler( val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value val loopSlotPlan = SlotPlan(mutableMapOf(), 0) slotPlanStack.add(loopSlotPlan) - declareSlotName(tVar.value) + declareSlotName(tVar.value, isMutable = true, isDelegated = false) val (canBreak, body, elseStatement) = try { withLocalNames(namesForLoop) { val loopParsed = cc.parseLoop { @@ -2500,7 +2554,7 @@ class Compiler( } finally { slotPlanStack.removeLast() } - val loopSlotPlanSnapshot = if (loopSlotPlan.slots.isEmpty()) emptyMap() else loopSlotPlan.slots.toMap() + val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan) return object : Statement() { override val pos: Pos = body.pos @@ -2805,7 +2859,7 @@ class Compiler( var result: Obj = ObjVoid var wasBroken = false while (condition.execute(scope).toBool()) { - val loopScope = scope.createChildScope() + val loopScope = scope.createChildScope().apply { skipScopeCreation = true } if (canBreak) { try { result = body.execute(loopScope) @@ -2962,7 +3016,7 @@ class Compiler( val t2 = cc.nextNonWhitespace() // we generate different statements: optimization - return if (t2.type == Token.Type.ID && t2.value == "else") { + val stmt = if (t2.type == Token.Type.ID && t2.value == "else") { val elseBody = parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement") IfStatement(condition, ifBody, elseBody, start) @@ -2970,6 +3024,7 @@ class Compiler( cc.previous() IfStatement(condition, ifBody, null, start) } + return wrapBytecode(stmt) } private suspend fun parseFunctionDeclaration( @@ -3115,7 +3170,7 @@ class Compiler( var closure: Scope? = null - val paramSlotPlanSnapshot = if (paramSlotPlan.slots.isEmpty()) emptyMap() else paramSlotPlan.slots.toMap() + val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) val fnBody = object : Statement() { override val pos: Pos = t.pos override suspend fun execute(callerContext: Scope): Obj { @@ -3323,7 +3378,7 @@ class Compiler( } finally { slotPlanStack.removeLast() } - val planSnapshot = if (blockSlotPlan.slots.isEmpty()) emptyMap() else blockSlotPlan.slots.toMap() + val planSnapshot = slotPlanIndices(blockSlotPlan) val stmt = object : Statement() { override val pos: Pos = startPos override suspend fun execute(scope: Scope): Obj { @@ -3333,7 +3388,8 @@ class Compiler( return block.execute(target) } } - return stmt.also { + val wrapped = wrapBytecode(stmt) + return wrapped.also { val t1 = cc.next() if (t1.type != Token.Type.RBRACE) throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }") @@ -3368,7 +3424,7 @@ class Compiler( // Register all names in the pattern pattern.forEachVariableWithPos { name, namePos -> - declareLocalName(name) + declareLocalName(name, isMutable) val declRange = MiniRange(namePos, namePos) val node = MiniValDecl( range = declRange, @@ -3526,7 +3582,7 @@ class Compiler( val effectiveEqToken = if (isProperty) null else eqToken // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals - if (!isStatic) declareLocalName(name) + if (!isStatic) declareLocalName(name, isMutable) val isDelegate = if (isAbstract || actualExtern) { if (!isProperty && (effectiveEqToken?.type == Token.Type.ASSIGN || effectiveEqToken?.type == Token.Type.BY)) @@ -3561,6 +3617,10 @@ class Compiler( else parseStatement(true) ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression") + if (!isStatic && isDelegate) { + markDelegatedSlot(name) + } + // Emit MiniValDecl for this declaration (before execution wiring), attach doc if any run { val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos()) @@ -3839,7 +3899,7 @@ class Compiler( } // Register the local name so subsequent identifiers can be emitted as fast locals - if (!isStatic) declareLocalName(name) + if (!isStatic) declareLocalName(name, isMutable) if (isDelegate) { val declaringClassName = declaringClassNameCaptured diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt index edda073..ca5c8d6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -56,10 +56,22 @@ class BytecodeBuilder { return fallbackStatements.lastIndex } - fun build(name: String, localCount: Int): BytecodeFunction { + fun build( + name: String, + localCount: Int, + scopeSlotDepths: IntArray = IntArray(0), + scopeSlotIndices: IntArray = IntArray(0), + scopeSlotNames: Array = emptyArray() + ): BytecodeFunction { + val scopeSlotCount = scopeSlotDepths.size + require(scopeSlotIndices.size == scopeSlotCount) { "scope slot mapping size mismatch" } + require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) { + "scope slot name mapping size mismatch" + } + val totalSlots = localCount + scopeSlotCount val slotWidth = when { - localCount < 256 -> 1 - localCount < 65536 -> 2 + totalSlots < 256 -> 1 + totalSlots < 65536 -> 2 else -> 4 } val constIdWidth = if (constPool.size < 65536) 2 else 4 @@ -102,6 +114,10 @@ class BytecodeBuilder { return BytecodeFunction( name = name, localCount = localCount, + scopeSlotCount = scopeSlotCount, + scopeSlotDepths = scopeSlotDepths, + scopeSlotIndices = scopeSlotIndices, + scopeSlotNames = if (scopeSlotNames.isEmpty()) Array(scopeSlotCount) { null } else scopeSlotNames, slotWidth = slotWidth, ipWidth = ipWidth, constIdWidth = constIdWidth, @@ -135,6 +151,7 @@ class BytecodeBuilder { Opcode.CMP_GTE_INT_REAL, Opcode.CMP_GTE_REAL_INT, Opcode.CMP_NEQ_INT_REAL, Opcode.CMP_NEQ_REAL_INT, Opcode.CMP_EQ_OBJ, Opcode.CMP_NEQ_OBJ, Opcode.CMP_REF_EQ_OBJ, Opcode.CMP_REF_NEQ_OBJ, Opcode.CMP_LT_OBJ, Opcode.CMP_LTE_OBJ, Opcode.CMP_GT_OBJ, Opcode.CMP_GTE_OBJ, + Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.AND_BOOL, Opcode.OR_BOOL -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 5e168c5..a8f693d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -23,12 +23,21 @@ import net.sergeych.lyng.Statement import net.sergeych.lyng.ToBoolStatement import net.sergeych.lyng.obj.* -class BytecodeCompiler { - private val builder = BytecodeBuilder() +class BytecodeCompiler( + private val allowLocalSlots: Boolean = true, +) { + private var builder = BytecodeBuilder() private var nextSlot = 0 + private var scopeSlotCount = 0 + private var scopeSlotDepths = IntArray(0) + private var scopeSlotIndices = IntArray(0) + private var scopeSlotNames = emptyArray() + private val scopeSlotMap = LinkedHashMap() + private val scopeSlotNameMap = LinkedHashMap() private val slotTypes = mutableMapOf() fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): BytecodeFunction? { + prepareCompilation(stmt) return when (stmt) { is ExpressionStatement -> compileExpression(name, stmt) is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) @@ -37,10 +46,11 @@ class BytecodeCompiler { } fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? { + prepareCompilation(stmt) val value = compileRefWithFallback(stmt.ref, null, stmt.pos) ?: return null builder.emit(Opcode.RET, value.slot) - val localCount = maxOf(nextSlot, value.slot + 1) - return builder.build(name, localCount) + val localCount = maxOf(nextSlot, value.slot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) } private data class CompiledValue(val slot: Int, val type: SlotType) @@ -51,9 +61,11 @@ class BytecodeCompiler { return when (ref) { is ConstRef -> compileConst(ref.constValue) is LocalSlotRef -> { + if (!allowLocalSlots) return null + if (ref.isDelegated) return null if (ref.name.isEmpty()) return null - if (refDepth(ref) != 0) return null - CompiledValue(refSlot(ref), slotTypes[refSlot(ref)] ?: SlotType.UNKNOWN) + val mapped = scopeSlotMap[ScopeSlotKey(refDepth(ref), refSlot(ref))] ?: return null + CompiledValue(mapped, slotTypes[mapped] ?: SlotType.UNKNOWN) } is BinaryOpRef -> compileBinary(ref) is UnaryOpRef -> compileUnary(ref) @@ -146,6 +158,7 @@ class BytecodeCompiler { CompiledValue(out, SlotType.INT) } SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.ADD_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } @@ -156,9 +169,15 @@ class BytecodeCompiler { CompiledValue(out, SlotType.REAL) } SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.ADD_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } + SlotType.OBJ -> { + if (b.type != SlotType.OBJ) return null + builder.emit(Opcode.ADD_OBJ, a.slot, b.slot, out) + CompiledValue(out, SlotType.OBJ) + } else -> null } BinOp.MINUS -> when (a.type) { @@ -169,6 +188,7 @@ class BytecodeCompiler { CompiledValue(out, SlotType.INT) } SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.SUB_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } @@ -179,9 +199,15 @@ class BytecodeCompiler { CompiledValue(out, SlotType.REAL) } SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.SUB_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } + SlotType.OBJ -> { + if (b.type != SlotType.OBJ) return null + builder.emit(Opcode.SUB_OBJ, a.slot, b.slot, out) + CompiledValue(out, SlotType.OBJ) + } else -> null } BinOp.STAR -> when (a.type) { @@ -192,6 +218,7 @@ class BytecodeCompiler { CompiledValue(out, SlotType.INT) } SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.MUL_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } @@ -202,9 +229,15 @@ class BytecodeCompiler { CompiledValue(out, SlotType.REAL) } SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.MUL_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } + SlotType.OBJ -> { + if (b.type != SlotType.OBJ) return null + builder.emit(Opcode.MUL_OBJ, a.slot, b.slot, out) + CompiledValue(out, SlotType.OBJ) + } else -> null } BinOp.SLASH -> when (a.type) { @@ -215,6 +248,7 @@ class BytecodeCompiler { CompiledValue(out, SlotType.INT) } SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.DIV_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } @@ -225,15 +259,31 @@ class BytecodeCompiler { CompiledValue(out, SlotType.REAL) } SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.DIV_REAL, a, b, out) + SlotType.OBJ -> null else -> null } } + SlotType.OBJ -> { + if (b.type != SlotType.OBJ) return null + builder.emit(Opcode.DIV_OBJ, a.slot, b.slot, out) + CompiledValue(out, SlotType.OBJ) + } else -> null } BinOp.PERCENT -> { - if (a.type != SlotType.INT) return null - builder.emit(Opcode.MOD_INT, a.slot, b.slot, out) - CompiledValue(out, SlotType.INT) + return when (a.type) { + SlotType.INT -> { + if (b.type != SlotType.INT) return null + builder.emit(Opcode.MOD_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.OBJ -> { + if (b.type != SlotType.OBJ) return null + builder.emit(Opcode.MOD_OBJ, a.slot, b.slot, out) + CompiledValue(out, SlotType.OBJ) + } + else -> null + } } BinOp.EQ -> { compileCompareEq(a, b, out) @@ -522,9 +572,11 @@ class BytecodeCompiler { private fun compileAssign(ref: AssignRef): CompiledValue? { val target = assignTarget(ref) ?: return null - if (refDepth(target) != 0) return null + if (!allowLocalSlots) return null + if (!target.isMutable || target.isDelegated) return null + if (refDepth(target) > 0) return null val value = compileRef(assignValue(ref)) ?: return null - val slot = refSlot(target) + val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null when (value.type) { SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot) SlotType.REAL -> builder.emit(Opcode.MOVE_REAL, value.slot, slot) @@ -563,8 +615,8 @@ class BytecodeCompiler { builder.mark(endLabel) builder.emit(Opcode.RET, resultSlot) - val localCount = maxOf(nextSlot, resultSlot + 1) - return builder.build(name, localCount) + val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) } private fun compileStatementValue(stmt: Statement): CompiledValue? { @@ -620,4 +672,71 @@ class BytecodeCompiler { slotTypes[slot] = type } } + + private fun prepareCompilation(stmt: Statement) { + builder = BytecodeBuilder() + nextSlot = 0 + slotTypes.clear() + scopeSlotMap.clear() + if (allowLocalSlots) { + collectScopeSlots(stmt) + } + scopeSlotCount = scopeSlotMap.size + scopeSlotDepths = IntArray(scopeSlotCount) + scopeSlotIndices = IntArray(scopeSlotCount) + scopeSlotNames = arrayOfNulls(scopeSlotCount) + for ((key, index) in scopeSlotMap) { + scopeSlotDepths[index] = key.depth + scopeSlotIndices[index] = key.slot + scopeSlotNames[index] = scopeSlotNameMap[key] + } + nextSlot = scopeSlotCount + } + + private fun collectScopeSlots(stmt: Statement) { + when (stmt) { + is ExpressionStatement -> collectScopeSlotsRef(stmt.ref) + is IfStatement -> { + collectScopeSlots(stmt.condition) + collectScopeSlots(stmt.ifBody) + stmt.elseBody?.let { collectScopeSlots(it) } + } + else -> {} + } + } + + private fun collectScopeSlotsRef(ref: ObjRef) { + when (ref) { + is LocalSlotRef -> { + val key = ScopeSlotKey(refDepth(ref), refSlot(ref)) + if (!scopeSlotMap.containsKey(key)) { + scopeSlotMap[key] = scopeSlotMap.size + } + if (!scopeSlotNameMap.containsKey(key)) { + scopeSlotNameMap[key] = ref.name + } + } + is BinaryOpRef -> { + collectScopeSlotsRef(binaryLeft(ref)) + collectScopeSlotsRef(binaryRight(ref)) + } + is UnaryOpRef -> collectScopeSlotsRef(unaryOperand(ref)) + is AssignRef -> { + val target = assignTarget(ref) + if (target != null) { + val key = ScopeSlotKey(refDepth(target), refSlot(target)) + if (!scopeSlotMap.containsKey(key)) { + scopeSlotMap[key] = scopeSlotMap.size + } + if (!scopeSlotNameMap.containsKey(key)) { + scopeSlotNameMap[key] = target.name + } + } + collectScopeSlotsRef(assignValue(ref)) + } + else -> {} + } + } + + private data class ScopeSlotKey(val depth: Int, val slot: Int) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt index e5c5bce..f9fd62e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt @@ -38,7 +38,8 @@ object BytecodeDisassembler { OperandKind.SLOT -> { val v = decoder.readSlot(code, ip) ip += fn.slotWidth - operands += "s$v" + val name = if (v < fn.scopeSlotCount) fn.scopeSlotNames[v] else null + operands += if (name != null) "s$v($name)" else "s$v" } OperandKind.CONST -> { val v = decoder.readConstId(code, ip, fn.constIdWidth) @@ -103,6 +104,7 @@ object BytecodeDisassembler { Opcode.CMP_GTE_INT_REAL, Opcode.CMP_GTE_REAL_INT, Opcode.CMP_NEQ_INT_REAL, Opcode.CMP_NEQ_REAL_INT, Opcode.CMP_EQ_OBJ, Opcode.CMP_NEQ_OBJ, Opcode.CMP_REF_EQ_OBJ, Opcode.CMP_REF_NEQ_OBJ, Opcode.CMP_LT_OBJ, Opcode.CMP_LTE_OBJ, Opcode.CMP_GT_OBJ, Opcode.CMP_GTE_OBJ, + Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.AND_BOOL, Opcode.OR_BOOL -> listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt index b04d0c6..3f0da78 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt @@ -19,6 +19,10 @@ package net.sergeych.lyng.bytecode data class BytecodeFunction( val name: String, val localCount: Int, + val scopeSlotCount: Int, + val scopeSlotDepths: IntArray, + val scopeSlotIndices: IntArray, + val scopeSlotNames: Array, val slotWidth: Int, val ipWidth: Int, val constIdWidth: Int, @@ -30,5 +34,8 @@ data class BytecodeFunction( require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" } require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" } require(constIdWidth == 2 || constIdWidth == 4) { "constIdWidth must be 2 or 4" } + require(scopeSlotDepths.size == scopeSlotCount) { "scopeSlotDepths size mismatch" } + require(scopeSlotIndices.size == scopeSlotCount) { "scopeSlotIndices size mismatch" } + require(scopeSlotNames.size == scopeSlotCount) { "scopeSlotNames size mismatch" } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt new file mode 100644 index 0000000..516fdb6 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -0,0 +1,50 @@ +/* + * 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 + +import net.sergeych.lyng.Pos +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Statement +import net.sergeych.lyng.obj.Obj + +class BytecodeStatement private constructor( + val original: Statement, + private val function: BytecodeFunction, +) : Statement(original.isStaticConst, original.isConst, original.returnType) { + override val pos: Pos = original.pos + + override suspend fun execute(scope: Scope): Obj { + return BytecodeVm().execute(function, scope, emptyList()) + } + + companion object { + fun wrap(statement: Statement, nameHint: String, allowLocalSlots: Boolean): Statement { + if (statement is BytecodeStatement) return statement + val compiler = BytecodeCompiler(allowLocalSlots = allowLocalSlots) + val compiled = compiler.compileStatement(nameHint, statement) + val fn = compiled ?: run { + val builder = BytecodeBuilder() + val slot = 0 + val id = builder.addFallback(statement) + builder.emit(Opcode.EVAL_FALLBACK, id, slot) + builder.emit(Opcode.RET, slot) + builder.build(nameHint, localCount = 1) + } + return BytecodeStatement(statement, fn) + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt index 1b8bc64..27d4a51 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -47,7 +47,7 @@ class BytecodeVm { ip += fn.slotWidth val c = fn.constants[constId] as? BytecodeConst.IntVal ?: error("CONST_INT expects IntVal at $constId") - frame.setInt(dst, c.value) + setInt(fn, frame, scope, dst, c.value) } Opcode.CONST_REAL -> { val constId = decoder.readConstId(code, ip, fn.constIdWidth) @@ -56,7 +56,7 @@ class BytecodeVm { ip += fn.slotWidth val c = fn.constants[constId] as? BytecodeConst.RealVal ?: error("CONST_REAL expects RealVal at $constId") - frame.setReal(dst, c.value) + setReal(fn, frame, scope, dst, c.value) } Opcode.CONST_BOOL -> { val constId = decoder.readConstId(code, ip, fn.constIdWidth) @@ -65,7 +65,7 @@ class BytecodeVm { ip += fn.slotWidth val c = fn.constants[constId] as? BytecodeConst.Bool ?: error("CONST_BOOL expects Bool at $constId") - frame.setBool(dst, c.value) + setBool(fn, frame, scope, dst, c.value) } Opcode.CONST_OBJ -> { val constId = decoder.readConstId(code, ip, fn.constIdWidth) @@ -76,76 +76,76 @@ class BytecodeVm { is BytecodeConst.ObjRef -> { val obj = c.value when (obj) { - is ObjInt -> frame.setInt(dst, obj.value) - is ObjReal -> frame.setReal(dst, obj.value) - is ObjBool -> frame.setBool(dst, obj.value) - else -> frame.setObj(dst, obj) + is ObjInt -> setInt(fn, frame, scope, dst, obj.value) + is ObjReal -> setReal(fn, frame, scope, dst, obj.value) + is ObjBool -> setBool(fn, frame, scope, dst, obj.value) + else -> setObj(fn, frame, scope, dst, obj) } } - is BytecodeConst.StringVal -> frame.setObj(dst, ObjString(c.value)) + is BytecodeConst.StringVal -> setObj(fn, frame, scope, dst, ObjString(c.value)) else -> error("CONST_OBJ expects ObjRef/StringVal at $constId") } } Opcode.CONST_NULL -> { val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setObj(dst, ObjNull) + setObj(fn, frame, scope, dst, ObjNull) } Opcode.MOVE_INT -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(src)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, src)) } Opcode.MOVE_REAL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getReal(src)) + setReal(fn, frame, scope, dst, getReal(fn, frame, scope, src)) } Opcode.MOVE_BOOL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getBool(src)) + setBool(fn, frame, scope, dst, getBool(fn, frame, scope, src)) } Opcode.MOVE_OBJ -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setObj(dst, frame.getObj(src)) + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, src)) } Opcode.INT_TO_REAL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getInt(src).toDouble()) + setReal(fn, frame, scope, dst, getInt(fn, frame, scope, src).toDouble()) } Opcode.REAL_TO_INT -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getReal(src).toLong()) + setInt(fn, frame, scope, dst, getReal(fn, frame, scope, src).toLong()) } Opcode.BOOL_TO_INT -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, if (frame.getBool(src)) 1L else 0L) + setInt(fn, frame, scope, dst, if (getBool(fn, frame, scope, src)) 1L else 0L) } Opcode.INT_TO_BOOL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(src) != 0L) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, src) != 0L) } Opcode.ADD_INT -> { val a = decoder.readSlot(code, ip) @@ -154,7 +154,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) + frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) + getInt(fn, frame, scope, b)) } Opcode.SUB_INT -> { val a = decoder.readSlot(code, ip) @@ -163,7 +163,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) - frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) - getInt(fn, frame, scope, b)) } Opcode.MUL_INT -> { val a = decoder.readSlot(code, ip) @@ -172,7 +172,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) * frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) * getInt(fn, frame, scope, b)) } Opcode.DIV_INT -> { val a = decoder.readSlot(code, ip) @@ -181,7 +181,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) / frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) / getInt(fn, frame, scope, b)) } Opcode.MOD_INT -> { val a = decoder.readSlot(code, ip) @@ -190,24 +190,24 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) % frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) % getInt(fn, frame, scope, b)) } Opcode.NEG_INT -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, -frame.getInt(src)) + setInt(fn, frame, scope, dst, -getInt(fn, frame, scope, src)) } Opcode.INC_INT -> { val slot = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(slot, frame.getInt(slot) + 1L) + setInt(fn, frame, scope, slot, getInt(fn, frame, scope, slot) + 1L) } Opcode.DEC_INT -> { val slot = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(slot, frame.getInt(slot) - 1L) + setInt(fn, frame, scope, slot, getInt(fn, frame, scope, slot) - 1L) } Opcode.ADD_REAL -> { val a = decoder.readSlot(code, ip) @@ -216,7 +216,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getReal(a) + frame.getReal(b)) + setReal(fn, frame, scope, dst, getReal(fn, frame, scope, a) + getReal(fn, frame, scope, b)) } Opcode.SUB_REAL -> { val a = decoder.readSlot(code, ip) @@ -225,7 +225,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getReal(a) - frame.getReal(b)) + setReal(fn, frame, scope, dst, getReal(fn, frame, scope, a) - getReal(fn, frame, scope, b)) } Opcode.MUL_REAL -> { val a = decoder.readSlot(code, ip) @@ -234,7 +234,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getReal(a) * frame.getReal(b)) + setReal(fn, frame, scope, dst, getReal(fn, frame, scope, a) * getReal(fn, frame, scope, b)) } Opcode.DIV_REAL -> { val a = decoder.readSlot(code, ip) @@ -243,14 +243,14 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, frame.getReal(a) / frame.getReal(b)) + setReal(fn, frame, scope, dst, getReal(fn, frame, scope, a) / getReal(fn, frame, scope, b)) } Opcode.NEG_REAL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setReal(dst, -frame.getReal(src)) + setReal(fn, frame, scope, dst, -getReal(fn, frame, scope, src)) } Opcode.AND_INT -> { val a = decoder.readSlot(code, ip) @@ -259,7 +259,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) and frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) and getInt(fn, frame, scope, b)) } Opcode.OR_INT -> { val a = decoder.readSlot(code, ip) @@ -268,7 +268,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) or frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) or getInt(fn, frame, scope, b)) } Opcode.XOR_INT -> { val a = decoder.readSlot(code, ip) @@ -277,7 +277,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) xor frame.getInt(b)) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) xor getInt(fn, frame, scope, b)) } Opcode.SHL_INT -> { val a = decoder.readSlot(code, ip) @@ -286,7 +286,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) shl frame.getInt(b).toInt()) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) shl getInt(fn, frame, scope, b).toInt()) } Opcode.SHR_INT -> { val a = decoder.readSlot(code, ip) @@ -295,7 +295,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) shr frame.getInt(b).toInt()) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) shr getInt(fn, frame, scope, b).toInt()) } Opcode.USHR_INT -> { val a = decoder.readSlot(code, ip) @@ -304,14 +304,14 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(a) ushr frame.getInt(b).toInt()) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, a) ushr getInt(fn, frame, scope, b).toInt()) } Opcode.INV_INT -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setInt(dst, frame.getInt(src).inv()) + setInt(fn, frame, scope, dst, getInt(fn, frame, scope, src).inv()) } Opcode.CMP_LT_INT -> { val a = decoder.readSlot(code, ip) @@ -320,7 +320,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) < frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) < getInt(fn, frame, scope, b)) } Opcode.CMP_LTE_INT -> { val a = decoder.readSlot(code, ip) @@ -329,7 +329,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) <= frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) <= getInt(fn, frame, scope, b)) } Opcode.CMP_GT_INT -> { val a = decoder.readSlot(code, ip) @@ -338,7 +338,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) > frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) > getInt(fn, frame, scope, b)) } Opcode.CMP_GTE_INT -> { val a = decoder.readSlot(code, ip) @@ -347,7 +347,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) >= frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) >= getInt(fn, frame, scope, b)) } Opcode.CMP_EQ_INT -> { val a = decoder.readSlot(code, ip) @@ -356,7 +356,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) == frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) == getInt(fn, frame, scope, b)) } Opcode.CMP_NEQ_INT -> { val a = decoder.readSlot(code, ip) @@ -365,7 +365,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a) != frame.getInt(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a) != getInt(fn, frame, scope, b)) } Opcode.CMP_EQ_REAL -> { val a = decoder.readSlot(code, ip) @@ -374,7 +374,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) == frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) == getReal(fn, frame, scope, b)) } Opcode.CMP_NEQ_REAL -> { val a = decoder.readSlot(code, ip) @@ -383,7 +383,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) != frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) != getReal(fn, frame, scope, b)) } Opcode.CMP_LT_REAL -> { val a = decoder.readSlot(code, ip) @@ -392,7 +392,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) < frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) < getReal(fn, frame, scope, b)) } Opcode.CMP_LTE_REAL -> { val a = decoder.readSlot(code, ip) @@ -401,7 +401,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) <= frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) <= getReal(fn, frame, scope, b)) } Opcode.CMP_GT_REAL -> { val a = decoder.readSlot(code, ip) @@ -410,7 +410,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) > frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) > getReal(fn, frame, scope, b)) } Opcode.CMP_GTE_REAL -> { val a = decoder.readSlot(code, ip) @@ -419,7 +419,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) >= frame.getReal(b)) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) >= getReal(fn, frame, scope, b)) } Opcode.CMP_EQ_BOOL -> { val a = decoder.readSlot(code, ip) @@ -428,7 +428,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getBool(a) == frame.getBool(b)) + setBool(fn, frame, scope, dst, getBool(fn, frame, scope, a) == getBool(fn, frame, scope, b)) } Opcode.CMP_NEQ_BOOL -> { val a = decoder.readSlot(code, ip) @@ -437,7 +437,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getBool(a) != frame.getBool(b)) + setBool(fn, frame, scope, dst, getBool(fn, frame, scope, a) != getBool(fn, frame, scope, b)) } Opcode.CMP_EQ_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -446,7 +446,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() == frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() == getReal(fn, frame, scope, b)) } Opcode.CMP_EQ_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -455,7 +455,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) == frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) == getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_LT_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -464,7 +464,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() < frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() < getReal(fn, frame, scope, b)) } Opcode.CMP_LT_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -473,7 +473,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) < frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) < getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_LTE_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -482,7 +482,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() <= frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() <= getReal(fn, frame, scope, b)) } Opcode.CMP_LTE_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -491,7 +491,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) <= frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) <= getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_GT_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -500,7 +500,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() > frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() > getReal(fn, frame, scope, b)) } Opcode.CMP_GT_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -509,7 +509,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) > frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) > getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_GTE_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -518,7 +518,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() >= frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() >= getReal(fn, frame, scope, b)) } Opcode.CMP_GTE_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -527,7 +527,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) >= frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) >= getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_NEQ_INT_REAL -> { val a = decoder.readSlot(code, ip) @@ -536,7 +536,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getInt(a).toDouble() != frame.getReal(b)) + setBool(fn, frame, scope, dst, getInt(fn, frame, scope, a).toDouble() != getReal(fn, frame, scope, b)) } Opcode.CMP_NEQ_REAL_INT -> { val a = decoder.readSlot(code, ip) @@ -545,7 +545,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getReal(a) != frame.getInt(b).toDouble()) + setBool(fn, frame, scope, dst, getReal(fn, frame, scope, a) != getInt(fn, frame, scope, b).toDouble()) } Opcode.CMP_EQ_OBJ -> { val a = decoder.readSlot(code, ip) @@ -554,7 +554,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a).equals(scope, frame.getObj(b))) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a).equals(scope, getObj(fn, frame, scope, b))) } Opcode.CMP_NEQ_OBJ -> { val a = decoder.readSlot(code, ip) @@ -563,7 +563,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, !frame.getObj(a).equals(scope, frame.getObj(b))) + setBool(fn, frame, scope, dst, !getObj(fn, frame, scope, a).equals(scope, getObj(fn, frame, scope, b))) } Opcode.CMP_REF_EQ_OBJ -> { val a = decoder.readSlot(code, ip) @@ -572,7 +572,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a) === frame.getObj(b)) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a) === getObj(fn, frame, scope, b)) } Opcode.CMP_REF_NEQ_OBJ -> { val a = decoder.readSlot(code, ip) @@ -581,7 +581,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a) !== frame.getObj(b)) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a) !== getObj(fn, frame, scope, b)) } Opcode.CMP_LT_OBJ -> { val a = decoder.readSlot(code, ip) @@ -590,7 +590,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a).compareTo(scope, frame.getObj(b)) < 0) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a).compareTo(scope, getObj(fn, frame, scope, b)) < 0) } Opcode.CMP_LTE_OBJ -> { val a = decoder.readSlot(code, ip) @@ -599,7 +599,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a).compareTo(scope, frame.getObj(b)) <= 0) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a).compareTo(scope, getObj(fn, frame, scope, b)) <= 0) } Opcode.CMP_GT_OBJ -> { val a = decoder.readSlot(code, ip) @@ -608,7 +608,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a).compareTo(scope, frame.getObj(b)) > 0) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a).compareTo(scope, getObj(fn, frame, scope, b)) > 0) } Opcode.CMP_GTE_OBJ -> { val a = decoder.readSlot(code, ip) @@ -617,14 +617,59 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getObj(a).compareTo(scope, frame.getObj(b)) >= 0) + setBool(fn, frame, scope, dst, getObj(fn, frame, scope, a).compareTo(scope, getObj(fn, frame, scope, b)) >= 0) + } + Opcode.ADD_OBJ -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, a).plus(scope, getObj(fn, frame, scope, b))) + } + Opcode.SUB_OBJ -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, a).minus(scope, getObj(fn, frame, scope, b))) + } + Opcode.MUL_OBJ -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, a).mul(scope, getObj(fn, frame, scope, b))) + } + Opcode.DIV_OBJ -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, a).div(scope, getObj(fn, frame, scope, b))) + } + Opcode.MOD_OBJ -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, getObj(fn, frame, scope, a).mod(scope, getObj(fn, frame, scope, b))) } Opcode.NOT_BOOL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, !frame.getBool(src)) + setBool(fn, frame, scope, dst, !getBool(fn, frame, scope, src)) } Opcode.AND_BOOL -> { val a = decoder.readSlot(code, ip) @@ -633,7 +678,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getBool(a) && frame.getBool(b)) + setBool(fn, frame, scope, dst, getBool(fn, frame, scope, a) && getBool(fn, frame, scope, b)) } Opcode.OR_BOOL -> { val a = decoder.readSlot(code, ip) @@ -642,7 +687,7 @@ class BytecodeVm { ip += fn.slotWidth val dst = decoder.readSlot(code, ip) ip += fn.slotWidth - frame.setBool(dst, frame.getBool(a) || frame.getBool(b)) + setBool(fn, frame, scope, dst, getBool(fn, frame, scope, a) || getBool(fn, frame, scope, b)) } Opcode.JMP -> { val target = decoder.readIp(code, ip, fn.ipWidth) @@ -653,7 +698,7 @@ class BytecodeVm { ip += fn.slotWidth val target = decoder.readIp(code, ip, fn.ipWidth) ip += fn.ipWidth - if (!frame.getBool(cond)) { + if (!getBool(fn, frame, scope, cond)) { ip = target } } @@ -662,7 +707,7 @@ class BytecodeVm { ip += fn.slotWidth val target = decoder.readIp(code, ip, fn.ipWidth) ip += fn.ipWidth - if (frame.getBool(cond)) { + if (getBool(fn, frame, scope, cond)) { ip = target } } @@ -675,15 +720,15 @@ class BytecodeVm { ?: error("Fallback statement not found: $id") val result = stmt.execute(scope) when (result) { - is ObjInt -> frame.setInt(dst, result.value) - is ObjReal -> frame.setReal(dst, result.value) - is ObjBool -> frame.setBool(dst, result.value) - else -> frame.setObj(dst, result) + is ObjInt -> setInt(fn, frame, scope, dst, result.value) + is ObjReal -> setReal(fn, frame, scope, dst, result.value) + is ObjBool -> setBool(fn, frame, scope, dst, result.value) + else -> setObj(fn, frame, scope, dst, result) } } Opcode.RET -> { val slot = decoder.readSlot(code, ip) - return slotToObj(frame, slot) + return slotToObj(fn, frame, scope, slot) } Opcode.RET_VOID -> return ObjVoid else -> error("Opcode not implemented: $op") @@ -692,13 +737,96 @@ class BytecodeVm { return ObjVoid } - private fun slotToObj(frame: BytecodeFrame, slot: Int): Obj { - return when (frame.getSlotTypeCode(slot)) { - SlotType.INT.code -> ObjInt.of(frame.getInt(slot)) - SlotType.REAL.code -> ObjReal.of(frame.getReal(slot)) - SlotType.BOOL.code -> if (frame.getBool(slot)) ObjTrue else ObjFalse - SlotType.OBJ.code -> frame.getObj(slot) + private fun slotToObj(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Obj { + if (slot < fn.scopeSlotCount) { + return resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value + } + val local = slot - fn.scopeSlotCount + return when (frame.getSlotTypeCode(local)) { + SlotType.INT.code -> ObjInt.of(frame.getInt(local)) + SlotType.REAL.code -> ObjReal.of(frame.getReal(local)) + SlotType.BOOL.code -> if (frame.getBool(local)) ObjTrue else ObjFalse + SlotType.OBJ.code -> frame.getObj(local) else -> ObjVoid } } + + private fun getObj(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Obj { + return if (slot < fn.scopeSlotCount) { + resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value + } else { + frame.getObj(slot - fn.scopeSlotCount) + } + } + + private fun setObj(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int, value: Obj) { + if (slot < fn.scopeSlotCount) { + setScopeSlotValue(scope, fn.scopeSlotDepths[slot], fn.scopeSlotIndices[slot], value) + } else { + frame.setObj(slot - fn.scopeSlotCount, value) + } + } + + private fun getInt(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Long { + return if (slot < fn.scopeSlotCount) { + resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value.toLong() + } else { + frame.getInt(slot - fn.scopeSlotCount) + } + } + + private fun setInt(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int, value: Long) { + if (slot < fn.scopeSlotCount) { + setScopeSlotValue(scope, fn.scopeSlotDepths[slot], fn.scopeSlotIndices[slot], ObjInt.of(value)) + } else { + frame.setInt(slot - fn.scopeSlotCount, value) + } + } + + private fun getReal(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Double { + return if (slot < fn.scopeSlotCount) { + resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value.toDouble() + } else { + frame.getReal(slot - fn.scopeSlotCount) + } + } + + private fun setReal(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int, value: Double) { + if (slot < fn.scopeSlotCount) { + setScopeSlotValue(scope, fn.scopeSlotDepths[slot], fn.scopeSlotIndices[slot], ObjReal.of(value)) + } else { + frame.setReal(slot - fn.scopeSlotCount, value) + } + } + + private fun getBool(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Boolean { + return if (slot < fn.scopeSlotCount) { + resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value.toBool() + } else { + frame.getBool(slot - fn.scopeSlotCount) + } + } + + private fun setBool(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int, value: Boolean) { + if (slot < fn.scopeSlotCount) { + setScopeSlotValue(scope, fn.scopeSlotDepths[slot], fn.scopeSlotIndices[slot], if (value) ObjTrue else ObjFalse) + } else { + frame.setBool(slot - fn.scopeSlotCount, value) + } + } + + private fun setScopeSlotValue(scope: Scope, depth: Int, index: Int, value: Obj) { + val target = resolveScope(scope, depth) + target.setSlotValue(index, value) + } + + private fun resolveScope(scope: Scope, depth: Int): Scope { + if (depth == 0) return scope + val next = when (scope) { + is net.sergeych.lyng.ClosureScope -> scope.closureScope + else -> scope.parent + } + return next?.let { resolveScope(it, depth - 1) } + ?: error("Scope depth $depth is out of range") + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index cff1407..c22aff9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -95,6 +95,11 @@ enum class Opcode(val code: Int) { CMP_LTE_OBJ(0x74), CMP_GT_OBJ(0x75), CMP_GTE_OBJ(0x76), + ADD_OBJ(0x77), + SUB_OBJ(0x78), + MUL_OBJ(0x79), + DIV_OBJ(0x7A), + MOD_OBJ(0x7B), JMP(0x80), JMP_IF_TRUE(0x81), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 8463ce4..d06a19b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -2405,6 +2405,8 @@ class LocalSlotRef( val name: String, internal val slot: Int, internal val depth: Int, + internal val isMutable: Boolean, + internal val isDelegated: Boolean, private val atPos: Pos, ) : ObjRef { override fun forEachVariable(block: (String) -> Unit) { diff --git a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt index d5735e4..759ec28 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt @@ -210,7 +210,7 @@ class BytecodeVmTest { @Test fun localSlotTypeTrackingEnablesArithmetic() = kotlinx.coroutines.test.runTest { - val slotRef = LocalSlotRef("a", 0, 0, net.sergeych.lyng.Pos.builtIn) + val slotRef = LocalSlotRef("a", 0, 0, true, false, net.sergeych.lyng.Pos.builtIn) val assign = AssignRef( slotRef, ConstRef(ObjInt.of(2).asReadonly), @@ -225,10 +225,32 @@ class BytecodeVmTest { net.sergeych.lyng.Pos.builtIn ) val fn = BytecodeCompiler().compileExpression("localSlotAdd", expr) ?: error("bytecode compile failed") - val result = BytecodeVm().execute(fn, Scope(), emptyList()) + val scope = Scope().apply { applySlotPlan(mapOf("a" to 0)) } + val result = BytecodeVm().execute(fn, scope, emptyList()) assertEquals(4, result.toInt()) } + @Test + fun parentScopeSlotAccessWorks() = kotlinx.coroutines.test.runTest { + val parentRef = LocalSlotRef("a", 0, 1, true, false, net.sergeych.lyng.Pos.builtIn) + val expr = ExpressionStatement( + BinaryOpRef( + BinOp.PLUS, + parentRef, + ConstRef(ObjInt.of(2).asReadonly) + ), + net.sergeych.lyng.Pos.builtIn + ) + val fn = BytecodeCompiler().compileExpression("parentSlotAdd", expr) ?: error("bytecode compile failed") + val parent = Scope().apply { + applySlotPlan(mapOf("a" to 0)) + setSlotValue(0, ObjInt.of(3)) + } + val child = Scope(parent) + val result = BytecodeVm().execute(fn, child, emptyList()) + assertEquals(5, result.toInt()) + } + @Test fun objectEqualityUsesBytecodeOps() = kotlinx.coroutines.test.runTest { val expr = ExpressionStatement( @@ -298,4 +320,19 @@ class BytecodeVmTest { val gteResult = BytecodeVm().execute(gteFn, Scope(), emptyList()) assertEquals(true, gteResult.toBool()) } + + @Test + fun objectArithmeticUsesBytecodeOps() = kotlinx.coroutines.test.runTest { + val expr = ExpressionStatement( + BinaryOpRef( + BinOp.PLUS, + ConstRef(ObjString("a").asReadonly), + ConstRef(ObjString("b").asReadonly), + ), + net.sergeych.lyng.Pos.builtIn + ) + val fn = BytecodeCompiler().compileExpression("objPlus", expr) ?: error("bytecode compile failed") + val result = BytecodeVm().execute(fn, Scope(), emptyList()) + assertEquals("ab", (result as ObjString).value) + } }