Add list literal opcode and bytecode wrappers

This commit is contained in:
Sergey Chernov 2026-01-28 22:35:14 +03:00
parent aebe0890d8
commit a4fc5ac6d5
14 changed files with 271 additions and 82 deletions

View File

@ -0,0 +1,30 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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 ClassDeclStatement(
private val delegate: Statement,
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope)
}
}

View File

@ -251,6 +251,10 @@ class Compiler(
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
// Track locals at script level for fast local refs
val needsSlotPlan = slotPlanStack.isEmpty()
if (needsSlotPlan) {
slotPlanStack.add(SlotPlan(mutableMapOf(), 0))
}
return try {
withLocalNames(emptySet()) {
// package level declarations
@ -360,6 +364,9 @@ class Compiler(
)
}
} finally {
if (needsSlotPlan) {
slotPlanStack.removeLast()
}
}
}
@ -387,9 +394,51 @@ class Compiler(
private val currentRangeParamNames: Set<String>
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
private fun containsLoopControl(stmt: Statement, inLoop: Boolean = false): Boolean {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
return when (target) {
is BreakStatement, is ContinueStatement -> !inLoop
is IfStatement -> {
containsLoopControl(target.ifBody, inLoop) ||
(target.elseBody?.let { containsLoopControl(it, inLoop) } ?: false)
}
is ForInStatement -> {
containsLoopControl(target.body, true) ||
(target.elseStatement?.let { containsLoopControl(it, inLoop) } ?: false)
}
is WhileStatement -> {
containsLoopControl(target.body, true) ||
(target.elseStatement?.let { containsLoopControl(it, inLoop) } ?: false)
}
is DoWhileStatement -> {
containsLoopControl(target.body, true) ||
(target.elseStatement?.let { containsLoopControl(it, inLoop) } ?: false)
}
is BlockStatement -> target.statements().any { containsLoopControl(it, inLoop) }
is VarDeclStatement -> target.initializer?.let { containsLoopControl(it, inLoop) } ?: false
is ReturnStatement, is ThrowStatement, is ExpressionStatement -> false
else -> false
}
}
private fun wrapBytecode(stmt: Statement): Statement {
if (!useBytecodeStatements) return stmt
val allowLocals = codeContexts.lastOrNull() is CodeContext.Function
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
return stmt
}
if (stmt is FunctionDeclStatement ||
stmt is ClassDeclStatement ||
stmt is EnumDeclStatement ||
stmt is BreakStatement ||
stmt is ContinueStatement ||
stmt is ReturnStatement
) {
return stmt
}
if (containsLoopControl(stmt)) {
return stmt
}
val allowLocals = codeContexts.lastOrNull() !is CodeContext.ClassBody
val returnLabels = returnLabelStack.lastOrNull() ?: emptySet()
return BytecodeStatement.wrap(
stmt,
@ -738,11 +787,7 @@ class Compiler(
isCall = true
val lambda = parseLambdaExpression()
val argPos = next.pos
val argStmt = object : Statement() {
override val pos: Pos = argPos
override suspend fun execute(scope: Scope): Obj = lambda.get(scope).value
}
val args = listOf(ParsedArgument(argStmt, next.pos))
val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos))
operand = when (left) {
is LocalVarRef -> if (left.name == "this") {
ThisMethodSlotCallRef(next.value, args, true, isOptional)
@ -1421,11 +1466,7 @@ class Compiler(
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
val localVar = LocalVarRef(name, t1.pos)
val argPos = t1.pos
val argStmt = object : Statement() {
override val pos: Pos = argPos
override suspend fun execute(scope: Scope): Obj = localVar.evalValue(scope)
}
return ParsedArgument(argStmt, t1.pos, isSplat = false, name = name)
return ParsedArgument(ExpressionStatement(localVar, argPos), t1.pos, isSplat = false, name = name)
}
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
@ -1468,11 +1509,7 @@ class Compiler(
// last argument - callable
val callableAccessor = parseLambdaExpression()
args += ParsedArgument(
// transform ObjRef to the callable value
object : Statement() {
override val pos: Pos = end.pos
override suspend fun execute(scope: Scope): Obj = callableAccessor.get(scope).value
},
ExpressionStatement(callableAccessor, end.pos),
end.pos
)
lastBlockArgument = true
@ -1499,11 +1536,7 @@ class Compiler(
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
val localVar = LocalVarRef(name, t1.pos)
val argPos = t1.pos
val argStmt = object : Statement() {
override val pos: Pos = argPos
override suspend fun execute(scope: Scope): Obj = localVar.evalValue(scope)
}
return ParsedArgument(argStmt, t1.pos, isSplat = false, name = name)
return ParsedArgument(ExpressionStatement(localVar, argPos), t1.pos, isSplat = false, name = name)
}
val rhs = parseExpression() ?: t2.raiseSyntax("expected expression after named argument '${name}:'")
return ParsedArgument(rhs, t1.pos, isSplat = false, name = name)
@ -1555,11 +1588,7 @@ class Compiler(
// into the lambda body. This ensures expected order:
// foo { ... }.bar() == (foo { ... }).bar()
val callableAccessor = parseLambdaExpression()
val argStmt = object : Statement() {
override val pos: Pos = cc.currentPos()
override suspend fun execute(scope: Scope): Obj = callableAccessor.get(scope).value
}
listOf(ParsedArgument(argStmt, cc.currentPos()))
listOf(ParsedArgument(ExpressionStatement(callableAccessor, cc.currentPos()), cc.currentPos()))
} else {
val r = parseArgs()
detectedBlockArgument = r.second
@ -2320,7 +2349,7 @@ class Compiler(
)
val stmtPos = startPos
return object : Statement() {
val enumDeclStatement = object : Statement() {
override val pos: Pos = stmtPos
override suspend fun execute(scope: Scope): Obj {
val enumClass = ObjEnumClass.createSimpleEnum(nameToken.value, names)
@ -2328,6 +2357,7 @@ class Compiler(
return enumClass
}
}
return EnumDeclStatement(enumDeclStatement, stmtPos)
}
private suspend fun parseObjectDeclaration(isExtern: Boolean = false): Statement {
@ -2586,7 +2616,7 @@ class Compiler(
return instance
}
}
object : Statement() {
val classDeclStatement = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
// the main statement should create custom ObjClass instance with field
@ -2643,6 +2673,7 @@ class Compiler(
return newClass
}
}
ClassDeclStatement(classDeclStatement, startPos)
}
}
@ -3279,11 +3310,12 @@ class Compiler(
return annotatedFnBody
}
}
val declaredFn = FunctionDeclStatement(fnCreateStatement, start)
if (isStatic) {
currentInitScope += fnCreateStatement
currentInitScope += declaredFn
NopStatement
} else
fnCreateStatement
declaredFn
}.also {
val bodyRange = lastParsedBlockRange
// Also emit a post-parse MiniFunDecl to be robust in case early emission was skipped by some path
@ -3823,43 +3855,26 @@ class Compiler(
}
}
return object : Statement() {
override val pos: Pos = start
override suspend fun execute(context: Scope): Obj {
if (extTypeName != null) {
val prop = if (getter != null || setter != null) {
ObjProperty(name, getter, setter)
} else {
// Simple val extension with initializer
val initExpr = initialExpression ?: throw ScriptError(start, "Extension val must be initialized")
ObjProperty(
name,
object : Statement() {
override val pos: Pos = initExpr.pos
override suspend fun execute(scp: Scope): Obj = initExpr.execute(scp)
},
null
)
ObjProperty(name, initExpr, null)
}
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
context.addExtension(
type,
name,
ObjRecord(
prop,
isMutable = false,
return ExtensionPropertyDeclStatement(
extTypeName = extTypeName,
property = prop,
visibility = visibility,
writeVisibility = setterVisibility,
declaringClass = null,
type = ObjRecord.Type.Property
setterVisibility = setterVisibility,
startPos = start
)
)
return prop
}
return object : Statement() {
override val pos: Pos = start
override suspend fun execute(context: Scope): Obj {
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
// Do NOT infer declaring class from runtime thisObj here; only the compile-time captured
// ClassBody qualifies for class-field storage. Otherwise, this is a plain local.

View File

@ -0,0 +1,30 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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 EnumDeclStatement(
private val delegate: Statement,
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope)
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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.ObjClass
import net.sergeych.lyng.obj.ObjProperty
import net.sergeych.lyng.obj.ObjRecord
class ExtensionPropertyDeclStatement(
val extTypeName: String,
val property: ObjProperty,
val visibility: Visibility,
val setterVisibility: Visibility?,
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
override suspend fun execute(context: Scope): Obj {
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
context.addExtension(
type,
property.name,
ObjRecord(
property,
isMutable = false,
visibility = visibility,
writeVisibility = setterVisibility,
declaringClass = null,
type = ObjRecord.Type.Property
)
)
return property
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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 FunctionDeclStatement(
private val delegate: Statement,
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
return delegate.execute(scope)
}
}

View File

@ -31,6 +31,7 @@ sealed class BytecodeConst {
data class ObjRef(val value: Obj) : BytecodeConst()
data class Ref(val value: net.sergeych.lyng.obj.ObjRef) : BytecodeConst()
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
data class ListLiteralPlan(val spreads: List<Boolean>) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
data class ExtensionPropertyDecl(
val extTypeName: String,

View File

@ -100,6 +100,7 @@ class BytecodeStatement private constructor(
target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false
is net.sergeych.lyng.ThrowStatement ->
containsUnsupportedStatement(target.throwExpr)
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> false
else -> true
}
}

View File

@ -182,6 +182,8 @@ class CmdBuilder {
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.EVAL_FALLBACK, Opcode.EVAL_REF, Opcode.EVAL_STMT ->
listOf(OperandKind.ID, OperandKind.SLOT)
}
@ -371,6 +373,7 @@ class CmdBuilder {
Opcode.GET_NAME -> CmdGetName(operands[0], operands[1])
Opcode.GET_INDEX -> CmdGetIndex(operands[0], operands[1], operands[2])
Opcode.SET_INDEX -> CmdSetIndex(operands[0], operands[1], operands[2])
Opcode.LIST_LITERAL -> CmdListLiteral(operands[0], operands[1], operands[2], operands[3])
Opcode.EVAL_FALLBACK -> CmdEvalFallback(operands[0], operands[1])
Opcode.EVAL_REF -> CmdEvalRef(operands[0], operands[1])
Opcode.EVAL_STMT -> CmdEvalStmt(operands[0], operands[1])

View File

@ -183,6 +183,7 @@ object CmdDisassembler {
is CmdGetName -> Opcode.GET_NAME to intArrayOf(cmd.nameId, cmd.dst)
is CmdGetIndex -> Opcode.GET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst)
is CmdSetIndex -> Opcode.SET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot)
is CmdListLiteral -> Opcode.LIST_LITERAL to intArrayOf(cmd.planId, cmd.baseSlot, cmd.count, cmd.dst)
is CmdEvalFallback -> Opcode.EVAL_FALLBACK to intArrayOf(cmd.id, cmd.dst)
is CmdEvalRef -> Opcode.EVAL_REF to intArrayOf(cmd.id, cmd.dst)
is CmdEvalStmt -> Opcode.EVAL_STMT to intArrayOf(cmd.id, cmd.dst)
@ -265,6 +266,8 @@ object CmdDisassembler {
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.LIST_LITERAL ->
listOf(OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.EVAL_FALLBACK, Opcode.EVAL_REF, Opcode.EVAL_STMT ->
listOf(OperandKind.ID, OperandKind.SLOT)
}

View File

@ -1223,6 +1223,35 @@ class CmdGetName(
}
}
class CmdListLiteral(
internal val planId: Int,
internal val baseSlot: Int,
internal val count: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val plan = frame.fn.constants.getOrNull(planId) as? BytecodeConst.ListLiteralPlan
?: error("LIST_LITERAL expects ListLiteralPlan at $planId")
val list = ArrayList<Obj>(count)
for (i in 0 until count) {
val value = frame.slotToObj(baseSlot + i)
if (plan.spreads.getOrNull(i) == true) {
when (value) {
is ObjList -> {
list.ensureCapacity(list.size + value.list.size)
list.addAll(value.list)
}
else -> frame.scope.raiseError("Spread element must be list")
}
} else {
list.add(value)
}
}
frame.storeObjResult(dst, ObjList(list))
return
}
}
class CmdSetField(
internal val recvSlot: Int,
internal val fieldId: Int,

View File

@ -130,6 +130,7 @@ enum class Opcode(val code: Int) {
GET_INDEX(0xA2),
SET_INDEX(0xA3),
GET_NAME(0xA4),
LIST_LITERAL(0xA5),
EVAL_FALLBACK(0xB0),
RESOLVE_SCOPE_SLOT(0xB1),

View File

@ -1,10 +1,8 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Ignore
import kotlin.test.Test
@Ignore("TODO(bytecode-only): uses fallback")
class IfNullAssignTest {
@Test

View File

@ -21,10 +21,8 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Ignore
import kotlin.test.Test
@Ignore("TODO(bytecode-only): uses fallback")
class ScriptTest_OptionalAssign {
@Test

View File

@ -249,20 +249,6 @@ fun List.sort() {
sortWith { a, b -> a <=> b }
}
/* Represents a single stack trace element. */
class StackTraceEntry(
val sourceName: String,
val line: Int,
val column: Int,
val sourceString: String
) {
val at by lazy { "%s:%s:%s"(sourceName,line+1,column+1) }
/* Formatted representation: source:line:column: text. */
override fun toString() {
"%s: %s"(at, sourceString.trim())
}
}
/* Print this exception and its stack trace to standard output. */
fun Exception.printStackTrace() {
println(this)
@ -337,3 +323,17 @@ class lazy(creatorParam) : Delegate {
value
}
}
/* Represents a single stack trace element. */
class StackTraceEntry(
val sourceName: String,
val line: Int,
val column: Int,
val sourceString: String
) {
val at by lazy { "%s:%s:%s"(sourceName,line+1,column+1) }
/* Formatted representation: source:line:column: text. */
override fun toString() {
"%s: %s"(at, sourceString.trim())
}
}