fix $73 reg #74 val assignment bug fix. Also, cosmetics on syntax highlighting

This commit is contained in:
Sergey Chernov 2025-12-04 22:11:49 +01:00
parent e765784170
commit 603023962e
7 changed files with 246 additions and 22 deletions

View File

@ -2644,7 +2644,8 @@ class Compiler(
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
val initStmt = statement(nameToken.pos) { scp ->
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
scp.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
ObjVoid
}
cls.instanceInitializers += initStmt
@ -2652,7 +2653,8 @@ class Compiler(
} else {
// We are in instance scope already: perform initialization immediately
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
// Preserve mutability of declaration: create record with correct mutability
context.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
initValue
}
} else {

View File

@ -172,6 +172,13 @@ class Script(
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
}
// alias used in tests
addVoidFn("assertEqual") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
}
addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)

View File

@ -40,6 +40,8 @@ enum class HighlightKind {
Label,
Directive,
Error,
/** Enum constant (both declaration and usage). */
EnumConstant,
}
/** A highlighted span: character range and its semantic/lexical kind. */

View File

@ -159,8 +159,10 @@ class SimpleLyngHighlighter : LyngHighlighter {
}
if (range.endExclusive > range.start) raw += HighlightSpan(range, k)
}
// Heuristics: mark enum constants in declaration blocks and on qualified usages Foo.BAR
val overridden = applyEnumConstantHeuristics(text, src, tokens, raw)
// Adjust single-line comment spans to extend till EOL to compensate for lexer offset/length quirks
val adjusted = extendSingleLineCommentsToEol(text, raw)
val adjusted = extendSingleLineCommentsToEol(text, overridden)
// Spans are in order; merge adjacent of the same kind for compactness
return mergeAdjacent(adjusted)
}
@ -202,3 +204,81 @@ private fun extendSingleLineCommentsToEol(
}
return out
}
/**
* Detect enum constants both in enum declarations and in qualified usages (TypeName.CONST)
* and override corresponding identifier spans with EnumConstant kind.
*/
private fun applyEnumConstantHeuristics(
text: String,
src: Source,
tokens: List<net.sergeych.lyng.Token>,
spans: MutableList<HighlightSpan>
): MutableList<HighlightSpan> {
if (tokens.isEmpty() || spans.isEmpty()) return spans
// Build quick lookup from range start to span index for identifiers only
val byStart = HashMap<Int, Int>(spans.size * 2)
for (i in spans.indices) {
val s = spans[i]
if (s.kind == HighlightKind.Identifier) byStart[s.range.start] = i
}
fun overrideIdAtToken(idx: Int) {
val t = tokens[idx]
if (t.type != Type.ID) return
val start = src.offsetOf(t.pos)
val spanIndex = byStart[start] ?: return
spans[spanIndex] = HighlightSpan(spans[spanIndex].range, HighlightKind.EnumConstant)
}
// 1) Enum declarations: enum Name { CONST1, CONST2 }
var i = 0
while (i < tokens.size) {
val t = tokens[i]
if (t.type == Type.ID && t.value.equals("enum", ignoreCase = true)) {
// expect: ID(enum) ID(name) LBRACE (ID (COMMA ID)* ) RBRACE
var j = i + 1
// skip optional whitespace/newlines tokens are separate types, so we just check IDs and braces
if (j < tokens.size && tokens[j].type == Type.ID) j++ else { i++; continue }
if (j < tokens.size && tokens[j].type == Type.LBRACE) {
j++
while (j < tokens.size) {
val tk = tokens[j]
if (tk.type == Type.RBRACE) { j++; break }
if (tk.type == Type.ID) {
// enum entry declaration
overrideIdAtToken(j)
j++
// optional comma
if (j < tokens.size && tokens[j].type == Type.COMMA) { j++ ; continue }
continue
}
// Any unexpected token ends enum entries scan
break
}
i = j
continue
}
}
i++
}
// 2) Qualified usages: Something.CONST where CONST is ALL_UPPERCASE (with digits/underscores)
fun isAllUpperCase(name: String): Boolean = name.isNotEmpty() && name.all { it == '_' || it.isDigit() || (it.isLetter() && it.isUpperCase()) }
i = 1
while (i + 0 < tokens.size) {
val dotTok = tokens[i]
if (dotTok.type == Type.DOT && i + 1 < tokens.size) {
val next = tokens[i + 1]
if (next.type == Type.ID && isAllUpperCase(next.value)) {
overrideIdAtToken(i + 1)
i += 2
continue
}
}
i++
}
return spans
}

View File

