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

This commit is contained in:
Sergey Chernov 2026-01-04 22:30:17 +01:00
parent 96e1ffc7d5
commit 11eadc1d9f
23 changed files with 861 additions and 200 deletions

View File

@ -2,6 +2,15 @@
### Unreleased
- Language: Abstract Classes and Interfaces
- Support for `abstract` modifier on classes, methods, and variables.
- Introduced `interface` as a synonym for `abstract class`, supporting full state (constructors, fields, `init` blocks) and implementation by parts via MI.
- New `closed` modifier (antonym to `open`) to prevent overriding class members.
- Refined `override` logic: mandatory keyword when re-declaring members that exist in the ancestor chain (MRO).
- MI Satisfaction: Abstract requirements are automatically satisfied by matching concrete members found later in the C3 MRO chain without requiring explicit proxy methods.
- Integration: Updated highlighters (lynglib, lyngweb, IDEA plugin), IDEA completion, and Grazie grammar checking.
- Documentation: Updated `docs/OOP.md` with sections on "Abstract Classes and Members", "Interfaces", and "Overriding and Virtual Dispatch".
- Language: Class properties with accessors
- Support for `val` (read-only) and `var` (read-write) properties in classes.
- Syntax: `val name [ : Type ] get() { body }` or `var name [ : Type ] get() { body } set(value) { body }`.

View File

