Bytecode for loop over typed range params

This commit is contained in:
Sergey Chernov 2026-01-28 07:20:58 +03:00
parent 2311cfc224
commit 37a8831fd7
7 changed files with 181 additions and 16 deletions

View File

@ -126,6 +126,18 @@ class Compiler(
return null return null
} }
private fun isRangeType(type: TypeDecl): Boolean {
val name = when (type) {
is TypeDecl.Simple -> type.name
is TypeDecl.Generic -> type.name
else -> return false
}
return name == "Range" ||
name == "IntRange" ||
name.endsWith(".Range") ||
name.endsWith(".IntRange")
}
var packageName: String? = null var packageName: String? = null
class Settings( class Settings(
@ -371,6 +383,9 @@ class Compiler(
private var lastLabel: String? = null private var lastLabel: String? = null
private val useBytecodeStatements: Boolean = true private val useBytecodeStatements: Boolean = true
private val returnLabelStack = ArrayDeque<Set<String>>() private val returnLabelStack = ArrayDeque<Set<String>>()
private val rangeParamNamesStack = mutableListOf<Set<String>>()
private val currentRangeParamNames: Set<String>
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
private fun wrapBytecode(stmt: Statement): Statement { private fun wrapBytecode(stmt: Statement): Statement {
if (!useBytecodeStatements) return stmt if (!useBytecodeStatements) return stmt
@ -380,7 +395,8 @@ class Compiler(
stmt, stmt,
"stmt@${stmt.pos}", "stmt@${stmt.pos}",
allowLocalSlots = allowLocals, allowLocalSlots = allowLocals,
returnLabels = returnLabels returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames
) )
} }
@ -391,7 +407,8 @@ class Compiler(
stmt, stmt,
"fn@$name", "fn@$name",
allowLocalSlots = true, allowLocalSlots = true,
returnLabels = returnLabels returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames
) )
} }
@ -3041,11 +3058,16 @@ class Compiler(
val paramNamesList = argsDeclaration.params.map { it.name } val paramNamesList = argsDeclaration.params.map { it.name }
val paramNames: Set<String> = paramNamesList.toSet() val paramNames: Set<String> = paramNamesList.toSet()
val paramSlotPlan = buildParamSlotPlan(paramNamesList) val paramSlotPlan = buildParamSlotPlan(paramNamesList)
val rangeParamNames = argsDeclaration.params
.filter { isRangeType(it.type) }
.map { it.name }
.toSet()
// Parse function body while tracking declared locals to compute precise capacity hints // Parse function body while tracking declared locals to compute precise capacity hints
currentLocalDeclCount currentLocalDeclCount
localDeclCountStack.add(0) localDeclCountStack.add(0)
slotPlanStack.add(paramSlotPlan) slotPlanStack.add(paramSlotPlan)
rangeParamNamesStack.add(rangeParamNames)
val parsedFnStatements = try { val parsedFnStatements = try {
val returnLabels = buildSet { val returnLabels = buildSet {
add(name) add(name)
@ -3083,6 +3105,7 @@ class Compiler(
returnLabelStack.removeLast() returnLabelStack.removeLast()
} }
} finally { } finally {
rangeParamNamesStack.removeLast()
slotPlanStack.removeLast() slotPlanStack.removeLast()
} }
val fnStatements = parsedFnStatements?.let { val fnStatements = parsedFnStatements?.let {

View File

@ -29,6 +29,7 @@ import net.sergeych.lyng.obj.*
class BytecodeCompiler( class BytecodeCompiler(
private val allowLocalSlots: Boolean = true, private val allowLocalSlots: Boolean = true,
private val returnLabels: Set<String> = emptySet(), private val returnLabels: Set<String> = emptySet(),
private val rangeLocalNames: Set<String> = emptySet(),
) { ) {
private var builder = CmdBuilder() private var builder = CmdBuilder()
private var nextSlot = 0 private var nextSlot = 0
@ -48,6 +49,7 @@ class BytecodeCompiler(
private var localSlotMutables = BooleanArray(0) private var localSlotMutables = BooleanArray(0)
private var localSlotDepths = IntArray(0) private var localSlotDepths = IntArray(0)
private val declaredLocalKeys = LinkedHashSet<ScopeSlotKey>() private val declaredLocalKeys = LinkedHashSet<ScopeSlotKey>()
private val localRangeRefs = LinkedHashMap<ScopeSlotKey, RangeRef>()
private val slotTypes = mutableMapOf<Int, SlotType>() private val slotTypes = mutableMapOf<Int, SlotType>()
private val intLoopVarNames = LinkedHashSet<String>() private val intLoopVarNames = LinkedHashSet<String>()
private val loopStack = ArrayDeque<LoopContext>() private val loopStack = ArrayDeque<LoopContext>()
@ -1672,8 +1674,12 @@ class BytecodeCompiler(
} }
private fun emitForIn(stmt: net.sergeych.lyng.ForInStatement, wantResult: Boolean): Int? { private fun emitForIn(stmt: net.sergeych.lyng.ForInStatement, wantResult: Boolean): Int? {
val range = stmt.constRange val range = stmt.constRange
val rangeRef = if (range == null) extractRangeRef(stmt.source) else null var rangeRef = if (range == null) extractRangeRef(stmt.source) else null
if (range == null && rangeRef == null) return null if (range == null && rangeRef == null) {
rangeRef = extractRangeFromLocal(stmt.source)
}
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
if (range == null && rangeRef == null && typedRangeLocal == null) return null
val loopLocalIndex = localSlotIndexByName[stmt.loopVarName] ?: return null val loopLocalIndex = localSlotIndexByName[stmt.loopVarName] ?: return null
val loopSlotId = scopeSlotCount + loopLocalIndex val loopSlotId = scopeSlotCount + loopLocalIndex
@ -1685,15 +1691,83 @@ class BytecodeCompiler(
builder.emit(Opcode.CONST_INT, startId, iSlot) builder.emit(Opcode.CONST_INT, startId, iSlot)
builder.emit(Opcode.CONST_INT, endId, endSlot) builder.emit(Opcode.CONST_INT, endId, endSlot)
} else { } else {
val left = rangeRef?.left ?: return null if (rangeRef != null) {
val right = rangeRef.right ?: return null val left = rangeRef.left ?: return null
val startValue = compileRef(left) ?: return null val right = rangeRef.right ?: return null
val endValue = compileRef(right) ?: return null val startValue = compileRef(left) ?: return null
if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null val endValue = compileRef(right) ?: return null
emitMove(startValue, iSlot) if (startValue.type != SlotType.INT || endValue.type != SlotType.INT) return null
emitMove(endValue, endSlot) emitMove(startValue, iSlot)
if (rangeRef.isEndInclusive) { emitMove(endValue, endSlot)
builder.emit(Opcode.INC_INT, endSlot) if (rangeRef.isEndInclusive) {
builder.emit(Opcode.INC_INT, endSlot)
}
} else {
val rangeLocal = typedRangeLocal ?: return null
val rangeValue = compileRef(rangeLocal) ?: return null
val rangeObj = ensureObjSlot(rangeValue)
val okSlot = allocSlot()
builder.emit(Opcode.RANGE_INT_BOUNDS, rangeObj.slot, iSlot, endSlot, okSlot)
val fallbackLabel = builder.label()
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(okSlot), CmdBuilder.Operand.LabelRef(fallbackLabel))
)
val breakFlagSlot = allocSlot()
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
val resultSlot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
val loopLabel = builder.label()
val continueLabel = builder.label()
val endLabel = builder.label()
val doneLabel = builder.label()
builder.mark(loopLabel)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot)
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel))
)
builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId)
updateSlotType(loopSlotId, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
loopStack.addLast(
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
}
builder.mark(continueLabel)
builder.emit(Opcode.INC_INT, iSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (stmt.elseStatement != null) {
val afterElse = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse))
)
val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) {
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
}
builder.mark(afterElse)
}
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(doneLabel)))
builder.mark(fallbackLabel)
val fallbackId = builder.addFallback(stmt)
builder.emit(Opcode.EVAL_FALLBACK, fallbackId, resultSlot)
builder.mark(doneLabel)
return resultSlot
} }
} }
@ -2105,6 +2179,7 @@ class BytecodeCompiler(
localSlotMutables = BooleanArray(0) localSlotMutables = BooleanArray(0)
localSlotDepths = IntArray(0) localSlotDepths = IntArray(0)
declaredLocalKeys.clear() declaredLocalKeys.clear()
localRangeRefs.clear()
intLoopVarNames.clear() intLoopVarNames.clear()
addrSlotByScopeSlot.clear() addrSlotByScopeSlot.clear()
loopStack.clear() loopStack.clear()
@ -2168,6 +2243,11 @@ class BytecodeCompiler(
if (!localSlotInfoMap.containsKey(key)) { if (!localSlotInfoMap.containsKey(key)) {
localSlotInfoMap[key] = LocalSlotInfo(stmt.name, stmt.isMutable, slotDepth) localSlotInfoMap[key] = LocalSlotInfo(stmt.name, stmt.isMutable, slotDepth)
} }
if (!stmt.isMutable) {
extractDeclaredRange(stmt.initializer)?.let { range ->
localRangeRefs[key] = range
}
}
} }
stmt.initializer?.let { collectScopeSlots(it) } stmt.initializer?.let { collectScopeSlots(it) }
} }
@ -2527,6 +2607,30 @@ class BytecodeCompiler(
return expr.ref as? RangeRef return expr.ref as? RangeRef
} }
private fun extractDeclaredRange(stmt: Statement?): RangeRef? {
if (stmt == null) return null
val target = if (stmt is BytecodeStatement) stmt.original else stmt
val expr = target as? ExpressionStatement ?: return null
return expr.ref as? RangeRef
}
private fun extractRangeFromLocal(source: Statement): RangeRef? {
val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null
val localRef = expr.ref as? LocalSlotRef ?: return null
val key = ScopeSlotKey(refScopeDepth(localRef), refSlot(localRef))
return localRangeRefs[key]
}
private fun extractTypedRangeLocal(source: Statement): LocalSlotRef? {
if (rangeLocalNames.isEmpty()) return null
val target = if (source is BytecodeStatement) source.original else source
val expr = target as? ExpressionStatement ?: return null
val localRef = expr.ref as? LocalSlotRef ?: return null
if (localRef.isDelegated) return null
return if (rangeLocalNames.contains(localRef.name)) localRef else null
}
private fun effectiveLocalDepth(depth: Int): Int { private fun effectiveLocalDepth(depth: Int): Int {
if (depth == 0 || virtualScopeDepths.isEmpty()) return depth if (depth == 0 || virtualScopeDepths.isEmpty()) return depth
var virtualCount = 0 var virtualCount = 0

View File

@ -40,12 +40,17 @@ class BytecodeStatement private constructor(
nameHint: String, nameHint: String,
allowLocalSlots: Boolean, allowLocalSlots: Boolean,
returnLabels: Set<String> = emptySet(), returnLabels: Set<String> = emptySet(),
rangeLocalNames: Set<String> = emptySet(),
): Statement { ): Statement {
if (statement is BytecodeStatement) return statement if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement) val hasUnsupported = containsUnsupportedStatement(statement)
if (hasUnsupported) return unwrapDeep(statement) if (hasUnsupported) return unwrapDeep(statement)
val safeLocals = allowLocalSlots val safeLocals = allowLocalSlots
val compiler = BytecodeCompiler(allowLocalSlots = safeLocals, returnLabels = returnLabels) val compiler = BytecodeCompiler(
allowLocalSlots = safeLocals,
returnLabels = returnLabels,
rangeLocalNames = rangeLocalNames
)
val compiled = compiler.compileStatement(nameHint, statement) val compiled = compiler.compileStatement(nameHint, statement)
val fn = compiled ?: run { val fn = compiled ?: run {
val builder = CmdBuilder() val builder = CmdBuilder()
@ -78,11 +83,15 @@ class BytecodeStatement private constructor(
is net.sergeych.lyng.ForInStatement -> { is net.sergeych.lyng.ForInStatement -> {
val rangeSource = target.source val rangeSource = target.source
val rangeRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref as? RangeRef val rangeRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref as? RangeRef
val hasRange = target.constRange != null || rangeRef != null val sourceRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref
!hasRange || val hasRange = target.constRange != null ||
rangeRef != null ||
(sourceRef is net.sergeych.lyng.obj.LocalSlotRef)
val unsupported = !hasRange ||
containsUnsupportedStatement(target.source) || containsUnsupportedStatement(target.source) ||
containsUnsupportedStatement(target.body) || containsUnsupportedStatement(target.body) ||
(target.elseStatement?.let { containsUnsupportedStatement(it) } ?: false) (target.elseStatement?.let { containsUnsupportedStatement(it) } ?: false)
unsupported
} }
is net.sergeych.lyng.WhileStatement -> { is net.sergeych.lyng.WhileStatement -> {
containsUnsupportedStatement(target.condition) || containsUnsupportedStatement(target.condition) ||

View File

@ -122,6 +122,8 @@ class CmdBuilder {
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 ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW -> Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->
@ -209,6 +211,7 @@ class CmdBuilder {
Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1]) Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1])
Opcode.CONST_NULL -> CmdConstNull(operands[0]) Opcode.CONST_NULL -> CmdConstNull(operands[0])
Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1]) Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1])
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
Opcode.RET_LABEL -> CmdRetLabel(operands[0], operands[1]) Opcode.RET_LABEL -> CmdRetLabel(operands[0], operands[1])
Opcode.THROW -> CmdThrow(operands[0], operands[1]) Opcode.THROW -> CmdThrow(operands[0], operands[1])
Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1]) Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1])