@ -17,6 +17,8 @@
package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.canAccessMember
@ -41,11 +43,17 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 // do not default to objClass for outsiders
if (!canAccessMember(it.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't access field $name (declared in ${decl?.className ?: "?"})"
)
)
return it
}
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
val cls = objClass
// self first, then parents
fun findMangled(): ObjRecord? {
// self
@ -66,7 +74,12 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 // do not default to objClass for outsiders
if (!canAccessMember(rec.visibility, declaring, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${declaring?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't access field $name (declared in ${declaring?.className ?: "?"})"
)
)
return rec
}
// Fall back to methods/properties on class
@ -83,7 +96,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 // do not default to objClass for outsiders
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise()
}
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
@ -99,6 +115,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}
return null
}
val rec = findMangled()
if (rec != null) {
val declaring = when {
@ -109,7 +126,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 // do not default to objClass for outsiders
if (!canAccessMember(rec.visibility, declaring, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${declaring?.className ?: "?"})").raise()
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
).raise()
}
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (rec.value.assign(scope, newValue) == null)
@ -119,14 +139,21 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: (()->Obj?)?): Obj =
override suspend fun invokeInstanceMethod(
scope: Scope, name: String, args: Arguments,
onNotFoundResult: (() -> Obj?)?
): Obj =
instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
)
)
// execute with lexical class context propagated to declaring class
val saved = instanceScope.currentClassCtx
instanceScope.currentClassCtx = decl
@ -134,7 +161,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
rec.value.invoke(
instanceScope,
this,
args)
args
)
} finally {
instanceScope.currentClassCtx = saved
}
@ -146,7 +174,12 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
)
)
val saved = instanceScope.currentClassCtx
instanceScope.currentClassCtx = decl
try {
@ -181,20 +214,45 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
serializeStateVars(scope, encoder)
}
protected val instanceVars: Map<String, ObjRecord> by lazy {
override suspend fun toJson(scope: Scope): JsonElement {
// Call the class-provided map serializer:
val custom = invokeInstanceMethod(scope, "toJsonObject", Arguments.EMPTY, { ObjVoid })
if (custom != ObjVoid) {
// class json serializer returned something, so use it:
return custom.toJson(scope)
}
// no class serializer, serialize from constructor
val result = mutableMapOf<String, JsonElement>()
val meta = objClass.constructorMeta
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
for (entry in meta.params)
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
for (i in serializingVars)
result[i.key] = i.value.value.toJson(scope)
return JsonObject(result)
}
val instanceVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter { it.value.type.serializable }
}
protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) {
val serializingVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.serializable &&
it.value.type == ObjRecord.Type.Field &&
it.value.isMutable }
}
protected suspend fun serializeStateVars(scope: Scope, encoder: LynonEncoder) {
val vars = instanceVars.values.map { it.value }
if( vars.isNotEmpty()) {
if (vars.isNotEmpty()) {
encoder.encodeAnyList(scope, vars)
}
}
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
val localVars = instanceVars.values.toList()
if( localVars.isNotEmpty() ) {
if (localVars.isNotEmpty()) {
val vars = decoder.decodeAnyList(scope)
if (vars.size > instanceVars.size)
scope.raiseIllegalArgument("serialized vars has bigger size than instance vars")
@ -250,7 +308,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't access field $name (declared in ${decl?.className ?: "?"})"
)
)
return rec
}
}
@ -261,7 +324,15 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
if (!canAccessMember(r.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return when (val value = r.value) {
is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable)
is net.sergeych.lyng.Statement -> ObjRecord(
value.execute(
instance.instanceScope.createChildScope(
scope.pos,
newThisObj = instance
)
), r.isMutable
)
else -> r
}
}
@ -273,7 +344,10 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl.className})"
).raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
return
@ -284,7 +358,10 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
ObjIllegalAssignmentException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
return
@ -299,7 +376,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
if (r.value.assign(scope, newValue) == null) r.value = newValue
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: (() -> Obj?)?): Obj {
override suspend fun invokeInstanceMethod(
scope: Scope,
name: String,
args: Arguments,
onNotFoundResult: (() -> Obj?)?
): Obj {
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
memberFromAncestor(name)?.let { rec ->
val decl = rec.declaringClass ?: startClass
@ -320,7 +402,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
scope.raiseError(
ObjAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
)
)
val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl
try {

View File

@ -3815,4 +3815,47 @@ class ScriptTest {
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
}
// @Test
// fun testInstanceVars() = runTest {
// val x = eval("""
// class T(x,y)
// T(1, 2)
// """.trimIndent()) as ObjInstance
// println(x.serializingVars.map { "${it.key}=${it.value.value}"})
// }
@Test
fun memberValCantBeAssigned() = runTest {
eval("""
class Point(foo,bar) {
val t = 42
}
val p = Point(1,2)
// val should not be assignable:
assertThrows { p = Point(3,4) }
// val field must be readonly:
assertThrows { p.t = "bad" }
// and the value should not be changed
assertEqual(42, p.t)
""")
}
// @Test
// fun testClassToJson() = runTest {
// val x = eval("""
// import lyng.serialization
// class Point(foo,bar) {
// val t = 42
// }
// val p = Point(1,2)
// p.t = 121
// println(Point(10,"bar").toJsonString())
// Lynon.encode(Point(1,2))
// """.trimIndent())
// println((x as ObjBitBuffer).bitArray.asUByteArray().toDump())
//
// }
}

View File

@ -154,6 +154,7 @@ fun ensureLyngHighlightStyles() {
.hl-class { color: #5a32a3; font-weight: 600; }
.hl-val { color: #1b7f5a; }
.hl-var { color: #1b7f5a; text-decoration: underline dotted currentColor; }
.hl-enumc { color: #b08800; font-weight: 600; }
.hl-param { color: #0969da; font-style: italic; }
.hl-num { color: #005cc5; }
.hl-str { color: #032f62; }
@ -177,6 +178,7 @@ fun ensureLyngHighlightStyles() {
[data-bs-theme="dark"] .hl-class{ color: #d2a8ff; font-weight: 700; }
[data-bs-theme="dark"] .hl-val { color: #7ee787; }
[data-bs-theme="dark"] .hl-var { color: #7ee787; text-decoration: underline dotted currentColor; }
[data-bs-theme="dark"] .hl-enumc{ color: #f2cc60; font-weight: 700; }
[data-bs-theme="dark"] .hl-param{ color: #a5d6ff; font-style: italic; }
[data-bs-theme="dark"] .hl-num { color: #79c0ff; }
[data-bs-theme="dark"] .hl-str,
@ -677,6 +679,7 @@ private fun cssClassForKind(kind: HighlightKind): String = when (kind) {
HighlightKind.Label -> "hl-lbl"
HighlightKind.Directive -> "hl-dir"
HighlightKind.Error -> "hl-err"
HighlightKind.EnumConstant -> "hl-enumc"
}
/**