diff --git a/docs/OOP.md b/docs/OOP.md index 91fa2a4..b6320b4 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -250,11 +250,10 @@ Note `Real` class: it is global variable for Real class; there are such class in assert('$'::class == Char) >>> void -More complex is singleton classes, because you don't need to compare their class -instances and generally don't need them at all, these are normally just Obj: +Singleton classes also have class: null::class - >>> Obj + >>> Null At this time, `Obj` can't be accessed as a class. diff --git a/docs/time.md b/docs/time.md index 33a0107..3ebec7a 100644 --- a/docs/time.md +++ b/docs/time.md @@ -98,11 +98,16 @@ so it is possible to truncate it to milliseconds, microseconds or seconds: import lyng.serialization // max supported size (now microseconds for serialized value): - assert( Lynon.encode(Instant.now()).size in [8,9] ) + // note that encoding return _bit array_ and this is a _bit size_: + val s0 = Lynon.encode(Instant.now()).size + // shorter: milliseconds only - assertEquals( 7, Lynon.encode(Instant.now().truncateToMillisecond()).size ) + val s1 = Lynon.encode(Instant.now().truncateToMillisecond()).size + // truncated to seconds, good for file mtime, etc: - assertEquals( 6, Lynon.encode(Instant.now().truncateToSecond()).size ) + val s2 = Lynon.encode(Instant.now().truncateToSecond()).size + assert( s1 < s0 ) + assert( s2 < s1 ) >>> void ## Formatting instants diff --git a/docs/tutorial.md b/docs/tutorial.md index ecdf482..8af8a28 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1217,25 +1217,26 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion. Typical set of String functions includes: -| fun/prop | description / notes | -|--------------------|------------------------------------------------------------| -| lower() | change case to unicode upper | -| upper() | change case to unicode lower | +| fun/prop | description / notes | +|-------------------|------------------------------------------------------------| +| lower() | change case to unicode upper | +| upper() | change case to unicode lower | | startsWith(prefix) | true if starts with a prefix | -| endsWith(prefix) | true if ends with a prefix | -| take(n) | get a new string from up to n first characters | -| takeLast(n) | get a new string from up to n last characters | -| drop(n) | get a new string dropping n first chars, or empty string | -| dropLast(n) | get a new string dropping n last chars, or empty string | -| size | size in characters like `length` because String is [Array] | -| (args...) | sprintf-like formatting, see [string formatting] | -| [index] | character at index | -| [Range] | substring at range | -| s1 + s2 | concatenation | -| s1 += s2 | self-modifying concatenation | -| toReal() | attempts to parse string as a Real value | -| toInt() | parse string to Int value | -| characters() | create [List] of characters (1) | +| endsWith(prefix) | true if ends with a prefix | +| take(n) | get a new string from up to n first characters | +| takeLast(n) | get a new string from up to n last characters | +| drop(n) | get a new string dropping n first chars, or empty string | +| dropLast(n) | get a new string dropping n last chars, or empty string | +| size | size in characters like `length` because String is [Array] | +| (args...) | sprintf-like formatting, see [string formatting] | +| [index] | character at index | +| [Range] | substring at range | +| s1 + s2 | concatenation | +| s1 += s2 | self-modifying concatenation | +| toReal() | attempts to parse string as a Real value | +| toInt() | parse string to Int value | +| characters() | create [List] of characters (1) | +| encodeUtf8() | returns [Buffer] with characters encoded to utf8 | (1) : List is mutable therefore a new copy is created on each call. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 961d311..f66ab97 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -46,6 +46,10 @@ open class Scope( fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing = raiseError(ObjIllegalArgumentException(this, message)) + @Suppress("unused") + fun raiseIllegalState(message: String = "Illegal argument error"): Nothing = + raiseError(ObjIllegalStateException(this, message)) + @Suppress("unused") fun raiseNoSuchElement(message: String = "No such element"): Nothing = raiseError(ObjIllegalArgumentException(this, message)) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt new file mode 100644 index 0000000..a872f6e --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Accessor.kt @@ -0,0 +1,33 @@ + +package net.sergeych.lyng.obj + +import net.sergeych.lyng.Compiler +import net.sergeych.lyng.Pos +import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScriptError + +// avoid KDOC bug: keep it +@Suppress("unused") +typealias DocCompiler = Compiler +/** + * When we need read-write access to an object in some abstract storage, we need Accessor, + * as in-site assigning is not always sufficient, in general case we need to replace the object + * in the storage. + * + * Note that assigning new value is more complex than just replacing the object, see how assignment + * operator is implemented in [Compiler.allOps]. + */ +data class Accessor( + val getter: suspend (Scope) -> ObjRecord, + val setterOrNull: (suspend (Scope, Obj) -> Unit)? +) { + /** + * Simplified constructor for immutable stores. + */ + constructor(getter: suspend (Scope) -> ObjRecord) : this(getter, null) + + /** + * Get the setter or throw. + */ + fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value") +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index cc1a2c6..a30fe02 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -6,54 +6,19 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.sergeych.bintools.encodeToHex import net.sergeych.lyng.* +import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType import net.sergeych.synctools.ProtectedOp import net.sergeych.synctools.withLock import kotlin.contracts.ExperimentalContracts -/** - * Record to store object with access rules, e.g. [isMutable] and access level [visibility]. - */ -data class ObjRecord( - var value: Obj, - val isMutable: Boolean, - val visibility: Visibility = Visibility.Public, - var importedFrom: Scope? = null -) { - @Suppress("unused") - fun qualifiedName(name: String): String = - "${importedFrom?.packageName ?: "anonymous"}.$name" -} - -/** - * When we need read-write access to an object in some abstract storage, we need Accessor, - * as in-site assigning is not always sufficient, in general case we need to replace the object - * in the storage. - * - * Note that assigning new value is more complex than just replacing the object, see how assignment - * operator is implemented in [Compiler.allOps]. - */ -data class Accessor( - val getter: suspend (Scope) -> ObjRecord, - val setterOrNull: (suspend (Scope, Obj) -> Unit)? -) { - /** - * Simplified constructor for immutable stores. - */ - constructor(getter: suspend (Scope) -> ObjRecord) : this(getter, null) - - /** - * Get the setter or throw. - */ - fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value") -} - open class Obj { open val isConst: Boolean = false fun ensureNotConst(scope: Scope) { - if( isConst ) scope.raiseError("can't assign to constant") + if (isConst) scope.raiseError("can't assign to constant") } val isNull by lazy { this === ObjNull } @@ -273,33 +238,35 @@ open class Obj { val asReadonly: ObjRecord by lazy { ObjRecord(this, false) } val asMutable: ObjRecord by lazy { ObjRecord(this, true) } - open suspend fun serialize(scope: Scope, encoder: LynonEncoder) { + open suspend fun lynonType(): LynonType = LynonType.Other + + open suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { scope.raiseNotImplemented() } companion object { val rootObjectType = ObjClass("Obj").apply { - addFn("toString") { - thisObj.asStr - } - addFn("contains") { - ObjBool(thisObj.contains(this, args.firstAndOnly())) - } - // utilities - addFn("let") { - args.firstAndOnly().callOn(copy(Arguments(thisObj))) - } - addFn("apply") { - val newContext = ( thisObj as? ObjInstance)?.instanceScope ?: this - args.firstAndOnly() - .callOn(newContext) - thisObj - } - addFn("also") { - args.firstAndOnly().callOn(copy(Arguments(thisObj))) - thisObj - } + addFn("toString") { + thisObj.asStr + } + addFn("contains") { + ObjBool(thisObj.contains(this, args.firstAndOnly())) + } + // utilities + addFn("let") { + args.firstAndOnly().callOn(copy(Arguments(thisObj))) + } + addFn("apply") { + val newContext = (thisObj as? ObjInstance)?.instanceScope ?: this + args.firstAndOnly() + .callOn(newContext) + thisObj + } + addFn("also") { + args.firstAndOnly().callOn(copy(Arguments(thisObj))) + thisObj + } addFn("getAt") { requireExactCount(1) thisObj.getAt(this, requiredArg(0)) @@ -395,6 +362,26 @@ object ObjNull : Obj() { override suspend fun toKotlin(scope: Scope): Any? { return null } + + override suspend fun lynonType(): LynonType { + return LynonType.Null + } + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { + if (lynonType == null) { + encoder.putBit(0) + } + } + + override val objClass: ObjClass by lazy { + object : ObjClass("Null") { + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { + if (lynonType == LynonType.Null) + return this@ObjNull + else + scope.raiseIllegalState("can't deserialize null directly or with wrong type: ${lynonType}") + } + } + } } interface Numeric { @@ -525,6 +512,9 @@ class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") : ObjException("IllegalArgumentException", scope, message) +class ObjIllegalStateException(scope: Scope, message: String = "illegal state") : + ObjException("IllegalStateException", scope, message) + @Suppress("unused") class ObjNoSuchElementException(scope: Scope, message: String = "no such element") : ObjException("IllegalArgumentException", scope, message) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt new file mode 100644 index 0000000..4f88d25 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt @@ -0,0 +1,37 @@ +package net.sergeych.lyng.obj + +import net.sergeych.bintools.toDump +import net.sergeych.lyng.Scope +import net.sergeych.lynon.BitArray + +class ObjBitBuffer(val bitArray: BitArray) : Obj() { + + override val objClass = type + + override suspend fun getAt(scope: Scope, index: Obj): Obj { + return bitArray[index.toLong()].toObj() + } + + companion object { + val type = object: ObjClass("BitBuffer", ObjArray) { + + }.apply { + addFn("toBuffer") { + requireNoArgs() + ObjBuffer(thisAs().bitArray.asUbyteArray()) + } + addFn("toDump") { + requireNoArgs() + ObjString( + thisAs().bitArray.asUbyteArray().toDump() + ) + } + addFn("size") { + thisAs().bitArray.size.toObj() + } + addFn("sizeInBytes") { + ObjInt((thisAs().bitArray.size + 7) shr 3) + } + } + } +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt index 672345e..16abd0e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBool.kt @@ -3,6 +3,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType data class ObjBool(val value: Boolean) : Obj() { override val asStr by lazy { ObjString(value.toString()) } @@ -30,7 +31,9 @@ data class ObjBool(val value: Boolean) : Obj() { return value } - override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { + override suspend fun lynonType(): LynonType = LynonType.Bool + + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { encoder.encodeBoolean(value) } @@ -45,7 +48,7 @@ data class ObjBool(val value: Boolean) : Obj() { companion object { val type = object : ObjClass("Bool") { - override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj { + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder,lynonType: LynonType?): Obj { return ObjBool(decoder.unpackBoolean()) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt index c3f4a8f..a02ef6e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt @@ -2,9 +2,13 @@ package net.sergeych.lyng.obj import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.toDump import net.sergeych.lyng.Scope import net.sergeych.lyng.statement +import net.sergeych.lynon.LynonDecoder +import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType import kotlin.math.min open class ObjBuffer(val byteArray: UByteArray) : Obj() { @@ -63,7 +67,7 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() { } override fun toString(): String { - return "Buffer(${byteArray.toList()})" + return "Buffer(${byteArray.encodeToHex()})" } override fun equals(other: Any?): Boolean { @@ -75,6 +79,14 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() { return byteArray contentEquals other.byteArray } + override suspend fun lynonType(): LynonType = LynonType.Buffer + + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { + encoder.encodeCached(byteArray) { + bout.compress(byteArray.asByteArray()) + } + } + companion object { private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer = when (obj) { @@ -124,6 +136,12 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() { } } } + + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = + ObjBuffer( decoder.decodeCached { + decoder.decompress().asUByteArray() + }) + }.apply { createField("size", statement { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 71def80..d7384bf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -2,6 +2,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.* import net.sergeych.lynon.LynonDecoder +import net.sergeych.lynon.LynonType val ObjClassType by lazy { ObjClass("Class") } @@ -98,7 +99,7 @@ open class ObjClass( return classMembers[name]?.value?.invoke(scope, this, args) ?: super.invokeInstanceMethod(scope, name, args) } - open fun deserialize(scope: Scope, decoder: LynonDecoder): Obj = scope.raiseNotImplemented() + open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented() } 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 c178094..337b06e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -3,6 +3,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Arguments import net.sergeych.lyng.Scope import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType class ObjInstance(override val objClass: ObjClass) : Obj() { @@ -45,7 +46,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { return "${objClass.className}($fields)" } - override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { val meta = objClass.constructorMeta ?: scope.raiseError("can't serialize non-serializable object (no constructor meta)") for( p in meta.params) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt index a566023..a9a2773 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt @@ -5,7 +5,10 @@ import kotlinx.datetime.Instant import kotlinx.datetime.isDistantFuture import kotlinx.datetime.isDistantPast import net.sergeych.lyng.Scope +import net.sergeych.lynon.LynonDecoder +import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonSettings +import net.sergeych.lynon.LynonType class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() { override val objClass: ObjClass get() = type @@ -53,6 +56,22 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru return instant == other.instant } + override suspend fun lynonType(): LynonType = LynonType.Instant + + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { + encoder.putBits(truncateMode.ordinal, 2) + when(truncateMode) { + LynonSettings.InstantTruncateMode.Millisecond -> + encoder.encodeSigned(instant.toEpochMilliseconds()) + LynonSettings.InstantTruncateMode.Second -> + encoder.encodeSigned(instant.epochSeconds) + LynonSettings.InstantTruncateMode.Microsecond -> { + encoder.encodeSigned(instant.epochSeconds) + encoder.encodeUnsigned(instant.nanosecondsOfSecond.toULong() / 1000UL) + } + } + } + companion object { val distantFuture by lazy { ObjInstant(Instant.DISTANT_FUTURE) @@ -86,6 +105,26 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru } ) } + + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { + val mode = LynonSettings.InstantTruncateMode.entries[decoder.getBitsAsInt(2)] + return when (mode) { + LynonSettings.InstantTruncateMode.Microsecond -> ObjInstant( + Instant.fromEpochSeconds( + decoder.unpackSigned(), decoder.unpackUnsignedInt() * 1000 + ) + ) + LynonSettings.InstantTruncateMode.Millisecond -> ObjInstant( + Instant.fromEpochMilliseconds( + decoder.unpackSigned() + ) + ) + LynonSettings.InstantTruncateMode.Second -> ObjInstant( + Instant.fromEpochSeconds(decoder.unpackSigned()) + ) + } + } + }.apply { addFn("epochSeconds") { val instant = thisAs().instant diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt index 5ff6867..6dbd327 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -3,8 +3,9 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope 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(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric { override val asStr get() = ObjString(value.toString()) override val longValue get() = value override val doubleValue get() = value.toDouble() @@ -101,16 +102,37 @@ class ObjInt(var value: Long,override val isConst: Boolean = false) : Obj(), Num return ObjInt(-value) } - override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { - encoder.encodeSigned(value) + override suspend fun lynonType(): LynonType = when (value) { + 0L -> LynonType.Int0 + else -> { + if (value > 0) LynonType.IntPositive + else LynonType.IntNegative + } + } + + + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { + when (lynonType) { + null -> encoder.encodeSigned(value) + LynonType.Int0 -> {} + LynonType.IntPositive -> encoder.encodeUnsigned(value.toULong()) + LynonType.IntNegative -> encoder.encodeUnsigned((-value).toULong()) + else -> scope.raiseIllegalArgument("Unsupported lynon type code for Int: $lynonType") + } } companion object { val Zero = ObjInt(0, true) val One = ObjInt(1, true) - val type = object: ObjClass("Int") { - override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj = - ObjInt(decoder.unpackSigned()) + val type = object : ObjClass("Int") { + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = + when (lynonType) { + null -> ObjInt(decoder.unpackSigned()) + LynonType.Int0 -> Zero + LynonType.IntPositive -> ObjInt(decoder.unpackUnsigned().toLong()) + LynonType.IntNegative -> ObjInt(-decoder.unpackUnsigned().toLong()) + else -> scope.raiseIllegalState("illegal type code for Int: $lynonType") + } } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt index 369e557..4981809 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt @@ -5,6 +5,7 @@ import net.sergeych.lyng.Scope import net.sergeych.lyng.statement import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType import kotlin.math.floor import kotlin.math.roundToLong @@ -65,13 +66,15 @@ data class ObjReal(val value: Double) : Obj(), Numeric { return ObjReal(-value) } - override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { + override suspend fun lynonType(): LynonType = LynonType.Real + + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { encoder.encodeReal(value) } companion object { val type: ObjClass = object : ObjClass("Real") { - override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj = + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = ObjReal(decoder.unpackDouble()) }.apply { createField( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt new file mode 100644 index 0000000..cc56a81 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -0,0 +1,18 @@ +package net.sergeych.lyng.obj + +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Visibility + +/** + * Record to store object with access rules, e.g. [isMutable] and access level [visibility]. + */ +data class ObjRecord( + var value: Obj, + val isMutable: Boolean, + val visibility: Visibility = Visibility.Public, + var importedFrom: Scope? = null +) { + @Suppress("unused") + fun qualifiedName(name: String): String = + "${importedFrom?.packageName ?: "anonymous"}.$name" +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index a09a390..9a90af3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt @@ -6,6 +6,7 @@ import net.sergeych.lyng.Scope import net.sergeych.lyng.statement import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder +import net.sergeych.lynon.LynonType import net.sergeych.sprintf.sprintf @Serializable @@ -80,13 +81,13 @@ data class ObjString(val value: String) : Obj() { return value == other.value } - override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { + override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) { encoder.encodeBinaryData(value.encodeToByteArray()) } companion object { val type = object : ObjClass("String") { - override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj = + override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = ObjString( decoder.unpackBinaryData().decodeToString() // ?: scope.raiseError("unexpected end of data") @@ -135,6 +136,7 @@ data class ObjString(val value: String) : Obj() { thisAs().value.map { ObjChar(it) }.toMutableList() ) } + addFn("encodeUtf8") { ObjBuffer(thisAs().value.encodeToByteArray().asUByteArray()) } addFn("size") { ObjInt(thisAs().value.length.toLong()) } addFn("toReal") { ObjReal(thisAs().value.toDouble()) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt index 51c8e69..4d6a12f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt @@ -1,14 +1,23 @@ package net.sergeych.lynon -import kotlinx.datetime.Instant import net.sergeych.lyng.Scope -import net.sergeych.lyng.obj.* +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) { - val cache = mutableListOf() + fun getBitsAsInt(bitsSize: Int): Int { + return bin.getBits(bitsSize).toInt() + } - inline fun decodeCached(f: LynonDecoder.() -> Obj): Obj { + fun unpackUnsignedInt(): Int = bin.unpackUnsigned().toInt() + + fun decompress() = bin.decompress() + + val cache = mutableListOf() + + + inline fun decodeCached(f: LynonDecoder.() -> T): T { return if (bin.getBit() == 0) { // unpack and cache f().also { @@ -20,46 +29,19 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe val id = bin.getBitsOrNull(size)?.toInt() ?: throw RuntimeException("Invalid object id: unexpected end of stream") if (id >= cache.size) throw RuntimeException("Invalid object id: $id should be in 0..<${cache.size}") - cache[id] + @Suppress("UNCHECKED_CAST") + cache[id] as T } } - fun decodeAny(scope: Scope): Obj = decodeCached { + suspend fun decodeAny(scope: Scope): Obj = decodeCached { val type = LynonType.entries[bin.getBits(4).toInt()] - return when (type) { - LynonType.Null -> ObjNull - LynonType.Int0 -> ObjInt.Zero - LynonType.IntPositive -> ObjInt(bin.unpackUnsigned().toLong()) - LynonType.IntNegative -> ObjInt(-bin.unpackUnsigned().toLong()) - LynonType.Bool -> ObjBool(bin.getBit() == 1) - LynonType.Real -> ObjReal(bin.unpackDouble()) - LynonType.Instant -> { - val mode = LynonSettings.InstantTruncateMode.entries[bin.getBits(2).toInt()] - when (mode) { - LynonSettings.InstantTruncateMode.Microsecond -> ObjInstant( - Instant.fromEpochSeconds( - bin.unpackSigned(), bin.unpackUnsigned().toInt() * 1000 - ) - ) - LynonSettings.InstantTruncateMode.Millisecond -> ObjInstant( - Instant.fromEpochMilliseconds( - bin.unpackSigned() - ) - ) - LynonSettings.InstantTruncateMode.Second -> ObjInstant( - Instant.fromEpochSeconds(bin.unpackSigned()) - ) - } - } - - else -> { - scope.raiseNotImplemented("lynon type $type") - } - } + type.objClass.deserialize(scope, this, type) } - fun unpackObject(scope: Scope, type: ObjClass): Obj { - return decodeCached { type.deserialize(scope, this) } + // todo: rewrite/remove? + suspend fun unpackObject(scope: Scope, type: ObjClass): Obj { + return decodeCached { type.deserialize(scope, this, null) } } fun unpackBinaryData(): ByteArray = bin.decompress() @@ -79,4 +61,8 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe return bin.unpackSigned() } + fun unpackUnsigned(): ULong { + return bin.unpackUnsigned() + } + } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonEncoder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonEncoder.kt index 64cd767..c5cc44b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonEncoder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonEncoder.kt @@ -1,30 +1,31 @@ package net.sergeych.lynon +import net.sergeych.bintools.ByteChunk import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.* -enum class LynonType { - Null, - Int0, - IntNegative, - IntPositive, - String, - Real, - Bool, - List, - Map, - Set, - Buffer, - Instant, - Duration, - Other; +enum class LynonType(val objClass: ObjClass) { + Null(ObjNull.objClass), + Int0(ObjInt.type), + IntNegative(ObjInt.type), + IntPositive(ObjInt.type), + String(ObjString.type), + Real(ObjReal.type), + Bool(ObjBool.type), + List(ObjList.type), + Map(ObjMap.type), + Set(ObjSet.type), + Buffer(ObjBuffer.type), + Instant(ObjInstant.type), + Duration(ObjDuration.type), + Other(Obj.rootObjectType); } open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonSettings.default) { val cache = mutableMapOf() - private suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) { + suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) { suspend fun serializeAndCache(key: Any=item) { bout.putBit(0) @@ -40,7 +41,8 @@ open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonS bout.putBits(cacheId.toULong(), size) } ?: serializeAndCache() - is ByteArray, is UByteArray -> serializeAndCache() + is ByteArray -> serializeAndCache(ByteChunk(item.asUByteArray())) + is UByteArray -> serializeAndCache(ByteChunk(item)) } } @@ -52,48 +54,9 @@ open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonS */ suspend fun encodeAny(scope: Scope,value: Obj) { encodeCached(value) { - when(value) { - is ObjNull -> putType(LynonType.Null) - is ObjInt -> { - when { - value.value == 0L -> putType(LynonType.Int0) - value.value < 0 -> { - putType(LynonType.IntNegative) - encodeUnsigned((-value.value).toULong()) - } - else -> { - putType(LynonType.IntPositive) - encodeUnsigned(value.value.toULong()) - } - } - } - is ObjBool -> { - putType(LynonType.Bool) - encodeBoolean(value.value) - } - is ObjReal -> { - putType(LynonType.Real) - encodeReal(value.value) - } - is ObjInstant -> { - putType(LynonType.Instant) - bout.putBits(value.truncateMode.ordinal, 2) - // todo: favor truncation mode from ObjInstant - when(value.truncateMode) { - LynonSettings.InstantTruncateMode.Millisecond -> - encodeSigned(value.instant.toEpochMilliseconds()) - LynonSettings.InstantTruncateMode.Second -> - encodeSigned(value.instant.epochSeconds) - LynonSettings.InstantTruncateMode.Microsecond -> { - encodeSigned(value.instant.epochSeconds) - encodeUnsigned(value.instant.nanosecondsOfSecond.toULong() / 1000UL) - } - } - } - else -> { - TODO() - } - } + val type = value.lynonType() + putType(type) + value.serialize(scope, this, type) } } @@ -103,7 +66,7 @@ open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonS suspend fun encodeObj(scope: Scope, obj: Obj) { encodeCached(obj) { - obj.serialize(scope, this) + obj.serialize(scope, this, null) } } @@ -133,4 +96,12 @@ open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonS bout.putBit(if (value) 1 else 0) } + fun putBits(value: Int, sizeInBits: Int) { + bout.putBits(value.toULong(), sizeInBits) + } + + fun putBit(bit: Int) { + bout.putBit(bit) + } + } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt index eac7bcc..3a47852 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt @@ -2,7 +2,7 @@ package net.sergeych.lynon import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjBuffer +import net.sergeych.lyng.obj.ObjBitBuffer import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjString @@ -15,11 +15,12 @@ val ObjLynonClass = object : ObjClass("Lynon") { val bout = MemoryBitOutput() val serializer = LynonEncoder(bout) serializer.encodeAny(this, obj) - return ObjBuffer(bout.toBitArray().bytes) + return ObjBitBuffer(bout.toBitArray()) } - suspend fun Scope.decodeAny(buffer: ObjBuffer): Obj { - val bin = BitArray(buffer.byteArray,8).toInput() + suspend fun Scope.decodeAny(source: Obj): Obj { + if( source !is ObjBitBuffer) throw Exception("Invalid source: $source") + val bin = source.bitArray.toInput() val deserializer = LynonDecoder(bin) return deserializer.decodeAny(this) } @@ -30,6 +31,6 @@ val ObjLynonClass = object : ObjClass("Lynon") { encodeAny(requireOnlyArg()) } addClassFn("decode") { - decodeAny(requireOnlyArg()) + decodeAny(requireOnlyArg()) } } \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/LynonTests.kt b/lynglib/src/jvmTest/kotlin/LynonTests.kt index 644d343..98be838 100644 --- a/lynglib/src/jvmTest/kotlin/LynonTests.kt +++ b/lynglib/src/jvmTest/kotlin/LynonTests.kt @@ -309,9 +309,9 @@ class LynonTests { assertEquals( -1 * π, -π ) """.trimIndent()) } - + @Test - fun testIntsNulls() = runTest{ + fun testSimpleTypes() = runTest{ testScope().eval(""" testEncode(null) testEncode(0) @@ -326,6 +326,9 @@ class LynonTests { testEncode(Instant.now().truncateToSecond()) testEncode(Instant.now().truncateToMillisecond()) testEncode(Instant.now().truncateToMicrosecond()) + + testEncode("Hello, world".encodeUtf8()) + """.trimIndent()) }