@ -50,7 +50,7 @@ Properties allow you to define member accessors that look like fields but execut
Properties are declared using `val` (read-only) or `var` (read-write) followed by a name and `get()`/`set()` blocks:
```kotlin
```lyng
class Person(private var _age: Int) {
// Read-only property
val ageCategory
@ -76,7 +76,7 @@ assertEquals("Adult", p.ageCategory)
For simple accessors and methods, you can use the `=` shorthand for a more elegant and laconic form:
```kotlin
```lyng
class Circle(val radius: Real) {
val area get() = π * radius * radius
val circumference get() = 2 * π * radius
@ -104,7 +104,7 @@ class Counter {
When you want to define a property that is computed only once (on demand) and then remembered, use the built-in `cached` function. This is more efficient than a regular property with `get()` if the computation is expensive, as it avoids re-calculating the value on every access.
```kotlin
```lyng
class DataService(val id: Int) {
// The lambda passed to cached is only executed once, the first time data() is called.
val data = cached {
@ -179,7 +179,7 @@ Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` t
You can declare a `val` field without an immediate initializer if you provide an assignment for it within an `init` block or the class body. This is useful when the initial value depends on logic that cannot be expressed in a single expression.
```kotlin
```lyng
class DataProcessor(data: Object) {
val result: Object
@ -200,7 +200,7 @@ Key rules for late-init `val`:
The `Unset` singleton represents a field that has been declared but not yet initialized. While it can be compared and converted to a string, most other operations on it are forbidden to prevent accidental use of uninitialized data.
```kotlin
```lyng
class T {
val x
fun check() {
@ -237,7 +237,7 @@ Functions defined inside a class body are methods, and unless declared
Lyng supports declaring a class with multiple direct base classes. The syntax is:
```
```lyng
class Foo(val a) {
var tag = "F"
fun runA() { "ResultA:" + a }
@ -286,16 +286,16 @@ Key rules and features:
- Syntax
- `class Derived(args) : Base1(b1Args), Base2(b2Args)`
- Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child (future extensions may add more control).
- Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child.
- Resolution order (C3 MRO — active)
- Resolution order (C3 MRO)
- Member lookup is deterministic and follows C3 linearization (Python‑like), which provides a monotonic, predictable order for complex hierarchies and diamonds.
- Intuition: for `class D() : B(), C()` where `B()` and `C()` both derive from `A()`, the C3 order is `D → B → C → A`.
- The first visible match along this order wins.
- Qualified dispatch
- Inside a class body, use `this@Type.member(...)` to start lookup at the specified ancestor.
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)` (safe‑call `?.` is already available in Lyng).
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)`.
- Qualified access does not relax visibility.
- Field inheritance (`val`/`var`) and collisions
@ -312,10 +312,120 @@ Key rules and features:
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.
- Diagnostics
- When a member/field is not found, error messages include the receiver class name and the considered linearization order, with suggestions to disambiguate using `this@Type` or casts if appropriate.
- Qualifying with a non‑ancestor in `this@Type` reports a clear error mentioning the receiver lineage.
- `as`/`as?` cast errors mention the actual and target types.
## Abstract Classes and Members
An `abstract` class is a class that cannot be instantiated and is intended to be inherited by other classes. It can contain `abstract` members that have no implementation and must be implemented by concrete subclasses.
### Abstract Classes
To declare an abstract class, use the `abstract` modifier:
```lyng
abstract class Shape {
abstract fun area(): Real
}
```
Abstract classes can have constructors, fields, and concrete methods, just like regular classes.
### Abstract Members
Methods and variables (`val`/`var`) can be marked as `abstract`. Abstract members must not have a body or initializer.
```lyng
abstract class Base {
abstract fun foo(): Int
abstract var bar: String
}
```
- **Safety**: `abstract` members cannot be `private`, as they must be visible to subclasses for implementation.
- **Contract of Capability**: An `abstract val/var` represents a requirement for a capability. It can be implemented by either a **field** (storage) or a **property** (logic) in a subclass.
## Interfaces
An `interface` in Lyng is a synonym for an `abstract class`. Following the principle that Lyng's Multiple Inheritance system is powerful enough to handle stateful contracts, interfaces support everything classes do, including constructors, fields, and `init` blocks.
```lyng
interface Named(val name: String) {
fun greet() { "Hello, " + name }
}
class Person(name) : Named(name)
```
Using `interface` instead of `abstract class` is a matter of semantic intent, signaling that the class is primarily intended to be used as a contract in MI.
### Implementation by Parts
One of the most powerful benefits of Lyng's Multiple Inheritance and C3 MRO is the ability to satisfy an interface's requirements "by parts" from different parent classes. Since an `interface` can have state and requirements, a subclass can inherit these requirements and satisfy them using members inherited from other parents in the MRO chain.
Example:
```lyng
// Interface with state (id) and abstract requirements
interface Character(val id) {
abstract var health
abstract var mana
fun isAlive() = health > 0
fun status() = name + " (#" + id + "): " + health + " HP, " + mana + " MP"
}
// Parent class 1: provides health
class HealthPool(var health)
// Parent class 2: provides mana and name
class ManaPool(var mana) {
val name = "Hero"
}
// Composite class: implements Character by combining HealthPool and ManaPool
class Warrior(id, h, m) : HealthPool(h), ManaPool(m), Character(id)
val w = Warrior(1, 100, 50)
assertEquals("Hero (#1): 100 HP, 50 MP", w.status())
```
In this example, `Warrior` inherits from `HealthPool`, `ManaPool`, and `Character`. The abstract requirements `health` and `mana` from `Character` are automatically satisfied by the matching members inherited from `HealthPool` and `ManaPool`. The `status()` method also successfully finds the `name` field from `ManaPool`. This pattern allows for highly modular and reusable "trait-like" classes that can be combined to fulfill complex contracts without boilerplate proxy methods.
## Overriding and Virtual Dispatch
When a class defines a member that already exists in one of its parents, it is called **overriding**.
### The `override` Keyword
In Lyng, the `override` keyword is **mandatory when declaring a member** that exists in the ancestor chain (MRO).
```lyng
class Parent {
fun foo() = 1
}
class Child : Parent() {
override fun foo() = 2 // Mandatory override keyword
}
```
- **Implicit Satisfaction**: If a class inherits an abstract requirement and a matching implementation from different parents, the requirement is satisfied automatically without needing an explicit `override` proxy.
- **No Accidental Overrides**: If you define a member that happens to match a parent's member but you didn't use `override`, the compiler will throw an error. This prevents the "Fragile Base Class" problem.
- **Private Members**: Private members in parent classes are NOT part of the virtual interface and cannot be overridden. Defining a member with the same name in a subclass is allowed without `override` and is treated as a new, independent member.
### Visibility Widening
A subclass can increase the visibility of an overridden member (e.g., `protected``public`), but it is strictly forbidden from narrowing it (e.g., `public``protected`).
### The `closed` Modifier
To prevent a member from being overridden in subclasses, use the `closed` modifier (equivalent to `final` in other languages).
```lyng
class Critical {
closed fun secureStep() { ... }
}
```
Attempting to override a `closed` member results in a compile-time error.
Compatibility notes:
@ -438,7 +548,7 @@ You can restrict the visibility of a `var` field's or property's setter by using
#### On Fields
```kotlin
```lyng
class SecretCounter {
var count = 0
private set // Can be read anywhere, but written only in SecretCounter

View File

@ -20,7 +20,7 @@ Files
- Constants: `true`, `false`, `null`, `this`
- Annotations: `@name` (Unicode identifiers supported)
- Labels: `name:` (Unicode identifiers supported)
- Declarations: highlights declared names in `fun|fn name`, `class|enum Name`, `val|var name`
- Declarations: highlights declared names in `fun|fn name`, `class|enum|interface Name`, `val|var name`
- Types: built-ins (`Int|Real|String|Bool|Char|Regex`) and Capitalized identifiers (heuristic)
- Operators including ranges (`..`, `..<`, `...`), null-safe (`?.`, `?[`, `?(`, `?{`, `?:`, `??`), arrows (`->`, `=>`, `::`), match operators (`=~`, `!~`), bitwise, arithmetic, etc.
- Shuttle operator `<=>`

View File

@ -2,7 +2,7 @@
"name": "lyng-textmate",
"displayName": "Lyng",
"description": "TextMate grammar for the Lyng language (for JetBrains IDEs via TextMate Bundles and VS Code).",
"version": "0.0.3",
"version": "0.1.0",
"publisher": "lyng",
"license": "Apache-2.0",
"engines": { "vscode": "^1.0.0" },

View File

@ -76,8 +76,8 @@
},
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] },
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|val|var|import|package|constructor|property|open|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum|interface)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] },
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|interface|val|var|import|package|constructor|property|abstract|override|open|closed|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
"constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] },
"types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] },
"operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] },

View File

@ -579,7 +579,8 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
val s = w.lowercase()
return s in setOf(
// common code words / language keywords to avoid noise
"val","var","fun","class","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null",
"val","var","fun","class","interface","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null",
"abstract","closed","override",
// very common English words
"the","and","or","not","with","from","into","this","that","file","found","count","name","value","object"
)

View File

@ -32,7 +32,8 @@ class LyngLexer : LexerBase() {
private var myTokenType: IElementType? = null
private val keywords = setOf(
"fun", "val", "var", "class", "type", "import", "as",
"fun", "val", "var", "class", "interface", "type", "import", "as",
"abstract", "closed", "override",
"if", "else", "for", "while", "return", "true", "false", "null",
"when", "in", "is", "break", "continue", "try", "catch", "finally",
"get", "set"

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.1.0-rc"
version = "1.1.0-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 }

View File

@ -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",

View File

@ -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))

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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)

View File

@ -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) {

View File

@ -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) {

View File

@ -52,8 +52,8 @@ class MIC3MroTest {
eval(
"""
class A() { fun common() { "A" } }
class B() : A() { fun common() { "B" } }
class C() : A() { fun common() { "C" } }
class B() : A() { override fun common() { "B" } }
class C() : A() { override fun common() { "C" } }
class D() : B(), C()
val d = D()

View File

@ -348,7 +348,8 @@ class OOTest {
@Test
fun testPropAsExtension() = runTest {
val scope = Script.newScope()
scope.eval("""
scope.eval(
"""
class A(x) {
private val privateVal = 100
val p1 get() = x + 1
@ -368,7 +369,8 @@ class OOTest {
// it should also work with properties:
val A.p10 get() = x * 10
assertEquals(20, A(2).p10)
""".trimIndent())
""".trimIndent()
)
// important is that such extensions should not be able to access private members
// and thus remove privateness:
@ -383,23 +385,28 @@ class OOTest {
@Test
fun testExtensionsAreScopeIsolated() = runTest {
val scope1 = Script.newScope()
scope1.eval("""
scope1.eval(
"""
val String.totalDigits get() {
// notice using `this`:
this.characters.filter{ it.isDigit() }.size()
}
assertEquals(2, "answer is 42".totalDigits)
""")
"""
)
val scope2 = Script.newScope()
scope2.eval("""
scope2.eval(
"""
// in scope2 we didn't override `totalDigits` extension:
assertThrows { "answer is 42".totalDigits }
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testCacheInClass() = runTest {
eval("""
eval(
"""
class T(salt) {
private var c
@ -416,24 +423,28 @@ class OOTest {
assertEquals("bar.", t2.getResult())
assertEquals("foo.", t1.getResult())
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testLateInitValsInClasses() = runTest {
assertFails {
eval("""
eval(
"""
class T {
val x
}
""")
"""
)
}
assertFails {
eval("val String.late")
}
eval("""
eval(
"""
// but we can "late-init" them in init block:
class OK {
val x
@ -463,12 +474,14 @@ class OOTest {
}
}
AccessBefore()
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testPrivateSet() = runTest {
eval("""
eval(
"""
class A {
var y = 100
private set
@ -505,7 +518,8 @@ class OOTest {
assertThrows(AccessException) { d.y = 10 }
d.setY(20)
assertEquals(20, d.y)
""")
"""
)
}
@Test
@ -515,4 +529,205 @@ class OOTest {
}
}
@Test
fun testAbstractClassesAndOverridingProposal() = runTest {
val scope = Script.newScope()
/*
Abstract class is a sort of interface on steroidsL it is a class some members/methods of which
are required to be implemented by heirs. Still it is a regular class in all other respects.
Just can't be instantiated
*/
scope.eval(
"""
// abstract modifier is required. It can have a constructor, or be without it:
abstract class A(someParam=1) {
// if the method is marked as abstract, it has no body:
abstract fun foo(): Int
// abstract var/var have no initializer:
abstract var bar
}
// can't create instance of the abstract class:
assertThrows { A() }
""".trimIndent()
)
// create abstract method with body or val/var with initializer is an error:
assertFails { scope.eval("abstract class B { abstract fun foo() = 1 }") }
assertFails { scope.eval("abstract class C { abstract val bar = 1 }") }
// inheriting an abstract class without implementing all of it abstract members and methods
// is not allowed:
assertFails { scope.eval("class D : A(1) { override fun foo() = 10 }") }
// but it is allowed to inherit in another abstract class:
scope.eval("abstract class E : A(1) { override fun foo() = 10 }")
// implementing all abstracts let us have regular class:
scope.eval(
"""
class F : E() { override val bar = 11 }
assertEquals(10, F().foo())
assertEquals(11, F().bar)
""".trimIndent()
)
// Another possibility to override symbol is multiple inheritance: the parent that
// follows the abstract class in MI chain can override the abstract symbol:
scope.eval(
"""
// This implementor know nothing of A but still implements de-facto its needs:
class Implementor {
val bar = 3
fun foo() = 1
}
// now we can use MI to implement abstract class:
class F2 : A(42), Implementor
assertEquals(1, F2().foo())
assertEquals(3, F2().bar)
"""
)
/*
Notes.
The override keyword is an _optional_ flag that the symbol must exist in one of the parents
and can be overridden.
Compiler checks as early as possible that the symbol exists in one of the parents and is open.
By default, all public/protected symbols are open. If there is no such symbol, the exception is thrown.
In contrast, if the symbol has no special flags, the compiler either creates a new one of overrides
existing, checking that override is allowed.
Question to AI: the keyword to mark non-overridable symbols? final is not the best option as for me.
Overriding the var/val should also be possible with an initializer of with get()/set(value).
overriding can't alter visibility: it must remain as declared in the parent. Private symbols can't be
neither declared abstract nor overridden.
*/
}
@Test
fun testAbstractAndOverrideEdgeCases() = runTest {
val scope = Script.newScope()
// 1. abstract private is an error:
assertFails { scope.eval("abstract class Err { abstract private fun foo() }") }
assertFails { scope.eval("abstract class Err { abstract private val x }") }
// 2. private member in parent is not visible for overriding:
scope.eval(
"""
class Base {
private fun secret() = 1
fun callSecret() = secret()
}
class Derived : Base() {
// This is NOT an override, but a new method
fun secret() = 2
}
val d = Derived()
assertEquals(2, d.secret())
assertEquals(1, d.callSecret())
""".trimIndent()
)
// Using override keyword when there is only a private member in parent is an error:
assertFails { scope.eval("class D2 : Base() { override fun secret() = 3 }") }
// 3. interface can have state (constructor, fields, init):
scope.eval(
"""
interface I(val x) {
var y = x * 2
val z
init {
z = y + 1
}
fun foo() = x + y + z
}
class Impl : I(10)
val impl = Impl()
assertEquals(10, impl.x)
assertEquals(20, impl.y)
assertEquals(21, impl.z)
assertEquals(51, impl.foo())
""".trimIndent()
)
// 4. closed members cannot be overridden:
scope.eval(
"""
class G {
closed fun locked() = "locked"
closed val permanent = 42
}
""".trimIndent()
)
assertFails { scope.eval("class H : G() { override fun locked() = \"free\" }") }
assertFails { scope.eval("class H : G() { override val permanent = 0 }") }
// Even without override keyword, it should fail if it's closed:
assertFails { scope.eval("class H : G() { fun locked() = \"free\" }") }
// 5. Visibility widening is allowed, narrowing is forbidden:
scope.eval(
"""
class BaseVis {
protected fun prot() = 1
}
class Widened : BaseVis() {
override fun prot() = 2 // Widened to public (default)
}
assertEquals(2, Widened().prot())
class BasePub {
fun pub() = 1
}
""".trimIndent()
)
// Narrowing:
assertFails { scope.eval("class Narrowed : BasePub() { override protected fun pub() = 2 }") }
assertFails { scope.eval("class Narrowed : BasePub() { override private fun pub() = 2 }") }
}
@Test
fun testInterfaceImplementationByParts() = runTest {
val scope = Script.newScope()
scope.eval(
"""
// Interface with state (id) and abstract requirements
interface Character(val id) {
abstract var health
abstract var mana
fun isAlive() = health > 0
fun status() = name + " (#" + id + "): " + health + " HP, " + mana + " MP"
// name is also abstractly required by the status method,
// even if not explicitly marked 'abstract val' here,
// it will be looked up in MRO
}
// Part 1: Provides health
class HealthPool(var health)
// Part 2: Provides mana and name
class ManaPool(var mana) {
val name = "Hero"
}
// Composite class implementing Character by parts
class Warrior(id, h, m) : HealthPool(h), ManaPool(m), Character(id)
val w = Warrior(1, 100, 50)
assertEquals(100, w.health)
assertEquals(50, w.mana)
assertEquals(1, w.id)
assert(w.isAlive())
assertEquals("Hero (#1): 100 HP, 50 MP", w.status())
w.health = 0
assert(!w.isAlive())
""".trimIndent()
)
}
}

View File

@ -530,10 +530,10 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
fun isIdentPart(ch: Char) = ch == '_' || ch == '$' || ch == '~' || ch.isLetterOrDigit()
// A conservative list of language keywords to avoid misclassifying as function calls
val kw = setOf(
"package", "import", "fun", "fn", "class", "enum", "val", "var",
"package", "import", "fun", "fn", "class", "interface", "enum", "val", "var",
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
"true", "false", "null", "private", "protected", "open", "extern", "static",
"true", "false", "null", "private", "protected", "abstract", "closed", "override", "open", "extern", "static",
"init", "get", "set", "Unset", "by"
)
fun skipWs(idx0: Int): Int {