Step 1: module import slots and extension lookup

This commit is contained in:
Sergey Chernov 2026-02-08 14:04:25 +03:00
parent a557d0cc59
commit 7b70a37e90
14 changed files with 954 additions and 289 deletions

View File

@ -1,41 +1,32 @@
# Bytecode migration plan (compiler -> frames/bytecode)
# Bytecode Migration Plan
This is a step-by-step checklist to track remaining non-bytecode paths in the compiler.
Mark items as you implement them. Priorities are ordered by expected simplicity.
Goal: migrate :lynglib compiler/runtime so values live in frame slots and bytecode is the default execution path.
## Priority 1: Quick wins (local changes)
- [ ] Implement bytecode emission for `DelegatedVarDeclStatement`.
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt:3284`, `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1667`
- [ ] Implement bytecode emission for `DestructuringVarDeclStatement`.
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt:3285`, `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1667`
- [ ] Ensure `ExtensionPropertyDeclStatement` is handled in both value and no-value bytecode paths.
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt:3332`
## Step 1: Imports as module slots (done)
- [x] Seed module slot plans from import bindings (lazy, unused imports do not allocate).
- [x] Avoid mutating scopes during compile-time imports; bind slots at runtime instead.
- [x] Make runtime member access honor extensions (methods + properties).
- [x] Ensure class members (ObjClass instances) resolve by slot id in bytecode runtime.
- [x] Expose `Iterator` in root scope so stdlib externs bind at runtime.
## Priority 2: Conservative wrapper guards to relax
- [ ] Allow wrapping `BreakStatement` / `ContinueStatement` / `ReturnStatement` where bytecode already supports them.
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1608`
- [ ] Revisit `containsLoopControl` as a hard blocker for wrapping (once label handling is verified).
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1542`
## Step 2: Class-scope member refs + qualified-this refs (pending)
- [ ] Bytecode-compile `ClassScopeMemberRef` (currently forced to AST in `Compiler.containsUnsupportedRef`).
- [ ] Bytecode-compile `QualifiedThisFieldSlotRef` / `QualifiedThisMethodSlotCallRef`.
- [ ] Ensure slot resolution uses class member ids, not scope lookup; no fallback opcodes.
- [ ] Add coverage for class static access + qualified-this access to keep JVM tests green.
## Priority 3: Medium complexity statements
- [ ] Implement bytecode support for `TryStatement`.
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1698`, `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt:3290`
- [ ] Expand `WhenStatement` condition coverage (remove "unsupported condition" paths).
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1699`
## Step 3: Expand bytecode coverage for control flow + literals (pending)
- [ ] Add bytecode support for `TryStatement` (catch/finally) in `Compiler.containsUnsupportedForBytecode` and `BytecodeCompiler`.
- [ ] Support `WhenStatement` conditions beyond the current limited set.
- [ ] Add map literal spread support (currently throws in `BytecodeCompiler`).
- [ ] Remove remaining `BytecodeCompileException` cases for common member access (missing id paths).
## Priority 4: Ref-level blockers
- [ ] Support dynamic member access in bytecode (`FieldRef` / `MethodCallRef` on `ObjDynamic`).
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1735`
- [ ] Implement bytecode for qualified/captured member refs:
- `QualifiedThisMethodSlotCallRef`
- `QualifiedThisFieldSlotRef`
- `ClassScopeMemberRef`
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1759`
## Known bytecode gaps (from current guards)
- [ ] `TryStatement` is always excluded by `Compiler.containsUnsupportedForBytecode`.
- [ ] `ClassScopeMemberRef` and qualified-this refs are excluded by `Compiler.containsUnsupportedRef`.
- [ ] `BytecodeCompiler` rejects map literal spreads and some argument expressions.
- [ ] Member access still fails when compile-time receiver class cannot be resolved.
## Priority 5: Wrapping policy improvements
- [ ] Allow partial script wrapping (wrap supported statements even when others are unsupported).
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:1333`
- [ ] Re-evaluate tooling paths that disable bytecode:
- `CompileTimeResolution.dryRun` (resolution-only)
- `LyngLanguageTools.analyze` (diagnostics/mini-ast)
- References: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/resolution/CompileTimeResolution.kt:67`, `lynglib/src/commonMain/kotlin/net/sergeych/lyng/tools/LyngLanguageTools.kt:97`
## Validation
- [ ] `./gradlew :lynglib:jvmTest` (full suite) after each step; if failures pre-exist, run targeted tests tied to the change and record the gap in this file.
- [ ] Baseline full suite: currently 46 failures on `:lynglib:jvmTest` (run 2026-02-08); keep targeted tests green until the baseline is addressed.

View File

@ -37,8 +37,14 @@ class BlockStatement(
?: applyScope.callScope.resolveCaptureRecord(capture.name)
} else {
scope.resolveCaptureRecord(capture.name)
} ?: (applyScope?.callScope ?: scope)
.raiseSymbolNotFound("symbol ${capture.name} not found")
}
if (rec == null) {
if (scope.getSlotIndexOf(capture.name) == null && scope.getLocalRecordDirect(capture.name) == null) {
continue
}
(applyScope?.callScope ?: scope)
.raiseSymbolNotFound("symbol ${capture.name} not found")
}
target.updateSlotFor(capture.name, rec)
}
}

View File

@ -107,6 +107,12 @@ class Compiler(
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)
}
@ -124,6 +130,20 @@ class Compiler(
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()
@ -211,6 +231,20 @@ class Compiler(
}
}
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
)
}
}
private fun predeclareTopLevelSymbols() {
val plan = moduleSlotPlan() ?: return
val saved = cc.savePos()
@ -448,11 +482,11 @@ class Compiler(
val scopeRec = seedScope?.get(name) ?: importManager.rootScope.get(name)
val clsFromScope = scopeRec?.value as? ObjClass
val clsFromImports = if (clsFromScope == null) {
importedScopes.asReversed().firstNotNullOfOrNull { it.get(name)?.value as? ObjClass }
importedModules.asReversed().firstNotNullOfOrNull { it.scope.get(name)?.value as? ObjClass }
} else {
null
}
val cls = clsFromScope ?: clsFromImports ?: return 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 }
@ -836,6 +870,12 @@ class Compiler(
}
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,
@ -854,6 +894,83 @@ class Compiler(
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 = 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
}
if (seedSlotIndex != null) {
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) {
val ref = 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)) {
@ -863,7 +980,7 @@ class Compiler(
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, preferredType)
}
}
val implicitType = implicitReceiverTypeForMember(name)
val implicitType = implicitTypeFromFunc
if (implicitType != null) {
resolutionSink?.referenceMember(name, pos, implicitType)
val ids = resolveImplicitThisMemberIds(name, pos, implicitType)
@ -873,52 +990,6 @@ class Compiler(
resolutionSink?.referenceMember(name, pos, classCtx.name)
return ClassScopeMemberRef(name, pos, classCtx.name)
}
val modulePlan = moduleSlotPlan()
val moduleEntry = modulePlan?.slots?.get(name)
if (moduleEntry != null) {
val moduleLoc = SlotLocation(
moduleEntry.index,
slotPlanStack.size - 1,
modulePlan.id,
moduleEntry.isMutable,
moduleEntry.isDelegated
)
captureLocalRef(name, moduleLoc, pos)?.let { ref ->
resolutionSink?.reference(name, pos)
return ref
}
val ref = LocalSlotRef(
name,
moduleLoc.slot,
moduleLoc.scopeId,
moduleLoc.isMutable,
moduleLoc.isDelegated,
pos,
strictSlotRefs
)
resolutionSink?.reference(name, pos)
return ref
}
val rootRecord = importManager.rootScope.objects[name]
if (rootRecord != null && rootRecord.visibility.isPublic) {
modulePlan?.let { plan ->
declareSlotNameIn(plan, name, rootRecord.isMutable, rootRecord.type == ObjRecord.Type.Delegated)
}
val rootSlot = lookupSlotLocation(name)
if (rootSlot != null) {
val ref = LocalSlotRef(
name,
rootSlot.slot,
rootSlot.scopeId,
rootSlot.isMutable,
rootSlot.isDelegated,
pos,
strictSlotRefs
)
resolutionSink?.reference(name, pos)
return ref
}
}
val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
if (classContext && extensionNames.contains(name)) {
resolutionSink?.referenceMember(name, pos)
@ -960,7 +1031,10 @@ class Compiler(
private val seedScope: Scope? = settings.seedScope
private var resolutionScriptDepth = 0
private val resolutionPredeclared = mutableSetOf<String>()
private val importedScopes = mutableListOf<Scope>()
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) ---
@ -997,6 +1071,118 @@ class Compiler(
}
}
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
when (val value = record.value) {
is ObjClass -> nameObjClass[name] = value
is ObjInstance -> nameObjClass[name] = value.objClass
}
}
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) {
moduleMatches.putIfAbsent(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) {
out.putIfAbsent(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
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
@ -1216,15 +1402,25 @@ class Compiler(
val needsSlotPlan = slotPlanStack.isEmpty()
if (needsSlotPlan) {
slotPlanStack.add(SlotPlan(mutableMapOf(), 0, nextScopeId++))
declareSlotNameIn(slotPlanStack.last(), "__PACKAGE__", isMutable = false, isDelegated = false)
declareSlotNameIn(slotPlanStack.last(), "$~", isMutable = true, isDelegated = false)
seedScope?.let { seedSlotPlanFromScope(it, includeParents = true) }
seedSlotPlanFromScope(importManager.rootScope)
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) }
seedNameObjClassFromScope(importManager.rootScope)
if (shouldSeedDefaultStdlib()) {
val stdlib = importManager.prepareImport(start, "lyng.stdlib", null)
seedResolutionFromScope(stdlib, start)
seedSlotPlanFromScope(stdlib)
importedScopes.add(stdlib)
seedNameObjClassFromScope(stdlib)
importedModules.add(ImportedModule(stdlib, start))
}
predeclareTopLevelSymbols()
}
@ -1293,16 +1489,8 @@ class Compiler(
}
}
val module = importManager.prepareImport(pos, name, null)
importedScopes.add(module)
importedModules.add(ImportedModule(module, pos))
seedResolutionFromScope(module, pos)
seedSlotPlanFromScope(module)
statements += object : Statement() {
override val pos: Pos = pos
override suspend fun execute(scope: Scope): Obj {
module.importInto(scope, null)
return ObjVoid
}
}
continue
}
}
@ -1336,7 +1524,8 @@ class Compiler(
statements.isNotEmpty() &&
codeContexts.lastOrNull() is CodeContext.Module &&
resolutionScriptDepth == 1 &&
statements.none { containsUnsupportedForBytecode(it) }
statements.none { containsUnsupportedForBytecode(it) } &&
statements.none { containsDelegatedRefs(it) }
val finalStatements = if (wrapScriptBytecode) {
val unwrapped = statements.map { unwrapBytecodeDeep(it) }
val block = InlineBlockStatement(unwrapped, start)
@ -1354,7 +1543,8 @@ class Compiler(
} else {
statements
}
Script(start, finalStatements, modulePlan)
val moduleRefs = importedModules.map { ImportBindingSource.Module(it.scope.packageName, it.pos) }
Script(start, finalStatements, modulePlan, importBindings.toMap(), moduleRefs)
}.also {
// Best-effort script end notification (use current position)
miniSink?.onScriptEnd(
@ -1415,8 +1605,23 @@ class Compiler(
val candidates = mutableListOf(typeName)
cls?.mro?.forEach { candidates.add(it.className) }
for (baseName in candidates) {
val wrapperName = extensionCallableName(baseName, memberName)
if (seedScope?.get(wrapperName) != null || importManager.rootScope.get(wrapperName) != null) {
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
}
}
@ -1578,8 +1783,8 @@ class Compiler(
}
addScope(seedScope)
addScope(importManager.rootScope)
for (scope in importedScopes) {
addScope(scope)
for (module in importedModules) {
addScope(module.scope)
}
for (name in compileClassInfos.keys) {
val cls = resolveClassByName(name) ?: continue
@ -1730,8 +1935,11 @@ class Compiler(
containsUnsupportedRef(ref.condition) || containsUnsupportedRef(ref.ifTrue) || containsUnsupportedRef(ref.ifFalse)
is ElvisRef -> containsUnsupportedRef(ref.left) || containsUnsupportedRef(ref.right)
is FieldRef -> {
val receiverClass = resolveReceiverClassForMember(ref.target)
val receiverClass = resolveReceiverClassForMember(ref.target) ?: return true
if (receiverClass == ObjDynamic.type) return true
val hasMember = receiverClass.instanceFieldIdMap()[ref.name] != null ||
receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] != null
if (!hasMember && !hasExtensionFor(receiverClass.className, ref.name)) return true
containsUnsupportedRef(ref.target)
}
is IndexRef -> containsUnsupportedRef(ref.targetRef) || containsUnsupportedRef(ref.indexRef)
@ -1747,12 +1955,24 @@ class Compiler(
is net.sergeych.lyng.obj.MapLiteralEntry.Spread -> containsUnsupportedRef(it.ref)
}
}
is CallRef -> containsUnsupportedRef(ref.target) || ref.args.any { containsUnsupportedForBytecode(it.value) }
is CallRef -> {
val targetName = when (val target = ref.target) {
is LocalVarRef -> target.name
is LocalSlotRef -> target.name
else -> null
}
if (targetName == "delay") return true
containsUnsupportedRef(ref.target) || ref.args.any { containsUnsupportedForBytecode(it.value) }
}
is MethodCallRef -> {
val receiverClass = resolveReceiverClassForMember(ref.receiver)
if (ref.name == "delay") return true
val receiverClass = resolveReceiverClassForMember(ref.receiver) ?: return true
if (receiverClass == ObjDynamic.type) return true
val hasMember = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] != null
if (!hasMember && !hasExtensionFor(receiverClass.className, ref.name)) return true
containsUnsupportedRef(ref.receiver) || ref.args.any { containsUnsupportedForBytecode(it.value) }
}
is ImplicitThisMethodCallRef -> true
is QualifiedThisMethodSlotCallRef -> true
is QualifiedThisFieldSlotRef -> true
is ClassScopeMemberRef -> true
@ -1766,7 +1986,7 @@ class Compiler(
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 DelegatedVarDeclStatement -> true
is DestructuringVarDeclStatement -> containsDelegatedRefs(target.initializer)
is IfStatement -> {
containsDelegatedRefs(target.condition) ||
@ -1812,6 +2032,18 @@ class Compiler(
private fun containsDelegatedRefs(ref: ObjRef): Boolean {
return when (ref) {
is LocalSlotRef -> ref.isDelegated
is ImplicitThisMemberRef -> {
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
val targetClass = typeName?.let { resolveClassByName(it) }
val member = targetClass?.findFirstConcreteMember(ref.name)
member?.type == ObjRecord.Type.Delegated
}
is ImplicitThisMethodCallRef -> {
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
val targetClass = typeName?.let { resolveClassByName(it) }
val member = targetClass?.findFirstConcreteMember(ref.methodName())
member?.type == ObjRecord.Type.Delegated
}
is BinaryOpRef -> containsDelegatedRefs(ref.left) || containsDelegatedRefs(ref.right)
is UnaryOpRef -> containsDelegatedRefs(ref.a)
is CastRef -> containsDelegatedRefs(ref.castValueRef()) || containsDelegatedRefs(ref.castTypeRef())
@ -1826,7 +2058,14 @@ class Compiler(
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 FieldRef -> {
val receiverClass = resolveReceiverClassForMember(ref.target)
if (receiverClass != null) {
val member = receiverClass.findFirstConcreteMember(ref.name)
if (member?.type == ObjRecord.Type.Delegated) return true
}
containsDelegatedRefs(ref.target)
}
is IndexRef -> containsDelegatedRefs(ref.targetRef) || containsDelegatedRefs(ref.indexRef)
is ListLiteralRef -> ref.entries().any {
when (it) {
@ -1841,7 +2080,14 @@ class Compiler(
}
}
is CallRef -> containsDelegatedRefs(ref.target) || ref.args.any { containsDelegatedRefs(it.value) }
is MethodCallRef -> containsDelegatedRefs(ref.receiver) || ref.args.any { containsDelegatedRefs(it.value) }
is MethodCallRef -> {
val receiverClass = resolveReceiverClassForMember(ref.receiver)
if (receiverClass != null) {
val member = receiverClass.findFirstConcreteMember(ref.name)
if (member?.type == ObjRecord.Type.Delegated) return true
}
containsDelegatedRefs(ref.receiver) || ref.args.any { containsDelegatedRefs(it.value) }
}
is StatementRef -> containsDelegatedRefs(ref.statement)
else -> false
}
@ -3939,6 +4185,15 @@ class Compiler(
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
}
@ -4020,6 +4275,7 @@ class Compiler(
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)) {
@ -4028,6 +4284,29 @@ class Compiler(
}
}
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",
@ -4489,7 +4768,7 @@ class Compiler(
}
}
val implicitThisTypeName = currentImplicitThisTypeName()
return when (left) {
val result = when (left) {
is ImplicitThisMemberRef ->
if (left.methodId == null && left.fieldId != null) {
CallRef(left, args, detectedBlockArgument, isOptional)
@ -4550,6 +4829,7 @@ class Compiler(
}
else -> CallRef(left, args, detectedBlockArgument, isOptional)
}
return result
}
private fun inferReceiverTypeFromArgs(args: List<ParsedArgument>): String? {
@ -5930,6 +6210,14 @@ class Compiler(
override suspend fun execute(scope: Scope): Obj {
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
if (isExtern) {
val rec = scope[className]
val existing = rec?.value as? ObjClass
val resolved = existing ?: resolveClassByName(className)
val stub = resolved ?: ObjInstanceClass(className).apply { this.isAbstract = true }
scope.addItem(declaredName, false, stub)
return stub
}
// Resolve parent classes by name at execution time
val parentClasses = baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name]
@ -6566,7 +6854,11 @@ class Compiler(
}
}
val fnStatements = rawFnStatements?.let { stmt ->
if (useBytecodeStatements && !containsUnsupportedForBytecode(stmt)) {
if (useBytecodeStatements &&
parentContext !is CodeContext.ClassBody &&
!containsUnsupportedForBytecode(stmt) &&
!containsDelegatedRefs(stmt)
) {
val paramKnownClasses = mutableMapOf<String, ObjClass>()
for (param in argsDeclaration.params) {
val cls = resolveTypeDeclObjClass(param.type) ?: continue
@ -6630,6 +6922,19 @@ class Compiler(
val fnCreateStatement = object : Statement() {
override val pos: Pos = start
override suspend fun execute(context: Scope): Obj {
if (actualExtern && extTypeName == null && parentContext !is CodeContext.ClassBody) {
val existing = context.get(name)
if (existing != null) {
context.addItem(
name,
false,
existing.value,
visibility,
callSignature = existing.callSignature
)
return existing.value
}
}
if (isDelegated) {
val accessType = ObjString("Callable")
val initValue = delegateExpression!!.execute(context)
@ -6758,7 +7063,9 @@ class Compiler(
)
execScope.currentClassCtx = cls
compiledFnBody.execute(execScope)
} ?: compiledFnBody.execute(thisObj.autoInstanceScope(this))
} ?: run {
compiledFnBody.execute(thisObj.autoInstanceScope(this))
}
} finally {
this.currentClassCtx = savedCtx
}
@ -7096,8 +7403,8 @@ class Compiler(
private fun resolveClassByName(name: String): ObjClass? {
val rec = seedScope?.get(name) ?: importManager.rootScope.get(name)
(rec?.value as? ObjClass)?.let { return it }
for (scope in importedScopes.asReversed()) {
val imported = scope.get(name)
for (module in importedModules.asReversed()) {
val imported = module.scope.get(name)
(imported?.value as? ObjClass)?.let { return it }
}
val info = compileClassInfos[name] ?: return null

View File

@ -33,6 +33,8 @@ class ModuleScope(
constructor(importProvider: ImportProvider, source: Source) : this(importProvider, source.startPos, source.fileName)
internal var importedModules: List<ModuleScope> = emptyList()
/**
* Import symbols into the scope. It _is called_ after the module is imported by [ImportProvider.prepareImport]
* which checks symbol availability and accessibility prior to execution.
@ -92,4 +94,3 @@ class ModuleScope(
super.get(name)
}
}

View File

@ -463,6 +463,24 @@ open class Scope(
}
}
internal fun applySlotPlanReset(plan: Map<String, Int>, records: Map<String, ObjRecord>) {
if (plan.isEmpty()) return
slots.clear()
nameToSlot.clear()
val maxIndex = plan.values.maxOrNull() ?: return
val targetSize = maxIndex + 1
repeat(targetSize) {
slots.add(ObjRecord(ObjUnset, isMutable = true))
}
for ((name, idx) in plan) {
nameToSlot[name] = idx
val record = records[name]
if (record != null && record.value !== ObjUnset) {
slots[idx] = record
}
}
}
fun applySlotPlanWithSnapshot(plan: Map<String, Int>): Map<String, Int?> {
if (plan.isEmpty()) return emptyMap()
val maxIndex = plan.values.maxOrNull() ?: return emptyMap()

View File

@ -34,13 +34,50 @@ class Script(
private val statements: List<Statement> = emptyList(),
private val moduleSlotPlan: Map<String, Int> = emptyMap(),
private val importBindings: Map<String, ImportBinding> = emptyMap(),
private val importedModules: List<ImportBindingSource.Module> = emptyList(),
// private val catchReturn: Boolean = false,
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
if (moduleSlotPlan.isNotEmpty()) {
scope.applySlotPlan(moduleSlotPlan)
seedModuleSlots(scope)
val isModuleScope = scope is ModuleScope
val shouldSeedModule = isModuleScope || scope.thisObj === ObjVoid
val moduleTarget = scope
if (moduleSlotPlan.isNotEmpty() && shouldSeedModule) {
val hasPlanMapping = moduleSlotPlan.keys.any { moduleTarget.getSlotIndexOf(it) != null }
val needsReset = moduleTarget is ModuleScope ||
moduleTarget.slotCount() == 0 ||
moduleTarget.hasSlotPlanConflict(moduleSlotPlan) ||
(!hasPlanMapping && moduleTarget.slotCount() > 0)
if (needsReset) {
val preserved = LinkedHashMap<String, ObjRecord>()
for (name in moduleSlotPlan.keys) {
moduleTarget.getLocalRecordDirect(name)?.let { preserved[name] = it }
}
moduleTarget.applySlotPlanReset(moduleSlotPlan, preserved)
for (name in moduleSlotPlan.keys) {
if (preserved.containsKey(name)) continue
val inherited = findSeedRecord(moduleTarget.parent, name)
if (inherited != null && inherited.value !== ObjUnset) {
moduleTarget.updateSlotFor(name, inherited)
}
}
} else {
moduleTarget.applySlotPlan(moduleSlotPlan)
for (name in moduleSlotPlan.keys) {
val local = moduleTarget.getLocalRecordDirect(name)
if (local != null && local.value !== ObjUnset) {
moduleTarget.updateSlotFor(name, local)
continue
}
val inherited = findSeedRecord(moduleTarget.parent, name)
if (inherited != null && inherited.value !== ObjUnset) {
moduleTarget.updateSlotFor(name, inherited)
}
}
}
}
if (shouldSeedModule) {
seedModuleSlots(moduleTarget)
}
var lastResult: Obj = ObjVoid
for (s in statements) {
@ -50,37 +87,24 @@ class Script(
}
private suspend fun seedModuleSlots(scope: Scope) {
if (importBindings.isNotEmpty()) {
seedImportBindings(scope)
return
}
val parent = scope.parent ?: return
for (name in moduleSlotPlan.keys) {
if (scope.objects.containsKey(name)) {
scope.updateSlotFor(name, scope.objects[name]!!)
continue
}
val seed = findSeedRecord(parent, name)
if (seed != null) {
if (name == "Exception" && seed.value !is ObjClass) {
scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false))
} else {
scope.updateSlotFor(name, seed)
}
continue
}
if (name == "Exception") {
scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false))
}
}
if (importBindings.isEmpty() && importedModules.isEmpty()) return
seedImportBindings(scope)
}
private suspend fun seedImportBindings(scope: Scope) {
val provider = scope.currentImportProvider
val importedModules = LinkedHashSet<ModuleScope>()
for (moduleRef in this.importedModules) {
importedModules.add(provider.prepareImport(moduleRef.pos, moduleRef.name, null))
}
if (scope is ModuleScope) {
scope.importedModules = importedModules.toList()
}
for ((name, binding) in importBindings) {
val record = when (val source = binding.source) {
is ImportBindingSource.Module -> {
val module = provider.prepareImport(source.pos, source.name, null)
importedModules.add(module)
module.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${source.name}.${binding.symbol} not found")
}
@ -99,6 +123,15 @@ class Script(
scope.updateSlotFor(name, record)
}
}
for (module in importedModules) {
for ((cls, map) in module.extensions) {
for ((symbol, record) in map) {
if (record.visibility.isPublic) {
scope.addExtension(cls, symbol, record)
}
}
}
}
}
private fun findSeedRecord(scope: Scope?, name: String): ObjRecord? {
@ -116,6 +149,15 @@ class Script(
return null
}
private fun resolveModuleScope(scope: Scope): ModuleScope? {
var current: Scope? = scope
while (current != null) {
if (current is ModuleScope) return current
current = current.parent
}
return null
}
internal fun debugStatements(): List<Statement> = statements
suspend fun execute() = execute(
@ -460,6 +502,7 @@ class Script(
// interfaces
addConst("Iterable", ObjIterable)
addConst("Collection", ObjCollection)
addConst("Iterator", ObjIterator)
addConst("Array", ObjArray)
addConst("RingBuffer", ObjRingBuffer.type)
addConst("Class", ObjClassType)

View File

@ -211,6 +211,12 @@ class BytecodeCompiler(
private fun allocSlot(): Int = nextSlot++
private fun encodeMemberId(receiverClass: ObjClass, id: Int?): Int? {
if (id == null) return null
if (receiverClass == ObjClassType) return -(id + 2)
return id
}
private fun compileRef(ref: ObjRef): CompiledValue? {
return when (ref) {
is ConstRef -> compileConst(ref.constValue)
@ -1620,13 +1626,13 @@ class BytecodeCompiler(
Pos.builtIn
)
val receiver = compileRefWithFallback(target.target, null, Pos.builtIn) ?: return null
val fieldId = receiverClass.instanceFieldIdMap()[target.name] ?: -1
val methodId = if (fieldId < 0) {
receiverClass.instanceMethodIdMap(includeAbstract = true)[target.name] ?: -1
val fieldId = receiverClass.instanceFieldIdMap()[target.name]
val methodId = if (fieldId == null) {
receiverClass.instanceMethodIdMap(includeAbstract = true)[target.name]
} else {
-1
null
}
if (fieldId < 0 && methodId < 0) {
if (fieldId == null && methodId == null) {
val extSlot = resolveExtensionSetterSlot(receiverClass, target.name)
?: throw BytecodeCompileException(
"Unknown member ${target.name} on ${receiverClass.className}",
@ -1660,8 +1666,10 @@ class BytecodeCompiler(
}
return value
}
val encodedFieldId = encodeMemberId(receiverClass, fieldId) ?: -1
val encodedMethodId = encodeMemberId(receiverClass, methodId) ?: -1
if (!target.isOptional) {
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId, methodId, value.slot)
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, encodedFieldId, encodedMethodId, value.slot)
} else {
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
@ -1672,7 +1680,7 @@ class BytecodeCompiler(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(endLabel))
)
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId, methodId, value.slot)
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, encodedFieldId, encodedMethodId, value.slot)
builder.mark(endLabel)
}
return value
@ -2048,11 +2056,13 @@ class BytecodeCompiler(
}
val fieldId = receiverClass.instanceFieldIdMap()[ref.name]
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name]
val encodedFieldId = encodeMemberId(receiverClass, fieldId)
val encodedMethodId = encodeMemberId(receiverClass, methodId)
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
val dst = allocSlot()
if (fieldId != null || methodId != null) {
if (!ref.isOptional) {
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, dst)
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, encodedFieldId ?: -1, encodedMethodId ?: -1, dst)
} else {
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
@ -2064,7 +2074,7 @@ class BytecodeCompiler(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(cmpSlot), CmdBuilder.Operand.LabelRef(nullLabel))
)
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId ?: -1, methodId ?: -1, dst)
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, encodedFieldId ?: -1, encodedMethodId ?: -1, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)
@ -2856,11 +2866,12 @@ class BytecodeCompiler(
val dst = allocSlot()
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name]
if (methodId != null) {
val encodedMethodId = encodeMemberId(receiverClass, methodId) ?: methodId
if (!ref.isOptional) {
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
val nullSlot = allocSlot()
@ -2876,7 +2887,7 @@ class BytecodeCompiler(
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, encodedMethodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)
@ -4659,43 +4670,17 @@ class BytecodeCompiler(
?: resolveReceiverClass(ref.castValueRef())
is FieldRef -> {
val targetClass = resolveReceiverClass(ref.target) ?: return null
if (targetClass == ObjString.type && ref.name == "re") {
ObjRegex.type
} else {
null
}
inferFieldReturnClass(targetClass, ref.name)
}
is MethodCallRef -> {
val targetClass = resolveReceiverClass(ref.receiver) ?: return null
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
ObjRegex.type
} else {
when (ref.name) {
"map",
"mapNotNull",
"filter",
"filterNotNull",
"drop",
"take",
"flatMap",
"flatten",
"sorted",
"sortedBy",
"sortedWith",
"reversed",
"toList",
"shuffle",
"shuffled" -> ObjList.type
"dropLast" -> ObjFlow.type
"takeLast" -> ObjRingBuffer.type
"count" -> ObjInt.type
"toSet" -> ObjSet.type
"toMap" -> ObjMap.type
"joinToString" -> ObjString.type
else -> null
}
inferMethodCallReturnClass(ref.name)
}
}
is CallRef -> inferCallReturnClass(ref)
else -> null
}
}
@ -4742,39 +4727,17 @@ class BytecodeCompiler(
?: resolveReceiverClassForScopeCollection(ref.castValueRef())
is FieldRef -> {
val targetClass = resolveReceiverClassForScopeCollection(ref.target) ?: return null
if (targetClass == ObjString.type && ref.name == "re") ObjRegex.type else null
inferFieldReturnClass(targetClass, ref.name)
}
is MethodCallRef -> {
val targetClass = resolveReceiverClassForScopeCollection(ref.receiver) ?: return null
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
ObjRegex.type
} else {
when (ref.name) {
"map",
"mapNotNull",
"filter",
"filterNotNull",
"drop",
"take",
"flatMap",
"flatten",
"sorted",
"sortedBy",
"sortedWith",
"reversed",
"toList",
"shuffle",
"shuffled" -> ObjList.type
"dropLast" -> ObjFlow.type
"takeLast" -> ObjRingBuffer.type
"count" -> ObjInt.type
"toSet" -> ObjSet.type
"toMap" -> ObjMap.type
"joinToString" -> ObjString.type
else -> null
}
inferMethodCallReturnClass(ref.name)
}
}
is CallRef -> inferCallReturnClass(ref)
else -> null
}
}
@ -4813,12 +4776,154 @@ class BytecodeCompiler(
"Regex" -> ObjRegex.type
"RegexMatch" -> ObjRegexMatch.type
"MapEntry" -> ObjMapEntry.type
"Instant" -> ObjInstant.type
"DateTime" -> ObjDateTime.type
"Duration" -> ObjDuration.type
"Exception" -> ObjException.Root
"Class" -> ObjClassType
"Callable" -> Statement.type
else -> null
}
}
private fun inferCallReturnClass(ref: CallRef): ObjClass? {
return when (val target = ref.target) {
is LocalSlotRef -> nameObjClass[target.name] ?: resolveTypeNameClass(target.name)
is LocalVarRef -> nameObjClass[target.name] ?: resolveTypeNameClass(target.name)
is ConstRef -> target.constValue as? ObjClass
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
"count" -> ObjInt.type
"toSet" -> ObjSet.type
"toMap" -> ObjMap.type
"joinToString" -> ObjString.type
"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
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
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",
"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 queueExtensionCallableNames(receiverClass: ObjClass, memberName: String) {
for (cls in receiverClass.mro) {
val name = extensionCallableName(cls.className, memberName)

View File

@ -1321,7 +1321,8 @@ class CmdCallSlot(
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
} else {
// Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now.
callee.callOn(frame.ensureScope().createChildScope(frame.ensureScope().pos, args = args))
val scope = frame.ensureScope()
callee.callOn(scope.createChildScope(scope.pos, args = args))
}
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
@ -1360,6 +1361,14 @@ class CmdListLiteral(
}
}
private fun decodeMemberId(id: Int): Pair<Int, Boolean> {
return if (id <= -2) {
Pair(-id - 2, true)
} else {
Pair(id, false)
}
}
class CmdGetMemberSlot(
internal val recvSlot: Int,
internal val fieldId: Int,
@ -1369,22 +1378,42 @@ class CmdGetMemberSlot(
override suspend fun perform(frame: CmdFrame) {
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val fieldRec = if (fieldId >= 0) {
inst?.fieldRecordForId(fieldId) ?: receiver.objClass.fieldRecordForId(fieldId)
val cls = receiver as? ObjClass
val (fieldIdResolved, fieldOnObjClass) = decodeMemberId(fieldId)
val (methodIdResolved, methodOnObjClass) = decodeMemberId(methodId)
val fieldRec = if (fieldIdResolved >= 0) {
when {
inst != null -> inst.fieldRecordForId(fieldIdResolved) ?: inst.objClass.fieldRecordForId(fieldIdResolved)
cls != null && fieldOnObjClass -> cls.objClass.fieldRecordForId(fieldIdResolved)
cls != null -> cls.fieldRecordForId(fieldIdResolved)
else -> receiver.objClass.fieldRecordForId(fieldIdResolved)
}
} else null
val rec = fieldRec ?: run {
if (methodId >= 0) {
inst?.methodRecordForId(methodId) ?: receiver.objClass.methodRecordForId(methodId)
if (methodIdResolved >= 0) {
when {
inst != null -> inst.methodRecordForId(methodIdResolved) ?: inst.objClass.methodRecordForId(methodIdResolved)
cls != null && methodOnObjClass -> cls.objClass.methodRecordForId(methodIdResolved)
cls != null -> cls.methodRecordForId(methodIdResolved)
else -> receiver.objClass.methodRecordForId(methodIdResolved)
}
} else null
} ?: frame.ensureScope().raiseSymbolNotFound("member")
val name = rec.memberName ?: "<member>"
suspend fun autoCallIfMethod(resolved: ObjRecord, recv: Obj): Obj {
return if (resolved.type == ObjRecord.Type.Fun && !resolved.isAbstract) {
resolved.value.invoke(frame.ensureScope(), resolved.receiver ?: recv, Arguments.EMPTY, resolved.declaringClass)
} else {
resolved.value
}
}
if (receiver is ObjQualifiedView) {
val resolved = receiver.readField(frame.ensureScope(), name)
frame.storeObjResult(dst, resolved.value)
frame.storeObjResult(dst, autoCallIfMethod(resolved, receiver))
return
}
val resolved = receiver.resolveRecord(frame.ensureScope(), rec, name, rec.declaringClass)
frame.storeObjResult(dst, resolved.value)
frame.storeObjResult(dst, autoCallIfMethod(resolved, receiver))
return
}
}
@ -1398,12 +1427,25 @@ class CmdSetMemberSlot(
override suspend fun perform(frame: CmdFrame) {
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val fieldRec = if (fieldId >= 0) {
inst?.fieldRecordForId(fieldId) ?: receiver.objClass.fieldRecordForId(fieldId)
val cls = receiver as? ObjClass
val (fieldIdResolved, fieldOnObjClass) = decodeMemberId(fieldId)
val (methodIdResolved, methodOnObjClass) = decodeMemberId(methodId)
val fieldRec = if (fieldIdResolved >= 0) {
when {
inst != null -> inst.fieldRecordForId(fieldIdResolved) ?: inst.objClass.fieldRecordForId(fieldIdResolved)
cls != null && fieldOnObjClass -> cls.objClass.fieldRecordForId(fieldIdResolved)
cls != null -> cls.fieldRecordForId(fieldIdResolved)
else -> receiver.objClass.fieldRecordForId(fieldIdResolved)
}
} else null
val rec = fieldRec ?: run {
if (methodId >= 0) {
inst?.methodRecordForId(methodId) ?: receiver.objClass.methodRecordForId(methodId)
if (methodIdResolved >= 0) {
when {
inst != null -> inst.methodRecordForId(methodIdResolved) ?: inst.objClass.methodRecordForId(methodIdResolved)
cls != null && methodOnObjClass -> cls.objClass.methodRecordForId(methodIdResolved)
cls != null -> cls.methodRecordForId(methodIdResolved)
else -> receiver.objClass.methodRecordForId(methodIdResolved)
}
} else null
} ?: frame.ensureScope().raiseSymbolNotFound("member")
val name = rec.memberName ?: "<member>"
@ -1429,8 +1471,14 @@ class CmdCallMemberSlot(
}
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val rec = inst?.methodRecordForId(methodId)
?: receiver.objClass.methodRecordForId(methodId)
val cls = receiver as? ObjClass
val (methodIdResolved, methodOnObjClass) = decodeMemberId(methodId)
val rec = inst?.methodRecordForId(methodIdResolved)
?: when {
cls != null && methodOnObjClass -> cls.objClass.methodRecordForId(methodIdResolved)
cls != null -> cls.methodRecordForId(methodIdResolved)
else -> receiver.objClass.methodRecordForId(methodIdResolved)
}
?: frame.ensureScope().raiseError("member id $methodId not found on ${receiver.objClass.className}")
val callArgs = frame.buildArguments(argBase, argCount)
val name = rec.memberName ?: "<member>"
@ -1605,15 +1653,51 @@ class CmdFrame(
}
private fun resolveModuleScope(scope: Scope): Scope {
var current: Scope? = scope
var last: Scope = scope
while (current != null) {
val moduleSlotName = fn.scopeSlotNames.indices
.firstOrNull { fn.scopeSlotIsModule.getOrNull(it) == true }
?.let { fn.scopeSlotNames[it] }
if (moduleSlotName != null) {
findScopeWithSlot(scope, moduleSlotName)?.let { return it }
}
findModuleScope(scope)?.let { return it }
return scope
}
private fun findScopeWithSlot(scope: Scope, slotName: String): Scope? {
val visited = HashSet<Scope>(16)
val queue = ArrayDeque<Scope>()
queue.add(scope)
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
if (!visited.add(current)) continue
if (current.getSlotIndexOf(slotName) != null) return current
current.parent?.let { queue.add(it) }
if (current is ClosureScope) {
queue.add(current.closureScope)
} else if (current is ApplyScope) {
queue.add(current.applied)
}
}
return null
}
private fun findModuleScope(scope: Scope): Scope? {
val visited = HashSet<Scope>(16)
val queue = ArrayDeque<Scope>()
queue.add(scope)
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
if (!visited.add(current)) continue
if (current is ModuleScope) return current
if (current.parent is ModuleScope) return current
last = current
current = current.parent
current.parent?.let { queue.add(it) }
if (current is ClosureScope) {
queue.add(current.closureScope)
} else if (current is ApplyScope) {
queue.add(current.applied)
}
}
return last
return null
}
fun ensureScope(): Scope {
@ -1737,7 +1821,7 @@ class CmdFrame(
scopeDepth -= 1
}
fun getObj(slot: Int): Obj {
suspend fun getObj(slot: Int): Obj {
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot)
} else {
@ -1755,7 +1839,7 @@ class CmdFrame(
}
}
fun getInt(slot: Int): Long {
suspend fun getInt(slot: Int): Long {
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toLong()
} else {
@ -1786,7 +1870,7 @@ class CmdFrame(
frame.setInt(local, value)
}
fun getReal(slot: Int): Double {
suspend fun getReal(slot: Int): Double {
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toDouble()
} else {
@ -1811,7 +1895,7 @@ class CmdFrame(
}
}
fun getBool(slot: Int): Boolean {
suspend fun getBool(slot: Int): Boolean {
return if (slot < fn.scopeSlotCount) {
getScopeSlotValue(slot).toBool()
} else {
@ -1850,7 +1934,7 @@ class CmdFrame(
addrScopeSlots[addrSlot] = scopeSlot
}
fun getAddrObj(addrSlot: Int): Obj {
suspend fun getAddrObj(addrSlot: Int): Obj {
return getScopeSlotValueAtAddr(addrSlot)
}
@ -1858,7 +1942,7 @@ class CmdFrame(
setScopeSlotValueAtAddr(addrSlot, value)
}
fun getAddrInt(addrSlot: Int): Long {
suspend fun getAddrInt(addrSlot: Int): Long {
return getScopeSlotValueAtAddr(addrSlot).toLong()
}
@ -1866,7 +1950,7 @@ class CmdFrame(
setScopeSlotValueAtAddr(addrSlot, ObjInt.of(value))
}
fun getAddrReal(addrSlot: Int): Double {
suspend fun getAddrReal(addrSlot: Int): Double {
return getScopeSlotValueAtAddr(addrSlot).toDouble()
}
@ -1874,7 +1958,7 @@ class CmdFrame(
setScopeSlotValueAtAddr(addrSlot, ObjReal.of(value))
}
fun getAddrBool(addrSlot: Int): Boolean {
suspend fun getAddrBool(addrSlot: Int): Boolean {
return getScopeSlotValueAtAddr(addrSlot).toBool()
}
@ -1882,11 +1966,18 @@ class CmdFrame(
setScopeSlotValueAtAddr(addrSlot, if (value) ObjTrue else ObjFalse)
}
fun slotToObj(slot: Int): Obj {
suspend fun slotToObj(slot: Int): Obj {
if (slot < fn.scopeSlotCount) {
return getScopeSlotValue(slot)
}
val local = slot - fn.scopeSlotCount
val localName = fn.localSlotNames.getOrNull(local)
if (localName != null) {
val rec = scope.getLocalRecordDirect(localName) ?: scope.localBindings[localName]
if (rec != null && (rec.type == ObjRecord.Type.Delegated || rec.type == ObjRecord.Type.Property || rec.value is ObjProperty)) {
return scope.resolve(rec, localName)
}
}
return when (frame.getSlotTypeCode(local)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(local))
SlotType.REAL.code -> ObjReal.of(frame.getReal(local))
@ -2067,14 +2158,20 @@ class CmdFrame(
}
}
private fun getScopeSlotValue(slot: Int): Obj {
private suspend fun getScopeSlotValue(slot: Int): Obj {
val target = scopeTarget(slot)
val index = ensureScopeSlot(target, slot)
val record = target.getSlotRecord(index)
val direct = record.value
if (direct is FrameSlotRef) return direct.read()
if (direct !== ObjUnset) return direct
val name = fn.scopeSlotNames[slot] ?: return record.value
val name = fn.scopeSlotNames[slot]
if (direct !== ObjUnset) {
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
return target.resolve(record, name)
}
return direct
}
if (name == null) return record.value
val resolved = target.get(name) ?: return record.value
if (resolved.value !== ObjUnset) {
target.updateSlotFor(name, resolved)
@ -2082,15 +2179,21 @@ class CmdFrame(
return resolved.value
}
private fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
private suspend fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
val index = addrIndices[addrSlot]
val record = target.getSlotRecord(index)
val direct = record.value
if (direct is FrameSlotRef) return direct.read()
if (direct !== ObjUnset) return direct
val slotId = addrScopeSlots[addrSlot]
val name = fn.scopeSlotNames[slotId] ?: return record.value
val name = fn.scopeSlotNames.getOrNull(slotId)
if (direct !== ObjUnset) {
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
return target.resolve(record, name)
}
return direct
}
if (name == null) return record.value
val resolved = target.get(name) ?: return record.value
if (resolved.value !== ObjUnset) {
target.updateSlotFor(name, resolved)

View File

@ -65,7 +65,8 @@ open class Obj {
fun isInstanceOf(someClass: Obj) = someClass === objClass ||
objClass.allParentsSet.contains(someClass) ||
someClass == rootObjectType ||
(someClass is ObjClass && objClass.allImplementingNames.contains(someClass.className))
(someClass is ObjClass && (objClass.allImplementingNames.contains(someClass.className) ||
objClass.className == someClass.className))
fun isInstanceOf(className: String) =
objClass.mro.any { it.className == className } ||
@ -141,6 +142,15 @@ open class Obj {
}
}
}
scope.findExtension(objClass, name)?.let { ext ->
if (ext.type == ObjRecord.Type.Property) {
if (args.isEmpty()) {
return (ext.value as ObjProperty).callGetter(scope, this, ext.declaringClass)
}
} else if (ext.type != ObjRecord.Type.Delegated) {
return ext.value.invoke(scope, this, args, ext.declaringClass)
}
}
return onNotFoundResult?.invoke()
?: scope.raiseError(
@ -472,6 +482,14 @@ open class Obj {
}
}
}
scope.findExtension(objClass, name)?.let { ext ->
return if (ext.type == ObjRecord.Type.Property) {
val prop = ext.value as ObjProperty
ObjRecord(prop.callGetter(scope, this, ext.declaringClass), isMutable = false)
} else {
ext.copy(value = ext.value.invoke(scope, this, Arguments.EMPTY, ext.declaringClass))
}
}
scope.raiseError(
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
@ -486,6 +504,7 @@ open class Obj {
if (getValueRec == null || getValueRec.declaringClass?.className == "Delegate") {
val wrapper = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(s: Scope): Obj {
val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj
val allArgs = (listOf(th2, ObjString(name)) + s.args.list).toTypedArray()
@ -587,16 +606,19 @@ open class Obj {
scope.raiseNotImplemented()
}
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj =
if (PerfFlags.SCOPE_POOL)
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj {
val usePool = PerfFlags.SCOPE_POOL && this !is Statement
return if (usePool) {
scope.withChildFrame(args, newThisObj = thisObj) { child ->
if (declaringClass != null) child.currentClassCtx = declaringClass
callOn(child)
}
else
} else {
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also {
if (declaringClass != null) it.currentClassCtx = declaringClass
})
}
}
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn(

View File

@ -92,34 +92,48 @@ open class ObjException(
companion object {
private var stackTraceCaptureDepth = 0
suspend fun captureStackTrace(scope: Scope): ObjList {
val result = ObjList()
val maybeCls = scope.get("StackTraceEntry")?.value as? ObjClass
val nestedCapture = stackTraceCaptureDepth > 0
stackTraceCaptureDepth += 1
val maybeCls = if (nestedCapture) null else scope.get("StackTraceEntry")?.value as? ObjClass
var s: Scope? = scope
var lastPos: Pos? = null
while (s != null) {
val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) {
if( (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) ) {
if (maybeCls != null) {
result.list += maybeCls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
} else {
// Fallback textual entry if StackTraceEntry class is not available in this scope
result.list += ObjString("#${pos.source.objSourceName}:${pos.line+1}:${pos.column+1}: ${pos.currentLine}")
try {
while (s != null) {
val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) {
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
val fallback =
ObjString("#${pos.source.objSourceName}:${pos.line+1}:${pos.column+1}: ${pos.currentLine}")
if (maybeCls != null) {
try {
result.list += maybeCls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
} catch (e: Throwable) {
// Fallback textual entry if StackTraceEntry fails to instantiate
result.list += fallback
}
} else {
// Fallback textual entry if StackTraceEntry class is not available in this scope
result.list += fallback
}
lastPos = pos
}
lastPos = pos
}
s = s.parent
}
s = s.parent
return result
} finally {
stackTraceCaptureDepth -= 1
}
return result
}
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {

View File

@ -174,6 +174,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (getValueRec == null || getValueRec.declaringClass?.className == "Delegate") {
val wrapper = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(s: Scope): Obj {
val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj
val allArgs = (listOf(th2, ObjString(name)) + s.args.list).toTypedArray()
@ -364,6 +365,16 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// Fast path for public members when outside any class context
if (caller == null) {
objClass.members[name]?.let { rec ->
if (rec.visibility == Visibility.Public && !rec.isAbstract) {
val decl = rec.declaringClass
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, decl)
}
}
}
objClass.publicMemberResolution[name]?.let { key ->
methodRecordForKey(key)?.let { rec ->
if (rec.visibility == Visibility.Public && !rec.isAbstract) {

View File

@ -286,5 +286,3 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
}
}

View File

@ -445,38 +445,64 @@ class CastRef(
override suspend fun get(scope: Scope): ObjRecord {
val v0 = valueRef.evalValue(scope)
val t = typeRef.evalValue(scope)
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${t} is not the class instance")
// unwrap qualified views
val v = when (v0) {
is ObjQualifiedView -> v0.instance
else -> v0
}
return if (v.isInstanceOf(target)) {
// For instances, return a qualified view to enforce ancestor-start dispatch
if (v is ObjInstance) ObjQualifiedView(v, target).asReadonly else v.asReadonly
} else {
if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${target.className}"
)
return when (t) {
is ObjClass -> {
if (v.isInstanceOf(t)) {
// For instances, return a qualified view to enforce ancestor-start dispatch
if (v is ObjInstance) ObjQualifiedView(v, t).asReadonly else v.asReadonly
} else {
if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${t.className}"
)
}
}
is ObjTypeExpr -> {
if (matchesTypeDecl(scope, v, t.typeDecl)) {
v.asReadonly
} else {
if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${t.typeDecl}"
)
}
}
else -> scope.raiseClassCastError("${t} is not the class instance")
}
}
override suspend fun evalValue(scope: Scope): Obj {
val v0 = valueRef.evalValue(scope)
val t = typeRef.evalValue(scope)
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${t} is not the class instance")
// unwrap qualified views
val v = when (v0) {
is ObjQualifiedView -> v0.instance
else -> v0
}
return if (v.isInstanceOf(target)) {
// For instances, return a qualified view to enforce ancestor-start dispatch
if (v is ObjInstance) ObjQualifiedView(v, target) else v
} else {
if (isNullable) ObjNull else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${target.className}"
)
return when (t) {
is ObjClass -> {
if (v.isInstanceOf(t)) {
// For instances, return a qualified view to enforce ancestor-start dispatch
if (v is ObjInstance) ObjQualifiedView(v, t) else v
} else {
if (isNullable) ObjNull else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${t.className}"
)
}
}
is ObjTypeExpr -> {
if (matchesTypeDecl(scope, v, t.typeDecl)) {
v
} else {
if (isNullable) ObjNull else scope.raiseClassCastError(
"Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${t.typeDecl}"
)
}
}
else -> scope.raiseClassCastError("${t} is not the class instance")
}
}
}
@ -1175,6 +1201,10 @@ class FieldRef(
if (rec.receiver != null && rec.declaringClass != null) {
return rec.receiver!!.resolveRecord(scope, rec, name, rec.declaringClass).value
}
if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) {
val receiver = rec.receiver ?: base
return rec.value.invoke(scope, receiver, Arguments.EMPTY, rec.declaringClass)
}
return rec.value
}
@ -2300,6 +2330,11 @@ class LocalSlotRef(
scope.raiseError("slot index out of range for $name")
}
val rec = owner.getSlotRecord(slotIndex)
val direct = owner.getLocalRecordDirect(name)
if (direct != null && direct !== rec) {
owner.updateSlotFor(name, direct)
return direct
}
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
@ -2324,6 +2359,11 @@ class LocalSlotRef(
scope.raiseError("slot index out of range for $name")
}
val rec = owner.getSlotRecord(slotIndex)
val direct = owner.getLocalRecordDirect(name)
if (direct != null && direct !== rec) {
owner.updateSlotFor(name, direct)
return scope.resolve(direct, name)
}
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}

View File

@ -68,11 +68,17 @@ abstract class ImportProvider(
suspend fun newStdScope(pos: Pos = Pos.builtIn): Scope =
cachedStdScope.get {
newModuleAt(pos).also {
it.eval("import lyng.stdlib\n")
val module = newModuleAt(pos)
val stdlib = prepareImport(pos, "lyng.stdlib", null)
val plan = LinkedHashMap<String, Int>()
for ((name, record) in stdlib.objects) {
if (!record.visibility.isPublic) continue
plan[name] = plan.size
}
if (plan.isNotEmpty()) module.applySlotPlan(plan)
stdlib.importInto(module, null)
module
}.createChildScope()
}