Stabilize bytecode interpreter and fallbacks

This commit is contained in:
Sergey Chernov 2026-01-26 22:13:30 +03:00
parent 2f4462858b
commit 7de856fc62
15 changed files with 728 additions and 115 deletions

View File

@ -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<String, Int>,
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<Statement> = block.debugStatements()
}

View File

@ -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?
}

View File

@ -239,11 +239,12 @@ class Compiler(
val statements = mutableListOf<Statement>()
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

View File

@ -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<String, Int>): Map<String, Int?> {
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<String, Int?>(plan.size)
for ((name, idx) in plan) {
snapshot[name] = nameToSlot[name]
nameToSlot[name] = idx
}
return snapshot
}
fun restoreSlotPlan(snapshot: Map<String, Int?>) {
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

View File

@ -43,6 +43,8 @@ class Script(
return lastResult
}
internal fun debugStatements(): List<Statement> = statements
suspend fun execute() = execute(
defaultImportManager.newStdScope()
)

View File

@ -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
}
}

View File

@ -129,7 +129,7 @@ class BytecodeBuilder {
private fun operandKinds(op: Opcode): List<OperandKind> {
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,

View File

@ -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<ScopeSlotKey, Int>()
private val scopeSlotNameMap = LinkedHashMap<ScopeSlotKey, String>()
private val slotTypes = mutableMapOf<Int, SlotType>()
private val intLoopVarNames = LinkedHashSet<String>()
private val loopVarNames = LinkedHashSet<String>()
private val loopVarSlotIndexByName = LinkedHashMap<String, Int>()
private val loopVarSlotIdByName = LinkedHashMap<String, Int>()
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

View File

@ -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<String, Int>) : 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<CallArgSpec>) : BytecodeConst()
data class CallArgSpec(val name: String?, val isSplat: Boolean)
}

View File

@ -82,7 +82,7 @@ object BytecodeDisassembler {
private fun operandKinds(op: Opcode): List<OperandKind> {
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,

View File

@ -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

View File

@ -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>): Obj {
val scopeStack = ArrayDeque<Scope>()
val scopeVirtualStack = ArrayDeque<Boolean>()
val slotPlanStack = ArrayDeque<Map<String, Int?>>()
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")
}
}

View File

@ -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),

View File

@ -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<Statement>) {
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: <not found>")
}
}
}

View File

@ -3989,6 +3989,7 @@ class ScriptTest {
)
}
@Test
fun testParserOverflow() = runTest {
try {
@ -5044,4 +5045,3 @@ class ScriptTest {
""".trimIndent())
}
}