abstract classes, interfaces, MI auto implementation and fine-grained visibility

This commit is contained in:
sergeych committed 2026-01-04 22:30:24 +01:00
1 parent 96e1ffc7d5
commit 11eadc1d9f
23 files changed
+861 -200

No files matched your search

@@ -53,26 +53,71 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
super.objects[name]?.let { return it }
super.localBindings[name]?.let { return it }
// 2) Members on the captured receiver instance
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
?.instanceScope
?.objects
?.get(name)
?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
// 1a) Priority: if we are in a class context, prefer our own private members to support
// non-virtual private dispatch. This prevents a subclass from accidentally
// capturing a private member call from a base class.
// We only return non-field/non-property members (methods) here; fields must
// be resolved via instance storage in priority 2.
currentClassCtx?.let { ctx ->
ctx.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property) return rec
}
}
// 2) Members on the captured receiver instance
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst ->
// Check direct locals in instance scope (unmangled)
inst.instanceScope.objects[name]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
}
// Check mangled names for fields along MRO
for (cls in inst.objClass.mro) {
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
}
}
}
findExtension(closureScope.thisObj.objClass, name)?.let { return it }
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
// Return only non-field/non-property members (methods) from class-level records.
// Fields and properties must be resolved via instance storage (mangled) or readField.
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
!rec.isAbstract) return rec
}
}
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
closureScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
// 4) Caller `this` members
(callScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst ->
// Check direct locals in instance scope (unmangled)
inst.instanceScope.objects[name]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
}
// Check mangled names for fields along MRO
for (cls in inst.objClass.mro) {
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
}
}
}
findExtension(callScope.thisObj.objClass, name)?.let { return it }
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
!rec.isAbstract) return rec
}
}
// 5) Caller chain (locals/parents + members)
@@ -236,7 +236,7 @@ class Compiler(
cc.next()
pendingDeclStart = t.pos
pendingDeclDoc = consumePendingDoc()
val st = parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
val st = parseFunctionDeclaration(isExtern = false, isStatic = false)
statements += st
continue
}
@@ -1298,11 +1298,93 @@ class Compiler(
else
null
private suspend fun parseDeclarationWithModifiers(firstId: Token): Statement {
val modifiers = mutableSetOf<String>()
var currentToken = firstId
while (true) {
when (currentToken.value) {
"private", "protected", "static", "abstract", "closed", "override", "extern", "open" -> {
modifiers.add(currentToken.value)
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ID) {
currentToken = cc.next()
} else {
break
}
}
else -> break
}
}
val visibility = when {
modifiers.contains("private") -> Visibility.Private
modifiers.contains("protected") -> Visibility.Protected
else -> Visibility.Public
}
val isStatic = modifiers.contains("static")
val isAbstract = modifiers.contains("abstract")
val isClosed = modifiers.contains("closed")
val isOverride = modifiers.contains("override")
val isExtern = modifiers.contains("extern")
if (isStatic && (isAbstract || isOverride || isClosed))
throw ScriptError(currentToken.pos, "static members cannot be abstract, closed or override")
if (visibility == Visibility.Private && isAbstract)
throw ScriptError(currentToken.pos, "abstract members cannot be private")
pendingDeclStart = firstId.pos
pendingDeclDoc = consumePendingDoc()
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
if (!isMember && (isOverride || isClosed))
throw ScriptError(currentToken.pos, "modifiers override and closed are only allowed for class members")
if (!isMember && isAbstract && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier abstract at top level is only allowed for classes")
return when (currentToken.value) {
"val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic)
"var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic)
"fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic)
"class" -> {
if (isStatic || isClosed || isOverride || isExtern)
throw ScriptError(currentToken.pos, "unsupported modifiers for class: ${modifiers.joinToString(" ")}")
parseClassDeclaration(isAbstract)
}
"interface" -> {
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for interface: ${modifiers.joinToString(" ")}"
)
// interface is synonym for abstract class
parseClassDeclaration(isAbstract = true)
}
else -> throw ScriptError(currentToken.pos, "expected declaration after modifiers, found ${currentToken.value}")
}
}
/**
* Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
"abstract", "closed", "override", "extern", "private", "protected", "static" -> {
parseDeclarationWithModifiers(id)
}
"interface" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseClassDeclaration(isAbstract = true)
}
"val" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
@@ -1318,71 +1400,15 @@ class Compiler(
"fun" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
parseFunctionDeclaration(isExtern = false, isStatic = false)
}
"fn" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
parseFunctionDeclaration(isExtern = false, isStatic = false)
}
// Visibility modifiers for declarations: private/protected val/var/fun/fn
"private" -> {
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'private'")
var isStatic = false
if (k.value == "static") {
isStatic = true
k = cc.requireToken(Token.Type.ID, "declaration expected after 'private static'")
}
when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(
visibility = Visibility.Private,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
"fn" -> parseFunctionDeclaration(
visibility = Visibility.Private,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
else -> k.raiseSyntax("unsupported private declaration kind: ${k.value}")
}
}
"protected" -> {
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'")
var isStatic = false
if (k.value == "static") {
isStatic = true
k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected static'")
}
when (k.value) {
"val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic)
"var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic)
"fun" -> parseFunctionDeclaration(
visibility = Visibility.Protected,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
"fn" -> parseFunctionDeclaration(
visibility = Visibility.Protected,
isOpen = false,
isExtern = false,
isStatic = isStatic
)
else -> k.raiseSyntax("unsupported protected declaration kind: ${k.value}")
}
}
"while" -> parseWhileStatement()
"do" -> parseDoWhileStatement()
"for" -> parseForStatement()
@@ -1444,17 +1470,17 @@ class Compiler(
)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isExtern = isExtern)
cc.matchQualifiers("fun") -> {
pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
consumePendingDoc(); parseFunctionDeclaration(isExtern = isExtern)
}
cc.matchQualifiers("fn") -> {
pendingDeclStart = id.pos; pendingDeclDoc =
consumePendingDoc(); parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
consumePendingDoc(); parseFunctionDeclaration(isExtern = isExtern)
}
cc.matchQualifiers("val", "private", "static") -> {
@@ -1821,7 +1847,7 @@ class Compiler(
}
}
private suspend fun parseClassDeclaration(): Statement {
private suspend fun parseClassDeclaration(isAbstract: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
@@ -1947,6 +1973,7 @@ class Compiler(
}
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also {
it.isAbstract = isAbstract
it.instanceConstructor = constructorCode
it.constructorMeta = constructorArgsDeclaration
// Attach per-parent constructor args (thunks) if provided
@@ -1954,6 +1981,22 @@ class Compiler(
val argsList = baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
// Register constructor fields in the class members
constructorArgsDeclaration?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name, ObjNull,
isMutable = p.accessType == AccessType.Var,
visibility = p.visibility ?: Visibility.Public,
declaringClass = it,
// Constructor fields are not currently supporting override/closed in parser
// but we should pass Pos.builtIn to skip validation for now if needed,
// or p.pos to allow it.
pos = Pos.builtIn,
type = ObjRecord.Type.ConstructorField
)
}
}
}
addItem(className, false, newClass)
@@ -1974,7 +2017,7 @@ class Compiler(
val v = rec.value
if (v is Statement) {
if (newClass.members[k] == null) {
newClass.addFn(k, isOpen = true) {
newClass.addFn(k, isMutable = true, pos = rec.importedFrom?.pos ?: nameToken.pos) {
(thisObj as? ObjInstance)?.let { i ->
v.execute(ClosureScope(this, i.instanceScope))
} ?: v.execute(thisObj.autoInstanceScope(this))
@@ -1982,6 +2025,7 @@ class Compiler(
}
}
}
newClass.checkAbstractSatisfaction(nameToken.pos)
// Debug summary: list registered instance methods and class-scope functions for this class
newClass
}
@@ -2395,7 +2439,9 @@ class Compiler(
private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isExtern: Boolean = false,
isStatic: Boolean = false,
): Statement {
@@ -2480,7 +2526,12 @@ class Compiler(
localDeclCountStack.add(0)
val fnStatements = if (isExtern)
statement { raiseError("extern function not provided: $name") }
else
else if (isAbstract) {
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ASSIGN || next.type == Token.Type.LBRACE)
throw ScriptError(next.pos, "abstract function $name cannot have a body")
null
} else
withLocalNames(paramNames) {
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ASSIGN) {
@@ -2518,7 +2569,7 @@ class Compiler(
if (extTypeName != null) {
context.thisObj = callerContext.thisObj
}
fnStatements.execute(context)
fnStatements?.execute(context) ?: ObjVoid
}
// parentContext
val fnCreateStatement = statement(start) { context ->
@@ -2550,7 +2601,15 @@ class Compiler(
if (!isStatic && th is ObjClass) {
// Instance method declared inside a class body: register on the class
val cls: ObjClass = th
cls.addFn(name, isOpen = true, visibility = visibility) {
cls.addFn(
name,
isMutable = true,
visibility = visibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start
) {
// Execute with the instance as receiver; set caller lexical class for visibility
val savedCtx = this.currentClassCtx
this.currentClassCtx = cls
@@ -2628,7 +2687,9 @@ class Compiler(
private suspend fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isStatic: Boolean = false
): Statement {
val nextToken = cc.next()
@@ -2741,7 +2802,15 @@ class Compiler(
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name)
val isDelegate = if (!isProperty && eqToken.isId("by")) {
val isDelegate = if (isAbstract) {
if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.isId("by")))
throw ScriptError(eqToken.pos, "abstract variable $name cannot have an initializer or delegate")
// Abstract variables don't have initializers
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
false
} else if (!isProperty && eqToken.isId("by")) {
true
} else {
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
@@ -2945,22 +3014,58 @@ class Compiler(
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Register the property
cls.instanceInitializers += statement(start) { scp ->
scp.addItem(
storageName,
isMutable,
prop,
visibility,
setterVisibility,
recordType = ObjRecord.Type.Property
// Register in class members for reflection/MRO/satisfaction checks
if (isProperty) {
cls.addProperty(
name,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start
)
ObjVoid
} else {
cls.createField(
name,
ObjNull,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
type = ObjRecord.Type.Field
)
}
// Register the property/field initialization thunk
if (!isAbstract) {
cls.instanceInitializers += statement(start) { scp ->
scp.addItem(
storageName,
isMutable,
prop,
visibility,
setterVisibility,
recordType = ObjRecord.Type.Property,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
ObjVoid
}
}
ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
context.addItem(storageName, isMutable, prop, visibility, setterVisibility, recordType = ObjRecord.Type.Property)
context.addItem(
storageName, isMutable, prop, visibility, setterVisibility,
recordType = ObjRecord.Type.Property,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
prop
}
} else {
@@ -2971,22 +3076,51 @@ class Compiler(
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
if (isClassScope) {
val cls = context.thisObj as ObjClass
// Register in class members for reflection/MRO/satisfaction checks
cls.createField(
name,
ObjNull,
isMutable = isMutable,
visibility = visibility,
writeVisibility = setterVisibility,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
pos = start,
type = ObjRecord.Type.Field
)
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
val initStmt = statement(start) { scp ->
val initValue =
initialExpression?.execute(scp)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
scp.addItem(storageName, isMutable, initValue, visibility, setterVisibility, recordType = ObjRecord.Type.Field)
ObjVoid
if (!isAbstract) {
val initStmt = statement(start) { scp ->
val initValue =
initialExpression?.execute(scp)?.byValueCopy()
?: if (isLateInitVal) ObjUnset else ObjNull
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
scp.addItem(
storageName, isMutable, initValue, visibility, setterVisibility,
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
ObjVoid
}
cls.instanceInitializers += initStmt
}
cls.instanceInitializers += initStmt
ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
val initValue =
initialExpression?.execute(context)?.byValueCopy() ?: if (isLateInitVal) ObjUnset else ObjNull
// Preserve mutability of declaration: create record with correct mutability
context.addItem(storageName, isMutable, initValue, visibility, setterVisibility, recordType = ObjRecord.Type.Field)
context.addItem(
storageName, isMutable, initValue, visibility, setterVisibility,
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
initValue
}
} else {
@@ -155,7 +155,10 @@ open class Scope(
this.extensions[cls]?.get(name)?.let { return it }
}
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec
} else null
}
}
@@ -176,7 +179,11 @@ open class Scope(
s.extensions[cls]?.get(name)?.let { return it }
}
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) {
// ignore fields, properties and abstracts here, they will be handled by the caller via readField
} else return rec
}
}
s = s.parent
}
@@ -332,7 +339,10 @@ open class Scope(
?: parent?.get(name)
// Finally, fallback to class members on thisObj
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec
} else null
}
)
}
@@ -435,7 +445,10 @@ open class Scope(
value: Obj,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
recordType: ObjRecord.Type = ObjRecord.Type.Other
recordType: ObjRecord.Type = ObjRecord.Type.Other,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false
): ObjRecord =
objects[name]?.let {
if( !it.isMutable )
@@ -449,7 +462,7 @@ open class Scope(
callScope.localBindings[name] = it
}
it
} ?: addItem(name, true, value, visibility, writeVisibility, recordType)
} ?: addItem(name, true, value, visibility, writeVisibility, recordType, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride)
fun addItem(
name: String,
@@ -458,9 +471,19 @@ open class Scope(
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
recordType: ObjRecord.Type = ObjRecord.Type.Other,
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false
): ObjRecord {
val rec = ObjRecord(value, isMutable, visibility, writeVisibility, declaringClass = declaringClass, type = recordType)
val rec = ObjRecord(
value, isMutable, visibility, writeVisibility,
declaringClass = declaringClass,
type = recordType,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
)
objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension
localBindings[name] = rec
@@ -18,7 +18,7 @@
package net.sergeych.lyng
enum class Visibility {
Public, Private, Protected;//, Internal
Public, Protected, Private;//, Internal
val isPublic by lazy { this == Public }
@Suppress("unused")
val isProtected by lazy { this == Protected }
@@ -41,7 +41,8 @@ private val fallbackKeywordIds = setOf(
// boolean operators
"and", "or", "not",
// declarations & modifiers
"fun", "fn", "class", "enum", "val", "var", "import", "package",
"fun", "fn", "class", "interface", "enum", "val", "var", "import", "package",
"abstract", "closed", "override",
"private", "protected", "static", "open", "extern", "init", "get", "set", "by",
// control flow and misc
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
@@ -112,8 +112,8 @@ object BuiltinDocRegistry : BuiltinDocSource {
fun extensionMemberNamesFor(className: String): List<String> {
val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
val out = LinkedHashSet<String>()
// Match lines like: fun String.trim(...) or val Int.isEven = ...
val re = Regex("^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b", RegexOption.MULTILINE)
// Match lines like: fun String.trim(...) or val Int.isEven = ... (allowing modifiers)
val re = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b", RegexOption.MULTILINE)
re.findAll(src).forEach { m ->
val name = m.groupValues.getOrNull(1)?.trim()
if (!name.isNullOrEmpty()) out.add(name)
@@ -371,20 +371,20 @@ private object StdlibInlineDocIndex {
else -> {
// Non-comment, non-blank: try to match a declaration just after comments
if (buf.isNotEmpty()) {
// fun/val/var Class.name( ... )
val mExt = Regex("^(?:fun|val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
// fun/val/var Class.name( ... ) (allowing modifiers)
val mExt = Regex("^(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(?:fun|val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
if (mExt != null) {
val (cls, name) = mExt.destructured
flushTo(Key.Method(cls, name))
} else {
// fun name( ... )
val mTop = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
// fun name( ... ) (allowing modifiers)
val mTop = Regex("^(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
if (mTop != null) {
val (name) = mTop.destructured
flushTo(Key.TopFun(name))
} else {
// class Name
val mClass = Regex("^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
// class/interface Name (allowing modifiers)
val mClass = Regex("^(?:(?:abstract|private|protected|static|open|extern)\\s+)*(?:class|interface)\\s+([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
if (mClass != null) {
val (name) = mClass.destructured
flushTo(Key.Clazz(name))
@@ -435,16 +435,16 @@ object DocLookupUtils {
if (start !in 0..end) return emptyMap()
val body = text.substring(start, end)
val map = LinkedHashMap<String, ScannedSig>()
// fun name(params): Type
val funRe = Regex("^\\s*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
// fun name(params): Type (allowing modifiers like abstract, override, closed)
val funRe = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
for (m in funRe.findAll(body)) {
val name = m.groupValues.getOrNull(1) ?: continue
val params = m.groupValues.getOrNull(2)?.split(',')?.mapNotNull { it.trim().takeIf { it.isNotEmpty() } } ?: emptyList()
val type = m.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }
map[name] = ScannedSig("fun", params, type)
}
// val/var name: Type
val valRe = Regex("^\\s*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
// val/var name: Type (allowing modifiers)
val valRe = Regex("^\\s*(?:(?:abstract|override|closed|private|protected|static|open|extern)\\s+)*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
for (m in valRe.findAll(body)) {
val kind = m.groupValues.getOrNull(1) ?: continue
val name = m.groupValues.getOrNull(2) ?: continue
@@ -89,7 +89,7 @@ open class Obj {
for (cls in objClass.mro) {
if (cls.className == "Obj") break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null) {
if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Property) {
val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
@@ -347,8 +347,12 @@ open class Obj {
// 1. Hierarchy members (excluding root fallback)
for (cls in objClass.mro) {
if (cls.className == "Obj") break
cls.members[name]?.let { return resolveRecord(scope, it, name, it.declaringClass) }
cls.classScope?.objects?.get(name)?.let { return resolveRecord(scope, it, name, it.declaringClass) }
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null) {
if (!rec.isAbstract) {
return resolveRecord(scope, rec, name, rec.declaringClass)
}
}
}
// 2. Extensions
@@ -393,8 +397,11 @@ open class Obj {
// 1. Hierarchy members (excluding root fallback)
for (cls in objClass.mro) {
if (cls.className == "Obj") break
field = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (field != null) break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) {
field = rec
break
}
}
// 2. Extensions
if (field == null) {
@@ -91,6 +91,8 @@ open class ObjClass(
vararg parents: ObjClass,
) : Obj() {
var isAbstract: Boolean = false
// Stable identity and simple structural version for PICs
val classId: Long = ClassIdGen.nextId()
var layoutVersion: Int = 0
@@ -178,7 +180,13 @@ open class ObjClass(
val mro: List<ObjClass> by lazy {
val base = c3Linearize(this, mutableMapOf())
if (this.className == "Obj" || base.any { it.className == "Obj" }) base
else base + rootObjectType
else {
// During very early bootstrap rootObjectType might not be initialized yet.
// We use a safe check here.
@Suppress("UNNECESSARY_SAFE_CALL")
val root = net.sergeych.lyng.obj.Obj.rootObjectType
if (root != null) base + root else base
}
}
/** Parents in C3 order (no self). */
@@ -204,6 +212,7 @@ open class ObjClass(
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(scope: Scope): Obj {
if (isAbstract) scope.raiseError("can't instantiate abstract class $className")
val instance = createInstance(scope)
initializeInstance(instance, scope.args, runConstructors = true)
return instance
@@ -347,14 +356,54 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
pos: Pos = Pos.builtIn,
declaringClass: ObjClass? = this
declaringClass: ObjClass? = this,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field,
) {
// Validation of override rules: only for non-system declarations
if (pos != Pos.builtIn) {
val existing = getInstanceMemberOrNull(name)
var actualOverride = false
if (existing != null && existing.declaringClass != this) {
// If the existing member is private in the ancestor, it's not visible for overriding.
// It should be treated as a new member in this class.
if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this)) {
// It's effectively not there for us, so actualOverride remains false
} else {
actualOverride = true
// It's an override (implicit or explicit)
if (existing.isClosed)
throw ScriptError(pos, "can't override closed member $name from ${existing.declaringClass?.className}")
if (!isOverride)
throw ScriptError(pos, "member $name overrides parent member but 'override' keyword is missing")
if (visibility.ordinal > existing.visibility.ordinal)
throw ScriptError(pos, "can't narrow visibility of $name from ${existing.visibility} to $visibility")
}
}
if (isOverride && !actualOverride) {
throw ScriptError(pos, "member $name is marked 'override' but does not override anything")
}
}
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
val existingInSelf = members[name]
if (existingInSelf != null && existingInSelf.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass")
// Install/override in this class
members[name] = ObjRecord(initialValue, isMutable, visibility, writeVisibility, declaringClass = declaringClass)
members[name] = ObjRecord(
initialValue, isMutable, visibility, writeVisibility,
declaringClass = declaringClass,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
type = type
)
// Structural change: bump layout version for PIC invalidation
layoutVersion += 1
}
@@ -383,14 +432,22 @@ open class ObjClass(
fun addFn(
name: String,
isOpen: Boolean = false,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
declaringClass: ObjClass? = this,
code: suspend Scope.() -> Obj
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
code: (suspend Scope.() -> Obj)? = null
) {
val stmt = statement { code() }
createField(name, stmt, isOpen, visibility, writeVisibility, Pos.builtIn, declaringClass)
val stmt = code?.let { statement { it() } } ?: ObjNull
createField(
name, stmt, isMutable, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
type = ObjRecord.Type.Fun
)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
@@ -401,13 +458,20 @@ open class ObjClass(
setter: (suspend Scope.(Obj) -> Unit)? = null,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
declaringClass: ObjClass? = this
declaringClass: ObjClass? = this,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
) {
val g = getter?.let { statement { it() } }
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
val prop = ObjProperty(name, g, s)
members[name] = ObjRecord(prop, false, visibility, writeVisibility, declaringClass, type = ObjRecord.Type.Property)
layoutVersion += 1
val prop = if (isAbstract) ObjNull else ObjProperty(name, g, s)
createField(
name, prop, false, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
type = ObjRecord.Type.Property
)
}
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
@@ -442,6 +506,38 @@ open class ObjClass(
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
fun findFirstConcreteMember(name: String): ObjRecord? {
for (cls in mro) {
cls.members[name]?.let {
if (!it.isAbstract) return it
}
}
return null
}
fun checkAbstractSatisfaction(pos: Pos) {
if (isAbstract) return
val missing = mutableSetOf<String>()
for (cls in mroParents) {
for ((name, rec) in cls.members) {
if (rec.isAbstract) {
val current = findFirstConcreteMember(name)
if (current == null) {
missing.add(name)
}
}
}
}
if (missing.isNotEmpty()) {
throw ScriptError(
pos,
"class $className is not abstract and does not implement abstract members: ${missing.joinToString(", ")}"
)
}
}
/**
* Resolve member starting from a specific ancestor class [start], not from this class.
* Searches [start] first, then traverses its linearized parents.
@@ -58,10 +58,16 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// self first, then parents
fun findMangled(): ObjRecord? {
// self
instanceScope.objects["${cls.className}::$name"]?.let { return it }
instanceScope.objects["${cls.className}::$name"]?.let {
if (name == "c") println("[DEBUG_LOG] findMangled('c') found in self (${cls.className}): value=${it.value}")
return it
}
// ancestors in deterministic C3 order
for (p in cls.mroParents) {
instanceScope.objects["${p.className}::$name"]?.let { return it }
instanceScope.objects["${p.className}::$name"]?.let {
if (name == "c") println("[DEBUG_LOG] findMangled('c') found in parent (${p.className}): value=${it.value}")
return it
}
}
return null
}
@@ -125,6 +131,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val rec = findMangled()
if (rec != null) {
if (name == "c") println("[DEBUG_LOG] writeField('c') found in mangled: value was ${rec.value}, setting to $newValue")
val declaring = when {
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
@@ -155,34 +162,40 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
onNotFoundResult: (suspend () -> Obj?)?
): Obj =
instanceScope[name]?.let { rec ->
val decl = rec.declaringClass
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
if (rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else {
val decl = rec.declaringClass
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
)
)
rec.value.invoke(
instanceScope,
this,
args
)
rec.value.invoke(
instanceScope,
this,
args
)
}
}
?: run {
// fallback: class-scope function (registered during class body execution)
objClass.classScope?.objects?.get(name)?.let { rec ->
val decl = rec.declaringClass
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
if (rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else {
val decl = rec.declaringClass
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
)
)
)
rec.value.invoke(instanceScope, this, args)
rec.value.invoke(instanceScope, this, args)
}
}
}
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
@@ -32,6 +32,9 @@ data class ObjRecord(
var importedFrom: Scope? = null,
val isTransient: Boolean = false,
val type: Type = Type.Other,
val isAbstract: Boolean = false,
val isClosed: Boolean = false,
val isOverride: Boolean = false,
) {
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
@@ -1182,8 +1182,11 @@ class MethodCallRef(
var hierarchyMember: ObjRecord? = null
for (cls in base.objClass.mro) {
if (cls.className == "Obj") break
hierarchyMember = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (hierarchyMember != null) break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Field) {
hierarchyMember = rec
break
}
}
if (hierarchyMember != null) {