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:
sergeych committed 2025-12-23 08:02:48 +01:00
1 parent 157b716eb7
commit 5f3a54d08f
19 files changed
+458 -193

No files matched your search

@@ -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"
@@ -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()
@@ -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
}
@@ -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
@@ -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)
}
@@ -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
@@ -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)"
}
@@ -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
@@ -42,6 +42,7 @@ data class ObjRecord(
@Suppress("unused")
Class,
Enum,
Property,
Other;
val isArgument get() = this == Argument
@@ -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
}