Make ObjInt and ObjReal immutable and update number operations accordingly. Add support for class properties with get/set accessors. Rework loop parsing logic to improve clarity and consistency. Update .gitignore and TextMate grammar. Enhance Changelog and document new features.

This commit is contained in:
Sergey Chernov 2025-12-23 08:02:48 +01:00
parent 157b716eb7
commit 5f3a54d08f
19 changed files with 458 additions and 193 deletions

3
.gitignore vendored
View File

@ -17,5 +17,6 @@ xcuserdata
/sample_texts/1.txt.gz /sample_texts/1.txt.gz
/kotlin-js-store/wasm/yarn.lock /kotlin-js-store/wasm/yarn.lock
/distributables /distributables
/.output.txt .output*.txt
debug.log
/build.log /build.log

View File

@ -2,6 +2,15 @@
### Unreleased ### Unreleased
- 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 }`.
- Laconic Expression Shorthand: `val prop get() = expression` and `var prop get() = read set(v) = write`.
- Properties are pure accessors and do **not** have automatic backing fields.
- Validation: `var` properties must have both accessors; `val` must have only a getter.
- Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords).
- Documentation: New "Properties" section in `docs/OOP.md`.
- Docs: Scopes and Closures guidance - Docs: Scopes and Closures guidance
- 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`).

View File

@ -189,6 +189,7 @@ Ready features:
- [x] better stack reporting - [x] better stack reporting
- [x] regular exceptions + extended `when` - [x] regular exceptions + extended `when`
- [x] multiple inheritance for user classes - [x] multiple inheritance for user classes
- [x] class properties (accessors)
## plan: towards v1.5 Enhancing ## plan: towards v1.5 Enhancing

View File

@ -42,6 +42,59 @@ a _constructor_ that requires two parameters for fields. So when creating it wit
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
example above. example above.
## Properties
Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors.
### Basic Syntax
Properties are declared using `val` (read-only) or `var` (read-write) followed by a name and `get()`/`set()` blocks:
```kotlin
class Person(private var _age: Int) {
// Read-only property
val ageCategory
get() {
if (_age < 18) "Minor" else "Adult"
}
// Read-write property
var age: Int
get() { _age }
set(value) {
if (value >= 0) _age = value
}
}
val p = Person(15)
assertEquals("Minor", p.ageCategory)
p.age = 20
assertEquals("Adult", p.ageCategory)
```
### Laconic Expression Shorthand
For simple accessors, you can use the `=` shorthand for a more elegant and laconic form:
```kotlin
class Circle(val radius: Real) {
val area get() = π * radius * radius
val circumference get() = 2 * π * radius
}
class Counter {
private var _count = 0
var count get() = _count set(v) = _count = v
}
```
### Key Rules
- **`val` properties** must have a `get()` accessor and cannot have a `set()`.
- **`var` properties** must have both `get()` and `set()` accessors.
- **No Backing Fields**: There is no magic `field` identifier. If you need to store state, you must declare a separate (usually `private`) field.
- **Type Inference**: You can omit the type declaration if it can be inferred or if you don't need strict typing.
## Instance initialization: init block ## Instance initialization: init block
In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters. In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.

View File

@ -77,7 +77,7 @@
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] }, "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]*" } ] }, "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}_]*)", "captures": { "1": { "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}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] }, "declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "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}_]*)", "captures": { "1": { "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)\\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" } ] }, "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" } ] },
"constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\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*\\()" } ] }, "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": "[!?]" } ] }, "operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] },

View File

