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
/kotlin-js-store/wasm/yarn.lock
/distributables
/.output.txt
.output*.txt
debug.log
/build.log

View File

@ -2,6 +2,15 @@
### 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
- 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`).

View File

@ -189,6 +189,7 @@ Ready features:
- [x] better stack reporting
- [x] regular exceptions + extended `when`
- [x] multiple inheritance for user classes
- [x] class properties (accessors)
## 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
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
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}_]*:" } ] },
"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" } } } ] },
"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|π)" } ] },
"types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] },
"operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] },

View File

@ -34,7 +34,8 @@ class LyngLexer : LexerBase() {
private val keywords = setOf(
"fun", "val", "var", "class", "type", "import", "as",
"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) {

View File

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

View File

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

View File

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

View File

@ -232,7 +232,7 @@ open class Obj {
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

View File

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

View File

@ -24,36 +24,32 @@ import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
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 doubleValue get() = value.toDouble()
override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value)
override fun byValueCopy(): Obj = this
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun getAndIncrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value++ }
return this
}
override suspend fun getAndDecrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value-- }
return this
}
override suspend fun incrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(++value)
return ObjInt(value + 1)
}
override suspend fun decrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(--value)
return ObjInt(value - 1)
}
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())
/**
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
* assignment
* Numbers are now immutable, so we can't do in-place assignment.
*/
override suspend fun assign(scope: Scope, other: Obj): Obj? {
return if (!isConst && other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun assign(scope: Scope, other: Obj): Obj? = null
override suspend fun toKotlin(scope: Scope): Any {
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 fun byValueCopy(): Obj = ObjReal(value)
override fun byValueCopy(): Obj = this
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2

View File

@ -42,6 +42,7 @@ data class ObjRecord(
@Suppress("unused")
Class,
Enum,
Property,
Other;
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")
val v = rec.value
val one = ObjInt.One
return if (v.isConst) {
// Mirror existing semantics in Compiler for const values
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
// write back
target.setAt(atPos, scope, result)
// For post-inc: previous code returned NEW value; for pre-inc: returned ORIGINAL value
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
}
// We now treat numbers as immutable and always perform write-back via setAt.
// This avoids issues where literals are shared and mutated in-place.
// For post-inc: return ORIGINAL value; for pre-inc: return NEW value.
val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one)
target.setAt(atPos, scope, result)
return (if (isPost) v else result).asReadonly
}
}
@ -635,12 +625,16 @@ class FieldRef(
val (k, v) = receiverKeyAndVersion(base)
val rec = tRecord
if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) {
// visibility/mutability checks
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
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 it is a property, we must go through writeField (slow path for now)
// or handle it here.
if (rec.type != ObjRecord.Type.Property) {
// visibility/mutability checks
if (!rec.isMutable) scope.raiseError(ObjIllegalAssignmentException(scope, "can't reassign val $name"))
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) {
@ -1229,7 +1223,7 @@ class MethodCallRef(
/**
* 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) {
block(name)
}
@ -1253,7 +1247,6 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos
// 1) Try fast slot/local
if (!PerfFlags.LOCAL_SLOT_PIC) {
scope.getSlotIndexOf(name)?.let {
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.
*/
class FastLocalVarRef(
private val name: String,
val name: String,
private val atPos: Pos,
) : ObjRef {
override fun forEachVariable(block: (String) -> Unit) {
@ -1779,10 +1772,16 @@ class AssignRef(
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val v = if (PerfFlags.RVAL_FASTPATH) value.evalValue(scope) else value.get(scope).value
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)
// For properties, we should not call get() on target because it invokes the getter.
// Instead, we call setAt directly.
if (target is FieldRef || target is IndexRef || target is LocalVarRef || target is FastLocalVarRef || target is BoundLocalVarRef) {
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
}

View File

@ -2318,29 +2318,33 @@ class ScriptTest {
@Test
fun doWhileValuesLabelTest() = runTest {
withTimeout(5.seconds) {
eval(
"""
var count = 0
var count2 = 0
var count3 = 0
val result = outer@ do {
count2++
count = 0
do {
count++
if( count < 10 || count2 < 5 ) {
continue
}
if( count % 2 == 1 )
break@outer "found "+count + "/" + count2
} while(count < 14)
count3++
} while( count2 < 100 )
else "no"
assertEquals("found 11/5", result)
assertEquals( 4, count3)
""".trimIndent()
)
try {
eval(
"""
var count = 0
var count2 = 0
var count3 = 0
val result = outer@ do {
count2++
count = 0
do {
count++
if( count < 10 || count2 < 5 ) {
continue
}
if( count % 2 == 1 )
break@outer "found "+count + "/" + count2
} while(count < 14)
count3++
} while( count2 < 100 )
else "no"
assertEquals("found 11/5", result)
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
actual object PerfDefaults {
actual val LOCAL_SLOT_PIC: Boolean = true
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
actual val LOCAL_SLOT_PIC: Boolean = false
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
actual val ARG_BUILDER: Boolean = true
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
actual val SCOPE_POOL: Boolean = true
actual val ARG_BUILDER: Boolean = false
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
actual val SCOPE_POOL: Boolean = false
actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC: Boolean = false
actual val METHOD_PIC: Boolean = false
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_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 PRIMITIVE_FASTOPS: Boolean = true
actual val RVAL_FASTPATH: Boolean = true
actual val PRIMITIVE_FASTOPS: Boolean = false
actual val RVAL_FASTPATH: Boolean = false
// Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true