5777 lines
262 KiB
Kotlin

/*
* 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.Compiler.Companion.compile
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.bytecode.CmdListLiteral
import net.sergeych.lyng.bytecode.CmdMakeRange
import net.sergeych.lyng.bytecode.CmdRangeIntBounds
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
import net.sergeych.lyng.resolution.CompileTimeResolver
import net.sergeych.lyng.resolution.ResolutionReport
import net.sergeych.lyng.resolution.ResolutionSink
import net.sergeych.lyng.resolution.ScopeKind
import net.sergeych.lyng.resolution.SymbolKind
/**
* The LYNG compiler.
*/
class Compiler(
val cc: CompilerContext,
val importManager: ImportProvider,
@Suppress("UNUSED_PARAMETER")
settings: Settings = Settings()
) {
// Stack of parameter-to-slot plans for current function being parsed (by declaration index)
@Suppress("unused")
private val paramSlotPlanStack = mutableListOf<Map<String, Int>>()
// private val currentParamSlotPlan: Map<String, Int>?
// get() = paramSlotPlanStack.lastOrNull()
// Track identifiers known to be locals/parameters in the current function for fast local emission
private val localNamesStack = mutableListOf<MutableSet<String>>()
private val currentLocalNames: MutableSet<String>?
get() = localNamesStack.lastOrNull()
private data class SlotEntry(val index: Int, val isMutable: Boolean, val isDelegated: Boolean)
private data class SlotPlan(val slots: MutableMap<String, SlotEntry>, var nextIndex: Int, val id: Int)
private data class SlotLocation(
val slot: Int,
val depth: Int,
val scopeId: Int,
val isMutable: Boolean,
val isDelegated: Boolean
)
private val slotPlanStack = mutableListOf<SlotPlan>()
private var nextScopeId = 0
// Track declared local variables count per function for precise capacity hints
private val localDeclCountStack = mutableListOf<Int>()
private val currentLocalDeclCount: Int
get() = localDeclCountStack.lastOrNull() ?: 0
private inline fun <T> withLocalNames(names: Set<String>, block: () -> T): T {
localNamesStack.add(names.toMutableSet())
return try {
block()
} finally {
localNamesStack.removeLast()
}
}
private fun declareLocalName(name: String, isMutable: Boolean, isDelegated: Boolean = false) {
// Add to current function's local set; only count if it was newly added (avoid duplicates)
val added = currentLocalNames?.add(name) == true
if (added && localDeclCountStack.isNotEmpty()) {
localDeclCountStack[localDeclCountStack.lastIndex] = currentLocalDeclCount + 1
}
declareSlotName(name, isMutable, isDelegated)
}
private fun declareSlotName(name: String, isMutable: Boolean, isDelegated: Boolean) {
if (codeContexts.lastOrNull() is CodeContext.ClassBody) return
val plan = slotPlanStack.lastOrNull() ?: return
if (plan.slots.containsKey(name)) return
plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated)
plan.nextIndex += 1
}
private fun declareSlotNameIn(plan: SlotPlan, name: String, isMutable: Boolean, isDelegated: Boolean) {
if (plan.slots.containsKey(name)) return
plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated)
plan.nextIndex += 1
}
private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull()
private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) {
val plan = moduleSlotPlan() ?: return
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
}
for ((name, slotIndex) in current.slotNameToIndexSnapshot()) {
val record = current.getSlotRecord(slotIndex)
if (!record.visibility.isPublic) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
}
if (!includeParents) return
current = current.parent
}
}
private fun predeclareTopLevelSymbols() {
val plan = moduleSlotPlan() ?: return
val saved = cc.savePos()
var depth = 0
fun nextNonWs(): Token {
var t = cc.next()
while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
t = cc.next()
}
return t
}
try {
while (cc.hasNext()) {
val t = cc.next()
when (t.type) {
Token.Type.LBRACE -> depth++
Token.Type.RBRACE -> if (depth > 0) depth--
Token.Type.ID -> if (depth == 0) {
when (t.value) {
"fun", "fn" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) continue
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionCallableName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
}
continue
}
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
}
"val", "var" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) continue
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionPropertyGetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
if (t.value == "var") {
declareSlotNameIn(plan, extensionPropertySetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
}
}
continue
}
declareSlotNameIn(plan, nameToken.value, isMutable = t.value == "var", isDelegated = false)
}
"class", "object" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
}
}
"enum" -> {
val next = nextNonWs()
val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
}
}
}
}
else -> {}
}
}
} finally {
cc.restorePos(saved)
}
}
private fun predeclareClassMembers(target: MutableSet<String>, overrides: MutableMap<String, Boolean>) {
val saved = cc.savePos()
var depth = 0
val modifiers = setOf(
"public", "private", "protected", "internal",
"override", "abstract", "extern", "static", "transient"
)
fun nextNonWs(): Token {
var t = cc.next()
while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
t = cc.next()
}
return t
}
try {
while (cc.hasNext()) {
var t = cc.next()
when (t.type) {
Token.Type.LBRACE -> depth++
Token.Type.RBRACE -> if (depth == 0) break else depth--
Token.Type.ID -> if (depth == 0) {
var sawOverride = false
while (t.type == Token.Type.ID && t.value in modifiers) {
if (t.value == "override") sawOverride = true
t = nextNonWs()
}
when (t.value) {
"fun", "fn", "val", "var" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
val afterName = cc.peekNextNonWhitespace()
if (afterName.type != Token.Type.DOT) {
target.add(nameToken.value)
overrides[nameToken.value] = sawOverride
}
}
}
"class", "object" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
}
}
"enum" -> {
val next = nextNonWs()
val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
}
}
}
}
else -> {}
}
}
} finally {
cc.restorePos(saved)
}
}
private fun resolveCompileClassInfo(name: String): CompileClassInfo? {
compileClassInfos[name]?.let { return it }
val scopeRec = seedScope?.get(name) ?: importManager.rootScope.get(name)
val cls = scopeRec?.value as? ObjClass ?: return null
val fieldIds = cls.instanceFieldIdMap()
val methodIds = cls.instanceMethodIdMap(includeAbstract = true)
val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1
val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1
return CompileClassInfo(name, fieldIds, methodIds, nextFieldId, nextMethodId)
}
private data class BaseMemberIds(
val fieldIds: Map<String, Int>,
val methodIds: Map<String, Int>,
val fieldConflicts: Set<String>,
val methodConflicts: Set<String>,
val nextFieldId: Int,
val nextMethodId: Int
)
private fun collectBaseMemberIds(baseNames: List<String>): BaseMemberIds {
val allBaseNames = if (baseNames.contains("Object")) baseNames else baseNames + "Object"
val fieldIds = mutableMapOf<String, Int>()
val methodIds = mutableMapOf<String, Int>()
val fieldConflicts = mutableSetOf<String>()
val methodConflicts = mutableSetOf<String>()
var maxFieldId = -1
var maxMethodId = -1
for (base in allBaseNames) {
val info = resolveCompileClassInfo(base) ?: continue
for ((name, id) in info.fieldIds) {
val prev = fieldIds.putIfAbsent(name, id)
if (prev != null && prev != id) fieldConflicts.add(name)
if (id > maxFieldId) maxFieldId = id
}
for ((name, id) in info.methodIds) {
val prev = methodIds.putIfAbsent(name, id)
if (prev != null && prev != id) methodConflicts.add(name)
if (id > maxMethodId) maxMethodId = id
}
}
return BaseMemberIds(
fieldIds = fieldIds,
methodIds = methodIds,
fieldConflicts = fieldConflicts,
methodConflicts = methodConflicts,
nextFieldId = maxFieldId + 1,
nextMethodId = maxMethodId + 1
)
}
private fun buildParamSlotPlan(names: List<String>): SlotPlan {
val map = mutableMapOf<String, Int>()
var idx = 0
for (name in names) {
if (!map.containsKey(name)) {
map[name] = idx
idx++
}
}
val entries = mutableMapOf<String, SlotEntry>()
for ((name, index) in map) {
entries[name] = SlotEntry(index, isMutable = false, isDelegated = false)
}
return SlotPlan(entries, idx, nextScopeId++)
}
private fun markDelegatedSlot(name: String) {
val plan = slotPlanStack.lastOrNull() ?: return
val entry = plan.slots[name] ?: return
if (!entry.isDelegated) {
plan.slots[name] = entry.copy(isDelegated = true)
}
}
private fun slotPlanIndices(plan: SlotPlan): Map<String, Int> {
if (plan.slots.isEmpty()) return emptyMap()
val result = LinkedHashMap<String, Int>(plan.slots.size)
for ((name, entry) in plan.slots) {
result[name] = entry.index
}
return result
}
private fun callSignatureForName(name: String): CallSignature? {
seedScope?.getLocalRecordDirect(name)?.callSignature?.let { return it }
return seedScope?.get(name)?.callSignature
?: importManager.rootScope.getLocalRecordDirect(name)?.callSignature
}
internal data class MemberIds(val fieldId: Int?, val methodId: Int?)
private fun resolveMemberIds(name: String, pos: Pos, qualifier: String? = null): MemberIds {
val ctx = if (qualifier == null) {
codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
} else null
if (ctx != null) {
val fieldId = ctx.memberFieldIds[name]
val methodId = ctx.memberMethodIds[name]
if (fieldId == null && methodId == null) {
if (allowUnresolvedRefs) return MemberIds(null, null)
throw ScriptError(pos, "unknown member $name")
}
return MemberIds(fieldId, methodId)
}
if (qualifier != null) {
val info = resolveCompileClassInfo(qualifier)
?: if (allowUnresolvedRefs) return MemberIds(null, null) else throw ScriptError(pos, "unknown type $qualifier")
val fieldId = info.fieldIds[name]
val methodId = info.methodIds[name]
if (fieldId == null && methodId == null) {
if (allowUnresolvedRefs) return MemberIds(null, null)
throw ScriptError(pos, "unknown member $name on $qualifier")
}
return MemberIds(fieldId, methodId)
}
if (allowUnresolvedRefs) return MemberIds(null, null)
throw ScriptError(pos, "member $name is not available without class context")
}
private fun tailBlockReceiverType(left: ObjRef): String? {
val name = when (left) {
is LocalVarRef -> left.name
is LocalSlotRef -> left.name
is ImplicitThisMemberRef -> left.name
else -> null
}
if (name == null) return null
val signature = callSignatureForName(name)
return signature?.tailBlockReceiverType ?: if (name == "flow") "FlowBuilder" else null
}
private fun currentImplicitThisTypeName(): String? {
for (ctx in codeContexts.asReversed()) {
val fn = ctx as? CodeContext.Function ?: continue
if (fn.implicitThisTypeName != null) return fn.implicitThisTypeName
}
return null
}
private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? {
for (i in slotPlanStack.indices.reversed()) {
if (!includeModule && i == 0) continue
val slot = slotPlanStack[i].slots[name] ?: continue
val depth = slotPlanStack.size - 1 - i
return SlotLocation(slot.index, depth, slotPlanStack[i].id, slot.isMutable, slot.isDelegated)
}
return null
}
private fun resolveIdentifierRef(name: String, pos: Pos): ObjRef {
if (name == "__PACKAGE__") {
resolutionSink?.reference(name, pos)
val value = ObjString(packageName ?: "unknown").asReadonly
return ConstRef(value)
}
if (name == "this") {
resolutionSink?.reference(name, pos)
return LocalVarRef(name, pos)
}
val slotLoc = lookupSlotLocation(name, includeModule = false)
if (slotLoc != null) {
val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
if (slotLoc.depth > 0 &&
classCtx?.slotPlanId == slotLoc.scopeId &&
classCtx.declaredMembers.contains(name)
) {
resolutionSink?.referenceMember(name, pos)
val ids = resolveMemberIds(name, pos, null)
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName())
}
captureLocalRef(name, slotLoc, pos)?.let { ref ->
resolutionSink?.reference(name, pos)
return ref
}
val captureOwner = capturePlanStack.lastOrNull()?.captureOwners?.get(name)
if (slotLoc.depth == 0 && captureOwner != null) {
val ref = LocalSlotRef(
name,
slotLoc.slot,
slotLoc.scopeId,
slotLoc.isMutable,
slotLoc.isDelegated,
pos,
strictSlotRefs,
captureOwnerScopeId = captureOwner.scopeId,
captureOwnerSlot = captureOwner.slot
)
resolutionSink?.reference(name, pos)
return ref
}
val ref = LocalSlotRef(
name,
slotLoc.slot,
slotLoc.scopeId,
slotLoc.isMutable,
slotLoc.isDelegated,
pos,
strictSlotRefs
)
resolutionSink?.reference(name, pos)
return ref
}
val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
if (classCtx != null && classCtx.declaredMembers.contains(name)) {
resolutionSink?.referenceMember(name, pos)
val ids = resolveMemberIds(name, pos, null)
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName())
}
val modulePlan = moduleSlotPlan()
val moduleEntry = modulePlan?.slots?.get(name)
if (moduleEntry != null) {
val moduleLoc = SlotLocation(
moduleEntry.index,
slotPlanStack.size - 1,
modulePlan.id,
moduleEntry.isMutable,
moduleEntry.isDelegated
)
captureLocalRef(name, moduleLoc, pos)?.let { ref ->
resolutionSink?.reference(name, pos)
return ref
}
val ref = LocalSlotRef(
name,
moduleLoc.slot,
moduleLoc.scopeId,
moduleLoc.isMutable,
moduleLoc.isDelegated,
pos,
strictSlotRefs
)
resolutionSink?.reference(name, pos)
return ref
}
val rootRecord = importManager.rootScope.objects[name]
if (rootRecord != null && rootRecord.visibility.isPublic) {
modulePlan?.let { plan ->
declareSlotNameIn(plan, name, rootRecord.isMutable, rootRecord.type == ObjRecord.Type.Delegated)
}
val rootSlot = lookupSlotLocation(name)
if (rootSlot != null) {
val ref = LocalSlotRef(
name,
rootSlot.slot,
rootSlot.scopeId,
rootSlot.isMutable,
rootSlot.isDelegated,
pos,
strictSlotRefs
)
resolutionSink?.reference(name, pos)
return ref
}
}
val implicitThis = codeContexts.any { ctx ->
(ctx as? CodeContext.Function)?.implicitThisMembers == true
}
if (implicitThis) {
val implicitType = currentImplicitThisTypeName()
resolutionSink?.referenceMember(name, pos, implicitType)
val ids = resolveImplicitThisMemberIds(name, pos, implicitType)
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType)
}
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
if (classContext && extensionNames.contains(name)) {
resolutionSink?.referenceMember(name, pos)
return LocalVarRef(name, pos)
}
resolutionSink?.reference(name, pos)
if (allowUnresolvedRefs) {
return LocalVarRef(name, pos)
}
throw ScriptError(pos, "unresolved name: $name")
}
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
class Settings(
val miniAstSink: MiniAstSink? = null,
val resolutionSink: ResolutionSink? = null,
val useBytecodeStatements: Boolean = true,
val strictSlotRefs: Boolean = true,
val allowUnresolvedRefs: Boolean = false,
val seedScope: Scope? = null,
)
// Optional sink for mini-AST streaming (null by default, zero overhead when not used)
private val miniSink: MiniAstSink? = settings.miniAstSink
private val resolutionSink: ResolutionSink? = settings.resolutionSink
private val seedScope: Scope? = settings.seedScope
private var resolutionScriptDepth = 0
private val resolutionPredeclared = mutableSetOf<String>()
// --- Doc-comment collection state (for immediate preceding declarations) ---
private val pendingDocLines = mutableListOf<String>()
private var pendingDocStart: Pos? = null
private var prevWasComment: Boolean = false
private fun stripCommentLexeme(raw: String): String {
return when {
raw.startsWith("//") -> raw.removePrefix("//")
raw.startsWith("/*") && raw.endsWith("*/") -> {
val inner = raw.substring(2, raw.length - 2)
// Trim leading "*" prefixes per line like Javadoc style
inner.lines().joinToString("\n") { line ->
val t = line.trimStart()
if (t.startsWith("*")) t.removePrefix("*").trimStart() else line
}
}
else -> raw
}
}
private fun seedResolutionFromScope(scope: Scope, pos: Pos) {
val sink = resolutionSink ?: return
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
if (!resolutionPredeclared.add(name)) continue
sink.declareSymbol(name, SymbolKind.LOCAL, record.isMutable, pos)
}
current = current.parent
}
}
private fun shouldSeedDefaultStdlib(): Boolean {
if (seedScope != null) return false
if (importManager !== Script.defaultImportManager) return false
val sourceName = cc.tokens.firstOrNull()?.pos?.source?.fileName
return sourceName != "lyng.stdlib"
}
private var anonCounter = 0
private fun generateAnonName(pos: Pos): String {
return "${"$"}${"Anon"}_${pos.line+1}_${pos.column}_${++anonCounter}"
}
private fun pushPendingDocToken(t: Token) {
val s = stripCommentLexeme(t.value)
if (pendingDocStart == null) pendingDocStart = t.pos
pendingDocLines += s
prevWasComment = true
}
private fun clearPendingDoc() {
pendingDocLines.clear()
pendingDocStart = null
prevWasComment = false
}
private fun consumePendingDoc(): MiniDoc? {
if (pendingDocLines.isEmpty()) return null
val start = pendingDocStart ?: cc.currentPos()
val doc = MiniDoc.parse(MiniRange(start, start), pendingDocLines)
clearPendingDoc()
return doc
}
private fun nextNonWhitespace(): Token {
while (true) {
val t = cc.next()
when (t.type) {
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> {
pushPendingDocToken(t)
}
Token.Type.NEWLINE -> {
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
}
Token.Type.EOF -> return t
else -> return t
}
}
}
// Set just before entering a declaration parse, taken from keyword token position
private var pendingDeclStart: Pos? = null
private var pendingDeclDoc: MiniDoc? = null
private val initStack = mutableListOf<MutableList<Statement>>()
private data class CompileClassInfo(
val name: String,
val fieldIds: Map<String, Int>,
val methodIds: Map<String, Int>,
val nextFieldId: Int,
val nextMethodId: Int
)
private val compileClassInfos = mutableMapOf<String, CompileClassInfo>()
private val compileClassStubs = mutableMapOf<String, ObjClass>()
val currentInitScope: MutableList<Statement>
get() =
initStack.lastOrNull() ?: cc.syntaxError("no initialization scope exists here")
private fun pushInitScope(): MutableList<Statement> = mutableListOf<Statement>().also { initStack.add(it) }
private fun popInitScope(): MutableList<Statement> = initStack.removeLast()
private val codeContexts = mutableListOf<CodeContext>(CodeContext.Module(null))
// Last parsed block range (for Mini-AST function body attachment)
private var lastParsedBlockRange: MiniRange? = null
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
codeContexts.add(context)
try {
val res = f()
if (context is CodeContext.ClassBody) {
if (context.pendingInitializations.isNotEmpty()) {
val (name, pos) = context.pendingInitializations.entries.first()
throw ScriptError(pos, "val '$name' must be initialized in the class body or init block")
}
}
return res
} finally {
codeContexts.removeLast()
}
}
private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
val atTopLevel = resolutionSink != null && resolutionScriptDepth == 0
if (atTopLevel) {
resolutionSink?.enterScope(ScopeKind.MODULE, start, null)
seedScope?.let { seedResolutionFromScope(it, start) }
seedResolutionFromScope(importManager.rootScope, start)
}
resolutionScriptDepth++
// Track locals at script level for fast local refs
val needsSlotPlan = slotPlanStack.isEmpty()
if (needsSlotPlan) {
slotPlanStack.add(SlotPlan(mutableMapOf(), 0, nextScopeId++))
declareSlotNameIn(slotPlanStack.last(), "__PACKAGE__", isMutable = false, isDelegated = false)
declareSlotNameIn(slotPlanStack.last(), "$~", isMutable = true, isDelegated = false)
seedScope?.let { seedSlotPlanFromScope(it, includeParents = true) }
seedSlotPlanFromScope(importManager.rootScope)
if (shouldSeedDefaultStdlib()) {
val stdlib = importManager.prepareImport(start, "lyng.stdlib", null)
seedResolutionFromScope(stdlib, start)
seedSlotPlanFromScope(stdlib)
}
predeclareTopLevelSymbols()
}
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) {
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t)
Token.Type.NEWLINE -> {
// A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
}
else -> {}
}
cc.next()
continue
}
if (t.type == Token.Type.ID) {
when (t.value) {
"package" -> {
cc.next()
val name = loadQualifiedName()
if (name.isEmpty()) throw ScriptError(cc.currentPos(), "Expecting package name here")
if (packageName != null) throw ScriptError(
cc.currentPos(),
"package name redefined, already set to $packageName"
)
packageName = name
continue
}
"import" -> {
cc.next()
val pos = cc.currentPos()
val name = loadQualifiedName()
// Emit MiniImport with approximate per-segment ranges
run {
try {
val parts = name.split('.')
if (parts.isNotEmpty()) {
var col = pos.column
val segs = parts.map { p ->
val start = Pos(pos.source, pos.line, col)
val end = Pos(pos.source, pos.line, col + p.length)
col += p.length + 1 // account for following '.' between segments
MiniImport.Segment(
p,
MiniRange(start, end)
)
}
val lastEnd = segs.last().range.end
miniSink?.onImport(
MiniImport(
MiniRange(pos, lastEnd),
segs
)
)
}
} catch (_: Throwable) {
// best-effort; ignore import mini emission failures
}
}
val module = importManager.prepareImport(pos, name, null)
seedResolutionFromScope(module, pos)
seedSlotPlanFromScope(module)
statements += object : Statement() {
override val pos: Pos = pos
override suspend fun execute(scope: Scope): Obj {
module.importInto(scope, null)
return ObjVoid
}
}
continue
}
}
// Fast-path: top-level function declarations. Handle here to ensure
// Mini-AST emission even if qualifier matcher paths change.
if (t.value == "fun" || t.value == "fn") {
// Consume the keyword and delegate to the function parser
cc.next()
pendingDeclStart = t.pos
pendingDeclDoc = consumePendingDoc()
val st = parseFunctionDeclaration(isExtern = false, isStatic = false)
statements += st
continue
}
}
val s = parseStatement(braceMeansLambda = true)?.also {
statements += it
}
if (s == null) {
when (t.type) {
Token.Type.RBRACE, Token.Type.EOF, Token.Type.SEMICOLON -> {}
else ->
throw ScriptError(t.pos, "unexpected `${t.value}` here")
}
break
}
} while (true)
val modulePlan = if (needsSlotPlan) slotPlanIndices(slotPlanStack.last()) else emptyMap()
val wrapScriptBytecode = useBytecodeStatements &&
statements.isNotEmpty() &&
codeContexts.lastOrNull() is CodeContext.Module &&
resolutionScriptDepth == 1 &&
statements.none { containsUnsupportedForBytecode(it) }
val finalStatements = if (wrapScriptBytecode) {
val unwrapped = statements.map { unwrapBytecodeDeep(it) }
val block = InlineBlockStatement(unwrapped, start)
listOf(
BytecodeStatement.wrap(
block,
"<script>",
allowLocalSlots = true,
allowedScopeNames = modulePlan.keys
)
)
} else {
statements
}
Script(start, finalStatements, modulePlan)
}.also {
// Best-effort script end notification (use current position)
miniSink?.onScriptEnd(
cc.currentPos(),
MiniScript(MiniRange(start, cc.currentPos()))
)
}
} finally {
resolutionScriptDepth--
if (atTopLevel) {
resolutionSink?.exitScope(cc.currentPos())
}
if (needsSlotPlan) {
slotPlanStack.removeLast()
}
}
}
fun loadQualifiedName(): String {
val result = StringBuilder()
var t = cc.next()
while (t.type == Token.Type.ID) {
result.append(t.value)
t = cc.next()
if (t.type == Token.Type.DOT) {
result.append('.')
t = cc.next()
}
}
cc.previous()
return result.toString()
}
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
private var isTransientFlag: Boolean = false
private var lastLabel: String? = null
private val useBytecodeStatements: Boolean = settings.useBytecodeStatements
private val strictSlotRefs: Boolean = settings.strictSlotRefs
private val allowUnresolvedRefs: Boolean = settings.allowUnresolvedRefs
private val returnLabelStack = ArrayDeque<Set<String>>()
private val rangeParamNamesStack = mutableListOf<Set<String>>()
private val extensionNames = mutableSetOf<String>()
private val extensionNamesByType = mutableMapOf<String, MutableSet<String>>()
private fun registerExtensionName(typeName: String, memberName: String) {
extensionNamesByType.getOrPut(typeName) { mutableSetOf() }.add(memberName)
}
private fun hasExtensionFor(typeName: String, memberName: String): Boolean {
if (extensionNamesByType[typeName]?.contains(memberName) == true) return true
val scopeRec = seedScope?.get(typeName) ?: importManager.rootScope.get(typeName)
val cls = (scopeRec?.value as? ObjClass) ?: resolveTypeDeclObjClass(TypeDecl.Simple(typeName, false))
if (cls != null) {
for (base in cls.mro) {
if (extensionNamesByType[base.className]?.contains(memberName) == true) return true
}
}
val candidates = mutableListOf(typeName)
cls?.mro?.forEach { candidates.add(it.className) }
for (baseName in candidates) {
val wrapperName = extensionCallableName(baseName, memberName)
if (seedScope?.get(wrapperName) != null || importManager.rootScope.get(wrapperName) != null) {
return true
}
}
return false
}
private fun resolveImplicitThisMemberIds(name: String, pos: Pos, implicitTypeName: String?): MemberIds {
if (implicitTypeName == null) return resolveMemberIds(name, pos, null)
val info = resolveCompileClassInfo(implicitTypeName)
?: if (allowUnresolvedRefs) return MemberIds(null, null) else throw ScriptError(pos, "unknown type $implicitTypeName")
val fieldId = info.fieldIds[name]
val methodId = info.methodIds[name]
if (fieldId == null && methodId == null) {
if (hasExtensionFor(implicitTypeName, name)) return MemberIds(null, null)
if (allowUnresolvedRefs) return MemberIds(null, null)
throw ScriptError(pos, "unknown member $name on $implicitTypeName")
}
return MemberIds(fieldId, methodId)
}
private val currentRangeParamNames: Set<String>
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
private val capturePlanStack = mutableListOf<CapturePlan>()
private data class CapturePlan(
val slotPlan: SlotPlan,
val captures: MutableList<CaptureSlot> = mutableListOf(),
val captureMap: MutableMap<String, CaptureSlot> = mutableMapOf(),
val captureOwners: MutableMap<String, SlotLocation> = mutableMapOf()
)
private fun recordCaptureSlot(name: String, slotLoc: SlotLocation) {
val plan = capturePlanStack.lastOrNull() ?: return
if (plan.captureMap.containsKey(name)) return
val capture = CaptureSlot(
name = name,
)
plan.captureMap[name] = capture
plan.captureOwners[name] = slotLoc
plan.captures += capture
if (!plan.slotPlan.slots.containsKey(name)) {
plan.slotPlan.slots[name] = SlotEntry(
plan.slotPlan.nextIndex,
isMutable = slotLoc.isMutable,
isDelegated = slotLoc.isDelegated
)
plan.slotPlan.nextIndex += 1
}
}
private fun captureLocalRef(name: String, slotLoc: SlotLocation, pos: Pos): LocalSlotRef? {
if (capturePlanStack.isEmpty() || slotLoc.depth == 0) return null
val moduleId = moduleSlotPlan()?.id
if (moduleId != null && slotLoc.scopeId == moduleId) return null
recordCaptureSlot(name, slotLoc)
val plan = capturePlanStack.lastOrNull() ?: return null
val entry = plan.slotPlan.slots[name] ?: return null
return LocalSlotRef(
name,
entry.index,
plan.slotPlan.id,
entry.isMutable,
entry.isDelegated,
pos,
strictSlotRefs,
captureOwnerScopeId = slotLoc.scopeId,
captureOwnerSlot = slotLoc.slot
)
}
private fun captureSlotRef(name: String, pos: Pos): ObjRef? {
if (capturePlanStack.isEmpty()) return null
if (name == "this") return null
val slotLoc = lookupSlotLocation(name) ?: return null
captureLocalRef(name, slotLoc, pos)?.let { return it }
if (slotLoc.depth > 0) {
recordCaptureSlot(name, slotLoc)
}
return LocalSlotRef(
name,
slotLoc.slot,
slotLoc.scopeId,
slotLoc.isMutable,
slotLoc.isDelegated,
pos,
strictSlotRefs
)
}
private fun containsLoopControl(stmt: Statement, inLoop: Boolean = false): Boolean {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
return when (target) {
is BreakStatement -> target.label != null || !inLoop
is ContinueStatement -> target.label != null || !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
if (codeContexts.lastOrNull() is CodeContext.Module) {
return stmt
}
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
return stmt
}
if (codeContexts.any { it is CodeContext.Function }) {
return stmt
}
if (containsDelegatedRefs(stmt)) {
return stmt
}
if (containsUnsupportedForBytecode(stmt)) {
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()
val allowedScopeNames = moduleSlotPlan()?.slots?.keys
return BytecodeStatement.wrap(
stmt,
"stmt@${stmt.pos}",
allowLocalSlots = allowLocals,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames
)
}
private fun wrapFunctionBytecode(stmt: Statement, name: String): Statement {
if (!useBytecodeStatements) return stmt
if (containsDelegatedRefs(stmt)) return stmt
if (containsUnsupportedForBytecode(stmt)) return stmt
val returnLabels = returnLabelStack.lastOrNull() ?: emptySet()
val allowedScopeNames = moduleSlotPlan()?.slots?.keys
return BytecodeStatement.wrap(
stmt,
"fn@$name",
allowLocalSlots = true,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames
)
}
private fun containsUnsupportedForBytecode(stmt: Statement): Boolean {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
return when (target) {
is ExpressionStatement -> containsUnsupportedRef(target.ref)
is IfStatement -> {
containsUnsupportedForBytecode(target.condition) ||
containsUnsupportedForBytecode(target.ifBody) ||
(target.elseBody?.let { containsUnsupportedForBytecode(it) } ?: false)
}
is ForInStatement -> {
containsUnsupportedForBytecode(target.source) ||
containsUnsupportedForBytecode(target.body) ||
(target.elseStatement?.let { containsUnsupportedForBytecode(it) } ?: false)
}
is WhileStatement -> {
containsUnsupportedForBytecode(target.condition) ||
containsUnsupportedForBytecode(target.body) ||
(target.elseStatement?.let { containsUnsupportedForBytecode(it) } ?: false)
}
is DoWhileStatement -> {
containsUnsupportedForBytecode(target.body) ||
containsUnsupportedForBytecode(target.condition) ||
(target.elseStatement?.let { containsUnsupportedForBytecode(it) } ?: false)
}
is BlockStatement -> target.statements().any { containsUnsupportedForBytecode(it) }
is InlineBlockStatement -> target.statements().any { containsUnsupportedForBytecode(it) }
is VarDeclStatement -> target.initializer?.let { containsUnsupportedForBytecode(it) } ?: false
is BreakStatement -> target.resultExpr?.let { containsUnsupportedForBytecode(it) } ?: false
is ContinueStatement -> false
is ReturnStatement -> target.resultExpr?.let { containsUnsupportedForBytecode(it) } ?: false
is ThrowStatement -> containsUnsupportedForBytecode(target.throwExpr)
is TryStatement -> true
is WhenStatement -> {
containsUnsupportedForBytecode(target.value) ||
target.cases.any { case ->
case.conditions.any { cond ->
when (cond) {
is WhenEqualsCondition -> containsUnsupportedForBytecode(cond.expr)
is WhenInCondition -> containsUnsupportedForBytecode(cond.expr)
is WhenIsCondition -> false
else -> true
}
} || containsUnsupportedForBytecode(case.block)
} ||
(target.elseCase?.let { containsUnsupportedForBytecode(it) } ?: false)
}
else -> true
}
}
private fun containsUnsupportedRef(ref: ObjRef): Boolean {
return when (ref) {
is net.sergeych.lyng.obj.StatementRef -> containsUnsupportedForBytecode(ref.statement)
is BinaryOpRef -> containsUnsupportedRef(ref.left) || containsUnsupportedRef(ref.right)
is UnaryOpRef -> containsUnsupportedRef(ref.a)
is CastRef -> containsUnsupportedRef(ref.castValueRef()) || containsUnsupportedRef(ref.castTypeRef())
is AssignRef -> {
val target = ref.target as? LocalSlotRef
(target?.isDelegated == true) || containsUnsupportedRef(ref.value)
}
is AssignOpRef -> containsUnsupportedRef(ref.target) || containsUnsupportedRef(ref.value)
is AssignIfNullRef -> containsUnsupportedRef(ref.target) || containsUnsupportedRef(ref.value)
is LogicalAndRef -> containsUnsupportedRef(ref.left()) || containsUnsupportedRef(ref.right())
is LogicalOrRef -> containsUnsupportedRef(ref.left()) || containsUnsupportedRef(ref.right())
is ConditionalRef ->
containsUnsupportedRef(ref.condition) || containsUnsupportedRef(ref.ifTrue) || containsUnsupportedRef(ref.ifFalse)
is ElvisRef -> containsUnsupportedRef(ref.left) || containsUnsupportedRef(ref.right)
is FieldRef -> containsUnsupportedRef(ref.target)
is IndexRef -> containsUnsupportedRef(ref.targetRef) || containsUnsupportedRef(ref.indexRef)
is ListLiteralRef -> ref.entries().any {
when (it) {
is ListEntry.Element -> containsUnsupportedRef(it.ref)
is ListEntry.Spread -> containsUnsupportedRef(it.ref)
}
}
is MapLiteralRef -> ref.entries().any {
when (it) {
is net.sergeych.lyng.obj.MapLiteralEntry.Named -> containsUnsupportedRef(it.value)
is net.sergeych.lyng.obj.MapLiteralEntry.Spread -> containsUnsupportedRef(it.ref)
}
}
is CallRef -> containsUnsupportedRef(ref.target) || ref.args.any { containsUnsupportedForBytecode(it.value) }
is MethodCallRef -> containsUnsupportedRef(ref.receiver) || ref.args.any { containsUnsupportedForBytecode(it.value) }
else -> false
}
}
private fun containsDelegatedRefs(stmt: Statement): Boolean {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
return when (target) {
is ExpressionStatement -> containsDelegatedRefs(target.ref)
is BlockStatement -> target.statements().any { containsDelegatedRefs(it) }
is VarDeclStatement -> target.initializer?.let { containsDelegatedRefs(it) } ?: false
is DelegatedVarDeclStatement -> containsDelegatedRefs(target.initializer)
is DestructuringVarDeclStatement -> containsDelegatedRefs(target.initializer)
is IfStatement -> {
containsDelegatedRefs(target.condition) ||
containsDelegatedRefs(target.ifBody) ||
(target.elseBody?.let { containsDelegatedRefs(it) } ?: false)
}
is ForInStatement -> {
containsDelegatedRefs(target.source) ||
containsDelegatedRefs(target.body) ||
(target.elseStatement?.let { containsDelegatedRefs(it) } ?: false)
}
is WhileStatement -> {
containsDelegatedRefs(target.condition) ||
containsDelegatedRefs(target.body) ||
(target.elseStatement?.let { containsDelegatedRefs(it) } ?: false)
}
is DoWhileStatement -> {
containsDelegatedRefs(target.body) ||
containsDelegatedRefs(target.condition) ||
(target.elseStatement?.let { containsDelegatedRefs(it) } ?: false)
}
is WhenStatement -> {
containsDelegatedRefs(target.value) ||
target.cases.any { case ->
case.conditions.any { cond ->
when (cond) {
is WhenEqualsCondition -> containsDelegatedRefs(cond.expr)
is WhenInCondition -> containsDelegatedRefs(cond.expr)
is WhenIsCondition -> false
else -> true
}
} || containsDelegatedRefs(case.block)
} ||
(target.elseCase?.let { containsDelegatedRefs(it) } ?: false)
}
is ReturnStatement -> target.resultExpr?.let { containsDelegatedRefs(it) } ?: false
is BreakStatement -> target.resultExpr?.let { containsDelegatedRefs(it) } ?: false
is ThrowStatement -> containsDelegatedRefs(target.throwExpr)
else -> false
}
}
private fun containsDelegatedRefs(ref: ObjRef): Boolean {
return when (ref) {
is LocalSlotRef -> ref.isDelegated
is BinaryOpRef -> containsDelegatedRefs(ref.left) || containsDelegatedRefs(ref.right)
is UnaryOpRef -> containsDelegatedRefs(ref.a)
is CastRef -> containsDelegatedRefs(ref.castValueRef()) || containsDelegatedRefs(ref.castTypeRef())
is AssignRef -> {
val target = ref.target as? LocalSlotRef
(target?.isDelegated == true) || containsDelegatedRefs(ref.value)
}
is AssignOpRef -> containsDelegatedRefs(ref.target) || containsDelegatedRefs(ref.value)
is AssignIfNullRef -> containsDelegatedRefs(ref.target) || containsDelegatedRefs(ref.value)
is IncDecRef -> containsDelegatedRefs(ref.target)
is ConditionalRef ->
containsDelegatedRefs(ref.condition) || containsDelegatedRefs(ref.ifTrue) || containsDelegatedRefs(ref.ifFalse)
is ElvisRef -> containsDelegatedRefs(ref.left) || containsDelegatedRefs(ref.right)
is FieldRef -> containsDelegatedRefs(ref.target)
is IndexRef -> containsDelegatedRefs(ref.targetRef) || containsDelegatedRefs(ref.indexRef)
is ListLiteralRef -> ref.entries().any {
when (it) {
is ListEntry.Element -> containsDelegatedRefs(it.ref)
is ListEntry.Spread -> containsDelegatedRefs(it.ref)
}
}
is MapLiteralRef -> ref.entries().any {
when (it) {
is net.sergeych.lyng.obj.MapLiteralEntry.Named -> containsDelegatedRefs(it.value)
is net.sergeych.lyng.obj.MapLiteralEntry.Spread -> containsDelegatedRefs(it.ref)
}
}
is CallRef -> containsDelegatedRefs(ref.target) || ref.args.any { containsDelegatedRefs(it.value) }
is MethodCallRef -> containsDelegatedRefs(ref.receiver) || ref.args.any { containsDelegatedRefs(it.value) }
is StatementRef -> containsDelegatedRefs(ref.statement)
else -> false
}
}
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.captureSlots, stmt.pos)
}
is InlineBlockStatement -> {
val unwrapped = stmt.statements().map { unwrapBytecodeDeep(it) }
InlineBlockStatement(unwrapped, stmt.pos)
}
is VarDeclStatement -> {
val init = stmt.initializer?.let { unwrapBytecodeDeep(it) }
VarDeclStatement(
stmt.name,
stmt.isMutable,
stmt.visibility,
init,
stmt.isTransient,
stmt.slotIndex,
stmt.scopeId,
stmt.pos,
stmt.initializerObjClass
)
}
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)
}
is ForInStatement -> {
val source = unwrapBytecodeDeep(stmt.source)
val body = unwrapBytecodeDeep(stmt.body)
val elseBody = stmt.elseStatement?.let { unwrapBytecodeDeep(it) }
ForInStatement(
stmt.loopVarName,
source,
stmt.constRange,
body,
elseBody,
stmt.label,
stmt.canBreak,
stmt.loopSlotPlan,
stmt.pos
)
}
is WhileStatement -> {
val condition = unwrapBytecodeDeep(stmt.condition)
val body = unwrapBytecodeDeep(stmt.body)
val elseBody = stmt.elseStatement?.let { unwrapBytecodeDeep(it) }
WhileStatement(
condition,
body,
elseBody,
stmt.label,
stmt.canBreak,
stmt.loopSlotPlan,
stmt.pos
)
}
is DoWhileStatement -> {
val body = unwrapBytecodeDeep(stmt.body)
val condition = unwrapBytecodeDeep(stmt.condition)
val elseBody = stmt.elseStatement?.let { unwrapBytecodeDeep(it) }
DoWhileStatement(
body,
condition,
elseBody,
stmt.label,
stmt.loopSlotPlan,
stmt.pos
)
}
is BreakStatement -> {
val resultExpr = stmt.resultExpr?.let { unwrapBytecodeDeep(it) }
BreakStatement(stmt.label, resultExpr, stmt.pos)
}
is ContinueStatement -> ContinueStatement(stmt.label, stmt.pos)
is ReturnStatement -> {
val resultExpr = stmt.resultExpr?.let { unwrapBytecodeDeep(it) }
ReturnStatement(stmt.label, resultExpr, stmt.pos)
}
is ThrowStatement -> ThrowStatement(unwrapBytecodeDeep(stmt.throwExpr), stmt.pos)
else -> stmt
}
}
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
lastAnnotation = null
lastLabel = null
isTransientFlag = false
while (true) {
val t = cc.next()
return when (t.type) {
Token.Type.ID, Token.Type.OBJECT -> {
parseKeywordStatement(t)?.let { wrapBytecode(it) }
?: run {
cc.previous()
parseExpression()?.let { wrapBytecode(it) }
}
}
Token.Type.PLUS2, Token.Type.MINUS2 -> {
cc.previous()
parseExpression()?.let { wrapBytecode(it) }
}
Token.Type.ATLABEL -> {
val label = t.value
if (label == "Transient") {
isTransientFlag = true
continue
}
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
lastLabel = label
}
lastAnnotation = parseAnnotation(t)
continue
}
Token.Type.LABEL -> continue
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> {
pushPendingDocToken(t)
continue
}
Token.Type.NEWLINE -> {
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
continue
}
Token.Type.SEMICOLON -> continue
Token.Type.LBRACE -> {
cc.previous()
if (braceMeansLambda)
parseExpression()?.let { wrapBytecode(it) }
else
wrapBytecode(parseBlock())
}
Token.Type.RBRACE, Token.Type.RBRACKET -> {
cc.previous()
return null
}
Token.Type.EOF -> null
else -> {
// could be expression
cc.previous()
parseExpression()?.let { wrapBytecode(it) }
}
}
}
}
private suspend fun parseExpression(): Statement? {
val pos = cc.currentPos()
return parseExpressionLevel()?.let { ref ->
ExpressionStatement(ref, pos)
}
}
private suspend fun parseExpressionLevel(level: Int = 0): ObjRef? {
if (level == lastLevel)
return parseTerm()
var lvalue: ObjRef? = parseExpressionLevel(level + 1) ?: return null
while (true) {
val opToken = cc.next()
val op = byLevel[level][opToken.type]
if (op == null) {
// handle ternary conditional at the top precedence level only: a ? b : c
if (opToken.type == Token.Type.QUESTION && level == 0) {
val thenRef = parseExpressionLevel(level + 1)
?: throw ScriptError(opToken.pos, "Expecting expression after '?'")
val colon = cc.next()
if (colon.type != Token.Type.COLON) colon.raiseSyntax("missing ':'")
val elseRef = parseExpressionLevel(level + 1)
?: throw ScriptError(colon.pos, "Expecting expression after ':'")
lvalue = ConditionalRef(lvalue!!, thenRef, elseRef)
continue
}
cc.previous()
break
}
val rvalue = parseExpressionLevel(level + 1)
?: throw ScriptError(opToken.pos, "Expecting expression")
val res = op.generate(opToken.pos, lvalue!!, rvalue)
if (opToken.type == Token.Type.ASSIGN) {
val ctx = codeContexts.lastOrNull()
if (ctx is CodeContext.ClassBody) {
val target = lvalue
val name = when (target) {
is LocalVarRef -> target.name
is FastLocalVarRef -> target.name
is LocalSlotRef -> target.name
is ImplicitThisMemberRef -> target.name
is ThisFieldSlotRef -> target.name
is QualifiedThisFieldSlotRef -> target.name
is FieldRef -> if (target.target is LocalVarRef && target.target.name == "this") target.name else null
else -> null
}
if (name != null) ctx.pendingInitializations.remove(name)
}
}
lvalue = res
}
return lvalue
}
private suspend fun parseTerm(): ObjRef? {
var operand: ObjRef? = null
// newlines _before_
cc.skipWsTokens()
while (true) {
val t = cc.next()
val startPos = t.pos
when (t.type) {
// Token.Type.NEWLINE, Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> {
// continue
// }
// very special case chained calls: call()<NL>.call2 {}.call3()
Token.Type.NEWLINE -> {
val saved = cc.savePos()
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
// chained call continue from it
continue
} else {
// restore position and stop parsing as a term:
cc.restorePos(saved)
cc.previous()
return operand
}
}
Token.Type.SEMICOLON, Token.Type.EOF, Token.Type.RBRACE, Token.Type.COMMA -> {
cc.previous()
return operand
}
Token.Type.NOT -> {
if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!' ")
val op = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = UnaryOpRef(UnaryOp.NOT, op)
}
Token.Type.BITNOT -> {
if (operand != null) throw ScriptError(t.pos, "unexpected operator '~'")
val op = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after '~'")
operand = UnaryOpRef(UnaryOp.BITNOT, op)
}
Token.Type.DOT, Token.Type.NULL_COALESCE -> {
val isOptional = t.type == Token.Type.NULL_COALESCE
operand?.let { left ->
// dot call: calling method on the operand, if next is ID, "("
var isCall = false
val next = cc.next()
if (next.type == Token.Type.ID) {
// could be () call or obj.method {} call
val nt = cc.current()
when (nt.type) {
Token.Type.LPAREN -> {
cc.next()
// instance method call
val parsed = parseArgs()
val args = parsed.first
val tailBlock = parsed.second
if (left is LocalVarRef && left.name == "scope") {
val first = args.firstOrNull()?.value
val const = (first as? ExpressionStatement)?.ref as? ConstRef
val name = const?.constValue as? ObjString
if (name != null) {
resolutionSink?.referenceReflection(name.value, next.pos)
}
}
isCall = true
operand = when (left) {
is LocalVarRef -> if (left.name == "this") {
resolutionSink?.referenceMember(next.value, next.pos)
val implicitType = currentImplicitThisTypeName()
val ids = resolveMemberIds(next.value, next.pos, implicitType)
ThisMethodSlotCallRef(next.value, ids.methodId, args, tailBlock, isOptional)
} else if (left.name == "scope") {
if (next.value == "get" || next.value == "set") {
val first = args.firstOrNull()?.value
val const = (first as? ExpressionStatement)?.ref as? ConstRef
val name = const?.constValue as? ObjString
if (name != null) {
resolutionSink?.referenceReflection(name.value, next.pos)
}
}
MethodCallRef(left, next.value, args, tailBlock, isOptional)
} else {
MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef(
left.typeName,
next.value,
resolveMemberIds(next.value, next.pos, left.typeName).methodId,
args,
tailBlock,
isOptional
).also {
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
// single lambda arg, like assertThrows { ... }
cc.next()
isCall = true
val lambda = parseLambdaExpression()
val argPos = next.pos
val args = listOf(ParsedArgument(ExpressionStatement(lambda, argPos), next.pos))
operand = when (left) {
is LocalVarRef -> if (left.name == "this") {
resolutionSink?.referenceMember(next.value, next.pos)
val implicitType = currentImplicitThisTypeName()
val ids = resolveMemberIds(next.value, next.pos, implicitType)
ThisMethodSlotCallRef(next.value, ids.methodId, args, true, isOptional)
} else if (left.name == "scope") {
if (next.value == "get" || next.value == "set") {
val first = args.firstOrNull()?.value
val const = (first as? ExpressionStatement)?.ref as? ConstRef
val name = const?.constValue as? ObjString
if (name != null) {
resolutionSink?.referenceReflection(name.value, next.pos)
}
}
MethodCallRef(left, next.value, args, true, isOptional)
} else {
MethodCallRef(left, next.value, args, true, isOptional)
}
is QualifiedThisRef ->
QualifiedThisMethodSlotCallRef(
left.typeName,
next.value,
resolveMemberIds(next.value, next.pos, left.typeName).methodId,
args,
true,
isOptional
).also {
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> MethodCallRef(left, next.value, args, true, isOptional)
}
}
else -> {}
}
}
if (!isCall) {
operand = when (left) {
is LocalVarRef -> if (left.name == "this") {
resolutionSink?.referenceMember(next.value, next.pos)
val implicitType = currentImplicitThisTypeName()
val ids = resolveMemberIds(next.value, next.pos, implicitType)
ThisFieldSlotRef(next.value, ids.fieldId, ids.methodId, isOptional)
} else {
FieldRef(left, next.value, isOptional)
}
is QualifiedThisRef -> run {
val ids = resolveMemberIds(next.value, next.pos, left.typeName)
QualifiedThisFieldSlotRef(
left.typeName,
next.value,
ids.fieldId,
ids.methodId,
isOptional
)
}.also {
resolutionSink?.referenceMember(next.value, next.pos, left.typeName)
}
else -> FieldRef(left, next.value, isOptional)
}
}
}
?: throw ScriptError(t.pos, "Expecting expression before dot")
}
Token.Type.COLONCOLON -> {
operand = parseScopeOperator(operand)
}
Token.Type.LPAREN, Token.Type.NULL_COALESCE_INVOKE -> {
operand?.let { left ->
// this is function call from <left>
operand = parseFunctionCall(
left,
false,
t.type == Token.Type.NULL_COALESCE_INVOKE
)
} ?: run {
// Expression in parentheses
val statement = parseStatement() ?: throw ScriptError(t.pos, "Expecting expression")
operand = StatementRef(statement)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
cc.skipTokenOfType(Token.Type.RPAREN, "missing ')'")
}
}
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
operand?.let { left ->
// array access via ObjRef
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
val index = parseStatement() ?: throw ScriptError(t.pos, "Expecting index expression")
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
operand = IndexRef(left, StatementRef(index), isOptional)
} ?: run {
// array literal
val entries = parseArrayLiteral()
// build list literal via ObjRef node (no per-access lambdas)
operand = ListLiteralRef(entries)
}
}
Token.Type.ID -> {
// there could be terminal operators or keywords:// variable to read or like
when (t.value) {
in stopKeywords -> {
if (t.value == "init" && !(codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE)) {
// Soft keyword: init is only a keyword in class body when followed by {
cc.previous()
operand = parseAccessor()
} else {
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
// Allow certain statement-like constructs to act as expressions
// when they appear in expression position (e.g., `if (...) ... else ...`).
// Other keywords should be handled by the outer statement parser.
when (t.value) {
"if" -> {
val s = parseIfStatement()
operand = StatementRef(s)
}
"when" -> {
val s = parseWhenStatement()
operand = StatementRef(s)
}
else -> {
// Do not consume the keyword as part of a term; backtrack
// and return null so outer parser handles it.
cc.previous()
return null
}
}
}
}
"else", "break", "continue" -> {
cc.previous()
return operand
}
"throw" -> {
val s = parseThrowStatement(t.pos)
operand = StatementRef(s)
}
else -> operand?.let { left ->
// selector: <lvalue>, '.' , <id>
// we replace operand with selector code, that
// is RW:
operand = when (left) {
is LocalVarRef -> if (left.name == "this") {
resolutionSink?.referenceMember(t.value, t.pos)
val ids = resolveMemberIds(t.value, t.pos, null)
ThisFieldSlotRef(t.value, ids.fieldId, ids.methodId, false)
} else {
FieldRef(left, t.value, false)
}
is QualifiedThisRef -> run {
val ids = resolveMemberIds(t.value, t.pos, left.typeName)
QualifiedThisFieldSlotRef(
left.typeName,
t.value,
ids.fieldId,
ids.methodId,
false
)
}.also {
resolutionSink?.referenceMember(t.value, t.pos, left.typeName)
}
else -> FieldRef(left, t.value, false)
}
} ?: run {
// variable to read or like
cc.previous()
operand = parseAccessor()
}
}
}
Token.Type.PLUS2 -> {
// ++ (post if operand exists, pre otherwise)
operand = operand?.let { left ->
IncDecRef(left, isIncrement = true, isPost = true, atPos = startPos)
} ?: run {
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
IncDecRef(next, isIncrement = true, isPost = false, atPos = startPos)
}
}
Token.Type.MINUS2 -> {
// -- (post if operand exists, pre otherwise)
operand = operand?.let { left ->
IncDecRef(left, isIncrement = false, isPost = true, atPos = startPos)
} ?: run {
val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
IncDecRef(next, isIncrement = false, isPost = false, atPos = startPos)
}
}
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
// range operator
val isEndInclusive = t.type == Token.Type.DOTDOT
val left = operand
// if it is an open end range, then the end of line could be here that we do not want
// to skip in parseExpression:
val current = cc.current()
val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT)
null
else
parseExpression()
val rightRef = right?.let { StatementRef(it) }
if (left != null && rightRef != null) {
val lConst = constIntValueOrNull(left)
val rConst = constIntValueOrNull(rightRef)
if (lConst != null && rConst != null) {
operand = ConstRef(ObjRange(ObjInt.of(lConst), ObjInt.of(rConst), isEndInclusive).asReadonly)
} else {
operand = RangeRef(left, rightRef, isEndInclusive)
}
} else {
operand = RangeRef(left, rightRef, isEndInclusive)
}
}
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
operand = operand?.let { left ->
// Trailing block-argument function call: the leading '{' is already consumed,
// and the lambda must be parsed as a single argument BEFORE any following
// selectors like ".foo" are considered. Do NOT rewind here, otherwise
// the expression parser may capture ".foo" as part of the lambda expression.
parseFunctionCall(
left,
blockArgument = true,
isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
)
} ?: run {
// Disambiguate between lambda and map literal.
// Heuristic: if there is a top-level '->' before the closing '}', it's a lambda.
// Otherwise, try to parse a map literal; if it fails, fall back to lambda.
val isLambda = hasTopLevelArrowBeforeRbrace()
if (!isLambda) {
parseMapLiteralOrNull() ?: parseLambdaExpression()
} else parseLambdaExpression()
}
}
Token.Type.RBRACKET, Token.Type.RPAREN -> {
cc.previous()
return operand
}
else -> {
cc.previous()
operand?.let { return it }
operand = parseAccessor() ?: return null //throw ScriptError(t.pos, "Expecting expression")
}
}
}
}
/**
* Parse lambda expression, leading '{' is already consumed
*/
private suspend fun parseLambdaExpression(expectedReceiverType: String? = null): ObjRef {
// lambda args are different:
val startPos = cc.currentPos()
val label = lastLabel
val argsDeclaration = parseArgsDeclaration()
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
throw ScriptError(
startPos,
"lambda must have either valid arguments declaration with '->' or no arguments"
)
val paramNames = argsDeclaration?.params?.map { it.name } ?: emptyList()
val hasImplicitIt = argsDeclaration == null
val slotParamNames = if (hasImplicitIt) paramNames + "it" else paramNames
val paramSlotPlan = buildParamSlotPlan(slotParamNames)
label?.let { cc.labels.add(it) }
slotPlanStack.add(paramSlotPlan)
val capturePlan = CapturePlan(paramSlotPlan)
capturePlanStack.add(capturePlan)
val parsedBody = try {
inCodeContext(CodeContext.Function("<lambda>", implicitThisMembers = true, implicitThisTypeName = expectedReceiverType)) {
val returnLabels = label?.let { setOf(it) } ?: emptySet()
returnLabelStack.addLast(returnLabels)
try {
resolutionSink?.enterScope(ScopeKind.FUNCTION, startPos, "<lambda>")
for (param in slotParamNames) {
resolutionSink?.declareSymbol(param, SymbolKind.PARAM, isMutable = false, pos = startPos)
}
withLocalNames(slotParamNames.toSet()) {
parseBlock(skipLeadingBrace = true)
}
} finally {
resolutionSink?.exitScope(cc.currentPos())
returnLabelStack.removeLast()
}
}
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val body = unwrapBytecodeDeep(parsedBody)
label?.let { cc.labels.remove(it) }
val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan)
val captureSlots = capturePlan.captures.toList()
return ValueFnRef { closureScope ->
val stmt = object : Statement() {
override val pos: Pos = body.pos
override suspend fun execute(scope: Scope): Obj {
// and the source closure of the lambda which might have other thisObj.
val context = scope.applyClosure(closureScope, preferredThisType = expectedReceiverType)
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
if (captureSlots.isNotEmpty()) {
val moduleScope = if (context is ApplyScope) {
var s: Scope? = closureScope
while (s != null && s !is ModuleScope) {
s = s.parent
}
s as? ModuleScope
} else {
null
}
for (capture in captureSlots) {
if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) {
continue
}
val rec = closureScope.resolveCaptureRecord(capture.name)
?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
context.updateSlotFor(capture.name, rec)
}
}
// Execute lambda body in a closure-aware context. Blocks inside the lambda
// will create child scopes as usual, so re-declarations inside loops work.
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = scope.args.list
val itValue: Obj = when (l.size) {
// no args: it == void
0 -> ObjVoid
// one args: it is this arg
1 -> l[0]
// more args: it is a list of args
else -> ObjList(l.toMutableList())
}
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
} else {
// assign vars as declared the standard way
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
}
return try {
body.execute(context)
} catch (e: ReturnException) {
if (e.label == null || e.label == label) e.result
else throw e
}
}
}
stmt.asReadonly
}
}
private suspend fun parseArrayLiteral(): List<ListEntry> {
// it should be called after Token.Type.LBRACKET is consumed
val entries = mutableListOf<ListEntry>()
while (true) {
val t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
// todo: check commas sequences like [,] [,,] before, after or instead of expressions
}
Token.Type.RBRACKET -> return entries
Token.Type.ELLIPSIS -> {
parseExpressionLevel()?.let { entries += ListEntry.Spread(it) }
?: throw ScriptError(t.pos, "spread element must have an expression")
}
else -> {
cc.previous()
parseExpressionLevel()?.let { expr ->
if (cc.current().type == Token.Type.ELLIPSIS) {
cc.next()
entries += ListEntry.Spread(expr)
} else {
entries += ListEntry.Element(expr)
}
} ?: throw ScriptError(t.pos, "invalid list literal: expecting expression")
}
}
}
}
private fun parseScopeOperator(operand: ObjRef?): ObjRef {
// implement global scope maybe?
if (operand == null) throw ScriptError(cc.next().pos, "Expecting expression before ::")
val t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "Expecting ID after ::")
return when (t.value) {
"class" -> ValueFnRef { scope ->
operand.get(scope).value.objClass.asReadonly
}
else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}")
}
}
/**
* Look ahead from current position (right after a leading '{') to find a top-level '->' before the matching '}'.
* Returns true if such arrow is found, meaning the construct should be parsed as a lambda.
* The scanner respects nested braces depth; only depth==1 arrows count.
* The current cursor is restored on exit.
*/
private fun hasTopLevelArrowBeforeRbrace(): Boolean {
val start = cc.savePos()
var depth = 1
var found = false
while (cc.hasNext()) {
val t = cc.next()
when (t.type) {
Token.Type.LBRACE -> depth++
Token.Type.RBRACE -> {
depth--
if (depth == 0) break
}
Token.Type.ARROW -> if (depth == 1) {
found = true
// Do not break; we still restore position below
}
else -> {}
}
}
cc.restorePos(start)
return found
}
/**
* Attempt to parse a map literal starting at the position after '{'.
* Returns null if the sequence does not look like a map literal (e.g., empty or first token is not STRING/ID/ELLIPSIS),
* in which case caller should treat it as a lambda/block.
* When it recognizes a map literal, it commits and throws on syntax errors.
*/
private suspend fun parseMapLiteralOrNull(): ObjRef? {
val startAfterLbrace = cc.savePos()
// Peek first non-ws token to decide whether it's likely a map literal
val first = cc.peekNextNonWhitespace()
// Empty {} should be parsed as an empty map literal in expression context
if (first.type == Token.Type.RBRACE) {
// consume '}' and return empty map literal
cc.next() // consume the RBRACE
return MapLiteralRef(emptyList())
}
if (first.type !in listOf(Token.Type.STRING, Token.Type.ID, Token.Type.ELLIPSIS)) return null
// Commit to map literal parsing
cc.skipWsTokens()
val entries = mutableListOf<MapLiteralEntry>()
val usedKeys = mutableSetOf<String>()
while (true) {
// Skip whitespace/comments/newlines between entries
val t0 = cc.nextNonWhitespace()
when (t0.type) {
Token.Type.RBRACE -> {
// end of map literal
return MapLiteralRef(entries)
}
Token.Type.COMMA -> {
// allow stray commas; continue
continue
}
Token.Type.ELLIPSIS -> {
// spread element: ... expression
val expr = parseExpressionLevel() ?: throw ScriptError(t0.pos, "invalid map spread: expecting expression")
entries += MapLiteralEntry.Spread(expr)
// Expect comma or '}' next; loop will handle
}
Token.Type.STRING, Token.Type.ID -> {
val isIdKey = t0.type == Token.Type.ID
val keyName = if (isIdKey) t0.value else t0.value
// After key we require ':'
cc.skipWsTokens()
val colon = cc.next()
if (colon.type != Token.Type.COLON) {
// Not a map literal after all; backtrack and signal null
cc.restorePos(startAfterLbrace)
return null
}
// Check for shorthand (only for id-keys): if next non-ws is ',' or '}'
cc.skipWsTokens()
val next = cc.next()
if ((next.type == Token.Type.COMMA || next.type == Token.Type.RBRACE)) {
if (!isIdKey) throw ScriptError(next.pos, "missing value after string-literal key '$keyName'")
// id: shorthand; value is the variable with the same name
// rewind one step if RBRACE so outer loop can handle it
if (next.type == Token.Type.RBRACE) cc.previous()
// Duplicate detection for literals only
if (!usedKeys.add(keyName)) throw ScriptError(t0.pos, "duplicate key '$keyName'")
resolutionSink?.reference(keyName, t0.pos)
entries += MapLiteralEntry.Named(keyName, resolveIdentifierRef(keyName, t0.pos))
// If the token was COMMA, the loop continues; if it's RBRACE, next iteration will end
} else {
// There is a value expression: push back token and parse expression
cc.previous()
val valueRef = parseExpressionLevel() ?: throw ScriptError(colon.pos, "expecting map entry value")
if (!usedKeys.add(keyName)) throw ScriptError(t0.pos, "duplicate key '$keyName'")
entries += MapLiteralEntry.Named(keyName, valueRef)
// After value, allow optional comma; do not require it
cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)
// The loop will continue and eventually see '}'
}
}
else -> {
// Not a map literal; backtrack and let caller treat as lambda
cc.restorePos(startAfterLbrace)
return null
}
}
}
}
/**
* Parse argument declaration, used in lambda (and later in fn too)
* @return declaration or null if there is no valid list of arguments
*/
private suspend fun parseArgsDeclaration(isClassDeclaration: Boolean = false): ArgsDeclaration? {
val result = mutableListOf<ArgsDeclaration.Item>()
var endTokenType: Token.Type? = null
val startPos = cc.savePos()
cc.skipWsTokens()
while (endTokenType == null) {
var t = cc.next()
when (t.type) {
Token.Type.RPAREN, Token.Type.ARROW -> {
// empty list?
endTokenType = t.type
}
Token.Type.NEWLINE -> {}
Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
Token.Type.ID, Token.Type.ATLABEL -> {
var isTransient = false
if (t.type == Token.Type.ATLABEL) {
if (t.value == "Transient") {
isTransient = true
t = cc.next()
} else throw ScriptError(t.pos, "Unexpected label in argument list")
}
// visibility
val visibility = if (isClassDeclaration && t.value == "private") {
t = cc.next()
Visibility.Private
} else Visibility.Public
// val/var?
val access = when (t.value) {
"val" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
AccessType.Val
}
"var" -> {
if (!isClassDeclaration) {
cc.restorePos(startPos); return null
}
t = cc.next()
AccessType.Var
}
else -> null
}
val effectiveAccess = if (isClassDeclaration && access == null) {
AccessType.Var
} else {
access
}
// type information (semantic + mini syntax)
val (typeInfo, miniType) = parseTypeDeclarationWithMini()
var defaultValue: Statement? = null
cc.ifNextIs(Token.Type.ASSIGN) {
defaultValue = parseExpression()
}
val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)
result += ArgsDeclaration.Item(
t.value,
typeInfo,
miniType,
t.pos,
isEllipsis,
defaultValue,
effectiveAccess,
visibility,
isTransient
)
// important: valid argument list continues with ',' and ends with '->' or ')'
// otherwise it is not an argument list:
when (val tt = cc.nextNonWhitespace().type) {
Token.Type.RPAREN -> {
// end of arguments
endTokenType = tt
}
Token.Type.ARROW -> {
// end of arguments too
endTokenType = tt
}
Token.Type.COMMA -> {
// next argument, OK
}
else -> {
// this is not a valid list of arguments:
cc.restorePos(startPos) // for the current
return null
}
}
}
else -> {
// if we get here. there os also no valid list of arguments:
cc.restorePos(startPos)
return null
}
}
}
return ArgsDeclaration(result, endTokenType)
}
@Suppress("unused")
private fun parseTypeDeclaration(): TypeDecl {
return parseTypeDeclarationWithMini().first
}
// Minimal helper to parse a type annotation and simultaneously build a MiniTypeRef.
// Currently supports a simple identifier with optional nullable '?'.
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
// Only parse a type if a ':' follows; otherwise keep current behavior
if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null)
return parseTypeExpressionWithMini()
}
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
// Parse a qualified base name: ID ('.' ID)*
val segments = mutableListOf<MiniTypeName.Segment>()
var first = true
val typeStart = cc.currentPos()
var lastEnd = typeStart
var lastName: String? = null
var lastPos: Pos? = null
while (true) {
val idTok =
if (first) cc.requireToken(Token.Type.ID, "type name or type expression required") else cc.requireToken(
Token.Type.ID,
"identifier expected after '.' in type"
)
first = false
segments += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
lastEnd = cc.currentPos()
lastName = idTok.value
lastPos = idTok.pos
val dotPos = cc.savePos()
val t = cc.next()
if (t.type == Token.Type.DOT) {
// continue
continue
} else {
cc.restorePos(dotPos)
break
}
}
val qualified = segments.joinToString(".") { it.name }
if (segments.size > 1) {
lastPos?.let { pos -> resolutionSink?.reference(qualified, pos) }
} else {
lastName?.let { name ->
lastPos?.let { pos -> resolutionSink?.reference(name, pos) }
}
}
// Helper to build MiniTypeRef (base or generic)
fun buildBaseRef(rangeEnd: Pos, args: List<MiniTypeRef>?, nullable: Boolean): MiniTypeRef {
val base = MiniTypeName(MiniRange(typeStart, rangeEnd), segments.toList(), nullable = false)
return if (args == null || args.isEmpty()) base.copy(
range = MiniRange(typeStart, rangeEnd),
nullable = nullable
)
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
}
// Optional generic arguments: '<' Type (',' Type)* '>'
var miniArgs: MutableList<MiniTypeRef>? = null
var semArgs: MutableList<TypeDecl>? = null
val afterBasePos = cc.savePos()
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
miniArgs = mutableListOf()
semArgs = mutableListOf()
do {
val (argSem, argMini) = parseTypeExpressionWithMini()
miniArgs += argMini
semArgs += argSem
val sep = cc.next()
if (sep.type == Token.Type.COMMA) {
// continue
} else if (sep.type == Token.Type.GT) {
break
} else if (sep.type == Token.Type.SHR) {
cc.pushPendingGT()
break
} else {
sep.raiseSyntax("expected ',' or '>' in generic arguments")
}
} while (true)
lastEnd = cc.currentPos()
} else {
cc.restorePos(afterBasePos)
}
// Nullable suffix after base or generic
val isNullable = if (cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)) {
true
} else if (cc.skipTokenOfType(Token.Type.IFNULLASSIGN, isOptional = true)) {
cc.pushPendingAssign()
true
} else false
val endPos = cc.currentPos()
val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable)
// Semantic: keep simple for now, just use qualified base name with nullable flag
val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable)
else TypeDecl.Simple(qualified, isNullable)
return Pair(sem, miniRef)
}
/**
* Parse arguments list during the call and detect last block argument
* _following the parenthesis_ call: `(1,2) { ... }`
*/
private suspend fun parseArgs(expectedTailBlockReceiver: String? = null): Pair<List<ParsedArgument>, Boolean> {
val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
// name: expr
val name = t1.value
// Check for shorthand: name: (comma or rparen)
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
val localVar = resolveIdentifierRef(name, t1.pos)
val argPos = t1.pos
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)
}
}
cc.restorePos(save)
return null
}
do {
val t = cc.next()
when (t.type) {
Token.Type.NEWLINE,
Token.Type.RPAREN, Token.Type.COMMA -> {
}
Token.Type.ELLIPSIS -> {
parseExpression()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
?: throw ScriptError(t.pos, "Expecting arguments list")
}
else -> {
cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
// In call-site arguments, ':' is reserved for named args. Do not parse type declarations here.
}
// Here should be a valid termination:
}
}
} while (t.type != Token.Type.RPAREN)
// block after?
val pos = cc.savePos()
val end = cc.next()
var lastBlockArgument = false
if (end.type == Token.Type.LBRACE) {
// last argument - callable
val callableAccessor = parseLambdaExpression(expectedTailBlockReceiver)
args += ParsedArgument(
ExpressionStatement(callableAccessor, end.pos),
end.pos
)
lastBlockArgument = true
} else
cc.restorePos(pos)
return args to lastBlockArgument
}
/**
* Parse arguments inside parentheses without consuming any optional trailing block after the RPAREN.
* Useful in contexts where a following '{' has different meaning (e.g., class bodies after base lists).
*/
private suspend fun parseArgsNoTailBlock(): List<ParsedArgument> {
val args = mutableListOf<ParsedArgument>()
suspend fun tryParseNamedArg(): ParsedArgument? {
val save = cc.savePos()
val t1 = cc.next()
if (t1.type == Token.Type.ID) {
val t2 = cc.next()
if (t2.type == Token.Type.COLON) {
val name = t1.value
// Check for shorthand: name: (comma or rparen)
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.COMMA || next.type == Token.Type.RPAREN) {
val localVar = resolveIdentifierRef(name, t1.pos)
val argPos = t1.pos
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)
}
}
cc.restorePos(save)
return null
}
do {
val t = cc.next()
when (t.type) {
Token.Type.NEWLINE,
Token.Type.RPAREN, Token.Type.COMMA -> {
}
Token.Type.ELLIPSIS -> {
parseExpression()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
?: throw ScriptError(t.pos, "Expecting arguments list")
}
else -> {
cc.previous()
val named = tryParseNamedArg()
if (named != null) {
args += named
} else {
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
?: throw ScriptError(t.pos, "Expecting arguments list")
// Do not parse type declarations in call args
}
}
}
} while (t.type != Token.Type.RPAREN)
// Do NOT peek for a trailing block; leave it to the outer parser
return args
}
private suspend fun parseFunctionCall(
left: ObjRef,
blockArgument: Boolean,
isOptional: Boolean
): ObjRef {
var detectedBlockArgument = blockArgument
val expectedReceiver = tailBlockReceiverType(left)
val args = if (blockArgument) {
// Leading '{' has already been consumed by the caller token branch.
// Parse only the lambda expression as the last argument and DO NOT
// allow any subsequent selectors (like ".last()") to be absorbed
// into the lambda body. This ensures expected order:
// foo { ... }.bar() == (foo { ... }).bar()
val callableAccessor = parseLambdaExpression(expectedReceiver)
listOf(ParsedArgument(ExpressionStatement(callableAccessor, cc.currentPos()), cc.currentPos()))
} else {
val r = parseArgs(expectedReceiver)
detectedBlockArgument = r.second
r.first
}
val implicitThisTypeName = currentImplicitThisTypeName()
return when (left) {
is ImplicitThisMemberRef ->
ImplicitThisMethodCallRef(
left.name,
left.methodId,
args,
detectedBlockArgument,
isOptional,
left.atPos,
implicitThisTypeName
)
is LocalVarRef -> {
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
val implicitThis = codeContexts.any { ctx ->
(ctx as? CodeContext.Function)?.implicitThisMembers == true
}
if ((classContext || implicitThis) && extensionNames.contains(left.name)) {
val ids = resolveImplicitThisMemberIds(left.name, left.pos(), implicitThisTypeName)
ImplicitThisMethodCallRef(
left.name,
ids.methodId,
args,
detectedBlockArgument,
isOptional,
left.pos(),
implicitThisTypeName
)
} else {
CallRef(left, args, detectedBlockArgument, isOptional)
}
}
is LocalSlotRef -> {
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
val implicitThis = codeContexts.any { ctx ->
(ctx as? CodeContext.Function)?.implicitThisMembers == true
}
if ((classContext || implicitThis) && extensionNames.contains(left.name)) {
val ids = resolveImplicitThisMemberIds(left.name, left.pos(), implicitThisTypeName)
ImplicitThisMethodCallRef(
left.name,
ids.methodId,
args,
detectedBlockArgument,
isOptional,
left.pos(),
implicitThisTypeName
)
} else {
CallRef(left, args, detectedBlockArgument, isOptional)
}
}
else -> CallRef(left, args, detectedBlockArgument, isOptional)
}
}
private suspend fun parseAccessor(): ObjRef? {
// could be: literal
val t = cc.next()
return when (t.type) {
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
cc.previous()
val n = parseNumber(true)
ConstRef(n.asReadonly)
}
Token.Type.STRING -> ConstRef(ObjString(t.value).asReadonly)
Token.Type.CHAR -> ConstRef(ObjChar(t.value[0]).asReadonly)
Token.Type.PLUS -> {
val n = parseNumber(true)
ConstRef(n.asReadonly)
}
Token.Type.MINUS -> {
parseNumberOrNull(false)?.let { n ->
ConstRef(n.asReadonly)
} ?: run {
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
UnaryOpRef(UnaryOp.NEGATE, n)
}
}
Token.Type.ID -> {
// Special case: qualified this -> this@Type
if (t.value == "this") {
val pos = cc.savePos()
val next = cc.next()
if (next.pos.line == t.pos.line && next.type == Token.Type.ATLABEL) {
resolutionSink?.reference(next.value, next.pos)
QualifiedThisRef(next.value, t.pos)
} else {
cc.restorePos(pos)
// plain this
resolveIdentifierRef("this", t.pos)
}
} else when (t.value) {
"void" -> ConstRef(ObjVoid.asReadonly)
"null" -> ConstRef(ObjNull.asReadonly)
"true" -> ConstRef(ObjTrue.asReadonly)
"false" -> ConstRef(ObjFalse.asReadonly)
else -> {
resolveIdentifierRef(t.value, t.pos)
}
}
}
Token.Type.OBJECT -> StatementRef(parseObjectDeclaration())
else -> null
}
}
private fun parseNumberOrNull(isPlus: Boolean): Obj? {
val pos = cc.savePos()
val t = cc.next()
return when (t.type) {
Token.Type.INT, Token.Type.HEX -> {
val n = t.value.replace("_", "").toLong(if (t.type == Token.Type.HEX) 16 else 10)
if (isPlus) ObjInt.of(n) else ObjInt.of(-n)
}
Token.Type.REAL -> {
val d = t.value.toDouble()
if (isPlus) ObjReal.of(d) else ObjReal.of(-d)
}
else -> {
cc.restorePos(pos)
null
}
}
}
@Suppress("SameParameterValue")
private fun parseNumber(isPlus: Boolean): Obj {
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
}
suspend fun parseAnnotation(t: Token): (suspend (Scope, ObjString, Statement) -> Statement) {
val extraArgs = parseArgsOrNull()
resolutionSink?.reference(t.value, t.pos)
// println("annotation ${t.value}: args: $extraArgs")
return { scope, name, body ->
val extras = extraArgs?.first?.toArguments(scope, extraArgs.second)?.list
val required = listOf(name, body)
val args = extras?.let { required + it } ?: required
val fn = scope.get(t.value)?.value ?: scope.raiseSymbolNotFound("annotation not found: ${t.value}")
if (fn !is Statement) scope.raiseIllegalArgument("annotation must be callable, got ${fn.objClass}")
(fn.execute(scope.createChildScope(Arguments(args))) as? Statement)
?: scope.raiseClassCastError("function annotation must return callable")
}
}
suspend fun parseArgsOrNull(): Pair<List<ParsedArgument>, Boolean>? =
if (cc.skipNextIf(Token.Type.LPAREN))
parseArgs()
else
null
private suspend fun parseDeclarationWithModifiers(firstId: Token): Statement {
val modifiers = mutableSetOf<String>()
var currentToken = firstId
while (true) {
when (currentToken.value) {
"private", "protected", "static", "abstract", "closed", "override", "extern", "open" -> {
modifiers.add(currentToken.value)
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
currentToken = nextNonWhitespace()
} else {
break
}
}
else -> break
}
}
val visibility = when {
modifiers.contains("private") -> Visibility.Private
modifiers.contains("protected") -> Visibility.Protected
else -> Visibility.Public
}
val isStatic = modifiers.contains("static")
val isAbstract = modifiers.contains("abstract")
val isClosed = modifiers.contains("closed")
val isOverride = modifiers.contains("override")
val isExtern = modifiers.contains("extern")
if (isStatic && (isAbstract || isOverride || isClosed))
throw ScriptError(currentToken.pos, "static members cannot be abstract, closed or override")
if (visibility == Visibility.Private && isAbstract)
throw ScriptError(currentToken.pos, "abstract members cannot be private")
pendingDeclStart = firstId.pos
// pendingDeclDoc might be already set by an annotation
if (pendingDeclDoc == null)
pendingDeclDoc = consumePendingDoc()
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
if (!isMember && isClosed)
throw ScriptError(currentToken.pos, "modifier closed is only allowed for class members")
if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn")
throw ScriptError(currentToken.pos, "modifier override outside class is only allowed for extension functions")
if (!isMember && isAbstract && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier abstract at top level is only allowed for classes")
return when (currentToken.value) {
"val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern)
"var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern)
"fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic)
"class" -> {
if (isStatic || isClosed || isOverride)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for class: ${modifiers.joinToString(" ")}"
)
parseClassDeclaration(isAbstract, isExtern)
}
"object" -> {
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for object: ${modifiers.joinToString(" ")}"
)
parseObjectDeclaration(isExtern)
}
"interface" -> {
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for interface: ${modifiers.joinToString(" ")}"
)
// interface is synonym for abstract class
parseClassDeclaration(isAbstract = true, isExtern = isExtern)
}
"enum" -> {
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for enum: ${modifiers.joinToString(" ")}"
)
parseEnumDeclaration(isExtern)
}
else -> throw ScriptError(
currentToken.pos,
"expected declaration after modifiers, found ${currentToken.value}"
)
}
}
/**
* Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
"abstract", "closed", "override", "extern", "private", "protected", "static", "open" -> {
parseDeclarationWithModifiers(id)
}
"interface" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseClassDeclaration(isAbstract = true)
}
"val" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseVarDeclaration(false, Visibility.Public)
}
"var" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseVarDeclaration(true, Visibility.Public)
}
// Ensure function declarations are recognized in all contexts (including class bodies)
"fun" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isExtern = false, isStatic = false)
}
"fn" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isExtern = false, isStatic = false)
}
// Visibility modifiers for declarations: private/protected val/var/fun/fn
"while" -> parseWhileStatement()
"do" -> parseDoWhileStatement()
"for" -> parseForStatement()
"return" -> parseReturnStatement(id.pos)
"break" -> parseBreakStatement(id.pos)
"continue" -> parseContinueStatement(id.pos)
"if" -> parseIfStatement()
"class" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseClassDeclaration()
}
"object" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseObjectDeclaration()
}
"init" -> {
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
miniSink?.onEnterFunction(null)
val block = parseBlock()
miniSink?.onExitFunction(cc.currentPos())
lastParsedBlockRange?.let { range ->
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
}
val initPos = id.pos
val initStmt = object : Statement() {
override val pos: Pos = initPos
override suspend fun execute(scope: Scope): Obj {
val cls = scope.thisObj.objClass
val saved = scope.currentClassCtx
scope.currentClassCtx = cls
try {
block.execute(scope)
} finally {
scope.currentClassCtx = saved
}
return ObjVoid
}
}
object : Statement() {
override val pos: Pos = id.pos
override suspend fun execute(scope: Scope): Obj {
scope.currentClassCtx?.instanceInitializers?.add(initStmt)
return ObjVoid
}
}
} else null
}
"enum" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseEnumDeclaration()
}
"try" -> parseTryStatement()
"throw" -> parseThrowStatement(id.pos)
"when" -> parseWhenStatement()
else -> {
// triples
cc.previous()
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(Visibility.Private, isExtern)
}
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(
Visibility.Private,
isExtern,
isStatic = true
)
cc.matchQualifiers("fun", "static") -> parseFunctionDeclaration(
Visibility.Public,
isExtern,
isStatic = true
)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isExtern = isExtern)
cc.matchQualifiers("fun") -> {
pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isExtern = isExtern)
}
cc.matchQualifiers("fn") -> {
pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isExtern = isExtern)
}
cc.matchQualifiers("val", "private", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Private,
isStatic = true
)
}
cc.matchQualifiers("val", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Public,
isStatic = true
)
}
cc.matchQualifiers("val", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Private
)
}
cc.matchQualifiers("var", "static") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Public,
isStatic = true
)
}
cc.matchQualifiers("var", "static", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Private,
isStatic = true
)
}
cc.matchQualifiers("var", "private") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Private
)
}
cc.matchQualifiers("val", "open") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
false,
Visibility.Private,
true
)
}
cc.matchQualifiers("var", "open") -> {
pendingDeclStart = id.pos; pendingDeclDoc = consumePendingDoc(); parseVarDeclaration(
true,
Visibility.Private,
true
)
}
else -> {
cc.next()
null
}
}
}
}
private suspend fun parseWhenStatement(): Statement {
// has a value, when(value) ?
var t = cc.nextNonWhitespace()
val stmt = if (t.type == Token.Type.LPAREN) {
// when(value)
val value = parseStatement() ?: throw ScriptError(cc.currentPos(), "when(value) expected")
cc.skipTokenOfType(Token.Type.RPAREN)
t = cc.next()
if (t.type != Token.Type.LBRACE) throw ScriptError(t.pos, "when { ... } expected")
val cases = mutableListOf<WhenCase>()
var elseCase: Statement? = null
// there could be 0+ then clauses
// condition could be a value, in and is clauses:
// parse several conditions for one then clause
// loop cases
outer@ while (true) {
var skipParseBody = false
val currentConditions = mutableListOf<WhenCondition>()
// loop conditions
while (true) {
t = cc.nextNonWhitespace()
when (t.type) {
Token.Type.IN,
Token.Type.NOTIN -> {
val negated = t.type == Token.Type.NOTIN
val container = parseExpression() ?: throw ScriptError(cc.currentPos(), "type expected")
currentConditions += WhenInCondition(container, negated, t.pos)
}
Token.Type.IS,
Token.Type.NOTIS -> {
val negated = t.type == Token.Type.NOTIS
val caseType = parseExpression() ?: throw ScriptError(cc.currentPos(), "type expected")
currentConditions += WhenIsCondition(caseType, negated, t.pos)
}
Token.Type.COMMA ->
continue
Token.Type.ARROW ->
break
Token.Type.RBRACE ->
break@outer
else -> {
if (t.value == "else") {
cc.skipTokens(Token.Type.ARROW)
if (elseCase != null) throw ScriptError(
cc.currentPos(),
"when else block already defined"
)
elseCase = parseStatement()?.let { unwrapBytecodeDeep(it) }
?: throw ScriptError(
cc.currentPos(),
"when else block expected"
)
skipParseBody = true
} else {
cc.previous()
val x = parseExpression()
?: throw ScriptError(cc.currentPos(), "when case condition expected")
currentConditions += WhenEqualsCondition(x, t.pos)
}
}
}
}
// parsed conditions?
if (!skipParseBody) {
val block = parseStatement()?.let { unwrapBytecodeDeep(it) }
?: throw ScriptError(cc.currentPos(), "when case block expected")
cases += WhenCase(currentConditions, block)
}
}
val whenPos = t.pos
WhenStatement(value, cases, elseCase, whenPos)
} else {
// when { cond -> ... }
TODO("when without object is not yet implemented")
}
return wrapBytecode(stmt)
}
private suspend fun parseThrowStatement(start: Pos): Statement {
val throwStatement = parseStatement() ?: throw ScriptError(cc.currentPos(), "throw object expected")
return wrapBytecode(ThrowStatement(throwStatement, start))
}
private data class CatchBlockData(
val catchVar: Token,
val classNames: List<String>,
val block: Statement
)
private suspend fun parseTryStatement(): Statement {
fun withCatchSlot(block: Statement, catchName: String): Statement {
val stmt = block as? BlockStatement ?: return block
if (stmt.slotPlan.containsKey(catchName)) return stmt
val basePlan = stmt.slotPlan
val newPlan = LinkedHashMap<String, Int>(basePlan.size + 1)
newPlan[catchName] = 0
for ((name, idx) in basePlan) {
newPlan[name] = idx + 1
}
return BlockStatement(stmt.block, newPlan, stmt.captureSlots, stmt.pos)
}
fun stripCatchCaptures(block: Statement): Statement {
val stmt = block as? BlockStatement ?: return block
if (stmt.captureSlots.isEmpty()) return stmt
return BlockStatement(stmt.block, stmt.slotPlan, emptyList(), stmt.pos)
}
fun resolveExceptionClass(scope: Scope, name: String): ObjClass {
val rec = scope[name]
val cls = rec?.value as? ObjClass
if (cls != null) return cls
if (name == "Exception") return ObjException.Root
scope.raiseSymbolNotFound("error class does not exist or is not a class: $name")
}
val body = unwrapBytecodeDeep(parseBlock())
val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next()
while (t.value == "catch") {
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
t = cc.next()
if (t.type != Token.Type.ID) throw ScriptError(t.pos, "expected catch variable")
val catchVar = t
val exClassNames = mutableListOf<String>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
// load list of exception classes
do {
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "expected exception class name")
exClassNames += t.value
resolutionSink?.reference(t.value, t.pos)
t = cc.next()
when (t.type) {
Token.Type.COMMA -> {
continue
}
Token.Type.RPAREN -> {
break
}
else -> throw ScriptError(t.pos, "syntax error: expected ',' or ')'")
}
} while (true)
} else {
// no type!
exClassNames += "Exception"
cc.skipTokenOfType(Token.Type.RPAREN)
}
val block = try {
resolutionSink?.enterScope(ScopeKind.BLOCK, catchVar.pos, null)
resolutionSink?.declareSymbol(catchVar.value, SymbolKind.LOCAL, isMutable = false, pos = catchVar.pos)
stripCatchCaptures(
withCatchSlot(
unwrapBytecodeDeep(parseBlockWithPredeclared(listOf(catchVar.value to false))),
catchVar.value
)
)
} finally {
resolutionSink?.exitScope(cc.currentPos())
}
catches += CatchBlockData(catchVar, exClassNames, block)
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
} else {
// no (e: Exception) block: should be the shortest variant `catch { ... }`
cc.skipTokenOfType(Token.Type.LBRACE, "expected catch(...) or catch { ... } here")
val itToken = Token("it", cc.currentPos(), Token.Type.ID)
val block = try {
resolutionSink?.enterScope(ScopeKind.BLOCK, itToken.pos, null)
resolutionSink?.declareSymbol(itToken.value, SymbolKind.LOCAL, isMutable = false, pos = itToken.pos)
stripCatchCaptures(
withCatchSlot(
unwrapBytecodeDeep(parseBlockWithPredeclared(listOf(itToken.value to false), skipLeadingBrace = true)),
itToken.value
)
)
} finally {
resolutionSink?.exitScope(cc.currentPos())
}
catches += CatchBlockData(itToken, listOf("Exception"), block)
t = cc.next()
}
}
val finallyClause = if (t.value == "finally") {
unwrapBytecodeDeep(parseBlock())
} else {
cc.previous()
null
}
if (catches.isEmpty() && finallyClause == null)
throw ScriptError(cc.currentPos(), "try block must have either catch or finally clause or both")
val stmtPos = body.pos
val tryStatement = object : Statement() {
override val pos: Pos = stmtPos
override suspend fun execute(scope: Scope): Obj {
var result: Obj = ObjVoid
try {
// body is a parsed block, it already has separate context
result = body.execute(scope)
} catch (e: ReturnException) {
throw e
} catch (e: LoopBreakContinueException) {
throw e
} catch (e: Exception) {
// convert to appropriate exception
val caughtObj = when (e) {
is ExecutionError -> e.errorObject
else -> ObjUnknownException(scope, e.message ?: e.toString())
}
// let's see if we should catch it:
var isCaught = false
for (cdata in catches) {
var match: Obj? = null
for (exceptionClassName in cdata.classNames) {
val exObj = resolveExceptionClass(scope, exceptionClassName)
if (caughtObj.isInstanceOf(exObj)) {
match = caughtObj
break
}
}
if (match != null) {
val catchContext = scope.createChildScope(pos = cdata.catchVar.pos).apply {
skipScopeCreation = true
}
catchContext.addItem(cdata.catchVar.value, false, caughtObj)
result = cdata.block.execute(catchContext)
isCaught = true
break
}
}
// rethrow if not caught this exception
if (!isCaught)
throw e
} finally {
// finally clause does not alter result!
finallyClause?.execute(scope)
}
return result
}
}
return TryStatement(tryStatement, stmtPos)
}
private fun parseEnumDeclaration(isExtern: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
resolutionSink?.declareSymbol(nameToken.value, SymbolKind.ENUM, isMutable = false, pos = nameToken.pos)
// so far only simplest enums:
val names = mutableListOf<String>()
val positions = mutableListOf<Pos>()
// skip '{'
cc.skipTokenOfType(Token.Type.LBRACE)
if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) {
do {
val t = cc.nextNonWhitespace()
when (t.type) {
Token.Type.ID -> {
names += t.value
positions += t.pos
val t1 = cc.nextNonWhitespace()
when (t1.type) {
Token.Type.COMMA ->
continue
Token.Type.RBRACE -> break
else -> {
t1.raiseSyntax("unexpected token")
}
}
}
else -> t.raiseSyntax("expected enum entry name")
}
} while (true)
} else {
cc.nextNonWhitespace()
}
miniSink?.onEnumDecl(
MiniEnumDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
entries = names,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern,
entryPositions = positions
)
)
val stmtPos = startPos
val enumDeclStatement = object : Statement() {
override val pos: Pos = stmtPos
override suspend fun execute(scope: Scope): Obj {
val enumClass = ObjEnumClass.createSimpleEnum(nameToken.value, names)
scope.addItem(nameToken.value, false, enumClass, recordType = ObjRecord.Type.Enum)
return enumClass
}
}
return EnumDeclStatement(enumDeclStatement, stmtPos)
}
private suspend fun parseObjectDeclaration(isExtern: Boolean = false): Statement {
val next = cc.peekNextNonWhitespace()
val nameToken = if (next.type == Token.Type.ID) cc.requireToken(Token.Type.ID) else null
val startPos = pendingDeclStart ?: nameToken?.pos ?: cc.current().pos
val className = nameToken?.value ?: generateAnonName(startPos)
if (nameToken != null) {
resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos)
}
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
// Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )?
data class BaseSpec(val name: String, val args: List<ParsedArgument>?)
val baseSpecs = mutableListOf<BaseSpec>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do {
val baseId = cc.requireToken(Token.Type.ID, "base class name expected")
resolutionSink?.reference(baseId.value, baseId.pos)
var argsList: List<ParsedArgument>? = null
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
argsList = parseArgsNoTailBlock()
}
baseSpecs += BaseSpec(baseId.value, argsList)
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
}
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
pushInitScope()
// Robust body detection
var classBodyRange: MiniRange? = null
val bodyInit: Statement? = inCodeContext(CodeContext.ClassBody(className, isExtern = isExtern)) {
val saved = cc.savePos()
val nextBody = cc.nextNonWhitespace()
if (nextBody.type == Token.Type.LBRACE) {
// Emit MiniClassDecl before body parsing to track members via enter/exit
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = className,
bases = baseSpecs.map { it.name },
bodyRange = null,
doc = doc,
nameStart = nameToken?.pos ?: startPos,
isObject = true,
isExtern = isExtern
)
miniSink?.onEnterClass(node)
}
val bodyStart = nextBody.pos
val classSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(classSlotPlan)
resolutionSink?.declareClass(className, baseSpecs.map { it.name }, startPos)
resolutionSink?.enterScope(ScopeKind.CLASS, startPos, className, baseSpecs.map { it.name })
val st = try {
withLocalNames(emptySet()) {
parseScript()
}
} finally {
slotPlanStack.removeLast()
resolutionSink?.exitScope(cc.currentPos())
}
val rbTok = cc.next()
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in object body")
classBodyRange = MiniRange(bodyStart, rbTok.pos)
miniSink?.onExitClass(rbTok.pos)
st
} else {
// No body, but still emit the class
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = className,
bases = baseSpecs.map { it.name },
bodyRange = null,
doc = doc,
nameStart = nameToken?.pos ?: startPos,
isObject = true,
isExtern = isExtern
)
miniSink?.onClassDecl(node)
}
resolutionSink?.declareClass(className, baseSpecs.map { it.name }, startPos)
cc.restorePos(saved)
null
}
}
val initScope = popInitScope()
val declStatement = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
val parentClasses = baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name] ?: throw ScriptError(startPos, "unknown base class: ${baseSpec.name}")
(rec.value as? ObjClass) ?: throw ScriptError(startPos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
newClass.isAnonymous = nameToken == null
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
for (i in parentClasses.indices) {
val argsList = baseSpecs[i].args
// In object, we evaluate parent args once at creation time
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
}
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
classScope.addConst("object", newClass)
bodyInit?.execute(classScope)
// Create instance (singleton)
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
if (nameToken != null)
scope.addItem(className, false, instance)
return instance
}
}
return ClassDeclStatement(declStatement, startPos)
}
private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos)
return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) {
val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
val classSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
classCtx?.slotPlanId = classSlotPlan.id
constructorArgsDeclaration?.params?.forEach { param ->
val mutable = param.accessType?.isMutable ?: false
declareSlotNameIn(classSlotPlan, param.name, mutable, isDelegated = false)
}
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType != null) {
classCtx?.declaredMembers?.add(param.name)
}
}
// Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )?
data class BaseSpec(val name: String, val args: List<ParsedArgument>?)
val baseSpecs = mutableListOf<BaseSpec>()
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do {
val baseId = cc.requireToken(Token.Type.ID, "base class name expected")
resolutionSink?.reference(baseId.value, baseId.pos)
var argsList: List<ParsedArgument>? = null
// Optional constructor args of the base — parse and ignore for now (MVP), just to consume tokens
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
// Parse args without consuming any following block so that a class body can follow safely
argsList = parseArgsNoTailBlock()
}
baseSpecs += BaseSpec(baseId.value, argsList)
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
}
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
pushInitScope()
// Robust body detection: peek next non-whitespace token; if it's '{', consume and parse the body
var classBodyRange: MiniRange? = null
val bodyInit: Statement? = run {
val saved = cc.savePos()
val next = cc.nextNonWhitespace()
val ctorFields = mutableListOf<MiniCtorField>()
constructorArgsDeclaration?.let { ad ->
for (p in ad.params) {
val at = p.accessType
val mutable = at == AccessType.Var
ctorFields += MiniCtorField(
name = p.name,
mutable = mutable,
type = p.miniType,
nameStart = p.pos
)
}
}
if (next.type == Token.Type.LBRACE) {
// Emit MiniClassDecl before body parsing to track members via enter/exit
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
bases = baseSpecs.map { it.name },
bodyRange = null,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern
)
miniSink?.onEnterClass(node)
}
// parse body
val bodyStart = next.pos
slotPlanStack.add(classSlotPlan)
resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos)
resolutionSink?.enterScope(ScopeKind.CLASS, startPos, nameToken.value, baseSpecs.map { it.name })
constructorArgsDeclaration?.params?.forEach { param ->
val accessType = param.accessType
val kind = if (accessType != null) SymbolKind.MEMBER else SymbolKind.PARAM
val mutable = accessType?.isMutable ?: false
resolutionSink?.declareSymbol(param.name, kind, mutable, param.pos)
}
val st = try {
classCtx?.let { ctx ->
predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides)
val baseIds = collectBaseMemberIds(baseSpecs.map { it.name })
ctx.memberFieldIds.putAll(baseIds.fieldIds)
ctx.memberMethodIds.putAll(baseIds.methodIds)
ctx.nextFieldId = maxOf(ctx.nextFieldId, baseIds.nextFieldId)
ctx.nextMethodId = maxOf(ctx.nextMethodId, baseIds.nextMethodId)
for (member in ctx.declaredMembers) {
val isOverride = ctx.memberOverrides[member] == true
val hasBaseField = member in baseIds.fieldIds
val hasBaseMethod = member in baseIds.methodIds
if (isOverride) {
if (!hasBaseField && !hasBaseMethod) {
throw ScriptError(nameToken.pos, "member $member is marked 'override' but does not override anything")
}
} else {
if (hasBaseField || hasBaseMethod) {
throw ScriptError(nameToken.pos, "member $member overrides parent member but 'override' keyword is missing")
}
}
if (!ctx.memberFieldIds.containsKey(member)) {
ctx.memberFieldIds[member] = ctx.nextFieldId++
}
if (!ctx.memberMethodIds.containsKey(member)) {
ctx.memberMethodIds[member] = ctx.nextMethodId++
}
}
compileClassInfos[nameToken.value] = CompileClassInfo(
name = nameToken.value,
fieldIds = ctx.memberFieldIds.toMap(),
methodIds = ctx.memberMethodIds.toMap(),
nextFieldId = ctx.nextFieldId,
nextMethodId = ctx.nextMethodId
)
}
withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
parseScript()
}
} finally {
slotPlanStack.removeLast()
resolutionSink?.exitScope(cc.currentPos())
}
val rbTok = cc.next()
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body")
classBodyRange = MiniRange(bodyStart, rbTok.pos)
miniSink?.onExitClass(rbTok.pos)
st
} else {
// No body, but still emit the class
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
bases = baseSpecs.map { it.name },
bodyRange = null,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern
)
miniSink?.onClassDecl(node)
}
resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos)
// restore if no body starts here
cc.restorePos(saved)
null
}
}
val initScope = popInitScope()
// create class
val className = nameToken.value
// @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Variable else AccessType.Initialization
// @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public
// create instance constructor
// create custom objClass with all fields and instance constructor
val constructorCode = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
val instance = scope.thisObj as ObjInstance
// Constructor parameters have been assigned to instance scope by ObjClass.callOn before
// invoking parent/child constructors.
// IMPORTANT: do not execute class body here; class body was executed once in the class scope
// to register methods and prepare initializers. Instance constructor should be empty unless
// we later add explicit constructor body syntax.
return instance
}
}
val classInfo = compileClassInfos[className]
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
// accessors, constructor registration, etc.
// Resolve parent classes by name at execution time
val parentClasses = baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name]
val cls = rec?.value as? ObjClass
if (cls != null) return@map cls
if (baseSpec.name == "Exception") return@map ObjException.Root
if (rec == null) throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}")
throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also {
it.isAbstract = isAbstract
it.instanceConstructor = constructorCode
it.constructorMeta = constructorArgsDeclaration
// Attach per-parent constructor args (thunks) if provided
for (i in parentClasses.indices) {
val argsList = baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
// Register constructor fields in the class members
constructorArgsDeclaration?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name, ObjNull,
isMutable = p.accessType == AccessType.Var,
visibility = p.visibility ?: Visibility.Public,
declaringClass = it,
// Constructor fields are not currently supporting override/closed in parser
// but we should pass Pos.builtIn to skip validation for now if needed,
// or p.pos to allow it.
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
fieldId = classInfo?.fieldIds?.get(p.name)
)
}
}
}
scope.addItem(className, false, newClass)
// Prepare class scope for class-scope members (static) and future registrations
val classScope = scope.createChildScope(newThisObj = newClass)
// Set lexical class context for visibility tagging inside class body
classScope.currentClassCtx = newClass
newClass.classScope = classScope
// Execute class body once in class scope to register instance methods and prepare instance field initializers
bodyInit?.execute(classScope)
if (initScope.isNotEmpty()) {
for (s in initScope)
s.execute(classScope)
}
newClass.checkAbstractSatisfaction(nameToken.pos)
// Debug summary: list registered instance methods and class-scope functions for this class
return newClass
}
}
ClassDeclStatement(classDeclStatement, startPos)
}
}
private fun getLabel(maxDepth: Int = 2): String? {
var cnt = 0
var found: String? = null
while (cc.hasPrevious() && cnt < maxDepth) {
val t = cc.previous()
cnt++
if (t.type == Token.Type.LABEL || t.type == Token.Type.ATLABEL) {
found = t.value
break
}
}
while (cnt-- > 0) cc.next()
return found
}
private suspend fun parseForStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val start = ensureLparen()
val tVar = cc.next()
if (tVar.type != Token.Type.ID)
throw ScriptError(tVar.pos, "Bad for statement: expected loop variable")
val tOp = cc.next()
if (tOp.value == "in") {
// in loop
// We must parse an expression here. Using parseStatement() would treat a leading '{'
// as a block, breaking inline map literals like: for (i in {foo: "bar"}) { ... }
// So we parse an expression explicitly and wrap it into a StatementRef.
val exprAfterIn = parseExpression() ?: throw ScriptError(start, "Bad for statement: expected expression")
val source: Statement = exprAfterIn
val constRange = (exprAfterIn as? ExpressionStatement)?.ref?.let { ref ->
constIntRangeOrNull(ref)
}
ensureRparen()
// Expose the loop variable name to the parser so identifiers inside the loop body
// can be emitted as FastLocalVarRef when enabled.
val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value
val loopSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(loopSlotPlan)
declareSlotName(tVar.value, isMutable = true, isDelegated = false)
val (canBreak, body, elseStatement) = try {
resolutionSink?.enterScope(ScopeKind.BLOCK, tVar.pos, null)
resolutionSink?.declareSymbol(tVar.value, SymbolKind.LOCAL, isMutable = true, pos = tVar.pos)
withLocalNames(namesForLoop) {
val loopParsed = cc.parseLoop {
if (cc.current().type == Token.Type.LBRACE) parseBlock()
else parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body")
}
// possible else clause
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val elseStmt = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) {
parseStatement()
} else {
cc.previous()
null
}
Triple(loopParsed.first, loopParsed.second, elseStmt)
}
} finally {
resolutionSink?.exitScope(cc.currentPos())
slotPlanStack.removeLast()
}
val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan)
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 fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? {
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? {
return when (ref) {
is ConstRef -> (ref.constValue as? ObjInt)?.value
is StatementRef -> {
val stmt = ref.statement
if (stmt is ExpressionStatement) constIntValueOrNull(stmt.ref) else null
}
else -> null
}
}
@Suppress("UNUSED_VARIABLE")
private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val loopSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(loopSlotPlan)
var conditionSlotPlan: SlotPlan = loopSlotPlan
val (canBreak, parsedBody) = try {
cc.parseLoop {
if (cc.current().type == Token.Type.LBRACE) {
val (blockStmt, blockPlan) = parseLoopBlockWithPlan()
conditionSlotPlan = blockPlan
blockStmt
} else {
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()
if (tWhile.type != Token.Type.ID || tWhile.value != "while")
throw ScriptError(tWhile.pos, "Expected 'while' after do body")
ensureLparen()
slotPlanStack.add(conditionSlotPlan)
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" }) {
val parsedElse = parseStatement()
parsedElse?.let { unwrapBytecodeDeep(it) }
} else {
cc.previous()
null
}
val loopPlanSnapshot = slotPlanIndices(conditionSlotPlan)
return DoWhileStatement(body, condition, elseStatement, label, loopPlanSnapshot, body.pos)
}
private suspend fun parseWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }
val start = ensureLparen()
val condition =
parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression")
ensureRparen()
val loopSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(loopSlotPlan)
val (canBreak, parsedBody) = try {
cc.parseLoop {
if (cc.current().type == Token.Type.LBRACE) parseLoopBlock()
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" }) {
val parsedElse = parseStatement()
parsedElse?.let { unwrapBytecodeDeep(it) }
} else {
cc.previous()
null
}
val loopPlanSnapshot = slotPlanIndices(loopSlotPlan)
return WhileStatement(condition, body, elseStatement, label, canBreak, loopPlanSnapshot, body.pos)
}
private suspend fun parseBreakStatement(start: Pos): Statement {
var t = cc.next()
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
cc.previous()
null
} else {
t.value
}?.also {
// check that label is defined
cc.ensureLabelIsValid(start, it)
}
// expression?
t = cc.next()
cc.previous()
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
t.type != Token.Type.SEMICOLON &&
t.type != Token.Type.NEWLINE &&
t.type != Token.Type.RBRACE &&
t.type != Token.Type.RPAREN &&
t.type != Token.Type.RBRACKET &&
t.type != Token.Type.COMMA &&
t.type != Token.Type.EOF)
) {
// we have something on this line, could be expression
parseStatement()
} else null
cc.addBreak()
return BreakStatement(label, resultExpr, start)
}
private fun parseContinueStatement(start: Pos): Statement {
val t = cc.next()
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
cc.previous()
null
} else {
t.value
}?.also {
// check that label is defined
cc.ensureLabelIsValid(start, it)
}
cc.addBreak()
return ContinueStatement(label, start)
}
private suspend fun parseReturnStatement(start: Pos): Statement {
var t = cc.next()
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
cc.previous()
null
} else {
t.value
}
// expression?
t = cc.next()
cc.previous()
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
t.type != Token.Type.SEMICOLON &&
t.type != Token.Type.NEWLINE &&
t.type != Token.Type.RBRACE &&
t.type != Token.Type.RPAREN &&
t.type != Token.Type.RBRACKET &&
t.type != Token.Type.COMMA &&
t.type != Token.Type.EOF)
) {
// we have something on this line, could be expression
parseExpression()
} else null
return ReturnStatement(label, resultExpr, start)
}
private fun ensureRparen(): Pos {
val t = cc.next()
if (t.type != Token.Type.RPAREN)
throw ScriptError(t.pos, "expected ')'")
return t.pos
}
private fun ensureLparen(): Pos {
val t = cc.next()
if (t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "expected '('")
return t.pos
}
private suspend fun parseIfStatement(): Statement {
val start = ensureLparen()
val condition = parseExpression()
?: throw ScriptError(start, "Bad if statement: expected expression")
val pos = ensureRparen()
val ifBody = parseStatement() ?: throw ScriptError(pos, "Bad if statement: expected statement")
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
// could be else block:
val t2 = cc.nextNonWhitespace()
// we generate different statements: optimization
val stmt = if (t2.type == Token.Type.ID && t2.value == "else") {
val elseBody =
parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement")
IfStatement(condition, ifBody, elseBody, start)
} else {
cc.previous()
IfStatement(condition, ifBody, null, start)
}
return wrapBytecode(stmt)
}
private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isExtern: Boolean = false,
isStatic: Boolean = false,
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
var t = cc.next()
val start = t.pos
var extTypeName: String? = null
var name = if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "Expected identifier after 'fun'")
else t.value
var nameStartPos: Pos = t.pos
var receiverMini: MiniTypeRef? = null
val annotation = lastAnnotation
val parentContext = codeContexts.last()
// Is extension?
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
cc.nextNonWhitespace() // consume DOT
extTypeName = name
resolutionSink?.reference(extTypeName, start)
val receiverEnd = Pos(start.source, start.line, start.column + name.length)
receiverMini = MiniTypeName(
range = MiniRange(start, receiverEnd),
segments = listOf(MiniTypeName.Segment(name, MiniRange(start, receiverEnd))),
nullable = false
)
t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "illegal extension format: expected function name")
name = t.value
nameStartPos = t.pos
registerExtensionName(extTypeName, name)
}
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
val memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
val externCallSignature = if (actualExtern) importManager.rootScope.getLocalRecordDirect(name)?.callSignature else null
val declKind = if (parentContext is CodeContext.ClassBody) SymbolKind.MEMBER else SymbolKind.FUNCTION
resolutionSink?.declareSymbol(name, declKind, isMutable = false, pos = nameStartPos, isOverride = isOverride)
if (parentContext is CodeContext.ClassBody && extTypeName == null) {
parentContext.declaredMembers.add(name)
}
if (declKind != SymbolKind.MEMBER) {
declareLocalName(name, isMutable = false)
}
if (extensionWrapperName != null) {
declareLocalName(extensionWrapperName, isMutable = false)
}
val argsDeclaration: ArgsDeclaration =
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.nextNonWhitespace() // consume (
parseArgsDeclaration() ?: ArgsDeclaration(emptyList(), Token.Type.RPAREN)
} else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
// Optional return type
val returnTypeMini: MiniTypeRef? = if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
parseTypeDeclarationWithMini().second
} else null
var isDelegated = false
var delegateExpression: Statement? = null
if (cc.peekNextNonWhitespace().type == Token.Type.BY) {
cc.nextNonWhitespace() // consume by
isDelegated = true
delegateExpression = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected delegate expression")
}
if (!isDelegated && argsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
t.pos,
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
)
// Capture doc locally to reuse even if we need to emit later
val declDocLocal = pendingDeclDoc
val outerLabel = lastLabel
val node = run {
val params = argsDeclaration.params.map { p ->
MiniParam(
name = p.name,
type = p.miniType,
nameStart = p.pos
)
}
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
val node = MiniFunDecl(
range = declRange,
name = name,
params = params,
returnType = returnTypeMini,
body = null,
doc = declDocLocal,
nameStart = nameStartPos,
receiver = receiverMini,
isExtern = actualExtern,
isStatic = isStatic
)
miniSink?.onFunDecl(node)
pendingDeclDoc = null
node
}
miniSink?.onEnterFunction(node)
val implicitThisMembers = extTypeName != null || (parentContext is CodeContext.ClassBody && !isStatic)
return inCodeContext(
CodeContext.Function(
name,
implicitThisMembers = implicitThisMembers,
implicitThisTypeName = extTypeName
)
) {
cc.labels.add(name)
outerLabel?.let { cc.labels.add(it) }
val paramNamesList = argsDeclaration.params.map { it.name }
val paramNames: Set<String> = paramNamesList.toSet()
val paramSlotPlan = buildParamSlotPlan(paramNamesList)
val capturePlan = CapturePlan(paramSlotPlan)
val rangeParamNames = argsDeclaration.params
.filter { isRangeType(it.type) }
.map { it.name }
.toSet()
// Parse function body while tracking declared locals to compute precise capacity hints
currentLocalDeclCount
localDeclCountStack.add(0)
slotPlanStack.add(paramSlotPlan)
capturePlanStack.add(capturePlan)
rangeParamNamesStack.add(rangeParamNames)
resolutionSink?.enterScope(ScopeKind.FUNCTION, start, name)
for (param in argsDeclaration.params) {
resolutionSink?.declareSymbol(param.name, SymbolKind.PARAM, isMutable = false, pos = param.pos)
}
val parsedFnStatements = try {
val returnLabels = buildSet {
add(name)
outerLabel?.let { add(it) }
}
returnLabelStack.addLast(returnLabels)
try {
if (actualExtern)
object : Statement() {
override val pos: Pos = start
override suspend fun execute(scope: Scope): Obj {
scope.raiseError("extern function not provided: $name")
}
}
else if (isAbstract || isDelegated) {
null
} else
withLocalNames(paramNames) {
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ASSIGN) {
cc.nextNonWhitespace() // consume '='
if (cc.peekNextNonWhitespace().value == "return")
throw ScriptError(cc.currentPos(), "return is not allowed in shorthand function")
val exprStmt = parseExpression()
?: throw ScriptError(cc.currentPos(), "Expected function body expression")
// Shorthand function returns the expression value.
exprStmt
} else {
parseBlock()
}
}
} finally {
returnLabelStack.removeLast()
}
} finally {
rangeParamNamesStack.removeLast()
capturePlanStack.removeLast()
slotPlanStack.removeLast()
resolutionSink?.exitScope(cc.currentPos())
}
val rawFnStatements = parsedFnStatements?.let {
if (containsUnsupportedForBytecode(it)) unwrapBytecodeDeep(it) else it
}
val fnStatements = rawFnStatements?.let { stmt ->
if (useBytecodeStatements && !containsUnsupportedForBytecode(stmt)) {
wrapFunctionBytecode(stmt, name)
} else {
stmt
}
}
// Capture and pop the local declarations count for this function
val fnLocalDecls = localDeclCountStack.removeLastOrNull() ?: 0
var closure: Scope? = null
var captureContext: Scope? = null
val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan)
val captureSlots = capturePlan.captures.toList()
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
// restore closure where the function was defined, and making a copy of it
// for local space. If there is no closure, we are in, say, class context where
// the closure is in the class initialization and we needn't more:
val context = closure?.let { ClosureScope(callerContext, it) }
?: callerContext
// Capacity hint: parameters + declared locals + small overhead
val capacityHint = paramNames.size + fnLocalDecls + 4
context.hintLocalCapacity(capacityHint)
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
val captureBase = captureContext ?: closure
if (captureBase != null && captureSlots.isNotEmpty()) {
for (capture in captureSlots) {
val rec = captureBase.resolveCaptureRecord(capture.name)
?: captureBase.raiseSymbolNotFound("symbol ${capture.name} not found")
context.updateSlotFor(capture.name, rec)
}
}
// load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
if (extTypeName != null) {
context.thisObj = callerContext.thisObj
}
return try {
fnStatements?.execute(context) ?: ObjVoid
} catch (e: ReturnException) {
if (e.label == null || e.label == name || e.label == outerLabel) e.result
else throw e
}
}
}
cc.labels.remove(name)
outerLabel?.let { cc.labels.remove(it) }
// parentContext
val fnCreateStatement = object : Statement() {
override val pos: Pos = start
override suspend fun execute(context: Scope): Obj {
if (isDelegated) {
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.Callable")
val initValue = delegateExpression!!.execute(context)
val finalDelegate = try {
initValue.invokeInstanceMethod(context, "bind", Arguments(ObjString(name), accessType, context.thisObj))
} catch (e: Exception) {
initValue
}
if (extTypeName != 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(ObjUnset, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply {
delegate = finalDelegate
})
return ObjVoid
}
val th = context.thisObj
if (isStatic) {
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, isTransient = isTransient, type = ObjRecord.Type.Delegated).apply {
delegate = finalDelegate
}
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
delegate = finalDelegate
}
} else if (th is ObjClass) {
val cls: ObjClass = th
val storageName = "${cls.className}::$name"
cls.createField(
name,
ObjUnset,
false,
visibility,
null,
start,
declaringClass = cls,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
type = ObjRecord.Type.Delegated,
methodId = memberMethodId
)
cls.instanceInitializers += object : Statement() {
override val pos: Pos = start
override suspend fun execute(scp: Scope): Obj {
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
val initValue2 = delegateExpression.execute(scp)
val finalDelegate2 = try {
initValue2.invokeInstanceMethod(scp, "bind", Arguments(ObjString(name), accessType2, scp.thisObj))
} catch (e: Exception) {
initValue2
}
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient).apply {
delegate = finalDelegate2
}
return ObjVoid
}
}
} else {
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
delegate = finalDelegate
}
}
return ObjVoid
}
// we added fn in the context. now we must save closure
// for the function, unless we're in the class scope:
if (isStatic || parentContext !is CodeContext.ClassBody)
closure = context
if (parentContext is CodeContext.ClassBody && captureSlots.isNotEmpty())
captureContext = context
val annotatedFnBody = annotation?.invoke(context, ObjString(name), fnBody)
?: fnBody
val compiledFnBody = annotatedFnBody
extTypeName?.let { typeName ->
// class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
val stmt = object : Statement() {
override val pos: Pos = start
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 ->
compiledFnBody.execute(ClosureScope(scope, i.instanceScope))
}
// other classes can create one-time scope for this rare case:
?: compiledFnBody.execute(scope.thisObj.autoInstanceScope(scope))
return result
}
}
context.addExtension(type, name, ObjRecord(stmt, isMutable = false, visibility = visibility, declaringClass = null))
val wrapperName = extensionWrapperName ?: extensionCallableName(typeName, name)
val wrapper = ObjExtensionMethodCallable(name, stmt)
context.addItem(wrapperName, false, wrapper, visibility, recordType = ObjRecord.Type.Fun)
}
// regular function/method
?: run {
val th = context.thisObj
if (!isStatic && th is ObjClass) {
// Instance method declared inside a class body: register on the class
val cls: ObjClass = th
cls.addFn(
name,
isMutable = true,
visibility = visibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start,
methodId = memberMethodId
) {
// Execute with the instance as receiver; set caller lexical class for visibility
val savedCtx = this.currentClassCtx
this.currentClassCtx = cls
try {
(thisObj as? ObjInstance)?.let { i ->
val execScope = i.instanceScope.createChildScope(
pos = this.pos,
args = this.args,
newThisObj = i
)
execScope.currentClassCtx = cls
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, compiledFnBody, visibility, callSignature = externCallSignature)
compiledFnBody
} else {
// top-level or nested function
context.addItem(name, false, compiledFnBody, visibility, callSignature = externCallSignature)
}
}
// as the function can be called from anywhere, we have
// saved the proper context in the closure
return annotatedFnBody
}
}
val declaredFn = FunctionDeclStatement(fnCreateStatement, start)
if (isStatic) {
currentInitScope += declaredFn
NopStatement
} else
declaredFn
}.also {
val bodyRange = lastParsedBlockRange
// Also emit a post-parse MiniFunDecl to be robust in case early emission was skipped by some path
val params = argsDeclaration.params.map { p ->
MiniParam(
name = p.name,
type = p.miniType,
nameStart = p.pos
)
}
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
val node = MiniFunDecl(
range = declRange,
name = name,
params = params,
returnType = returnTypeMini,
body = bodyRange?.let { MiniBlock(it) },
doc = declDocLocal,
nameStart = nameStartPos,
receiver = receiverMini,
isExtern = actualExtern,
isStatic = isStatic
)
miniSink?.onExitFunction(cc.currentPos())
miniSink?.onFunDecl(node)
}
}
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {
return parseBlockWithPredeclared(emptyList(), skipLeadingBrace)
}
private fun resolveInitializerObjClass(initializer: Statement?): ObjClass? {
if (initializer is BytecodeStatement) {
val fn = initializer.bytecodeFunction()
if (fn.cmds.any { it is CmdListLiteral }) return ObjList.type
if (fn.cmds.any { it is CmdMakeRange || it is CmdRangeIntBounds }) return ObjRange.type
}
var initStmt = initializer
while (initStmt is BytecodeStatement) {
initStmt = initStmt.original
}
val initRef = (initStmt as? ExpressionStatement)?.ref
val directRef = when (initRef) {
is StatementRef -> (initRef.statement as? ExpressionStatement)?.ref
else -> initRef
}
return when (directRef) {
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
is ImplicitThisMethodCallRef -> {
if (directRef.methodName() == "iterator") ObjIterator else null
}
is ThisMethodSlotCallRef -> {
if (directRef.methodName() == "iterator") ObjIterator else null
}
is MethodCallRef -> {
if (directRef.name == "iterator") ObjIterator else null
}
is CallRef -> {
val target = directRef.target
when {
target is LocalVarRef && target.name == "List" -> ObjList.type
target is LocalVarRef && target.name == "Map" -> ObjMap.type
target is LocalVarRef && target.name == "iterator" -> ObjIterator
target is ImplicitThisMemberRef && target.name == "iterator" -> ObjIterator
target is ThisFieldSlotRef && target.name == "iterator" -> ObjIterator
target is FieldRef && target.name == "iterator" -> ObjIterator
target is LocalSlotRef -> resolveClassByName(target.name)
target is LocalVarRef -> resolveClassByName(target.name)
target is ConstRef -> target.constValue as? ObjClass
else -> null
}
}
is ConstRef -> when (directRef.constValue) {
is ObjList -> ObjList.type
is ObjMap -> ObjMap.type
is ObjRange -> ObjRange.type
else -> null
}
else -> null
}
}
private fun resolveTypeDeclObjClass(type: TypeDecl): ObjClass? {
val rawName = when (type) {
is TypeDecl.Simple -> type.name
is TypeDecl.Generic -> type.name
else -> return null
}
val name = rawName.substringAfterLast('.')
return when (name) {
"Object", "Obj" -> Obj.rootObjectType
"String" -> ObjString.type
"Int" -> ObjInt.type
"Real" -> ObjReal.type
"Bool" -> ObjBool.type
"Char" -> ObjChar.type
"List" -> ObjList.type
"Map" -> ObjMap.type
"Set" -> ObjSet.type
"Range", "IntRange" -> ObjRange.type
"Iterator" -> ObjIterator
"Iterable" -> ObjIterable
"Collection" -> ObjCollection
"Array" -> ObjArray
"Deferred" -> ObjDeferred.type
"CompletableDeferred" -> ObjCompletableDeferred.type
"Mutex" -> ObjMutex.type
"Flow" -> ObjFlow.type
"FlowBuilder" -> ObjFlowBuilder.type
"Regex" -> ObjRegex.type
"RegexMatch" -> ObjRegexMatch.type
"MapEntry" -> ObjMapEntry.type
"Exception" -> ObjException.Root
"Callable" -> Statement.type
else -> resolveClassByName(rawName) ?: resolveClassByName(name)
}
}
private fun resolveClassByName(name: String): ObjClass? {
val rec = seedScope?.get(name) ?: importManager.rootScope.get(name)
(rec?.value as? ObjClass)?.let { return it }
val info = compileClassInfos[name] ?: return null
return compileClassStubs.getOrPut(info.name) {
val stub = ObjInstanceClass(info.name)
for ((fieldName, fieldId) in info.fieldIds) {
stub.createField(
fieldName,
ObjNull,
isMutable = true,
visibility = Visibility.Public,
pos = Pos.builtIn,
declaringClass = stub,
type = ObjRecord.Type.Field,
fieldId = fieldId
)
}
for ((methodName, methodId) in info.methodIds) {
stub.createField(
methodName,
ObjNull,
isMutable = false,
visibility = Visibility.Public,
pos = Pos.builtIn,
declaringClass = stub,
isAbstract = true,
type = ObjRecord.Type.Fun,
methodId = methodId
)
}
stub
}
}
private suspend fun parseBlockWithPredeclared(
predeclared: List<Pair<String, Boolean>>,
skipLeadingBrace: Boolean = false
): Statement {
val startPos = cc.currentPos()
if (!skipLeadingBrace) {
val t = cc.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
}
resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null)
val blockSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
for ((name, isMutable) in predeclared) {
declareSlotNameIn(blockSlotPlan, name, isMutable, isDelegated = false)
}
slotPlanStack.add(blockSlotPlan)
val capturePlan = CapturePlan(blockSlotPlan)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos)
val wrapped = wrapBytecode(stmt)
return wrapped.also {
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
// Record last parsed block range and notify Mini-AST sink
val range = MiniRange(startPos, t1.pos)
lastParsedBlockRange = range
miniSink?.onBlock(MiniBlock(range))
resolutionSink?.exitScope(t1.pos)
}
}
private suspend fun parseLoopBlock(): Statement {
val startPos = cc.currentPos()
val t = cc.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null)
val blockSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(blockSlotPlan)
val capturePlan = CapturePlan(blockSlotPlan)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos)
val wrapped = wrapBytecode(stmt)
return wrapped.also {
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
val range = MiniRange(startPos, t1.pos)
lastParsedBlockRange = range
miniSink?.onBlock(MiniBlock(range))
resolutionSink?.exitScope(t1.pos)
}
}
private suspend fun parseLoopBlockWithPlan(): Pair<Statement, SlotPlan> {
val startPos = cc.currentPos()
val t = cc.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null)
val blockSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
slotPlanStack.add(blockSlotPlan)
val capturePlan = CapturePlan(blockSlotPlan)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, capturePlan.captures.toList(), startPos)
val wrapped = wrapBytecode(stmt)
val t1 = cc.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
val range = MiniRange(startPos, t1.pos)
lastParsedBlockRange = range
miniSink?.onBlock(MiniBlock(range))
resolutionSink?.exitScope(t1.pos)
return wrapped to blockSlotPlan
}
private suspend fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isStatic: Boolean = false,
isExtern: Boolean = false,
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
val nextToken = cc.next()
val start = nextToken.pos
if (nextToken.type == Token.Type.LBRACKET) {
// Destructuring
if (isStatic) throw ScriptError(start, "static destructuring is not supported")
val entries = parseArrayLiteral()
val pattern = ListLiteralRef(entries)
// Register all names in the pattern
pattern.forEachVariableWithPos { name, namePos ->
declareLocalName(name, isMutable)
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
SymbolKind.MEMBER
} else {
SymbolKind.LOCAL
}
resolutionSink?.declareSymbol(name, declKind, isMutable, namePos, isOverride = false)
val declRange = MiniRange(namePos, namePos)
val node = MiniValDecl(
range = declRange,
name = name,
mutable = isMutable,
type = null,
initRange = null,
doc = pendingDeclDoc,
nameStart = namePos,
isExtern = actualExtern,
isStatic = false
)
miniSink?.onValDecl(node)
}
pendingDeclDoc = null
val eqToken = cc.next()
if (eqToken.type != Token.Type.ASSIGN)
throw ScriptError(eqToken.pos, "destructuring declaration must be initialized")
val initialExpression = parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
val names = mutableListOf<String>()
pattern.forEachVariable { names.add(it) }
return DestructuringVarDeclStatement(
pattern,
names,
initialExpression,
isMutable,
visibility,
isTransient,
start
)
}
if (nextToken.type != Token.Type.ID)
throw ScriptError(nextToken.pos, "Expected identifier or [ here")
var name = nextToken.value
var extTypeName: String? = null
var nameStartPos: Pos = nextToken.pos
var receiverMini: MiniTypeRef? = null
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
cc.skipWsTokens()
cc.next() // consume dot
extTypeName = name
resolutionSink?.reference(extTypeName, nextToken.pos)
val receiverEnd = Pos(nextToken.pos.source, nextToken.pos.line, nextToken.pos.column + name.length)
receiverMini = MiniTypeName(
range = MiniRange(nextToken.pos, receiverEnd),
segments = listOf(MiniTypeName.Segment(name, MiniRange(nextToken.pos, receiverEnd))),
nullable = false
)
val nameToken = cc.next()
if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier after dot in extension declaration")
name = nameToken.value
nameStartPos = nameToken.pos
registerExtensionName(extTypeName, name)
}
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
val memberFieldId = if (extTypeName == null) classCtx?.memberFieldIds?.get(name) else null
val memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
// Optional explicit type annotation
cc.skipWsTokens()
val (varTypeDecl, varTypeMini) = if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
parseTypeDeclarationWithMini()
} else {
TypeDecl.TypeAny to null
}
val markBeforeEq = cc.savePos()
cc.skipWsTokens()
val eqToken = cc.next()
var setNull = false
var isProperty = false
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
if (declaringClassNameCaptured != null || extTypeName != null) {
val mark = cc.savePos()
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
// Heuristic: if we see 'get(' or 'set(' or 'private set(' or 'protected set(',
// look ahead for a body.
fun hasAccessorWithBody(): Boolean {
val t = cc.peekNextNonWhitespace()
if (t.isId("get") || t.isId("set")) {
val saved = cc.savePos()
cc.next() // consume get/set
val nextToken = cc.peekNextNonWhitespace()
if (nextToken.type == Token.Type.LPAREN) {
cc.next() // consume (
var depth = 1
while (cc.hasNext() && depth > 0) {
val tt = cc.next()
if (tt.type == Token.Type.LPAREN) depth++
else if (tt.type == Token.Type.RPAREN) depth--
}
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
} else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
cc.restorePos(saved)
} else if (t.isId("private") || t.isId("protected")) {
val saved = cc.savePos()
cc.next() // consume modifier
if (cc.skipWsTokens().isId("set")) {
cc.next() // consume set
val nextToken = cc.peekNextNonWhitespace()
if (nextToken.type == Token.Type.LPAREN) {
cc.next() // consume (
var depth = 1
while (cc.hasNext() && depth > 0) {
val tt = cc.next()
if (tt.type == Token.Type.LPAREN) depth++
else if (tt.type == Token.Type.RPAREN) depth--
}
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
} else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
}
cc.restorePos(saved)
}
return false
}
if (hasAccessorWithBody()) {
isProperty = true
cc.restorePos(markBeforeEq)
// Do not consume eqToken if it's an accessor keyword
} else {
cc.restorePos(mark)
}
}
val effectiveEqToken = if (isProperty) null else eqToken
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name, isMutable)
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
SymbolKind.MEMBER
} else {
SymbolKind.LOCAL
}
resolutionSink?.declareSymbol(name, declKind, isMutable, nameStartPos, isOverride = isOverride)
if (declKind == SymbolKind.MEMBER && extTypeName == null) {
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.declaredMembers?.add(name)
}
val isDelegate = if (isAbstract || actualExtern) {
if (!isProperty && (effectiveEqToken?.type == Token.Type.ASSIGN || effectiveEqToken?.type == Token.Type.BY))
throw ScriptError(effectiveEqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate")
// Abstract or extern variables don't have initializers
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
false
} else if (!isProperty && effectiveEqToken?.type == Token.Type.BY) {
true
} else {
if (!isProperty && effectiveEqToken?.type != Token.Type.ASSIGN) {
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
throw ScriptError(start, "val must be initialized")
else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) {
// lateinit val in class: track it
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.pendingInitializations?.put(name, start)
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
} else {
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
}
}
false
}
val initialExpression = if (setNull || isProperty) null
else parseStatement(true)
?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression")
if (!isStatic && isDelegate) {
markDelegatedSlot(name)
}
// Emit MiniValDecl for this declaration (before execution wiring), attach doc if any
run {
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
val initR = if (setNull || isProperty) null else MiniRange(effectiveEqToken!!.pos, cc.currentPos())
val node = MiniValDecl(
range = declRange,
name = name,
mutable = isMutable,
type = varTypeMini,
initRange = initR,
doc = pendingDeclDoc,
nameStart = nameStartPos,
receiver = receiverMini,
isExtern = actualExtern,
isStatic = isStatic
)
miniSink?.onValDecl(node)
pendingDeclDoc = null
}
if (declaringClassNameCaptured == null &&
extTypeName == null &&
!isStatic &&
!isProperty &&
!actualExtern &&
!isAbstract
) {
if (isDelegate) {
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
return DelegatedVarDeclStatement(
name,
isMutable,
visibility,
initExpr,
isTransient,
start
)
}
val slotPlan = slotPlanStack.lastOrNull()
val slotIndex = slotPlan?.slots?.get(name)?.index
val scopeId = slotPlan?.id
val initObjClass = resolveInitializerObjClass(initialExpression) ?: resolveTypeDeclObjClass(varTypeDecl)
return VarDeclStatement(
name,
isMutable,
visibility,
initialExpression,
isTransient,
slotIndex,
scopeId,
start,
initObjClass
)
}
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
// is missing as for now. Add it to the compiler context?
currentInitScope += object : Statement() {
override val pos: Pos = start
override suspend fun execute(scope: Scope): Obj {
val initValue = initialExpression?.execute(scope)?.byValueCopy() ?: ObjNull
if (isDelegate) {
val accessTypeStr = if (isMutable) "Var" else "Val"
val accessType = scope.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
val finalDelegate = try {
initValue.invokeInstanceMethod(
scope,
"bind",
Arguments(ObjString(name), accessType, scope.thisObj)
)
} catch (e: Exception) {
initValue
}
(scope.thisObj as ObjClass).createClassField(
name,
ObjUnset,
isMutable,
visibility,
null,
start,
isTransient = isTransient,
type = ObjRecord.Type.Delegated
).apply {
delegate = finalDelegate
}
// Also expose in current init scope
scope.addItem(
name,
isMutable,
ObjUnset,
visibility,
null,
ObjRecord.Type.Delegated,
isTransient = isTransient
).apply {
delegate = finalDelegate
}
} else {
(scope.thisObj as ObjClass).createClassField(
name,
initValue,
isMutable,
visibility,
null,
start,
isTransient = isTransient
)
scope.addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
}
return ObjVoid
}
}
return NopStatement
}
// Check for accessors if it is a class member
var getter: Statement? = null
var setter: Statement? = null
var setterVisibility: Visibility? = null
if (declaringClassNameCaptured != null || extTypeName != null) {
while (true) {
val t = cc.skipWsTokens()
if (t.isId("get")) {
val getStart = cc.currentPos()
cc.next() // consume 'get'
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.next() // consume (
cc.requireToken(Token.Type.RPAREN)
}
miniSink?.onEnterFunction(null)
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
inCodeContext(
CodeContext.Function(
"<getter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
parseBlock()
}
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
inCodeContext(
CodeContext.Function(
"<getter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
val expr = parseExpression()
?: throw ScriptError(cc.current().pos, "Expected getter expression")
expr
}
} else {
throw ScriptError(cc.current().pos, "Expected { or = after get()")
}
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("set")) {
val setStart = cc.currentPos()
cc.next() // consume 'set'
var setArgName = "it"
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.next() // consume (
setArgName = cc.requireToken(Token.Type.ID, "Expected setter argument name").value
cc.requireToken(Token.Type.RPAREN)
}
miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
val body = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
parseBlock()
}
object : Statement() {
override val pos: Pos = body.pos
override suspend fun execute(scope: Scope): Obj {
val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
return body.execute(scope)
}
}
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
val expr = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
parseExpression()
?: throw ScriptError(cc.current().pos, "Expected setter expression")
}
val st = expr
object : Statement() {
override val pos: Pos = st.pos
override suspend fun execute(scope: Scope): Obj {
val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
return st.execute(scope)
}
}
} else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
}
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("private") || t.isId("protected")) {
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
val mark = cc.savePos()
cc.next() // consume modifier
if (cc.skipWsTokens().isId("set")) {
cc.next() // consume 'set'
setterVisibility = vis
if (cc.skipWsTokens().type == Token.Type.LPAREN) {
cc.next() // consume '('
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
val finalSetter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
val body = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
parseBlock()
}
object : Statement() {
override val pos: Pos = body.pos
override suspend fun execute(scope: Scope): Obj {
val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
return body.execute(scope)
}
}
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
val st = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = extTypeName != null,
implicitThisTypeName = extTypeName
)
) {
parseExpression() ?: throw ScriptError(
cc.current().pos,
"Expected setter expression"
)
}
object : Statement() {
override val pos: Pos = st.pos
override suspend fun execute(scope: Scope): Obj {
val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
return st.execute(scope)
}
}
} else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
}
setter = finalSetter
miniSink?.onExitFunction(cc.currentPos())
} else {
// private set without body: setter remains null, visibility is restricted
}
} else {
cc.restorePos(mark)
break
}
} else break
}
if (getter != null || setter != null) {
if (isMutable) {
if (getter == null || setter == null) {
throw ScriptError(start, "var property must have both get() and set()")
}
} else {
if (setter != null || setterVisibility != null)
throw ScriptError(start, "val property cannot have a setter or restricted visibility set (name: $name)")
if (getter == null)
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
}
} else if (setterVisibility != null && !isMutable) {
throw ScriptError(start, "val field cannot have restricted visibility set (name: $name)")
}
}
if (extTypeName != null) {
declareLocalName(extensionPropertyGetterName(extTypeName, name), isMutable = false)
if (setter != null) {
declareLocalName(extensionPropertySetterName(extTypeName, name), isMutable = false)
}
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, initExpr, null)
}
return ExtensionPropertyDeclStatement(
extTypeName = extTypeName,
property = prop,
visibility = visibility,
setterVisibility = setterVisibility,
startPos = start
)
}
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.
isProperty = getter != null || setter != null
val declaringClassName = declaringClassNameCaptured
if (declaringClassName == null) {
if (context.containsLocal(name))
throw ScriptError(start, "Variable $name is already defined")
}
// Register the local name so subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name, isMutable)
if (isDelegate) {
val declaringClassName = declaringClassNameCaptured
if (declaringClassName != null) {
val storageName = "$declaringClassName::$name"
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
cls.createField(
name,
ObjUnset,
isMutable,
visibility,
setterVisibility,
start,
isTransient = isTransient,
type = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
methodId = memberMethodId
)
cls.instanceInitializers += object : Statement() {
override val pos: Pos = start
override suspend fun execute(scp: Scope): Obj {
val initValue = initialExpression!!.execute(scp)
val accessTypeStr = if (isMutable) "Var" else "Val"
val accessType = scp.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
val finalDelegate = try {
initValue.invokeInstanceMethod(
scp,
"bind",
Arguments(ObjString(name), accessType, scp.thisObj)
)
} catch (e: Exception) {
initValue
}
scp.addItem(
storageName, isMutable, ObjUnset, visibility, setterVisibility,
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
).apply {
delegate = finalDelegate
}
return ObjVoid
}
}
return ObjVoid
} else {
val initValue = initialExpression!!.execute(context)
val accessTypeStr = if (isMutable) "Var" else "Val"
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
val finalDelegate = try {
initValue.invokeInstanceMethod(
context,
"bind",
Arguments(ObjString(name), accessType, context.thisObj)
)
} catch (e: Exception) {
initValue
}
val rec = context.addItem(
storageName, isMutable, ObjUnset, visibility, setterVisibility,
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
)
rec.delegate = finalDelegate
return finalDelegate
}
} else {
val initValue = initialExpression!!.execute(context)
val accessTypeStr = if (isMutable) "Var" else "Val"
val accessType = context.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
val finalDelegate = try {
initValue.invokeInstanceMethod(context, "bind", Arguments(ObjString(name), accessType, ObjNull))
} catch (e: Exception) {
initValue
}
val rec = context.addItem(
name, isMutable, ObjUnset, visibility, setterVisibility,
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
)
rec.delegate = finalDelegate
return finalDelegate
}
} else if (getter != null || setter != null) {
val declaringClassName = declaringClassNameCaptured!!
val storageName = "$declaringClassName::$name"
val prop = ObjProperty(name, getter, setter)
// If we are in class scope now (defining instance field), defer initialization to instance time
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Register in class members for reflection/MRO/satisfaction checks
if (isProperty) {
cls.addProperty(
name,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start,
prop = prop,
methodId = memberMethodId
)
} else {
cls.createField(
name,
ObjNull,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
type = ObjRecord.Type.Field,
fieldId = memberFieldId
)
}
// Register the property/field initialization thunk
if (!isAbstract) {
cls.instanceInitializers += object : Statement() {
override val pos: Pos = start
override suspend fun execute(scp: Scope): Obj {
scp.addItem(
storageName,
isMutable,
prop,
visibility,
setterVisibility,
recordType = ObjRecord.Type.Property,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
return ObjVoid
}
}
}
return ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
context.addItem(
storageName, isMutable, prop, visibility, setterVisibility,
recordType = ObjRecord.Type.Property,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
)
return prop
}
} else {
val isLateInitVal = !isMutable && initialExpression == null
if (declaringClassName != null && !isStatic) {
val storageName = "$declaringClassName::$name"
// If we are in class scope now (defining instance field), defer initialization to instance time
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Register in class members for reflection/MRO/satisfaction checks
cls.createField(
name,
ObjNull,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start,
isTransient = isTransient,
type = ObjRecord.Type.Field,
fieldId = memberFieldId
)
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
if (!isAbstract) {
val initStmt = object : Statement() {
override val pos: Pos = start
override suspend fun execute(scp: Scope): Obj {
val initValue =
initialExpression?.execute(scp)?.byValueCopy()
?: if (isLateInitVal) ObjUnset else ObjNull
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
scp.addItem(
storageName, isMutable, initValue, visibility, setterVisibility,
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
)
return ObjVoid
}
}
cls.instanceInitializers += initStmt
}
return ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
val initValue =
initialExpression?.execute(context)?.byValueCopy()
?: if (isLateInitVal) ObjUnset else ObjNull
// Preserve mutability of declaration: create record with correct mutability
context.addItem(
storageName, isMutable, initValue, visibility, setterVisibility,
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient
)
return initValue
}
} else {
// Not in class body: regular local/var declaration
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other, isTransient = isTransient)
return initValue
}
}
}
}
}
data class Operator(
val tokenType: Token.Type,
val priority: Int, val arity: Int = 2,
val generate: (Pos, ObjRef, ObjRef) -> ObjRef
) {
// fun isLeftAssociative() = tokenType != Token.Type.OR && tokenType != Token.Type.AND
companion object
}
companion object {
suspend fun compile(source: Source, importManager: ImportProvider): Script {
return Compiler(CompilerContext(parseLyng(source)), importManager).parseScript()
}
suspend fun dryRun(source: Source, importManager: ImportProvider): ResolutionReport {
return CompileTimeResolver.dryRun(source, importManager)
}
/**
* Compile [source] while streaming a Mini-AST into the provided [sink].
* When [sink] is null, behaves like [compile].
*/
suspend fun compileWithMini(
source: Source,
importManager: ImportProvider,
sink: MiniAstSink?
): Script {
return compileWithResolution(source, importManager, sink, null)
}
/** Convenience overload to compile raw [code] with a Mini-AST [sink]. */
suspend fun compileWithMini(code: String, sink: MiniAstSink?): Script =
compileWithMini(Source("<eval>", code), Script.defaultImportManager, sink)
suspend fun compileWithResolution(
source: Source,
importManager: ImportProvider,
miniSink: MiniAstSink? = null,
resolutionSink: ResolutionSink? = null,
useBytecodeStatements: Boolean = true,
strictSlotRefs: Boolean = true,
allowUnresolvedRefs: Boolean = false,
seedScope: Scope? = null
): Script {
return Compiler(
CompilerContext(parseLyng(source)),
importManager,
Settings(
miniAstSink = miniSink,
resolutionSink = resolutionSink,
useBytecodeStatements = useBytecodeStatements,
strictSlotRefs = strictSlotRefs,
allowUnresolvedRefs = allowUnresolvedRefs,
seedScope = seedScope
)
).parseScript()
}
private var lastPriority = 0
// Helpers for conservative constant folding (literal-only). Only pure, side-effect-free ops.
private fun constOf(r: ObjRef): Obj? = (r as? ConstRef)?.constValue
private fun foldBinary(op: BinOp, aRef: ObjRef, bRef: ObjRef): Obj? {
val a = constOf(aRef) ?: return null
val b = constOf(bRef) ?: return null
return when (op) {
// Boolean logic
BinOp.OR -> if (a is ObjBool && b is ObjBool) if (a.value || b.value) ObjTrue else ObjFalse else null
BinOp.AND -> if (a is ObjBool && b is ObjBool) if (a.value && b.value) ObjTrue else ObjFalse else null
// Equality and comparisons for ints/strings/chars
BinOp.EQ -> when {
a is ObjInt && b is ObjInt -> if (a.value == b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value == b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value == b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.NEQ -> when {
a is ObjInt && b is ObjInt -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value != b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value != b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LT -> when {
a is ObjInt && b is ObjInt -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value < b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value < b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.LTE -> when {
a is ObjInt && b is ObjInt -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value <= b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value <= b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GT -> when {
a is ObjInt && b is ObjInt -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value > b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value > b.value) ObjTrue else ObjFalse
else -> null
}
BinOp.GTE -> when {
a is ObjInt && b is ObjInt -> if (a.value >= b.value) ObjTrue else ObjFalse
a is ObjString && b is ObjString -> if (a.value >= b.value) ObjTrue else ObjFalse
a is ObjChar && b is ObjChar -> if (a.value >= b.value) ObjTrue else ObjFalse
else -> null
}
// Arithmetic for ints only (keep semantics simple at compile time)
BinOp.PLUS -> when {
a is ObjInt && b is ObjInt -> ObjInt.of(a.value + b.value)
a is ObjString && b is ObjString -> ObjString(a.value + b.value)
else -> null
}
BinOp.MINUS -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value - b.value) else null
BinOp.STAR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value * b.value) else null
BinOp.SLASH -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt.of(a.value / b.value) else null
BinOp.PERCENT -> if (a is ObjInt && b is ObjInt && b.value != 0L) ObjInt.of(a.value % b.value) else null
// Bitwise for ints
BinOp.BAND -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value and b.value) else null
BinOp.BXOR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value xor b.value) else null
BinOp.BOR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value or b.value) else null
BinOp.SHL -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value shl (b.value.toInt() and 63)) else null
BinOp.SHR -> if (a is ObjInt && b is ObjInt) ObjInt.of(a.value shr (b.value.toInt() and 63)) else null
// Non-folded / side-effecting or type-dependent ops
BinOp.EQARROW, BinOp.REF_EQ, BinOp.REF_NEQ, BinOp.MATCH, BinOp.NOTMATCH,
BinOp.IN, BinOp.NOTIN, BinOp.IS, BinOp.NOTIS, BinOp.SHUTTLE -> null
}
}
private fun foldUnary(op: UnaryOp, aRef: ObjRef): Obj? {
val a = constOf(aRef) ?: return null
return when (op) {
UnaryOp.NOT -> if (a is ObjBool) if (!a.value) ObjTrue else ObjFalse else null
UnaryOp.NEGATE -> when (a) {
is ObjInt -> ObjInt.of(-a.value)
is ObjReal -> ObjReal.of(-a.value)
else -> null
}
UnaryOp.BITNOT -> if (a is ObjInt) ObjInt.of(a.value.inv()) else null
}
}
val allOps = listOf(
// assignments, lowest priority
Operator(Token.Type.ASSIGN, lastPriority) { pos, a, b ->
AssignRef(a, b, pos)
},
Operator(Token.Type.PLUSASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.PLUS, a, b, pos)
},
Operator(Token.Type.MINUSASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.MINUS, a, b, pos)
},
Operator(Token.Type.STARASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.STAR, a, b, pos)
},
Operator(Token.Type.SLASHASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.SLASH, a, b, pos)
},
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.PERCENT, a, b, pos)
},
Operator(Token.Type.IFNULLASSIGN, lastPriority) { pos, a, b ->
AssignIfNullRef(a, b, pos)
},
// logical 1
Operator(Token.Type.OR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.OR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
LogicalOrRef(a, b)
},
// logical 2
Operator(Token.Type.AND, ++lastPriority) { _, a, b ->
LogicalAndRef(a, b)
},
// bitwise or/xor/and (tighter than &&, looser than equality)
Operator(Token.Type.BITOR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BOR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BOR, a, b)
},
Operator(Token.Type.BITXOR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BXOR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BXOR, a, b)
},
Operator(Token.Type.BITAND, ++lastPriority) { _, a, b ->
foldBinary(BinOp.BAND, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.BAND, a, b)
},
// equality/not equality and related
Operator(Token.Type.EQARROW, ++lastPriority) { _, a, b ->
BinaryOpRef(BinOp.EQARROW, a, b)
},
Operator(Token.Type.EQ, ++lastPriority) { _, a, b ->
foldBinary(BinOp.EQ, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.EQ, a, b)
},
Operator(Token.Type.NEQ, lastPriority) { _, a, b ->
foldBinary(BinOp.NEQ, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.NEQ, a, b)
},
Operator(Token.Type.REF_EQ, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.REF_EQ, a, b)
},
Operator(Token.Type.REF_NEQ, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.REF_NEQ, a, b)
},
Operator(Token.Type.MATCH, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.MATCH, a, b)
},
Operator(Token.Type.NOTMATCH, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.NOTMATCH, a, b)
},
// relational <=,...
Operator(Token.Type.LTE, ++lastPriority) { _, a, b ->
foldBinary(BinOp.LTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.LTE, a, b)
},
Operator(Token.Type.LT, lastPriority) { _, a, b ->
foldBinary(BinOp.LT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.LT, a, b)
},
Operator(Token.Type.GTE, lastPriority) { _, a, b ->
foldBinary(BinOp.GTE, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.GTE, a, b)
},
Operator(Token.Type.GT, lastPriority) { _, a, b ->
foldBinary(BinOp.GT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.GT, a, b)
},
// in, is:
Operator(Token.Type.IN, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.IN, a, b)
},
Operator(Token.Type.NOTIN, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.NOTIN, a, b)
},
Operator(Token.Type.IS, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.IS, a, b)
},
Operator(Token.Type.NOTIS, lastPriority) { _, a, b ->
BinaryOpRef(BinOp.NOTIS, a, b)
},
// casts: as / as?
Operator(Token.Type.AS, lastPriority) { pos, a, b ->
CastRef(a, b, false, pos)
},
Operator(Token.Type.ASNULL, lastPriority) { pos, a, b ->
CastRef(a, b, true, pos)
},
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _, a, b ->
ElvisRef(a, b)
},
// shuttle <=>
Operator(Token.Type.SHUTTLE, ++lastPriority) { _, a, b ->
BinaryOpRef(BinOp.SHUTTLE, a, b)
},
// shifts (tighter than shuttle, looser than +/-)
Operator(Token.Type.SHL, ++lastPriority) { _, a, b ->
foldBinary(BinOp.SHL, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SHL, a, b)
},
Operator(Token.Type.SHR, lastPriority) { _, a, b ->
foldBinary(BinOp.SHR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SHR, a, b)
},
// arithmetic
Operator(Token.Type.PLUS, ++lastPriority) { _, a, b ->
foldBinary(BinOp.PLUS, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.PLUS, a, b)
},
Operator(Token.Type.MINUS, lastPriority) { _, a, b ->
foldBinary(BinOp.MINUS, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.MINUS, a, b)
},
Operator(Token.Type.STAR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.STAR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.STAR, a, b)
},
Operator(Token.Type.SLASH, lastPriority) { _, a, b ->
foldBinary(BinOp.SLASH, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.SLASH, a, b)
},
Operator(Token.Type.PERCENT, lastPriority) { _, a, b ->
foldBinary(BinOp.PERCENT, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
BinaryOpRef(BinOp.PERCENT, a, b)
},
)
// private val assigner = allOps.first { it.tokenType == Token.Type.ASSIGN }
//
// fun performAssignment(context: Context, left: Accessor, right: Accessor) {
// assigner.generate(context.pos, left, right)
// }
// Compute levels from the actual operator table rather than relying on
// the mutable construction counter. This prevents accidental inflation
// of precedence depth that could lead to deep recursive descent and
// StackOverflowError during parsing.
val lastLevel = (allOps.maxOf { it.priority }) + 1
val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->
allOps.filter { it.priority == l }.associateBy { it.tokenType }
}
suspend fun compile(code: String): Script = compile(Source("<eval>", code), Script.defaultImportManager)
/**
* The keywords that stop processing of expression term
*/
val stopKeywords =
setOf(
"break", "continue", "return", "if", "when", "do", "while", "for", "class",
"private", "protected", "val", "var", "fun", "fn", "static", "init", "enum"
)
}
}
suspend fun eval(code: String) = compile(code).execute()
suspend fun evalNamed(name: String, code: String, importManager: ImportManager = Script.defaultImportManager) =
compile(Source(name,code), importManager).execute()