View File

@ -68,6 +68,7 @@ object CmdDisassembler {
is CmdConstBool -> Opcode.CONST_BOOL to intArrayOf(cmd.constId, cmd.dst) is CmdConstBool -> Opcode.CONST_BOOL to intArrayOf(cmd.constId, cmd.dst)
is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst) is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst)
is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst) is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst)
is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot)
is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot) is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot)
is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst) is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot) is CmdStoreObjAddr -> Opcode.STORE_OBJ_ADDR to intArrayOf(cmd.src, cmd.addrSlot)
@ -196,6 +197,8 @@ object CmdDisassembler {
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 ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW -> Opcode.RET_LABEL, Opcode.THROW ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.RESOLVE_SCOPE_SLOT -> Opcode.RESOLVE_SCOPE_SLOT ->

View File

@ -154,6 +154,28 @@ class CmdBoxObj(internal val src: Int, internal val dst: Int) : Cmd() {
} }
} }
class CmdRangeIntBounds(
internal val src: Int,
internal val startSlot: Int,
internal val endSlot: Int,
internal val okSlot: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val obj = frame.slotToObj(src)
val range = obj as? ObjRange
if (range == null || !range.isIntRange) {
frame.setBool(okSlot, false)
return
}
val start = (range.start as ObjInt).value
val end = (range.end as ObjInt).value
frame.setInt(startSlot, start)
frame.setInt(endSlot, if (range.isEndInclusive) end + 1 else end)
frame.setBool(okSlot, true)
return
}
}
class CmdResolveScopeSlot(internal val scopeSlot: Int, internal val addrSlot: Int) : Cmd() { class CmdResolveScopeSlot(internal val scopeSlot: Int, internal val addrSlot: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) { override suspend fun perform(frame: CmdFrame) {
frame.resolveScopeSlotAddr(scopeSlot, addrSlot) frame.resolveScopeSlotAddr(scopeSlot, addrSlot)

View File

@ -28,6 +28,7 @@ enum class Opcode(val code: Int) {
CONST_BOOL(0x08), CONST_BOOL(0x08),
CONST_NULL(0x09), CONST_NULL(0x09),
BOX_OBJ(0x0A), BOX_OBJ(0x0A),
RANGE_INT_BOUNDS(0x0B),
INT_TO_REAL(0x10), INT_TO_REAL(0x10),
REAL_TO_INT(0x11), REAL_TO_INT(0x11),