From 11eadc1d9f100d677f6e7ec3d8cdd4b8fe980a64 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 4 Jan 2026 22:30:17 +0100 Subject: [PATCH] abstract classes, interfaces, MI auto implementation and fine-grained visibility --- CHANGELOG.md | 9 + docs/OOP.md | 138 +++++++- editors/lyng-textmate/README.md | 2 +- editors/lyng-textmate/package.json | 2 +- .../syntaxes/lyng.tmLanguage.json | 4 +- .../lyng/idea/grazie/LyngGrazieAnnotator.kt | 3 +- .../sergeych/lyng/idea/highlight/LyngLexer.kt | 3 +- lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/ClosureScope.kt | 63 +++- .../kotlin/net/sergeych/lyng/Compiler.kt | 314 +++++++++++++----- .../kotlin/net/sergeych/lyng/Scope.kt | 37 ++- .../kotlin/net/sergeych/lyng/Visibility.kt | 2 +- .../lyng/highlight/SimpleLyngHighlighter.kt | 3 +- .../lyng/miniast/BuiltinDocRegistry.kt | 16 +- .../sergeych/lyng/miniast/DocLookupUtils.kt | 8 +- .../kotlin/net/sergeych/lyng/obj/Obj.kt | 17 +- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 118 ++++++- .../net/sergeych/lyng/obj/ObjInstance.kt | 59 ++-- .../kotlin/net/sergeych/lyng/obj/ObjRecord.kt | 3 + .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 7 +- lynglib/src/commonTest/kotlin/MIC3MroTest.kt | 4 +- lynglib/src/commonTest/kotlin/OOTest.kt | 243 +++++++++++++- .../kotlin/net/sergeych/lyngweb/Highlight.kt | 4 +- 23 files changed, 861 insertions(+), 200 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6fdca..d0977b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 }`. diff --git a/docs/OOP.md b/docs/OOP.md index 8e1494d..f15d14f 100644 --- a/docs/OOP.md +++ b/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 diff --git a/editors/lyng-textmate/README.md b/editors/lyng-textmate/README.md index b3ac197..55a9e2a 100644 --- a/editors/lyng-textmate/README.md +++ b/editors/lyng-textmate/README.md @@ -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 `<=>` diff --git a/editors/lyng-textmate/package.json b/editors/lyng-textmate/package.json index 0215009..71cb03e 100644 --- a/editors/lyng-textmate/package.json +++ b/editors/lyng-textmate/package.json @@ -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" }, diff --git a/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json b/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json index 0fc69b8..8d80d79 100644 --- a/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json +++ b/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json @@ -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": "[!?]" } ] }, diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt index 80b4be4..8bc8ab6 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt @@ -579,7 +579,8 @@ class LyngGrazieAnnotator : ExternalAnnotator - 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index f019770..f5a11d8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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() + 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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 7c09c5c..9fc5777 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt index ace52c1..d0af831 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt @@ -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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt index 70e0a42..d9096d6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -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", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index 242dd67..20a5724 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -112,8 +112,8 @@ object BuiltinDocRegistry : BuiltinDocSource { fun extensionMemberNamesFor(className: String): List { val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList() val out = LinkedHashSet() - // 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)) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt index d098493..3b11992 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt @@ -435,16 +435,16 @@ object DocLookupUtils { if (start !in 0..end) return emptyMap() val body = text.substring(start, end) val map = LinkedHashMap() - // 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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 9e0c315..a209ac6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 004edbf..0a1ec77 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -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 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() + 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. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 398d64e..2504fbd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt index dd50403..11e3dfe 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index eff8ae8..6158696 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -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) { diff --git a/lynglib/src/commonTest/kotlin/MIC3MroTest.kt b/lynglib/src/commonTest/kotlin/MIC3MroTest.kt index dec9bef..a2f42c3 100644 --- a/lynglib/src/commonTest/kotlin/MIC3MroTest.kt +++ b/lynglib/src/commonTest/kotlin/MIC3MroTest.kt @@ -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() diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index a7a7ee7..516b371 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -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() + ) + } } \ No newline at end of file diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt index 3b8905a..adb85e8 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt @@ -530,10 +530,10 @@ private fun detectDeclarationAndParamOverrides(text: String): Map 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 {