9121 lines
407 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.*
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.*
/**
* 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 localShadowedNamesStack = mutableListOf<MutableSet<String>>()
private val currentLocalNames: MutableSet<String>?
get() = localNamesStack.lastOrNull()
private val currentShadowedLocalNames: MutableSet<String>?
get() = localShadowedNamesStack.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
private val genericFunctionDeclsStack = mutableListOf<MutableMap<String, GenericFunctionDecl>>(mutableMapOf())
// 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 data class GenericFunctionDecl(
val typeParams: List<TypeDecl.TypeParam>,
val params: List<ArgsDeclaration.Item>,
val pos: Pos
)
private fun pushGenericFunctionScope() {
genericFunctionDeclsStack.add(mutableMapOf())
}
private fun popGenericFunctionScope() {
genericFunctionDeclsStack.removeLast()
}
private fun currentGenericFunctionDecls(): MutableMap<String, GenericFunctionDecl> {
return genericFunctionDeclsStack.last()
}
private fun lookupGenericFunctionDecl(name: String): GenericFunctionDecl? {
for (i in genericFunctionDeclsStack.indices.reversed()) {
genericFunctionDeclsStack[i][name]?.let { return it }
}
return null
}
private inline fun <T> withLocalNames(names: Set<String>, block: () -> T): T {
localNamesStack.add(names.toMutableSet())
localShadowedNamesStack.add(mutableSetOf())
return try {
block()
} finally {
localShadowedNamesStack.removeLast()
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) {
currentShadowedLocalNames?.add(name)
}
if (added) {
scopeSeedNames.remove(name)
}
if (added && localDeclCountStack.isNotEmpty()) {
localDeclCountStack[localDeclCountStack.lastIndex] = currentLocalDeclCount + 1
}
capturePlanStack.lastOrNull()?.let { plan ->
if (plan.captureMap.remove(name) != null) {
plan.captureOwners.remove(name)
plan.captures.removeAll { it.name == name }
}
}
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
if (!seedingSlotPlan && plan == moduleSlotPlan()) {
moduleDeclaredNames.add(name)
}
}
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 declareSlotNameAt(
plan: SlotPlan,
name: String,
slotIndex: Int,
isMutable: Boolean,
isDelegated: Boolean
) {
if (plan.slots.containsKey(name)) return
plan.slots[name] = SlotEntry(slotIndex, isMutable, isDelegated)
if (slotIndex >= plan.nextIndex) {
plan.nextIndex = slotIndex + 1
}
}
private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull()
private val slotTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val nameObjClass: MutableMap<String, ObjClass> = mutableMapOf()
private val scopeSeedNames: MutableSet<String> = mutableSetOf()
private val slotTypeDeclByScopeId: MutableMap<Int, MutableMap<Int, TypeDecl>> = mutableMapOf()
private val nameTypeDecl: MutableMap<String, TypeDecl> = mutableMapOf()
private data class TypeAliasDecl(
val name: String,
val typeParams: List<TypeDecl.TypeParam>,
val body: TypeDecl,
val pos: Pos
)
private val typeAliases: MutableMap<String, TypeAliasDecl> = mutableMapOf()
private val methodReturnTypeDeclByRef: MutableMap<ObjRef, TypeDecl> = mutableMapOf()
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val lambdaReturnTypeByRef: MutableMap<ObjRef, ObjClass> = mutableMapOf()
private val lambdaCaptureEntriesByRef: MutableMap<ValueFnRef, List<net.sergeych.lyng.bytecode.LambdaCaptureEntry>> =
mutableMapOf()
private val classFieldTypesByName: MutableMap<String, MutableMap<String, ObjClass>> = mutableMapOf()
private val classScopeMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
private val classScopeCallableMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
private val encodedPayloadTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val objectDeclNames: MutableSet<String> = mutableSetOf()
private val externCallableNames: MutableSet<String> = mutableSetOf()
private val moduleDeclaredNames: MutableSet<String> = mutableSetOf()
private var seedingSlotPlan: Boolean = false
private fun moduleForcedLocalSlotInfo(): Map<String, ForcedLocalSlotInfo> {
val plan = moduleSlotPlan() ?: return emptyMap()
if (plan.slots.isEmpty()) return emptyMap()
val result = LinkedHashMap<String, ForcedLocalSlotInfo>(plan.slots.size)
for ((name, entry) in plan.slots) {
result[name] = ForcedLocalSlotInfo(
index = entry.index,
isMutable = entry.isMutable,
isDelegated = entry.isDelegated
)
}
return result
}
private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) {
val plan = moduleSlotPlan() ?: return
seedingSlotPlan = true
try {
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
val instance = record.value as? ObjInstance
if (instance != null && nameObjClass[name] == null) {
nameObjClass[name] = instance.objClass
}
}
for ((cls, map) in current.extensions) {
for ((name, record) in map) {
if (!record.visibility.isPublic) continue
when (record.type) {
ObjRecord.Type.Property -> {
val getterName = extensionPropertyGetterName(cls.className, name)
if (!plan.slots.containsKey(getterName)) {
declareSlotNameIn(
plan,
getterName,
isMutable = false,
isDelegated = false
)
scopeSeedNames.add(getterName)
}
val prop = record.value as? ObjProperty
if (prop?.setter != null) {
val setterName = extensionPropertySetterName(cls.className, name)
if (!plan.slots.containsKey(setterName)) {
declareSlotNameIn(
plan,
setterName,
isMutable = false,
isDelegated = false
)
scopeSeedNames.add(setterName)
}
}
}
else -> {
val callableName = extensionCallableName(cls.className, name)
if (!plan.slots.containsKey(callableName)) {
declareSlotNameIn(
plan,
callableName,
isMutable = false,
isDelegated = false
)
scopeSeedNames.add(callableName)
}
}
}
}
}
for ((name, slotIndex) in current.slotNameToIndexSnapshot()) {
val record = current.getSlotRecord(slotIndex)
if (!record.visibility.isPublic) continue
if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name)
}
if (!includeParents) return
current = current.parent
}
} finally {
seedingSlotPlan = false
}
}
private fun seedSlotPlanFromSeedScope(scope: Scope) {
val plan = moduleSlotPlan() ?: return
for ((name, slotIndex) in scope.slotNameToIndexSnapshot()) {
val record = scope.getSlotRecord(slotIndex)
declareSlotNameAt(
plan,
name,
slotIndex,
record.isMutable,
record.type == ObjRecord.Type.Delegated
)
scopeSeedNames.add(name)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
if (record.typeDecl != null) {
slotTypeDeclByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = record.typeDecl
}
}
}
private fun predeclareTopLevelSymbols() {
val plan = moduleSlotPlan() ?: return
val saved = cc.savePos()
var depth = 0
var parenDepth = 0
var bracketDepth = 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.LPAREN -> parenDepth++
Token.Type.RPAREN -> if (parenDepth > 0) parenDepth--
Token.Type.LBRACKET -> bracketDepth++
Token.Type.RBRACKET -> if (bracketDepth > 0) bracketDepth--
Token.Type.ID -> if (depth == 0) {
if (parenDepth > 0 || bracketDepth > 0) continue
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)
scopeSeedNames.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) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
scopeSeedNames.add(nameToken.value)
}
}
}
}
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
}
}
}
}
}
else -> {}
}
}
} finally {
cc.restorePos(saved)
}
}
private fun predeclareClassScopeMembers(
className: String,
target: MutableSet<String>,
callableTarget: MutableSet<String>
) {
val saved = cc.savePos()
var depth = 0
val modifiers = setOf(
"public", "private", "protected", "internal",
"override", "abstract", "extern", "static", "transient", "open", "closed"
)
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 sawStatic = false
while (t.type == Token.Type.ID && t.value in modifiers) {
if (t.value == "static") sawStatic = true
t = nextNonWs()
}
when (t.value) {
"class" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
callableTarget.add(nameToken.value)
registerClassScopeMember(className, nameToken.value)
registerClassScopeCallableMember(className, nameToken.value)
}
}
"object" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
registerClassScopeMember(className, 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)
callableTarget.add(nameToken.value)
registerClassScopeMember(className, nameToken.value)
registerClassScopeCallableMember(className, nameToken.value)
}
}
"type" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
registerClassScopeMember(className, nameToken.value)
}
}
"fun", "fn", "val", "var" -> {
if (sawStatic) {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
target.add(nameToken.value)
registerClassScopeMember(className, nameToken.value)
}
}
}
}
}
else -> {}
}
}
} finally {
cc.restorePos(saved)
}
}
private fun registerClassScopeMember(className: String, name: String) {
classScopeMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name)
}
private fun registerClassScopeCallableMember(className: String, name: String) {
classScopeCallableMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name)
}
private fun isClassScopeCallableMember(className: String, name: String): Boolean {
return classScopeCallableMembersByClassName[className]?.contains(name) == true
}
private fun registerClassScopeFieldType(ownerClassName: String?, memberName: String, memberClassName: String) {
if (ownerClassName == null) return
val memberClass = resolveClassByName(memberClassName) ?: return
classFieldTypesByName.getOrPut(ownerClassName) { mutableMapOf() }[memberName] = memberClass
}
private fun resolveCompileClassInfo(name: String): CompileClassInfo? {
compileClassInfos[name]?.let { return it }
val scopeRec = seedScope?.get(name) ?: importManager.rootScope.get(name)
val clsFromScope = scopeRec?.value as? ObjClass
val clsFromImports = if (clsFromScope == null) {
importedModules.asReversed().firstNotNullOfOrNull { it.scope.get(name)?.value as? ObjClass }
} else {
null
}
val cls = clsFromScope ?: clsFromImports ?: resolveClassByName(name) ?: return null
val fieldIds = cls.instanceFieldIdMap()
val methodIds = cls.instanceMethodIdMap(includeAbstract = true)
val baseNames = cls.directParents.map { it.className }
val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1
val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1
return CompileClassInfo(name, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames)
}
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[name]
if (prev == null) fieldIds[name] = id
else if (prev != id) fieldConflicts.add(name)
if (id > maxFieldId) maxFieldId = id
}
for ((name, id) in info.methodIds) {
val prev = methodIds[name]
if (prev == null) methodIds[name] = id
else if (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 classCtx = codeContexts.asReversed()
.firstOrNull { it is CodeContext.ClassBody && it.name == qualifier } as? CodeContext.ClassBody
if (classCtx != null) {
val fieldId = classCtx.memberFieldIds[name]
val methodId = classCtx.memberMethodIds[name]
if (fieldId != null || methodId != null) return MemberIds(fieldId, methodId)
}
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 implicitReceiverTypeForMember(name: String): String? {
for (ctx in codeContexts.asReversed()) {
val fn = ctx as? CodeContext.Function ?: continue
if (!fn.implicitThisMembers) continue
val typeName = fn.implicitThisTypeName ?: continue
if (hasImplicitThisMember(name, typeName)) return typeName
}
return null
}
private fun currentEnclosingClassName(): String? {
val ctx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
return ctx?.name
}
private fun currentTypeParams(): Set<String> {
val result = mutableSetOf<String>()
pendingTypeParamStack.lastOrNull()?.let { result.addAll(it) }
for (ctx in codeContexts.asReversed()) {
when (ctx) {
is CodeContext.Function -> result.addAll(ctx.typeParams)
is CodeContext.ClassBody -> result.addAll(ctx.typeParams)
else -> {}
}
}
return result
}
private val pendingTypeParamStack = mutableListOf<Set<String>>()
private fun parseTypeParamList(): List<TypeDecl.TypeParam> {
if (cc.peekNextNonWhitespace().type != Token.Type.LT) return emptyList()
val typeParams = mutableListOf<TypeDecl.TypeParam>()
cc.nextNonWhitespace()
while (true) {
val varianceToken = cc.peekNextNonWhitespace()
val variance = when (varianceToken.value) {
"in" -> {
cc.nextNonWhitespace()
TypeDecl.Variance.In
}
"out" -> {
cc.nextNonWhitespace()
TypeDecl.Variance.Out
}
else -> TypeDecl.Variance.Invariant
}
val idTok = cc.requireToken(Token.Type.ID, "type parameter name expected")
var bound: TypeDecl? = null
var defaultType: TypeDecl? = null
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
bound = parseTypeExpressionWithMini().first
}
if (cc.skipTokenOfType(Token.Type.ASSIGN, isOptional = true)) {
defaultType = parseTypeExpressionWithMini().first
}
typeParams.add(TypeDecl.TypeParam(idTok.value, variance, bound, defaultType))
val sep = cc.nextNonWhitespace()
when (sep.type) {
Token.Type.COMMA -> continue
Token.Type.GT -> break
Token.Type.SHR -> {
cc.pushPendingGT()
break
}
else -> sep.raiseSyntax("expected ',' or '>' in type parameter list")
}
}
return typeParams
}
private fun looksLikeTypeAliasDeclaration(): Boolean {
val saved = cc.savePos()
try {
val nameTok = cc.nextNonWhitespace()
if (nameTok.type != Token.Type.ID) return false
val afterName = cc.savePos()
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
var depth = 1
while (depth > 0) {
val t = cc.nextNonWhitespace()
when (t.type) {
Token.Type.LT -> depth += 1
Token.Type.GT -> depth -= 1
Token.Type.SHR -> {
cc.pushPendingGT()
depth -= 1
}
Token.Type.EOF -> return false
else -> {}
}
}
} else {
cc.restorePos(afterName)
}
cc.skipWsTokens()
return cc.peekNextNonWhitespace().type == Token.Type.ASSIGN
} finally {
cc.restorePos(saved)
}
}
private suspend fun parseTypeAliasDeclaration(): Statement {
val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected")
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
val declaredName = nameToken.value
val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
if (typeAliases.containsKey(qualifiedName)) {
throw ScriptError(nameToken.pos, "type alias $qualifiedName already declared")
}
if (resolveTypeDeclObjClass(TypeDecl.Simple(qualifiedName, false)) != null) {
throw ScriptError(nameToken.pos, "type alias $qualifiedName conflicts with existing class")
}
val typeParams = parseTypeParamList()
val uniqueParams = typeParams.map { it.name }.toSet()
if (uniqueParams.size != typeParams.size) {
throw ScriptError(nameToken.pos, "type alias $qualifiedName has duplicate type parameters")
}
val typeParamNames = uniqueParams
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames)
val (body, bodyMini) = try {
cc.skipWsTokens()
val eq = cc.nextNonWhitespace()
if (eq.type != Token.Type.ASSIGN) {
throw ScriptError(eq.pos, "type alias $qualifiedName expects '='")
}
parseTypeExpressionWithMini()
} finally {
if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast()
}
val alias = TypeAliasDecl(qualifiedName, typeParams, body, nameToken.pos)
typeAliases[qualifiedName] = alias
declareLocalName(declaredName, isMutable = false)
resolutionSink?.declareSymbol(declaredName, SymbolKind.LOCAL, isMutable = false, pos = nameToken.pos)
if (outerClassName != null) {
val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
}
miniSink?.onTypeAliasDecl(
MiniTypeAliasDecl(
range = MiniRange(startPos, cc.currentPos()),
name = declaredName,
typeParams = typeParams.map { it.name },
target = bodyMini,
doc = doc,
nameStart = nameToken.pos
)
)
val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos)
val initStmt = ExpressionStatement(aliasExpr, nameToken.pos)
val slotPlan = slotPlanStack.lastOrNull()
val slotIndex = slotPlan?.slots?.get(declaredName)?.index
val scopeId = slotPlan?.id
return VarDeclStatement(
name = declaredName,
isMutable = false,
visibility = Visibility.Public,
initializer = initStmt,
isTransient = false,
typeDecl = null,
slotIndex = slotIndex,
scopeId = scopeId,
startPos = nameToken.pos,
initializerObjClass = 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 (classCtx?.slotPlanId == slotLoc.scopeId &&
classCtx.declaredMembers.contains(name)
) {
val fieldId = classCtx.memberFieldIds[name]
val methodId = classCtx.memberMethodIds[name]
if (fieldId != null || methodId != null) {
resolutionSink?.referenceMember(name, pos)
return ImplicitThisMemberRef(name, pos, fieldId, methodId, currentImplicitThisTypeName())
}
}
captureLocalRef(name, slotLoc, pos)?.let { ref ->
resolutionSink?.reference(name, pos)
return ref
}
val captureOwner = capturePlanStack.lastOrNull()?.captureOwners?.get(name)
if (useFastLocalRefs &&
slotLoc.depth == 0 &&
captureOwner == null &&
currentLocalNames?.contains(name) == true &&
currentShadowedLocalNames?.contains(name) != true &&
!slotLoc.isDelegated
) {
resolutionSink?.reference(name, pos)
return FastLocalVarRef(name, pos)
}
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 moduleLoc = if (slotPlanStack.size == 1) lookupSlotLocation(name, includeModule = true) else null
if (moduleLoc != null) {
val moduleDeclaredNames = localNamesStack.firstOrNull()
if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) {
resolveImportBinding(name, pos)?.let { resolved ->
registerImportBinding(name, resolved.binding, pos)
}
}
val ref = LocalSlotRef(
name,
moduleLoc.slot,
moduleLoc.scopeId,
moduleLoc.isMutable,
moduleLoc.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 implicitTypeFromFunc = implicitReceiverTypeForMember(name)
val hasImplicitClassMember = classCtx != null && hasImplicitThisMember(name, classCtx.name)
if (implicitTypeFromFunc == null && !hasImplicitClassMember) {
val modulePlan = moduleSlotPlan()
val moduleEntry = modulePlan?.slots?.get(name)
if (moduleEntry != null) {
val moduleDeclaredNames = localNamesStack.firstOrNull()
if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) {
resolveImportBinding(name, pos)?.let { resolved ->
registerImportBinding(name, resolved.binding, pos)
}
}
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 = if (capturePlanStack.isEmpty() && moduleLoc.depth > 0) {
LocalSlotRef(
name,
moduleLoc.slot,
moduleLoc.scopeId,
moduleLoc.isMutable,
moduleLoc.isDelegated,
pos,
strictSlotRefs,
captureOwnerScopeId = moduleLoc.scopeId,
captureOwnerSlot = moduleLoc.slot
)
} else {
LocalSlotRef(
name,
moduleLoc.slot,
moduleLoc.scopeId,
moduleLoc.isMutable,
moduleLoc.isDelegated,
pos,
strictSlotRefs
)
}
resolutionSink?.reference(name, pos)
return ref
}
resolveImportBinding(name, pos)?.let { resolved ->
val sourceRecord = resolved.record
if (modulePlan != null && !modulePlan.slots.containsKey(name)) {
val seedSlotIndex = if (resolved.binding.source is ImportBindingSource.Seed) {
seedScope?.getSlotIndexOf(name)
} else {
null
}
val seedSlotFree = seedSlotIndex != null &&
modulePlan.slots.values.none { it.index == seedSlotIndex }
if (seedSlotFree) {
declareSlotNameAt(
modulePlan,
name,
seedSlotIndex,
sourceRecord.isMutable,
sourceRecord.type == ObjRecord.Type.Delegated
)
} else {
declareSlotNameIn(
modulePlan,
name,
sourceRecord.isMutable,
sourceRecord.type == ObjRecord.Type.Delegated
)
}
}
registerImportBinding(name, resolved.binding, pos)
val slot = lookupSlotLocation(name)
if (slot != null) {
captureLocalRef(name, slot, pos)?.let { ref ->
resolutionSink?.reference(name, pos)
return ref
}
val ref = if (capturePlanStack.isEmpty() && slot.depth > 0) {
LocalSlotRef(
name,
slot.slot,
slot.scopeId,
slot.isMutable,
slot.isDelegated,
pos,
strictSlotRefs,
captureOwnerScopeId = slot.scopeId,
captureOwnerSlot = slot.slot
)
} else {
LocalSlotRef(
name,
slot.slot,
slot.scopeId,
slot.isMutable,
slot.isDelegated,
pos,
strictSlotRefs
)
}
resolutionSink?.reference(name, pos)
return ref
}
}
}
if (classCtx != null) {
val implicitType = classCtx.name
if (hasImplicitThisMember(name, implicitType)) {
resolutionSink?.referenceMember(name, pos, implicitType)
val ids = resolveImplicitThisMemberIds(name, pos, implicitType)
val preferredType = if (currentImplicitThisTypeName() == null) null else implicitType
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, preferredType)
}
}
val implicitType = implicitTypeFromFunc
if (implicitType != null) {
resolutionSink?.referenceMember(name, pos, implicitType)
val ids = resolveImplicitThisMemberIds(name, pos, implicitType)
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType)
}
if (classCtx != null && classCtx.classScopeMembers.contains(name)) {
resolutionSink?.referenceMember(name, pos, classCtx.name)
return ClassScopeMemberRef(name, pos, classCtx.name)
}
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 compileBytecode: Boolean = true,
val strictSlotRefs: Boolean = true,
val allowUnresolvedRefs: Boolean = false,
val seedScope: Scope? = null,
val useFastLocalRefs: Boolean = false,
)
// 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 compileBytecode: Boolean = settings.compileBytecode
private val seedScope: Scope? = settings.seedScope
private val useFastLocalRefs: Boolean = settings.useFastLocalRefs
private var resolutionScriptDepth = 0
private val resolutionPredeclared = mutableSetOf<String>()
private data class ImportedModule(val scope: ModuleScope, val pos: Pos)
private data class ImportBindingResolution(val binding: ImportBinding, val record: ObjRecord)
private val importedModules = mutableListOf<ImportedModule>()
private val importBindings = mutableMapOf<String, ImportBinding>()
private val enumEntriesByName = mutableMapOf<String, List<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 seedNameObjClassFromScope(scope: Scope) {
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
if (nameObjClass.containsKey(name)) continue
val resolved = when (val raw = record.value) {
is FrameSlotRef -> raw.peekValue()
is RecordSlotRef -> raw.peekValue()
else -> raw
} ?: continue
when (resolved) {
is ObjClass -> nameObjClass[name] = resolved
is ObjInstance -> nameObjClass[name] = resolved.objClass
is ObjDynamic -> nameObjClass[name] = resolved.objClass
}
}
current = current.parent
}
}
private fun seedNameTypeDeclFromScope(scope: Scope) {
var current: Scope? = scope
while (current != null) {
for ((name, record) in current.objects) {
if (!record.visibility.isPublic) continue
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
}
for ((name, slotIndex) in current.slotNameToIndexSnapshot()) {
val record = current.getSlotRecord(slotIndex)
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
}
current = current.parent
}
}
private fun resolveImportBinding(name: String, pos: Pos): ImportBindingResolution? {
val seedRecord = findSeedScopeRecord(name)?.takeIf { it.visibility.isPublic }
val rootRecord = importManager.rootScope.objects[name]?.takeIf { it.visibility.isPublic }
val moduleMatches = LinkedHashMap<String, Pair<ImportedModule, ObjRecord>>()
for (module in importedModules.asReversed()) {
val found = LinkedHashMap<String, Pair<ModuleScope, ObjRecord>>()
collectModuleRecordMatches(module.scope, name, mutableSetOf(), found)
for ((pkg, pair) in found) {
if (!moduleMatches.containsKey(pkg)) {
moduleMatches[pkg] = ImportedModule(pair.first, module.pos) to pair.second
}
}
}
if (seedRecord != null) {
val value = seedRecord.value
if (!nameObjClass.containsKey(name)) {
when (value) {
is ObjClass -> nameObjClass[name] = value
is ObjInstance -> nameObjClass[name] = value.objClass
}
}
return ImportBindingResolution(ImportBinding(name, ImportBindingSource.Seed), seedRecord)
}
if (rootRecord != null) {
val value = rootRecord.value
if (!nameObjClass.containsKey(name)) {
when (value) {
is ObjClass -> nameObjClass[name] = value
is ObjInstance -> nameObjClass[name] = value.objClass
}
}
return ImportBindingResolution(ImportBinding(name, ImportBindingSource.Root), rootRecord)
}
if (moduleMatches.isEmpty()) return null
if (moduleMatches.size > 1) {
val moduleNames = moduleMatches.keys.toList()
throw ScriptError(pos, "symbol $name is ambiguous between imports: ${moduleNames.joinToString(", ")}")
}
val (module, record) = moduleMatches.values.first()
val binding = ImportBinding(name, ImportBindingSource.Module(module.scope.packageName, module.pos))
val value = record.value
if (!nameObjClass.containsKey(name)) {
when (value) {
is ObjClass -> nameObjClass[name] = value
is ObjInstance -> nameObjClass[name] = value.objClass
}
}
return ImportBindingResolution(binding, record)
}
private fun collectModuleRecordMatches(
scope: ModuleScope,
name: String,
visited: MutableSet<String>,
out: MutableMap<String, Pair<ModuleScope, ObjRecord>>
) {
if (!visited.add(scope.packageName)) return
val record = scope.objects[name]
if (record != null && record.visibility.isPublic) {
if (!out.containsKey(scope.packageName)) {
out[scope.packageName] = scope to record
}
}
for (child in scope.importedModules) {
collectModuleRecordMatches(child, name, visited, out)
}
}
private fun registerImportBinding(name: String, binding: ImportBinding, pos: Pos) {
val existing = importBindings[name] ?: run {
importBindings[name] = binding
scopeSeedNames.add(name)
return
}
if (!sameImportBinding(existing, binding)) {
throw ScriptError(pos, "symbol $name resolves to multiple imports")
}
}
private fun sameImportBinding(left: ImportBinding, right: ImportBinding): Boolean {
if (left.symbol != right.symbol) return false
val leftSrc = left.source
val rightSrc = right.source
return when (leftSrc) {
is ImportBindingSource.Module -> {
rightSrc is ImportBindingSource.Module && leftSrc.name == rightSrc.name
}
ImportBindingSource.Root -> rightSrc is ImportBindingSource.Root
ImportBindingSource.Seed -> rightSrc is ImportBindingSource.Seed
}
}
private fun findSeedScopeRecord(name: String): ObjRecord? {
var current = seedScope
var hops = 0
while (current != null && hops++ < 1024) {
current.objects[name]?.let { return it }
current = current.parent
}
return null
}
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 fun looksLikeExtensionReceiver(): Boolean {
val saved = cc.savePos()
try {
if (cc.peekNextNonWhitespace().type != Token.Type.ID) return false
cc.nextNonWhitespace()
// consume qualified name segments
while (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
val dotPos = cc.savePos()
cc.nextNonWhitespace()
if (cc.peekNextNonWhitespace().type != Token.Type.ID) {
cc.restorePos(dotPos)
break
}
cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace()
if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION &&
afterSegment.type != Token.Type.IFNULLASSIGN
) {
cc.restorePos(dotPos)
break
}
}
// optional generic arguments
if (cc.peekNextNonWhitespace().type == Token.Type.LT) {
var depth = 0
while (true) {
val tok = cc.nextNonWhitespace()
when (tok.type) {
Token.Type.LT -> depth += 1
Token.Type.GT -> {
depth -= 1
if (depth <= 0) break
}
Token.Type.SHR -> {
depth -= 2
if (depth <= 0) break
}
Token.Type.EOF -> return false
else -> {}
}
}
}
// nullable suffix
if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION ||
cc.peekNextNonWhitespace().type == Token.Type.IFNULLASSIGN
) {
cc.nextNonWhitespace()
}
val dotTok = cc.peekNextNonWhitespace()
if (dotTok.type != Token.Type.DOT) return false
val savedDot = cc.savePos()
cc.nextNonWhitespace()
val nameTok = cc.peekNextNonWhitespace()
cc.restorePos(savedDot)
return nameTok.type == Token.Type.ID
} finally {
cc.restorePos(saved)
}
}
private fun shouldImplicitTypeVar(name: String, explicit: Set<String>): Boolean {
if (explicit.contains(name)) return true
if (name.contains('.')) return false
if (resolveClassByName(name) != null) return false
if (resolveTypeDeclObjClass(TypeDecl.Simple(name, false)) != null) return false
return name.length == 1 || name in setOf("T", "R", "E", "K", "V")
}
private fun normalizeReceiverTypeDecl(
receiver: TypeDecl?,
explicitTypeParams: Set<String>
): Pair<TypeDecl?, Set<String>> {
if (receiver == null) return null to emptySet()
val implicit = mutableSetOf<String>()
fun transform(decl: TypeDecl): TypeDecl = when (decl) {
is TypeDecl.Simple -> {
if (shouldImplicitTypeVar(decl.name, explicitTypeParams)) {
if (!explicitTypeParams.contains(decl.name)) implicit += decl.name
TypeDecl.TypeVar(decl.name, decl.isNullable)
} else decl
}
is TypeDecl.TypeVar -> {
if (!explicitTypeParams.contains(decl.name)) implicit += decl.name
decl
}
is TypeDecl.Generic -> TypeDecl.Generic(
decl.name,
decl.args.map { transform(it) },
decl.isNullable
)
is TypeDecl.Function -> TypeDecl.Function(
receiver = decl.receiver?.let { transform(it) },
params = decl.params.map { transform(it) },
returnType = transform(decl.returnType),
nullable = decl.isNullable
)
is TypeDecl.Ellipsis -> TypeDecl.Ellipsis(transform(decl.elementType), decl.isNullable)
is TypeDecl.Union -> TypeDecl.Union(decl.options.map { transform(it) }, decl.isNullable)
is TypeDecl.Intersection -> TypeDecl.Intersection(decl.options.map { transform(it) }, decl.isNullable)
else -> decl
}
return transform(receiver) to implicit
}
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,
val baseNames: List<String>
)
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)
pushGenericFunctionScope()
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 {
popGenericFunctionScope()
codeContexts.removeLast()
}
}
private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
val topLevelSink = if (resolutionScriptDepth == 0) resolutionSink else null
val atTopLevel = topLevelSink != null
if (atTopLevel) {
topLevelSink.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++))
seedScope?.let { scope ->
if (scope !is ModuleScope) {
seedSlotPlanFromSeedScope(scope)
}
}
val plan = slotPlanStack.last()
if (!plan.slots.containsKey("__PACKAGE__")) {
declareSlotNameIn(plan, "__PACKAGE__", isMutable = false, isDelegated = false)
}
if (!plan.slots.containsKey("$~")) {
declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false)
}
seedScope?.let { seedNameObjClassFromScope(it) }
seedScope?.let { seedNameTypeDeclFromScope(it) }
seedNameObjClassFromScope(importManager.rootScope)
if (shouldSeedDefaultStdlib()) {
val stdlib = importManager.prepareImport(start, "lyng.stdlib", null)
seedResolutionFromScope(stdlib, start)
seedNameObjClassFromScope(stdlib)
importedModules.add(ImportedModule(stdlib, start))
}
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
}
}
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)
importedModules.add(ImportedModule(module, pos))
seedResolutionFromScope(module, pos)
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 forcedLocalInfo = if (useScopeSlots) emptyMap() else moduleForcedLocalSlotInfo()
val forcedLocalScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id
val allowedScopeNames = if (useScopeSlots) modulePlan.keys else null
val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null
val moduleScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id
val isModuleScript = codeContexts.lastOrNull() is CodeContext.Module && resolutionScriptDepth == 1
val wrapScriptBytecode = compileBytecode && isModuleScript
val (finalStatements, moduleBytecode) = if (wrapScriptBytecode) {
val unwrapped = statements.map { unwrapBytecodeDeep(it) }
val block = InlineBlockStatement(unwrapped, start)
val bytecodeStmt = BytecodeStatement.wrap(
block,
"<script>",
allowLocalSlots = true,
allowedScopeNames = allowedScopeNames,
scopeSlotNameSet = scopeSlotNameSet,
moduleScopeId = moduleScopeId,
forcedLocalSlotInfo = forcedLocalInfo,
forcedLocalScopeId = forcedLocalScopeId,
slotTypeByScopeId = slotTypeByScopeId,
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownObjectNames = objectDeclNames,
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) as BytecodeStatement
unwrapped to bytecodeStmt.bytecodeFunction()
} else {
statements to null
}
val moduleRefs = importedModules.map { ImportBindingSource.Module(it.scope.packageName, it.pos) }
Script(
start,
finalStatements,
modulePlan,
moduleDeclaredNames.toSet(),
importBindings.toMap(),
moduleRefs,
moduleBytecode
)
}.also {
// Best-effort script end notification (use current position)
miniSink?.onScriptEnd(
cc.currentPos(),
MiniScript(MiniRange(start, cc.currentPos()))
)
}
} finally {
resolutionScriptDepth--
if (atTopLevel) {
topLevelSink.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 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 val useScopeSlots: Boolean = seedScope != null && seedScope !is ModuleScope
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 wrapperNames = listOf(
extensionCallableName(baseName, memberName),
extensionPropertyGetterName(baseName, memberName),
extensionPropertySetterName(baseName, memberName)
)
for (wrapperName in wrapperNames) {
val resolved = resolveImportBinding(wrapperName, Pos.builtIn) ?: continue
val plan = moduleSlotPlan()
if (plan != null && !plan.slots.containsKey(wrapperName)) {
declareSlotNameIn(
plan,
wrapperName,
resolved.record.isMutable,
resolved.record.type == ObjRecord.Type.Delegated
)
}
registerImportBinding(wrapperName, resolved.binding, Pos.builtIn)
return true
}
}
return false
}
private fun resolveImplicitThisMemberIds(name: String, pos: Pos, implicitTypeName: String?): MemberIds {
if (implicitTypeName == null) return resolveMemberIds(name, pos, null)
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
if (classCtx != null && classCtx.name == implicitTypeName) {
val fieldId = classCtx.memberFieldIds[name]
val methodId = classCtx.memberMethodIds[name]
if (fieldId != null || methodId != null) {
return MemberIds(fieldId, methodId)
}
}
val info = resolveCompileClassInfo(implicitTypeName)
val fieldId = info?.fieldIds?.get(name)
val methodId = info?.methodIds?.get(name)
if (fieldId == null && methodId == null) {
val cls = resolveClassByName(implicitTypeName)
if (cls != null) {
val fId = cls.instanceFieldIdMap()[name]
val mId = cls.instanceMethodIdMap(includeAbstract = true)[name]
if (fId != null || mId != null) return MemberIds(fId, mId)
}
}
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 fun hasImplicitThisMember(name: String, implicitTypeName: String?): Boolean {
if (implicitTypeName == null) return false
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
if (classCtx != null && classCtx.name == implicitTypeName) {
if (classCtx.memberFieldIds.containsKey(name) || classCtx.memberMethodIds.containsKey(name)) {
return true
}
}
val info = resolveCompileClassInfo(implicitTypeName)
if (info != null && (info.fieldIds.containsKey(name) || info.methodIds.containsKey(name))) return true
val cls = resolveClassByName(implicitTypeName)
if (cls != null) {
if (cls.instanceFieldIdMap().containsKey(name) || cls.instanceMethodIdMap(includeAbstract = true).containsKey(name)) {
return true
}
}
return hasExtensionFor(implicitTypeName, name)
}
private val currentRangeParamNames: Set<String>
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
private val capturePlanStack = mutableListOf<CapturePlan>()
private var lambdaDepth = 0
private data class CapturePlan(
val slotPlan: SlotPlan,
val isFunction: Boolean,
val propagateToParentFunction: Boolean,
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
recordCaptureSlotInto(plan, name, slotLoc)
if (plan.propagateToParentFunction) {
for (i in capturePlanStack.size - 2 downTo 0) {
val parent = capturePlanStack[i]
if (parent.isFunction) {
recordCaptureSlotInto(parent, name, slotLoc)
break
}
}
}
}
private fun recordCaptureSlotInto(plan: CapturePlan, name: String, slotLoc: SlotLocation) {
if (plan.captureMap.containsKey(name)) return
val capture = CaptureSlot(
name = name,
ownerScopeId = slotLoc.scopeId,
ownerSlot = slotLoc.slot,
)
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 functionPlan = capturePlanStack.asReversed().firstOrNull { it.isFunction } ?: return null
val functionIndex = slotPlanStack.indexOfLast { it.id == functionPlan.slotPlan.id }
val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId }
if (functionIndex >= 0 && scopeIndex >= functionIndex) return null
val modulePlan = moduleSlotPlan()
if (scopeSeedNames.contains(name)) {
val isModuleSlot = modulePlan != null && slotLoc.scopeId == modulePlan.id
if (!isModuleSlot || useScopeSlots) 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 knownClassMapForBytecode(): Map<String, ObjClass> {
val result = LinkedHashMap<String, ObjClass>()
fun addScope(scope: Scope?) {
if (scope == null) return
for ((name, rec) in scope.objects) {
val cls = rec.value as? ObjClass
if (cls != null) {
if (!result.containsKey(name)) result[name] = cls
continue
}
val obj = rec.value as? ObjInstance ?: continue
if (!result.containsKey(name)) result[name] = obj.objClass
}
}
addScope(seedScope)
addScope(importManager.rootScope)
for (module in importedModules) {
addScope(module.scope)
}
for (name in compileClassInfos.keys) {
val cls = resolveClassByName(name) ?: continue
if (!result.containsKey(name)) result[name] = cls
}
return result
}
private fun wrapBytecode(stmt: Statement): Statement {
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 (stmt is FunctionDeclStatement ||
stmt is ClassDeclStatement ||
stmt is EnumDeclStatement
) return stmt
val allowLocals = codeContexts.lastOrNull() !is CodeContext.ClassBody
val returnLabels = returnLabelStack.lastOrNull() ?: emptySet()
val modulePlan = moduleSlotPlan()
val allowedScopeNames = if (useScopeSlots) modulePlan?.slots?.keys else null
val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null
val moduleScopeId = modulePlan?.id
return BytecodeStatement.wrap(
stmt,
"stmt@${stmt.pos}",
allowLocalSlots = allowLocals,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames,
scopeSlotNameSet = scopeSlotNameSet,
moduleScopeId = moduleScopeId,
slotTypeByScopeId = slotTypeByScopeId,
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownObjectNames = objectDeclNames,
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
private fun wrapClassBodyBytecode(stmt: Statement, name: String): Statement {
val target = if (stmt is Script) InlineBlockStatement(stmt.statements(), stmt.pos) else stmt
val returnLabels = returnLabelStack.lastOrNull() ?: emptySet()
val modulePlan = moduleSlotPlan()
val allowedScopeNames = if (useScopeSlots) modulePlan?.slots?.keys else null
val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null
val moduleScopeId = modulePlan?.id
return BytecodeStatement.wrap(
target,
name,
allowLocalSlots = true,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames,
scopeSlotNameSet = scopeSlotNameSet,
moduleScopeId = moduleScopeId,
slotTypeByScopeId = slotTypeByScopeId,
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownClassMapForBytecode(),
knownObjectNames = objectDeclNames,
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
private fun wrapInstanceInitBytecode(stmt: Statement, name: String): BytecodeStatement {
val wrapped = wrapFunctionBytecode(stmt, name)
return (wrapped as? BytecodeStatement)
?: throw ScriptError(stmt.pos, "non-bytecode init statement: $name")
}
private fun wrapFunctionBytecode(
stmt: Statement,
name: String,
extraKnownNameObjClass: Map<String, ObjClass> = emptyMap(),
forcedLocalSlots: Map<String, Int> = emptyMap(),
forcedLocalScopeId: Int? = null
): Statement {
val returnLabels = returnLabelStack.lastOrNull() ?: emptySet()
val modulePlan = moduleSlotPlan()
val allowedScopeNames = if (useScopeSlots) modulePlan?.slots?.keys else null
val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null
val moduleScopeId = modulePlan?.id
val globalSlotInfo = if (useScopeSlots) emptyMap() else moduleForcedLocalSlotInfo()
val globalSlotScopeId = if (useScopeSlots) null else moduleScopeId
val knownNames = if (extraKnownNameObjClass.isEmpty()) {
knownClassMapForBytecode()
} else {
val merged = LinkedHashMap<String, ObjClass>()
merged.putAll(knownClassMapForBytecode())
merged.putAll(extraKnownNameObjClass)
merged
}
return BytecodeStatement.wrap(
stmt,
"fn@$name",
allowLocalSlots = true,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames,
scopeSlotNameSet = scopeSlotNameSet,
moduleScopeId = moduleScopeId,
forcedLocalSlots = forcedLocalSlots,
forcedLocalScopeId = forcedLocalScopeId,
globalSlotInfo = globalSlotInfo,
globalSlotScopeId = globalSlotScopeId,
slotTypeByScopeId = slotTypeByScopeId,
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
knownNameObjClass = knownNames,
knownObjectNames = objectDeclNames,
classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
private fun wrapDefaultArgsBytecode(
args: ArgsDeclaration,
forcedLocalSlots: Map<String, Int>,
forcedLocalScopeId: Int
): ArgsDeclaration {
if (!compileBytecode) return args
if (args.params.none { it.defaultValue is Statement }) return args
val updated = args.params.map { param ->
val defaultValue = param.defaultValue
val stmt = defaultValue as? Statement
if (stmt == null) return@map param
val bytecode = when (stmt) {
is BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
if (bytecode != null) {
param
} else {
val wrapped = wrapFunctionBytecode(
stmt,
"argDefault@${param.name}",
forcedLocalSlots = forcedLocalSlots,
forcedLocalScopeId = forcedLocalScopeId
)
param.copy(defaultValue = wrapped)
}
}
return if (updated == args.params) args else args.copy(params = updated)
}
private fun wrapParsedArgsBytecode(
args: List<ParsedArgument>?,
forcedLocalSlots: Map<String, Int> = emptyMap(),
forcedLocalScopeId: Int? = null
): List<ParsedArgument>? {
if (!compileBytecode || args == null || args.isEmpty()) return args
var changed = false
val updated = args.mapIndexed { index, arg ->
val stmt = arg.value as? Statement ?: return@mapIndexed arg
val bytecode = when (stmt) {
is BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
if (bytecode != null) return@mapIndexed arg
val wrapped = wrapFunctionBytecode(
stmt,
"arg@${index}",
forcedLocalSlots = forcedLocalSlots,
forcedLocalScopeId = forcedLocalScopeId
)
changed = true
arg.copy(value = wrapped)
}
return if (changed) updated else args
}
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
}
} || 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 net.sergeych.lyng.obj.TypeDeclRef -> false
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 {
val v = it.value
when (v) {
is Statement -> containsDelegatedRefs(v)
is net.sergeych.lyng.obj.ObjRef -> containsDelegatedRefs(v)
else -> false
}
}
is MethodCallRef -> containsDelegatedRefs(ref.receiver) || ref.args.any {
val v = it.value
when (v) {
is Statement -> containsDelegatedRefs(v)
is net.sergeych.lyng.obj.ObjRef -> containsDelegatedRefs(v)
else -> false
}
}
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.scopeId, 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.typeDecl,
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.loopScopeId,
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 res = if (opToken.type == Token.Type.AS || opToken.type == Token.Type.ASNULL) {
val (typeDecl, _) = parseTypeExpressionWithMini()
val typeRef = typeDeclToTypeRef(typeDecl, opToken.pos)
if (opToken.type == Token.Type.AS) {
CastRef(lvalue!!, typeRef, false, opToken.pos)
} else {
CastRef(lvalue!!, typeRef, true, opToken.pos)
}
} else if (opToken.type == Token.Type.IS || opToken.type == Token.Type.NOTIS) {
val (typeDecl, _) = parseTypeExpressionWithMini()
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, opToken.pos)
if (opToken.type == Token.Type.IS) {
BinaryOpRef(BinOp.IS, lvalue!!, typeRef)
} else {
BinaryOpRef(BinOp.NOTIS, lvalue!!, typeRef)
}
} else {
val rvalue = parseExpressionLevel(level + 1)
?: throw ScriptError(opToken.pos, "Expecting expression")
op.generate(opToken.pos, lvalue!!, rvalue)
}
if (opToken.type == Token.Type.ASSIGN) {
val ctx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
if (ctx != null) {
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()
if (shouldTreatAsClassScopeCall(left, next.value)) {
val parsed = parseArgs(null)
val args = parsed.first
val tailBlock = parsed.second
isCall = true
val receiverClass = resolveReceiverClassForMember(left)
val nestedClass = receiverClass?.let { resolveClassByName("${it.className}.${next.value}") }
if (nestedClass != null) {
val field = FieldRef(left, next.value, isOptional)
operand = CallRef(field, args, tailBlock, isOptional)
} else {
operand = MethodCallRef(left, next.value, args, tailBlock, isOptional)
}
} else {
// instance method call
val receiverType = if (next.value == "apply" || next.value == "run") {
inferReceiverTypeFromRef(left)
} else null
val parsed = parseArgs(receiverType)
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 {
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 -> {
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, tailBlock)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 receiverType = if (next.value == "apply" || next.value == "run") {
inferReceiverTypeFromRef(left)
} else null
val itType = if (next.value == "let" || next.value == "also") {
inferReceiverTypeFromRef(left)
} else null
val lambda = parseLambdaExpression(receiverType, implicitItType = itType)
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 {
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 -> {
val unionCall = buildUnionMethodCall(left, next.value, next.pos, isOptional, args, true)
if (unionCall != null) {
unionCall
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 {
val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
if (unionField != null) {
unionField
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 -> {
val unionField = buildUnionFieldAccess(left, next.value, next.pos, isOptional)
if (unionField != null) {
unionField
} else {
enforceReceiverTypeForMember(left, next.value, next.pos)
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 ||
current.type == Token.Type.STEP
)
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.STEP -> {
val left = operand ?: throw ScriptError(t.pos, "step requires a range")
val rangeRef = when (left) {
is RangeRef -> left
is ConstRef -> {
val range = left.constValue as? ObjRange ?: run {
cc.previous()
return operand
}
val leftRef = range.start?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
val rightRef = range.end?.takeUnless { it.isNull }?.let { ConstRef(it.asReadonly) }
RangeRef(leftRef, rightRef, range.isEndInclusive)
}
else -> {
cc.previous()
return operand
}
}
if (rangeRef.step != null) throw ScriptError(t.pos, "step is already specified for this range")
val stepExpr = parseExpression() ?: throw ScriptError(t.pos, "Expected step expression")
val stepRef = StatementRef(stepExpr)
operand = RangeRef(rangeRef.left, rangeRef.right, rangeRef.isEndInclusive, stepRef)
}
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,
wrapAsExtensionCallable: Boolean = false,
implicitItType: 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)
if (implicitItType != null) {
val cls = resolveClassByName(implicitItType)
?: resolveTypeDeclObjClass(TypeDecl.Simple(implicitItType, false))
val itSlot = paramSlotPlan.slots["it"]?.index
if (cls != null && itSlot != null) {
val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
paramTypeMap[itSlot] = cls
}
}
label?.let { cc.labels.add(it) }
slotPlanStack.add(paramSlotPlan)
val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = lambdaDepth > 0)
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)
}
lambdaDepth += 1
try {
withLocalNames(slotParamNames.toSet()) {
parseBlock(skipLeadingBrace = true)
}
} finally {
lambdaDepth -= 1
}
} 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()
val captureEntries = if (captureSlots.isNotEmpty()) {
captureSlots.map { capture ->
val owner = capturePlan.captureOwners[capture.name]
?: error("Missing capture owner for ${capture.name}")
net.sergeych.lyng.bytecode.LambdaCaptureEntry(
ownerKind = net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL,
ownerScopeId = owner.scopeId,
ownerSlotId = owner.slot,
ownerName = capture.name,
ownerIsMutable = owner.isMutable,
ownerIsDelegated = owner.isDelegated
)
}
} else {
emptyList()
}
val returnClass = inferReturnClassFromStatement(body)
val paramKnownClasses = mutableMapOf<String, ObjClass>()
argsDeclaration?.params?.forEach { param ->
val cls = resolveTypeDeclObjClass(param.type) ?: return@forEach
paramKnownClasses[param.name] = cls
}
val returnLabels = label?.let { setOf(it) } ?: emptySet()
val extraKnownNames = if (compileClassInfos.isEmpty()) {
paramKnownClasses
} else {
val merged = LinkedHashMap<String, ObjClass>()
for (className in compileClassInfos.keys) {
merged[className] = if (objectDeclNames.contains(className)) {
ObjDynamic.type
} else {
ObjClassType
}
}
merged.putAll(paramKnownClasses)
merged
}
val fnStatements = if (compileBytecode) {
returnLabelStack.addLast(returnLabels)
try {
wrapFunctionBytecode(
body,
"<lambda>",
extraKnownNames,
forcedLocalSlots = paramSlotPlanSnapshot,
forcedLocalScopeId = paramSlotPlan.id
)
} finally {
returnLabelStack.removeLast()
}
} else {
body
}
val bytecodeFn = (fnStatements as? BytecodeStatement)?.bytecodeFunction()
val ref = LambdaFnRef(
valueFn = { closureScope ->
val captureRecords = closureScope.captureRecords
val stmt = object : Statement(), BytecodeBodyProvider {
override val pos: Pos = fnStatements.pos
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
override suspend fun execute(scope: Scope): Obj {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also {
it.args = scope.args
}
if (captureSlots.isNotEmpty()) {
if (captureRecords != null) {
context.captureRecords = captureRecords
context.captureNames = captureSlots.map { it.name }
} else {
val resolvedRecords = ArrayList<ObjRecord>(captureSlots.size)
val resolvedNames = ArrayList<String>(captureSlots.size)
for (capture in captureSlots) {
val rec = closureScope.chainLookupIgnoreClosure(
capture.name,
followClosure = true,
caller = context.currentClassCtx
) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
resolvedRecords.add(rec)
resolvedNames.add(capture.name)
}
context.captureRecords = resolvedRecords
context.captureNames = resolvedNames
}
}
val bytecodeBody = fnStatements as? BytecodeStatement
?: scope.raiseIllegalState("non-bytecode lambda body encountered")
val bytecodeFn = bytecodeBody.bytecodeFunction()
val binder: suspend (net.sergeych.lyng.bytecode.CmdFrame, Arguments) -> Unit = { frame, arguments ->
val slotPlan = bytecodeFn.localSlotPlanByName()
if (argsDeclaration == null) {
val l = arguments.list
val itValue: Obj = when (l.size) {
0 -> ObjVoid
1 -> l[0]
else -> ObjList(l.toMutableList())
}
val itSlot = slotPlan["it"]
if (itSlot != null) {
frame.frame.setObj(itSlot, itValue)
}
} else {
argsDeclaration.assignToFrame(
context,
arguments,
slotPlan,
frame.frame
)
}
}
return try {
net.sergeych.lyng.bytecode.CmdVm().execute(bytecodeFn, context, scope.args, binder)
} catch (e: ReturnException) {
if (e.label == null || returnLabels.contains(e.label)) e.result
else throw e
}
}
}
val callable: Obj = if (wrapAsExtensionCallable) {
ObjExtensionMethodCallable("<lambda>", stmt)
} else {
stmt
}
callable.asReadonly
},
bytecodeFn = bytecodeFn,
paramSlotPlan = paramSlotPlanSnapshot,
argsDeclaration = argsDeclaration,
captureEntries = captureEntries,
preferredThisType = expectedReceiverType,
wrapAsExtensionCallable = wrapAsExtensionCallable,
returnLabels = returnLabels,
pos = startPos
)
if (returnClass != null) {
lambdaReturnTypeByRef[ref] = returnClass
}
if (captureEntries.isNotEmpty()) {
lambdaCaptureEntriesByRef[ref] = captureEntries
}
return ref
}
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 suspend fun parseDestructuringPattern(): 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 -> {
// allow trailing/extra commas
}
Token.Type.RBRACKET -> return entries
Token.Type.LBRACKET -> {
val nested = parseDestructuringPattern()
entries += ListEntry.Element(ListLiteralRef(nested))
}
Token.Type.ELLIPSIS -> {
val id = cc.requireToken(Token.Type.ID, "Expected identifier after ...")
val ref = LocalVarRef(id.value, id.pos)
entries += ListEntry.Spread(ref)
}
Token.Type.ID -> {
val ref = LocalVarRef(t.value, t.pos)
if (cc.peekNextNonWhitespace().type == Token.Type.ELLIPSIS) {
cc.next()
entries += ListEntry.Spread(ref)
} else {
entries += ListEntry.Element(ref)
}
}
else -> throw ScriptError(t.pos, "invalid destructuring pattern: expected identifier")
}
}
}
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" -> {
val ref = ClassOperatorRef(operand, t.pos)
lambdaReturnTypeByRef[ref] = ObjClassType
ref
}
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
}
// Nullable shorthand: "x?" means Object? (or makes explicit type nullable)
var nullableHint = false
val nullableHintPos = cc.savePos()
cc.skipWsTokens()
if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION) {
cc.nextNonWhitespace()
nullableHint = true
} else {
cc.restorePos(nullableHintPos)
}
// type information (semantic + mini syntax)
if (nullableHint) {
val afterHint = cc.savePos()
cc.skipWsTokens()
if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
throw ScriptError(t.pos, "nullable shorthand '?' cannot be combined with explicit type")
}
cc.restorePos(afterHint)
}
var (typeInfo, miniType) = parseTypeDeclarationWithMini()
if (nullableHint) {
typeInfo = makeTypeDeclNullable(typeInfo)
miniType = miniType?.let { makeMiniTypeNullable(it) }
}
var defaultValue: Obj? = null
cc.ifNextIs(Token.Type.ASSIGN) {
val expr = parseExpression()
?: throw ScriptError(cc.current().pos, "Expected default value expression")
defaultValue = wrapBytecode(expr)
}
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> {
val start = cc.currentPos()
val (decl, mini) = parseTypeUnionWithMini()
return expandTypeAliases(decl, start) to mini
}
private fun expandTypeAliases(type: TypeDecl, pos: Pos, seen: MutableSet<String> = mutableSetOf()): TypeDecl {
return when (type) {
is TypeDecl.Simple -> expandTypeAliasReference(type.name, emptyList(), type.isNullable, pos, seen) ?: type
is TypeDecl.Generic -> {
val expandedArgs = type.args.map { expandTypeAliases(it, pos, seen) }
expandTypeAliasReference(type.name, expandedArgs, type.isNullable, pos, seen)
?: TypeDecl.Generic(type.name, expandedArgs, type.isNullable)
}
is TypeDecl.Function -> {
val receiver = type.receiver?.let { expandTypeAliases(it, pos, seen) }
val params = type.params.map { expandTypeAliases(it, pos, seen) }
val ret = expandTypeAliases(type.returnType, pos, seen)
TypeDecl.Function(receiver, params, ret, type.nullable)
}
is TypeDecl.Ellipsis -> {
val elem = expandTypeAliases(type.elementType, pos, seen)
TypeDecl.Ellipsis(elem, type.nullable)
}
is TypeDecl.Union -> {
val options = type.options.map { expandTypeAliases(it, pos, seen) }
TypeDecl.Union(options, type.isNullable)
}
is TypeDecl.Intersection -> {
val options = type.options.map { expandTypeAliases(it, pos, seen) }
TypeDecl.Intersection(options, type.isNullable)
}
else -> type
}
}
private fun expandTypeAliasReference(
name: String,
args: List<TypeDecl>,
isNullable: Boolean,
pos: Pos,
seen: MutableSet<String>
): TypeDecl? {
val alias = run {
if (!name.contains('.')) {
val classCtx = currentEnclosingClassName()
if (classCtx != null) {
typeAliases["$classCtx.$name"]?.let { return@run it }
}
}
typeAliases[name] ?: typeAliases[name.substringAfterLast('.')]
} ?: return null
if (!seen.add(alias.name)) throw ScriptError(pos, "circular type alias: ${alias.name}")
val bindings = buildTypeAliasBindings(alias, args, pos)
val substituted = substituteTypeAliasTypeVars(alias.body, bindings)
val expanded = expandTypeAliases(substituted, pos, seen)
seen.remove(alias.name)
return if (isNullable) makeTypeDeclNullable(expanded) else expanded
}
private fun buildTypeAliasBindings(
alias: TypeAliasDecl,
args: List<TypeDecl>,
pos: Pos
): Map<String, TypeDecl> {
val params = alias.typeParams
val resolvedArgs = if (args.size >= params.size) {
if (args.size > params.size) {
throw ScriptError(pos, "type alias ${alias.name} expects ${params.size} type arguments")
}
args.toMutableList()
} else {
val filled = args.toMutableList()
for (param in params.drop(args.size)) {
val fallback = param.defaultType
?: throw ScriptError(pos, "type alias ${alias.name} expects ${params.size} type arguments")
filled.add(fallback)
}
filled
}
val bindings = mutableMapOf<String, TypeDecl>()
for ((index, param) in params.withIndex()) {
val arg = resolvedArgs[index]
val bound = param.bound
if (bound != null && arg !is TypeDecl.TypeVar && !typeDeclSatisfiesBound(arg, bound)) {
throw ScriptError(pos, "type alias ${alias.name} type argument ${param.name} does not satisfy bound")
}
bindings[param.name] = arg
}
return bindings
}
private fun substituteTypeAliasTypeVars(type: TypeDecl, bindings: Map<String, TypeDecl>): TypeDecl {
return when (type) {
is TypeDecl.TypeVar -> {
val bound = bindings[type.name] ?: return type
if (type.isNullable) makeTypeDeclNullable(bound) else bound
}
is TypeDecl.Simple -> type
is TypeDecl.Generic -> {
val args = type.args.map { substituteTypeAliasTypeVars(it, bindings) }
TypeDecl.Generic(type.name, args, type.isNullable)
}
is TypeDecl.Function -> {
val receiver = type.receiver?.let { substituteTypeAliasTypeVars(it, bindings) }
val params = type.params.map { substituteTypeAliasTypeVars(it, bindings) }
val ret = substituteTypeAliasTypeVars(type.returnType, bindings)
TypeDecl.Function(receiver, params, ret, type.nullable)
}
is TypeDecl.Ellipsis -> {
val elem = substituteTypeAliasTypeVars(type.elementType, bindings)
TypeDecl.Ellipsis(elem, type.nullable)
}
is TypeDecl.Union -> {
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
TypeDecl.Union(options, type.isNullable)
}
is TypeDecl.Intersection -> {
val options = type.options.map { substituteTypeAliasTypeVars(it, bindings) }
TypeDecl.Intersection(options, type.isNullable)
}
else -> type
}
}
private fun makeTypeDeclNullable(type: TypeDecl): TypeDecl {
if (type.isNullable) return type
return when (type) {
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
TypeDecl.TypeNullableAny -> type
is TypeDecl.Function -> type.copy(nullable = true)
is TypeDecl.Ellipsis -> type.copy(nullable = true)
is TypeDecl.TypeVar -> type.copy(nullable = true)
is TypeDecl.Union -> type.copy(nullable = true)
is TypeDecl.Intersection -> type.copy(nullable = true)
is TypeDecl.Simple -> TypeDecl.Simple(type.name, true)
is TypeDecl.Generic -> TypeDecl.Generic(type.name, type.args, true)
}
}
private fun makeMiniTypeNullable(type: MiniTypeRef): MiniTypeRef {
return when (type) {
is MiniTypeName -> type.copy(nullable = true)
is MiniGenericType -> type.copy(nullable = true)
is MiniFunctionType -> type.copy(nullable = true)
is MiniTypeVar -> type.copy(nullable = true)
is MiniTypeUnion -> type.copy(nullable = true)
is MiniTypeIntersection -> type.copy(nullable = true)
}
}
private fun parseTypeUnionWithMini(): Pair<TypeDecl, MiniTypeRef> {
var left = parseTypeIntersectionWithMini()
val options = mutableListOf(left)
while (cc.skipTokenOfType(Token.Type.BITOR, isOptional = true)) {
options += parseTypeIntersectionWithMini()
}
if (options.size == 1) return left
val rangeStart = options.first().second.range.start
val rangeEnd = cc.currentPos()
val mini = MiniTypeUnion(MiniRange(rangeStart, rangeEnd), options.map { it.second }, nullable = false)
return TypeDecl.Union(options.map { it.first }, nullable = false) to mini
}
private fun parseTypeIntersectionWithMini(): Pair<TypeDecl, MiniTypeRef> {
var left = parseTypePrimaryWithMini()
val options = mutableListOf(left)
while (cc.skipTokenOfType(Token.Type.BITAND, isOptional = true)) {
options += parseTypePrimaryWithMini()
}
if (options.size == 1) return left
val rangeStart = options.first().second.range.start
val rangeEnd = cc.currentPos()
val mini = MiniTypeIntersection(MiniRange(rangeStart, rangeEnd), options.map { it.second }, nullable = false)
return TypeDecl.Intersection(options.map { it.first }, nullable = false) to mini
}
private fun parseTypePrimaryWithMini(): Pair<TypeDecl, MiniTypeRef> {
parseFunctionTypeWithMini()?.let { return it }
return parseSimpleTypeExpressionWithMini()
}
private fun parseFunctionTypeWithMini(): Pair<TypeDecl, MiniTypeRef>? {
val saved = cc.savePos()
val startPos = cc.currentPos()
fun parseParamTypes(): List<Pair<TypeDecl, MiniTypeRef>> {
val params = mutableListOf<Pair<TypeDecl, MiniTypeRef>>()
var seenEllipsis = false
cc.skipWsTokens()
if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) {
cc.nextNonWhitespace()
return params
}
while (true) {
cc.skipWsTokens()
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ELLIPSIS) {
val ell = cc.nextNonWhitespace()
if (seenEllipsis) {
ell.raiseSyntax("function type can contain only one ellipsis")
}
seenEllipsis = true
val paramDecl = TypeDecl.Ellipsis(TypeDecl.TypeAny)
val mini = MiniTypeName(
MiniRange(ell.pos, ell.pos),
listOf(MiniTypeName.Segment("Object", MiniRange(ell.pos, ell.pos))),
nullable = false
)
params += paramDecl to mini
} else {
val (paramDecl, paramMini) = parseTypeExpressionWithMini()
val finalDecl = if (cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true)) {
if (seenEllipsis) {
cc.current().raiseSyntax("function type can contain only one ellipsis")
}
seenEllipsis = true
TypeDecl.Ellipsis(paramDecl)
} else {
paramDecl
}
params += finalDecl to paramMini
}
val sep = cc.nextNonWhitespace()
when (sep.type) {
Token.Type.COMMA -> continue
Token.Type.RPAREN -> return params
else -> sep.raiseSyntax("expected ',' or ')' in function type")
}
}
}
var receiverDecl: TypeDecl? = null
var receiverMini: MiniTypeRef? = null
val first = cc.peekNextNonWhitespace()
if (first.type == Token.Type.LPAREN) {
cc.nextNonWhitespace()
} else {
val recv = parseSimpleTypeExpressionWithMini()
val dotPos = cc.savePos()
if (cc.skipTokenOfType(Token.Type.DOT, isOptional = true) && cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
receiverDecl = recv.first
receiverMini = recv.second
cc.nextNonWhitespace()
} else {
cc.restorePos(saved)
return null
}
}
val params = parseParamTypes()
val arrow = cc.nextNonWhitespace()
if (arrow.type != Token.Type.ARROW) {
cc.restorePos(saved)
return null
}
val (retDecl, retMini) = parseTypeExpressionWithMini()
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 normalizedReceiverDecl = receiverDecl?.let { decl ->
if (decl is TypeDecl.Simple && (decl.name == "Object" || decl.name == "Obj")) null else decl
}
val normalizedReceiverMini = if (normalizedReceiverDecl == null) null else receiverMini
val rangeStart = when (normalizedReceiverMini) {
null -> startPos
else -> normalizedReceiverMini.range.start
}
val rangeEnd = cc.currentPos()
val mini = MiniFunctionType(
range = MiniRange(rangeStart, rangeEnd),
receiver = normalizedReceiverMini,
params = params.map { it.second },
returnType = retMini,
nullable = isNullable
)
val sem = TypeDecl.Function(
receiver = normalizedReceiverDecl,
params = params.map { it.first },
returnType = retDecl,
nullable = isNullable
)
return sem to mini
}
private fun parseSimpleTypeExpressionWithMini(): 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 = ""
var lastPos = typeStart
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) {
val nextAfterDot = cc.peekNextNonWhitespace()
if (nextAfterDot.type == Token.Type.LPAREN) {
cc.restorePos(dotPos)
break
}
continue
} else {
cc.restorePos(dotPos)
break
}
}
val qualified = segments.joinToString(".") { it.name }
val typeParams = currentTypeParams()
if (segments.size == 1 && typeParams.contains(qualified)) {
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 rangeEnd = cc.currentPos()
val miniRef = MiniTypeVar(MiniRange(typeStart, rangeEnd), qualified, isNullable)
return TypeDecl.TypeVar(qualified, isNullable) to miniRef
}
if (segments.size > 1) {
resolutionSink?.reference(qualified, lastPos)
} else {
resolutionSink?.reference(lastName, lastPos)
}
// 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)
}
private fun parseExtensionReceiverTypeWithMini(): Pair<TypeDecl, MiniTypeRef> {
val segments = mutableListOf<MiniTypeName.Segment>()
var first = true
val typeStart = cc.currentPos()
var lastEnd = typeStart
var lastName = ""
var lastPos = typeStart
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) {
val nextAfterDot = cc.peekNextNonWhitespace()
if (nextAfterDot.type != Token.Type.ID) {
cc.restorePos(dotPos)
break
}
cc.nextNonWhitespace()
val afterSegment = cc.peekNextNonWhitespace()
if (afterSegment.type != Token.Type.DOT &&
afterSegment.type != Token.Type.LT &&
afterSegment.type != Token.Type.QUESTION &&
afterSegment.type != Token.Type.IFNULLASSIGN
) {
cc.restorePos(dotPos)
break
}
segments += MiniTypeName.Segment(nextAfterDot.value, MiniRange(nextAfterDot.pos, nextAfterDot.pos))
lastEnd = cc.currentPos()
lastName = nextAfterDot.value
lastPos = nextAfterDot.pos
continue
} else {
cc.restorePos(dotPos)
break
}
}
val qualified = segments.joinToString(".") { it.name }
val typeParams = currentTypeParams()
if (segments.size == 1 && typeParams.contains(qualified)) {
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 rangeEnd = cc.currentPos()
val miniRef = MiniTypeVar(MiniRange(typeStart, rangeEnd), qualified, isNullable)
return TypeDecl.TypeVar(qualified, isNullable) to miniRef
}
if (segments.size > 1) {
resolutionSink?.reference(qualified, lastPos)
} else {
resolutionSink?.reference(lastName, lastPos)
}
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)
}
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)
}
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)
val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable)
else TypeDecl.Simple(qualified, isNullable)
return Pair(sem, miniRef)
}
private fun typeDeclToTypeRef(typeDecl: TypeDecl, pos: Pos): ObjRef {
return when (typeDecl) {
TypeDecl.TypeAny,
TypeDecl.TypeNullableAny -> ConstRef(Obj.rootObjectType.asReadonly)
is TypeDecl.TypeVar -> resolveLocalTypeRef(typeDecl.name, pos) ?: ConstRef(Obj.rootObjectType.asReadonly)
else -> {
val name = typeDeclName(typeDecl)
resolveLocalTypeRef(name, pos)?.let { return it }
val cls = resolveTypeDeclObjClass(typeDecl)
if (cls != null) return ConstRef(cls.asReadonly)
throw ScriptError(pos, "unknown type $name")
}
}
}
private fun typeDeclName(typeDecl: TypeDecl): String = when (typeDecl) {
is TypeDecl.Simple -> typeDecl.name
is TypeDecl.Generic -> typeDecl.name
is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> "${typeDeclName(typeDecl.elementType)}..."
is TypeDecl.TypeVar -> typeDecl.name
is TypeDecl.Union -> typeDecl.options.joinToString(" | ") { typeDeclName(it) }
is TypeDecl.Intersection -> typeDecl.options.joinToString(" & ") { typeDeclName(it) }
TypeDecl.TypeAny -> "Object"
TypeDecl.TypeNullableAny -> "Object?"
}
private fun inferTypeDeclFromInitializer(stmt: Statement): TypeDecl? {
val directRef = unwrapDirectRef(stmt) ?: return null
return inferTypeDeclFromRef(directRef)
}
private fun inferTypeDeclFromRef(ref: ObjRef): TypeDecl? {
resolveReceiverTypeDecl(ref)?.let { return it }
return when (ref) {
is ListLiteralRef -> inferListLiteralTypeDecl(ref)
is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
else -> null
}
}
private fun inferTypeDeclFromConst(value: Obj): TypeDecl? = when (value) {
is ObjInt -> TypeDecl.Simple("Int", false)
is ObjReal -> TypeDecl.Simple("Real", false)
is ObjString -> TypeDecl.Simple("String", false)
is ObjBool -> TypeDecl.Simple("Bool", false)
is ObjChar -> TypeDecl.Simple("Char", false)
is ObjNull -> TypeDecl.TypeNullableAny
is ObjList -> TypeDecl.Generic("List", listOf(TypeDecl.TypeAny), false)
is ObjMap -> TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
else -> null
}
private fun inferListLiteralTypeDecl(ref: ListLiteralRef): TypeDecl {
val elementType = inferListLiteralElementType(ref.entries())
return TypeDecl.Generic("List", listOf(elementType), false)
}
private fun inferMapLiteralTypeDecl(ref: MapLiteralRef): TypeDecl {
val (keyType, valueType) = inferMapLiteralEntryTypes(ref.entries())
return TypeDecl.Generic("Map", listOf(keyType, valueType), false)
}
private fun inferListLiteralElementType(entries: List<ListEntry>): TypeDecl {
var nullable = false
val collected = mutableListOf<TypeDecl>()
val seen = mutableSetOf<String>()
fun addType(type: TypeDecl) {
val (base, isNullable) = stripNullable(type)
nullable = nullable || isNullable
if (base == TypeDecl.TypeAny) {
collected.clear()
collected += base
seen.clear()
seen += typeDeclKey(base)
return
}
val key = typeDeclKey(base)
if (seen.add(key)) {
collected += base
}
}
for (entry in entries) {
val type = when (entry) {
is ListEntry.Element -> inferTypeDeclFromRef(entry.ref)
is ListEntry.Spread -> inferElementTypeFromSpread(entry.ref)
} ?: return if (nullable) TypeDecl.TypeNullableAny else TypeDecl.TypeAny
addType(type)
if (collected.size == 1 && collected[0] == TypeDecl.TypeAny) break
}
if (collected.isEmpty()) return TypeDecl.TypeAny
val base = if (collected.size == 1) {
collected[0]
} else {
TypeDecl.Union(collected.toList(), nullable = false)
}
return if (nullable) makeTypeDeclNullable(base) else base
}
private fun inferMapLiteralEntryTypes(entries: List<MapLiteralEntry>): Pair<TypeDecl, TypeDecl> {
var keyNullable = false
var valueNullable = false
val keyTypes = mutableListOf<TypeDecl>()
val valueTypes = mutableListOf<TypeDecl>()
val seenKeys = mutableSetOf<String>()
val seenValues = mutableSetOf<String>()
fun addKey(type: TypeDecl) {
val (base, isNullable) = stripNullable(type)
keyNullable = keyNullable || isNullable
if (base == TypeDecl.TypeAny) {
keyTypes.clear()
keyTypes += base
seenKeys.clear()
seenKeys += typeDeclKey(base)
return
}
val key = typeDeclKey(base)
if (seenKeys.add(key)) keyTypes += base
}
fun addValue(type: TypeDecl) {
val (base, isNullable) = stripNullable(type)
valueNullable = valueNullable || isNullable
if (base == TypeDecl.TypeAny) {
valueTypes.clear()
valueTypes += base
seenValues.clear()
seenValues += typeDeclKey(base)
return
}
val key = typeDeclKey(base)
if (seenValues.add(key)) valueTypes += base
}
for (entry in entries) {
when (entry) {
is MapLiteralEntry.Named -> {
addKey(TypeDecl.Simple("String", false))
val vType = inferTypeDeclFromRef(entry.value) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
addValue(vType)
}
is MapLiteralEntry.Spread -> {
val mapType = inferTypeDeclFromRef(entry.ref) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
if (mapType is TypeDecl.Generic) {
val base = mapType.name.substringAfterLast('.')
if (base == "Map") {
val k = mapType.args.getOrNull(0) ?: TypeDecl.TypeAny
val v = mapType.args.getOrNull(1) ?: TypeDecl.TypeAny
addKey(k)
addValue(v)
} else {
return TypeDecl.TypeAny to TypeDecl.TypeAny
}
} else {
return TypeDecl.TypeAny to TypeDecl.TypeAny
}
}
}
}
val keyBase = when {
keyTypes.isEmpty() -> TypeDecl.TypeAny
keyTypes.size == 1 -> keyTypes[0]
else -> TypeDecl.Union(keyTypes.toList(), nullable = false)
}
val valueBase = when {
valueTypes.isEmpty() -> TypeDecl.TypeAny
valueTypes.size == 1 -> valueTypes[0]
else -> TypeDecl.Union(valueTypes.toList(), nullable = false)
}
val finalKey = if (keyNullable) makeTypeDeclNullable(keyBase) else keyBase
val finalValue = if (valueNullable) makeTypeDeclNullable(valueBase) else valueBase
return finalKey to finalValue
}
private fun inferElementTypeFromSpread(ref: ObjRef): TypeDecl? {
val listType = inferTypeDeclFromRef(ref) ?: return null
if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType
if (listType is TypeDecl.Generic) {
val base = listType.name.substringAfterLast('.')
if (base == "List" || base == "Array" || base == "Iterable") {
return listType.args.firstOrNull() ?: TypeDecl.TypeAny
}
}
return TypeDecl.TypeAny
}
private fun stripNullable(type: TypeDecl): Pair<TypeDecl, Boolean> {
if (type is TypeDecl.TypeNullableAny) return TypeDecl.TypeAny to true
val nullable = type.isNullable
val base = if (!nullable) type else when (type) {
is TypeDecl.Function -> type.copy(nullable = false)
is TypeDecl.Ellipsis -> type.copy(nullable = false)
is TypeDecl.TypeVar -> type.copy(nullable = false)
is TypeDecl.Union -> type.copy(nullable = false)
is TypeDecl.Intersection -> type.copy(nullable = false)
is TypeDecl.Simple -> TypeDecl.Simple(type.name, false)
is TypeDecl.Generic -> TypeDecl.Generic(type.name, type.args, false)
else -> type
}
return base to nullable
}
private fun typeDeclKey(type: TypeDecl): String = when (type) {
TypeDecl.TypeAny -> "Any"
TypeDecl.TypeNullableAny -> "Any?"
is TypeDecl.Simple -> "S:${type.name}"
is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}"
is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}"
is TypeDecl.TypeVar -> "V:${type.name}"
is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}"
is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}"
}
private fun inferObjClassFromRef(ref: ObjRef): ObjClass? = when (ref) {
is ConstRef -> ref.constValue as? ObjClass ?: ref.constValue.objClass
is LocalVarRef -> nameObjClass[ref.name] ?: resolveClassByName(ref.name)
is FastLocalVarRef -> nameObjClass[ref.name] ?: resolveClassByName(ref.name)
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
?: nameObjClass[ref.name]
?: resolveClassByName(ref.name)
}
is ClassScopeMemberRef -> {
val targetClass = resolveClassByName(ref.ownerClassName()) ?: return null
inferFieldReturnClass(targetClass, ref.name)
}
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
else -> null
}
private fun resolveReceiverTypeDecl(ref: ObjRef): TypeDecl? {
return when (ref) {
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeDeclByScopeId[ownerScopeId]?.get(ownerSlot)
}
is LocalVarRef -> nameTypeDecl[ref.name]
is FastLocalVarRef -> nameTypeDecl[ref.name]
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
else -> null
}
}
private fun resolveReceiverClassForMember(ref: ObjRef): ObjClass? {
return when (ref) {
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
?: slotTypeDeclByScopeId[ownerScopeId]?.get(ownerSlot)?.let { resolveTypeDeclObjClass(it) }
?: nameObjClass[ref.name]
?: resolveClassByName(ref.name)
}
is LocalVarRef -> nameObjClass[ref.name]
?: nameTypeDecl[ref.name]?.let { resolveTypeDeclObjClass(it) }
?: resolveClassByName(ref.name)
is FastLocalVarRef -> nameObjClass[ref.name]
?: nameTypeDecl[ref.name]?.let { resolveTypeDeclObjClass(it) }
?: resolveClassByName(ref.name)
is ClassScopeMemberRef -> {
val targetClass = resolveClassByName(ref.ownerClassName())
inferFieldReturnClass(targetClass, ref.name)
}
is ImplicitThisMemberRef -> {
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
val targetClass = typeName?.let { resolveClassByName(it) }
inferFieldReturnClass(targetClass, ref.name)
}
is ThisFieldSlotRef -> {
val typeName = currentImplicitThisTypeName()
val targetClass = typeName?.let { resolveClassByName(it) }
inferFieldReturnClass(targetClass, ref.name)
}
is QualifiedThisFieldSlotRef -> {
val targetClass = resolveClassByName(ref.receiverTypeName())
inferFieldReturnClass(targetClass, ref.name)
}
is ConstRef -> ref.constValue as? ObjClass ?: ref.constValue.objClass
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
is QualifiedThisRef -> resolveClassByName(ref.typeName)
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForMember(it.ref) }
is MethodCallRef -> inferMethodCallReturnClass(ref)
is ImplicitThisMethodCallRef -> inferMethodCallReturnClass(ref.methodName())
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
is CallRef -> inferCallReturnClass(ref)
is FieldRef -> {
val targetClass = resolveReceiverClassForMember(ref.target)
inferFieldReturnClass(targetClass, ref.name)
}
else -> null
}
}
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
if (leftClass == null || rightClass == null) return null
return when (ref.op) {
BinOp.PLUS, BinOp.MINUS -> when {
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
leftClass == ObjInstant.type && rightClass == ObjDuration.type -> ObjInstant.type
leftClass == ObjDuration.type && rightClass == ObjInstant.type && ref.op == BinOp.PLUS -> ObjInstant.type
leftClass == ObjDuration.type && rightClass == ObjDuration.type -> ObjDuration.type
(leftClass == ObjBuffer.type || leftClass.allParentsSet.contains(ObjBuffer.type)) &&
(rightClass == ObjBuffer.type || rightClass.allParentsSet.contains(ObjBuffer.type)) &&
ref.op == BinOp.PLUS -> ObjBuffer.type
else -> null
}
else -> null
}
}
private fun inferCallReturnClass(ref: CallRef): ObjClass? {
return when (val target = ref.target) {
is LocalSlotRef -> callableReturnTypeByScopeId[target.scopeId]?.get(target.slot)
?: resolveClassByName(target.name)
is LocalVarRef -> callableReturnTypeByName[target.name]
?: resolveClassByName(target.name)
is FastLocalVarRef -> callableReturnTypeByName[target.name]
?: resolveClassByName(target.name)
is ConstRef -> when (val value = target.constValue) {
is ObjClass -> value
is ObjString -> ObjString.type
else -> null
}
is FieldRef -> {
val receiverClass = resolveReceiverClassForMember(target.target) ?: return null
if (!isClassScopeCallableMember(receiverClass.className, target.name)) return null
resolveClassByName("${receiverClass.className}.${target.name}")
}
else -> null
}
}
private fun inferMethodCallReturnClass(ref: MethodCallRef): ObjClass? {
val receiverDecl = resolveReceiverTypeDecl(ref.receiver)
val genericReturnDecl = inferMethodCallReturnTypeDecl(ref.name, receiverDecl)
if (genericReturnDecl != null) {
methodReturnTypeDeclByRef[ref] = genericReturnDecl
resolveTypeDeclObjClass(genericReturnDecl)?.let { return it }
if (genericReturnDecl is TypeDecl.TypeVar) {
return Obj.rootObjectType
}
}
if (ref.name == "decode") {
val payload = inferEncodedPayloadClass(ref.args)
if (payload != null) return payload
}
val receiverClass = resolveReceiverClassForMember(ref.receiver)
return inferMethodCallReturnClass(ref.name)
}
private fun inferMethodCallReturnTypeDecl(name: String, receiver: TypeDecl?): TypeDecl? {
val base = when (receiver) {
is TypeDecl.Generic -> receiver.name.substringAfterLast('.')
is TypeDecl.Simple -> receiver.name.substringAfterLast('.')
else -> null
}
return when {
name == "iterator" && receiver is TypeDecl.Generic && base == "Iterable" -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("Iterator", listOf(arg), false)
}
name == "next" && receiver is TypeDecl.Generic && base == "Iterator" -> {
receiver.args.firstOrNull()
}
else -> null
}
}
private fun inferEncodedPayloadClass(args: List<ParsedArgument>): ObjClass? {
val stmt = args.firstOrNull()?.value as? ExpressionStatement ?: return null
val ref = stmt.ref
val byEncoded = when (ref) {
is LocalSlotRef -> encodedPayloadTypeByScopeId[ref.scopeId]?.get(ref.slot)
is LocalVarRef -> encodedPayloadTypeByName[ref.name]
else -> null
}
if (byEncoded != null) return byEncoded
return when (ref) {
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)
}
is LocalVarRef -> nameObjClass[ref.name]
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
is MethodCallRef -> if (ref.name == "encode") inferEncodedPayloadClass(ref.args) else null
else -> null
}
}
private fun inferMethodCallReturnClass(name: String): ObjClass? = when (name) {
"map",
"mapNotNull",
"filter",
"filterNotNull",
"drop",
"take",
"flatMap",
"flatten",
"sorted",
"sortedBy",
"sortedWith",
"reversed",
"toList",
"shuffle",
"shuffled" -> ObjList.type
"dropLast" -> ObjFlow.type
"takeLast" -> ObjRingBuffer.type
"iterator" -> ObjIterator
"now",
"truncateToSecond",
"truncateToMinute",
"truncateToMillisecond" -> ObjInstant.type
"toDateTime",
"toTimeZone",
"toUTC",
"parseRFC3339",
"addYears",
"addMonths",
"addDays",
"addHours",
"addMinutes",
"addSeconds" -> ObjDateTime.type
"toInstant" -> ObjInstant.type
"toRFC3339",
"toSortableString",
"toJsonString",
"decodeUtf8",
"toDump",
"toString" -> ObjString.type
"startsWith",
"matches" -> ObjBool.type
"toInt",
"toEpochSeconds" -> ObjInt.type
"toMutable" -> ObjMutableBuffer.type
"seq" -> ObjFlow.type
"encode" -> ObjBitBuffer.type
"assertThrows" -> ObjException.Root
else -> null
}
private fun inferFieldReturnClass(targetClass: ObjClass?, name: String): ObjClass? {
if (targetClass == null) return null
if (targetClass == ObjDynamic.type) return ObjDynamic.type
classFieldTypesByName[targetClass.className]?.get(name)?.let { return it }
enumEntriesByName[targetClass.className]?.let { entries ->
return when {
name == "entries" -> ObjList.type
name == "name" -> ObjString.type
name == "ordinal" -> ObjInt.type
entries.contains(name) -> targetClass
else -> null
}
}
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
return ObjInstant.type
}
if (targetClass == ObjInstant.type && name in listOf(
"truncateToMinute",
"truncateToSecond",
"truncateToMillisecond",
"truncateToMicrosecond"
)
) {
return ObjInstant.type
}
if (targetClass == ObjString.type && name == "re") {
return ObjRegex.type
}
if (targetClass == ObjInt.type || targetClass == ObjReal.type) {
return when (name) {
"day",
"days",
"hour",
"hours",
"minute",
"minutes",
"second",
"seconds",
"millisecond",
"milliseconds",
"microsecond",
"microseconds" -> ObjDuration.type
else -> null
}
}
if (targetClass == ObjDuration.type) {
return when (name) {
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds" -> ObjReal.type
else -> null
}
}
if (targetClass == ObjInstant.type) {
return when (name) {
"epochSeconds" -> ObjInt.type
"epochWholeSeconds" -> ObjInt.type
"truncateToSecond",
"truncateToMinute",
"truncateToMillisecond" -> ObjInstant.type
else -> null
}
}
if (targetClass == ObjDateTime.type) {
return when (name) {
"year",
"month",
"day",
"hour",
"minute",
"second",
"dayOfWeek",
"nanosecond" -> ObjInt.type
"timeZone" -> ObjString.type
else -> null
}
}
if (targetClass == ObjException.Root || targetClass.allParentsSet.contains(ObjException.Root)) {
return when (name) {
"message" -> ObjString.type
"stackTrace" -> ObjList.type
else -> null
}
}
if (targetClass == ObjRegex.type && name == "pattern") {
return ObjString.type
}
return null
}
private fun shouldTreatAsClassScopeCall(left: ObjRef, memberName: String): Boolean {
val receiverClass = resolveReceiverClassForMember(left) ?: return false
return isClassScopeCallableMember(receiverClass.className, memberName)
}
private fun enforceReceiverTypeForMember(left: ObjRef, memberName: String, pos: Pos) {
if (left is LocalVarRef && left.name == "scope") return
if (left is LocalSlotRef && left.name == "scope") return
val receiverClass = resolveReceiverClassForMember(left)
if (receiverClass == null) {
if (isAllowedObjectMember(memberName)) return
throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
}
registerExtensionWrapperBindings(receiverClass, memberName, pos)
if (receiverClass == Obj.rootObjectType) {
val allowed = isAllowedObjectMember(memberName)
if (!allowed && !hasExtensionFor(receiverClass.className, memberName)) {
throw ScriptError(pos, "member $memberName is not available on Object without explicit cast")
}
}
}
private fun resolveMemberHostForUnion(typeDecl: TypeDecl, memberName: String, pos: Pos): ObjClass {
val receiverClass = when (typeDecl) {
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> Obj.rootObjectType
else -> resolveTypeDeclObjClass(typeDecl)
} ?: throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
if (receiverClass == ObjDynamic.type) {
return receiverClass
}
registerExtensionWrapperBindings(receiverClass, memberName, pos)
val hasMember = receiverClass.instanceFieldIdMap()[memberName] != null ||
receiverClass.instanceMethodIdMap(includeAbstract = true)[memberName] != null
if (!hasMember && !hasExtensionFor(receiverClass.className, memberName)) {
if (receiverClass == Obj.rootObjectType && isAllowedObjectMember(memberName)) {
return receiverClass
}
val ownerName = receiverClass.className
val message = if (receiverClass == Obj.rootObjectType) {
"member $memberName is not available on Object without explicit cast"
} else {
"unknown member $memberName on $ownerName"
}
throw ScriptError(pos, message)
}
return receiverClass
}
private fun buildUnionMemberAccess(
left: ObjRef,
union: TypeDecl.Union,
memberName: String,
pos: Pos,
isOptional: Boolean,
makeRef: (ObjRef) -> ObjRef
): ObjRef {
val options = union.options
if (options.isEmpty()) {
throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
}
for (option in options) {
resolveMemberHostForUnion(option, memberName, pos)
}
val unionName = typeDeclName(union)
val errorMessage = ObjString("value is not $unionName").asReadonly
val throwExpr = ExpressionStatement(ConstRef(errorMessage), pos)
val throwStmt = ThrowStatement(throwExpr, pos)
var current: ObjRef = net.sergeych.lyng.obj.StatementRef(throwStmt)
for (option in options.asReversed()) {
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(option, pos)
val cond = BinaryOpRef(BinOp.IS, left, typeRef)
val casted = CastRef(left, typeRef, false, pos)
val branch = makeRef(casted)
current = ConditionalRef(cond, branch, current)
}
if (isOptional) {
val nullRef = ConstRef(ObjNull.asReadonly)
val nullCond = BinaryOpRef(BinOp.REF_EQ, left, nullRef)
current = ConditionalRef(nullCond, nullRef, current)
}
return current
}
private fun buildUnionFieldAccess(
left: ObjRef,
memberName: String,
pos: Pos,
isOptional: Boolean
): ObjRef? {
val receiverDecl = resolveReceiverTypeDecl(left) as? TypeDecl.Union ?: return null
return buildUnionMemberAccess(left, receiverDecl, memberName, pos, isOptional) { receiver ->
FieldRef(receiver, memberName, false)
}
}
private fun buildUnionMethodCall(
left: ObjRef,
memberName: String,
pos: Pos,
isOptional: Boolean,
args: List<ParsedArgument>,
tailBlock: Boolean
): ObjRef? {
val receiverDecl = resolveReceiverTypeDecl(left) as? TypeDecl.Union ?: return null
return buildUnionMemberAccess(left, receiverDecl, memberName, pos, isOptional) { receiver ->
MethodCallRef(receiver, memberName, args, tailBlock, false)
}
}
private fun registerExtensionWrapperBindings(receiverClass: ObjClass, memberName: String, pos: Pos) {
for (cls in receiverClass.mro) {
val wrapperNames = listOf(
extensionCallableName(cls.className, memberName),
extensionPropertyGetterName(cls.className, memberName),
extensionPropertySetterName(cls.className, memberName)
)
for (wrapperName in wrapperNames) {
val resolved = resolveImportBinding(wrapperName, pos) ?: continue
val plan = moduleSlotPlan()
if (plan != null && !plan.slots.containsKey(wrapperName)) {
declareSlotNameIn(
plan,
wrapperName,
resolved.record.isMutable,
resolved.record.type == ObjRecord.Type.Delegated
)
}
registerImportBinding(wrapperName, resolved.binding, pos)
}
}
}
private fun isAllowedObjectMember(memberName: String): Boolean {
return when (memberName) {
"toString",
"toInspectString",
"let",
"also",
"apply",
"run" -> true
else -> false
}
}
private fun resolveTypeRefClass(ref: ObjRef): ObjClass? = when (ref) {
is ConstRef -> ref.constValue as? ObjClass
is LocalSlotRef -> resolveTypeDeclObjClass(TypeDecl.Simple(ref.name, false)) ?: nameObjClass[ref.name]
is LocalVarRef -> resolveTypeDeclObjClass(TypeDecl.Simple(ref.name, false)) ?: nameObjClass[ref.name]
is FastLocalVarRef -> resolveTypeDeclObjClass(TypeDecl.Simple(ref.name, false)) ?: nameObjClass[ref.name]
else -> null
}
private fun typeParamBoundSatisfied(argClass: ObjClass, bound: TypeDecl): Boolean = when (bound) {
is TypeDecl.Union -> bound.options.any { typeParamBoundSatisfied(argClass, it) }
is TypeDecl.Intersection -> bound.options.all { typeParamBoundSatisfied(argClass, it) }
is TypeDecl.Simple, is TypeDecl.Generic -> {
val boundClass = resolveTypeDeclObjClass(bound) ?: return false
argClass == boundClass || argClass.allParentsSet.contains(boundClass)
}
else -> true
}
private fun checkGenericBoundsAtCall(
name: String,
args: List<ParsedArgument>,
pos: Pos
) {
val decl = lookupGenericFunctionDecl(name) ?: return
val inferred = mutableMapOf<String, TypeDecl>()
val limit = minOf(args.size, decl.params.size)
for (i in 0 until limit) {
val paramType = decl.params[i].type
val argRef = (args[i].value as? ExpressionStatement)?.ref ?: continue
val argTypeDecl = inferTypeDeclFromRef(argRef)
?: inferObjClassFromRef(argRef)?.let { TypeDecl.Simple(it.className, false) }
?: continue
collectTypeVarBindings(paramType, argTypeDecl, inferred)
}
for (tp in decl.typeParams) {
val argType = inferred[tp.name] ?: continue
val bound = tp.bound ?: continue
if (!typeDeclSatisfiesBound(argType, bound)) {
throw ScriptError(pos, "type argument ${typeDeclName(argType)} does not satisfy bound ${typeDeclName(bound)}")
}
}
}
private fun checkFunctionTypeCallArity(
target: ObjRef,
args: List<ParsedArgument>,
pos: Pos
) {
val decl = (resolveReceiverTypeDecl(target) as? TypeDecl.Function)
?: seedTypeDeclFromRef(target) as? TypeDecl.Function
?: return
if (args.any { it.isSplat }) return
val actual = args.size
val receiverCount = if (decl.receiver != null) 1 else 0
val paramList = mutableListOf<TypeDecl>()
decl.receiver?.let { paramList += it }
paramList += decl.params
val ellipsisIndex = paramList.indexOfFirst { it is TypeDecl.Ellipsis }
if (ellipsisIndex < 0) {
val expected = paramList.size
if (actual != expected) {
throw ScriptError(pos, "expected $expected arguments, got $actual")
}
return
}
val headCount = ellipsisIndex
val tailCount = paramList.size - ellipsisIndex - 1
val minArgs = headCount + tailCount
if (actual < minArgs) {
throw ScriptError(pos, "expected at least $minArgs arguments, got $actual")
}
}
private fun seedTypeDeclFromRef(ref: ObjRef): TypeDecl? {
val name = when (ref) {
is LocalVarRef -> ref.name
is LocalSlotRef -> ref.name
is FastLocalVarRef -> ref.name
else -> null
} ?: return null
seedScope?.getLocalRecordDirect(name)?.typeDecl?.let { return it }
return seedScope?.get(name)?.typeDecl
}
private fun checkFunctionTypeCallTypes(
target: ObjRef,
args: List<ParsedArgument>,
pos: Pos
) {
val decl = (resolveReceiverTypeDecl(target) as? TypeDecl.Function)
?: seedTypeDeclFromRef(target) as? TypeDecl.Function
?: return
val paramList = mutableListOf<TypeDecl>()
decl.receiver?.let { paramList += it }
paramList += decl.params
if (paramList.isEmpty()) return
val ellipsisIndex = paramList.indexOfFirst { it is TypeDecl.Ellipsis }
fun argTypeDecl(arg: ParsedArgument): TypeDecl? {
val stmt = arg.value as? ExpressionStatement ?: return null
val ref = stmt.ref
return inferTypeDeclFromRef(ref)
?: inferObjClassFromRef(ref)?.let { TypeDecl.Simple(it.className, false) }
}
fun typeDeclSubtypeOf(arg: TypeDecl, param: TypeDecl): Boolean {
if (param == TypeDecl.TypeAny || param == TypeDecl.TypeNullableAny) return true
val (argBase, argNullable) = stripNullable(arg)
val (paramBase, paramNullable) = stripNullable(param)
if (argNullable && !paramNullable) return false
if (paramBase == TypeDecl.TypeAny) return true
if (paramBase is TypeDecl.TypeVar) return true
if (argBase is TypeDecl.TypeVar) return true
if (paramBase is TypeDecl.Simple && (paramBase.name == "Object" || paramBase.name == "Obj")) return true
if (argBase is TypeDecl.Ellipsis) return typeDeclSubtypeOf(argBase.elementType, paramBase)
if (paramBase is TypeDecl.Ellipsis) return typeDeclSubtypeOf(argBase, paramBase.elementType)
return when (argBase) {
is TypeDecl.Union -> argBase.options.all { typeDeclSubtypeOf(it, paramBase) }
is TypeDecl.Intersection -> argBase.options.any { typeDeclSubtypeOf(it, paramBase) }
else -> when (paramBase) {
is TypeDecl.Union -> paramBase.options.any { typeDeclSubtypeOf(argBase, it) }
is TypeDecl.Intersection -> paramBase.options.all { typeDeclSubtypeOf(argBase, it) }
else -> {
val argClass = resolveTypeDeclObjClass(argBase) ?: return false
val paramClass = resolveTypeDeclObjClass(paramBase) ?: return false
argClass == paramClass || argClass.allParentsSet.contains(paramClass)
}
}
}
}
fun fail(argPos: Pos, expected: TypeDecl, got: TypeDecl) {
throw ScriptError(argPos, "argument type ${typeDeclName(got)} does not match ${typeDeclName(expected)}")
}
if (ellipsisIndex < 0) {
val limit = minOf(paramList.size, args.size)
for (i in 0 until limit) {
val arg = args[i]
val argType = argTypeDecl(arg) ?: continue
val paramType = paramList[i]
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
return
}
val headCount = ellipsisIndex
val tailCount = paramList.size - ellipsisIndex - 1
val ellipsisType = paramList[ellipsisIndex] as TypeDecl.Ellipsis
val argCount = args.size
val headLimit = minOf(headCount, argCount)
for (i in 0 until headLimit) {
val arg = args[i]
val argType = argTypeDecl(arg) ?: continue
val paramType = paramList[i]
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
val tailStartArg = maxOf(headCount, argCount - tailCount)
for (i in tailStartArg until argCount) {
val arg = args[i]
val paramType = paramList[paramList.size - (argCount - i)]
val argType = argTypeDecl(arg) ?: continue
if (!typeDeclSubtypeOf(argType, paramType)) {
fail(arg.pos, paramType, argType)
}
}
val ellipsisArgEnd = argCount - tailCount
for (i in headCount until ellipsisArgEnd) {
val arg = args[i]
val argType = if (arg.isSplat) {
val stmt = arg.value as? ExpressionStatement
val ref = stmt?.ref
ref?.let { inferElementTypeFromSpread(it) }
} else {
argTypeDecl(arg)
} ?: continue
if (!typeDeclSubtypeOf(argType, ellipsisType.elementType)) {
fail(arg.pos, ellipsisType.elementType, argType)
}
}
}
private fun collectTypeVarBindings(
paramType: TypeDecl,
argType: TypeDecl,
out: MutableMap<String, TypeDecl>
) {
when (paramType) {
is TypeDecl.TypeVar -> {
val current = out[paramType.name]
out[paramType.name] = mergeTypeDecls(current, argType)
}
is TypeDecl.Generic -> {
if (argType is TypeDecl.Generic && argType.name == paramType.name &&
argType.args.size == paramType.args.size
) {
for (i in paramType.args.indices) {
collectTypeVarBindings(paramType.args[i], argType.args[i], out)
}
}
}
is TypeDecl.Union -> {
if (argType is TypeDecl.Union) {
val limit = minOf(paramType.options.size, argType.options.size)
for (i in 0 until limit) {
collectTypeVarBindings(paramType.options[i], argType.options[i], out)
}
}
}
is TypeDecl.Intersection -> {
if (argType is TypeDecl.Intersection) {
val limit = minOf(paramType.options.size, argType.options.size)
for (i in 0 until limit) {
collectTypeVarBindings(paramType.options[i], argType.options[i], out)
}
}
}
else -> {}
}
}
private fun mergeTypeDecls(a: TypeDecl?, b: TypeDecl): TypeDecl {
if (a == null) return b
if (a == TypeDecl.TypeAny || b == TypeDecl.TypeAny) {
return if (a.isNullable || b.isNullable) TypeDecl.TypeNullableAny else TypeDecl.TypeAny
}
if (a == TypeDecl.TypeNullableAny || b == TypeDecl.TypeNullableAny) return TypeDecl.TypeNullableAny
val (aBase, aNullable) = stripNullable(a)
val (bBase, bNullable) = stripNullable(b)
if (typeDeclKey(aBase) == typeDeclKey(bBase)) {
return if (aNullable || bNullable) makeTypeDeclNullable(aBase) else aBase
}
val options = mutableListOf<TypeDecl>()
val seen = mutableSetOf<String>()
fun addOpt(t: TypeDecl) {
val key = typeDeclKey(t)
if (seen.add(key)) options += t
}
val nullable = aNullable || bNullable
if (aBase is TypeDecl.Union) aBase.options.forEach { addOpt(it) } else addOpt(aBase)
if (bBase is TypeDecl.Union) bBase.options.forEach { addOpt(it) } else addOpt(bBase)
val merged = TypeDecl.Union(options, nullable = false)
return if (nullable) makeTypeDeclNullable(merged) else merged
}
private fun typeDeclSatisfiesBound(argType: TypeDecl, bound: TypeDecl): Boolean {
return when (argType) {
TypeDecl.TypeAny, TypeDecl.TypeNullableAny -> true
is TypeDecl.Union -> argType.options.all { typeDeclSatisfiesBound(it, bound) }
is TypeDecl.Intersection -> argType.options.all { typeDeclSatisfiesBound(it, bound) }
is TypeDecl.Ellipsis -> typeDeclSatisfiesBound(argType.elementType, bound)
else -> when (bound) {
is TypeDecl.Union -> bound.options.any { typeDeclSatisfiesBound(argType, it) }
is TypeDecl.Intersection -> bound.options.all { typeDeclSatisfiesBound(argType, it) }
is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function, is TypeDecl.Ellipsis -> {
val argClass = resolveTypeDeclObjClass(argType) ?: return false
val boundClass = resolveTypeDeclObjClass(bound) ?: return false
argClass == boundClass || argClass.allParentsSet.contains(boundClass)
}
else -> true
}
}
}
private fun bindTypeParamsAtRuntime(
context: Scope,
argsDeclaration: ArgsDeclaration,
typeParams: List<TypeDecl.TypeParam>
): Map<String, Obj> {
if (typeParams.isEmpty()) return emptyMap()
val inferred = mutableMapOf<String, TypeDecl>()
val argValues = context.args.list
for ((index, param) in argsDeclaration.params.withIndex()) {
val direct = argValues.getOrNull(index) ?: continue
val value = when (direct) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read()
else -> direct
}
collectRuntimeTypeVarBindings(param.type, value, inferred)
}
val boundValues = LinkedHashMap<String, Obj>(typeParams.size)
for (tp in typeParams) {
val inferredType = inferred[tp.name] ?: tp.defaultType ?: TypeDecl.TypeAny
val normalized = normalizeRuntimeTypeDecl(inferredType)
val cls = resolveTypeDeclObjClass(normalized)
val boundValue = if (cls != null &&
!normalized.isNullable &&
normalized !is TypeDecl.Union &&
normalized !is TypeDecl.Intersection
) {
cls
} else {
net.sergeych.lyng.obj.ObjTypeExpr(normalized)
}
context.addConst(tp.name, boundValue)
boundValues[tp.name] = boundValue
val bound = tp.bound ?: continue
if (!typeDeclSatisfiesBound(normalized, bound)) {
context.raiseError("type argument ${typeDeclName(normalized)} does not satisfy bound ${typeDeclName(bound)}")
}
}
return boundValues
}
private fun collectRuntimeTypeVarBindings(
paramType: TypeDecl,
value: Obj,
inferred: MutableMap<String, TypeDecl>
) {
when (paramType) {
is TypeDecl.TypeVar -> {
if (value !== ObjNull) {
inferred[paramType.name] = inferRuntimeTypeDecl(value)
}
}
is TypeDecl.Generic -> {
val base = paramType.name.substringAfterLast('.')
val arg = paramType.args.firstOrNull()
if (base == "List" && arg is TypeDecl.TypeVar && value is ObjList) {
val elementType = inferListElementTypeDecl(value)
inferred[arg.name] = elementType
}
}
else -> {}
}
}
private fun inferRuntimeTypeDecl(value: Obj): TypeDecl {
return when (value) {
is ObjInt -> TypeDecl.Simple("Int", false)
is ObjReal -> TypeDecl.Simple("Real", false)
is ObjString -> TypeDecl.Simple("String", false)
is ObjBool -> TypeDecl.Simple("Bool", false)
is ObjChar -> TypeDecl.Simple("Char", false)
is ObjNull -> TypeDecl.TypeNullableAny
is ObjList -> TypeDecl.Generic("List", listOf(inferListElementTypeDecl(value)), false)
is ObjMap -> TypeDecl.Generic("Map", listOf(inferMapKeyTypeDecl(value), inferMapValueTypeDecl(value)), false)
is ObjClass -> TypeDecl.Simple(value.className, false)
else -> TypeDecl.Simple(value.objClass.className, false)
}
}
private fun inferListElementTypeDecl(list: ObjList): TypeDecl {
var nullable = false
val options = mutableListOf<TypeDecl>()
val seen = mutableSetOf<String>()
for (elem in list.list) {
if (elem === ObjNull) {
nullable = true
continue
}
val elemType = inferRuntimeTypeDecl(elem)
val base = stripNullable(elemType).first
val key = typeDeclKey(base)
if (seen.add(key)) options += base
}
val base = when {
options.isEmpty() -> TypeDecl.TypeAny
options.size == 1 -> options[0]
else -> TypeDecl.Union(options, nullable = false)
}
return if (nullable) makeTypeDeclNullable(base) else base
}
private fun inferMapKeyTypeDecl(map: ObjMap): TypeDecl {
var nullable = false
val options = mutableListOf<TypeDecl>()
val seen = mutableSetOf<String>()
for (key in map.map.keys) {
if (key === ObjNull) {
nullable = true
continue
}
val keyType = inferRuntimeTypeDecl(key)
val base = stripNullable(keyType).first
val k = typeDeclKey(base)
if (seen.add(k)) options += base
}
val base = when {
options.isEmpty() -> TypeDecl.TypeAny
options.size == 1 -> options[0]
else -> TypeDecl.Union(options, nullable = false)
}
return if (nullable) makeTypeDeclNullable(base) else base
}
private fun inferMapValueTypeDecl(map: ObjMap): TypeDecl {
var nullable = false
val options = mutableListOf<TypeDecl>()
val seen = mutableSetOf<String>()
for (value in map.map.values) {
if (value === ObjNull) {
nullable = true
continue
}
val valueType = inferRuntimeTypeDecl(value)
val base = stripNullable(valueType).first
val k = typeDeclKey(base)
if (seen.add(k)) options += base
}
val base = when {
options.isEmpty() -> TypeDecl.TypeAny
options.size == 1 -> options[0]
else -> TypeDecl.Union(options, nullable = false)
}
return if (nullable) makeTypeDeclNullable(base) else base
}
private fun normalizeRuntimeTypeDecl(type: TypeDecl): TypeDecl {
return when (type) {
is TypeDecl.Union -> TypeDecl.Union(type.options.distinctBy { typeDeclKey(it) }, type.isNullable)
is TypeDecl.Intersection -> TypeDecl.Intersection(type.options.distinctBy { typeDeclKey(it) }, type.isNullable)
else -> type
}
}
private fun resolveLocalTypeRef(name: String, pos: Pos): ObjRef? {
val slotLoc = lookupSlotLocation(name, includeModule = true) ?: return null
captureLocalRef(name, slotLoc, pos)?.let { return it }
return LocalSlotRef(
name,
slotLoc.slot,
slotLoc.scopeId,
slotLoc.isMutable,
slotLoc.isDelegated,
pos,
strictSlotRefs
)
}
/**
* 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 withReceiver = when (left) {
is LocalVarRef -> if (left.name == "with") left.name else null
is LocalSlotRef -> if (left.name == "with") left.name else null
else -> null
}
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 {
if (withReceiver != null) {
val parsedArgs = parseArgsNoTailBlock().toMutableList()
val pos = cc.savePos()
val end = cc.next()
if (end.type == Token.Type.LBRACE) {
val receiverType = inferReceiverTypeFromArgs(parsedArgs)
val callableAccessor = parseLambdaExpression(receiverType, wrapAsExtensionCallable = true)
parsedArgs += ParsedArgument(ExpressionStatement(callableAccessor, end.pos), end.pos)
detectedBlockArgument = true
} else {
cc.restorePos(pos)
}
parsedArgs
} else {
val r = parseArgs(expectedReceiver)
detectedBlockArgument = r.second
r.first
}
}
val implicitThisTypeName = currentImplicitThisTypeName()
val result = when (left) {
is ImplicitThisMemberRef ->
if (left.methodId == null && left.fieldId != null) {
CallRef(left, args, detectedBlockArgument, isOptional)
} else {
ImplicitThisMethodCallRef(
left.name,
left.methodId,
args,
detectedBlockArgument,
isOptional,
left.atPos,
left.preferredThisTypeName() ?: 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 receiverTypeName = implicitReceiverTypeForMember(left.name) ?: implicitThisTypeName
val ids = resolveImplicitThisMemberIds(left.name, left.pos(), receiverTypeName)
ImplicitThisMethodCallRef(
left.name,
ids.methodId,
args,
detectedBlockArgument,
isOptional,
left.pos(),
receiverTypeName
)
} else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos())
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 receiverTypeName = implicitReceiverTypeForMember(left.name) ?: implicitThisTypeName
val ids = resolveImplicitThisMemberIds(left.name, left.pos(), receiverTypeName)
ImplicitThisMethodCallRef(
left.name,
ids.methodId,
args,
detectedBlockArgument,
isOptional,
left.pos(),
receiverTypeName
)
} else {
checkFunctionTypeCallArity(left, args, left.pos())
checkFunctionTypeCallTypes(left, args, left.pos())
checkGenericBoundsAtCall(left.name, args, left.pos())
CallRef(left, args, detectedBlockArgument, isOptional)
}
}
else -> CallRef(left, args, detectedBlockArgument, isOptional)
}
return result
}
private fun inferReceiverTypeFromArgs(args: List<ParsedArgument>): String? {
val stmt = args.firstOrNull()?.value as? ExpressionStatement ?: return null
val ref = stmt.ref
val bySlot = (ref as? LocalSlotRef)?.let { slotRef ->
slotTypeByScopeId[slotRef.scopeId]?.get(slotRef.slot)
}
val byName = (ref as? LocalVarRef)?.let { nameObjClass[it.name] }
val cls = bySlot ?: byName ?: resolveInitializerObjClass(stmt)
return cls?.className
}
private fun inferReceiverTypeFromRef(ref: ObjRef): String? {
return when (ref) {
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
slotTypeByScopeId[ownerScopeId]?.get(ownerSlot)?.className
?: slotTypeDeclByScopeId[ownerScopeId]?.get(ownerSlot)?.let { typeDeclName(it) }
}
is LocalVarRef -> nameObjClass[ref.name]?.className
?: nameTypeDecl[ref.name]?.let { typeDeclName(it) }
is FastLocalVarRef -> nameObjClass[ref.name]?.className
?: nameTypeDecl[ref.name]?.let { typeDeclName(it) }
is QualifiedThisRef -> ref.typeName
else -> resolveReceiverClassForMember(ref)?.className
}
}
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 && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier closed at top level is only allowed for classes")
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 || isOverride)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for class: ${modifiers.joinToString(" ")}"
)
parseClassDeclaration(isAbstract, isExtern, isClosed)
}
"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) {
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
val implicitThisType = classCtx?.name
miniSink?.onEnterFunction(null)
val block = inCodeContext(
CodeContext.Function(
"<init>",
implicitThisMembers = true,
implicitThisTypeName = implicitThisType
)
) {
parseBlock()
}
miniSink?.onExitFunction(cc.currentPos())
lastParsedBlockRange?.let { range ->
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
}
val initStmt = wrapInstanceInitBytecode(block, "init@${id.pos}")
ClassInstanceInitDeclStatement(initStmt, id.pos)
} else null
}
"enum" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseEnumDeclaration()
}
"type" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
if (looksLikeTypeAliasDeclaration()) {
parseTypeAliasDeclaration()
} else {
null
}
}
"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 (typeDecl, _) = parseTypeExpressionWithMini()
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, t.pos)
val caseType = ExpressionStatement(typeRef, t.pos)
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.scopeId, 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, stmt.scopeId, emptyList(), stmt.pos)
}
fun resolveCatchVarClass(names: List<String>): ObjClass? {
if (names.size == 1) {
val name = names.first()
return resolveClassByName(name) ?: if (name == "Exception") ObjException.Root else null
}
return ObjException.Root
}
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)
val catchType = resolveCatchVarClass(exClassNames)
stripCatchCaptures(
withCatchSlot(
unwrapBytecodeDeep(
parseBlockWithPredeclared(
listOf(catchVar.value to false),
predeclaredTypes = catchType?.let { mapOf(catchVar.value to it) } ?: emptyMap()
)
),
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)
val catchType = resolveCatchVarClass(listOf("Exception"))
stripCatchCaptures(
withCatchSlot(
unwrapBytecodeDeep(
parseBlockWithPredeclared(
listOf(itToken.value to false),
skipLeadingBrace = true,
predeclaredTypes = catchType?.let { mapOf(itToken.value to it) } ?: emptyMap()
)
),
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 tryCatches = catches.map { cdata ->
TryStatement.CatchBlock(
catchVarName = cdata.catchVar.value,
catchVarPos = cdata.catchVar.pos,
classNames = cdata.classNames,
block = cdata.block
)
}
return TryStatement(body, tryCatches, finallyClause, 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
val declaredName = nameToken.value
val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
resolutionSink?.declareSymbol(declaredName, SymbolKind.ENUM, isMutable = false, pos = nameToken.pos)
if (codeContexts.lastOrNull() is CodeContext.Module) {
scopeSeedNames.add(declaredName)
}
if (outerClassName != null) {
val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
registerClassScopeCallableMember(outerClassName, declaredName)
}
val lifted = if (cc.peekNextNonWhitespace().type == Token.Type.STAR) {
cc.nextNonWhitespace()
true
} else {
false
}
// 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 = declaredName,
entries = names,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern,
entryPositions = positions
)
)
if (lifted) {
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
val conflicts = when {
classCtx != null -> names.filter { entry ->
classCtx.classScopeMembers.contains(entry) || classCtx.declaredMembers.contains(entry)
}
else -> {
val modulePlan = moduleSlotPlan()
names.filter { entry -> modulePlan?.slots?.containsKey(entry) == true }
}
}
if (conflicts.isNotEmpty()) {
val entry = conflicts.first()
val disambiguation = "${qualifiedName}.$entry"
throw ScriptError(
nameToken.pos,
"lifted enum entry '$entry' conflicts with existing member; use '$disambiguation'"
)
}
}
val fieldIds = LinkedHashMap<String, Int>(names.size + 1)
fieldIds["entries"] = 0
for ((index, entry) in names.withIndex()) {
fieldIds[entry] = index + 1
}
val baseMethodMax = net.sergeych.lyng.obj.EnumBase
.instanceMethodIdMap(includeAbstract = true)
.values
.maxOrNull() ?: -1
val methodIds = linkedMapOf(
"valueOf" to baseMethodMax + 1,
"name" to baseMethodMax + 2,
"ordinal" to baseMethodMax + 3
)
compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName,
fieldIds = fieldIds,
methodIds = methodIds,
nextFieldId = fieldIds.size,
nextMethodId = methodIds.size,
baseNames = listOf("Enum")
)
enumEntriesByName[qualifiedName] = names.toList()
registerClassScopeFieldType(outerClassName, declaredName, qualifiedName)
if (lifted && outerClassName != null) {
val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
for (entry in names) {
outerCtx?.classScopeMembers?.add(entry)
registerClassScopeMember(outerClassName, entry)
registerClassScopeFieldType(outerClassName, entry, qualifiedName)
}
} else if (lifted) {
for (entry in names) {
declareLocalName(entry, isMutable = false)
}
}
val stmtPos = startPos
return EnumDeclStatement(
declaredName = declaredName,
qualifiedName = qualifiedName,
entries = names.toList(),
lifted = lifted,
startPos = 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 declaredName = nameToken?.value
val outerClassName = currentEnclosingClassName()
val baseName = declaredName ?: generateAnonName(startPos)
val className = if (declaredName != null && outerClassName != null) "$outerClassName.$declaredName" else baseName
if (declaredName != null) {
val namePos = nameToken.pos
resolutionSink?.declareSymbol(declaredName, SymbolKind.CLASS, isMutable = false, pos = namePos)
declareLocalName(declaredName, isMutable = false)
if (codeContexts.lastOrNull() is CodeContext.Module) {
objectDeclNames.add(declaredName)
}
if (outerClassName != null) {
val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
}
}
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()
}
argsList = wrapParsedArgsBytecode(argsList)
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 = declaredName ?: 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 classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody
classCtx?.let { ctx ->
val callableMembers = classScopeCallableMembersByClassName.getOrPut(className) { mutableSetOf() }
predeclareClassScopeMembers(className, ctx.classScopeMembers, callableMembers)
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)
}
val st = try {
val parsed = withLocalNames(emptySet()) {
parseScript()
}
if (!isExtern) {
classCtx?.let { ctx ->
compileClassInfos[className] = CompileClassInfo(
name = className,
fieldIds = ctx.memberFieldIds.toMap(),
methodIds = ctx.memberMethodIds.toMap(),
nextFieldId = ctx.nextFieldId,
nextMethodId = ctx.nextMethodId,
baseNames = baseSpecs.map { it.name }
)
}
}
if (declaredName != null) {
registerClassScopeFieldType(outerClassName, declaredName, className)
}
parsed
} 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 = declaredName ?: 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)
if (!isExtern) {
val baseIds = collectBaseMemberIds(baseSpecs.map { it.name })
compileClassInfos[className] = CompileClassInfo(
name = className,
fieldIds = baseIds.fieldIds,
methodIds = baseIds.methodIds,
nextFieldId = baseIds.nextFieldId,
nextMethodId = baseIds.nextMethodId,
baseNames = baseSpecs.map { it.name }
)
}
if (declaredName != null) {
registerClassScopeFieldType(outerClassName, declaredName, className)
}
cc.restorePos(saved)
null
}
}
val initScope = popInitScope()
val wrappedBodyInit = bodyInit?.let { wrapClassBodyBytecode(it, "object@$className") }
val spec = ClassDeclSpec(
declaredName = declaredName,
className = className,
typeName = className,
startPos = startPos,
isExtern = false,
isAbstract = false,
isClosed = false,
isObject = true,
isAnonymous = nameToken == null,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
constructorArgs = null,
constructorFieldIds = null,
bodyInit = wrappedBodyInit,
initScope = emptyList()
)
return ClassDeclStatement(spec)
}
private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false, isClosed: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
val declaredName = nameToken.value
val outerClassName = currentEnclosingClassName()
val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName
resolutionSink?.declareSymbol(declaredName, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos)
if (codeContexts.lastOrNull() is CodeContext.Module) {
declareLocalName(declaredName, isMutable = false)
}
if (codeContexts.lastOrNull() is CodeContext.Module) {
scopeSeedNames.add(declaredName)
}
if (outerClassName != null) {
val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
outerCtx?.classScopeMembers?.add(declaredName)
registerClassScopeMember(outerClassName, declaredName)
registerClassScopeCallableMember(outerClassName, declaredName)
}
return inCodeContext(CodeContext.ClassBody(qualifiedName, isExtern = isExtern)) {
val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody
val typeParamDecls = parseTypeParamList()
classCtx?.typeParamDecls = typeParamDecls
val classTypeParams = typeParamDecls.map { it.name }.toSet()
classCtx?.typeParams = classTypeParams
pendingTypeParamStack.add(classTypeParams)
var constructorArgsDeclaration: ArgsDeclaration?
try {
constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
} finally {
pendingTypeParamStack.removeLast()
}
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType != null) {
val declClass = resolveTypeDeclObjClass(param.type)
if (declClass != null) {
classFieldTypesByName.getOrPut(qualifiedName) { mutableMapOf() }[param.name] = declClass
}
}
}
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)
}
val ctorForcedLocalSlots = LinkedHashMap<String, Int>()
if (constructorArgsDeclaration != null) {
val ctorDecl = constructorArgsDeclaration
val snapshot = slotPlanIndices(classSlotPlan)
for (param in ctorDecl.params) {
val idx = snapshot[param.name] ?: continue
ctorForcedLocalSlots[param.name] = idx
}
constructorArgsDeclaration =
wrapDefaultArgsBytecode(ctorDecl, ctorForcedLocalSlots, classSlotPlan.id)
}
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>()
pendingTypeParamStack.add(classTypeParams)
slotPlanStack.add(classSlotPlan)
try {
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
do {
val (baseDecl, _) = parseSimpleTypeExpressionWithMini()
val baseName = when (baseDecl) {
is TypeDecl.Simple -> baseDecl.name
is TypeDecl.Generic -> baseDecl.name
else -> throw ScriptError(cc.currentPos(), "base class name expected")
}
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()
}
argsList = wrapParsedArgsBytecode(argsList, ctorForcedLocalSlots, classSlotPlan.id)
baseSpecs += BaseSpec(baseName, argsList)
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
}
} finally {
slotPlanStack.removeLast()
pendingTypeParamStack.removeLast()
}
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 = declaredName,
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(qualifiedName, baseSpecs.map { it.name }, startPos)
resolutionSink?.enterScope(ScopeKind.CLASS, startPos, qualifiedName, 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 ->
val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() }
predeclareClassScopeMembers(qualifiedName, ctx.classScopeMembers, callableMembers)
predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides)
val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null
if (existingExternInfo != null) {
ctx.memberFieldIds.putAll(existingExternInfo.fieldIds)
ctx.memberMethodIds.putAll(existingExternInfo.methodIds)
ctx.nextFieldId = maxOf(ctx.nextFieldId, existingExternInfo.nextFieldId)
ctx.nextMethodId = maxOf(ctx.nextMethodId, existingExternInfo.nextMethodId)
for (member in ctx.declaredMembers) {
val hasField = member in existingExternInfo.fieldIds
val hasMethod = member in existingExternInfo.methodIds
if (!hasField && !hasMethod) {
throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class $qualifiedName")
}
}
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType == null) return@forEach
if (!ctx.memberFieldIds.containsKey(param.name)) {
val fieldId = existingExternInfo.fieldIds[param.name]
?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class $qualifiedName")
ctx.memberFieldIds[param.name] = fieldId
}
}
compileClassInfos[qualifiedName] = existingExternInfo
} else {
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")
}
}
}
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType == null) return@forEach
if (!ctx.memberFieldIds.containsKey(param.name)) {
ctx.memberFieldIds[param.name] = ctx.nextFieldId++
}
}
compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName,
fieldIds = ctx.memberFieldIds.toMap(),
methodIds = ctx.memberMethodIds.toMap(),
nextFieldId = ctx.nextFieldId,
nextMethodId = ctx.nextMethodId,
baseNames = baseSpecs.map { it.name }
)
}
}
val parsed = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
parseScript()
}
if (!isExtern) {
classCtx?.let { ctx ->
compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName,
fieldIds = ctx.memberFieldIds.toMap(),
methodIds = ctx.memberMethodIds.toMap(),
nextFieldId = ctx.nextFieldId,
nextMethodId = ctx.nextMethodId,
baseNames = baseSpecs.map { it.name }
)
}
}
registerClassScopeFieldType(outerClassName, declaredName, qualifiedName)
parsed
} 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 = declaredName,
bases = baseSpecs.map { it.name },
bodyRange = null,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern
)
miniSink?.onClassDecl(node)
}
resolutionSink?.declareClass(qualifiedName, baseSpecs.map { it.name }, startPos)
classCtx?.let { ctx ->
val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null
if (existingExternInfo != null) {
ctx.memberFieldIds.putAll(existingExternInfo.fieldIds)
ctx.memberMethodIds.putAll(existingExternInfo.methodIds)
ctx.nextFieldId = maxOf(ctx.nextFieldId, existingExternInfo.nextFieldId)
ctx.nextMethodId = maxOf(ctx.nextMethodId, existingExternInfo.nextMethodId)
for (member in ctx.declaredMembers) {
val hasField = member in existingExternInfo.fieldIds
val hasMethod = member in existingExternInfo.methodIds
if (!hasField && !hasMethod) {
throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class $qualifiedName")
}
}
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType == null) return@forEach
if (!ctx.memberFieldIds.containsKey(param.name)) {
val fieldId = existingExternInfo.fieldIds[param.name]
?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class $qualifiedName")
ctx.memberFieldIds[param.name] = fieldId
}
}
compileClassInfos[qualifiedName] = existingExternInfo
} else {
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")
}
}
}
constructorArgsDeclaration?.params?.forEach { param ->
if (param.accessType == null) return@forEach
if (!ctx.memberFieldIds.containsKey(param.name)) {
ctx.memberFieldIds[param.name] = ctx.nextFieldId++
}
}
compileClassInfos[qualifiedName] = CompileClassInfo(
name = qualifiedName,
fieldIds = ctx.memberFieldIds.toMap(),
methodIds = ctx.memberMethodIds.toMap(),
nextFieldId = ctx.nextFieldId,
nextMethodId = ctx.nextMethodId,
baseNames = baseSpecs.map { it.name }
)
}
}
registerClassScopeFieldType(outerClassName, declaredName, qualifiedName)
// restore if no body starts here
cc.restorePos(saved)
null
}
}
val initScope = popInitScope()
val wrappedBodyInit = bodyInit?.let { wrapClassBodyBytecode(it, "class@$qualifiedName") }
val wrappedInitScope = initScope.map { wrapClassBodyBytecode(it, "classInit@$qualifiedName") }
// create class
val className = qualifiedName
// @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 classInfo = compileClassInfos[className]
val spec = ClassDeclSpec(
declaredName = declaredName,
className = className,
typeName = className,
startPos = startPos,
isExtern = isExtern,
isAbstract = isAbstract,
isClosed = isClosed,
isObject = false,
isAnonymous = false,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
constructorArgs = constructorArgsDeclaration,
constructorFieldIds = classInfo?.fieldIds,
bodyInit = wrappedBodyInit,
initScope = wrappedInitScope
)
ClassDeclStatement(spec)
}
}
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,
loopScopeId = loopSlotPlan.id,
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
if (range.step != null && !range.step.isNull) 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 -> {
if (ref.step != null) return null
val start = constIntValueOrNull(ref.left) ?: return null
val end = constIntValueOrNull(ref.right) ?: return null
val endExclusive = if (ref.isEndInclusive) end + 1 else end
return ConstIntRange(start, endExclusive)
}
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 data class SmartCastTarget(
val name: String? = null,
val scopeId: Int? = null,
val slot: Int? = null,
val typeDecl: TypeDecl,
)
private data class SmartCastRestore(
val name: String?,
val nameHad: Boolean,
val namePrev: TypeDecl?,
val scopeId: Int?,
val slot: Int?,
val slotHad: Boolean,
val slotPrev: TypeDecl?,
val slotMapCreated: Boolean,
)
private fun smartCastTargetFromRef(ref: ObjRef, typeDecl: TypeDecl): SmartCastTarget? = when (ref) {
is LocalSlotRef -> {
val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId
val ownerSlot = ref.captureOwnerSlot ?: ref.slot
SmartCastTarget(scopeId = ownerScopeId, slot = ownerSlot, typeDecl = typeDecl)
}
is LocalVarRef -> SmartCastTarget(name = ref.name, typeDecl = typeDecl)
is FastLocalVarRef -> SmartCastTarget(name = ref.name, typeDecl = typeDecl)
else -> null
}
private fun extractSmartCasts(condition: Statement): Pair<List<SmartCastTarget>, List<SmartCastTarget>> {
val ref = unwrapDirectRef(condition) ?: return emptyList<SmartCastTarget>() to emptyList()
if (ref !is BinaryOpRef) return emptyList<SmartCastTarget>() to emptyList()
if (ref.op != BinOp.IS && ref.op != BinOp.NOTIS) return emptyList<SmartCastTarget>() to emptyList()
val typeRef = ref.right as? net.sergeych.lyng.obj.TypeDeclRef ?: return emptyList<SmartCastTarget>() to emptyList()
val typeDecl = expandTypeAliases(typeRef.decl(), typeRef.pos())
val target = smartCastTargetFromRef(ref.left, typeDecl) ?: return emptyList<SmartCastTarget>() to emptyList()
return if (ref.op == BinOp.IS) {
listOf(target) to emptyList()
} else {
emptyList<SmartCastTarget>() to listOf(target)
}
}
private fun applySmartCasts(overrides: List<SmartCastTarget>): List<SmartCastRestore> {
if (overrides.isEmpty()) return emptyList()
val restores = ArrayList<SmartCastRestore>(overrides.size)
for (override in overrides) {
if (override.name != null) {
val had = nameTypeDecl.containsKey(override.name)
val prev = nameTypeDecl[override.name]
nameTypeDecl[override.name] = override.typeDecl
restores.add(
SmartCastRestore(
name = override.name,
nameHad = had,
namePrev = prev,
scopeId = null,
slot = null,
slotHad = false,
slotPrev = null,
slotMapCreated = false,
)
)
} else if (override.scopeId != null && override.slot != null) {
val map = slotTypeDeclByScopeId[override.scopeId]
val mapCreated = map == null
val targetMap = map ?: mutableMapOf<Int, TypeDecl>().also { slotTypeDeclByScopeId[override.scopeId] = it }
val had = targetMap.containsKey(override.slot)
val prev = targetMap[override.slot]
targetMap[override.slot] = override.typeDecl
restores.add(
SmartCastRestore(
name = null,
nameHad = false,
namePrev = null,
scopeId = override.scopeId,
slot = override.slot,
slotHad = had,
slotPrev = prev,
slotMapCreated = mapCreated,
)
)
}
}
return restores
}
private fun restoreSmartCasts(restores: List<SmartCastRestore>) {
if (restores.isEmpty()) return
for (restore in restores.asReversed()) {
if (restore.name != null) {
if (restore.nameHad) {
nameTypeDecl[restore.name] = restore.namePrev!!
} else {
nameTypeDecl.remove(restore.name)
}
} else if (restore.scopeId != null && restore.slot != null) {
val map = slotTypeDeclByScopeId[restore.scopeId]
if (map != null) {
if (restore.slotHad) {
map[restore.slot] = restore.slotPrev!!
} else {
map.remove(restore.slot)
}
if (restore.slotMapCreated && map.isEmpty()) {
slotTypeDeclByScopeId.remove(restore.scopeId)
}
}
}
}
}
private suspend fun parseIfStatement(): Statement {
val start = ensureLparen()
val condition = parseExpression()
?: throw ScriptError(start, "Bad if statement: expected expression")
val pos = ensureRparen()
val (trueCasts, falseCasts) = extractSmartCasts(condition)
val ifRestores = applySmartCasts(trueCasts)
val ifBody = try {
parseStatement() ?: throw ScriptError(pos, "Bad if statement: expected statement")
} finally {
restoreSmartCasts(ifRestores)
}
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 elseRestores = applySmartCasts(falseCasts)
val elseBody = try {
parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement")
} finally {
restoreSmartCasts(elseRestores)
}
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 start = cc.currentPos()
var extTypeName: String? = null
var receiverTypeDecl: TypeDecl? = null
var name: String
var nameStartPos: Pos
var receiverMini: MiniTypeRef? = null
val annotation = lastAnnotation
val parentContext = codeContexts.last()
// Is extension?
if (looksLikeExtensionReceiver()) {
val (recvDecl, recvMini) = parseExtensionReceiverTypeWithMini()
receiverTypeDecl = recvDecl
receiverMini = recvMini
val dot = cc.nextNonWhitespace()
if (dot.type != Token.Type.DOT) {
throw ScriptError(dot.pos, "illegal extension format: expected '.' after receiver type")
}
val 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
extTypeName = when (recvDecl) {
is TypeDecl.Simple -> recvDecl.name.substringAfterLast('.')
is TypeDecl.Generic -> recvDecl.name.substringAfterLast('.')
else -> throw ScriptError(
recvMini.range.start,
"illegal extension receiver type: ${typeDeclName(recvDecl)}"
)
}
registerExtensionName(extTypeName, name)
} else {
val t = cc.next()
if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "Expected identifier after 'fun'")
start = t.pos
name = t.value
nameStartPos = t.pos
}
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var 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) {
if (isStatic) {
parentContext.classScopeMembers.add(name)
registerClassScopeMember(parentContext.name, name)
registerClassScopeCallableMember(parentContext.name, name)
} else {
parentContext.declaredMembers.add(name)
if (!parentContext.memberMethodIds.containsKey(name)) {
parentContext.memberMethodIds[name] = parentContext.nextMethodId++
}
memberMethodId = parentContext.memberMethodIds[name]
}
}
if (declKind != SymbolKind.MEMBER) {
declareLocalName(name, isMutable = false)
}
if (extensionWrapperName != null) {
declareLocalName(extensionWrapperName, isMutable = false)
}
if (actualExtern && declKind != SymbolKind.MEMBER) {
externCallableNames.add(name)
}
if (actualExtern && extensionWrapperName != null) {
externCallableNames.add(extensionWrapperName)
}
val declSlotPlan = if (declKind != SymbolKind.MEMBER) slotPlanStack.lastOrNull() else null
val declSlotIndex = declSlotPlan?.slots?.get(name)?.index
val declScopeId = declSlotPlan?.id
val typeParamDecls = parseTypeParamList()
val explicitTypeParams = typeParamDecls.map { it.name }.toSet()
val receiverNormalization = normalizeReceiverTypeDecl(receiverTypeDecl, explicitTypeParams)
receiverTypeDecl = receiverNormalization.first
val implicitTypeParams = receiverNormalization.second
val mergedTypeParamDecls = if (implicitTypeParams.isEmpty()) {
typeParamDecls
} else {
typeParamDecls + implicitTypeParams.filter { it !in explicitTypeParams }
.map { TypeDecl.TypeParam(it) }
}
val typeParams = mergedTypeParamDecls.map { it.name }.toSet()
pendingTypeParamStack.add(typeParams)
var argsDeclaration: ArgsDeclaration
val returnTypeMini: MiniTypeRef?
val returnTypeDecl: TypeDecl?
try {
argsDeclaration =
if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.nextNonWhitespace() // consume (
parseArgsDeclaration() ?: ArgsDeclaration(emptyList(), Token.Type.RPAREN)
} else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
if (mergedTypeParamDecls.isNotEmpty() && declKind != SymbolKind.MEMBER) {
currentGenericFunctionDecls()[name] = GenericFunctionDecl(mergedTypeParamDecls, argsDeclaration.params, nameStartPos)
}
// Optional return type
val parsedReturn = if (cc.peekNextNonWhitespace().type == Token.Type.COLON) {
parseTypeDeclarationWithMini()
} else null
returnTypeMini = parsedReturn?.second
returnTypeDecl = parsedReturn?.first
} finally {
pendingTypeParamStack.removeLast()
}
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 (compileBytecode) {
delegateExpression = wrapFunctionBytecode(delegateExpression, "delegate@$name")
}
}
if (isDelegated && declKind != SymbolKind.MEMBER) {
val plan = slotPlanStack.lastOrNull()
val entry = plan?.slots?.get(name)
if (entry != null && !entry.isDelegated) {
plan.slots[name] = SlotEntry(entry.index, entry.isMutable, isDelegated = true)
}
}
if (!isDelegated && argsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(
nameStartPos,
"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)
val implicitThisTypeName = when {
extTypeName != null -> extTypeName
parentContext is CodeContext.ClassBody && !isStatic -> parentContext.name
else -> null
}
return inCodeContext(
CodeContext.Function(
name,
implicitThisMembers = implicitThisMembers,
implicitThisTypeName = implicitThisTypeName,
typeParams = typeParams,
typeParamDecls = typeParamDecls
)
) {
cc.labels.add(name)
outerLabel?.let { cc.labels.add(it) }
val paramNamesList = argsDeclaration.params.map { it.name }
val typeParamNames = mergedTypeParamDecls.map { it.name }
val paramNames: Set<String> = paramNamesList.toSet()
val paramSlotPlan = buildParamSlotPlan(paramNamesList + typeParamNames)
val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan)
val forcedLocalSlots = LinkedHashMap<String, Int>()
for (name in paramNamesList) {
val idx = paramSlotPlanSnapshot[name] ?: continue
forcedLocalSlots[name] = idx
}
for (name in typeParamNames) {
val idx = paramSlotPlanSnapshot[name] ?: continue
forcedLocalSlots[name] = idx
}
argsDeclaration = wrapDefaultArgsBytecode(argsDeclaration, forcedLocalSlots, paramSlotPlan.id)
val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = false)
val rangeParamNames = argsDeclaration.params
.filter { isRangeType(it.type) }
.map { it.name }
.toSet()
val paramTypeMap = slotTypeByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
val paramTypeDeclMap = slotTypeDeclByScopeId.getOrPut(paramSlotPlan.id) { mutableMapOf() }
for (param in argsDeclaration.params) {
val cls = resolveTypeDeclObjClass(param.type) ?: continue
val slot = paramSlotPlan.slots[param.name]?.index ?: continue
paramTypeMap[slot] = cls
}
for (param in argsDeclaration.params) {
val slot = paramSlotPlan.slots[param.name]?.index ?: continue
paramTypeDeclMap[slot] = param.type
}
// 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) {
val msg = ObjString("extern function not provided: $name").asReadonly
val expr = ExpressionStatement(ConstRef(msg), start)
ThrowStatement(expr, start)
}
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 { unwrapBytecodeDeep(it) }
val inferredReturnClass = returnTypeDecl?.let { resolveTypeDeclObjClass(it) }
?: inferReturnClassFromStatement(rawFnStatements)
if (declKind != SymbolKind.MEMBER && inferredReturnClass != null) {
callableReturnTypeByName[name] = inferredReturnClass
val slotLoc = lookupSlotLocation(name, includeModule = true)
if (slotLoc != null) {
callableReturnTypeByScopeId.getOrPut(slotLoc.scopeId) { mutableMapOf() }[slotLoc.slot] =
inferredReturnClass
}
}
val fnStatements = rawFnStatements?.let { stmt ->
if (!compileBytecode) return@let stmt
val paramKnownClasses = mutableMapOf<String, ObjClass>()
for (param in argsDeclaration.params) {
val cls = resolveTypeDeclObjClass(param.type) ?: continue
paramKnownClasses[param.name] = cls
}
wrapFunctionBytecode(
stmt,
name,
paramKnownClasses,
forcedLocalSlots = forcedLocalSlots,
forcedLocalScopeId = paramSlotPlan.id
)
}
// Capture and pop the local declarations count for this function
val fnLocalDecls = localDeclCountStack.removeLastOrNull() ?: 0
val closureBox = FunctionClosureBox()
val captureSlots = capturePlan.captures.toList()
val fnBody = object : Statement(), BytecodeBodyProvider {
override val pos: Pos = start
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
override suspend fun execute(scope: Scope): Obj {
scope.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 = closureBox.closure?.let { closure ->
scope.applyClosureForBytecode(closure).also {
it.args = scope.args
}
} ?: scope
// Capacity hint: parameters + declared locals + small overhead
val capacityHint = paramNames.size + fnLocalDecls + 4
context.hintLocalCapacity(capacityHint)
val captureBase = closureBox.captureContext ?: closureBox.closure
val bytecodeBody = (fnStatements as? BytecodeStatement)
?: context.raiseIllegalState("non-bytecode function body encountered")
val bytecodeFn = bytecodeBody.bytecodeFunction()
val captureNames = if (captureSlots.isNotEmpty()) {
captureSlots.map { it.name }
} else {
val fn = bytecodeFn
val names = fn.localSlotNames
val captures = fn.localSlotCaptures
val ordered = LinkedHashSet<String>()
for (i in names.indices) {
if (captures.getOrNull(i) != true) continue
val name = names[i] ?: continue
ordered.add(name)
}
ordered.toList()
}
val prebuiltCaptures = closureBox.captureRecords
if (prebuiltCaptures != null && captureNames.isNotEmpty()) {
context.captureRecords = prebuiltCaptures
context.captureNames = captureNames
} else if (captureBase != null && captureNames.isNotEmpty()) {
val resolvedRecords = ArrayList<ObjRecord>(captureNames.size)
for (name in captureNames) {
val rec = captureBase.chainLookupIgnoreClosure(
name,
followClosure = true,
caller = context.currentClassCtx
) ?: captureBase.raiseSymbolNotFound("symbol $name not found")
resolvedRecords.add(rec)
}
context.captureRecords = resolvedRecords
context.captureNames = captureNames
}
val binder: suspend (net.sergeych.lyng.bytecode.CmdFrame, Arguments) -> Unit = { frame, arguments ->
val slotPlan = bytecodeFn.localSlotPlanByName()
argsDeclaration.assignToFrame(
context,
arguments,
slotPlan,
frame.frame
)
val typeBindings = bindTypeParamsAtRuntime(context, argsDeclaration, mergedTypeParamDecls)
if (typeBindings.isNotEmpty()) {
for ((name, bound) in typeBindings) {
val slot = slotPlan[name] ?: continue
frame.frame.setObj(slot, bound)
}
}
if (extTypeName != null) {
context.thisObj = scope.thisObj
}
}
return try {
net.sergeych.lyng.bytecode.CmdVm().execute(bytecodeFn, context, scope.args, binder)
} 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) }
val parentIsClassBody = parentContext is CodeContext.ClassBody
val delegateInitStatement = if (isDelegated && parentIsClassBody && !isStatic && extTypeName == null) {
val className = currentEnclosingClassName()
?: throw ScriptError(start, "delegated function outside class body")
val initExpr = delegateExpression ?: throw ScriptError(start, "delegated function missing delegate")
val storageName = "$className::$name"
val initStatement = InstanceDelegatedInitStatement(
storageName = storageName,
memberName = name,
isMutable = false,
visibility = visibility,
writeVisibility = null,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
accessTypeLabel = "Callable",
initializer = initExpr,
pos = start
)
wrapInstanceInitBytecode(initStatement, "fnDelegate@${storageName}")
} else null
val spec = FunctionDeclSpec(
name = name,
visibility = visibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isStatic = isStatic,
isTransient = isTransient,
isDelegated = isDelegated,
delegateExpression = delegateExpression,
delegateInitStatement = delegateInitStatement,
extTypeName = extTypeName,
extensionWrapperName = extensionWrapperName,
memberMethodId = memberMethodId,
actualExtern = actualExtern,
parentIsClassBody = parentIsClassBody,
externCallSignature = externCallSignature,
annotation = annotation,
fnBody = fnBody,
closureBox = closureBox,
captureSlots = captureSlots,
slotIndex = declSlotIndex,
scopeId = declScopeId,
startPos = start
)
val declaredFn = FunctionDeclStatement(spec)
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 suspend fun parseExpressionWithPredeclared(
predeclared: List<Pair<String, Boolean>>
): Statement {
val startPos = cc.currentPos()
resolutionSink?.enterScope(ScopeKind.BLOCK, startPos, null)
val exprSlotPlan = SlotPlan(mutableMapOf(), 0, nextScopeId++)
for ((name, isMutable) in predeclared) {
declareSlotNameIn(exprSlotPlan, name, isMutable, isDelegated = false)
resolutionSink?.declareSymbol(name, SymbolKind.LOCAL, isMutable, startPos, isOverride = false)
}
slotPlanStack.add(exprSlotPlan)
val capturePlan = CapturePlan(exprSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0)
capturePlanStack.add(capturePlan)
val expr = try {
parseExpression() ?: throw ScriptError(cc.current().pos, "Expected expression")
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
resolutionSink?.exitScope(cc.currentPos())
return expr
}
private suspend fun parseExpressionBlockWithPredeclared(
predeclared: List<Pair<String, Boolean>>
): Statement {
val startPos = cc.currentPos()
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, isFunction = false, propagateToParentFunction = lambdaDepth > 0)
capturePlanStack.add(capturePlan)
val expr = try {
parseExpression() ?: throw ScriptError(cc.current().pos, "Expected expression")
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val block = Script(startPos, listOf(expr))
val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, capturePlan.captures.toList(), startPos)
resolutionSink?.exitScope(cc.currentPos())
return stmt
}
private fun inferReturnClassFromStatement(stmt: Statement?): ObjClass? {
if (stmt == null) return null
val unwrapped = unwrapBytecodeDeep(stmt)
return when (unwrapped) {
is ExpressionStatement -> resolveInitializerObjClass(unwrapped)
is ReturnStatement -> resolveInitializerObjClass(unwrapped.resultExpr)
is VarDeclStatement -> unwrapped.initializerObjClass ?: resolveInitializerObjClass(unwrapped.initializer)
is BlockStatement -> {
val stmts = unwrapped.statements()
val returnTypes = stmts.mapNotNull { s ->
(s as? ReturnStatement)?.let { resolveInitializerObjClass(it.resultExpr) }
}
if (returnTypes.isNotEmpty()) {
val first = returnTypes.first()
if (returnTypes.all { it == first }) first else Obj.rootObjectType
} else {
val last = stmts.lastOrNull()
inferReturnClassFromStatement(last)
}
}
is InlineBlockStatement -> {
val stmts = unwrapped.statements()
val last = stmts.lastOrNull()
inferReturnClassFromStatement(last)
}
is IfStatement -> {
val ifType = inferReturnClassFromStatement(unwrapped.ifBody)
val elseType = unwrapped.elseBody?.let { inferReturnClassFromStatement(it) }
when {
ifType == null && elseType == null -> null
ifType != null && elseType != null && ifType == elseType -> ifType
else -> Obj.rootObjectType
}
}
else -> null
}
}
private fun unwrapDirectRef(initializer: Statement?): ObjRef? {
var initStmt = initializer
while (initStmt is BytecodeStatement) {
initStmt = initStmt.original
}
val initRef = (initStmt as? ExpressionStatement)?.ref
return when (initRef) {
is StatementRef -> (initRef.statement as? ExpressionStatement)?.ref ?: initRef
else -> initRef
}
}
private fun resolveInitializerObjClass(initializer: Statement?): ObjClass? {
var unwrapped = initializer
while (unwrapped is BytecodeStatement) {
val fn = unwrapped.bytecodeFunction()
if (fn.cmds.any { it is CmdListLiteral }) return ObjList.type
if (fn.cmds.any { it is CmdMakeRange || it is CmdRangeIntBounds }) return ObjRange.type
unwrapped = unwrapped.original
}
if (unwrapped is DoWhileStatement) {
val bodyType = inferReturnClassFromStatement(unwrapped.body)
val elseType = unwrapped.elseStatement?.let { inferReturnClassFromStatement(it) }
return when {
bodyType == null && elseType == null -> null
bodyType != null && elseType != null && bodyType == elseType -> bodyType
bodyType != null && elseType == null -> bodyType
bodyType == null -> elseType
else -> Obj.rootObjectType
}
}
if (unwrapped is ClassDeclStatement) {
return resolveClassByName(unwrapped.typeName)
}
val directRef = unwrapDirectRef(unwrapped)
return when (directRef) {
is ConstRef -> when (directRef.constValue) {
is ObjInt -> ObjInt.type
is ObjReal -> ObjReal.type
is ObjBool -> ObjBool.type
is ObjString -> ObjString.type
is ObjRange -> ObjRange.type
is ObjList -> ObjList.type
is ObjMap -> ObjMap.type
is ObjChar -> ObjChar.type
is ObjNull -> Obj.rootObjectType
is ObjVoid -> ObjVoid.objClass
else -> null
}
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
is StatementRef -> {
val decl = directRef.statement as? ClassDeclStatement
decl?.let { resolveClassByName(it.typeName) }
}
is ValueFnRef -> lambdaReturnTypeByRef[directRef]
is ClassOperatorRef -> lambdaReturnTypeByRef[directRef]
is CastRef -> resolveTypeRefClass(directRef.castTypeRef())
is BinaryOpRef -> inferBinaryOpReturnClass(directRef)
is ImplicitThisMethodCallRef -> {
when (directRef.methodName()) {
"iterator" -> ObjIterator
"lazy" -> ObjLazyDelegate.type
else -> inferMethodCallReturnClass(directRef.methodName())
}
}
is ThisMethodSlotCallRef -> {
if (directRef.methodName() == "iterator") ObjIterator else null
}
is MethodCallRef -> {
inferMethodCallReturnClass(directRef)
}
is FieldRef -> {
val targetClass = resolveReceiverClassForMember(directRef.target)
inferFieldReturnClass(targetClass, directRef.name)
}
is CallRef -> {
val target = directRef.target
when {
target is LocalSlotRef -> {
when (target.name) {
"lazy" -> ObjLazyDelegate.type
"iterator" -> ObjIterator
"flow" -> ObjFlow.type
"launch" -> ObjDeferred.type
"dynamic" -> ObjDynamic.type
else -> callableReturnTypeByScopeId[target.scopeId]?.get(target.slot)
?: resolveClassByName(target.name)
}
}
target is LocalVarRef -> {
when (target.name) {
"lazy" -> ObjLazyDelegate.type
"iterator" -> ObjIterator
"flow" -> ObjFlow.type
"launch" -> ObjDeferred.type
"dynamic" -> ObjDynamic.type
else -> callableReturnTypeByName[target.name]
?: resolveClassByName(target.name)
}
}
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 FieldRef -> {
val receiverClass = resolveReceiverClassForMember(target.target) ?: return null
if (!isClassScopeCallableMember(receiverClass.className, target.name)) return null
resolveClassByName("${receiverClass.className}.${target.name}")
}
target is ConstRef -> when (val value = target.constValue) {
is ObjClass -> value
is ObjString -> ObjString.type
else -> null
}
else -> null
}
}
else -> null
}
}
private fun ensureDelegateType(initializer: Statement) {
val delegateClass = resolveClassByName("Delegate")
?: throw ScriptError(initializer.pos, "Delegate type is not available")
val initClass = resolveInitializerObjClass(initializer)
?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) }
?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time")
if (initClass !== delegateClass &&
!initClass.allParentsSet.contains(delegateClass) &&
!initClass.allImplementingNames.contains(delegateClass.className)
) {
throw ScriptError(
initializer.pos,
"Delegate initializer must return Delegate, got ${initClass.className}"
)
}
}
private fun resolveTypeDeclObjClass(type: TypeDecl): ObjClass? {
val rawName = when (type) {
is TypeDecl.Simple -> type.name
is TypeDecl.Generic -> type.name
is TypeDecl.Function -> "Callable"
is TypeDecl.Ellipsis -> return resolveTypeDeclObjClass(type.elementType)
is TypeDecl.TypeVar -> return null
is TypeDecl.Union -> return null
is TypeDecl.Intersection -> return null
else -> return null
}
val name = rawName.substringAfterLast('.')
if (!rawName.contains('.')) {
val classCtx = currentEnclosingClassName()
if (classCtx != null) {
resolveClassByName("$classCtx.$rawName")?.let { return it }
}
}
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
"Instant" -> ObjInstant.type
"DateTime" -> ObjDateTime.type
"Duration" -> ObjDuration.type
"Buffer" -> ObjBuffer.type
"MutableBuffer" -> ObjMutableBuffer.type
"RingBuffer" -> ObjRingBuffer.type
"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 }
for (module in importedModules.asReversed()) {
val imported = module.scope.get(name)
(imported?.value as? ObjClass)?.let { return it }
}
val info = compileClassInfos[name] ?: return null
val stub = compileClassStubs.getOrPut(info.name) {
val parents = info.baseNames.mapNotNull { resolveClassByName(it) }
val closedParent = parents.firstOrNull { it.isClosed }
if (closedParent != null) {
throw ScriptError(Pos.builtIn, "can't inherit from closed class ${closedParent.className}")
}
ObjInstanceClass(info.name, *parents.toTypedArray())
}
if (stub is ObjInstanceClass) {
for ((fieldName, fieldId) in info.fieldIds) {
if (stub.members[fieldName] == null) {
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) {
if (stub.members[methodName] == null) {
stub.createField(
methodName,
ObjNull,
isMutable = false,
visibility = Visibility.Public,
pos = Pos.builtIn,
declaringClass = stub,
isAbstract = true,
type = ObjRecord.Type.Fun,
methodId = methodId
)
}
}
}
return stub
}
private suspend fun parseBlockWithPredeclared(
predeclared: List<Pair<String, Boolean>>,
skipLeadingBrace: Boolean = false,
predeclaredTypes: Map<String, ObjClass> = emptyMap()
): 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)
resolutionSink?.declareSymbol(name, SymbolKind.LOCAL, isMutable, startPos, isOverride = false)
}
if (predeclaredTypes.isNotEmpty()) {
val typeMap = slotTypeByScopeId.getOrPut(blockSlotPlan.id) { mutableMapOf() }
for ((name, cls) in predeclaredTypes) {
val slot = blockSlotPlan.slots[name]?.index ?: continue
typeMap[slot] = cls
}
}
slotPlanStack.add(blockSlotPlan)
val capturePlan = CapturePlan(blockSlotPlan, isFunction = false, propagateToParentFunction = lambdaDepth > 0)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, 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, isFunction = false, propagateToParentFunction = lambdaDepth > 0)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, 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, isFunction = false, propagateToParentFunction = lambdaDepth > 0)
capturePlanStack.add(capturePlan)
val block = try {
parseScript()
} finally {
capturePlanStack.removeLast()
slotPlanStack.removeLast()
}
val planSnapshot = slotPlanIndices(blockSlotPlan)
val stmt = BlockStatement(block, planSnapshot, blockSlotPlan.id, 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 markStart = cc.savePos()
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 = parseDestructuringPattern()
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
)
}
cc.restorePos(markStart)
var name: String
var extTypeName: String? = null
var nameStartPos: Pos
var receiverMini: MiniTypeRef? = null
var receiverTypeDecl: TypeDecl? = null
if (looksLikeExtensionReceiver()) {
val (recvDecl, recvMini) = parseExtensionReceiverTypeWithMini()
receiverTypeDecl = recvDecl
receiverMini = recvMini
val dot = cc.nextNonWhitespace()
if (dot.type != Token.Type.DOT)
throw ScriptError(dot.pos, "Expected '.' after extension receiver type")
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
extTypeName = when (recvDecl) {
is TypeDecl.Simple -> recvDecl.name.substringAfterLast('.')
is TypeDecl.Generic -> recvDecl.name.substringAfterLast('.')
else -> throw ScriptError(
recvMini.range.start,
"illegal extension receiver type: ${typeDeclName(recvDecl)}"
)
}
registerExtensionName(extTypeName, name)
} else {
val nameToken = cc.next()
if (nameToken.type != Token.Type.ID)
throw ScriptError(nameToken.pos, "Expected identifier or [ here")
name = nameToken.value
nameStartPos = nameToken.pos
}
val receiverNormalization = normalizeReceiverTypeDecl(receiverTypeDecl, emptySet())
val implicitTypeParams = receiverNormalization.second
if (implicitTypeParams.isNotEmpty()) pendingTypeParamStack.add(implicitTypeParams)
try {
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var memberFieldId = if (extTypeName == null) classCtx?.memberFieldIds?.get(name) else null
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
// Optional explicit type annotation
cc.skipWsTokens()
var (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 && declaringClassNameCaptured == null) 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
}
if (declaringClassNameCaptured != null && extTypeName == null) {
if (isStatic) {
classCtx?.classScopeMembers?.add(name)
registerClassScopeMember(declaringClassNameCaptured, name)
} else if (isDelegate || isProperty) {
if (classCtx != null) {
if (!classCtx.memberMethodIds.containsKey(name)) {
classCtx.memberMethodIds[name] = classCtx.nextMethodId++
}
memberMethodId = classCtx.memberMethodIds[name]
}
} else {
if (classCtx != null) {
if (!classCtx.memberFieldIds.containsKey(name)) {
classCtx.memberFieldIds[name] = classCtx.nextFieldId++
}
memberFieldId = classCtx.memberFieldIds[name]
}
}
}
val initialExpression = if (setNull || isProperty) null
else parseStatement(true)
?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression")
if (varTypeDecl == TypeDecl.TypeAny && initialExpression != null) {
val inferred = inferTypeDeclFromInitializer(initialExpression)
if (inferred != null) {
varTypeDecl = inferred
}
}
if (isDelegate && initialExpression != null) {
ensureDelegateType(initialExpression)
if (isMutable && resolveInitializerObjClass(initialExpression) == ObjLazyDelegate.type) {
throw ScriptError(initialExpression.pos, "lazy delegate is read-only")
}
}
if (!isStatic && isDelegate) {
markDelegatedSlot(name)
}
if (declaringClassNameCaptured != null && extTypeName == null && !isStatic) {
val directRef = unwrapDirectRef(initialExpression)
val declClass = resolveTypeDeclObjClass(varTypeDecl)
val initFromExpr = resolveInitializerObjClass(initialExpression)
val isNullLiteral = (directRef as? ConstRef)?.constValue == ObjNull
val initClass = when {
isDelegate && declClass != null -> declClass
declClass != null && isNullLiteral -> declClass
else -> initFromExpr ?: declClass
}
if (initClass != null) {
classFieldTypesByName.getOrPut(declaringClassNameCaptured) { mutableMapOf() }[name] = initClass
}
}
// 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
}
fun buildVarDecl(initialExpression: Statement?): VarDeclStatement {
val slotPlan = slotPlanStack.lastOrNull()
val slotIndex = slotPlan?.slots?.get(name)?.index
val scopeId = slotPlan?.id
val directRef = unwrapDirectRef(initialExpression)
val declClass = resolveTypeDeclObjClass(varTypeDecl)
val initFromExpr = resolveInitializerObjClass(initialExpression)
val isNullLiteral = (directRef as? ConstRef)?.constValue == ObjNull
val initObjClass = if (declClass != null && isNullLiteral) declClass else initFromExpr ?: declClass
if (varTypeDecl !is TypeDecl.TypeAny && varTypeDecl !is TypeDecl.TypeNullableAny) {
if (slotIndex != null && scopeId != null) {
slotTypeDeclByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = varTypeDecl
}
nameTypeDecl[name] = varTypeDecl
}
if (directRef is ValueFnRef) {
val returnClass = lambdaReturnTypeByRef[directRef]
if (returnClass != null) {
if (slotIndex != null && scopeId != null) {
callableReturnTypeByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = returnClass
}
callableReturnTypeByName[name] = returnClass
}
}
if (directRef is MethodCallRef && directRef.name == "encode") {
val payloadClass = inferEncodedPayloadClass(directRef.args)
if (payloadClass != null) {
if (slotIndex != null && scopeId != null) {
encodedPayloadTypeByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = payloadClass
}
encodedPayloadTypeByName[name] = payloadClass
}
}
if (initObjClass != null) {
if (slotIndex != null && scopeId != null) {
slotTypeByScopeId.getOrPut(scopeId) { mutableMapOf() }[slotIndex] = initObjClass
}
nameObjClass[name] = initObjClass
}
val declaredType = if (varTypeDecl == TypeDecl.TypeAny || varTypeDecl == TypeDecl.TypeNullableAny) {
null
} else {
varTypeDecl
}
return VarDeclStatement(
name,
isMutable,
visibility,
initialExpression,
isTransient,
declaredType,
slotIndex,
scopeId,
start,
initObjClass
)
}
if (declaringClassNameCaptured == null &&
extTypeName == null &&
!isStatic &&
!isProperty
) {
if (isDelegate) {
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
val slotPlan = slotPlanStack.lastOrNull()
val slotIndex = slotPlan?.slots?.get(name)?.index
val scopeId = slotPlan?.id
return DelegatedVarDeclStatement(
name,
isMutable,
visibility,
initExpr,
isTransient,
slotIndex,
scopeId,
start
)
}
return buildVarDecl(initialExpression)
}
if (isStatic) {
if (declaringClassNameCaptured != null) {
val directRef = unwrapDirectRef(initialExpression)
val declClass = resolveTypeDeclObjClass(varTypeDecl)
val initFromExpr = resolveInitializerObjClass(initialExpression)
val isNullLiteral = (directRef as? ConstRef)?.constValue == ObjNull
val initClass = if (declClass != null && isNullLiteral) declClass else initFromExpr ?: declClass
if (initClass != null) {
classFieldTypesByName.getOrPut(declaringClassNameCaptured) { mutableMapOf() }[name] = initClass
}
}
// 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 += ClassStaticFieldInitStatement(
name = name,
isMutable = isMutable,
visibility = visibility,
writeVisibility = null,
initializer = initialExpression,
isDelegated = isDelegate,
isTransient = isTransient,
startPos = start
)
return NopStatement
}
// Check for accessors if it is a class member
var getter: Obj? = null
var setter: Obj? = null
var setterVisibility: Visibility? = null
if (declaringClassNameCaptured != null || extTypeName != null) {
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
val accessorImplicitThisMembers = extTypeName != null || (classCtx != null && !isStatic)
val accessorImplicitThisTypeName = extTypeName ?: if (classCtx != null && !isStatic) classCtx.name else 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 = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
wrapFunctionBytecode(parseBlock(), "<getter>")
}
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
inCodeContext(
CodeContext.Function(
"<getter>",
implicitThisMembers = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
val expr = parseExpression()
?: throw ScriptError(cc.current().pos, "Expected getter expression")
wrapFunctionBytecode(expr, "<getter>")
}
} 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 = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
wrapFunctionBytecode(parseBlockWithPredeclared(listOf(setArgName to true)), "<setter>")
}
PropertyAccessorStatement(body, setArgName, body.pos)
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
val expr = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
parseExpressionBlockWithPredeclared(listOf(setArgName to true))
}
val st = wrapFunctionBytecode(expr, "<setter>")
PropertyAccessorStatement(st, setArgName, st.pos)
} 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 = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
parseBlockWithPredeclared(listOf(setArg.value to true))
}
val wrapped = wrapFunctionBytecode(body, "<setter>")
PropertyAccessorStatement(wrapped, setArg.value, body.pos)
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
val st = inCodeContext(
CodeContext.Function(
"<setter>",
implicitThisMembers = accessorImplicitThisMembers,
implicitThisTypeName = accessorImplicitThisTypeName
)
) {
parseExpressionBlockWithPredeclared(listOf(setArg.value to true))
}
val wrapped = wrapFunctionBytecode(st, "<setter>")
PropertyAccessorStatement(wrapped, setArg.value, st.pos)
} 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
)
}
if (declaringClassNameCaptured != null) {
val storageName = "$declaringClassNameCaptured::$name"
if (isDelegate) {
val initExpr = initialExpression ?: throw ScriptError(start, "Delegate must be initialized")
val initStmt = if (!isAbstract) {
val accessType = if (isMutable) "Var" else "Val"
val initStatement = InstanceDelegatedInitStatement(
storageName = storageName,
memberName = name,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
accessTypeLabel = accessType,
initializer = initExpr,
pos = start
)
wrapInstanceInitBytecode(initStatement, "delegated@${storageName}")
} else null
return ClassInstanceDelegatedDeclStatement(
name = name,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
methodId = memberMethodId,
initStatement = initStmt,
pos = start
)
}
if (getter != null || setter != null) {
val prop = ObjProperty(name, getter, setter)
val initStmt = if (!isAbstract) {
val initStatement = InstancePropertyInitStatement(
storageName = storageName,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
prop = prop,
pos = start
)
wrapInstanceInitBytecode(initStatement, "property@${storageName}")
} else null
return ClassInstancePropertyDeclStatement(
name = name,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
prop = prop,
methodId = memberMethodId,
initStatement = initStmt,
pos = start
)
}
val isLateInitVal = !isMutable && initialExpression == null
val initStmt = if (!isAbstract) {
val initStatement = InstanceFieldInitStatement(
storageName = storageName,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
isLateInitVal = isLateInitVal,
initializer = initialExpression,
pos = start
)
wrapInstanceInitBytecode(initStatement, "field@${storageName}")
} else null
return ClassInstanceFieldDeclStatement(
name = name,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
fieldId = memberFieldId,
initStatement = initStmt,
pos = start
)
}
return buildVarDecl(initialExpression)
} finally {
if (implicitTypeParams.isNotEmpty()) pendingTypeParamStack.removeLast()
}
}
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,
compileBytecode: Boolean = true,
strictSlotRefs: Boolean = true,
allowUnresolvedRefs: Boolean = false,
seedScope: Scope? = null,
useFastLocalRefs: Boolean = false
): Script {
return Compiler(
CompilerContext(parseLyng(source)),
importManager,
Settings(
miniAstSink = miniSink,
resolutionSink = resolutionSink,
compileBytecode = compileBytecode,
strictSlotRefs = strictSlotRefs,
allowUnresolvedRefs = allowUnresolvedRefs,
seedScope = seedScope,
useFastLocalRefs = useFastLocalRefs
)
).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()