Fix implicit extension calls and apply scope captures

This commit is contained in:
sergeych committed 2026-01-30 16:20:55 +03:00
1 parent ecf64dcbc3
commit 68122df6d7
83 files changed
+1502 -146

No files matched your search

@@ -21,6 +21,7 @@ import net.sergeych.lyng.obj.Obj
class BlockStatement(
val block: Script,
val slotPlan: Map<String, Int>,
val captureSlots: List<CaptureSlot> = emptyList(),
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
@@ -28,8 +29,16 @@ class BlockStatement(
override suspend fun execute(scope: Scope): Obj {
val target = if (scope.skipScopeCreation) scope else scope.createChildScope(startPos)
if (slotPlan.isNotEmpty()) target.applySlotPlan(slotPlan)
if (captureSlots.isNotEmpty()) {
for (capture in captureSlots) {
val rec = scope.resolveCaptureRecord(capture.name)
?: scope.raiseSymbolNotFound("symbol ${capture.name} not found")
target.updateSlotFor(capture.name, rec)
}
}
return block.execute(target)
}
fun statements(): List<Statement> = block.debugStatements()
}
@@ -0,0 +1,22 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
data class CaptureSlot(
val name: String,
)
@@ -71,7 +71,7 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
}
}
class ApplyScope(_parent: Scope,val applied: Scope) : Scope(_parent, thisObj = applied.thisObj) {
class ApplyScope(callScope: Scope, val applied: Scope) : Scope(callScope.parent?.parent ?: callScope.parent ?: callScope, thisObj = applied.thisObj) {
override fun get(name: String): ObjRecord? {
return applied.get(name) ?: super.get(name)
@@ -81,4 +81,4 @@ class ApplyScope(_parent: Scope,val applied: Scope) : Scope(_parent, thisObj = a
return this
}
}
}
@@ -19,8 +19,9 @@ package net.sergeych.lyng
sealed class CodeContext {
class Module(@Suppress("unused") val packageName: String?): CodeContext()
class Function(val name: String): CodeContext()
class Function(val name: String, val implicitThisMembers: Boolean = false): CodeContext()
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
val pendingInitializations = mutableMapOf<String, Pos>()
val declaredMembers = mutableSetOf<String>()
}
}
}
@@ -132,27 +132,29 @@ class Compiler(
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) continue
val afterName = cc.peekNextNonWhitespace()
val fnName = if (afterName.type == Token.Type.DOT) {
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) actual.value else null
} else nameToken.value
if (fnName != null) {
declareSlotNameIn(plan, fnName, isMutable = false, isDelegated = false)
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
}
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()
val varName = if (afterName.type == Token.Type.DOT) {
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) actual.value else null
} else nameToken.value
if (varName != null) {
declareSlotNameIn(plan, varName, isMutable = t.value == "var", isDelegated = false)
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
}
continue
}
declareSlotNameIn(plan, nameToken.value, isMutable = t.value == "var", isDelegated = false)
}
"class", "object" -> {
val nameToken = nextNonWs()
@@ -231,6 +233,22 @@ class Compiler(
resolutionSink?.reference(name, pos)
return ref
}
val captureOwner = capturePlanStack.lastOrNull()?.captureOwners?.get(name)
if (slotLoc.depth == 0 && captureOwner != null) {
val ref = LocalSlotRef(
name,
slotLoc.slot,
slotLoc.scopeId,
slotLoc.isMutable,
slotLoc.isDelegated,
pos,
strictSlotRefs,
captureOwnerScopeId = captureOwner.scopeId,
captureOwnerSlot = captureOwner.slot
)
resolutionSink?.reference(name, pos)
return ref
}
val ref = LocalSlotRef(
name,
slotLoc.slot,
@@ -301,6 +319,11 @@ class Compiler(
resolutionSink?.referenceMember(name, pos)
return ImplicitThisMemberRef(name, pos)
}
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)
@@ -611,6 +634,7 @@ class Compiler(
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 currentRangeParamNames: Set<String>
get() = rangeParamNamesStack.lastOrNull() ?: emptySet()
private val capturePlanStack = mutableListOf<CapturePlan>()
@@ -618,7 +642,8 @@ class Compiler(
private data class CapturePlan(
val slotPlan: SlotPlan,
val captures: MutableList<CaptureSlot> = mutableListOf(),
val captureMap: MutableMap<String, CaptureSlot> = mutableMapOf()
val captureMap: MutableMap<String, CaptureSlot> = mutableMapOf(),
val captureOwners: MutableMap<String, SlotLocation> = mutableMapOf()
)
private fun recordCaptureSlot(name: String, slotLoc: SlotLocation) {
@@ -628,6 +653,7 @@ class Compiler(
name = name,
)
plan.captureMap[name] = capture
plan.captureOwners[name] = slotLoc
plan.captures += capture
if (!plan.slotPlan.slots.containsKey(name)) {
plan.slotPlan.slots[name] = SlotEntry(
@@ -1383,7 +1409,7 @@ class Compiler(
// and the source closure of the lambda which might have other thisObj.
val context = scope.applyClosure(closureScope)
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
if (captureSlots.isNotEmpty()) {
if (captureSlots.isNotEmpty() && context !is ApplyScope) {
for (capture in captureSlots) {
val rec = closureScope.resolveCaptureRecord(capture.name)
?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
@@ -1949,6 +1975,28 @@ class Compiler(
return when (left) {
is ImplicitThisMemberRef ->
ImplicitThisMethodCallRef(left.name, args, detectedBlockArgument, isOptional, left.atPos)
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)) {
ImplicitThisMethodCallRef(left.name, args, detectedBlockArgument, isOptional, left.pos())
} else {
CallRef(left, args, detectedBlockArgument, isOptional)
}
}
is LocalSlotRef -> {
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
val implicitThis = codeContexts.any { ctx ->
(ctx as? CodeContext.Function)?.implicitThisMembers == true
}
if ((classContext || implicitThis) && extensionNames.contains(left.name)) {
ImplicitThisMethodCallRef(left.name, args, detectedBlockArgument, isOptional, left.pos())
} else {
CallRef(left, args, detectedBlockArgument, isOptional)
}
}
else -> CallRef(left, args, detectedBlockArgument, isOptional)
}
}
@@ -59,6 +59,7 @@ class ModuleScope(
// when importing records, we keep track of its package (not otherwise needed)
if (record.importedFrom == null) record.importedFrom = this
scope.objects[newName] = record
scope.updateSlotFor(newName, record)
}
}
}
@@ -92,4 +93,3 @@ class ModuleScope(
}
}
@@ -32,10 +32,15 @@ import kotlin.math.*
class Script(
override val pos: Pos,
private val statements: List<Statement> = emptyList(),
private val moduleSlotPlan: Map<String, Int> = emptyMap(),
// private val catchReturn: Boolean = false,
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
if (moduleSlotPlan.isNotEmpty()) {
scope.applySlotPlan(moduleSlotPlan)
seedModuleSlots(scope)
}
var lastResult: Obj = ObjVoid
for (s in statements) {
lastResult = s.execute(scope)
@@ -43,6 +48,17 @@ class Script(
return lastResult
}
private fun seedModuleSlots(scope: Scope) {
val parent = scope.parent ?: return
for (name in moduleSlotPlan.keys) {
if (scope.objects.containsKey(name)) {
scope.updateSlotFor(name, scope.objects[name]!!)
continue
}
parent.get(name)?.let { scope.updateSlotFor(name, it) }
}
}
internal fun debugStatements(): List<Statement> = statements
suspend fun execute() = execute(
@@ -27,7 +27,7 @@ class VarDeclStatement(
val initializer: Statement?,
val isTransient: Boolean,
val slotIndex: Int?,
val slotDepth: Int?,
val scopeId: Int?,
private val startPos: Pos,
) : Statement() {
override val pos: Pos = startPos
@@ -259,12 +259,43 @@ class BytecodeCompiler(
updateSlotType(slot, SlotType.OBJ)
CompiledValue(slot, SlotType.OBJ)
}
is ImplicitThisMethodCallRef -> compileEvalRef(ref)
is ImplicitThisMethodCallRef -> compileImplicitThisMethodCall(ref)
is IndexRef -> compileIndexRef(ref)
else -> null
}
}
private fun compileImplicitThisMethodCall(ref: ImplicitThisMethodCallRef): CompiledValue? {
val receiver = compileNameLookup("this")
val methodId = builder.addConst(BytecodeConst.StringVal(ref.methodName()))
if (methodId > 0xFFFF) return null
val dst = allocSlot()
if (!ref.optionalInvoke()) {
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, receiver.slot, nullSlot, cmpSlot)
val nullLabel = builder.label()
val endLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
)
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)
builder.mark(endLabel)
return CompiledValue(dst, SlotType.OBJ)
}
private fun compileConst(obj: Obj): CompiledValue? {
val slot = allocSlot()
when (obj) {
@@ -1743,6 +1774,42 @@ class BytecodeCompiler(
}
private fun compileCall(ref: CallRef): CompiledValue? {
val localTarget = ref.target as? LocalVarRef
if (localTarget != null) {
val direct = resolveDirectNameSlot(localTarget.name)
if (direct == null) {
val thisSlot = resolveDirectNameSlot("this")
if (thisSlot != null) {
val methodId = builder.addConst(BytecodeConst.StringVal(localTarget.name))
if (methodId > 0xFFFF) return null
val dst = allocSlot()
if (!ref.isOptionalInvoke) {
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_VIRTUAL, thisSlot.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, thisSlot.slot, nullSlot, cmpSlot)
val nullLabel = builder.label()
val endLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
)
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_VIRTUAL, thisSlot.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)
builder.mark(endLabel)
return CompiledValue(dst, SlotType.OBJ)
}
}
}
val fieldTarget = ref.target as? FieldRef
if (fieldTarget != null) {
val receiver = compileRefWithFallback(fieldTarget.target, null, Pos.builtIn) ?: return null
@@ -1802,6 +1869,32 @@ class BytecodeCompiler(
return CompiledValue(dst, SlotType.OBJ)
}
private fun resolveDirectNameSlot(name: String): CompiledValue? {
if (!allowLocalSlots) return null
if (pendingScopeNameRefs.contains(name)) return null
if (!forceScopeSlots) {
scopeSlotIndexByName[name]?.let { slot ->
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved)
}
loopSlotOverrides[name]?.let { slot ->
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved)
}
localSlotIndexByName[name]?.let { localIndex ->
val slot = scopeSlotCount + localIndex
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved)
}
return null
}
scopeSlotIndexByName[name]?.let { slot ->
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
return CompiledValue(slot, resolved)
}
return null
}
private fun compileMethodCall(ref: MethodCallRef): CompiledValue? {
val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null
val methodId = builder.addConst(BytecodeConst.StringVal(ref.name))
@@ -2899,9 +2992,11 @@ class BytecodeCompiler(
private fun ensureScopeAddr(scopeSlot: Int): Int {
val existing = addrSlotByScopeSlot[scopeSlot]
if (existing != null) return existing
val addrSlot = nextAddrSlot++
addrSlotByScopeSlot[scopeSlot] = addrSlot
val addrSlot = existing ?: run {
val created = nextAddrSlot++
addrSlotByScopeSlot[scopeSlot] = created
created
}
builder.emit(Opcode.RESOLVE_SCOPE_SLOT, scopeSlot, addrSlot)
return addrSlot
}
@@ -33,7 +33,7 @@ sealed class BytecodeConst {
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
data class ListLiteralPlan(val spreads: List<Boolean>) : BytecodeConst()
data class ValueFn(val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst()
data class ExtensionPropertyDecl(
val extTypeName: String,
val property: ObjProperty,
@@ -52,7 +52,7 @@ class BytecodeStatement private constructor(
if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement)
if (hasUnsupported) {
val statementName = statement::class.qualifiedName ?: statement.javaClass.name
val statementName = statement::class.qualifiedName ?: statement::class.simpleName ?: "UnknownStatement"
throw BytecodeFallbackException(
"Bytecode fallback: unsupported statement $statementName in '$nameHint'",
statement.pos
@@ -135,6 +135,7 @@ class BytecodeStatement private constructor(
net.sergeych.lyng.BlockStatement(
net.sergeych.lyng.Script(stmt.pos, unwrapped),
stmt.slotPlan,
stmt.captureSlots,
stmt.pos
)
}
@@ -146,7 +147,7 @@ class BytecodeStatement private constructor(
stmt.initializer?.let { unwrapDeep(it) },
stmt.isTransient,
stmt.slotIndex,
stmt.slotDepth,
stmt.scopeId,
stmt.pos
)
}
@@ -1549,6 +1549,9 @@ class CmdFrame(
fun pushScope(plan: Map<String, Int>, captures: List<String>) {
val parentScope = scope
if (captures.isNotEmpty()) {
syncFrameToScope()
}
val captureRecords = if (captures.isNotEmpty()) {
captures.map { name ->
val rec = parentScope.resolveCaptureRecord(name)
@@ -1558,9 +1561,6 @@ class CmdFrame(
} else {
emptyList()
}
if (shouldSyncLocalCaptures(captures)) {
syncFrameToScope()
}
if (scope.skipScopeCreation) {
val snapshot = scope.applySlotPlanWithSnapshot(plan)
slotPlanStack.addLast(snapshot)
@@ -1597,7 +1597,7 @@ class CmdFrame(
?: error("Scope stack underflow in POP_SCOPE")
val captures = captureStack.removeLastOrNull() ?: emptyList()
scopeDepth -= 1
if (shouldSyncLocalCaptures(captures)) {
if (captures.isNotEmpty()) {
syncFrameToScope()
}
}
@@ -1917,6 +1917,7 @@ class ThisMethodSlotCallRef(
* Reference to a local/visible variable by name (Phase A: scope lookup).
*/
class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
internal fun pos(): Pos = atPos
override fun forEachVariable(block: (String) -> Unit) {
block(name)
}
@@ -2287,14 +2288,7 @@ class ImplicitThisMemberRef(
val caller = scope.currentClassCtx
val th = scope.thisObj
// 1) locals in the same `this` chain
var s: Scope? = scope
while (s != null && s.thisObj === th) {
scope.tryGetLocalRecord(s, name, caller)?.let { return it }
s = s.parent
}
// 2) member slots on this instance
// member slots on this instance
if (th is ObjInstance) {
// private member access for current class context
caller?.let { c ->
@@ -2326,14 +2320,7 @@ class ImplicitThisMemberRef(
}
}
// 3) fallback to normal scope resolution (globals/outer scopes)
scope[name]?.let { return it }
try {
return th.readField(scope, name)
} catch (e: ExecutionError) {
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
throw e
}
scope.raiseSymbolNotFound(name)
}
override suspend fun evalValue(scope: Scope): Obj {
@@ -2346,18 +2333,7 @@ class ImplicitThisMemberRef(
val caller = scope.currentClassCtx
val th = scope.thisObj
// 1) locals in the same `this` chain
var s: Scope? = scope
while (s != null && s.thisObj === th) {
val rec = scope.tryGetLocalRecord(s, name, caller)
if (rec != null) {
scope.assign(rec, name, newValue)
return
}
s = s.parent
}
// 2) member slots on this instance
// member slots on this instance
if (th is ObjInstance) {
val key = th.objClass.publicMemberResolution[name] ?: name
th.fieldRecordForKey(key)?.let { rec ->
@@ -2388,12 +2364,7 @@ class ImplicitThisMemberRef(
}
}
// 3) fallback to normal scope resolution
scope[name]?.let { stored ->
scope.assign(stored, name, newValue)
return
}
th.writeField(scope, name, newValue)
scope.raiseSymbolNotFound(name)
}
}
@@ -2409,22 +2380,30 @@ class ImplicitThisMethodCallRef(
private val atPos: Pos
) : ObjRef {
private val memberRef = ImplicitThisMemberRef(name, atPos)
internal fun methodName(): String = name
internal fun arguments(): List<ParsedArgument> = args
internal fun hasTailBlock(): Boolean = tailBlock
internal fun optionalInvoke(): Boolean = isOptional
override suspend fun get(scope: Scope): ObjRecord = evalValue(scope).asReadonly
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
val callee = memberRef.evalValue(scope)
if (callee == ObjNull && isOptional) return ObjNull
val callArgs = args.toArguments(scope, tailBlock)
val usePool = PerfFlags.SCOPE_POOL
return if (usePool) {
scope.withChildFrame(callArgs) { child ->
callee.callOn(child)
val localRecord = scope.chainLookupIgnoreClosure(name, followClosure = true, caller = scope.currentClassCtx)
if (localRecord != null) {
val callee = scope.resolve(localRecord, name)
if (callee == ObjNull && isOptional) return ObjNull
val usePool = PerfFlags.SCOPE_POOL
return if (usePool) {
scope.withChildFrame(callArgs) { child ->
callee.callOn(child)
}
} else {
callee.callOn(scope.createChildScope(scope.pos, callArgs))
}
} else {
callee.callOn(scope.createChildScope(scope.pos, callArgs))
}
return scope.thisObj.invokeInstanceMethod(scope, name, callArgs)
}
}
@@ -2435,57 +2414,31 @@ class ImplicitThisMethodCallRef(
class LocalSlotRef(
val name: String,
internal val slot: Int,
internal val depth: Int,
internal val scopeDepth: Int,
internal val scopeId: Int,
internal val isMutable: Boolean,
internal val isDelegated: Boolean,
private val atPos: Pos,
private val strict: Boolean = false,
internal val captureOwnerScopeId: Int? = null,
internal val captureOwnerSlot: Int? = null,
) : ObjRef {
internal fun pos(): Pos = atPos
override fun forEachVariable(block: (String) -> Unit) {
block(name)
}
private val fallbackRef = LocalVarRef(name, atPos)
private var cachedFrameId: Long = 0L
private var cachedOwner: Scope? = null
private var cachedOwnerVerified: Boolean = false
private fun resolveOwner(scope: Scope): Scope? {
if (cachedOwner != null && cachedFrameId == scope.frameId && cachedOwnerVerified) {
val cached = cachedOwner!!
val candidate = if (depth == 0) scope else {
var s: Scope? = scope
var remaining = depth
while (s != null && remaining > 0) {
s = s.parent
remaining--
}
s
}
if (candidate === cached && candidate?.getSlotIndexOf(name) == slot) return cached
}
var s: Scope? = scope
var remaining = depth
while (s != null && remaining > 0) {
s = s.parent
remaining--
}
if (s == null || s.getSlotIndexOf(name) != slot) {
cachedOwner = null
cachedOwnerVerified = false
cachedFrameId = scope.frameId
return null
}
cachedOwner = s
cachedOwnerVerified = true
cachedFrameId = scope.frameId
return s
private fun resolveOwner(scope: Scope): Scope {
return scope
}
override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos
val owner = resolveOwner(scope) ?: return fallbackRef.get(scope)
if (slot < 0 || slot >= owner.slotCount()) return fallbackRef.get(scope)
val owner = resolveOwner(scope)
if (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name")
return fallbackRef.get(scope)
}
val rec = owner.getSlotRecord(slot)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
@@ -2495,8 +2448,11 @@ class LocalSlotRef(
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
val owner = resolveOwner(scope) ?: return fallbackRef.evalValue(scope)
if (slot < 0 || slot >= owner.slotCount()) return fallbackRef.evalValue(scope)
val owner = resolveOwner(scope)
if (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name")
return fallbackRef.evalValue(scope)
}
val rec = owner.getSlotRecord(slot)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
@@ -2506,11 +2462,9 @@ class LocalSlotRef(
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos
val owner = resolveOwner(scope) ?: run {
fallbackRef.setAt(pos, scope, newValue)
return
}
val owner = resolveOwner(scope)
if (slot < 0 || slot >= owner.slotCount()) {
if (strict) scope.raiseError("slot index out of range for $name")
fallbackRef.setAt(pos, scope, newValue)
return
}
@@ -0,0 +1,376 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.resolution
import net.sergeych.lyng.Pos
class ResolutionCollector(private val moduleName: String) : ResolutionSink {
private data class Decl(
val name: String,
val kind: SymbolKind,
val isMutable: Boolean,
val pos: Pos,
val isOverride: Boolean
)
private data class Ref(
val name: String,
val pos: Pos,
val qualifier: String? = null
)
private data class ReflectRef(
val name: String,
val pos: Pos
)
private data class MemberInfo(
val name: String,
val isOverride: Boolean,
val pos: Pos
)
private data class ClassInfo(
val name: String,
val bases: List<String>,
val pos: Pos,
val members: MutableMap<String, MemberInfo> = LinkedHashMap()
)
private class ScopeNode(
val kind: ScopeKind,
val pos: Pos,
val parent: ScopeNode?,
val className: String? = null,
val bases: List<String> = emptyList()
) {
val decls: LinkedHashMap<String, Decl> = LinkedHashMap()
val refs: MutableList<Ref> = ArrayList()
val memberRefs: MutableList<Ref> = ArrayList()
val reflectRefs: MutableList<ReflectRef> = ArrayList()
val captures: LinkedHashMap<String, CaptureInfo> = LinkedHashMap()
val children: MutableList<ScopeNode> = ArrayList()
}
private var root: ScopeNode? = null
private var current: ScopeNode? = null
private val symbols = ArrayList<ResolvedSymbol>()
private val captures = LinkedHashMap<String, CaptureInfo>()
private val errors = ArrayList<ResolutionError>()
private val warnings = ArrayList<ResolutionWarning>()
private val classes = LinkedHashMap<String, ClassInfo>()
override fun enterScope(kind: ScopeKind, pos: Pos, className: String?, bases: List<String>) {
val parent = current
val node = ScopeNode(kind, pos, parent, className, bases)
if (root == null) {
root = node
}
parent?.children?.add(node)
current = node
if (kind == ScopeKind.CLASS && className != null) {
classes.getOrPut(className) { ClassInfo(className, bases.toList(), pos) }
}
}
override fun exitScope(pos: Pos) {
current = current?.parent
}
override fun declareClass(name: String, bases: List<String>, pos: Pos) {
val existing = classes[name]
if (existing == null) {
classes[name] = ClassInfo(name, bases.toList(), pos)
} else if (existing.bases.isEmpty() && bases.isNotEmpty()) {
classes[name] = existing.copy(bases = bases.toList())
}
}
override fun declareSymbol(
name: String,
kind: SymbolKind,
isMutable: Boolean,
pos: Pos,
isOverride: Boolean
) {
val node = current ?: return
node.decls[name] = Decl(name, kind, isMutable, pos, isOverride)
if (kind == SymbolKind.LOCAL || kind == SymbolKind.PARAM) {
val classScope = findNearestClassScope(node)
if (classScope != null && classScope.decls.containsKey(name)) {
warnings += ResolutionWarning("shadowing member: $name", pos)
}
}
if (kind == SymbolKind.MEMBER) {
val classScope = findNearestClassScope(node)
val className = classScope?.className
if (className != null) {
val info = classes.getOrPut(className) { ClassInfo(className, classScope.bases, classScope.pos) }
info.members[name] = MemberInfo(name, isOverride, pos)
}
}
symbols += ResolvedSymbol(
name = name,
origin = originForDecl(node, kind),
slotIndex = -1,
pos = pos
)
}
override fun reference(name: String, pos: Pos) {
val node = current ?: return
node.refs += Ref(name, pos)
}
override fun referenceMember(name: String, pos: Pos, qualifier: String?) {
val node = current ?: return
node.memberRefs += Ref(name, pos, qualifier)
}
override fun referenceReflection(name: String, pos: Pos) {
val node = current ?: return
node.reflectRefs += ReflectRef(name, pos)
}
fun buildReport(): ResolutionReport {
root?.let { resolveScope(it) }
checkMiConflicts()
return ResolutionReport(
moduleName = moduleName,
symbols = symbols.toList(),
captures = captures.values.toList(),
errors = errors.toList(),
warnings = warnings.toList()
)
}
private fun resolveScope(node: ScopeNode) {
for (ref in node.refs) {
if (ref.name == "this") continue
if (ref.name == "scope") continue
val resolved = resolveName(node, ref)
if (!resolved) {
errors += ResolutionError("unresolved name: ${ref.name}", ref.pos)
}
}
for (ref in node.memberRefs) {
val resolved = resolveMemberName(node, ref)
if (!resolved) {
errors += ResolutionError("unresolved member: ${ref.name}", ref.pos)
}
}
for (ref in node.reflectRefs) {
val resolved = resolveName(node, Ref(ref.name, ref.pos)) ||
resolveMemberName(node, Ref(ref.name, ref.pos))
if (!resolved) {
errors += ResolutionError("unresolved reflected name: ${ref.name}", ref.pos)
}
}
for (child in node.children) {
resolveScope(child)
}
}
private fun resolveName(node: ScopeNode, ref: Ref): Boolean {
if (ref.name.contains('.')) return true
var scope: ScopeNode? = node
while (scope != null) {
val decl = scope.decls[ref.name]
if (decl != null) {
if (scope !== node) {
recordCapture(node, decl, scope)
}
return true
}
scope = scope.parent
}
return false
}
private fun recordCapture(owner: ScopeNode, decl: Decl, targetScope: ScopeNode) {
if (owner.captures.containsKey(decl.name)) return
val origin = when (targetScope.kind) {
ScopeKind.MODULE -> SymbolOrigin.MODULE
else -> SymbolOrigin.OUTER
}
val capture = CaptureInfo(
name = decl.name,
origin = origin,
slotIndex = -1,
isMutable = decl.isMutable,
pos = decl.pos
)
owner.captures[decl.name] = capture
captures[decl.name] = capture
}
private fun resolveMemberName(node: ScopeNode, ref: Ref): Boolean {
val classScope = findNearestClassScope(node) ?: return false
val className = classScope.className ?: return false
val qualifier = ref.qualifier
return if (qualifier != null) {
resolveQualifiedMember(className, qualifier, ref.name, ref.pos)
} else {
resolveMemberInClass(className, ref.name, ref.pos)
}
}
private fun findNearestClassScope(node: ScopeNode): ScopeNode? {
var scope: ScopeNode? = node
while (scope != null) {
if (scope.kind == ScopeKind.CLASS) return scope
scope = scope.parent
}
return null
}
private fun originForDecl(scope: ScopeNode, kind: SymbolKind): SymbolOrigin {
return when (kind) {
SymbolKind.PARAM -> SymbolOrigin.PARAM
SymbolKind.MEMBER -> SymbolOrigin.MEMBER
else -> when (scope.kind) {
ScopeKind.MODULE -> SymbolOrigin.MODULE
ScopeKind.CLASS -> SymbolOrigin.MEMBER
else -> SymbolOrigin.LOCAL
}
}
}
private fun resolveMemberInClass(className: String, member: String, pos: Pos): Boolean {
val info = classes[className] ?: return false
val currentMember = info.members[member]
val definers = findDefiningClasses(className, member)
if (currentMember != null) {
if (definers.size > 1 && !currentMember.isOverride) {
errors += ResolutionError("override required for $member in $className", pos)
}
return true
}
if (definers.size > 1) {
errors += ResolutionError("ambiguous member '$member' in $className", pos)
return true
}
return definers.isNotEmpty()
}
private fun resolveQualifiedMember(className: String, qualifier: String, member: String, pos: Pos): Boolean {
val mro = linearize(className)
val idx = mro.indexOf(qualifier)
if (idx < 0) return false
for (name in mro.drop(idx)) {
val info = classes[name]
if (info?.members?.containsKey(member) == true) return true
}
errors += ResolutionError("member '$member' not found in $qualifier", pos)
return true
}
private fun findDefiningClasses(className: String, member: String): List<String> {
val parents = linearize(className).drop(1)
val raw = parents.filter { classes[it]?.members?.containsKey(member) == true }
if (raw.size <= 1) return raw
val filtered = raw.toMutableList()
val iterator = raw.iterator()
while (iterator.hasNext()) {
val candidate = iterator.next()
for (other in raw) {
if (candidate == other) continue
if (linearize(other).contains(candidate)) {
filtered.remove(candidate)
break
}
}
}
return filtered
}
private fun linearize(className: String, visited: MutableMap<String, List<String>> = mutableMapOf()): List<String> {
visited[className]?.let { return it }
val info = classes[className]
val parents = info?.bases ?: emptyList()
if (parents.isEmpty()) {
val single = listOf(className)
visited[className] = single
return single
}
val parentLinearizations = parents.map { linearize(it, visited).toMutableList() }
val merge = mutableListOf<MutableList<String>>()
merge.addAll(parentLinearizations)
merge.add(parents.toMutableList())
val merged = c3Merge(merge)
val result = listOf(className) + merged
visited[className] = result
return result
}
private fun c3Merge(seqs: MutableList<MutableList<String>>): List<String> {
val result = mutableListOf<String>()
while (seqs.isNotEmpty()) {
seqs.removeAll { it.isEmpty() }
if (seqs.isEmpty()) break
var candidate: String? = null
outer@ for (seq in seqs) {
val head = seq.first()
var inTail = false
for (other in seqs) {
if (other === seq || other.size <= 1) continue
if (other.drop(1).contains(head)) {
inTail = true
break
}
}
if (!inTail) {
candidate = head
break@outer
}
}
val picked = candidate ?: run {
errors += ResolutionError("C3 MRO failed for $moduleName", Pos.builtIn)
return result
}
result += picked
for (seq in seqs) {
if (seq.isNotEmpty() && seq.first() == picked) {
seq.removeAt(0)
}
}
}
return result
}
private fun checkMiConflicts() {
for (info in classes.values) {
val baseNames = linearize(info.name).drop(1)
if (baseNames.isEmpty()) continue
val baseMemberNames = linkedSetOf<String>()
for (base in baseNames) {
classes[base]?.members?.keys?.let { baseMemberNames.addAll(it) }
}
for (member in baseMemberNames) {
val definers = findDefiningClasses(info.name, member)
if (definers.size <= 1) continue
val current = info.members[member]
if (current == null || !current.isOverride) {
errors += ResolutionError("ambiguous member '$member' in ${info.name}", info.pos)
}
}
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.resolution
import net.sergeych.lyng.Pos
enum class ScopeKind {
MODULE,
FUNCTION,
BLOCK,
CLASS
}
enum class SymbolKind {
LOCAL,
PARAM,
FUNCTION,
CLASS,
ENUM,
MEMBER
}
interface ResolutionSink {
fun enterScope(kind: ScopeKind, pos: Pos, className: String? = null, bases: List<String> = emptyList()) {}
fun exitScope(pos: Pos) {}
fun declareClass(name: String, bases: List<String>, pos: Pos) {}
fun declareSymbol(
name: String,
kind: SymbolKind,
isMutable: Boolean,
pos: Pos,
isOverride: Boolean = false
) {}
fun reference(name: String, pos: Pos) {}
fun referenceMember(name: String, pos: Pos, qualifier: String? = null) {}
fun referenceReflection(name: String, pos: Pos) {}
}