diff --git a/README.md b/README.md index cb79d83..d687332 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ Point(x:, y:).dist() //< 5 fun swapEnds(first, args..., last, f) { f( last, ...args, first) } + +class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } +} +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +assertEquals(A.One, A.E.One) ``` - extremely simple Kotlin integration on any platform (JVM, JS, WasmJS, Lunux, MacOS, iOS, Windows) diff --git a/docs/OOP.md b/docs/OOP.md index 4057f84..633aee5 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -113,6 +113,48 @@ val handler = object { - **Serialization**: Anonymous objects are **not serializable**. Attempting to encode an anonymous object via `Lynon` will throw a `SerializationException`. This is because their class definition is transient and cannot be safely restored in a different session or process. - **Type Identity**: Every object expression creates a unique anonymous class. Two identical object expressions will result in two different classes with distinct type identities. +## Nested Declarations + +Lyng allows classes, objects, enums, and type aliases to be declared inside another class. These declarations live in the **class namespace** (not the instance), so they do not capture an outer instance and are accessed with a qualifier. + +```lyng +class A { + class B(x?) + object Inner { val foo = "bar" } + type Alias = B + enum E { One, Two } +} + +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +``` + +Rules: +- **Qualified access**: use `Outer.Inner` for nested classes/objects/enums/aliases. Inside `Outer` you can refer to them by unqualified name unless shadowed. +- **No inner semantics**: nested declarations do not capture an instance of the outer class. They are resolved at compile time. +- **Visibility**: `private` restricts a nested declaration to the declaring class body (not visible from outside or subclasses). +- **Reflection name**: a nested class reports `Outer.Inner` (e.g., `A.B::class.name` is `"A.B"`). +- **Type aliases**: behave as aliases of the qualified nested type and are expanded by the type system. + +### Lifted Enum Entries + +Enums can optionally lift their entries into the surrounding class namespace using `*`: + +```lyng +class A { + enum E* { One, Two } +} + +assertEquals(A.One, A.E.One) +assertEquals(A.Two, A.E.Two) +``` + +Notes: +- `E*` exposes entries in `A` as if they were direct members (`A.One`). +- If a name would conflict with an existing class member, compilation fails (no implicit fallback). +- Without `*`, use the normal `A.E.One` form. + ## Properties Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors. diff --git a/docs/tutorial.md b/docs/tutorial.md index 5f9a054..aa6f787 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -107,6 +107,23 @@ Singleton objects are declared using the `object` keyword. They define a class a Logger.log("Hello singleton!") +## Nested Declarations (short) + +Classes, objects, and enums can be declared inside another class. They live in the class namespace (no outer instance capture), so you access them with a qualifier: + + class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } + } + + val ab = A.B() + assertEquals(ab.x, null) + assertEquals(A.Inner.foo, "bar") + assertEquals(A.One, A.E.One) + +See [OOP notes](OOP.md#nested-declarations) for rules, visibility, and enum lifting details. + ## Delegation (briefly) You can delegate properties and functions to other objects using the `by` keyword. This is perfect for patterns like `lazy` initialization. diff --git a/docs/whats_new.md b/docs/whats_new.md index 59bcddb..a27e6a6 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -108,6 +108,24 @@ object Config { Config.show() ``` +### Nested Declarations and Lifted Enums +You can now declare classes, objects, enums, and type aliases inside another class. These nested declarations live in the class namespace (no outer instance capture) and are accessed with a qualifier. + +```lyng +class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } +} + +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +assertEquals(A.One, A.E.One) +``` + +The `*` on `enum E*` lifts entries into the enclosing class namespace (compile-time error on ambiguity). + ### Object Expressions You can now create anonymous objects that inherit from classes or interfaces using the `object : Base { ... }` syntax. These expressions capture their lexical scope and support multiple inheritance. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 5f756ba..16703ba 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng import net.sergeych.lyng.miniast.MiniTypeRef import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjList +import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjRecord /** @@ -61,12 +62,20 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) } } if (!hasComplex) { - if (arguments.list.size != params.size) + if (arguments.list.size > params.size) scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + if (arguments.list.size < params.size) { + for (i in arguments.list.size until params.size) { + val a = params[i] + if (!a.type.isNullable) { + scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + } + } + } for (i in params.indices) { val a = params[i] - val value = arguments.list[i] + val value = if (i < arguments.list.size) arguments.list[i] else ObjNull val recordType = if (declaringClass != null && a.accessType != null) { ObjRecord.Type.ConstructorField } else { @@ -103,6 +112,11 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) ) } + suspend fun missingValue(a: Item, error: String): Obj { + return a.defaultValue?.execute(scope) + ?: if (a.type.isNullable) ObjNull else scope.raiseIllegalArgument(error) + } + // Prepare positional args and parameter count, handle tail-block binding val callArgs: List val paramsSize: Int @@ -181,8 +195,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) assign(a, namedValues[i]!!) } else { val value = if (hp < callArgs.size) callArgs[hp++] - else a.defaultValue?.execute(scope) - ?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})") + else missingValue(a, "too few arguments for the call (missing ${a.name})") assign(a, value) } i++ @@ -202,8 +215,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) assign(a, namedValues[i]!!) } else { val value = if (tp >= headPosBound) callArgs[tp--] - else a.defaultValue?.execute(scope) - ?: scope.raiseIllegalArgument("too few arguments for the call") + else missingValue(a, "too few arguments for the call") assign(a, value) } i-- diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index 75c6168..3a6c6fc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -31,6 +31,7 @@ sealed class CodeContext { var typeParamDecls: List = emptyList() val pendingInitializations = mutableMapOf() val declaredMembers = mutableSetOf() + val classScopeMembers = mutableSetOf() val memberOverrides = mutableMapOf() val memberFieldIds = mutableMapOf() val memberMethodIds = mutableMapOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a62a142..cb48802 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -141,6 +141,8 @@ class Compiler( private val callableReturnTypeByName: MutableMap = mutableMapOf() private val lambdaReturnTypeByRef: MutableMap = mutableMapOf() private val classFieldTypesByName: MutableMap> = mutableMapOf() + private val classScopeMembersByClassName: MutableMap> = mutableMapOf() + private val classScopeCallableMembersByClassName: MutableMap> = mutableMapOf() private val encodedPayloadTypeByScopeId: MutableMap> = mutableMapOf() private val encodedPayloadTypeByName: MutableMap = mutableMapOf() @@ -330,10 +332,61 @@ class Compiler( } } } - "class", "object" -> { + } + } + else -> {} + } + } + } finally { + cc.restorePos(saved) + } + } + + private fun predeclareClassScopeMembers( + className: String, + target: MutableSet, + callableTarget: MutableSet + ) { + val saved = cc.savePos() + var depth = 0 + val modifiers = setOf( + "public", "private", "protected", "internal", + "override", "abstract", "extern", "static", "transient", "open", "closed" + ) + fun nextNonWs(): Token { + var t = cc.next() + while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { + t = cc.next() + } + return t + } + try { + while (cc.hasNext()) { + var t = cc.next() + when (t.type) { + Token.Type.LBRACE -> depth++ + Token.Type.RBRACE -> if (depth == 0) break else depth-- + Token.Type.ID -> if (depth == 0) { + var sawStatic = false + while (t.type == Token.Type.ID && t.value in modifiers) { + if (t.value == "static") sawStatic = true + t = nextNonWs() + } + when (t.value) { + "class" -> { val nameToken = nextNonWs() if (nameToken.type == Token.Type.ID) { target.add(nameToken.value) + callableTarget.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + registerClassScopeCallableMember(className, nameToken.value) + } + } + "object" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) } } "enum" -> { @@ -341,6 +394,25 @@ class Compiler( val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next if (nameToken.type == Token.Type.ID) { target.add(nameToken.value) + callableTarget.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + registerClassScopeCallableMember(className, nameToken.value) + } + } + "type" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + } + } + "fun", "fn", "val", "var" -> { + if (sawStatic) { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + } } } } @@ -353,6 +425,24 @@ class Compiler( } } + private fun registerClassScopeMember(className: String, name: String) { + classScopeMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name) + } + + private fun registerClassScopeCallableMember(className: String, name: String) { + classScopeCallableMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name) + } + + private fun isClassScopeCallableMember(className: String, name: String): Boolean { + return classScopeCallableMembersByClassName[className]?.contains(name) == true + } + + private fun registerClassScopeFieldType(ownerClassName: String?, memberName: String, memberClassName: String) { + if (ownerClassName == null) return + val memberClass = resolveClassByName(memberClassName) ?: return + classFieldTypesByName.getOrPut(ownerClassName) { mutableMapOf() }[memberName] = memberClass + } + private fun resolveCompileClassInfo(name: String): CompileClassInfo? { compileClassInfos[name]?.let { return it } val scopeRec = seedScope?.get(name) ?: importManager.rootScope.get(name) @@ -509,6 +599,11 @@ class Compiler( return null } + private fun currentEnclosingClassName(): String? { + val ctx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + return ctx?.name + } + private fun currentTypeParams(): Set { val result = mutableSetOf() pendingTypeParamStack.lastOrNull()?.let { result.addAll(it) } @@ -597,21 +692,20 @@ class Compiler( } private suspend fun parseTypeAliasDeclaration(): Statement { - if (codeContexts.lastOrNull() is CodeContext.ClassBody) { - throw ScriptError(cc.currentPos(), "type alias is not allowed in class body") - } val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected") - val name = nameToken.value - if (typeAliases.containsKey(name)) { - throw ScriptError(nameToken.pos, "type alias $name already declared") + val declaredName = nameToken.value + val outerClassName = currentEnclosingClassName() + val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName + if (typeAliases.containsKey(qualifiedName)) { + throw ScriptError(nameToken.pos, "type alias $qualifiedName already declared") } - if (resolveTypeDeclObjClass(TypeDecl.Simple(name, false)) != null) { - throw ScriptError(nameToken.pos, "type alias $name conflicts with existing class") + if (resolveTypeDeclObjClass(TypeDecl.Simple(qualifiedName, false)) != null) { + throw ScriptError(nameToken.pos, "type alias $qualifiedName conflicts with existing class") } val typeParams = parseTypeParamList() val uniqueParams = typeParams.map { it.name }.toSet() if (uniqueParams.size != typeParams.size) { - throw ScriptError(nameToken.pos, "type alias $name has duplicate type parameters") + throw ScriptError(nameToken.pos, "type alias $qualifiedName has duplicate type parameters") } val typeParamNames = uniqueParams if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames) @@ -619,25 +713,30 @@ class Compiler( cc.skipWsTokens() val eq = cc.nextNonWhitespace() if (eq.type != Token.Type.ASSIGN) { - throw ScriptError(eq.pos, "type alias $name expects '='") + throw ScriptError(eq.pos, "type alias $qualifiedName expects '='") } parseTypeExpressionWithMini().first } finally { if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast() } - val alias = TypeAliasDecl(name, typeParams, body, nameToken.pos) - typeAliases[name] = alias - declareLocalName(name, isMutable = false) - resolutionSink?.declareSymbol(name, SymbolKind.LOCAL, isMutable = false, pos = nameToken.pos) + val alias = TypeAliasDecl(qualifiedName, typeParams, body, nameToken.pos) + typeAliases[qualifiedName] = alias + declareLocalName(declaredName, isMutable = false) + resolutionSink?.declareSymbol(declaredName, SymbolKind.LOCAL, isMutable = false, pos = nameToken.pos) + if (outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + outerCtx?.classScopeMembers?.add(declaredName) + registerClassScopeMember(outerClassName, declaredName) + } pendingDeclDoc = null val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos) val initStmt = ExpressionStatement(aliasExpr, nameToken.pos) val slotPlan = slotPlanStack.lastOrNull() - val slotIndex = slotPlan?.slots?.get(name)?.index + val slotIndex = slotPlan?.slots?.get(declaredName)?.index val scopeId = slotPlan?.id return VarDeclStatement( - name = name, + name = declaredName, isMutable = false, visibility = Visibility.Public, initializer = initStmt, @@ -749,6 +848,10 @@ class Compiler( val ids = resolveImplicitThisMemberIds(name, pos, implicitType) return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) } + if (classCtx != null && classCtx.classScopeMembers.contains(name)) { + resolutionSink?.referenceMember(name, pos, classCtx.name) + return ClassScopeMemberRef(name, pos, classCtx.name) + } val modulePlan = moduleSlotPlan() val moduleEntry = modulePlan?.slots?.get(name) if (moduleEntry != null) { @@ -1634,6 +1737,7 @@ class Compiler( } is QualifiedThisMethodSlotCallRef -> true is QualifiedThisFieldSlotRef -> true + is ClassScopeMemberRef -> true else -> false } } @@ -2015,56 +2119,65 @@ class Compiler( when (nt.type) { Token.Type.LPAREN -> { cc.next() - // instance method call - val receiverType = if (next.value == "apply" || next.value == "run") { - inferReceiverTypeFromRef(left) - } else null - val parsed = parseArgs(receiverType) - val args = parsed.first - val tailBlock = parsed.second - if (left is LocalVarRef && left.name == "scope") { - val first = args.firstOrNull()?.value - val const = (first as? ExpressionStatement)?.ref as? ConstRef - val name = const?.constValue as? ObjString - if (name != null) { - resolutionSink?.referenceReflection(name.value, next.pos) + if (shouldTreatAsClassScopeCall(left, next.value)) { + val parsed = parseArgs(null) + val args = parsed.first + val tailBlock = parsed.second + isCall = true + val field = FieldRef(left, next.value, isOptional) + operand = CallRef(field, args, tailBlock, isOptional) + } else { + // instance method call + val receiverType = if (next.value == "apply" || next.value == "run") { + inferReceiverTypeFromRef(left) + } else null + val parsed = parseArgs(receiverType) + val args = parsed.first + val tailBlock = parsed.second + if (left is LocalVarRef && left.name == "scope") { + val first = args.firstOrNull()?.value + val const = (first as? ExpressionStatement)?.ref as? ConstRef + val name = const?.constValue as? ObjString + if (name != null) { + resolutionSink?.referenceReflection(name.value, next.pos) + } } - } - isCall = true - operand = when (left) { - is LocalVarRef -> if (left.name == "this") { - resolutionSink?.referenceMember(next.value, next.pos) - val implicitType = currentImplicitThisTypeName() - val ids = resolveMemberIds(next.value, next.pos, implicitType) - ThisMethodSlotCallRef(next.value, ids.methodId, args, tailBlock, isOptional) - } else if (left.name == "scope") { - if (next.value == "get" || next.value == "set") { - val first = args.firstOrNull()?.value - val const = (first as? ExpressionStatement)?.ref as? ConstRef - val name = const?.constValue as? ObjString - if (name != null) { - resolutionSink?.referenceReflection(name.value, next.pos) + isCall = true + operand = when (left) { + is LocalVarRef -> if (left.name == "this") { + resolutionSink?.referenceMember(next.value, next.pos) + val implicitType = currentImplicitThisTypeName() + val ids = resolveMemberIds(next.value, next.pos, implicitType) + ThisMethodSlotCallRef(next.value, ids.methodId, args, tailBlock, isOptional) + } else if (left.name == "scope") { + if (next.value == "get" || next.value == "set") { + val first = args.firstOrNull()?.value + val const = (first as? ExpressionStatement)?.ref as? ConstRef + val name = const?.constValue as? ObjString + if (name != null) { + resolutionSink?.referenceReflection(name.value, next.pos) + } } + MethodCallRef(left, next.value, args, tailBlock, isOptional) + } else { + enforceReceiverTypeForMember(left, next.value, next.pos) + MethodCallRef(left, next.value, args, tailBlock, isOptional) } - MethodCallRef(left, next.value, args, tailBlock, isOptional) - } else { - enforceReceiverTypeForMember(left, next.value, next.pos) - MethodCallRef(left, next.value, args, tailBlock, isOptional) - } - is QualifiedThisRef -> - QualifiedThisMethodSlotCallRef( - left.typeName, - next.value, - resolveMemberIds(next.value, next.pos, left.typeName).methodId, - args, - tailBlock, - isOptional - ).also { - resolutionSink?.referenceMember(next.value, next.pos, left.typeName) + is QualifiedThisRef -> + QualifiedThisMethodSlotCallRef( + left.typeName, + next.value, + resolveMemberIds(next.value, next.pos, left.typeName).methodId, + args, + tailBlock, + isOptional + ).also { + resolutionSink?.referenceMember(next.value, next.pos, left.typeName) + } + else -> { + enforceReceiverTypeForMember(left, next.value, next.pos) + MethodCallRef(left, next.value, args, tailBlock, isOptional) } - else -> { - enforceReceiverTypeForMember(left, next.value, next.pos) - MethodCallRef(left, next.value, args, tailBlock, isOptional) } } } @@ -2872,7 +2985,15 @@ class Compiler( pos: Pos, seen: MutableSet ): TypeDecl? { - val alias = typeAliases[name] ?: typeAliases[name.substringAfterLast('.')] ?: return null + val alias = run { + if (!name.contains('.')) { + val classCtx = currentEnclosingClassName() + if (classCtx != null) { + typeAliases["$classCtx.$name"]?.let { return@run it } + } + } + typeAliases[name] ?: typeAliases[name.substringAfterLast('.')] + } ?: return null if (!seen.add(alias.name)) throw ScriptError(pos, "circular type alias: ${alias.name}") val bindings = buildTypeAliasBindings(alias, args, pos) val substituted = substituteTypeAliasTypeVars(alias.body, bindings) @@ -3534,6 +3655,10 @@ class Compiler( ?: nameObjClass[ref.name] ?: resolveClassByName(ref.name) } + is ClassScopeMemberRef -> { + val targetClass = resolveClassByName(ref.ownerClassName()) ?: return null + inferFieldReturnClass(targetClass, ref.name) + } is ListLiteralRef -> ObjList.type is MapLiteralRef -> ObjMap.type is RangeRef -> ObjRange.type @@ -3568,6 +3693,10 @@ class Compiler( is LocalVarRef -> nameObjClass[ref.name] ?: nameTypeDecl[ref.name]?.let { resolveTypeDeclObjClass(it) } ?: resolveClassByName(ref.name) + is ClassScopeMemberRef -> { + val targetClass = resolveClassByName(ref.ownerClassName()) + inferFieldReturnClass(targetClass, ref.name) + } is ImplicitThisMemberRef -> { val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName() val targetClass = typeName?.let { resolveClassByName(it) } @@ -3632,6 +3761,11 @@ class Compiler( is ObjString -> ObjString.type else -> null } + is FieldRef -> { + val receiverClass = resolveReceiverClassForMember(target.target) ?: return null + if (!isClassScopeCallableMember(receiverClass.className, target.name)) return null + resolveClassByName("${receiverClass.className}.${target.name}") + } else -> null } } @@ -3828,6 +3962,11 @@ class Compiler( return null } + private fun shouldTreatAsClassScopeCall(left: ObjRef, memberName: String): Boolean { + val receiverClass = resolveReceiverClassForMember(left) ?: return false + return isClassScopeCallableMember(receiverClass.className, memberName) + } + private fun enforceReceiverTypeForMember(left: ObjRef, memberName: String, pos: Pos) { if (left is LocalVarRef && left.name == "scope") return if (left is LocalSlotRef && left.name == "scope") return @@ -5098,7 +5237,22 @@ class Compiler( val doc = pendingDeclDoc ?: consumePendingDoc() pendingDeclDoc = null pendingDeclStart = null - resolutionSink?.declareSymbol(nameToken.value, SymbolKind.ENUM, isMutable = false, pos = nameToken.pos) + val declaredName = nameToken.value + val outerClassName = currentEnclosingClassName() + val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName + resolutionSink?.declareSymbol(declaredName, SymbolKind.ENUM, isMutable = false, pos = nameToken.pos) + if (outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + outerCtx?.classScopeMembers?.add(declaredName) + registerClassScopeMember(outerClassName, declaredName) + registerClassScopeCallableMember(outerClassName, declaredName) + } + val lifted = if (cc.peekNextNonWhitespace().type == Token.Type.STAR) { + cc.nextNonWhitespace() + true + } else { + false + } // so far only simplest enums: val names = mutableListOf() val positions = mutableListOf() @@ -5134,7 +5288,7 @@ class Compiler( miniSink?.onEnumDecl( MiniEnumDecl( range = MiniRange(startPos, cc.currentPos()), - name = nameToken.value, + name = declaredName, entries = names, doc = doc, nameStart = nameToken.pos, @@ -5142,28 +5296,69 @@ class Compiler( entryPositions = positions ) ) + if (lifted) { + val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + val conflicts = when { + classCtx != null -> names.filter { entry -> + classCtx.classScopeMembers.contains(entry) || classCtx.declaredMembers.contains(entry) + } + else -> { + val modulePlan = moduleSlotPlan() + names.filter { entry -> modulePlan?.slots?.containsKey(entry) == true } + } + } + if (conflicts.isNotEmpty()) { + val entry = conflicts.first() + val disambiguation = "${qualifiedName}.$entry" + throw ScriptError( + nameToken.pos, + "lifted enum entry '$entry' conflicts with existing member; use '$disambiguation'" + ) + } + } val fieldIds = LinkedHashMap(names.size + 1) fieldIds["entries"] = 0 for ((index, entry) in names.withIndex()) { fieldIds[entry] = index + 1 } val methodIds = mapOf("valueOf" to 0) - compileClassInfos[nameToken.value] = CompileClassInfo( - name = nameToken.value, + compileClassInfos[qualifiedName] = CompileClassInfo( + name = qualifiedName, fieldIds = fieldIds, methodIds = methodIds, nextFieldId = fieldIds.size, nextMethodId = methodIds.size, baseNames = listOf("Object") ) - enumEntriesByName[nameToken.value] = names.toList() + enumEntriesByName[qualifiedName] = names.toList() + registerClassScopeFieldType(outerClassName, declaredName, qualifiedName) + if (lifted && outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + for (entry in names) { + outerCtx?.classScopeMembers?.add(entry) + registerClassScopeMember(outerClassName, entry) + registerClassScopeFieldType(outerClassName, entry, qualifiedName) + } + } else if (lifted) { + for (entry in names) { + declareLocalName(entry, isMutable = false) + } + } val stmtPos = startPos val enumDeclStatement = object : Statement() { override val pos: Pos = stmtPos override suspend fun execute(scope: Scope): Obj { - val enumClass = ObjEnumClass.createSimpleEnum(nameToken.value, names) - scope.addItem(nameToken.value, false, enumClass, recordType = ObjRecord.Type.Enum) + val enumClass = ObjEnumClass.createSimpleEnum(qualifiedName, names) + scope.addItem(declaredName, false, enumClass, recordType = ObjRecord.Type.Enum) + if (lifted) { + for (entry in names) { + val rec = enumClass.getInstanceMemberOrNull(entry, includeAbstract = false, includeStatic = true) + if (rec != null) { + scope.addItem(entry, false, rec.value) + } + } + } return enumClass } } @@ -5175,10 +5370,18 @@ class Compiler( val nameToken = if (next.type == Token.Type.ID) cc.requireToken(Token.Type.ID) else null val startPos = pendingDeclStart ?: nameToken?.pos ?: cc.current().pos - val className = nameToken?.value ?: generateAnonName(startPos) - if (nameToken != null) { - resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos) - declareLocalName(nameToken.value, isMutable = false) + val declaredName = nameToken?.value + val outerClassName = currentEnclosingClassName() + val baseName = declaredName ?: generateAnonName(startPos) + val className = if (declaredName != null && outerClassName != null) "$outerClassName.$declaredName" else baseName + if (declaredName != null) { + resolutionSink?.declareSymbol(declaredName, SymbolKind.CLASS, isMutable = false, pos = nameToken!!.pos) + declareLocalName(declaredName, isMutable = false) + if (outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + outerCtx?.classScopeMembers?.add(declaredName) + registerClassScopeMember(outerClassName, declaredName) + } } val doc = pendingDeclDoc ?: consumePendingDoc() @@ -5215,7 +5418,7 @@ class Compiler( run { val node = MiniClassDecl( range = MiniRange(startPos, cc.currentPos()), - name = className, + name = declaredName ?: className, bases = baseSpecs.map { it.name }, bodyRange = null, doc = doc, @@ -5232,6 +5435,8 @@ class Compiler( resolutionSink?.enterScope(ScopeKind.CLASS, startPos, className, baseSpecs.map { it.name }) val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody classCtx?.let { ctx -> + val callableMembers = classScopeCallableMembersByClassName.getOrPut(className) { mutableSetOf() } + predeclareClassScopeMembers(className, ctx.classScopeMembers, callableMembers) val baseIds = collectBaseMemberIds(baseSpecs.map { it.name }) ctx.memberFieldIds.putAll(baseIds.fieldIds) ctx.memberMethodIds.putAll(baseIds.methodIds) @@ -5254,6 +5459,9 @@ class Compiler( ) } } + if (declaredName != null) { + registerClassScopeFieldType(outerClassName, declaredName, className) + } parsed } finally { slotPlanStack.removeLast() @@ -5269,7 +5477,7 @@ class Compiler( run { val node = MiniClassDecl( range = MiniRange(startPos, cc.currentPos()), - name = className, + name = declaredName ?: className, bases = baseSpecs.map { it.name }, bodyRange = null, doc = doc, @@ -5291,6 +5499,9 @@ class Compiler( baseNames = baseSpecs.map { it.name } ) } + if (declaredName != null) { + registerClassScopeFieldType(outerClassName, declaredName, className) + } cc.restorePos(saved) null } @@ -5324,8 +5535,8 @@ class Compiler( // Create instance (singleton) val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY)) - if (nameToken != null) - scope.addItem(className, false, instance) + if (declaredName != null) + scope.addItem(declaredName, false, instance) return instance } } @@ -5338,8 +5549,17 @@ class Compiler( val doc = pendingDeclDoc ?: consumePendingDoc() pendingDeclDoc = null pendingDeclStart = null - resolutionSink?.declareSymbol(nameToken.value, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos) - return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) { + val declaredName = nameToken.value + val outerClassName = currentEnclosingClassName() + val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName + resolutionSink?.declareSymbol(declaredName, SymbolKind.CLASS, isMutable = false, pos = nameToken.pos) + if (outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + outerCtx?.classScopeMembers?.add(declaredName) + registerClassScopeMember(outerClassName, declaredName) + registerClassScopeCallableMember(outerClassName, declaredName) + } + return inCodeContext(CodeContext.ClassBody(qualifiedName, isExtern = isExtern)) { val classCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody val typeParamDecls = parseTypeParamList() classCtx?.typeParamDecls = typeParamDecls @@ -5366,7 +5586,7 @@ class Compiler( if (param.accessType != null) { val declClass = resolveTypeDeclObjClass(param.type) if (declClass != null) { - classFieldTypesByName.getOrPut(nameToken.value) { mutableMapOf() }[param.name] = declClass + classFieldTypesByName.getOrPut(qualifiedName) { mutableMapOf() }[param.name] = declClass } } } @@ -5440,7 +5660,7 @@ class Compiler( run { val node = MiniClassDecl( range = MiniRange(startPos, cc.currentPos()), - name = nameToken.value, + name = declaredName, bases = baseSpecs.map { it.name }, bodyRange = null, ctorFields = ctorFields, @@ -5453,8 +5673,8 @@ class Compiler( // parse body val bodyStart = next.pos slotPlanStack.add(classSlotPlan) - resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos) - resolutionSink?.enterScope(ScopeKind.CLASS, startPos, nameToken.value, baseSpecs.map { it.name }) + resolutionSink?.declareClass(qualifiedName, baseSpecs.map { it.name }, startPos) + resolutionSink?.enterScope(ScopeKind.CLASS, startPos, qualifiedName, baseSpecs.map { it.name }) constructorArgsDeclaration?.params?.forEach { param -> val accessType = param.accessType val kind = if (accessType != null) SymbolKind.MEMBER else SymbolKind.PARAM @@ -5463,8 +5683,10 @@ class Compiler( } val st = try { classCtx?.let { ctx -> + val callableMembers = classScopeCallableMembersByClassName.getOrPut(qualifiedName) { mutableSetOf() } + predeclareClassScopeMembers(qualifiedName, ctx.classScopeMembers, callableMembers) predeclareClassMembers(ctx.declaredMembers, ctx.memberOverrides) - val existingExternInfo = if (isExtern) resolveCompileClassInfo(nameToken.value) else null + val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null if (existingExternInfo != null) { ctx.memberFieldIds.putAll(existingExternInfo.fieldIds) ctx.memberMethodIds.putAll(existingExternInfo.methodIds) @@ -5474,18 +5696,18 @@ class Compiler( val hasField = member in existingExternInfo.fieldIds val hasMethod = member in existingExternInfo.methodIds if (!hasField && !hasMethod) { - throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class ${nameToken.value}") + throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class $qualifiedName") } } constructorArgsDeclaration?.params?.forEach { param -> if (param.accessType == null) return@forEach if (!ctx.memberFieldIds.containsKey(param.name)) { val fieldId = existingExternInfo.fieldIds[param.name] - ?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class ${nameToken.value}") + ?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class $qualifiedName") ctx.memberFieldIds[param.name] = fieldId } } - compileClassInfos[nameToken.value] = existingExternInfo + compileClassInfos[qualifiedName] = existingExternInfo } else { val baseIds = collectBaseMemberIds(baseSpecs.map { it.name }) ctx.memberFieldIds.putAll(baseIds.fieldIds) @@ -5512,8 +5734,8 @@ class Compiler( ctx.memberFieldIds[param.name] = ctx.nextFieldId++ } } - compileClassInfos[nameToken.value] = CompileClassInfo( - name = nameToken.value, + compileClassInfos[qualifiedName] = CompileClassInfo( + name = qualifiedName, fieldIds = ctx.memberFieldIds.toMap(), methodIds = ctx.memberMethodIds.toMap(), nextFieldId = ctx.nextFieldId, @@ -5527,8 +5749,8 @@ class Compiler( } if (!isExtern) { classCtx?.let { ctx -> - compileClassInfos[nameToken.value] = CompileClassInfo( - name = nameToken.value, + compileClassInfos[qualifiedName] = CompileClassInfo( + name = qualifiedName, fieldIds = ctx.memberFieldIds.toMap(), methodIds = ctx.memberMethodIds.toMap(), nextFieldId = ctx.nextFieldId, @@ -5537,6 +5759,7 @@ class Compiler( ) } } + registerClassScopeFieldType(outerClassName, declaredName, qualifiedName) parsed } finally { slotPlanStack.removeLast() @@ -5552,7 +5775,7 @@ class Compiler( run { val node = MiniClassDecl( range = MiniRange(startPos, cc.currentPos()), - name = nameToken.value, + name = declaredName, bases = baseSpecs.map { it.name }, bodyRange = null, ctorFields = ctorFields, @@ -5562,9 +5785,9 @@ class Compiler( ) miniSink?.onClassDecl(node) } - resolutionSink?.declareClass(nameToken.value, baseSpecs.map { it.name }, startPos) + resolutionSink?.declareClass(qualifiedName, baseSpecs.map { it.name }, startPos) classCtx?.let { ctx -> - val existingExternInfo = if (isExtern) resolveCompileClassInfo(nameToken.value) else null + val existingExternInfo = if (isExtern) resolveCompileClassInfo(qualifiedName) else null if (existingExternInfo != null) { ctx.memberFieldIds.putAll(existingExternInfo.fieldIds) ctx.memberMethodIds.putAll(existingExternInfo.methodIds) @@ -5574,18 +5797,18 @@ class Compiler( val hasField = member in existingExternInfo.fieldIds val hasMethod = member in existingExternInfo.methodIds if (!hasField && !hasMethod) { - throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class ${nameToken.value}") + throw ScriptError(nameToken.pos, "extern member $member is not found in runtime class $qualifiedName") } } constructorArgsDeclaration?.params?.forEach { param -> if (param.accessType == null) return@forEach if (!ctx.memberFieldIds.containsKey(param.name)) { val fieldId = existingExternInfo.fieldIds[param.name] - ?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class ${nameToken.value}") + ?: throw ScriptError(nameToken.pos, "extern field ${param.name} is not found in runtime class $qualifiedName") ctx.memberFieldIds[param.name] = fieldId } } - compileClassInfos[nameToken.value] = existingExternInfo + compileClassInfos[qualifiedName] = existingExternInfo } else { val baseIds = collectBaseMemberIds(baseSpecs.map { it.name }) ctx.memberFieldIds.putAll(baseIds.fieldIds) @@ -5612,8 +5835,8 @@ class Compiler( ctx.memberFieldIds[param.name] = ctx.nextFieldId++ } } - compileClassInfos[nameToken.value] = CompileClassInfo( - name = nameToken.value, + compileClassInfos[qualifiedName] = CompileClassInfo( + name = qualifiedName, fieldIds = ctx.memberFieldIds.toMap(), methodIds = ctx.memberMethodIds.toMap(), nextFieldId = ctx.nextFieldId, @@ -5622,6 +5845,7 @@ class Compiler( ) } } + registerClassScopeFieldType(outerClassName, declaredName, qualifiedName) // restore if no body starts here cc.restorePos(saved) null @@ -5631,7 +5855,7 @@ class Compiler( val initScope = popInitScope() // create class - val className = nameToken.value + val className = qualifiedName // @Suppress("UNUSED_VARIABLE") val defaultAccess = if (isStruct) AccessType.Variable else AccessType.Initialization // @Suppress("UNUSED_VARIABLE") val defaultVisibility = Visibility.Public @@ -5698,7 +5922,7 @@ class Compiler( } } - scope.addItem(className, false, newClass) + scope.addItem(declaredName, false, newClass) // Prepare class scope for class-scope members (static) and future registrations val classScope = scope.createChildScope(newThisObj = newClass) // Set lexical class context for visibility tagging inside class body @@ -5715,7 +5939,7 @@ class Compiler( return newClass } } - ClassDeclStatement(classDeclStatement, startPos, nameToken.value) + ClassDeclStatement(classDeclStatement, startPos, qualifiedName) } } @@ -6721,6 +6945,11 @@ class Compiler( target is ImplicitThisMemberRef && target.name == "iterator" -> ObjIterator target is ThisFieldSlotRef && target.name == "iterator" -> ObjIterator target is FieldRef && target.name == "iterator" -> ObjIterator + target is FieldRef -> { + val receiverClass = resolveReceiverClassForMember(target.target) ?: return null + if (!isClassScopeCallableMember(receiverClass.className, target.name)) return null + resolveClassByName("${receiverClass.className}.${target.name}") + } target is ConstRef -> when (val value = target.constValue) { is ObjClass -> value is ObjString -> ObjString.type @@ -6774,6 +7003,12 @@ class Compiler( else -> return null } val name = rawName.substringAfterLast('.') + if (!rawName.contains('.')) { + val classCtx = currentEnclosingClassName() + if (classCtx != null) { + resolveClassByName("$classCtx.$rawName")?.let { return it } + } + } return when (name) { "Object", "Obj" -> Obj.rootObjectType "String" -> ObjString.type diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt index bd5b625..b29938a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -242,6 +242,7 @@ private fun applyEnumConstantHeuristics( var j = i + 1 // skip optional whitespace/newlines tokens are separate types, so we just check IDs and braces if (j < tokens.size && tokens[j].type == Type.ID) j++ else { i++; continue } + if (j < tokens.size && tokens[j].type == Type.STAR) j++ if (j < tokens.size && tokens[j].type == Type.LBRACE) { j++ while (j < tokens.size) { 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 3f17917..dd4e0e0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -2133,6 +2133,50 @@ class ImplicitThisMemberRef( } } +/** + * Reference to a class-scope member in the nearest enclosing class context. + */ +class ClassScopeMemberRef( + val name: String, + private val atPos: Pos, + private val ownerClassName: String +) : ObjRef { + internal fun ownerClassName(): String = ownerClassName + override fun forEachVariable(block: (String) -> Unit) { + block(name) + } + + override fun forEachVariableWithPos(block: (String, Pos) -> Unit) { + block(name, atPos) + } + + private fun resolveClass(scope: Scope): ObjClass { + scope.thisVariants.firstOrNull { it is ObjClass && it.className == ownerClassName }?.let { + return it as ObjClass + } + val cls = scope[ownerClassName]?.value as? ObjClass + if (cls != null) return cls + scope.raiseSymbolNotFound(ownerClassName) + } + + override suspend fun get(scope: Scope): ObjRecord { + scope.pos = atPos + val cls = resolveClass(scope) + return cls.readField(scope, name) + } + + override suspend fun evalValue(scope: Scope): Obj { + val rec = get(scope) + return scope.resolve(rec, name) + } + + override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { + scope.pos = atPos + val cls = resolveClass(scope) + cls.writeField(scope, name, newValue) + } +} + /** * Fast path for implicit member calls in class bodies: `foo(...)` resolves locals first, * then falls back to member lookup on `this`. diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 98f869c..69759e7 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3545,6 +3545,45 @@ class ScriptTest { ) } + @Test + fun nestedTypesAndObjects() = runTest { + eval( + """ + class A { + class B(x?) + object Inner { val foo = "bar" } + } + + val ab = A.B() + assertEquals(ab.x, null) + assertEquals(A.Inner.foo, "bar") + """.trimIndent() + ) + } + + @Test + fun liftedEnumEntries() = runTest { + eval( + """ + class A { + enum E* { One, Two } + } + assertEquals(A.One, A.E.One) + assertEquals(A.Two, A.E.Two) + """.trimIndent() + ) + assertFailsWith { + eval( + """ + class A { + val One = 1 + enum E* { One, Two } + } + """.trimIndent() + ) + } + } + @Test fun enumSerializationTest() = runTest { eval( diff --git a/notes/compile_time_name_resolution_spec.md b/notes/compile_time_name_resolution_spec.md index d5bfc5a..5984969 100644 --- a/notes/compile_time_name_resolution_spec.md +++ b/notes/compile_time_name_resolution_spec.md @@ -75,6 +75,18 @@ class B { fun foo() = 2 } class C : A, B { } // error: requires override ``` +## Class Namespace (Nested Declarations) +Nested classes, objects, enums, and type aliases belong to the **class namespace** of their enclosing class. They are not instance members and do not capture an outer instance. + +Resolution rules: +- Qualified access (`Outer.Inner`) resolves to a class-namespace member at compile time. +- Unqualified access inside `Outer` can resolve to nested declarations if not shadowed by locals/params. +- Class-namespace members are never resolved via runtime name lookup; failures are compile-time errors. + +Enum lifting: +- `enum E* { ... }` lifts entries into the enclosing class namespace (e.g., `Outer.Entry`). +- Any ambiguity with existing class members is a compile-time error. + ## Shadowing Rules Shadowing policy is configurable: - Locals may shadow parameters (allowed by default).