@ -34,7 +34,8 @@ class LyngLexer : LexerBase() {
private val keywords = setOf( private val keywords = setOf(
"fun", "val", "var", "class", "type", "import", "as", "fun", "val", "var", "class", "type", "import", "as",
"if", "else", "for", "while", "return", "true", "false", "null", "if", "else", "for", "while", "return", "true", "false", "null",
"when", "in", "is", "break", "continue", "try", "catch", "finally" "when", "in", "is", "break", "continue", "try", "catch", "finally",
"get", "set"
) )
override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) { override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) {

View File

@ -1956,7 +1956,7 @@ class Compiler(
while (cc.hasPrevious() && cnt < maxDepth) { while (cc.hasPrevious() && cnt < maxDepth) {
val t = cc.previous() val t = cc.previous()
cnt++ cnt++
if (t.type == Token.Type.LABEL) { if (t.type == Token.Type.LABEL || t.type == Token.Type.ATLABEL) {
found = t.value found = t.value
break break
} }
@ -1987,7 +1987,8 @@ class Compiler(
val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value
val (canBreak, body, elseStatement) = withLocalNames(namesForLoop) { val (canBreak, body, elseStatement) = withLocalNames(namesForLoop) {
val loopParsed = cc.parseLoop { val loopParsed = cc.parseLoop {
parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body") if (cc.current().type == Token.Type.LBRACE) parseBlock()
else parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body")
} }
// possible else clause // possible else clause
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
@ -2050,21 +2051,19 @@ class Compiler(
var index = 0 var index = 0
while (true) { while (true) {
loopSO.value = current loopSO.value = current
if (canBreak) { try {
try { result = body.execute(forContext)
result = body.execute(forContext) } catch (lbe: LoopBreakContinueException) {
} catch (lbe: LoopBreakContinueException) { if (lbe.label == label || lbe.label == null) {
if (lbe.label == label || lbe.label == null) { breakCaught = true
breakCaught = true if (lbe.doContinue) continue
if (lbe.doContinue) continue else {
else { result = lbe.result
result = lbe.result break
break }
} } else
} else throw lbe
throw lbe }
}
} else result = body.execute(forContext)
if (++index >= size) break if (++index >= size) break
current = sourceObj.getAt(forContext, ObjInt(index.toLong())) current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
} }
@ -2086,11 +2085,9 @@ class Compiler(
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
): Obj { ): Obj {
var result: Obj = ObjVoid var result: Obj = ObjVoid
val iVar = ObjInt(0)
loopVar.value = iVar
if (catchBreak) { if (catchBreak) {
for (i in start..<end) { for (i in start..<end) {
iVar.value = i//.toLong() loopVar.value = ObjInt(i)
try { try {
result = body.execute(forScope) result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) { } catch (lbe: LoopBreakContinueException) {
@ -2103,7 +2100,7 @@ class Compiler(
} }
} else { } else {
for (i in start..<end) { for (i in start..<end) {
iVar.value = i loopVar.value = ObjInt(i)
result = body.execute(forScope) result = body.execute(forScope)
} }
} }
@ -2152,20 +2149,18 @@ class Compiler(
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
private suspend fun parseDoWhileStatement(): Statement { private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it } val label = getLabel()?.also { cc.labels += it }
val (breakFound, body) = cc.parseLoop { val (canBreak, body) = cc.parseLoop {
parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement") parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad do-while statement: expected body statement")
} }
label?.also { cc.labels -= it } label?.also { cc.labels -= it }
cc.skipTokens(Token.Type.NEWLINE) cc.skipWsTokens()
val tWhile = cc.next()
if (tWhile.type != Token.Type.ID || tWhile.value != "while")
throw ScriptError(tWhile.pos, "Expected 'while' after do body")
val t = cc.next() ensureLparen()
if (t.type != Token.Type.ID && t.value != "while") val condition = parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected condition after 'while'")
cc.skipTokenOfType(Token.Type.LPAREN, "expected '(' here")
val conditionStart = ensureLparen()
val condition =
parseExpression() ?: throw ScriptError(conditionStart, "Bad while statement: expected expression")
ensureRparen() ensureRparen()
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
@ -2176,13 +2171,11 @@ class Compiler(
null null
} }
return statement(body.pos) { return statement(body.pos) {
var wasBroken = false var wasBroken = false
var result: Obj = ObjVoid var result: Obj = ObjVoid
lateinit var doScope: Scope
while (true) { while (true) {
doScope = it.createChildScope().apply { skipScopeCreation = true } val doScope = it.createChildScope().apply { skipScopeCreation = true }
try { try {
result = body.execute(doScope) result = body.execute(doScope)
} catch (e: LoopBreakContinueException) { } catch (e: LoopBreakContinueException) {
@ -2194,11 +2187,12 @@ class Compiler(
} }
// for continue: just fall through to condition check below // for continue: just fall through to condition check below
} else { } else {
// Not our label, let outer loops handle it
throw e throw e
} }
} }
if (!condition.execute(doScope).toBool()) break if (!condition.execute(doScope).toBool()) {
break
}
} }
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) } if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result result
@ -2212,7 +2206,10 @@ class Compiler(
parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression") parseExpression() ?: throw ScriptError(start, "Bad while statement: expected expression")
ensureRparen() ensureRparen()
val body = parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement") val (canBreak, body) = cc.parseLoop {
if (cc.current().type == Token.Type.LBRACE) parseBlock()
else parseStatement() ?: throw ScriptError(start, "Bad while statement: expected statement")
}
label?.also { cc.labels -= it } label?.also { cc.labels -= it }
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
@ -2226,21 +2223,23 @@ class Compiler(
var result: Obj = ObjVoid var result: Obj = ObjVoid
var wasBroken = false var wasBroken = false
while (condition.execute(it).toBool()) { while (condition.execute(it).toBool()) {
try { val loopScope = it.createChildScope()
// we don't need to create new context here: if body is a block, if (canBreak) {
// parse block will do it, otherwise single statement doesn't need it: try {
result = body.execute(it) result = body.execute(loopScope)
} catch (lbe: LoopBreakContinueException) { } catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) { if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue if (lbe.doContinue) continue
else { else {
result = lbe.result result = lbe.result
wasBroken = true wasBroken = true
break break
} }
} else } else
throw lbe throw lbe
} }
} else
result = body.execute(loopScope)
} }
if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) } if (!wasBroken) elseStatement?.let { s -> result = s.execute(it) }
result result
@ -2265,7 +2264,12 @@ class Compiler(
cc.previous() cc.previous()
val resultExpr = if (t.pos.line == start.line && (!t.isComment && val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
t.type != Token.Type.SEMICOLON && t.type != Token.Type.SEMICOLON &&
t.type != Token.Type.NEWLINE) t.type != Token.Type.NEWLINE &&
t.type != Token.Type.RBRACE &&
t.type != Token.Type.RPAREN &&
t.type != Token.Type.RBRACKET &&
t.type != Token.Type.COMMA &&
t.type != Token.Type.EOF)
) { ) {
// we have something on this line, could be expression // we have something on this line, could be expression
parseStatement() parseStatement()
@ -2635,34 +2639,50 @@ class Compiler(
parseTypeDeclarationWithMini().second parseTypeDeclarationWithMini().second
} else null } else null
val markBeforeEq = cc.savePos()
val eqToken = cc.next() val eqToken = cc.next()
var setNull = false var setNull = false
var isProperty = false
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
if (declaringClassNameCaptured != null) {
val mark = cc.savePos()
cc.restorePos(markBeforeEq)
val next = cc.peekNextNonWhitespace()
if (next.isId("get") || next.isId("set")) {
isProperty = true
cc.restorePos(markBeforeEq)
} else {
cc.restorePos(mark)
}
}
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name) if (!isStatic) declareLocalName(name)
val isDelegate = if (eqToken.isId("by")) { val isDelegate = if (!isProperty && eqToken.isId("by")) {
true true
} else { } else {
if (eqToken.type != Token.Type.ASSIGN) { if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
if (!isMutable) if (!isMutable && (declaringClassNameCaptured == null))
throw ScriptError(start, "val must be initialized") throw ScriptError(start, "val must be initialized")
else { else {
cc.previous() cc.restorePos(markBeforeEq)
setNull = true setNull = true
} }
} }
false false
} }
val initialExpression = if (setNull) null val initialExpression = if (setNull || isProperty) null
else parseStatement(true) else parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression") ?: throw ScriptError(eqToken.pos, "Expected initializer expression")
// Emit MiniValDecl for this declaration (before execution wiring), attach doc if any // Emit MiniValDecl for this declaration (before execution wiring), attach doc if any
run { run {
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos()) val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
val initR = if (setNull) null else MiniRange(eqToken.pos, cc.currentPos()) val initR = if (setNull || isProperty) null else MiniRange(eqToken.pos, cc.currentPos())
val node = MiniValDecl( val node = MiniValDecl(
range = declRange, range = declRange,
name = name, name = name,
@ -2691,8 +2711,70 @@ class Compiler(
return NopStatement return NopStatement
} }
// Determine declaring class (if inside class body) at compile time, capture it in the closure // Check for accessors if it is a class member
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name var getter: Statement? = null
var setter: Statement? = null
if (declaringClassNameCaptured != null) {
while (true) {
val t = cc.peekNextNonWhitespace()
if (t.isId("get")) {
cc.skipWsTokens()
cc.next() // consume 'get'
cc.requireToken(Token.Type.LPAREN)
cc.requireToken(Token.Type.RPAREN)
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
parseBlock()
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens()
cc.next() // consume '='
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected getter expression")
(expr as? Statement) ?: statement(expr.pos) { s -> expr.execute(s) }
} else {
throw ScriptError(cc.current().pos, "Expected { or = after get()")
}
} else if (t.isId("set")) {
cc.skipWsTokens()
cc.next() // consume 'set'
cc.requireToken(Token.Type.LPAREN)
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 break
}
if (getter != null || setter != null) {
if (isMutable) {
if (getter == null || setter == null) {
throw ScriptError(start, "var property must have both get() and set()")
}
} else {
if (setter != null)
throw ScriptError(start, "val property cannot have a setter (name: $name)")
if (getter == null)
throw ScriptError(start, "val property with accessors must have a getter (name: $name)")
}
}
}
return statement(start) { context -> return statement(start) { context ->
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions // In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
@ -2709,23 +2791,32 @@ class Compiler(
if (isDelegate) { if (isDelegate) {
TODO() TODO()
// println("initial expr = $initialExpression") } else if (getter != null || setter != null) {
// val initValue = val declaringClassName = declaringClassNameCaptured!!
// (initialExpression?.execute(context.copy(Arguments(ObjString(name)))) as? Statement) val storageName = "$declaringClassName::$name"
// ?.execute(context.copy(Arguments(ObjString(name)))) val prop = ObjProperty(name, getter, setter)
// ?: context.raiseError("delegate initialization required")
// println("delegate init: $initValue") // If we are in class scope now (defining instance field), defer initialization to instance time
// if (!initValue.isInstanceOf(ObjArray)) val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
// context.raiseIllegalArgument("delegate initialized must be an array") if (isClassScope) {
// val s = initValue.getAt(context, 1) val cls = context.thisObj as ObjClass
// val setter = if (s == ObjNull) statement { raiseNotImplemented("setter is not provided") } // Register the property
// else (s as? Statement) ?: context.raiseClassCastError("setter must be a callable") cls.instanceInitializers += statement(start) { scp ->
// ObjDelegate( scp.addItem(
// (initValue.getAt(context, 0) as? Statement) storageName,
// ?: context.raiseClassCastError("getter must be a callable"), setter isMutable,
// ).also { prop,
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field) visibility,
// } recordType = ObjRecord.Type.Property
)
ObjVoid
}
ObjVoid
} else {
// We are in instance scope already: perform initialization immediately
context.addItem(storageName, isMutable, prop, visibility, recordType = ObjRecord.Type.Property)
prop
}
} else { } else {
if (declaringClassName != null && !isStatic) { if (declaringClassName != null && !isStatic) {
val storageName = "$declaringClassName::$name" val storageName = "$declaringClassName::$name"

View File

@ -25,11 +25,12 @@ class CompilerContext(val tokens: List<Token>) {
var loopLevel = 0 var loopLevel = 0
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> { inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false val oldBreakFound = breakFound
breakFound = false
val result = f() val result = f()
return Pair(breakFound, result).also { val currentBreakFound = breakFound
--loopLevel breakFound = oldBreakFound || currentBreakFound
} return Pair(currentBreakFound, result)
} }
var currentIndex = 0 var currentIndex = 0
@ -108,7 +109,6 @@ class CompilerContext(val tokens: List<Token>) {
val t = next() val t = next()
return if (t.type != tokenType) { return if (t.type != tokenType) {
if (!isOptional) { if (!isOptional) {
println("unexpected: $t (needed $tokenType)")
throw ScriptError(t.pos, errorMessage) throw ScriptError(t.pos, errorMessage)
} else { } else {
previous() previous()

View File

@ -419,9 +419,12 @@ open class Scope(
callScope.allocateSlotFor(name, rec) callScope.allocateSlotFor(name, rec)
} }
} }
// Map to a slot for fast local access (if not already mapped) // Map to a slot for fast local access (ensure consistency)
if (getSlotIndexOf(name) == null) { val idx = getSlotIndexOf(name)
if (idx == null) {
allocateSlotFor(name, rec) allocateSlotFor(name, rec)
} else {
slots[idx] = rec
} }
return rec return rec
} }

View File

@ -232,7 +232,7 @@ open class Obj {
open suspend fun assign(scope: Scope, other: Obj): Obj? = null open suspend fun assign(scope: Scope, other: Obj): Obj? = null
open fun getValue(scope: Scope) = this open suspend fun getValue(scope: Scope) = this
/** /**
* a += b * a += b

View File

@ -32,19 +32,24 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
// Direct (unmangled) lookup first // Direct (unmangled) lookup first
instanceScope[name]?.let { instanceScope[name]?.let { rec ->
val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name) val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
// Allow unconditional access when accessing through `this` of the same instance // Allow unconditional access when accessing through `this` of the same instance
if (scope.thisObj === this) return it if (scope.thisObj !== this) {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(it.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError( scope.raiseError(
ObjAccessException( ObjAccessException(
scope, scope,
"can't access field $name (declared in ${decl?.className ?: "?"})" "can't access field $name (declared in ${decl?.className ?: "?"})"
)
) )
) }
return it if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty
return rec.copy(value = prop.callGetter(scope, this))
}
return rec
} }
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name // Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
val cls = objClass val cls = objClass
@ -65,15 +70,20 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
instanceScope.objects.containsKey("${cls.className}::$name") -> cls instanceScope.objects.containsKey("${cls.className}::$name") -> cls
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
} }
if (scope.thisObj === this) return rec if (scope.thisObj !== this) {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, declaring, caller)) if (!canAccessMember(rec.visibility, declaring, caller))
scope.raiseError( scope.raiseError(
ObjAccessException( ObjAccessException(
scope, scope,
"can't access field $name (declared in ${declaring?.className ?: "?"})" "can't access field $name (declared in ${declaring?.className ?: "?"})"
)
) )
) }
if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty
return rec.copy(value = prop.callGetter(scope, this))
}
return rec return rec
} }
// Fall back to methods/properties on class // Fall back to methods/properties on class
@ -84,9 +94,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// Direct (unmangled) first // Direct (unmangled) first
instanceScope[name]?.let { f -> instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name) val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name)
if (scope.thisObj === this) { if (scope.thisObj !== this) {
// direct self-assignment allowed; enforce mutability below
} else {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(f.visibility, decl, caller)) if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException( ObjIllegalAssignmentException(
@ -94,6 +102,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
"can't assign to field $name (declared in ${decl?.className ?: "?"})" "can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise() ).raise()
} }
if (f.type == ObjRecord.Type.Property) {
val prop = f.value as ObjProperty
prop.callSetter(scope, this, newValue)
return
}
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() if (!f.isMutable) 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
@ -123,6 +136,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
"can't assign to field $name (declared in ${declaring?.className ?: "?"})" "can't assign to field $name (declared in ${declaring?.className ?: "?"})"
).raise() ).raise()
} }
if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty
prop.callSetter(scope, this, newValue)
return
}
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() if (!rec.isMutable) 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
@ -211,10 +229,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// using objlist allow for some optimizations: // using objlist allow for some optimizations:
val params = meta.params.map { readField(scope, it.name).value } val params = meta.params.map { readField(scope, it.name).value }
println("serializing $objClass with params: $params")
encoder.encodeAnyList(scope, params) encoder.encodeAnyList(scope, params)
val vars = serializingVars.values.map { it.value } val vars = serializingVars.values.map { it.value }
println("encoding vars: $vars")
if (vars.isNotEmpty<Obj>()) { if (vars.isNotEmpty<Obj>()) {
encoder.encodeAnyList(scope, vars) encoder.encodeAnyList(scope, vars)
} }

View File

@ -24,36 +24,32 @@ import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric { class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
override val longValue get() = value override val longValue get() = value
override val doubleValue get() = value.toDouble() override val doubleValue get() = value.toDouble()
override val toObjInt get() = this override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue) override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value) override fun byValueCopy(): Obj = this
override fun hashCode(): Int { override fun hashCode(): Int {
return value.hashCode() return value.hashCode()
} }
override suspend fun getAndIncrement(scope: Scope): Obj { override suspend fun getAndIncrement(scope: Scope): Obj {
ensureNotConst(scope) return this
return ObjInt(value).also { value++ }
} }
override suspend fun getAndDecrement(scope: Scope): Obj { override suspend fun getAndDecrement(scope: Scope): Obj {
ensureNotConst(scope) return this
return ObjInt(value).also { value-- }
} }
override suspend fun incrementAndGet(scope: Scope): Obj { override suspend fun incrementAndGet(scope: Scope): Obj {
ensureNotConst(scope) return ObjInt(value + 1)
return ObjInt(++value)
} }
override suspend fun decrementAndGet(scope: Scope): Obj { override suspend fun decrementAndGet(scope: Scope): Obj {
ensureNotConst(scope) return ObjInt(value - 1)
return ObjInt(--value)
} }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
@ -93,15 +89,9 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu
else ObjReal(this.value.toDouble() % other.toDouble()) else ObjReal(this.value.toDouble() % other.toDouble())
/** /**
* We are by-value type ([byValueCopy] is implemented) so we can do in-place * Numbers are now immutable, so we can't do in-place assignment.
* assignment
*/ */
override suspend fun assign(scope: Scope, other: Obj): Obj? { override suspend fun assign(scope: Scope, other: Obj): Obj? = null
return if (!isConst && other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun toKotlin(scope: Scope): Any { override suspend fun toKotlin(scope: Scope): Any {
return value return value

View File

@ -0,0 +1,30 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
/**
* Property accessor storage. Per instructions, properties do NOT have
* automatic backing fields. They are pure accessors.
*/
class ObjProperty(
val name: String,
val getter: Statement?,
val setter: Statement?
) : Obj() {
suspend fun callGetter(scope: Scope, instance: ObjInstance): Obj {
val g = getter ?: scope.raiseError("property $name has no getter")
// Execute getter in a child scope of the instance with 'this' properly set
return g.execute(instance.instanceScope.createChildScope(newThisObj = instance))
}
suspend fun callSetter(scope: Scope, instance: ObjInstance, value: Obj) {
val s = setter ?: scope.raiseError("property $name has no setter")
// Execute setter in a child scope of the instance with 'this' properly set and the value as an argument
s.execute(instance.instanceScope.createChildScope(args = Arguments(value), newThisObj = instance))
}
override fun toString(): String = "Property($name)"
}

View File

@ -39,7 +39,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override val objClass: ObjClass = type override val objClass: ObjClass = type
override fun byValueCopy(): Obj = ObjReal(value) override fun byValueCopy(): Obj = this
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2 if (other !is Numeric) return -2

View File

@ -42,6 +42,7 @@ data class ObjRecord(
@Suppress("unused") @Suppress("unused")
Class, Class,
Enum, Enum,
Property,
Other; Other;
val isArgument get() = this == Argument val isArgument get() = this == Argument

View File

@ -375,22 +375,12 @@ class IncDecRef(
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value") if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
val v = rec.value val v = rec.value
val one = ObjInt.One val one = ObjInt.One
return if (v.isConst) { // We now treat numbers as immutable and always perform write-back via setAt.
// Mirror existing semantics in Compiler for const values // This avoids issues where literals are shared and mutated in-place.
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one) // For post-inc: return ORIGINAL value; for pre-inc: return NEW value.
// write back val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
target.setAt(atPos, scope, result) target.setAt(atPos, scope, result)
// For post-inc: previous code returned NEW value; for pre-inc: returned ORIGINAL value return (if (isPost) v else result).asReadonly
if (isPost) result.asReadonly else v.asReadonly
} else {
val res = when {
isIncrement && isPost -> v.getAndIncrement(scope)
isIncrement && !isPost -> v.incrementAndGet(scope)
!isIncrement && isPost -> v.getAndDecrement(scope)
else -> v.decrementAndGet(scope)
}
res.asReadonly
}
} }
} }
@ -635,12 +625,16 @@ class FieldRef(
val (k, v) = receiverKeyAndVersion(base) val (k, v) = receiverKeyAndVersion(base)
val rec = tRecord val rec = tRecord
if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) { if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) {
// visibility/mutability checks // If it is a property, we must go through writeField (slow path for now)
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name")) // or handle it here.
if (!rec.visibility.isPublic) if (rec.type != ObjRecord.Type.Property) {
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name")) // visibility/mutability checks
if (rec.value.assign(scope, newValue) == null) rec.value = newValue if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
return if (!rec.visibility.isPublic)
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
if (rec.value.assign(scope, newValue) == null) rec.value = newValue
return
}
} }
} }
if (fieldPic) { if (fieldPic) {
@ -1229,7 +1223,7 @@ class MethodCallRef(
/** /**
* Reference to a local/visible variable by name (Phase A: scope lookup). * Reference to a local/visible variable by name (Phase A: scope lookup).
*/ */
class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef { class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
override fun forEachVariable(block: (String) -> Unit) { override fun forEachVariable(block: (String) -> Unit) {
block(name) block(name)
} }
@ -1253,7 +1247,6 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos scope.pos = atPos
// 1) Try fast slot/local
if (!PerfFlags.LOCAL_SLOT_PIC) { if (!PerfFlags.LOCAL_SLOT_PIC) {
scope.getSlotIndexOf(name)?.let { scope.getSlotIndexOf(name)?.let {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicHit++
@ -1472,7 +1465,7 @@ class BoundLocalVarRef(
* It resolves the slot once per frame and never falls back to global/module lookup. * It resolves the slot once per frame and never falls back to global/module lookup.
*/ */
class FastLocalVarRef( class FastLocalVarRef(
private val name: String, val name: String,
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
override fun forEachVariable(block: (String) -> Unit) { override fun forEachVariable(block: (String) -> Unit) {
@ -1779,10 +1772,16 @@ class AssignRef(
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
val rec = target.get(scope) // For properties, we should not call get() on target because it invokes the getter.
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable") // Instead, we call setAt directly.
if (rec.value.assign(scope, v) == null) { if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
target.setAt(atPos, scope, v) target.setAt(atPos, scope, v)
} else {
val rec = target.get(scope)
if (!rec.isMutable) throw ScriptError(atPos, "cannot assign to immutable variable")
if (rec.value.assign(scope, v) == null) {
target.setAt(atPos, scope, v)
}
} }
return v.asReadonly return v.asReadonly
} }

View File

@ -2318,29 +2318,33 @@ class ScriptTest {
@Test @Test
fun doWhileValuesLabelTest() = runTest { fun doWhileValuesLabelTest() = runTest {
withTimeout(5.seconds) { withTimeout(5.seconds) {
eval( try {
""" eval(
var count = 0 """
var count2 = 0 var count = 0
var count3 = 0 var count2 = 0
val result = outer@ do { var count3 = 0
count2++ val result = outer@ do {
count = 0 count2++
do { count = 0
count++ do {
if( count < 10 || count2 < 5 ) { count++
continue if( count < 10 || count2 < 5 ) {
} continue
if( count % 2 == 1 ) }
break@outer "found "+count + "/" + count2 if( count % 2 == 1 )
} while(count < 14) break@outer "found "+count + "/" + count2
count3++ } while(count < 14)
} while( count2 < 100 ) count3++
else "no" } while( count2 < 100 )
assertEquals("found 11/5", result) else "no"
assertEquals( 4, count3) assertEquals("found 11/5", result)
""".trimIndent() assertEquals( 4, count3)
) """.trimIndent()
)
} catch (e: ExecutionError) {
throw e
}
} }
} }

View File

@ -0,0 +1,66 @@
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class PropsTest {
@Test
fun propsProposal() = runTest {
eval("""
class WithProps {
// readonly property without declared type:
val readonlyProp
get() {
"readonly foo"
}
val readonlyWithType: Int get() { 42 }
private var field = 0
private var field2 = ""
// with type declaration
var propName: Int
get() {
field * 10
}
set(value) {
field = value
}
// or without
var propNameWithoutType
get() {
"/"+ field2 + "/"
}
set(value) {
field2 = value
}
}
val w = WithProps()
assertEquals("readonly foo", w.readonlyProp)
assertEquals(42, w.readonlyWithType)
w.propNameWithoutType = "foo"
assertEquals("/foo/", w.propNameWithoutType)
w.propName = 123
assertEquals(1230, w.propName)
class Shorthand {
private var _p = 0
var p: Int
get() = _p * 2
set(v) = _p = v
}
val s = Shorthand()
s.p = 21
assertEquals(42, s.p)
""".trimIndent())
}
}

View File

@ -18,15 +18,15 @@
package net.sergeych.lyng package net.sergeych.lyng
actual object PerfDefaults { actual object PerfDefaults {
actual val LOCAL_SLOT_PIC: Boolean = true actual val LOCAL_SLOT_PIC: Boolean = false
actual val EMIT_FAST_LOCAL_REFS: Boolean = true actual val EMIT_FAST_LOCAL_REFS: Boolean = false
actual val ARG_BUILDER: Boolean = true actual val ARG_BUILDER: Boolean = false
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
actual val SCOPE_POOL: Boolean = true actual val SCOPE_POOL: Boolean = false
actual val FIELD_PIC: Boolean = true actual val FIELD_PIC: Boolean = false
actual val METHOD_PIC: Boolean = true actual val METHOD_PIC: Boolean = false
actual val FIELD_PIC_SIZE_4: Boolean = false actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
@ -35,8 +35,8 @@ actual object PerfDefaults {
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = true actual val PRIMITIVE_FASTOPS: Boolean = false
actual val RVAL_FASTPATH: Boolean = true actual val RVAL_FASTPATH: Boolean = false
// Regex caching (JVM-first): enabled by default on JVM // Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true