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 statements = mutableListOf<Statement>()
val start = cc.currentPos() val start = cc.currentPos()
// Track locals at script level for fast local refs // Track locals at script level for fast local refs
return withLocalNames(emptySet()) { return try {
// package level declarations withLocalNames(emptySet()) {
// Notify sink about script start // package level declarations
miniSink?.onScriptStart(start) // Notify sink about script start
do { miniSink?.onScriptStart(start)
do {
val t = cc.current() val t = cc.current()
if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
when (t.type) { when (t.type) {
@ -337,14 +338,16 @@ class Compiler(
break break
} }
} while (true) } while (true)
Script(start, statements) Script(start, statements)
}.also { }.also {
// Best-effort script end notification (use current position) // Best-effort script end notification (use current position)
miniSink?.onScriptEnd( miniSink?.onScriptEnd(
cc.currentPos(), cc.currentPos(),
MiniScript(MiniRange(start, cc.currentPos())) MiniScript(MiniRange(start, cc.currentPos()))
) )
}
} finally {
} }
} }
@ -373,6 +376,40 @@ class Compiler(
return BytecodeStatement.wrap(stmt, "stmt@${stmt.pos}", allowLocalSlots = true) 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? { private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
lastAnnotation = null lastAnnotation = null
lastLabel = null lastLabel = null
@ -2376,7 +2413,7 @@ class Compiler(
slotPlanStack.add(classSlotPlan) slotPlanStack.add(classSlotPlan)
val st = try { val st = try {
withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) { withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
parseScript() parseScript()
} }
} finally { } finally {
slotPlanStack.removeLast() slotPlanStack.removeLast()
@ -2574,11 +2611,23 @@ class Compiler(
} }
private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? { private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? {
if (ref !is RangeRef) return null when (ref) {
val start = constIntValueOrNull(ref.left) ?: return null is ConstRef -> {
val end = constIntValueOrNull(ref.right) ?: return null val range = ref.constValue as? ObjRange ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end if (!range.isIntRange) return null
return ConstIntRange(start, endExclusive) 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? { private fun constIntValueOrNull(ref: ObjRef?): Long? {
@ -2595,10 +2644,17 @@ class Compiler(
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
private suspend fun parseDoWhileStatement(): Statement { private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it } val label = getLabel()?.also { cc.labels += it }
val (canBreak, body) = cc.parseLoop { val loopSlotPlan = SlotPlan(mutableMapOf(), 0)
parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad do-while statement: expected body statement") 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 } label?.also { cc.labels -= it }
val body = unwrapBytecodeDeep(parsedBody)
cc.skipWsTokens() cc.skipWsTokens()
val tWhile = cc.next() val tWhile = cc.next()
@ -2606,12 +2662,18 @@ class Compiler(
throw ScriptError(tWhile.pos, "Expected 'while' after do body") throw ScriptError(tWhile.pos, "Expected 'while' after do body")
ensureLparen() 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() ensureRparen()
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement() val parsedElse = parseStatement()
parsedElse?.let { unwrapBytecodeDeep(it) }
} else { } else {
cc.previous() cc.previous()
null null
@ -2655,15 +2717,23 @@ class Compiler(
parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression") parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression")
ensureRparen() ensureRparen()
val (canBreak, body) = cc.parseLoop { val loopSlotPlan = SlotPlan(mutableMapOf(), 0)
if (cc.current().type == Token.Type.LBRACE) parseBlock() slotPlanStack.add(loopSlotPlan)
else parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement") 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 } label?.also { cc.labels -= it }
val body = unwrapBytecodeDeep(parsedBody)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { val elseStatement = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement() val parsedElse = parseStatement()
parsedElse?.let { unwrapBytecodeDeep(it) }
} else { } else {
cc.previous() cc.previous()
null null
@ -2986,8 +3056,9 @@ class Compiler(
var closure: Scope? = null var closure: Scope? = null
val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan)
val fnBody = object : Statement() { val fnBody = object : Statement(), BytecodeBodyProvider {
override val pos: Pos = t.pos override val pos: Pos = t.pos
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
override suspend fun execute(callerContext: Scope): Obj { override suspend fun execute(callerContext: Scope): Obj {
callerContext.pos = start callerContext.pos = start
@ -3082,6 +3153,7 @@ class Compiler(
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody) val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody ?: fnBody
val compiledFnBody = annotatedFnBody
extTypeName?.let { typeName -> extTypeName?.let { typeName ->
// class extension method // class extension method
@ -3092,10 +3164,10 @@ class Compiler(
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
// ObjInstance has a fixed instance scope, so we need to build a closure // ObjInstance has a fixed instance scope, so we need to build a closure
val result = (scope.thisObj as? ObjInstance)?.let { i -> 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: // 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 return result
} }
} }
@ -3127,18 +3199,18 @@ class Compiler(
newThisObj = i newThisObj = i
) )
execScope.currentClassCtx = cls execScope.currentClassCtx = cls
annotatedFnBody.execute(execScope) compiledFnBody.execute(execScope)
} ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this)) } ?: compiledFnBody.execute(thisObj.autoInstanceScope(this))
} finally { } finally {
this.currentClassCtx = savedCtx this.currentClassCtx = savedCtx
} }
} }
// also expose the symbol in the class scope for possible references // also expose the symbol in the class scope for possible references
context.addItem(name, false, annotatedFnBody, visibility) context.addItem(name, false, compiledFnBody, visibility)
annotatedFnBody compiledFnBody
} else { } else {
// top-level or nested function // 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 // as the function can be called from anywhere, we have
@ -3194,15 +3266,7 @@ class Compiler(
slotPlanStack.removeLast() slotPlanStack.removeLast()
} }
val planSnapshot = slotPlanIndices(blockSlotPlan) val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = object : Statement() { val stmt = BlockStatement(block, planSnapshot, startPos)
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 wrapped = wrapBytecode(stmt) val wrapped = wrapBytecode(stmt)
return wrapped.also { return wrapped.also {
val t1 = cc.next() val t1 = cc.next()
@ -3456,6 +3520,17 @@ class Compiler(
pendingDeclDoc = null pendingDeclDoc = null
} }
if (declaringClassNameCaptured == null &&
extTypeName == null &&
!isStatic &&
!isProperty &&
!isDelegate &&
!actualExtern &&
!isAbstract
) {
return VarDeclStatement(name, isMutable, visibility, initialExpression, isTransient, start)
}
if (isStatic) { if (isStatic) {
// find objclass instance: this is tricky: this code executes in object initializer, // 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 // when creating instance, but we need to execute it in the class initializer which

View File

@ -18,6 +18,8 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.* 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.ImportManager
import net.sergeych.lyng.pacman.ImportProvider 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. * 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) { fun addFn(vararg names: String, fn: suspend Scope.() -> Obj) {
val newFn = object : Statement() { val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn override val pos: Pos = Pos.builtIn

View File

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

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> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { 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.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ,
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
@ -138,8 +138,10 @@ class BytecodeBuilder {
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE -> Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
listOf(OperandKind.CONST) 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_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.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, 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 package net.sergeych.lyng.bytecode
import net.sergeych.lyng.BlockStatement
import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.IfStatement import net.sergeych.lyng.IfStatement
import net.sergeych.lyng.ParsedArgument import net.sergeych.lyng.ParsedArgument
import net.sergeych.lyng.Pos import net.sergeych.lyng.Pos
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.ToBoolStatement import net.sergeych.lyng.ToBoolStatement
import net.sergeych.lyng.VarDeclStatement
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
class BytecodeCompiler( class BytecodeCompiler(
@ -36,6 +38,10 @@ class BytecodeCompiler(
private val scopeSlotMap = LinkedHashMap<ScopeSlotKey, Int>() private val scopeSlotMap = LinkedHashMap<ScopeSlotKey, Int>()
private val scopeSlotNameMap = LinkedHashMap<ScopeSlotKey, String>() private val scopeSlotNameMap = LinkedHashMap<ScopeSlotKey, String>()
private val slotTypes = mutableMapOf<Int, SlotType>() 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? { fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): BytecodeFunction? {
prepareCompilation(stmt) prepareCompilation(stmt)
@ -43,6 +49,8 @@ class BytecodeCompiler(
is ExpressionStatement -> compileExpression(name, stmt) is ExpressionStatement -> compileExpression(name, stmt)
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt) is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt)
is BlockStatement -> compileBlock(name, stmt)
is VarDeclStatement -> compileVarDecl(name, stmt)
else -> null else -> null
} }
} }
@ -66,8 +74,14 @@ class BytecodeCompiler(
if (!allowLocalSlots) return null if (!allowLocalSlots) return null
if (ref.isDelegated) return null if (ref.isDelegated) return null
if (ref.name.isEmpty()) return null if (ref.name.isEmpty()) return null
val mapped = scopeSlotMap[ScopeSlotKey(refDepth(ref), refSlot(ref))] ?: return null val loopSlotId = loopVarSlotIdByName[ref.name]
CompiledValue(mapped, slotTypes[mapped] ?: SlotType.UNKNOWN) 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 BinaryOpRef -> compileBinary(ref)
is UnaryOpRef -> compileUnary(ref) is UnaryOpRef -> compileUnary(ref)
@ -150,8 +164,30 @@ class BytecodeCompiler(
if (op == BinOp.AND || op == BinOp.OR) { if (op == BinOp.AND || op == BinOp.OR) {
return compileLogical(op, binaryLeft(ref), binaryRight(ref), refPos(ref)) return compileLogical(op, binaryLeft(ref), binaryRight(ref), refPos(ref))
} }
val a = compileRef(binaryLeft(ref)) ?: return null val leftRef = binaryLeft(ref)
val b = compileRef(binaryRight(ref)) ?: return null 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 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)) { if (typesMismatch && op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)) {
return null return null
@ -701,9 +737,8 @@ class BytecodeCompiler(
val target = ref.target as? LocalSlotRef ?: return null val target = ref.target as? LocalSlotRef ?: return null
if (!allowLocalSlots) return null if (!allowLocalSlots) return null
if (!target.isMutable || target.isDelegated) 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 slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null
val slotType = slotTypes[slot] ?: return null val slotType = slotTypes[slot] ?: SlotType.UNKNOWN
return when (slotType) { return when (slotType) {
SlotType.INT -> { SlotType.INT -> {
if (ref.isPost) { if (ref.isPost) {
@ -732,6 +767,41 @@ class BytecodeCompiler(
CompiledValue(slot, SlotType.REAL) 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 else -> null
} }
} }
@ -794,6 +864,20 @@ class BytecodeCompiler(
private fun compileCall(ref: CallRef): CompiledValue? { private fun compileCall(ref: CallRef): CompiledValue? {
if (ref.isOptionalInvoke) return null 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 callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: 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? { 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 if (stmt.canBreak) return null
val range = stmt.constRange ?: return null val range = stmt.constRange ?: return null
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] ?: return null val loopSlotId = loopVarSlotIdByName[stmt.loopVarName] ?: return null
val loopSlot = scopeSlotMap[ScopeSlotKey(0, loopSlotIndex)] ?: return null val slotIndex = loopVarSlotIndexByName[stmt.loopVarName] ?: return null
val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.loopSlotPlan)) val planId = builder.addConst(BytecodeConst.SlotPlan(mapOf(stmt.loopVarName to slotIndex)))
builder.emit(Opcode.PUSH_SCOPE, planId) val useLoopScope = scopeSlotMap.isEmpty()
if (useLoopScope) {
builder.emit(Opcode.PUSH_SCOPE, planId)
} else {
builder.emit(Opcode.PUSH_SLOT_PLAN, planId)
}
val iSlot = allocSlot() val iSlot = allocSlot()
val endSlot = allocSlot() val endSlot = allocSlot()
@ -917,7 +1101,9 @@ class BytecodeCompiler(
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_TRUE,
listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(endLabel)) 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 bodyValue = compileStatementValueOrFallback(stmt.body) ?: return null
val bodyObj = ensureObjSlot(bodyValue) val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
@ -930,30 +1116,23 @@ class BytecodeCompiler(
val elseObj = ensureObjSlot(elseValue) val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
} }
builder.emit(Opcode.POP_SCOPE) if (useLoopScope) {
builder.emit(Opcode.RET, resultSlot) builder.emit(Opcode.POP_SCOPE)
} else {
val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount builder.emit(Opcode.POP_SLOT_PLAN)
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
} }
return resultSlot
} }
private fun compileStatementValueOrFallback(stmt: Statement): CompiledValue? { private fun updateSlotTypeByName(name: String, type: SlotType) {
return when (stmt) { val loopSlotId = loopVarSlotIdByName[name]
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) if (loopSlotId != null) {
is IfStatement -> compileIfExpression(stmt) updateSlotType(loopSlotId, type)
else -> { return
val slot = allocSlot() }
val id = builder.addFallback(stmt) for ((key, index) in scopeSlotMap) {
builder.emit(Opcode.EVAL_FALLBACK, id, slot) if (scopeSlotNameMap[key] == name) {
updateSlotType(slot, SlotType.OBJ) updateSlotType(index, type)
CompiledValue(slot, SlotType.OBJ)
} }
} }
} }
@ -1025,8 +1204,13 @@ class BytecodeCompiler(
} }
val id = builder.addFallback(stmt) val id = builder.addFallback(stmt)
builder.emit(Opcode.EVAL_FALLBACK, id, slot) builder.emit(Opcode.EVAL_FALLBACK, id, slot)
updateSlotType(slot, forceType ?: SlotType.OBJ) if (forceType == null) {
return CompiledValue(slot, forceType ?: SlotType.OBJ) 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 private fun refSlot(ref: LocalSlotRef): Int = ref.slot
@ -1053,7 +1237,12 @@ class BytecodeCompiler(
nextSlot = 0 nextSlot = 0
slotTypes.clear() slotTypes.clear()
scopeSlotMap.clear() scopeSlotMap.clear()
intLoopVarNames.clear()
loopVarNames.clear()
loopVarSlotIndexByName.clear()
loopVarSlotIdByName.clear()
if (allowLocalSlots) { if (allowLocalSlots) {
collectLoopVarNames(stmt)
collectScopeSlots(stmt) collectScopeSlots(stmt)
} }
scopeSlotCount = scopeSlotMap.size scopeSlotCount = scopeSlotMap.size
@ -1062,31 +1251,55 @@ class BytecodeCompiler(
scopeSlotNames = arrayOfNulls(scopeSlotCount) scopeSlotNames = arrayOfNulls(scopeSlotCount)
for ((key, index) in scopeSlotMap) { for ((key, index) in scopeSlotMap) {
scopeSlotDepths[index] = key.depth scopeSlotDepths[index] = key.depth
val name = scopeSlotNameMap[key]
scopeSlotIndices[index] = key.slot 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 nextSlot = scopeSlotCount
} }
private fun collectScopeSlots(stmt: Statement) { private fun collectScopeSlots(stmt: Statement) {
if (stmt is BytecodeStatement) {
collectScopeSlots(stmt.original)
return
}
when (stmt) { when (stmt) {
is ExpressionStatement -> collectScopeSlotsRef(stmt.ref) is ExpressionStatement -> collectScopeSlotsRef(stmt.ref)
is BlockStatement -> {
for (child in stmt.statements()) {
collectScopeSlots(child)
}
}
is VarDeclStatement -> {
stmt.initializer?.let { collectScopeSlots(it) }
}
is IfStatement -> { is IfStatement -> {
collectScopeSlots(stmt.condition) collectScopeSlots(stmt.condition)
collectScopeSlots(stmt.ifBody) collectScopeSlots(stmt.ifBody)
stmt.elseBody?.let { collectScopeSlots(it) } stmt.elseBody?.let { collectScopeSlots(it) }
} }
is net.sergeych.lyng.ForInStatement -> { 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.source)
collectScopeSlots(stmt.body) collectScopeSlots(stmt.body)
stmt.elseStatement?.let { collectScopeSlots(it) } 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) { private fun collectScopeSlotsRef(ref: ObjRef) {
when (ref) { when (ref) {
is LocalSlotRef -> { is LocalSlotRef -> {
if (loopVarNames.contains(ref.name)) return
val key = ScopeSlotKey(refDepth(ref), refSlot(ref)) val key = ScopeSlotKey(refDepth(ref), refSlot(ref))
if (!scopeSlotMap.containsKey(key)) { if (!scopeSlotMap.containsKey(key)) {
scopeSlotMap[key] = scopeSlotMap.size scopeSlotMap[key] = scopeSlotMap.size

View File

@ -16,6 +16,7 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
sealed class BytecodeConst { sealed class BytecodeConst {
@ -26,6 +27,12 @@ sealed class BytecodeConst {
data class StringVal(val value: String) : BytecodeConst() data class StringVal(val value: String) : BytecodeConst()
data class ObjRef(val value: Obj) : BytecodeConst() data class ObjRef(val value: Obj) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>) : 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 CallArgsPlan(val tailBlock: Boolean, val specs: List<CallArgSpec>) : BytecodeConst()
data class CallArgSpec(val name: String?, val isSplat: Boolean) data class CallArgSpec(val name: String?, val isSplat: Boolean)
} }

View File

@ -82,7 +82,7 @@ object BytecodeDisassembler {
private fun operandKinds(op: Opcode): List<OperandKind> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { 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.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ,
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
@ -91,8 +91,10 @@ object BytecodeDisassembler {
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE -> Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
listOf(OperandKind.CONST) 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_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.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, 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()) return BytecodeVm().execute(function, scope, emptyList())
} }
internal fun bytecodeFunction(): BytecodeFunction = function
companion object { companion object {
fun wrap(statement: Statement, nameHint: String, allowLocalSlots: Boolean): Statement { fun wrap(statement: Statement, nameHint: String, allowLocalSlots: Boolean): Statement {
if (statement is BytecodeStatement) return 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.Arguments
import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
class BytecodeVm { class BytecodeVm {
@ -27,11 +28,16 @@ class BytecodeVm {
private const val ARG_PLAN_MASK = 0x7FFF private const val ARG_PLAN_MASK = 0x7FFF
} }
private var virtualDepth = 0
suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj { suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj {
val scopeStack = ArrayDeque<Scope>() val scopeStack = ArrayDeque<Scope>()
val scopeVirtualStack = ArrayDeque<Boolean>()
val slotPlanStack = ArrayDeque<Map<String, Int?>>()
var scope = scope0 var scope = scope0
val methodCallSites = BytecodeCallSiteCache.methodCallSites(fn) val methodCallSites = BytecodeCallSiteCache.methodCallSites(fn)
val frame = BytecodeFrame(fn.localCount, args.size) val frame = BytecodeFrame(fn.localCount, args.size)
virtualDepth = 0
for (i in args.indices) { for (i in args.indices) {
frame.setObj(frame.argBase + i, args[i]) frame.setObj(frame.argBase + i, args[i])
} }
@ -734,16 +740,65 @@ class BytecodeVm {
ip += fn.constIdWidth ip += fn.constIdWidth
val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan
?: error("PUSH_SCOPE expects SlotPlan at $constId") ?: error("PUSH_SCOPE expects SlotPlan at $constId")
scopeStack.addLast(scope) if (scope.skipScopeCreation) {
scope = scope.createChildScope() val snapshot = scope.applySlotPlanWithSnapshot(planConst.plan)
if (planConst.plan.isNotEmpty()) { slotPlanStack.addLast(snapshot)
scope.applySlotPlan(planConst.plan) 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 -> { 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() scope = scopeStack.removeLastOrNull()
?: error("Scope stack underflow in POP_SCOPE") ?: 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 -> { Opcode.CALL_SLOT -> {
val calleeSlot = decoder.readSlot(code, ip) val calleeSlot = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth
@ -979,11 +1034,16 @@ class BytecodeVm {
private fun resolveScope(scope: Scope, depth: Int): Scope { private fun resolveScope(scope: Scope, depth: Int): Scope {
if (depth == 0) return scope if (depth == 0) return scope
var effectiveDepth = depth
if (virtualDepth > 0) {
if (effectiveDepth <= virtualDepth) return scope
effectiveDepth -= virtualDepth
}
val next = when (scope) { val next = when (scope) {
is net.sergeych.lyng.ClosureScope -> scope.closureScope is net.sergeych.lyng.ClosureScope -> scope.closureScope
else -> scope.parent 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") ?: error("Scope depth $depth is out of range")
} }
} }

View File

@ -109,6 +109,9 @@ enum class Opcode(val code: Int) {
RET_VOID(0x84), RET_VOID(0x84),
PUSH_SCOPE(0x85), PUSH_SCOPE(0x85),
POP_SCOPE(0x86), POP_SCOPE(0x86),
PUSH_SLOT_PLAN(0x87),
POP_SLOT_PLAN(0x88),
DECL_LOCAL(0x89),
CALL_DIRECT(0x90), CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91), CALL_VIRTUAL(0x91),

View File

@ -17,7 +17,12 @@
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Benchmarks 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 net.sergeych.lyng.obj.ObjInt
import kotlin.time.TimeSource import kotlin.time.TimeSource
import kotlin.test.Test import kotlin.test.Test
@ -27,25 +32,65 @@ class NestedRangeBenchmarkTest {
@Test @Test
fun benchmarkHappyNumbersNestedRanges() = runTest { fun benchmarkHappyNumbersNestedRanges() = runTest {
if (!Benchmarks.enabled) return@runTest if (!Benchmarks.enabled) return@runTest
val script = """ val bodyScript = """
fun naiveCountHappyNumbers() { var count = 0
var count = 0 for( n1 in 0..9 )
for( n1 in 0..9 ) for( n2 in 0..9 )
for( n2 in 0..9 ) for( n3 in 0..9 )
for( n3 in 0..9 ) for( n4 in 0..9 )
for( n4 in 0..9 ) for( n5 in 0..9 )
for( n5 in 0..9 ) for( n6 in 0..9 )
for( n6 in 0..9 ) if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ count
count
}
naiveCountHappyNumbers()
""".trimIndent() """.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 start = TimeSource.Monotonic.markNow()
val result = eval(script) as ObjInt val result = scope.eval("naiveCountHappyNumbers()") as ObjInt
val elapsedMs = start.elapsedNow().inWholeMilliseconds val elapsedMs = start.elapsedNow().inWholeMilliseconds
println("[DEBUG_LOG] [BENCH] nested-happy elapsed=${elapsedMs} ms") println("[DEBUG_LOG] [BENCH] nested-happy elapsed=${elapsedMs} ms")
assertEquals(55252L, result.value) 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 @Test
fun testParserOverflow() = runTest { fun testParserOverflow() = runTest {
try { try {
@ -5044,4 +5045,3 @@ class ScriptTest {
""".trimIndent()) """.trimIndent())
} }
} }