From 7de856fc62e6173241e7f8efd1fa476aa2cfe7d8 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 26 Jan 2026 22:13:30 +0300 Subject: [PATCH] Stabilize bytecode interpreter and fallbacks --- .../net/sergeych/lyng/BlockStatement.kt | 35 ++ .../net/sergeych/lyng/BytecodeBodyProvider.kt | 23 ++ .../kotlin/net/sergeych/lyng/Compiler.kt | 163 +++++--- .../kotlin/net/sergeych/lyng/Scope.kt | 39 ++ .../kotlin/net/sergeych/lyng/Script.kt | 4 +- .../net/sergeych/lyng/VarDeclStatement.kt | 45 +++ .../sergeych/lyng/bytecode/BytecodeBuilder.kt | 6 +- .../lyng/bytecode/BytecodeCompiler.kt | 363 +++++++++++++++--- .../sergeych/lyng/bytecode/BytecodeConst.kt | 7 + .../lyng/bytecode/BytecodeDisassembler.kt | 6 +- .../lyng/bytecode/BytecodeStatement.kt | 2 + .../net/sergeych/lyng/bytecode/BytecodeVm.kt | 70 +++- .../net/sergeych/lyng/bytecode/Opcode.kt | 3 + .../kotlin/NestedRangeBenchmarkTest.kt | 75 +++- lynglib/src/commonTest/kotlin/ScriptTest.kt | 2 +- 15 files changed, 728 insertions(+), 115 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt new file mode 100644 index 0000000..c76604e --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import net.sergeych.lyng.obj.Obj + +class BlockStatement( + val block: Script, + val slotPlan: Map, + private val startPos: Pos, +) : Statement() { + override val pos: Pos = startPos + + override suspend fun execute(scope: Scope): Obj { + val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos) + if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan) + return block.execute(target) + } + + fun statements(): List = block.debugStatements() +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt new file mode 100644 index 0000000..f28bdac --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt @@ -0,0 +1,23 @@ +/* + * 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 + +import net.sergeych.lyng.bytecode.BytecodeStatement + +interface BytecodeBodyProvider { + fun bytecodeBody(): BytecodeStatement? +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index b4ceee8..82088b5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -239,11 +239,12 @@ class Compiler( val statements = mutableListOf() val start = cc.currentPos() // Track locals at script level for fast local refs - return withLocalNames(emptySet()) { - // package level declarations - // Notify sink about script start - miniSink?.onScriptStart(start) - do { + return try { + withLocalNames(emptySet()) { + // package level declarations + // Notify sink about script start + miniSink?.onScriptStart(start) + do { val t = cc.current() if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { when (t.type) { @@ -337,14 +338,16 @@ class Compiler( break } - } while (true) - Script(start, statements) - }.also { - // Best-effort script end notification (use current position) - miniSink?.onScriptEnd( - cc.currentPos(), - MiniScript(MiniRange(start, cc.currentPos())) - ) + } while (true) + Script(start, statements) + }.also { + // Best-effort script end notification (use current position) + miniSink?.onScriptEnd( + cc.currentPos(), + MiniScript(MiniRange(start, cc.currentPos())) + ) + } + } finally { } } @@ -373,6 +376,40 @@ class Compiler( return BytecodeStatement.wrap(stmt, "stmt@${stmt.pos}", allowLocalSlots = true) } + private fun wrapFunctionBytecode(stmt: Statement, name: String): Statement { + if (!useBytecodeStatements) return stmt + return BytecodeStatement.wrap(stmt, "fn@$name", allowLocalSlots = true) + } + + private fun unwrapBytecodeDeep(stmt: Statement): Statement { + return when (stmt) { + is BytecodeStatement -> unwrapBytecodeDeep(stmt.original) + is BlockStatement -> { + val unwrapped = stmt.statements().map { unwrapBytecodeDeep(it) } + val script = Script(stmt.block.pos, unwrapped) + BlockStatement(script, stmt.slotPlan, stmt.pos) + } + is VarDeclStatement -> { + val init = stmt.initializer?.let { unwrapBytecodeDeep(it) } + VarDeclStatement( + stmt.name, + stmt.isMutable, + stmt.visibility, + init, + stmt.isTransient, + stmt.pos + ) + } + is IfStatement -> { + val cond = unwrapBytecodeDeep(stmt.condition) + val ifBody = unwrapBytecodeDeep(stmt.ifBody) + val elseBody = stmt.elseBody?.let { unwrapBytecodeDeep(it) } + IfStatement(cond, ifBody, elseBody, stmt.pos) + } + else -> stmt + } + } + private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { lastAnnotation = null lastLabel = null @@ -2376,7 +2413,7 @@ class Compiler( slotPlanStack.add(classSlotPlan) val st = try { withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) { - parseScript() + parseScript() } } finally { slotPlanStack.removeLast() @@ -2574,11 +2611,23 @@ class Compiler( } private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? { - if (ref !is RangeRef) return null - val start = constIntValueOrNull(ref.left) ?: return null - val end = constIntValueOrNull(ref.right) ?: return null - val endExclusive = if (ref.isEndInclusive) end + 1 else end - return ConstIntRange(start, endExclusive) + when (ref) { + is ConstRef -> { + val range = ref.constValue as? ObjRange ?: return null + if (!range.isIntRange) return null + val start = range.start?.toLong() ?: return null + val end = range.end?.toLong() ?: return null + val endExclusive = if (range.isEndInclusive) end + 1 else end + return ConstIntRange(start, endExclusive) + } + is RangeRef -> { + val start = constIntValueOrNull(ref.left) ?: return null + val end = constIntValueOrNull(ref.right) ?: return null + val endExclusive = if (ref.isEndInclusive) end + 1 else end + return ConstIntRange(start, endExclusive) + } + else -> return null + } } private fun constIntValueOrNull(ref: ObjRef?): Long? { @@ -2595,10 +2644,17 @@ class Compiler( @Suppress("UNUSED_VARIABLE") private suspend fun parseDoWhileStatement(): Statement { val label = getLabel()?.also { cc.labels += it } - val (canBreak, body) = cc.parseLoop { - parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad do-while statement: expected body statement") + val loopSlotPlan = SlotPlan(mutableMapOf(), 0) + slotPlanStack.add(loopSlotPlan) + val (canBreak, parsedBody) = try { + cc.parseLoop { + parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad do-while statement: expected body statement") + } + } finally { + slotPlanStack.removeLast() } label?.also { cc.labels -= it } + val body = unwrapBytecodeDeep(parsedBody) cc.skipWsTokens() val tWhile = cc.next() @@ -2606,12 +2662,18 @@ class Compiler( throw ScriptError(tWhile.pos, "Expected 'while' after do body") ensureLparen() - val condition = parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected condition after 'while'") + slotPlanStack.add(loopSlotPlan) + val condition = try { + parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected condition after 'while'") + } finally { + slotPlanStack.removeLast() + } ensureRparen() cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { - parseStatement() + val parsedElse = parseStatement() + parsedElse?.let { unwrapBytecodeDeep(it) } } else { cc.previous() null @@ -2655,15 +2717,23 @@ class Compiler( parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression") ensureRparen() - val (canBreak, body) = cc.parseLoop { - if (cc.current().type == Token.Type.LBRACE) parseBlock() - else parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement") + val loopSlotPlan = SlotPlan(mutableMapOf(), 0) + slotPlanStack.add(loopSlotPlan) + val (canBreak, parsedBody) = try { + cc.parseLoop { + if (cc.current().type == Token.Type.LBRACE) parseBlock() + else parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement") + } + } finally { + slotPlanStack.removeLast() } label?.also { cc.labels -= it } + val body = unwrapBytecodeDeep(parsedBody) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { - parseStatement() + val parsedElse = parseStatement() + parsedElse?.let { unwrapBytecodeDeep(it) } } else { cc.previous() null @@ -2986,8 +3056,9 @@ class Compiler( var closure: Scope? = null val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) - val fnBody = object : Statement() { + val fnBody = object : Statement(), BytecodeBodyProvider { override val pos: Pos = t.pos + override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement override suspend fun execute(callerContext: Scope): Obj { callerContext.pos = start @@ -3082,6 +3153,7 @@ class Compiler( val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody) ?: fnBody + val compiledFnBody = annotatedFnBody extTypeName?.let { typeName -> // class extension method @@ -3092,10 +3164,10 @@ class Compiler( override suspend fun execute(scope: Scope): Obj { // ObjInstance has a fixed instance scope, so we need to build a closure val result = (scope.thisObj as? ObjInstance)?.let { i -> - annotatedFnBody.execute(ClosureScope(scope, i.instanceScope)) + compiledFnBody.execute(ClosureScope(scope, i.instanceScope)) } // other classes can create one-time scope for this rare case: - ?: annotatedFnBody.execute(scope.thisObj.autoInstanceScope(scope)) + ?: compiledFnBody.execute(scope.thisObj.autoInstanceScope(scope)) return result } } @@ -3127,18 +3199,18 @@ class Compiler( newThisObj = i ) execScope.currentClassCtx = cls - annotatedFnBody.execute(execScope) - } ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this)) + compiledFnBody.execute(execScope) + } ?: compiledFnBody.execute(thisObj.autoInstanceScope(this)) } finally { this.currentClassCtx = savedCtx } } // also expose the symbol in the class scope for possible references - context.addItem(name, false, annotatedFnBody, visibility) - annotatedFnBody + context.addItem(name, false, compiledFnBody, visibility) + compiledFnBody } else { // top-level or nested function - context.addItem(name, false, annotatedFnBody, visibility) + context.addItem(name, false, compiledFnBody, visibility) } } // as the function can be called from anywhere, we have @@ -3194,15 +3266,7 @@ class Compiler( slotPlanStack.removeLast() } val planSnapshot = slotPlanIndices(blockSlotPlan) - val stmt = object : Statement() { - override val pos: Pos = startPos - override suspend fun execute(scope: Scope): Obj { - // block run on inner context: - val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos) - if (planSnapshot.isNotEmpty()) target.applySlotPlan(planSnapshot) - return block.execute(target) - } - } + val stmt = BlockStatement(block, planSnapshot, startPos) val wrapped = wrapBytecode(stmt) return wrapped.also { val t1 = cc.next() @@ -3456,6 +3520,17 @@ class Compiler( pendingDeclDoc = null } + if (declaringClassNameCaptured == null && + extTypeName == null && + !isStatic && + !isProperty && + !isDelegate && + !actualExtern && + !isAbstract + ) { + return VarDeclStatement(name, isMutable, visibility, initialExpression, isTransient, start) + } + if (isStatic) { // find objclass instance: this is tricky: this code executes in object initializer, // when creating instance, but we need to execute it in the class initializer which diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 86ca7cf..ffab50d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -18,6 +18,8 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.* +import net.sergeych.lyng.bytecode.BytecodeDisassembler +import net.sergeych.lyng.bytecode.BytecodeStatement import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportProvider @@ -410,6 +412,34 @@ open class Scope( } } + fun applySlotPlanWithSnapshot(plan: Map): Map { + if (plan.isEmpty()) return emptyMap() + val maxIndex = plan.values.maxOrNull() ?: return emptyMap() + if (slots.size <= maxIndex) { + val targetSize = maxIndex + 1 + while (slots.size < targetSize) { + slots.add(ObjRecord(ObjUnset, isMutable = true)) + } + } + val snapshot = LinkedHashMap(plan.size) + for ((name, idx) in plan) { + snapshot[name] = nameToSlot[name] + nameToSlot[name] = idx + } + return snapshot + } + + fun restoreSlotPlan(snapshot: Map) { + if (snapshot.isEmpty()) return + for ((name, idx) in snapshot) { + if (idx == null) { + nameToSlot.remove(name) + } else { + nameToSlot[name] = idx + } + } + } + /** * Clear all references and maps to prevent memory leaks when pooled. */ @@ -615,6 +645,15 @@ open class Scope( } } + fun disassembleSymbol(name: String): String { + val record = get(name) ?: return "$name is not found" + val stmt = record.value as? Statement ?: return "$name is not a compiled body" + val bytecode = (stmt as? BytecodeStatement)?.bytecodeFunction() + ?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction() + ?: return "$name is not a compiled body" + return BytecodeDisassembler.disassemble(bytecode) + } + fun addFn(vararg names: String, fn: suspend Scope.() -> Obj) { val newFn = object : Statement() { override val pos: Pos = Pos.builtIn diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index f65ef7d..40ab1bd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -43,6 +43,8 @@ class Script( return lastResult } + internal fun debugStatements(): List = statements + suspend fun execute() = execute( defaultImportManager.newStdScope() ) @@ -462,4 +464,4 @@ class Script( } } -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt new file mode 100644 index 0000000..8fbb2aa --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt @@ -0,0 +1,45 @@ +/* + * 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 + +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjNull +import net.sergeych.lyng.obj.ObjRecord + +class VarDeclStatement( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val initializer: Statement?, + val isTransient: Boolean, + private val startPos: Pos, +) : Statement() { + override val pos: Pos = startPos + + override suspend fun execute(context: Scope): Obj { + val initValue = initializer?.execute(context)?.byValueCopy() ?: ObjNull + context.addItem( + name, + isMutable, + initValue, + visibility, + recordType = ObjRecord.Type.Other, + isTransient = isTransient + ) + return initValue + } +} 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 6740239..d8eb3aa 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -129,7 +129,7 @@ class BytecodeBuilder { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList() + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> 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.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> @@ -138,8 +138,10 @@ class BytecodeBuilder { listOf(OperandKind.SLOT) Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> listOf(OperandKind.CONST, OperandKind.SLOT) - Opcode.PUSH_SCOPE -> + Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) + Opcode.DECL_LOCAL -> + listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_REAL, Opcode.SUB_REAL, Opcode.MUL_REAL, Opcode.DIV_REAL, Opcode.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT, 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 1cb9fc2..c762ca0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -16,12 +16,14 @@ package net.sergeych.lyng.bytecode +import net.sergeych.lyng.BlockStatement import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.IfStatement import net.sergeych.lyng.ParsedArgument import net.sergeych.lyng.Pos import net.sergeych.lyng.Statement import net.sergeych.lyng.ToBoolStatement +import net.sergeych.lyng.VarDeclStatement import net.sergeych.lyng.obj.* class BytecodeCompiler( @@ -36,6 +38,10 @@ class BytecodeCompiler( private val scopeSlotMap = LinkedHashMap() private val scopeSlotNameMap = LinkedHashMap() private val slotTypes = mutableMapOf() + private val intLoopVarNames = LinkedHashSet() + private val loopVarNames = LinkedHashSet() + private val loopVarSlotIndexByName = LinkedHashMap() + private val loopVarSlotIdByName = LinkedHashMap() fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): BytecodeFunction? { prepareCompilation(stmt) @@ -43,6 +49,8 @@ class BytecodeCompiler( is ExpressionStatement -> compileExpression(name, stmt) is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt) + is BlockStatement -> compileBlock(name, stmt) + is VarDeclStatement -> compileVarDecl(name, stmt) else -> null } } @@ -66,8 +74,14 @@ class BytecodeCompiler( if (!allowLocalSlots) return null if (ref.isDelegated) return null if (ref.name.isEmpty()) return null - val mapped = scopeSlotMap[ScopeSlotKey(refDepth(ref), refSlot(ref))] ?: return null - CompiledValue(mapped, slotTypes[mapped] ?: SlotType.UNKNOWN) + val loopSlotId = loopVarSlotIdByName[ref.name] + val mapped = loopSlotId ?: scopeSlotMap[ScopeSlotKey(refDepth(ref), refSlot(ref))] ?: return null + val resolved = slotTypes[mapped] ?: SlotType.UNKNOWN + if (resolved == SlotType.UNKNOWN && intLoopVarNames.contains(ref.name)) { + updateSlotType(mapped, SlotType.INT) + return CompiledValue(mapped, SlotType.INT) + } + CompiledValue(mapped, resolved) } is BinaryOpRef -> compileBinary(ref) is UnaryOpRef -> compileUnary(ref) @@ -150,8 +164,30 @@ class BytecodeCompiler( if (op == BinOp.AND || op == BinOp.OR) { return compileLogical(op, binaryLeft(ref), binaryRight(ref), refPos(ref)) } - val a = compileRef(binaryLeft(ref)) ?: return null - val b = compileRef(binaryRight(ref)) ?: return null + val leftRef = binaryLeft(ref) + val rightRef = binaryRight(ref) + var a = compileRef(leftRef) ?: return null + var b = compileRef(rightRef) ?: return null + val intOps = setOf( + BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT, + BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR + ) + val leftIsLoopVar = (leftRef as? LocalSlotRef)?.name?.let { intLoopVarNames.contains(it) } == true + val rightIsLoopVar = (rightRef as? LocalSlotRef)?.name?.let { intLoopVarNames.contains(it) } == true + if (a.type == SlotType.UNKNOWN && b.type == SlotType.INT && op in intOps && leftIsLoopVar) { + updateSlotType(a.slot, SlotType.INT) + a = CompiledValue(a.slot, SlotType.INT) + } + if (b.type == SlotType.UNKNOWN && a.type == SlotType.INT && op in intOps && rightIsLoopVar) { + updateSlotType(b.slot, SlotType.INT) + b = CompiledValue(b.slot, SlotType.INT) + } + if (a.type == SlotType.UNKNOWN && b.type == SlotType.UNKNOWN && op in intOps && leftIsLoopVar && rightIsLoopVar) { + updateSlotType(a.slot, SlotType.INT) + updateSlotType(b.slot, SlotType.INT) + a = CompiledValue(a.slot, SlotType.INT) + b = CompiledValue(b.slot, SlotType.INT) + } val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN if (typesMismatch && op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)) { return null @@ -701,9 +737,8 @@ class BytecodeCompiler( val target = ref.target as? LocalSlotRef ?: return null if (!allowLocalSlots) return null if (!target.isMutable || target.isDelegated) return null - if (refDepth(target) > 0) return null val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null - val slotType = slotTypes[slot] ?: return null + val slotType = slotTypes[slot] ?: SlotType.UNKNOWN return when (slotType) { SlotType.INT -> { if (ref.isPost) { @@ -732,6 +767,41 @@ class BytecodeCompiler( CompiledValue(slot, SlotType.REAL) } } + SlotType.OBJ -> { + val oneSlot = allocSlot() + val oneId = builder.addConst(BytecodeConst.ObjRef(ObjInt.One)) + builder.emit(Opcode.CONST_OBJ, oneId, oneSlot) + val current = allocSlot() + builder.emit(Opcode.BOX_OBJ, slot, current) + if (ref.isPost) { + val result = allocSlot() + val op = if (ref.isIncrement) Opcode.ADD_OBJ else Opcode.SUB_OBJ + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.MOVE_OBJ, result, slot) + updateSlotType(slot, SlotType.OBJ) + CompiledValue(current, SlotType.OBJ) + } else { + val result = allocSlot() + val op = if (ref.isIncrement) Opcode.ADD_OBJ else Opcode.SUB_OBJ + builder.emit(op, current, oneSlot, result) + builder.emit(Opcode.MOVE_OBJ, result, slot) + updateSlotType(slot, SlotType.OBJ) + CompiledValue(result, SlotType.OBJ) + } + } + SlotType.UNKNOWN -> { + if (ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_INT, slot, old) + builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot) + updateSlotType(slot, SlotType.INT) + CompiledValue(old, SlotType.INT) + } else { + builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot) + updateSlotType(slot, SlotType.INT) + CompiledValue(slot, SlotType.INT) + } + } else -> null } } @@ -794,6 +864,20 @@ class BytecodeCompiler( private fun compileCall(ref: CallRef): CompiledValue? { if (ref.isOptionalInvoke) return null + if (ref.target is LocalVarRef || ref.target is FastLocalVarRef || ref.target is BoundLocalVarRef) { + return null + } + val fieldTarget = ref.target as? FieldRef + if (fieldTarget != null && !fieldTarget.isOptional) { + val receiver = compileRefWithFallback(fieldTarget.target, null, Pos.builtIn) ?: return null + val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null + val encodedCount = encodeCallArgCount(args) ?: return null + val methodId = builder.addConst(BytecodeConst.StringVal(fieldTarget.name)) + if (methodId > 0xFFFF) return null + val dst = allocSlot() + builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, encodedCount, dst) + return CompiledValue(dst, SlotType.UNKNOWN) + } val callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null val encodedCount = encodeCallArgCount(args) ?: return null @@ -890,12 +974,112 @@ class BytecodeCompiler( } private fun compileForIn(name: String, stmt: net.sergeych.lyng.ForInStatement): BytecodeFunction? { + val resultSlot = emitForIn(stmt) ?: return null + builder.emit(Opcode.RET, resultSlot) + val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) + } + + private fun compileBlock(name: String, stmt: BlockStatement): BytecodeFunction? { + val result = emitBlock(stmt) ?: return null + builder.emit(Opcode.RET, result.slot) + val localCount = maxOf(nextSlot, result.slot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) + } + + private fun compileVarDecl(name: String, stmt: VarDeclStatement): BytecodeFunction? { + val result = emitVarDecl(stmt) ?: return null + builder.emit(Opcode.RET, result.slot) + val localCount = maxOf(nextSlot, result.slot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) + } + + private fun compileStatementValue(stmt: Statement): CompiledValue? { + return when (stmt) { + is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) + else -> null + } + } + + private fun compileStatementValueOrFallback(stmt: Statement): CompiledValue? { + val target = if (stmt is BytecodeStatement) stmt.original else stmt + return when (target) { + is ExpressionStatement -> compileRefWithFallback(target.ref, null, target.pos) + is IfStatement -> compileIfExpression(target) + is net.sergeych.lyng.ForInStatement -> { + val resultSlot = emitForIn(target) ?: return null + updateSlotType(resultSlot, SlotType.OBJ) + CompiledValue(resultSlot, SlotType.OBJ) + } + is BlockStatement -> emitBlock(target) + is VarDeclStatement -> emitVarDecl(target) + else -> { + val slot = allocSlot() + val id = builder.addFallback(target) + builder.emit(Opcode.EVAL_FALLBACK, id, slot) + builder.emit(Opcode.BOX_OBJ, slot, slot) + updateSlotType(slot, SlotType.OBJ) + CompiledValue(slot, SlotType.OBJ) + } + } + } + + private fun emitBlock(stmt: BlockStatement): CompiledValue? { + val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.slotPlan)) + builder.emit(Opcode.PUSH_SCOPE, planId) + val statements = stmt.statements() + var lastValue: CompiledValue? = null + for (statement in statements) { + lastValue = compileStatementValueOrFallback(statement) ?: return null + } + var result = lastValue ?: run { + val slot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, slot) + CompiledValue(slot, SlotType.OBJ) + } + if (result.slot < scopeSlotCount) { + val captured = allocSlot() + emitMove(result, captured) + result = CompiledValue(captured, result.type) + } + builder.emit(Opcode.POP_SCOPE) + return result + } + + private fun emitVarDecl(stmt: VarDeclStatement): CompiledValue? { + val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run { + val slot = allocSlot() + builder.emit(Opcode.CONST_NULL, slot) + updateSlotType(slot, SlotType.OBJ) + CompiledValue(slot, SlotType.OBJ) + } + val declId = builder.addConst( + BytecodeConst.LocalDecl( + stmt.name, + stmt.isMutable, + stmt.visibility, + stmt.isTransient + ) + ) + builder.emit(Opcode.DECL_LOCAL, declId, value.slot) + if (value.type != SlotType.UNKNOWN) { + updateSlotTypeByName(stmt.name, value.type) + } + return value + } + private fun emitForIn(stmt: net.sergeych.lyng.ForInStatement): Int? { if (stmt.canBreak) return null val range = stmt.constRange ?: return null - val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] ?: return null - val loopSlot = scopeSlotMap[ScopeSlotKey(0, loopSlotIndex)] ?: return null - val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.loopSlotPlan)) - builder.emit(Opcode.PUSH_SCOPE, planId) + val loopSlotId = loopVarSlotIdByName[stmt.loopVarName] ?: return null + val slotIndex = loopVarSlotIndexByName[stmt.loopVarName] ?: return null + val planId = builder.addConst(BytecodeConst.SlotPlan(mapOf(stmt.loopVarName to slotIndex))) + val useLoopScope = scopeSlotMap.isEmpty() + if (useLoopScope) { + builder.emit(Opcode.PUSH_SCOPE, planId) + } else { + builder.emit(Opcode.PUSH_SLOT_PLAN, planId) + } val iSlot = allocSlot() val endSlot = allocSlot() @@ -917,7 +1101,9 @@ class BytecodeCompiler( Opcode.JMP_IF_TRUE, listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(endLabel)) ) - builder.emit(Opcode.MOVE_INT, iSlot, loopSlot) + builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId) + updateSlotType(loopSlotId, SlotType.INT) + updateSlotTypeByName(stmt.loopVarName, SlotType.INT) val bodyValue = compileStatementValueOrFallback(stmt.body) ?: return null val bodyObj = ensureObjSlot(bodyValue) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) @@ -930,30 +1116,23 @@ class BytecodeCompiler( val elseObj = ensureObjSlot(elseValue) builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) } - builder.emit(Opcode.POP_SCOPE) - builder.emit(Opcode.RET, resultSlot) - - val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount - return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) - } - - private fun compileStatementValue(stmt: Statement): CompiledValue? { - return when (stmt) { - is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) - else -> null + if (useLoopScope) { + builder.emit(Opcode.POP_SCOPE) + } else { + builder.emit(Opcode.POP_SLOT_PLAN) } + return resultSlot } - private fun compileStatementValueOrFallback(stmt: Statement): CompiledValue? { - return when (stmt) { - is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) - is IfStatement -> compileIfExpression(stmt) - else -> { - val slot = allocSlot() - val id = builder.addFallback(stmt) - builder.emit(Opcode.EVAL_FALLBACK, id, slot) - updateSlotType(slot, SlotType.OBJ) - CompiledValue(slot, SlotType.OBJ) + private fun updateSlotTypeByName(name: String, type: SlotType) { + val loopSlotId = loopVarSlotIdByName[name] + if (loopSlotId != null) { + updateSlotType(loopSlotId, type) + return + } + for ((key, index) in scopeSlotMap) { + if (scopeSlotNameMap[key] == name) { + updateSlotType(index, type) } } } @@ -1025,8 +1204,13 @@ class BytecodeCompiler( } val id = builder.addFallback(stmt) builder.emit(Opcode.EVAL_FALLBACK, id, slot) - updateSlotType(slot, forceType ?: SlotType.OBJ) - return CompiledValue(slot, forceType ?: SlotType.OBJ) + if (forceType == null) { + builder.emit(Opcode.BOX_OBJ, slot, slot) + updateSlotType(slot, SlotType.OBJ) + return CompiledValue(slot, SlotType.OBJ) + } + updateSlotType(slot, forceType) + return CompiledValue(slot, forceType) } private fun refSlot(ref: LocalSlotRef): Int = ref.slot @@ -1053,7 +1237,12 @@ class BytecodeCompiler( nextSlot = 0 slotTypes.clear() scopeSlotMap.clear() + intLoopVarNames.clear() + loopVarNames.clear() + loopVarSlotIndexByName.clear() + loopVarSlotIdByName.clear() if (allowLocalSlots) { + collectLoopVarNames(stmt) collectScopeSlots(stmt) } scopeSlotCount = scopeSlotMap.size @@ -1062,31 +1251,55 @@ class BytecodeCompiler( scopeSlotNames = arrayOfNulls(scopeSlotCount) for ((key, index) in scopeSlotMap) { scopeSlotDepths[index] = key.depth + val name = scopeSlotNameMap[key] scopeSlotIndices[index] = key.slot - scopeSlotNames[index] = scopeSlotNameMap[key] + scopeSlotNames[index] = name + } + if (loopVarNames.isNotEmpty()) { + var maxSlotIndex = scopeSlotMap.keys.maxOfOrNull { it.slot } ?: -1 + for (name in loopVarNames) { + maxSlotIndex += 1 + loopVarSlotIndexByName[name] = maxSlotIndex + } + val start = scopeSlotCount + val total = scopeSlotCount + loopVarSlotIndexByName.size + scopeSlotDepths = scopeSlotDepths.copyOf(total) + scopeSlotIndices = scopeSlotIndices.copyOf(total) + scopeSlotNames = scopeSlotNames.copyOf(total) + var cursor = start + for ((name, slotIndex) in loopVarSlotIndexByName) { + loopVarSlotIdByName[name] = cursor + scopeSlotDepths[cursor] = 0 + scopeSlotIndices[cursor] = slotIndex + scopeSlotNames[cursor] = name + cursor += 1 + } + scopeSlotCount = total } nextSlot = scopeSlotCount } private fun collectScopeSlots(stmt: Statement) { + if (stmt is BytecodeStatement) { + collectScopeSlots(stmt.original) + return + } when (stmt) { is ExpressionStatement -> collectScopeSlotsRef(stmt.ref) + is BlockStatement -> { + for (child in stmt.statements()) { + collectScopeSlots(child) + } + } + is VarDeclStatement -> { + stmt.initializer?.let { collectScopeSlots(it) } + } is IfStatement -> { collectScopeSlots(stmt.condition) collectScopeSlots(stmt.ifBody) stmt.elseBody?.let { collectScopeSlots(it) } } is net.sergeych.lyng.ForInStatement -> { - val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] - if (loopSlotIndex != null) { - val key = ScopeSlotKey(0, loopSlotIndex) - if (!scopeSlotMap.containsKey(key)) { - scopeSlotMap[key] = scopeSlotMap.size - } - if (!scopeSlotNameMap.containsKey(key)) { - scopeSlotNameMap[key] = stmt.loopVarName - } - } collectScopeSlots(stmt.source) collectScopeSlots(stmt.body) stmt.elseStatement?.let { collectScopeSlots(it) } @@ -1095,9 +1308,69 @@ class BytecodeCompiler( } } + private fun collectLoopVarNames(stmt: Statement) { + if (stmt is BytecodeStatement) { + collectLoopVarNames(stmt.original) + return + } + when (stmt) { + is net.sergeych.lyng.ForInStatement -> { + if (stmt.constRange != null) { + intLoopVarNames.add(stmt.loopVarName) + loopVarNames.add(stmt.loopVarName) + } + collectLoopVarNames(stmt.source) + collectLoopVarNames(stmt.body) + stmt.elseStatement?.let { collectLoopVarNames(it) } + } + is BlockStatement -> { + for (child in stmt.statements()) { + collectLoopVarNames(child) + } + } + is VarDeclStatement -> { + stmt.initializer?.let { collectLoopVarNames(it) } + } + is IfStatement -> { + collectLoopVarNames(stmt.condition) + collectLoopVarNames(stmt.ifBody) + stmt.elseBody?.let { collectLoopVarNames(it) } + } + is ExpressionStatement -> collectLoopVarNamesRef(stmt.ref) + else -> {} + } + } + + private fun collectLoopVarNamesRef(ref: ObjRef) { + when (ref) { + is BinaryOpRef -> { + collectLoopVarNamesRef(binaryLeft(ref)) + collectLoopVarNamesRef(binaryRight(ref)) + } + is UnaryOpRef -> collectLoopVarNamesRef(unaryOperand(ref)) + is AssignRef -> collectLoopVarNamesRef(assignValue(ref)) + is AssignOpRef -> { + collectLoopVarNamesRef(ref.target) + collectLoopVarNamesRef(ref.value) + } + is IncDecRef -> collectLoopVarNamesRef(ref.target) + is ConditionalRef -> { + collectLoopVarNamesRef(ref.condition) + collectLoopVarNamesRef(ref.ifTrue) + collectLoopVarNamesRef(ref.ifFalse) + } + is ElvisRef -> { + collectLoopVarNamesRef(ref.left) + collectLoopVarNamesRef(ref.right) + } + else -> {} + } + } + private fun collectScopeSlotsRef(ref: ObjRef) { when (ref) { is LocalSlotRef -> { + if (loopVarNames.contains(ref.name)) return val key = ScopeSlotKey(refDepth(ref), refSlot(ref)) if (!scopeSlotMap.containsKey(key)) { scopeSlotMap[key] = scopeSlotMap.size diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt index 117de97..0c555af 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -16,6 +16,7 @@ package net.sergeych.lyng.bytecode +import net.sergeych.lyng.Visibility import net.sergeych.lyng.obj.Obj sealed class BytecodeConst { @@ -26,6 +27,12 @@ sealed class BytecodeConst { data class StringVal(val value: String) : BytecodeConst() data class ObjRef(val value: Obj) : BytecodeConst() data class SlotPlan(val plan: Map) : BytecodeConst() + data class LocalDecl( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val isTransient: Boolean, + ) : BytecodeConst() data class CallArgsPlan(val tailBlock: Boolean, val specs: List) : BytecodeConst() data class CallArgSpec(val name: String?, val isSplat: Boolean) } 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 b66ae11..1164773 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt @@ -82,7 +82,7 @@ object BytecodeDisassembler { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList() + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> 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.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> @@ -91,8 +91,10 @@ object BytecodeDisassembler { listOf(OperandKind.SLOT) Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> listOf(OperandKind.CONST, OperandKind.SLOT) - Opcode.PUSH_SCOPE -> + Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) + Opcode.DECL_LOCAL -> + listOf(OperandKind.CONST, OperandKind.SLOT) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_REAL, Opcode.SUB_REAL, Opcode.MUL_REAL, Opcode.DIV_REAL, Opcode.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index 516fdb6..c842184 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -31,6 +31,8 @@ class BytecodeStatement private constructor( return BytecodeVm().execute(function, scope, emptyList()) } + internal fun bytecodeFunction(): BytecodeFunction = function + companion object { fun wrap(statement: Statement, nameHint: String, allowLocalSlots: Boolean): Statement { if (statement is BytecodeStatement) return statement 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 41bd616..e4f31fc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -19,6 +19,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.Arguments import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.Scope +import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.* class BytecodeVm { @@ -27,11 +28,16 @@ class BytecodeVm { private const val ARG_PLAN_MASK = 0x7FFF } + private var virtualDepth = 0 + suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List): Obj { val scopeStack = ArrayDeque() + val scopeVirtualStack = ArrayDeque() + val slotPlanStack = ArrayDeque>() var scope = scope0 val methodCallSites = BytecodeCallSiteCache.methodCallSites(fn) val frame = BytecodeFrame(fn.localCount, args.size) + virtualDepth = 0 for (i in args.indices) { frame.setObj(frame.argBase + i, args[i]) } @@ -734,16 +740,65 @@ class BytecodeVm { ip += fn.constIdWidth val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan ?: error("PUSH_SCOPE expects SlotPlan at $constId") - scopeStack.addLast(scope) - scope = scope.createChildScope() - if (planConst.plan.isNotEmpty()) { - scope.applySlotPlan(planConst.plan) + if (scope.skipScopeCreation) { + val snapshot = scope.applySlotPlanWithSnapshot(planConst.plan) + slotPlanStack.addLast(snapshot) + virtualDepth += 1 + scopeStack.addLast(scope) + scopeVirtualStack.addLast(true) + } else { + scopeStack.addLast(scope) + scopeVirtualStack.addLast(false) + scope = scope.createChildScope() + if (planConst.plan.isNotEmpty()) { + scope.applySlotPlan(planConst.plan) + } } } Opcode.POP_SCOPE -> { + val isVirtual = scopeVirtualStack.removeLastOrNull() + ?: error("Scope stack underflow in POP_SCOPE") + if (isVirtual) { + val snapshot = slotPlanStack.removeLastOrNull() + ?: error("Slot plan stack underflow in POP_SCOPE") + scope.restoreSlotPlan(snapshot) + virtualDepth -= 1 + } scope = scopeStack.removeLastOrNull() ?: error("Scope stack underflow in POP_SCOPE") } + Opcode.PUSH_SLOT_PLAN -> { + val constId = decoder.readConstId(code, ip, fn.constIdWidth) + ip += fn.constIdWidth + val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan + ?: error("PUSH_SLOT_PLAN expects SlotPlan at $constId") + val snapshot = scope.applySlotPlanWithSnapshot(planConst.plan) + slotPlanStack.addLast(snapshot) + virtualDepth += 1 + } + Opcode.POP_SLOT_PLAN -> { + val snapshot = slotPlanStack.removeLastOrNull() + ?: error("Slot plan stack underflow in POP_SLOT_PLAN") + scope.restoreSlotPlan(snapshot) + virtualDepth -= 1 + } + Opcode.DECL_LOCAL -> { + val constId = decoder.readConstId(code, ip, fn.constIdWidth) + ip += fn.constIdWidth + val slot = decoder.readSlot(code, ip) + ip += fn.slotWidth + val decl = fn.constants[constId] as? BytecodeConst.LocalDecl + ?: error("DECL_LOCAL expects LocalDecl at $constId") + val value = slotToObj(fn, frame, scope, slot).byValueCopy() + scope.addItem( + decl.name, + decl.isMutable, + value, + decl.visibility, + recordType = ObjRecord.Type.Other, + isTransient = decl.isTransient + ) + } Opcode.CALL_SLOT -> { val calleeSlot = decoder.readSlot(code, ip) ip += fn.slotWidth @@ -979,11 +1034,16 @@ class BytecodeVm { private fun resolveScope(scope: Scope, depth: Int): Scope { if (depth == 0) return scope + var effectiveDepth = depth + if (virtualDepth > 0) { + if (effectiveDepth <= virtualDepth) return scope + effectiveDepth -= virtualDepth + } val next = when (scope) { is net.sergeych.lyng.ClosureScope -> scope.closureScope else -> scope.parent } - return next?.let { resolveScope(it, depth - 1) } + return next?.let { resolveScope(it, effectiveDepth - 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 008e2f4..ab7c80f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -109,6 +109,9 @@ enum class Opcode(val code: Int) { RET_VOID(0x84), PUSH_SCOPE(0x85), POP_SCOPE(0x86), + PUSH_SLOT_PLAN(0x87), + POP_SLOT_PLAN(0x88), + DECL_LOCAL(0x89), CALL_DIRECT(0x90), CALL_VIRTUAL(0x91), diff --git a/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt index aaa975b..808c8af 100644 --- a/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt +++ b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt @@ -17,7 +17,12 @@ import kotlinx.coroutines.test.runTest import net.sergeych.lyng.Benchmarks -import net.sergeych.lyng.eval +import net.sergeych.lyng.Compiler +import net.sergeych.lyng.ForInStatement +import net.sergeych.lyng.Script +import net.sergeych.lyng.Statement +import net.sergeych.lyng.bytecode.BytecodeDisassembler +import net.sergeych.lyng.bytecode.BytecodeStatement import net.sergeych.lyng.obj.ObjInt import kotlin.time.TimeSource import kotlin.test.Test @@ -27,25 +32,65 @@ class NestedRangeBenchmarkTest { @Test fun benchmarkHappyNumbersNestedRanges() = runTest { if (!Benchmarks.enabled) return@runTest - val script = """ - fun naiveCountHappyNumbers() { - var count = 0 - for( n1 in 0..9 ) - for( n2 in 0..9 ) - for( n3 in 0..9 ) - for( n4 in 0..9 ) - for( n5 in 0..9 ) - for( n6 in 0..9 ) - if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ - count - } - naiveCountHappyNumbers() + val bodyScript = """ + var count = 0 + for( n1 in 0..9 ) + for( n2 in 0..9 ) + for( n3 in 0..9 ) + for( n4 in 0..9 ) + for( n5 in 0..9 ) + for( n6 in 0..9 ) + if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ + count """.trimIndent() + val compiled = Compiler.compile(bodyScript) + dumpNestedLoopBytecode(compiled.debugStatements()) + + val script = """ + fun naiveCountHappyNumbers() { + $bodyScript + } + """.trimIndent() + + val scope = Script.newScope() + scope.eval(script) + val fnDisasm = scope.disassembleSymbol("naiveCountHappyNumbers") + println("[DEBUG_LOG] [BENCH] nested-happy function naiveCountHappyNumbers bytecode:\n$fnDisasm") + val start = TimeSource.Monotonic.markNow() - val result = eval(script) as ObjInt + val result = scope.eval("naiveCountHappyNumbers()") as ObjInt val elapsedMs = start.elapsedNow().inWholeMilliseconds println("[DEBUG_LOG] [BENCH] nested-happy elapsed=${elapsedMs} ms") assertEquals(55252L, result.value) } + + private fun dumpNestedLoopBytecode(statements: List) { + var current: Statement? = statements.firstOrNull { stmt -> + stmt is BytecodeStatement && stmt.original is ForInStatement + } + var depth = 1 + while (current is BytecodeStatement && current.original is ForInStatement) { + val original = current.original as ForInStatement + println( + "[DEBUG_LOG] [BENCH] nested-happy loop depth=$depth " + + "constRange=${original.constRange} canBreak=${original.canBreak} " + + "loopSlotPlan=${original.loopSlotPlan}" + ) + val fn = current.bytecodeFunction() + val slots = fn.scopeSlotNames.mapIndexed { idx, name -> + val slotName = name ?: "s$idx" + "$slotName@${fn.scopeSlotDepths[idx]}:${fn.scopeSlotIndices[idx]}" + } + println("[DEBUG_LOG] [BENCH] nested-happy slots depth=$depth: ${slots.joinToString(", ")}") + val disasm = BytecodeDisassembler.disassemble(fn) + println("[DEBUG_LOG] [BENCH] nested-happy bytecode depth=$depth:\n$disasm") + current = original.body + depth += 1 + } + if (depth == 1) { + println("[DEBUG_LOG] [BENCH] nested-happy bytecode: ") + } + } + } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index a93a045..c64d8e1 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3989,6 +3989,7 @@ class ScriptTest { ) } + @Test fun testParserOverflow() = runTest { try { @@ -5044,4 +5045,3 @@ class ScriptTest { """.trimIndent()) } } -