Expand bytecode expressions and loops
This commit is contained in:
parent
72901d9d4c
commit
144082733c
@ -30,6 +30,9 @@ slots[localCount .. localCount+argCount-1] arguments
|
||||
- scopeSlotNames: array sized scopeSlotCount, each entry nullable.
|
||||
- Intended for disassembly/debug tooling; VM semantics do not depend on it.
|
||||
|
||||
### Constant pool extras
|
||||
- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals.
|
||||
|
||||
## 2) Slot ID Width
|
||||
|
||||
Per frame, select:
|
||||
@ -185,6 +188,12 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
|
||||
- JMP_IF_FALSE S, I
|
||||
- RET S
|
||||
- RET_VOID
|
||||
- PUSH_SCOPE K
|
||||
- POP_SCOPE
|
||||
|
||||
### Scope setup
|
||||
- PUSH_SCOPE uses const `SlotPlan` (name -> slot index) to create a child scope and apply slot mapping.
|
||||
- POP_SCOPE restores the parent scope.
|
||||
|
||||
### Calls
|
||||
- CALL_DIRECT F, S, C, S
|
||||
|
||||
@ -2556,174 +2556,23 @@ class Compiler(
|
||||
}
|
||||
val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan)
|
||||
|
||||
return object : Statement() {
|
||||
override val pos: Pos = body.pos
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val forContext = scope.createChildScope(start)
|
||||
if (loopSlotPlanSnapshot.isNotEmpty()) {
|
||||
forContext.applySlotPlan(loopSlotPlanSnapshot)
|
||||
}
|
||||
|
||||
// loop var: StoredObject
|
||||
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
|
||||
|
||||
if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1
|
||||
return loopIntRange(
|
||||
forContext,
|
||||
constRange.start,
|
||||
constRange.endExclusive,
|
||||
loopSO,
|
||||
loopSlotIndex,
|
||||
body,
|
||||
elseStatement,
|
||||
label,
|
||||
canBreak
|
||||
)
|
||||
}
|
||||
// insofar we suggest source object is enumerable. Later we might need to add checks
|
||||
val sourceObj = source.execute(forContext)
|
||||
|
||||
if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1
|
||||
return loopIntRange(
|
||||
forContext,
|
||||
sourceObj.start!!.toLong(),
|
||||
if (sourceObj.isEndInclusive)
|
||||
sourceObj.end!!.toLong() + 1
|
||||
else
|
||||
sourceObj.end!!.toLong(),
|
||||
loopSO,
|
||||
loopSlotIndex,
|
||||
body,
|
||||
elseStatement,
|
||||
label,
|
||||
canBreak
|
||||
)
|
||||
} else if (sourceObj.isInstanceOf(ObjIterable)) {
|
||||
return loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
|
||||
} else {
|
||||
val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
tOp.pos,
|
||||
"object is not enumerable: no size in $sourceObj",
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
var result: Obj = ObjVoid
|
||||
var breakCaught = false
|
||||
|
||||
if (size > 0) {
|
||||
var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
tOp.pos,
|
||||
"object is not enumerable: no index access for ${sourceObj.inspect(scope)}",
|
||||
it
|
||||
)
|
||||
}
|
||||
var index = 0
|
||||
while (true) {
|
||||
loopSO.value = current
|
||||
try {
|
||||
result = body.execute(forContext)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
breakCaught = true
|
||||
if (lbe.doContinue) continue
|
||||
else {
|
||||
result = lbe.result
|
||||
break
|
||||
}
|
||||
} else
|
||||
throw lbe
|
||||
}
|
||||
if (++index >= size) break
|
||||
current = sourceObj.getAt(forContext, ObjInt.of(index.toLong()))
|
||||
}
|
||||
}
|
||||
if (!breakCaught && elseStatement != null) {
|
||||
result = elseStatement.execute(scope)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return ForInStatement(
|
||||
loopVarName = tVar.value,
|
||||
source = source,
|
||||
constRange = constRange,
|
||||
body = body,
|
||||
elseStatement = elseStatement,
|
||||
label = label,
|
||||
canBreak = canBreak,
|
||||
loopSlotPlan = loopSlotPlanSnapshot,
|
||||
pos = body.pos
|
||||
)
|
||||
} else {
|
||||
// maybe other loops?
|
||||
throw ScriptError(tOp.pos, "Unsupported for-loop syntax")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loopIntRange(
|
||||
forScope: Scope, start: Long, end: Long, loopVar: ObjRecord, loopSlotIndex: Int,
|
||||
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
val cacheLow = ObjInt.CACHE_LOW
|
||||
val cacheHigh = ObjInt.CACHE_HIGH
|
||||
val useCache = start >= cacheLow && end <= cacheHigh + 1
|
||||
val cache = if (useCache) ObjInt.cacheArray() else null
|
||||
val useSlot = loopSlotIndex >= 0
|
||||
if (catchBreak) {
|
||||
if (useCache && cache != null) {
|
||||
var i = start
|
||||
while (i < end) {
|
||||
val v = cache[(i - cacheLow).toInt()]
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
return lbe.result
|
||||
}
|
||||
throw lbe
|
||||
}
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
val v = ObjInt.of(i)
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) continue
|
||||
return lbe.result
|
||||
}
|
||||
throw lbe
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (useCache && cache != null) {
|
||||
var i = start
|
||||
while (i < end) {
|
||||
val v = cache[(i - cacheLow).toInt()]
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
result = body.execute(forScope)
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
val v = ObjInt.of(i)
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
result = body.execute(forScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
return elseStatement?.execute(forScope) ?: result
|
||||
}
|
||||
|
||||
private data class ConstIntRange(val start: Long, val endExclusive: Long)
|
||||
|
||||
private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? {
|
||||
if (ref !is RangeRef) return null
|
||||
val start = constIntValueOrNull(ref.left) ?: return null
|
||||
@ -2743,40 +2592,6 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loopIterable(
|
||||
forScope: Scope, sourceObj: Obj, loopVar: ObjRecord,
|
||||
body: Statement, elseStatement: Statement?, label: String?,
|
||||
catchBreak: Boolean
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
var breakCaught = false
|
||||
sourceObj.enumerate(forScope) { item ->
|
||||
loopVar.value = item
|
||||
if (catchBreak) {
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) true
|
||||
else {
|
||||
result = lbe.result
|
||||
breakCaught = true
|
||||
false
|
||||
}
|
||||
} else
|
||||
throw lbe
|
||||
}
|
||||
} else {
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
}
|
||||
}
|
||||
return if (!breakCaught && elseStatement != null) {
|
||||
elseStatement.execute(forScope)
|
||||
} else result
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
private suspend fun parseDoWhileStatement(): Statement {
|
||||
val label = getLabel()?.also { cc.labels += it }
|
||||
|
||||
@ -129,7 +129,7 @@ class BytecodeBuilder {
|
||||
|
||||
private fun operandKinds(op: Opcode): List<OperandKind> {
|
||||
return when (op) {
|
||||
Opcode.NOP, Opcode.RET_VOID -> emptyList()
|
||||
Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> 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,6 +138,8 @@ 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 ->
|
||||
listOf(OperandKind.CONST)
|
||||
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,
|
||||
|
||||
@ -42,6 +42,7 @@ class BytecodeCompiler(
|
||||
return when (stmt) {
|
||||
is ExpressionStatement -> compileExpression(name, stmt)
|
||||
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
|
||||
is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -71,6 +72,10 @@ class BytecodeCompiler(
|
||||
is BinaryOpRef -> compileBinary(ref)
|
||||
is UnaryOpRef -> compileUnary(ref)
|
||||
is AssignRef -> compileAssign(ref)
|
||||
is AssignOpRef -> compileAssignOp(ref)
|
||||
is IncDecRef -> compileIncDec(ref)
|
||||
is ConditionalRef -> compileConditional(ref)
|
||||
is ElvisRef -> compileElvis(ref)
|
||||
is CallRef -> compileCall(ref)
|
||||
is MethodCallRef -> compileMethodCall(ref)
|
||||
else -> null
|
||||
@ -590,6 +595,173 @@ class BytecodeCompiler(
|
||||
return CompiledValue(slot, value.type)
|
||||
}
|
||||
|
||||
private fun compileAssignOp(ref: AssignOpRef): CompiledValue? {
|
||||
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 targetType = slotTypes[slot] ?: return null
|
||||
val rhs = compileRef(ref.value) ?: return null
|
||||
val out = slot
|
||||
val result = when (ref.op) {
|
||||
BinOp.PLUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.ADD_INT, Opcode.ADD_REAL, Opcode.ADD_OBJ)
|
||||
BinOp.MINUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.SUB_INT, Opcode.SUB_REAL, Opcode.SUB_OBJ)
|
||||
BinOp.STAR -> compileAssignOpBinary(targetType, rhs, out, Opcode.MUL_INT, Opcode.MUL_REAL, Opcode.MUL_OBJ)
|
||||
BinOp.SLASH -> compileAssignOpBinary(targetType, rhs, out, Opcode.DIV_INT, Opcode.DIV_REAL, Opcode.DIV_OBJ)
|
||||
BinOp.PERCENT -> compileAssignOpBinary(targetType, rhs, out, Opcode.MOD_INT, null, Opcode.MOD_OBJ)
|
||||
else -> null
|
||||
} ?: return null
|
||||
updateSlotType(out, result.type)
|
||||
return CompiledValue(out, result.type)
|
||||
}
|
||||
|
||||
private fun compileAssignOpBinary(
|
||||
targetType: SlotType,
|
||||
rhs: CompiledValue,
|
||||
out: Int,
|
||||
intOp: Opcode,
|
||||
realOp: Opcode?,
|
||||
objOp: Opcode?,
|
||||
): CompiledValue? {
|
||||
return when (targetType) {
|
||||
SlotType.INT -> {
|
||||
when (rhs.type) {
|
||||
SlotType.INT -> {
|
||||
builder.emit(intOp, out, rhs.slot, out)
|
||||
CompiledValue(out, SlotType.INT)
|
||||
}
|
||||
SlotType.REAL -> {
|
||||
if (realOp == null) return null
|
||||
val left = allocSlot()
|
||||
builder.emit(Opcode.INT_TO_REAL, out, left)
|
||||
builder.emit(realOp, left, rhs.slot, out)
|
||||
CompiledValue(out, SlotType.REAL)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
SlotType.REAL -> {
|
||||
if (realOp == null) return null
|
||||
when (rhs.type) {
|
||||
SlotType.REAL -> {
|
||||
builder.emit(realOp, out, rhs.slot, out)
|
||||
CompiledValue(out, SlotType.REAL)
|
||||
}
|
||||
SlotType.INT -> {
|
||||
val right = allocSlot()
|
||||
builder.emit(Opcode.INT_TO_REAL, rhs.slot, right)
|
||||
builder.emit(realOp, out, right, out)
|
||||
CompiledValue(out, SlotType.REAL)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
SlotType.OBJ -> {
|
||||
if (objOp == null) return null
|
||||
if (rhs.type != SlotType.OBJ) return null
|
||||
builder.emit(objOp, out, rhs.slot, out)
|
||||
CompiledValue(out, SlotType.OBJ)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileIncDec(ref: IncDecRef): CompiledValue? {
|
||||
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
|
||||
return when (slotType) {
|
||||
SlotType.INT -> {
|
||||
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)
|
||||
CompiledValue(old, SlotType.INT)
|
||||
} else {
|
||||
builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot)
|
||||
CompiledValue(slot, SlotType.INT)
|
||||
}
|
||||
}
|
||||
SlotType.REAL -> {
|
||||
val oneSlot = allocSlot()
|
||||
val oneId = builder.addConst(BytecodeConst.RealVal(1.0))
|
||||
builder.emit(Opcode.CONST_REAL, oneId, oneSlot)
|
||||
if (ref.isPost) {
|
||||
val old = allocSlot()
|
||||
builder.emit(Opcode.MOVE_REAL, slot, old)
|
||||
val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL
|
||||
builder.emit(op, slot, oneSlot, slot)
|
||||
CompiledValue(old, SlotType.REAL)
|
||||
} else {
|
||||
val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL
|
||||
builder.emit(op, slot, oneSlot, slot)
|
||||
CompiledValue(slot, SlotType.REAL)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileConditional(ref: ConditionalRef): CompiledValue? {
|
||||
val condition = compileRefWithFallback(ref.condition, SlotType.BOOL, Pos.builtIn) ?: return null
|
||||
if (condition.type != SlotType.BOOL) return null
|
||||
val resultSlot = allocSlot()
|
||||
val elseLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_FALSE,
|
||||
listOf(BytecodeBuilder.Operand.IntVal(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel))
|
||||
)
|
||||
val thenValue = compileRefWithFallback(ref.ifTrue, null, Pos.builtIn) ?: return null
|
||||
val thenObj = ensureObjSlot(thenValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot)
|
||||
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
|
||||
builder.mark(elseLabel)
|
||||
val elseValue = compileRefWithFallback(ref.ifFalse, null, Pos.builtIn) ?: return null
|
||||
val elseObj = ensureObjSlot(elseValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
|
||||
builder.mark(endLabel)
|
||||
updateSlotType(resultSlot, SlotType.OBJ)
|
||||
return CompiledValue(resultSlot, SlotType.OBJ)
|
||||
}
|
||||
|
||||
private fun compileElvis(ref: ElvisRef): CompiledValue? {
|
||||
val leftValue = compileRefWithFallback(ref.left, null, Pos.builtIn) ?: return null
|
||||
val leftObj = ensureObjSlot(leftValue)
|
||||
val resultSlot = allocSlot()
|
||||
val nullSlot = allocSlot()
|
||||
builder.emit(Opcode.CONST_NULL, nullSlot)
|
||||
val cmpSlot = allocSlot()
|
||||
builder.emit(Opcode.CMP_REF_EQ_OBJ, leftObj.slot, nullSlot, cmpSlot)
|
||||
val rightLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(rightLabel))
|
||||
)
|
||||
builder.emit(Opcode.MOVE_OBJ, leftObj.slot, resultSlot)
|
||||
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
|
||||
builder.mark(rightLabel)
|
||||
val rightValue = compileRefWithFallback(ref.right, null, Pos.builtIn) ?: return null
|
||||
val rightObj = ensureObjSlot(rightValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, rightObj.slot, resultSlot)
|
||||
builder.mark(endLabel)
|
||||
updateSlotType(resultSlot, SlotType.OBJ)
|
||||
return CompiledValue(resultSlot, SlotType.OBJ)
|
||||
}
|
||||
|
||||
private fun ensureObjSlot(value: CompiledValue): CompiledValue {
|
||||
if (value.type == SlotType.OBJ) return value
|
||||
val dst = allocSlot()
|
||||
builder.emit(Opcode.BOX_OBJ, value.slot, dst)
|
||||
updateSlotType(dst, SlotType.OBJ)
|
||||
return CompiledValue(dst, SlotType.OBJ)
|
||||
}
|
||||
|
||||
private data class CallArgs(val base: Int, val count: Int)
|
||||
|
||||
private fun compileCall(ref: CallRef): CompiledValue? {
|
||||
@ -680,6 +852,54 @@ class BytecodeCompiler(
|
||||
return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames)
|
||||
}
|
||||
|
||||
private fun compileForIn(name: String, stmt: net.sergeych.lyng.ForInStatement): BytecodeFunction? {
|
||||
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 iSlot = allocSlot()
|
||||
val endSlot = allocSlot()
|
||||
val startId = builder.addConst(BytecodeConst.IntVal(range.start))
|
||||
val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive))
|
||||
builder.emit(Opcode.CONST_INT, startId, iSlot)
|
||||
builder.emit(Opcode.CONST_INT, endId, endSlot)
|
||||
|
||||
val resultSlot = allocSlot()
|
||||
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
|
||||
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
|
||||
|
||||
val loopLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
builder.mark(loopLabel)
|
||||
val cmpSlot = allocSlot()
|
||||
builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot)
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(endLabel))
|
||||
)
|
||||
builder.emit(Opcode.MOVE_INT, iSlot, loopSlot)
|
||||
val bodyValue = compileStatementValueOrFallback(stmt.body) ?: return null
|
||||
val bodyObj = ensureObjSlot(bodyValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
|
||||
builder.emit(Opcode.INC_INT, iSlot)
|
||||
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(loopLabel)))
|
||||
|
||||
builder.mark(endLabel)
|
||||
if (stmt.elseStatement != null) {
|
||||
val elseValue = compileStatementValueOrFallback(stmt.elseStatement) ?: return null
|
||||
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)
|
||||
@ -687,6 +907,61 @@ class BytecodeCompiler(
|
||||
}
|
||||
}
|
||||
|
||||
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 compileIfExpression(stmt: IfStatement): CompiledValue? {
|
||||
val condition = compileCondition(stmt.condition, stmt.pos) ?: return null
|
||||
if (condition.type != SlotType.BOOL) return null
|
||||
val resultSlot = allocSlot()
|
||||
val elseLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_FALSE,
|
||||
listOf(BytecodeBuilder.Operand.IntVal(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel))
|
||||
)
|
||||
val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null
|
||||
val thenObj = ensureObjSlot(thenValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot)
|
||||
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
|
||||
builder.mark(elseLabel)
|
||||
if (stmt.elseBody != null) {
|
||||
val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null
|
||||
val elseObj = ensureObjSlot(elseValue)
|
||||
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
|
||||
} else {
|
||||
val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
|
||||
builder.emit(Opcode.CONST_OBJ, id, resultSlot)
|
||||
}
|
||||
builder.mark(endLabel)
|
||||
updateSlotType(resultSlot, SlotType.OBJ)
|
||||
return CompiledValue(resultSlot, SlotType.OBJ)
|
||||
}
|
||||
|
||||
private fun compileCondition(stmt: Statement, pos: Pos): CompiledValue? {
|
||||
return when (stmt) {
|
||||
is ExpressionStatement -> compileRefWithFallback(stmt.ref, SlotType.BOOL, stmt.pos)
|
||||
else -> {
|
||||
val slot = allocSlot()
|
||||
val id = builder.addFallback(ToBoolStatement(stmt, pos))
|
||||
builder.emit(Opcode.EVAL_FALLBACK, id, slot)
|
||||
updateSlotType(slot, SlotType.BOOL)
|
||||
CompiledValue(slot, SlotType.BOOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitMove(value: CompiledValue, dstSlot: Int) {
|
||||
when (value.type) {
|
||||
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot)
|
||||
@ -764,6 +1039,21 @@ class BytecodeCompiler(
|
||||
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) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@ -797,6 +1087,20 @@ class BytecodeCompiler(
|
||||
}
|
||||
collectScopeSlotsRef(assignValue(ref))
|
||||
}
|
||||
is AssignOpRef -> {
|
||||
collectScopeSlotsRef(ref.target)
|
||||
collectScopeSlotsRef(ref.value)
|
||||
}
|
||||
is IncDecRef -> collectScopeSlotsRef(ref.target)
|
||||
is ConditionalRef -> {
|
||||
collectScopeSlotsRef(ref.condition)
|
||||
collectScopeSlotsRef(ref.ifTrue)
|
||||
collectScopeSlotsRef(ref.ifFalse)
|
||||
}
|
||||
is ElvisRef -> {
|
||||
collectScopeSlotsRef(ref.left)
|
||||
collectScopeSlotsRef(ref.right)
|
||||
}
|
||||
is CallRef -> {
|
||||
collectScopeSlotsRef(ref.target)
|
||||
collectScopeSlotsArgs(ref.args)
|
||||
|
||||
@ -25,4 +25,5 @@ sealed class BytecodeConst {
|
||||
data class RealVal(val value: Double) : BytecodeConst()
|
||||
data class StringVal(val value: String) : BytecodeConst()
|
||||
data class ObjRef(val value: Obj) : BytecodeConst()
|
||||
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ object BytecodeDisassembler {
|
||||
|
||||
private fun operandKinds(op: Opcode): List<OperandKind> {
|
||||
return when (op) {
|
||||
Opcode.NOP, Opcode.RET_VOID -> emptyList()
|
||||
Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> 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,6 +91,8 @@ 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 ->
|
||||
listOf(OperandKind.CONST)
|
||||
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,
|
||||
|
||||
@ -22,7 +22,9 @@ import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.*
|
||||
|
||||
class BytecodeVm {
|
||||
suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj {
|
||||
suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj {
|
||||
val scopeStack = ArrayDeque<Scope>()
|
||||
var scope = scope0
|
||||
val frame = BytecodeFrame(fn.localCount, args.size)
|
||||
for (i in args.indices) {
|
||||
frame.setObj(frame.argBase + i, args[i])
|
||||
@ -721,6 +723,21 @@ class BytecodeVm {
|
||||
ip = target
|
||||
}
|
||||
}
|
||||
Opcode.PUSH_SCOPE -> {
|
||||
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Opcode.POP_SCOPE -> {
|
||||
scope = scopeStack.removeLastOrNull()
|
||||
?: error("Scope stack underflow in POP_SCOPE")
|
||||
}
|
||||
Opcode.CALL_SLOT -> {
|
||||
val calleeSlot = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
|
||||
@ -107,6 +107,8 @@ enum class Opcode(val code: Int) {
|
||||
JMP_IF_FALSE(0x82),
|
||||
RET(0x83),
|
||||
RET_VOID(0x84),
|
||||
PUSH_SCOPE(0x85),
|
||||
POP_SCOPE(0x86),
|
||||
|
||||
CALL_DIRECT(0x90),
|
||||
CALL_VIRTUAL(0x91),
|
||||
|
||||
@ -385,9 +385,9 @@ class BinaryOpRef(internal val op: BinOp, internal val left: ObjRef, internal va
|
||||
|
||||
/** Conditional (ternary) operator reference: cond ? a : b */
|
||||
class ConditionalRef(
|
||||
private val condition: ObjRef,
|
||||
private val ifTrue: ObjRef,
|
||||
private val ifFalse: ObjRef
|
||||
internal val condition: ObjRef,
|
||||
internal val ifTrue: ObjRef,
|
||||
internal val ifFalse: ObjRef
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
return evalCondition(scope).get(scope)
|
||||
@ -661,9 +661,9 @@ class QualifiedThisMethodSlotCallRef(
|
||||
|
||||
/** Assignment compound op: target op= value */
|
||||
class AssignOpRef(
|
||||
private val op: BinOp,
|
||||
private val target: ObjRef,
|
||||
private val value: ObjRef,
|
||||
internal val op: BinOp,
|
||||
internal val target: ObjRef,
|
||||
internal val value: ObjRef,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
@ -723,9 +723,9 @@ class AssignOpRef(
|
||||
|
||||
/** Pre/post ++/-- on l-values */
|
||||
class IncDecRef(
|
||||
private val target: ObjRef,
|
||||
private val isIncrement: Boolean,
|
||||
private val isPost: Boolean,
|
||||
internal val target: ObjRef,
|
||||
internal val isIncrement: Boolean,
|
||||
internal val isPost: Boolean,
|
||||
private val atPos: Pos,
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
@ -751,7 +751,7 @@ class IncDecRef(
|
||||
}
|
||||
|
||||
/** Elvis operator reference: a ?: b */
|
||||
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef {
|
||||
class ElvisRef(internal val left: ObjRef, internal val right: ObjRef) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
val a = left.evalValue(scope)
|
||||
val r = if (a != ObjNull) a else right.evalValue(scope)
|
||||
|
||||
@ -19,8 +19,15 @@ package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjIterable
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjRange
|
||||
import net.sergeych.lyng.obj.ObjRecord
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.obj.toBool
|
||||
import net.sergeych.lyng.obj.toInt
|
||||
import net.sergeych.lyng.obj.toLong
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
@ -79,6 +86,217 @@ class IfStatement(
|
||||
}
|
||||
}
|
||||
|
||||
data class ConstIntRange(val start: Long, val endExclusive: Long)
|
||||
|
||||
class ForInStatement(
|
||||
val loopVarName: String,
|
||||
val source: Statement,
|
||||
val constRange: ConstIntRange?,
|
||||
val body: Statement,
|
||||
val elseStatement: Statement?,
|
||||
val label: String?,
|
||||
val canBreak: Boolean,
|
||||
val loopSlotPlan: Map<String, Int>,
|
||||
override val pos: Pos,
|
||||
) : Statement() {
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val forContext = scope.createChildScope(pos)
|
||||
if (loopSlotPlan.isNotEmpty()) {
|
||||
forContext.applySlotPlan(loopSlotPlan)
|
||||
}
|
||||
|
||||
val loopSO = forContext.addItem(loopVarName, true, ObjNull)
|
||||
val loopSlotIndex = forContext.getSlotIndexOf(loopVarName) ?: -1
|
||||
|
||||
if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
return loopIntRange(
|
||||
forContext,
|
||||
constRange.start,
|
||||
constRange.endExclusive,
|
||||
loopSO,
|
||||
loopSlotIndex,
|
||||
body,
|
||||
elseStatement,
|
||||
label,
|
||||
canBreak
|
||||
)
|
||||
}
|
||||
|
||||
val sourceObj = source.execute(forContext)
|
||||
return if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
|
||||
loopIntRange(
|
||||
forContext,
|
||||
sourceObj.start!!.toLong(),
|
||||
if (sourceObj.isEndInclusive) sourceObj.end!!.toLong() + 1 else sourceObj.end!!.toLong(),
|
||||
loopSO,
|
||||
loopSlotIndex,
|
||||
body,
|
||||
elseStatement,
|
||||
label,
|
||||
canBreak
|
||||
)
|
||||
} else if (sourceObj.isInstanceOf(ObjIterable)) {
|
||||
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
|
||||
} else {
|
||||
val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
pos,
|
||||
"object is not enumerable: no size in $sourceObj",
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
var result: Obj = ObjVoid
|
||||
var breakCaught = false
|
||||
|
||||
if (size > 0) {
|
||||
var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
pos,
|
||||
"object is not enumerable: no index access for ${sourceObj.inspect(scope)}",
|
||||
it
|
||||
)
|
||||
}
|
||||
var index = 0
|
||||
while (true) {
|
||||
loopSO.value = current
|
||||
try {
|
||||
result = body.execute(forContext)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
breakCaught = true
|
||||
if (lbe.doContinue) continue
|
||||
result = lbe.result
|
||||
break
|
||||
} else {
|
||||
throw lbe
|
||||
}
|
||||
}
|
||||
if (++index >= size) break
|
||||
current = sourceObj.getAt(forContext, ObjInt.of(index.toLong()))
|
||||
}
|
||||
}
|
||||
if (!breakCaught && elseStatement != null) {
|
||||
result = elseStatement.execute(scope)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loopIntRange(
|
||||
forScope: Scope,
|
||||
start: Long,
|
||||
end: Long,
|
||||
loopVar: ObjRecord,
|
||||
loopSlotIndex: Int,
|
||||
body: Statement,
|
||||
elseStatement: Statement?,
|
||||
label: String?,
|
||||
catchBreak: Boolean,
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
val cacheLow = ObjInt.CACHE_LOW
|
||||
val cacheHigh = ObjInt.CACHE_HIGH
|
||||
val useCache = start >= cacheLow && end <= cacheHigh + 1
|
||||
val cache = if (useCache) ObjInt.cacheArray() else null
|
||||
val useSlot = loopSlotIndex >= 0
|
||||
if (catchBreak) {
|
||||
if (useCache && cache != null) {
|
||||
var i = start
|
||||
while (i < end) {
|
||||
val v = cache[(i - cacheLow).toInt()]
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
return lbe.result
|
||||
}
|
||||
throw lbe
|
||||
}
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
val v = ObjInt.of(i)
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
if (lbe.doContinue) continue
|
||||
return lbe.result
|
||||
}
|
||||
throw lbe
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (useCache && cache != null) {
|
||||
var i = start
|
||||
while (i < end) {
|
||||
val v = cache[(i - cacheLow).toInt()]
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
result = body.execute(forScope)
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
for (i in start..<end) {
|
||||
val v = ObjInt.of(i)
|
||||
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
|
||||
result = body.execute(forScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
return elseStatement?.execute(forScope) ?: result
|
||||
}
|
||||
|
||||
private suspend fun loopIterable(
|
||||
forScope: Scope,
|
||||
sourceObj: Obj,
|
||||
loopVar: ObjRecord,
|
||||
body: Statement,
|
||||
elseStatement: Statement?,
|
||||
label: String?,
|
||||
catchBreak: Boolean,
|
||||
): Obj {
|
||||
var result: Obj = ObjVoid
|
||||
var breakCaught = false
|
||||
sourceObj.enumerate(forScope) { item ->
|
||||
loopVar.value = item
|
||||
if (catchBreak) {
|
||||
try {
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
} catch (lbe: LoopBreakContinueException) {
|
||||
if (lbe.label == label || lbe.label == null) {
|
||||
breakCaught = true
|
||||
if (lbe.doContinue) true else {
|
||||
result = lbe.result
|
||||
false
|
||||
}
|
||||
} else {
|
||||
throw lbe
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = body.execute(forScope)
|
||||
true
|
||||
}
|
||||
}
|
||||
if (!breakCaught && elseStatement != null) {
|
||||
result = elseStatement.execute(forScope)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class ToBoolStatement(
|
||||
val expr: Statement,
|
||||
override val pos: Pos,
|
||||
|
||||
11
notes/bytecode_exprs_loops.md
Normal file
11
notes/bytecode_exprs_loops.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Bytecode expression + for-in loop support
|
||||
|
||||
Changes
|
||||
- Added bytecode compilation for conditional/elvis expressions, inc/dec, and compound assignments where safe.
|
||||
- Added ForInStatement and ConstIntRange to keep for-loop structure explicit (no anonymous Statement).
|
||||
- Added PUSH_SCOPE/POP_SCOPE opcodes with SlotPlan constants to create loop scopes in bytecode.
|
||||
- Bytecode compiler emits int-range for-in loops when const range is known and no break/continue.
|
||||
|
||||
Tests
|
||||
- ./gradlew :lynglib:jvmTest
|
||||
- ./gradlew :lynglib:allTests -x :lynglib:jvmTest
|
||||
Loading…
x
Reference in New Issue
Block a user