From 7b70a37e90ea1dd524fe757485de97e8cc8a822c Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 8 Feb 2026 14:04:25 +0300 Subject: [PATCH] Step 1: module import slots and extension lookup --- bytecode_migration_plan.md | 61 +-- .../net/sergeych/lyng/BlockStatement.kt | 10 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 471 +++++++++++++++--- .../kotlin/net/sergeych/lyng/ModuleScope.kt | 3 +- .../kotlin/net/sergeych/lyng/Scope.kt | 18 + .../kotlin/net/sergeych/lyng/Script.kt | 95 +++- .../lyng/bytecode/BytecodeCompiler.kt | 235 ++++++--- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 171 +++++-- .../kotlin/net/sergeych/lyng/obj/Obj.kt | 30 +- .../net/sergeych/lyng/obj/ObjException.kt | 52 +- .../net/sergeych/lyng/obj/ObjInstance.kt | 11 + .../net/sergeych/lyng/obj/ObjInstant.kt | 2 - .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 72 ++- .../sergeych/lyng/pacman/ImportProvider.kt | 12 +- 14 files changed, 954 insertions(+), 289 deletions(-) diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 0c0319b..ddb17dc 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -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. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt index bac0a79..20274d0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt @@ -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) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index c9d8edd..a754ee4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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> = mutableMapOf() private val nameObjClass: MutableMap = 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() - private val importedScopes = mutableListOf() + private data class ImportedModule(val scope: ModuleScope, val pos: Pos) + private data class ImportBindingResolution(val binding: ImportBinding, val record: ObjRecord) + private val importedModules = mutableListOf() + private val importBindings = mutableMapOf() private val enumEntriesByName = mutableMapOf>() // --- 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>() + for (module in importedModules.asReversed()) { + val found = LinkedHashMap>() + 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, + out: MutableMap> + ) { + 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): 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() 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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt index cb70448..306792a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt @@ -33,6 +33,8 @@ class ModuleScope( constructor(importProvider: ImportProvider, source: Source) : this(importProvider, source.startPos, source.fileName) + internal var importedModules: List = 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) } } - diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index eddab22..58d1ccd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -463,6 +463,24 @@ open class Scope( } } + internal fun applySlotPlanReset(plan: Map, records: Map) { + 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): Map { if (plan.isEmpty()) return emptyMap() val maxIndex = plan.values.maxOrNull() ?: return emptyMap() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 4b19eff..625ed1e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -34,13 +34,50 @@ class Script( private val statements: List = emptyList(), private val moduleSlotPlan: Map = emptyMap(), private val importBindings: Map = emptyMap(), + private val importedModules: List = 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() + 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() + 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 = 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 31a836a..6d633e4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 73e42ef..93aa028 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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 { + 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 ?: "" + 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 ?: "" @@ -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 ?: "" @@ -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(16) + val queue = ArrayDeque() + 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(16) + val queue = ArrayDeque() + 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 326638e..01eadc2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt index af24a2d..a76a8f7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index d5d1550..58f956f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt index ed50dfc..daf7e28 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt @@ -286,5 +286,3 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru } } - - diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 8c7bb50..266f9de 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -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")) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportProvider.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportProvider.kt index 77073e1..da19417 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportProvider.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportProvider.kt @@ -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() + 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() } -