+ @Tranient for serialization

This commit is contained in:
sergeych committed 2026-01-16 07:47:27 +03:00
1 parent 05e15e8e42
commit b7dfda2f5d
14 files changed
+450 -54

No files matched your search

@@ -71,7 +71,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
declaringClass = declaringClass,
isTransient = a.isTransient)
}
return
}
@@ -82,7 +83,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
declaringClass = declaringClass,
isTransient = a.isTransient)
}
// Prepare positional args and parameter count, handle tail-block binding
@@ -239,5 +241,6 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val defaultValue: Statement? = null,
val accessType: AccessType? = null,
val visibility: Visibility? = null,
val isTransient: Boolean = false,
)
}
@@ -283,11 +283,13 @@ class Compiler(
}
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
private var isTransientFlag: Boolean = false
private var lastLabel: String? = null
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
lastAnnotation = null
lastLabel = null
isTransientFlag = false
while (true) {
val t = cc.next()
return when (t.type) {
@@ -306,6 +308,10 @@ class Compiler(
Token.Type.ATLABEL -> {
val label = t.value
if (label == "Transient") {
isTransientFlag = true
continue
}
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
lastLabel = label
}
@@ -887,7 +893,15 @@ class Compiler(
Token.Type.NEWLINE -> {}
Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
Token.Type.ID -> {
Token.Type.ID, Token.Type.ATLABEL -> {
var isTransient = false
if (t.type == Token.Type.ATLABEL) {
if (t.value == "Transient") {
isTransient = true
t = cc.next()
} else throw ScriptError(t.pos, "Unexpected label in argument list")
}
// visibility
val visibility = if (isClassDeclaration && t.value == "private") {
t = cc.next()
@@ -931,7 +945,8 @@ class Compiler(
isEllipsis,
defaultValue,
access,
visibility
visibility,
isTransient
)
// important: valid argument list continues with ',' and ends with '->' or ')'
@@ -2015,6 +2030,7 @@ class Compiler(
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
newClass.isAnonymous = nameToken == null
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
for (i in parentClasses.indices) {
val argsList = baseSpecs[i].args
// In object, we evaluate parent args once at creation time
@@ -2194,6 +2210,7 @@ class Compiler(
// but we should pass Pos.builtIn to skip validation for now if needed,
// or p.pos to allow it.
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField
)
}
@@ -2663,7 +2680,9 @@ class Compiler(
isOverride: Boolean = false,
isExtern: Boolean = false,
isStatic: Boolean = false,
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
var t = cc.next()
val start = t.pos
@@ -2833,16 +2852,16 @@ class Compiler(
val th = context.thisObj
if (isStatic) {
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, type = ObjRecord.Type.Delegated).apply {
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, isTransient = isTransient, type = ObjRecord.Type.Delegated).apply {
delegate = finalDelegate
}
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
delegate = finalDelegate
}
} else if (th is ObjClass) {
val cls: ObjClass = th
val storageName = "${cls.className}::$name"
cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, type = ObjRecord.Type.Delegated)
cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient, type = ObjRecord.Type.Delegated)
cls.instanceInitializers += statement(start) { scp ->
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
val initValue2 = delegateExpression.execute(scp)
@@ -2851,13 +2870,13 @@ class Compiler(
} catch (e: Exception) {
initValue2
}
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride).apply {
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient).apply {
delegate = finalDelegate2
}
ObjVoid
}
} else {
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
delegate = finalDelegate
}
}
@@ -2986,8 +3005,10 @@ class Compiler(
isClosed: Boolean = false,
isOverride: Boolean = false,
isStatic: Boolean = false,
isExtern: Boolean = false
isExtern: Boolean = false,
isTransient: Boolean = isTransientFlag
): Statement {
isTransientFlag = false
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
val nextToken = cc.next()
val start = nextToken.pos
@@ -3031,7 +3052,7 @@ class Compiler(
return statement(start) { context ->
val value = initialExpression.execute(context)
for (name in names) {
context.addItem(name, true, ObjVoid, visibility)
context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient)
}
pattern.setAt(start, context, value)
if (!isMutable) {
@@ -3233,17 +3254,18 @@ class Compiler(
visibility,
null,
start,
isTransient = isTransient,
type = ObjRecord.Type.Delegated
).apply {
delegate = finalDelegate
}
// Also expose in current init scope
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated).apply {
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated, isTransient = isTransient).apply {
delegate = finalDelegate
}
} else {
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start)
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start, isTransient = isTransient)
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
}
ObjVoid
}
@@ -3429,6 +3451,7 @@ class Compiler(
visibility,
setterVisibility,
start,
isTransient = isTransient,
type = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
@@ -3448,7 +3471,8 @@ class Compiler(
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
).apply {
delegate = finalDelegate
}
@@ -3469,7 +3493,8 @@ class Compiler(
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
rec.delegate = finalDelegate
return@statement finalDelegate
@@ -3488,7 +3513,8 @@ class Compiler(
recordType = ObjRecord.Type.Delegated,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
rec.delegate = finalDelegate
return@statement finalDelegate
@@ -3524,6 +3550,7 @@ class Compiler(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
type = ObjRecord.Type.Field
)
}
@@ -3553,7 +3580,8 @@ class Compiler(
recordType = ObjRecord.Type.Property,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
prop
}
@@ -3576,6 +3604,7 @@ class Compiler(
isClosed = isClosed,
isOverride = isOverride,
pos = start,
isTransient = isTransient,
type = ObjRecord.Type.Field
)
@@ -3591,7 +3620,8 @@ class Compiler(
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
ObjVoid
}
@@ -3608,14 +3638,15 @@ class Compiler(
recordType = ObjRecord.Type.Field,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
initValue
}
} else {
// Not in class body: regular local/var declaration
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other)
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other, isTransient = isTransient)
initValue
}
}
@@ -516,7 +516,8 @@ open class Scope(
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false
isOverride: Boolean = false,
isTransient: Boolean = false
): ObjRecord {
val rec = ObjRecord(
value, isMutable, visibility, writeVisibility,
@@ -524,7 +525,8 @@ open class Scope(
type = recordType,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension
@@ -17,16 +17,24 @@
package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
// Simple id generator for class identities (not thread-safe; fine for scripts)
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
val ObjClassType by lazy {
ObjClass("Class").apply {
object : ObjClass("Class") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val name = decoder.decodeObject(scope, ObjString.type, null) as ObjString
return scope.resolveQualifiedIdentifier(name.value)
}
}.apply {
addPropertyDoc(
name = "className",
doc = "Full name of this class including package if available.",
@@ -451,6 +459,7 @@ open class ObjClass(
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field,
): ObjRecord {
// Validation of override rules: only for non-system declarations
@@ -494,6 +503,7 @@ open class ObjClass(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
type = type
)
members[name] = rec
@@ -514,13 +524,14 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
pos: Pos = Pos.builtIn,
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field
): ObjRecord {
initClassScope()
val existing = classScope!!.objects[name]
if (existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type)
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type, isTransient = isTransient)
// Structural change: bump layout version for PIC invalidation
layoutVersion += 1
return rec
@@ -707,6 +718,22 @@ open class ObjClass(
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
if (isAnonymous) scope.raiseError("Cannot serialize anonymous class")
encoder.encodeObject(scope, classNameObj, ObjString.type.lynonType())
}
override suspend fun toJson(scope: Scope): JsonElement {
val result = mutableMapOf<String, JsonElement>()
result["__class_name"] = classNameObj.toJson(scope)
classScope?.objects?.forEach { (name, rec) ->
if (rec.type.serializable && rec.visibility.isPublic && !rec.isTransient) {
result[name] = rec.value.toJson(scope)
}
}
return JsonObject(result)
}
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
scope.raiseNotImplemented()
@@ -338,7 +338,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// values, so we save size of the construction:
// using objlist allow for some optimizations:
val params = meta.params.map { readField(scope, it.name).value }
val params = meta.params.filter { !it.isTransient }.map { readField(scope, it.name).value }
encoder.encodeAnyList(scope, params)
val vars = serializingVars.values.map { it.value }
if (vars.isNotEmpty()) {
@@ -357,8 +357,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
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 (entry in meta.params) {
if (!entry.isTransient)
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
}
for (i in serializingVars) {
// remove T:: prefix from the field name for JSON
val parts = i.key.split("::")
@@ -377,7 +379,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
it.value.type.serializable &&
it.value.type == ObjRecord.Type.Field &&
it.value.isMutable &&
!metaParams.contains(it.key)
!metaParams.contains(it.key) &&
!it.value.isTransient
}
}
@@ -398,7 +401,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable)
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable) && !it.value.isTransient
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,13 +28,26 @@ import net.sergeych.lynon.LynonType
class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val args = decoder.decodeAnyList(scope)
val actualSize = constructorMeta?.params?.size ?: 0
if (args.size > actualSize)
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
val newScope = scope.createChildScope(args = Arguments(args))
val instance = createInstance(newScope)
initializeInstance(instance, newScope.args, runConstructors = false)
val serializedArgs = decoder.decodeAnyList(scope)
val meta = constructorMeta ?: scope.raiseError("no constructor meta for $name")
val nonTransientCount = meta.params.count { !it.isTransient }
if (serializedArgs.size != nonTransientCount)
scope.raiseIllegalArgument("constructor $name expects $nonTransientCount non-transient arguments, but serialized version has ${serializedArgs.size}")
var sIdx = 0
val namedArgs = mutableMapOf<String, Obj>()
for (p in meta.params) {
if (!p.isTransient) {
namedArgs[p.name] = serializedArgs[sIdx++]
} else if (p.defaultValue == null) {
// If transient parameter has no default value, we use ObjNull to avoid "too few arguments" error
namedArgs[p.name] = ObjNull
}
}
// Using named arguments allows the constructor to apply default values for transient parameters
val newArgs = Arguments(list = emptyList<Obj>(), named = namedArgs)
val instance = createInstance(scope.createChildScope(args = newArgs))
initializeInstance(instance, newArgs, runConstructors = false)
return instance.apply {
deserializeStateVars(scope, decoder)
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ package net.sergeych.lynon
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjString
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
@@ -79,15 +80,16 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe
private suspend fun decodeClassObj(scope: Scope): ObjClass {
val className = decodeObject(scope, ObjString.type, null) as ObjString
return scope.get(className.value)?.value?.let {
if (it !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
it
if (it is ObjClass) return it
if (it is ObjInstance && it.objClass.className == className.value) return it.objClass
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
} ?: run {
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
val evaluated = scope.resolveQualifiedIdentifier(className.value)
if (evaluated !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
evaluated
if (evaluated is ObjClass) return evaluated
if (evaluated is ObjInstance && evaluated.objClass.className == className.value) return evaluated.objClass
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
evaluated as ObjClass // unreachable but for compiler
}
}