abstract classes, interfaces, MI auto implementation and fine-grained visibility
This commit is contained in:
parent
96e1ffc7d5
commit
11eadc1d9f
@ -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 }`.
|
||||
|
||||
138
docs/OOP.md
138
docs/OOP.md
@ -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
|
||||
|
||||
@ -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 `<=>`
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -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": "[!?]" } ] },
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user