+ @Tranient for serialization
This commit is contained in:
parent
05e15e8e42
commit
b7dfda2f5d
@ -14,6 +14,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation
|
||||
4. `void` (if loop body never executed and no `else`).
|
||||
- **Implicit Coroutines**: All functions are coroutines. No `async/await`. Use `launch { ... }` (returns `Deferred`) or `flow { ... }`.
|
||||
- **Variables**: `val` (read-only), `var` (mutable). Supports late-init `val` in classes (must be assigned in `init` or body).
|
||||
- **Serialization**: Use `@Transient` attribute before `val`/`var` or constructor parameters to exclude them from Lynon/JSON serialization. Transient fields are also ignored during `==` structural equality checks.
|
||||
- **Null Safety**: `?` (nullable type), `?.` (safe access), `?( )` (safe invoke), `?{ }` (safe block invoke), `?[ ]` (safe index), `?:` or `??` (elvis), `?=` (assign-if-null).
|
||||
- **Equality**: `==` (equals), `!=` (not equals), `===` (ref identity), `!==` (ref not identity).
|
||||
- **Comparison**: `<`, `>`, `<=`, `>=`, `<=>` (shuttle/spaceship, returns -1, 0, 1).
|
||||
|
||||
13
docs/OOP.md
13
docs/OOP.md
@ -857,6 +857,19 @@ Private fields are visible only _inside the class instance_:
|
||||
void
|
||||
>>> void
|
||||
|
||||
### Transient fields
|
||||
|
||||
You can mark a field or a constructor parameter as transient using the `@Transient` attribute. Transient members are ignored during serialization (Lynon and JSON) and are also excluded from structural equality (`==`) checks.
|
||||
|
||||
```lyng
|
||||
class Session(@Transient val token, val userId) {
|
||||
@Transient var lastAccess = time.now()
|
||||
var data = Map()
|
||||
}
|
||||
```
|
||||
|
||||
For more details on how transient fields behave during restoration, see the [Serialization Guide](serialization.md).
|
||||
|
||||
### Protected members
|
||||
|
||||
Protected members are available to the declaring class and all of its transitive subclasses (including via MI). Additionally, an ancestor class can access a `protected` member of its descendant if the ancestor also defines or inherits a member with the same name (i.e., it is an override of something the ancestor knows about).
|
||||
|
||||
@ -20,7 +20,18 @@ Simple classes serialization is supported:
|
||||
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
|
||||
>>> void
|
||||
|
||||
Note that mutable members are serialized:
|
||||
Note that mutable members are serialized by default. You can exclude any member (including constructor parameters) from JSON serialization using the `@Transient` attribute:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
class Point2(@Transient val foo, val bar) {
|
||||
@Transient var reason = 42
|
||||
var visible = 100
|
||||
}
|
||||
assertEquals( "{\"bar\":2,\"visible\":100}", Point2(1,2).toJsonString() )
|
||||
>>> void
|
||||
|
||||
Note that if you override json serialization:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
|
||||
@ -20,20 +20,37 @@ It is as simple as:
|
||||
assert( text.length > encodedBits.toBuffer().size )
|
||||
>>> void
|
||||
|
||||
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields:
|
||||
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields.
|
||||
|
||||
import lyng.serialization
|
||||
## Transient Fields
|
||||
|
||||
class Point(x,y)
|
||||
Sometimes you have fields that should not be serialized, for example, temporary caches, secret data, or derived values that are recomputed in `init` blocks. You can mark such fields with the `@Transient` attribute:
|
||||
|
||||
val p = Lynon.decode( Lynon.encode( Point(5,6) ) )
|
||||
```lyng
|
||||
class MyData(@Transient val tempSecret, val publicData) {
|
||||
@Transient var cachedValue = 0
|
||||
var persistentValue = 42
|
||||
|
||||
assertEquals( 5, p.x )
|
||||
assertEquals( 6, p.y )
|
||||
>>> void
|
||||
init {
|
||||
// cachedValue can be recomputed here upon deserialization
|
||||
cachedValue = computeCache(publicData)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Transient fields:
|
||||
- Are **omitted** from Lynon binary streams.
|
||||
- Are **omitted** from JSON output (via `toJson`).
|
||||
- Are **ignored** during structural equality checks (`==`).
|
||||
- If a transient constructor parameter has a **default value**, it will be restored to that default value during deserialization. Otherwise, it will be `null`.
|
||||
- Class body fields marked as `@Transient` will keep their initial values (or values assigned in `init`) after deserialization.
|
||||
|
||||
just as expected.
|
||||
## Serialization of Objects and Classes
|
||||
|
||||
- **Singleton Objects**: `object` declarations are serializable by name. Their state (mutable fields) is also serialized and restored, respecting `@Transient`.
|
||||
- **Classes**: Class objects themselves can be serialized. They are serialized by their full qualified name. When converted to JSON, a class object includes its public static fields (excluding those marked `@Transient`).
|
||||
|
||||
## Custom Serialization
|
||||
|
||||
Important is to understand that normally `Lynon.decode` wants [BitBuffer], as `Lynon.encode` produces. If you have the regular [Buffer], be sure to convert it:
|
||||
|
||||
|
||||
@ -171,6 +171,21 @@ settings["theme"] ?= "dark"
|
||||
|
||||
The operator returns the final value of the receiver (the original value if it was not `null`, or the new value if the assignment occurred).
|
||||
|
||||
### Transient Attribute (`@Transient`)
|
||||
The `@Transient` attribute can now be applied to class fields, constructor parameters, and static fields to exclude them from serialization.
|
||||
|
||||
```lyng
|
||||
class MyData(@Transient val tempSecret, val publicData) {
|
||||
@Transient var cachedValue = 0
|
||||
var persistentValue = 42
|
||||
}
|
||||
```
|
||||
|
||||
Key features:
|
||||
- **Serialization**: Transient members are omitted from both Lynon binary streams and JSON output.
|
||||
- **Structural Equality**: Transient fields are automatically ignored during `==` equality checks.
|
||||
- **Deserialization**: Transient constructor parameters with default values are correctly restored to those defaults upon restoration.
|
||||
|
||||
## Tooling and Infrastructure
|
||||
|
||||
### CLI: Formatting Command
|
||||
|
||||
@ -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)
|
||||
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)
|
||||
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}")
|
||||
it
|
||||
} ?: run {
|
||||
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
|
||||
val evaluated = scope.resolveQualifiedIdentifier(className.value)
|
||||
if (evaluated !is ObjClass)
|
||||
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
|
||||
evaluated as ObjClass // unreachable but for compiler
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3209,6 +3209,23 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInstantComponents() = runTest {
|
||||
// This is a proposal
|
||||
"""
|
||||
val t1 = Instant.fromRFC3339("1970-05-06T07:11:56Z")
|
||||
// components use default system calendar or modern
|
||||
assertEquals(t1.year, 1970)
|
||||
assertEquals(t1.month, 5)
|
||||
assertEquals(t1.dayOfMonth, 6)
|
||||
assertEquals(t1.hour, 7)
|
||||
assertEquals(t1.minute, 11)
|
||||
assertEquals(t1.second, 56)
|
||||
assertEquals("1970-05-06T07:11:56Z", t1.toRFC3339())
|
||||
assertEquals("1970-05-06T07:11:56Z", t1.toSortableString())
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoubleImports() = runTest {
|
||||
val s = Scope.new()
|
||||
|
||||
241
lynglib/src/commonTest/kotlin/net/sergeych/lyng/TransientTest.kt
Normal file
241
lynglib/src/commonTest/kotlin/net/sergeych/lyng/TransientTest.kt
Normal file
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.toBool
|
||||
import net.sergeych.lynon.lynonDecodeAny
|
||||
import net.sergeych.lynon.lynonEncodeAny
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class TransientTest {
|
||||
|
||||
@Test
|
||||
fun testTransient() = runTest {
|
||||
val script = """
|
||||
class TestTransient(@Transient val a, val b) {
|
||||
@Transient var c = 10
|
||||
var d = 20
|
||||
|
||||
fun check() {
|
||||
a == 1 && b == 2 && c == 10 && d == 20
|
||||
}
|
||||
}
|
||||
|
||||
val t = TestTransient(1, 2)
|
||||
t.c = 30
|
||||
t.d = 40
|
||||
t
|
||||
""".trimIndent()
|
||||
|
||||
val scope = Scope()
|
||||
val t = scope.eval(script) as ObjInstance
|
||||
|
||||
// Check initial state
|
||||
assertEquals(1, (t.readField(scope, "a").value as ObjInt).value)
|
||||
assertEquals(2, (t.readField(scope, "b").value as ObjInt).value)
|
||||
assertEquals(30, (t.readField(scope, "c").value as ObjInt).value)
|
||||
assertEquals(40, (t.readField(scope, "d").value as ObjInt).value)
|
||||
|
||||
// Serialize
|
||||
val serialized = lynonEncodeAny(scope, t)
|
||||
println("[DEBUG_LOG] Serialized size: ${serialized.size}")
|
||||
|
||||
// Deserialized
|
||||
val t2 = lynonDecodeAny(scope, serialized) as ObjInstance
|
||||
|
||||
// b and d should be preserved
|
||||
assertEquals(2, (t2.readField(scope, "b").value as ObjInt).value)
|
||||
assertEquals(40, (t2.readField(scope, "d").value as ObjInt).value)
|
||||
|
||||
// a and c should be transient (lost or default/null)
|
||||
// For constructor args, we currently set ObjNull if transient
|
||||
assertEquals(ObjNull, t2.readField(scope, "a").value)
|
||||
// For class fields, if it's transient it's not serialized, so it gets its initial value during construction
|
||||
assertEquals(10, (t2.readField(scope, "c").value as ObjInt).value)
|
||||
|
||||
// Check JSON
|
||||
val json = t.toJson(scope).toString()
|
||||
println("[DEBUG_LOG] JSON: $json")
|
||||
assertFalse(json.contains("\"a\":"))
|
||||
assertFalse(json.contains("\"c\":"))
|
||||
assertNotNull(json.contains("\"b\":2"))
|
||||
assertNotNull(json.contains("\"d\":40"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransientDefaultAndEquality() = runTest {
|
||||
val script = """
|
||||
class TestExt(@Transient val a = 100, val b) {
|
||||
@Transient var c = 200
|
||||
var d = 300
|
||||
}
|
||||
|
||||
val t1 = TestExt(b: 2)
|
||||
t1.c = 300
|
||||
t1.d = 400
|
||||
|
||||
val t2 = TestExt(a: 50, b: 2)
|
||||
t2.c = 500
|
||||
t2.d = 400
|
||||
|
||||
// Equality should ignore transient fields a and c
|
||||
val equal = (t1 == t2)
|
||||
|
||||
[t1, t2, equal]
|
||||
""".trimIndent()
|
||||
|
||||
val scope = Scope()
|
||||
val result = (scope.eval(script) as net.sergeych.lyng.obj.ObjList).list
|
||||
val t1 = result[0] as ObjInstance
|
||||
val t2 = result[1] as ObjInstance
|
||||
val equal = result[2].toBool()
|
||||
|
||||
assertEquals(true, equal, "Objects should be equal despite different transient fields")
|
||||
|
||||
// Serialize t1
|
||||
val serialized = lynonEncodeAny(scope, t1)
|
||||
val t1d = lynonDecodeAny(scope, serialized) as ObjInstance
|
||||
|
||||
// a should have its default value 100, not null or 10
|
||||
assertEquals(100, (t1d.readField(scope, "a").value as ObjInt).value)
|
||||
// c should have its initial value 200
|
||||
assertEquals(200, (t1d.readField(scope, "c").value as ObjInt).value)
|
||||
// b and d should be preserved
|
||||
assertEquals(2, (t1d.readField(scope, "b").value as ObjInt).value)
|
||||
assertEquals(400, (t1d.readField(scope, "d").value as ObjInt).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStaticTransient() = runTest {
|
||||
val script = """
|
||||
class TestStatic {
|
||||
@Transient static var x = 10
|
||||
static var y = 20
|
||||
}
|
||||
TestStatic.x = 30
|
||||
TestStatic.y = 40
|
||||
TestStatic
|
||||
""".trimIndent()
|
||||
val scope = Scope()
|
||||
scope.eval(script)
|
||||
// Static fields aren't serialized yet, but we ensure the parser accepts it
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransientSize() = runTest {
|
||||
val script = """
|
||||
class Data1(val a, val b) {
|
||||
var c = 30
|
||||
}
|
||||
class Data2(val a, val b, @Transient val x) {
|
||||
var c = 30
|
||||
@Transient var y = 40
|
||||
}
|
||||
|
||||
val d1 = Data1(10, 20)
|
||||
val d2 = Data2(10, 20, 100)
|
||||
d2.y = 200
|
||||
|
||||
[d1, d2]
|
||||
""".trimIndent()
|
||||
|
||||
val scope = Scope()
|
||||
val result = (scope.eval(script) as net.sergeych.lyng.obj.ObjList).list
|
||||
val d1 = result[0] as ObjInstance
|
||||
val d2 = result[1] as ObjInstance
|
||||
|
||||
val s1 = lynonEncodeAny(scope, d1)
|
||||
val s2 = lynonEncodeAny(scope, d2)
|
||||
|
||||
println("[DEBUG_LOG] Data1 size: ${s1.size}")
|
||||
println("[DEBUG_LOG] Data2 size: ${s2.size}")
|
||||
|
||||
assertEquals(s1.size, s2.size, "Serialized sizes should match because transient fields are not serialized")
|
||||
|
||||
val j1 = d1.toJson(scope).toString()
|
||||
val j2 = d2.toJson(scope).toString()
|
||||
|
||||
println("[DEBUG_LOG] Data1 JSON: $j1")
|
||||
println("[DEBUG_LOG] Data2 JSON: $j2")
|
||||
|
||||
assertEquals(j1.length, j2.length, "JSON lengths should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testObjectTransient() = runTest {
|
||||
val script = """
|
||||
object MyObject {
|
||||
@Transient var temp = 10
|
||||
var persistent = 20
|
||||
}
|
||||
MyObject.temp = 30
|
||||
MyObject.persistent = 40
|
||||
MyObject
|
||||
""".trimIndent()
|
||||
|
||||
val scope = Scope()
|
||||
val obj = scope.eval(script) as ObjInstance
|
||||
|
||||
val serialized = lynonEncodeAny(scope, obj)
|
||||
val deserialized = lynonDecodeAny(scope, serialized) as ObjInstance
|
||||
|
||||
// persistent should be 40
|
||||
assertEquals(40, (deserialized.readField(scope, "persistent").value as ObjInt).value)
|
||||
// temp should be restored to 10
|
||||
assertEquals(10, (deserialized.readField(scope, "temp").value as ObjInt).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStaticTransientToJson() = runTest {
|
||||
val script = """
|
||||
class TestStatic {
|
||||
@Transient static var s1 = 10
|
||||
static var s2 = 20
|
||||
private static var s3 = 30
|
||||
}
|
||||
TestStatic
|
||||
""".trimIndent()
|
||||
|
||||
val scope = Scope()
|
||||
val cls = scope.eval(script) as net.sergeych.lyng.obj.ObjClass
|
||||
|
||||
val json = cls.toJson(scope).toString()
|
||||
println("[DEBUG_LOG] Class JSON: $json")
|
||||
|
||||
// s2 should be in JSON
|
||||
assertNotNull(json.contains("\"s2\":20"))
|
||||
// s1 should NOT be in JSON (transient)
|
||||
assertFalse(json.contains("\"s1\":"))
|
||||
// s3 should NOT be in JSON (private)
|
||||
assertFalse(json.contains("\"s3\":"))
|
||||
// __class_name should be there
|
||||
assertNotNull(json.contains("\"__class_name\":\"TestStatic\""))
|
||||
|
||||
// Test serialization/deserialization of the class itself
|
||||
val serialized = lynonEncodeAny(scope, cls)
|
||||
val deserialized = lynonDecodeAny(scope, serialized) as net.sergeych.lyng.obj.ObjClass
|
||||
assertEquals(cls, deserialized)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user