diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index b3affbc..d507a04 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 21c507a..48f100d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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(0) + val b = requiredArg(1) + if( a.compareTo(this, b) != 0 ) + raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}")) + } addVoidFn("assertNotEquals") { val a = requiredArg(0) val b = requiredArg(1) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/HighlightApi.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/HighlightApi.kt index 7b05550..a689eea 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/HighlightApi.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/HighlightApi.kt @@ -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. */ diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt index 07faed0..1338ab7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -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, + spans: MutableList +): MutableList { + if (tokens.isEmpty() || spans.isEmpty()) return spans + + // Build quick lookup from range start to span index for identifiers only + val byStart = HashMap(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 +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index d58839e..79ad5df 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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 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() + 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 by lazy { instanceScope.objects.filter { it.value.type.serializable } } - protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) { + val serializingVars: Map 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 { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 9d68192..30dcf94 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3815,4 +3815,47 @@ class ScriptTest { assertEquals(JSTest1("bar", 1, true), x.decodeSerializable()) } +// @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()) +// +// } + } diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt index 7685e22..5ac8579 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt @@ -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" } /**