Compare commits
2 Commits
ce0fc3650d
...
eca451b5a3
| Author | SHA1 | Date | |
|---|---|---|---|
| eca451b5a3 | |||
| 54af50d6d6 |
17
CHANGELOG.md
17
CHANGELOG.md
@ -11,7 +11,22 @@
|
|||||||
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
|
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
|
||||||
- Documentation: New "Properties" section in `docs/OOP.md`.
|
- Documentation: New "Properties" section in `docs/OOP.md`.
|
||||||
|
|
||||||
- Docs: Scopes and Closures guidance
|
- Language: Restricted Setter Visibility
|
||||||
|
- Support for `private set` and `protected set` modifiers on `var` fields and properties.
|
||||||
|
- Allows members to be publicly readable but only writable from within the declaring class or its subclasses.
|
||||||
|
- Enforcement at runtime: throws `AccessException` on unauthorized writes.
|
||||||
|
- Supported only for declarations in class bodies (fields and properties).
|
||||||
|
- Documentation: New "Restricted Setter Visibility" section in `docs/OOP.md`.
|
||||||
|
|
||||||
|
- Language: Late-initialized `val` fields in classes
|
||||||
|
- Support for declaring `val` without an immediate initializer in class bodies.
|
||||||
|
- Compulsory initialization: every late-init `val` must be assigned at least once within the class body or an `init` block.
|
||||||
|
- Write-once enforcement: assigning to a `val` is allowed only if its current value is `Unset`.
|
||||||
|
- Access protection: reading a late-init `val` before it is assigned returns the `Unset` singleton; using `Unset` for most operations throws an `UnsetException`.
|
||||||
|
- Extension properties do not support late-init.
|
||||||
|
- Documentation: New "Late-initialized `val` fields" and "The `Unset` singleton" sections in `docs/OOP.md`.
|
||||||
|
|
||||||
|
- Docs: OOP improvements
|
||||||
- New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`).
|
- New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`).
|
||||||
- Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`).
|
- Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`).
|
||||||
- Tutorial: added quick link to Scopes and Closures.
|
- Tutorial: added quick link to Scopes and Closures.
|
||||||
|
|||||||
101
docs/OOP.md
101
docs/OOP.md
@ -175,6 +175,44 @@ statements discussed later, there could be default values, ellipsis, etc.
|
|||||||
|
|
||||||
Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions.
|
Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions.
|
||||||
|
|
||||||
|
### Late-initialized `val` fields
|
||||||
|
|
||||||
|
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
|
||||||
|
class DataProcessor(data: Object) {
|
||||||
|
val result: Object
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Complex initialization logic
|
||||||
|
result = transform(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key rules for late-init `val`:
|
||||||
|
- **Compile-time Check**: The compiler ensures that every `val` declared without an initializer in a class body has at least one assignment within that class body (including `init` blocks). Failing to do so results in a syntax error.
|
||||||
|
- **Write-Once**: A `val` can only be assigned once. Even if it was declared without an initializer, once it is assigned a value (e.g., in `init`), any subsequent assignment will throw an `IllegalAssignmentException`.
|
||||||
|
- **Access before Initialization**: If you attempt to read a late-init `val` before it has been assigned (for example, by calling a method in `init` that reads the field before its assignment), it will hold a special `Unset` value. Using `Unset` for most operations (like arithmetic or method calls) will throw an `UnsetException`.
|
||||||
|
- **No Extensions**: Extension properties do not support late initialization as they do not have per-instance storage. Extension `val`s must always have an initializer or a `get()` accessor.
|
||||||
|
|
||||||
|
### The `Unset` singleton
|
||||||
|
|
||||||
|
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
|
||||||
|
class T {
|
||||||
|
val x
|
||||||
|
fun check() {
|
||||||
|
if (x == Unset) println("Not ready")
|
||||||
|
}
|
||||||
|
init {
|
||||||
|
check() // Prints "Not ready"
|
||||||
|
x = 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
Functions defined inside a class body are methods, and unless declared
|
Functions defined inside a class body are methods, and unless declared
|
||||||
@ -394,6 +432,69 @@ Are declared with var
|
|||||||
assert( p.isSpecial == true )
|
assert( p.isSpecial == true )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
### Restricted Setter Visibility
|
||||||
|
|
||||||
|
You can restrict the visibility of a `var` field's or property's setter by using `private set` or `protected set` modifiers. This allows the member to be publicly readable but only writable from within the class or its subclasses.
|
||||||
|
|
||||||
|
#### On Fields
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class SecretCounter {
|
||||||
|
var count = 0
|
||||||
|
private set // Can be read anywhere, but written only in SecretCounter
|
||||||
|
|
||||||
|
fun increment() { count++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = SecretCounter()
|
||||||
|
println(c.count) // OK
|
||||||
|
c.count = 10 // Throws AccessException
|
||||||
|
c.increment() // OK
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Properties
|
||||||
|
|
||||||
|
You can also apply restricted visibility to custom property setters:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class Person(private var _age: Int) {
|
||||||
|
var age
|
||||||
|
get() = _age
|
||||||
|
private set(v) { if (v >= 0) _age = v }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Protected Setters and Inheritance
|
||||||
|
|
||||||
|
A `protected set` allows subclasses to modify a field that is otherwise read-only to the public:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class Base {
|
||||||
|
var state = "initial"
|
||||||
|
protected set
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base() {
|
||||||
|
fun changeState(newVal) {
|
||||||
|
state = newVal // OK: protected access from subclass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val d = Derived()
|
||||||
|
println(d.state) // OK: "initial"
|
||||||
|
d.changeState("updated")
|
||||||
|
println(d.state) // OK: "updated"
|
||||||
|
d.state = "bad" // Throws AccessException: public write not allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules and Limitations
|
||||||
|
|
||||||
|
- **Only for `var`**: Restricted setter visibility cannot be used with `val` declarations, as they are inherently read-only. Attempting to use it with `val` results in a syntax error.
|
||||||
|
- **Class Body Only**: These modifiers can only be used on members declared within the class body. They are not supported for primary constructor parameters.
|
||||||
|
- **`private set`**: The setter is only accessible within the same class context (specifically, when `this` is an instance of that class).
|
||||||
|
- **`protected set`**: The setter is accessible within the declaring class and all its transitive subclasses.
|
||||||
|
- **Multiple Inheritance**: In MI scenarios, visibility is checked against the class that actually declared the member. Qualified access (e.g., `this@Base.field = value`) also respects restricted setter visibility.
|
||||||
|
|
||||||
### Private fields
|
### Private fields
|
||||||
|
|
||||||
Private fields are visible only _inside the class instance_:
|
Private fields are visible only _inside the class instance_:
|
||||||
|
|||||||
@ -20,5 +20,7 @@ package net.sergeych.lyng
|
|||||||
sealed class CodeContext {
|
sealed class CodeContext {
|
||||||
class Module(@Suppress("unused") val packageName: String?): CodeContext()
|
class Module(@Suppress("unused") val packageName: String?): CodeContext()
|
||||||
class Function(val name: String): CodeContext()
|
class Function(val name: String): CodeContext()
|
||||||
class ClassBody(val name: String): CodeContext()
|
class ClassBody(val name: String): CodeContext() {
|
||||||
|
val pendingInitializations = mutableMapOf<String, Pos>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -138,9 +138,16 @@ class Compiler(
|
|||||||
private var lastParsedBlockRange: MiniRange? = null
|
private var lastParsedBlockRange: MiniRange? = null
|
||||||
|
|
||||||
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
|
private suspend fun <T> inCodeContext(context: CodeContext, f: suspend () -> T): T {
|
||||||
return try {
|
codeContexts.add(context)
|
||||||
codeContexts.add(context)
|
try {
|
||||||
f()
|
val res = f()
|
||||||
|
if (context is CodeContext.ClassBody) {
|
||||||
|
if (context.pendingInitializations.isNotEmpty()) {
|
||||||
|
val (name, pos) = context.pendingInitializations.entries.first()
|
||||||
|
throw ScriptError(pos, "val '$name' must be initialized in the class body or init block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
} finally {
|
} finally {
|
||||||
codeContexts.removeLast()
|
codeContexts.removeLast()
|
||||||
}
|
}
|
||||||
@ -360,7 +367,21 @@ class Compiler(
|
|||||||
val rvalue = parseExpressionLevel(level + 1)
|
val rvalue = parseExpressionLevel(level + 1)
|
||||||
?: throw ScriptError(opToken.pos, "Expecting expression")
|
?: throw ScriptError(opToken.pos, "Expecting expression")
|
||||||
|
|
||||||
lvalue = op.generate(opToken.pos, lvalue!!, rvalue)
|
val res = op.generate(opToken.pos, lvalue!!, rvalue)
|
||||||
|
if (opToken.type == Token.Type.ASSIGN) {
|
||||||
|
val ctx = codeContexts.lastOrNull()
|
||||||
|
if (ctx is CodeContext.ClassBody) {
|
||||||
|
val target = lvalue
|
||||||
|
val name = when (target) {
|
||||||
|
is LocalVarRef -> target.name
|
||||||
|
is FastLocalVarRef -> target.name
|
||||||
|
is FieldRef -> if (target.target is LocalVarRef && target.target.name == "this") target.name else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (name != null) ctx.pendingInitializations.remove(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lvalue = res
|
||||||
}
|
}
|
||||||
return lvalue
|
return lvalue
|
||||||
}
|
}
|
||||||
@ -492,24 +513,32 @@ class Compiler(
|
|||||||
// there could be terminal operators or keywords:// variable to read or like
|
// there could be terminal operators or keywords:// variable to read or like
|
||||||
when (t.value) {
|
when (t.value) {
|
||||||
in stopKeywords -> {
|
in stopKeywords -> {
|
||||||
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
if (t.value == "init" && !(codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE)) {
|
||||||
// Allow certain statement-like constructs to act as expressions
|
// Soft keyword: init is only a keyword in class body when followed by {
|
||||||
// when they appear in expression position (e.g., `if (...) ... else ...`).
|
cc.previous()
|
||||||
// Other keywords should be handled by the outer statement parser.
|
operand = parseAccessor()
|
||||||
when (t.value) {
|
} else {
|
||||||
"if" -> {
|
if (operand != null) throw ScriptError(t.pos, "unexpected keyword")
|
||||||
val s = parseIfStatement()
|
// Allow certain statement-like constructs to act as expressions
|
||||||
operand = StatementRef(s)
|
// when they appear in expression position (e.g., `if (...) ... else ...`).
|
||||||
}
|
// Other keywords should be handled by the outer statement parser.
|
||||||
"when" -> {
|
when (t.value) {
|
||||||
val s = parseWhenStatement()
|
"if" -> {
|
||||||
operand = StatementRef(s)
|
val s = parseIfStatement()
|
||||||
}
|
operand = StatementRef(s)
|
||||||
else -> {
|
}
|
||||||
// Do not consume the keyword as part of a term; backtrack
|
|
||||||
// and return null so outer parser handles it.
|
"when" -> {
|
||||||
cc.previous()
|
val s = parseWhenStatement()
|
||||||
return null
|
operand = StatementRef(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Do not consume the keyword as part of a term; backtrack
|
||||||
|
// and return null so outer parser handles it.
|
||||||
|
cc.previous()
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1367,7 +1396,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
"init" -> {
|
"init" -> {
|
||||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
val block = parseBlock()
|
val block = parseBlock()
|
||||||
lastParsedBlockRange?.let { range ->
|
lastParsedBlockRange?.let { range ->
|
||||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||||
@ -2696,7 +2725,7 @@ class Compiler(
|
|||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.isId("get") || next.isId("set")) {
|
if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) {
|
||||||
isProperty = true
|
isProperty = true
|
||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
@ -2714,7 +2743,13 @@ class Compiler(
|
|||||||
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
||||||
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
||||||
throw ScriptError(start, "val must be initialized")
|
throw ScriptError(start, "val must be initialized")
|
||||||
else {
|
else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) {
|
||||||
|
// lateinit val in class: track it
|
||||||
|
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.pendingInitializations?.put(name, start)
|
||||||
|
cc.restorePos(markBeforeEq)
|
||||||
|
cc.skipWsTokens()
|
||||||
|
setNull = true
|
||||||
|
} else {
|
||||||
cc.restorePos(markBeforeEq)
|
cc.restorePos(markBeforeEq)
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
setNull = true
|
setNull = true
|
||||||
@ -2753,8 +2788,8 @@ class Compiler(
|
|||||||
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
|
// if (isDelegate) throw ScriptError(start, "static delegates are not yet implemented")
|
||||||
currentInitScope += statement {
|
currentInitScope += statement {
|
||||||
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
|
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
|
||||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos)
|
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, pos)
|
||||||
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
|
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
return NopStatement
|
return NopStatement
|
||||||
@ -2763,11 +2798,11 @@ class Compiler(
|
|||||||
// Check for accessors if it is a class member
|
// Check for accessors if it is a class member
|
||||||
var getter: Statement? = null
|
var getter: Statement? = null
|
||||||
var setter: Statement? = null
|
var setter: Statement? = null
|
||||||
|
var setterVisibility: Visibility? = null
|
||||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.peekNextNonWhitespace()
|
val t = cc.skipWsTokens()
|
||||||
if (t.isId("get")) {
|
if (t.isId("get")) {
|
||||||
cc.skipWsTokens()
|
|
||||||
cc.next() // consume 'get'
|
cc.next() // consume 'get'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
cc.requireToken(Token.Type.RPAREN)
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
@ -2783,7 +2818,6 @@ class Compiler(
|
|||||||
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
||||||
}
|
}
|
||||||
} else if (t.isId("set")) {
|
} else if (t.isId("set")) {
|
||||||
cc.skipWsTokens()
|
|
||||||
cc.next() // consume 'set'
|
cc.next() // consume 'set'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
@ -2809,6 +2843,46 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
}
|
}
|
||||||
|
} else if (t.isId("private") || t.isId("protected")) {
|
||||||
|
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
|
||||||
|
val mark = cc.savePos()
|
||||||
|
cc.next() // consume modifier
|
||||||
|
if (cc.skipWsTokens().isId("set")) {
|
||||||
|
cc.next() // consume 'set'
|
||||||
|
setterVisibility = vis
|
||||||
|
if (cc.skipWsTokens().type == Token.Type.LPAREN) {
|
||||||
|
cc.next() // consume '('
|
||||||
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
|
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val body = parseBlock()
|
||||||
|
statement(body.pos) { scope ->
|
||||||
|
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||||
|
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||||
|
body.execute(scope)
|
||||||
|
}
|
||||||
|
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||||
|
cc.skipWsTokens()
|
||||||
|
cc.next() // consume '='
|
||||||
|
val expr = parseExpression() ?: throw ScriptError(
|
||||||
|
cc.current().pos,
|
||||||
|
"Expected setter expression"
|
||||||
|
)
|
||||||
|
val st = (expr as? Statement) ?: statement(expr.pos) { s -> expr.execute(s) }
|
||||||
|
statement(st.pos) { scope ->
|
||||||
|
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||||
|
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||||
|
st.execute(scope)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cc.restorePos(mark)
|
||||||
|
break
|
||||||
|
}
|
||||||
} else break
|
} else break
|
||||||
}
|
}
|
||||||
if (getter != null || setter != null) {
|
if (getter != null || setter != null) {
|
||||||
@ -2817,11 +2891,13 @@ class Compiler(
|
|||||||
throw ScriptError(start, "var property must have both get() and set()")
|
throw ScriptError(start, "var property must have both get() and set()")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (setter != null)
|
if (setter != null || setterVisibility != null)
|
||||||
throw ScriptError(start, "val property cannot have a setter (name: $name)")
|
throw ScriptError(start, "val property cannot have a setter or restricted visibility set (name: $name)")
|
||||||
if (getter == null)
|
if (getter == null)
|
||||||
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
|
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
|
||||||
}
|
}
|
||||||
|
} else if (setterVisibility != null && !isMutable) {
|
||||||
|
throw ScriptError(start, "val field cannot have restricted visibility set (name: $name)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2838,7 +2914,7 @@ class Compiler(
|
|||||||
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
||||||
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
||||||
|
|
||||||
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Property))
|
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, writeVisibility = setterVisibility, declaringClass = null, type = ObjRecord.Type.Property))
|
||||||
|
|
||||||
return@statement prop
|
return@statement prop
|
||||||
}
|
}
|
||||||
@ -2872,6 +2948,7 @@ class Compiler(
|
|||||||
isMutable,
|
isMutable,
|
||||||
prop,
|
prop,
|
||||||
visibility,
|
visibility,
|
||||||
|
setterVisibility,
|
||||||
recordType = ObjRecord.Type.Property
|
recordType = ObjRecord.Type.Property
|
||||||
)
|
)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
@ -2879,38 +2956,41 @@ class Compiler(
|
|||||||
ObjVoid
|
ObjVoid
|
||||||
} else {
|
} else {
|
||||||
// We are in instance scope already: perform initialization immediately
|
// We are in instance scope already: perform initialization immediately
|
||||||
context.addItem(storageName, isMutable, prop, visibility, recordType = ObjRecord.Type.Property)
|
context.addItem(storageName, isMutable, prop, visibility, setterVisibility, recordType = ObjRecord.Type.Property)
|
||||||
prop
|
prop
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (declaringClassName != null && !isStatic) {
|
val isLateInitVal = !isMutable && initialExpression == null && getter == null && setter == null
|
||||||
val storageName = "$declaringClassName::$name"
|
if (declaringClassName != null && !isStatic) {
|
||||||
// If we are in class scope now (defining instance field), defer initialization to instance time
|
val storageName = "$declaringClassName::$name"
|
||||||
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
// If we are in class scope now (defining instance field), defer initialization to instance time
|
||||||
if (isClassScope) {
|
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
||||||
val cls = context.thisObj as ObjClass
|
if (isClassScope) {
|
||||||
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
val cls = context.thisObj as ObjClass
|
||||||
val initStmt = statement(start) { scp ->
|
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||||
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
val initStmt = statement(start) { scp ->
|
||||||
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
val initValue =
|
||||||
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
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
|
||||||
|
}
|
||||||
|
cls.instanceInitializers += initStmt
|
||||||
ObjVoid
|
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)
|
||||||
|
initValue
|
||||||
}
|
}
|
||||||
cls.instanceInitializers += initStmt
|
|
||||||
ObjVoid
|
|
||||||
} else {
|
} else {
|
||||||
// We are in instance scope already: perform initialization immediately
|
// Not in class body: regular local/var declaration
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||||
// Preserve mutability of declaration: create record with correct mutability
|
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||||
context.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
|
||||||
initValue
|
initValue
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Not in class body: regular local/var declaration
|
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
|
||||||
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
|
||||||
initValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3213,7 +3293,10 @@ class Compiler(
|
|||||||
* The keywords that stop processing of expression term
|
* The keywords that stop processing of expression term
|
||||||
*/
|
*/
|
||||||
val stopKeywords =
|
val stopKeywords =
|
||||||
setOf("do", "break", "continue", "return", "if", "when", "do", "while", "for", "class")
|
setOf(
|
||||||
|
"break", "continue", "return", "if", "when", "do", "while", "for", "class",
|
||||||
|
"private", "protected", "val", "var", "fun", "fn", "static", "init", "enum"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -262,6 +262,9 @@ open class Scope(
|
|||||||
|
|
||||||
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
||||||
|
|
||||||
|
fun raiseUnset(message: String = "property is unset (not initialized)"): Nothing =
|
||||||
|
raiseError(ObjUnsetException(this, message))
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun raiseSymbolNotFound(name: String): Nothing =
|
fun raiseSymbolNotFound(name: String): Nothing =
|
||||||
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
|
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
|
||||||
@ -431,6 +434,7 @@ open class Scope(
|
|||||||
name: String,
|
name: String,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||||
): ObjRecord =
|
): ObjRecord =
|
||||||
objects[name]?.let {
|
objects[name]?.let {
|
||||||
@ -445,17 +449,18 @@ open class Scope(
|
|||||||
callScope.localBindings[name] = it
|
callScope.localBindings[name] = it
|
||||||
}
|
}
|
||||||
it
|
it
|
||||||
} ?: addItem(name, true, value, visibility, recordType)
|
} ?: addItem(name, true, value, visibility, writeVisibility, recordType)
|
||||||
|
|
||||||
fun addItem(
|
fun addItem(
|
||||||
name: String,
|
name: String,
|
||||||
isMutable: Boolean,
|
isMutable: Boolean,
|
||||||
value: Obj,
|
value: Obj,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
|
val rec = ObjRecord(value, isMutable, visibility, writeVisibility, declaringClass = declaringClass, type = recordType)
|
||||||
objects[name] = rec
|
objects[name] = rec
|
||||||
// Index this binding within the current frame to help resolve locals across suspension
|
// Index this binding within the current frame to help resolve locals across suspension
|
||||||
localBindings[name] = rec
|
localBindings[name] = rec
|
||||||
|
|||||||
@ -60,6 +60,7 @@ class Script(
|
|||||||
|
|
||||||
internal val rootScope: Scope = Scope(null).apply {
|
internal val rootScope: Scope = Scope(null).apply {
|
||||||
ObjException.addExceptionsToContext(this)
|
ObjException.addExceptionsToContext(this)
|
||||||
|
addConst("Unset", ObjUnset)
|
||||||
addFn("print") {
|
addFn("print") {
|
||||||
for ((i, a) in args.withIndex()) {
|
for ((i, a) in args.withIndex()) {
|
||||||
if (i > 0) print(' ' + a.toString(this).value)
|
if (i > 0) print(' ' + a.toString(this).value)
|
||||||
|
|||||||
@ -416,7 +416,7 @@ open class Obj {
|
|||||||
|
|
||||||
val decl = field.declaringClass
|
val decl = field.declaringClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(field.visibility, decl, caller))
|
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
|
||||||
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.value is ObjProperty) {
|
if (field.value is ObjProperty) {
|
||||||
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
||||||
@ -665,6 +665,50 @@ object ObjNull : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("unset")
|
||||||
|
object ObjUnset : Obj() {
|
||||||
|
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||||
|
override fun equals(other: Any?): Boolean = other === this
|
||||||
|
override fun toString(): String = "Unset"
|
||||||
|
|
||||||
|
override suspend fun readField(scope: Scope, name: String): ObjRecord = scope.raiseUnset()
|
||||||
|
override suspend fun invokeInstanceMethod(
|
||||||
|
scope: Scope,
|
||||||
|
name: String,
|
||||||
|
args: Arguments,
|
||||||
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
|
): Obj = scope.raiseUnset()
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) = scope.raiseUnset()
|
||||||
|
override suspend fun callOn(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun plus(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun minus(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun negate(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun mul(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun div(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun mod(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalNot(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun operatorMatch(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitAnd(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitOr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitXor(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun shl(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun shr(scope: Scope, other: Obj): Obj = scope.raiseUnset()
|
||||||
|
override suspend fun bitNot(scope: Scope): Obj = scope.raiseUnset()
|
||||||
|
|
||||||
|
override val objClass: ObjClass by lazy {
|
||||||
|
object : ObjClass("Unset") {
|
||||||
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
|
return ObjUnset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: get rid of it. Maybe we ise some Lyng inheritance instead
|
* TODO: get rid of it. Maybe we ise some Lyng inheritance instead
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -345,6 +345,7 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn,
|
pos: Pos = Pos.builtIn,
|
||||||
declaringClass: ObjClass? = this
|
declaringClass: ObjClass? = this
|
||||||
) {
|
) {
|
||||||
@ -353,7 +354,7 @@ open class ObjClass(
|
|||||||
if (existingInSelf != null && existingInSelf.isMutable == false)
|
if (existingInSelf != null && existingInSelf.isMutable == false)
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass")
|
throw ScriptError(pos, "$name is already defined in $objClass")
|
||||||
// Install/override in this class
|
// Install/override in this class
|
||||||
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = declaringClass)
|
members[name] = ObjRecord(initialValue, isMutable, visibility, writeVisibility, declaringClass = declaringClass)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
@ -368,13 +369,14 @@ open class ObjClass(
|
|||||||
initialValue: Obj,
|
initialValue: Obj,
|
||||||
isMutable: Boolean = false,
|
isMutable: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn
|
pos: Pos = Pos.builtIn
|
||||||
) {
|
) {
|
||||||
initClassScope()
|
initClassScope()
|
||||||
val existing = classScope!!.objects[name]
|
val existing = classScope!!.objects[name]
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||||
classScope!!.addItem(name, isMutable, initialValue, visibility)
|
classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
@ -383,11 +385,12 @@ open class ObjClass(
|
|||||||
name: String,
|
name: String,
|
||||||
isOpen: Boolean = false,
|
isOpen: Boolean = false,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
declaringClass: ObjClass? = this,
|
declaringClass: ObjClass? = this,
|
||||||
code: suspend Scope.() -> Obj
|
code: suspend Scope.() -> Obj
|
||||||
) {
|
) {
|
||||||
val stmt = statement { code() }
|
val stmt = statement { code() }
|
||||||
createField(name, stmt, isOpen, visibility, Pos.builtIn, declaringClass)
|
createField(name, stmt, isOpen, visibility, writeVisibility, Pos.builtIn, declaringClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||||
@ -397,12 +400,13 @@ open class ObjClass(
|
|||||||
getter: (suspend Scope.() -> Obj)? = null,
|
getter: (suspend Scope.() -> Obj)? = null,
|
||||||
setter: (suspend Scope.(Obj) -> Unit)? = null,
|
setter: (suspend Scope.(Obj) -> Unit)? = null,
|
||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
|
writeVisibility: Visibility? = null,
|
||||||
declaringClass: ObjClass? = this
|
declaringClass: ObjClass? = this
|
||||||
) {
|
) {
|
||||||
val g = getter?.let { statement { it() } }
|
val g = getter?.let { statement { it() } }
|
||||||
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
|
||||||
val prop = ObjProperty(name, g, s)
|
val prop = ObjProperty(name, g, s)
|
||||||
members[name] = ObjRecord(prop, false, visibility, declaringClass, type = ObjRecord.Type.Property)
|
members[name] = ObjRecord(prop, false, visibility, writeVisibility, declaringClass, type = ObjRecord.Type.Property)
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -194,7 +194,9 @@ open class ObjException(
|
|||||||
"AccessException",
|
"AccessException",
|
||||||
"UnknownException",
|
"UnknownException",
|
||||||
"NotFoundException",
|
"NotFoundException",
|
||||||
"IllegalOperationException"
|
"IllegalOperationException",
|
||||||
|
"UnsetException",
|
||||||
|
"SyntaxError"
|
||||||
)) {
|
)) {
|
||||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||||
}
|
}
|
||||||
@ -245,3 +247,6 @@ class ObjIllegalOperationException(scope: Scope, message: String = "Operation is
|
|||||||
|
|
||||||
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
class ObjNotFoundException(scope: Scope, message: String = "not found") :
|
||||||
ObjException("NotFoundException", scope, message)
|
ObjException("NotFoundException", scope, message)
|
||||||
|
|
||||||
|
class ObjUnsetException(scope: Scope, message: String = "property is unset (not initialized)") :
|
||||||
|
ObjException("UnsetException", scope, message)
|
||||||
|
|||||||
@ -97,8 +97,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
val decl = f.declaringClass
|
val decl = f.declaringClass
|
||||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
@ -108,7 +108,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, decl)
|
prop.callSetter(scope, this, newValue, decl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null)
|
if (f.value.assign(scope, newValue) == null)
|
||||||
f.value = newValue
|
f.value = newValue
|
||||||
return
|
return
|
||||||
@ -131,8 +131,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
@ -142,7 +142,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
prop.callSetter(scope, this, newValue, declaring)
|
prop.callSetter(scope, this, newValue, declaring)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (rec.value.assign(scope, newValue) == null)
|
if (rec.value.assign(scope, newValue) == null)
|
||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
return
|
return
|
||||||
@ -350,12 +350,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
instance.instanceScope.objects[mangled]?.let { f ->
|
instance.instanceScope.objects[mangled]?.let { f ->
|
||||||
val decl = f.declaringClass ?: startClass
|
val decl = f.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl.className})"
|
"can't assign to field $name (declared in ${decl.className})"
|
||||||
).raise()
|
).raise()
|
||||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -364,12 +364,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
instance.instanceScope[name]?.let { f ->
|
instance.instanceScope[name]?.let { f ->
|
||||||
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.visibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(
|
ObjAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||||
).raise()
|
).raise()
|
||||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -377,8 +377,8 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
val decl = r.declaringClass ?: startClass
|
val decl = r.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(r.visibility, decl, caller))
|
if (!canAccessMember(r.effectiveWriteVisibility, decl, caller))
|
||||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
ObjAccessException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
||||||
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
||||||
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,12 +26,14 @@ data class ObjRecord(
|
|||||||
var value: Obj,
|
var value: Obj,
|
||||||
val isMutable: Boolean,
|
val isMutable: Boolean,
|
||||||
val visibility: Visibility = Visibility.Public,
|
val visibility: Visibility = Visibility.Public,
|
||||||
|
val writeVisibility: Visibility? = null,
|
||||||
/** If non-null, denotes the class that declared this member (field/method). */
|
/** If non-null, denotes the class that declared this member (field/method). */
|
||||||
val declaringClass: ObjClass? = null,
|
val declaringClass: ObjClass? = null,
|
||||||
var importedFrom: Scope? = null,
|
var importedFrom: Scope? = null,
|
||||||
val isTransient: Boolean = false,
|
val isTransient: Boolean = false,
|
||||||
val type: Type = Type.Other,
|
val type: Type = Type.Other,
|
||||||
) {
|
) {
|
||||||
|
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
|
||||||
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
|
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
|
||||||
Field(true, true),
|
Field(true, true),
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|||||||
@ -442,9 +442,9 @@ class ConstRef(private val record: ObjRecord) : ObjRef {
|
|||||||
* Reference to an object's field with optional chaining.
|
* Reference to an object's field with optional chaining.
|
||||||
*/
|
*/
|
||||||
class FieldRef(
|
class FieldRef(
|
||||||
private val target: ObjRef,
|
val target: ObjRef,
|
||||||
private val name: String,
|
val name: String,
|
||||||
private val isOptional: Boolean,
|
val isOptional: Boolean,
|
||||||
) : ObjRef {
|
) : ObjRef {
|
||||||
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
|
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
|
||||||
// Reads
|
// Reads
|
||||||
|
|||||||
@ -418,4 +418,101 @@ class OOTest {
|
|||||||
|
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLateInitValsInClasses() = runTest {
|
||||||
|
assertFails {
|
||||||
|
eval("""
|
||||||
|
class T {
|
||||||
|
val x
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFails {
|
||||||
|
eval("val String.late")
|
||||||
|
}
|
||||||
|
|
||||||
|
eval("""
|
||||||
|
// but we can "late-init" them in init block:
|
||||||
|
class OK {
|
||||||
|
val x
|
||||||
|
|
||||||
|
init {
|
||||||
|
x = "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ok = OK()
|
||||||
|
assertEquals("foo", ok.x)
|
||||||
|
|
||||||
|
// they can't be reassigned:
|
||||||
|
assertThrows(IllegalAssignmentException) {
|
||||||
|
ok.x = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
// To test access before init, we need a trick:
|
||||||
|
class AccessBefore {
|
||||||
|
val x
|
||||||
|
fun readX() = x
|
||||||
|
init {
|
||||||
|
assertEquals(x, Unset)
|
||||||
|
// if we call readX() here, x is Unset.
|
||||||
|
// Just reading it is fine, but using it should throw:
|
||||||
|
assertThrows(UnsetException) { readX() + 1 }
|
||||||
|
x = 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccessBefore()
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrivateSet() = runTest {
|
||||||
|
eval("""
|
||||||
|
class A {
|
||||||
|
var y = 100
|
||||||
|
private set
|
||||||
|
fun setValue(newValue) { y = newValue }
|
||||||
|
}
|
||||||
|
assertEquals(100, A().y)
|
||||||
|
assertThrows(AccessException) { A().y = 200 }
|
||||||
|
val a = A()
|
||||||
|
a.setValue(200)
|
||||||
|
assertEquals(200, a.y)
|
||||||
|
|
||||||
|
class B(initial) {
|
||||||
|
var y = initial
|
||||||
|
protected set
|
||||||
|
}
|
||||||
|
class C(initial) : B(initial) {
|
||||||
|
fun setBValue(v) { y = v }
|
||||||
|
}
|
||||||
|
val c = C(10)
|
||||||
|
assertEquals(10, c.y)
|
||||||
|
assertThrows(AccessException) { c.y = 20 }
|
||||||
|
c.setBValue(30)
|
||||||
|
assertEquals(30, c.y)
|
||||||
|
|
||||||
|
class D {
|
||||||
|
private var _y = 0
|
||||||
|
var y
|
||||||
|
get() = _y
|
||||||
|
private set(v) { _y = v }
|
||||||
|
fun setY(v) { y = v }
|
||||||
|
}
|
||||||
|
val d = D()
|
||||||
|
assertEquals(0, d.y)
|
||||||
|
assertThrows(AccessException) { d.y = 10 }
|
||||||
|
d.setY(20)
|
||||||
|
assertEquals(20, d.y)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testValPrivateSetError() = runTest {
|
||||||
|
assertFails {
|
||||||
|
eval("class E { val x = 1 private set }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user