Stabilize bytecode interpreter and fallbacks
This commit is contained in:
parent
2f4462858b
commit
7de856fc62
@ -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()
|
||||
}
|
||||
@ -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?
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -43,6 +43,8 @@ class Script(
|
||||
return lastResult
|
||||
}
|
||||
|
||||
internal fun debugStatements(): List<Statement> = statements
|
||||
|
||||
suspend fun execute() = execute(
|
||||
defaultImportManager.newStdScope()
|
||||
)
|
||||
@ -462,4 +464,4 @@ class Script(
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3989,6 +3989,7 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testParserOverflow() = runTest {
|
||||
try {
|
||||
@ -5044,4 +5045,3 @@ class ScriptTest {
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user