refs #35 serialize any: null, int, real, boolean, Instant. Added unary minus as general operator, not only to numbers. Instant truncation (nice for serialization)

This commit is contained in:
Sergey Chernov 2025-07-17 23:18:00 +03:00
parent cffe4eaffc
commit 6ab438b1f6
14 changed files with 208 additions and 51 deletions

View File

@ -3,17 +3,22 @@
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models: Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
- `Instant` class for time stamps with platform-dependent resolution - `Instant` class for time stamps with platform-dependent resolution
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds, hours, days) - `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
hours, days)
## Time instant: `Instant` ## Time instant: `Instant`
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time can be for example introduced or dropped). It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin. Some moment of time; not the calendar date. Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time
can be for example introduced or dropped). It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin. Some moment of
time; not the calendar date.
Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on the calendar, e.g. absolute time period. Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on
the calendar, e.g. absolute time period.
It is possible to add or subtract `Duration` to and from `Instant`, that gives another `Instant`. It is possible to add or subtract `Duration` to and from `Instant`, that gives another `Instant`.
Instants are converted to and from `Real` number of seconds before or after Unix Epoch, 01.01.1970. Constructor with single number parameter constructs from such number of seconds, Instants are converted to and from `Real` number of seconds before or after Unix Epoch, 01.01.1970. Constructor with
single number parameter constructs from such number of seconds,
and any instance provide `.epochSeconds` member: and any instance provide `.epochSeconds` member:
import lyng.time import lyng.time
@ -61,13 +66,13 @@ The resolution of system clock could be more precise and double precision real n
## Getting the max precision ## Getting the max precision
Normally, subtracting instants gives precision to microseconds, which is well inside the jitter Normally, subtracting instants gives precision to microseconds, which is well inside the jitter
the language VM adds. Still `Instant()` captures most precise system timer at hand and provide the language VM adds. Still `Instant()` or `Instant.now()` capture most precise system timer at hand and provide inner
inner value of 12 bytes, up to nanoseconds (hopefully). To access it use: value of 12 bytes, up to nanoseconds (hopefully). To access it use:
import lyng.time import lyng.time
// capture time // capture time
val now = Instant() val now = Instant.now()
// this is Int value, number of whole epoch // this is Int value, number of whole epoch
// milliseconds to the moment, it fits 8 bytes Int well // milliseconds to the moment, it fits 8 bytes Int well
@ -84,6 +89,22 @@ inner value of 12 bytes, up to nanoseconds (hopefully). To access it use:
assertEquals( now.epochSeconds, nanos * 1e-9 + seconds ) assertEquals( now.epochSeconds, nanos * 1e-9 + seconds )
>>> void >>> void
## Truncating to more realistic precision
Full precision Instant is way too long and impractical to store, especially when serializing,
so it is possible to truncate it to milliseconds, microseconds or seconds:
import lyng.time
import lyng.serialization
// max supported size (now microseconds for serialized value):
assert( Lynon.encode(Instant.now()).size in [8,9] )
// shorter: milliseconds only
assertEquals( 7, Lynon.encode(Instant.now().truncateToMillisecond()).size )
// truncated to seconds, good for file mtime, etc:
assertEquals( 6, Lynon.encode(Instant.now().truncateToSecond()).size )
>>> void
## Formatting instants ## Formatting instants
You can freely use `Instant` in string formatting. It supports usual sprintf-style formats: You can freely use `Instant` in string formatting. It supports usual sprintf-style formats:
@ -104,27 +125,36 @@ You can freely use `Instant` in string formatting. It supports usual sprintf-sty
assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) ) assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) )
>>> void >>> void
See the [complete list of available formats](https://github.com/sergeych/mp_stools?tab=readme-ov-file#datetime-formatting) and the [formatting reference](https://github.com/sergeych/mp_stools?tab=readme-ov-file#printf--sprintf): it all works in Lyng as `"format"(args...)`! See
the [complete list of available formats](https://github.com/sergeych/mp_stools?tab=readme-ov-file#datetime-formatting)
and the [formatting reference](https://github.com/sergeych/mp_stools?tab=readme-ov-file#printf--sprintf): it all works
in Lyng as `"format"(args...)`!
## Instant members ## Instant members
| member | description | | member | description |
|--------------------------|---------------------------------------------------------| |--------------------------------|---------------------------------------------------------|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch | | epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster | | epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) | | nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) |
| isDistantFuture: Bool | true if it `Instant.distantFuture` | | isDistantFuture: Bool | true if it `Instant.distantFuture` |
| isDistantPast: Bool | true if it `Instant.distantPast` | | isDistantPast: Bool | true if it `Instant.distantPast` |
| truncateToSecond: Intant | create new instnce truncated to second |
| truncateToMillisecond: Instant | truncate new instance with to millisecond |
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
(1) (1)
: The value of nanoseconds is to be added to `epochWholeSeconds` to get exact time point. It is in 0..999_999_999 range. The precise time instant value therefore needs as for now 12 bytes integer; we might use bigint later (it is planned to be added) : The value of nanoseconds is to be added to `epochWholeSeconds` to get exact time point. It is in 0..999_999_999 range.
The precise time instant value therefore needs as for now 12 bytes integer; we might use bigint later (it is planned to
be added)
## Class members ## Class members
| member | description | | member | description |
|--------------------------------|---------------------------------------------------------| |--------------------------------|----------------------------------------------|
| Instant.distantPast: Instant | most distant instant in past | | Instant.now() | create new instance with current system time |
| Instant.distantFuture: Instant | most distant instant in future | | Instant.distantPast: Instant | most distant instant in past |
| Instant.distantFuture: Instant | most distant instant in future |
# `Duraion` class # `Duraion` class
@ -153,7 +183,8 @@ Duration can be converted from numbers, like `5.minutes` and so on. Extensions a
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`. The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration` instance: Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration`
instance:
- `d.microseconds` - `d.microseconds`
- `d.milliseconds` - `d.milliseconds`

View File

@ -715,7 +715,7 @@ class Compiler(
} }
} }
private fun parseAccessor(): Accessor? { private suspend fun parseAccessor(): Accessor? {
// could be: literal // could be: literal
val t = cc.next() val t = cc.next()
return when (t.type) { return when (t.type) {
@ -737,8 +737,14 @@ class Compiler(
} }
Token.Type.MINUS -> { Token.Type.MINUS -> {
val n = parseNumber(false) parseNumberOrNull(false)?.let { n ->
Accessor { n.asReadonly } Accessor { n.asReadonly }
} ?: run {
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary minus")
Accessor {
n.getter.invoke(it).value.negate(it).asReadonly
}
}
} }
Token.Type.ID -> { Token.Type.ID -> {
@ -769,7 +775,8 @@ class Compiler(
} }
} }
private fun parseNumber(isPlus: Boolean): Obj { private fun parseNumberOrNull(isPlus: Boolean): Obj? {
val pos = cc.savePos()
val t = cc.next() val t = cc.next()
return when (t.type) { return when (t.type) {
Token.Type.INT, Token.Type.HEX -> { Token.Type.INT, Token.Type.HEX -> {
@ -783,11 +790,16 @@ class Compiler(
} }
else -> { else -> {
throw ScriptError(t.pos, "expected number") cc.restorePos(pos)
null
} }
} }
} }
private fun parseNumber(isPlus: Boolean): Obj {
return parseNumberOrNull(isPlus) ?: throw ScriptError(cc.currentPos(), "Expecting number")
}
/** /**
* Parse keyword-starting statement. * Parse keyword-starting statement.
* @return parsed statement or null if, for example. [id] is not among keywords * @return parsed statement or null if, for example. [id] is not among keywords

View File

@ -138,6 +138,10 @@ open class Obj {
scope.raiseNotImplemented() scope.raiseNotImplemented()
} }
open suspend fun negate(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun mul(scope: Scope, other: Obj): Obj { open suspend fun mul(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented() scope.raiseNotImplemented()
} }

View File

@ -5,8 +5,9 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.isDistantFuture import kotlinx.datetime.isDistantFuture
import kotlinx.datetime.isDistantPast import kotlinx.datetime.isDistantPast
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonSettings
class ObjInstant(val instant: Instant) : Obj() { class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() {
override val objClass: ObjClass get() = type override val objClass: ObjClass get() = type
override fun toString(): String { override fun toString(): String {
@ -102,10 +103,31 @@ class ObjInstant(val instant: Instant) : Obj() {
addFn("nanosecondsOfSecond") { addFn("nanosecondsOfSecond") {
ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong())
} }
addFn("truncateToSecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
}
addFn("truncateToMillisecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
LynonSettings.InstantTruncateMode.Millisecond
)
}
addFn("truncateToMicrosecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),
LynonSettings.InstantTruncateMode.Microsecond
)
}
// class members // class members
addClassConst("distantFuture", distantFuture) addClassConst("distantFuture", distantFuture)
addClassConst("distantPast", distantPast) addClassConst("distantPast", distantPast)
addClassFn("now") {
ObjInstant(Clock.System.now())
}
// addFn("epochMilliseconds") { // addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds()) // ObjInt(instant.toEpochMilliseconds())
// } // }

View File

@ -97,6 +97,10 @@ class ObjInt(var value: Long,override val isConst: Boolean = false) : Obj(), Num
return value == other.value return value == other.value
} }
override suspend fun negate(scope: Scope): Obj {
return ObjInt(-value)
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { override suspend fun serialize(scope: Scope, encoder: LynonEncoder) {
encoder.encodeSigned(value) encoder.encodeSigned(value)
} }

View File

@ -61,6 +61,10 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value == other.value return value == other.value
} }
override suspend fun negate(scope: Scope): Obj {
return ObjReal(-value)
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder) { override suspend fun serialize(scope: Scope, encoder: LynonEncoder) {
encoder.encodeReal(value) encoder.encodeReal(value)
} }

View File

@ -88,8 +88,8 @@ data class ObjString(val value: String) : Obj() {
val type = object : ObjClass("String") { val type = object : ObjClass("String") {
override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj = override fun deserialize(scope: Scope, decoder: LynonDecoder): Obj =
ObjString( ObjString(
decoder.unpackBinaryData()?.decodeToString() decoder.unpackBinaryData().decodeToString()
?: scope.raiseError("unexpected end of data") // ?: scope.raiseError("unexpected end of data")
) )
}.apply { }.apply {
addFn("toInt") { addFn("toInt") {

View File

@ -82,5 +82,9 @@ interface BitInput {
fun decompressStringOrNull(): String? = decompressOrNull()?.decodeToString() fun decompressStringOrNull(): String? = decompressOrNull()?.decodeToString()
fun decompressString(): String = decompress().decodeToString() fun decompressString(): String = decompress().decodeToString()
fun unpackDouble(): Double {
val bits = getBits(64)
return Double.fromBits(bits.toLong())
}
} }

View File

@ -1,36 +1,57 @@
package net.sergeych.lynon package net.sergeych.lynon
import kotlinx.datetime.Instant
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.*
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
open class LynonDecoder(val bin: BitInput,val settings: LynonSettings = LynonSettings.default) { open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
val cache = mutableListOf<Obj>() val cache = mutableListOf<Obj>()
inline fun decodeCached(f: LynonDecoder.() -> Obj): Obj { inline fun decodeCached(f: LynonDecoder.() -> Obj): Obj {
return if( bin.getBit() == 0 ) { return if (bin.getBit() == 0) {
// unpack and cache // unpack and cache
f().also { f().also {
if( settings.shouldCache(it) ) cache.add(it) if (settings.shouldCache(it)) cache.add(it)
} }
} } else {
else {
// get cache reference // get cache reference
val size = sizeInBits(cache.size) val size = sizeInBits(cache.size)
val id = bin.getBitsOrNull(size)?.toInt() ?: throw RuntimeException("Invalid object id: unexpected end of stream") val id = bin.getBitsOrNull(size)?.toInt()
if( id >= cache.size ) throw RuntimeException("Invalid object id: $id should be in 0..<${cache.size}") ?: 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] cache[id]
} }
} }
fun decodeAny(scope: Scope): Obj = decodeCached { fun decodeAny(scope: Scope): Obj = decodeCached {
val type = LynonType.entries[bin.getBits(4).toInt()] val type = LynonType.entries[bin.getBits(4).toInt()]
return when(type) { return when (type) {
LynonType.Null -> ObjNull LynonType.Null -> ObjNull
LynonType.Int0 -> ObjInt.Zero 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 -> { else -> {
scope.raiseNotImplemented("lynon type $type") scope.raiseNotImplemented("lynon type $type")
} }

View File

@ -1,9 +1,7 @@
package net.sergeych.lynon package net.sergeych.lynon
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.*
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
enum class LynonType { enum class LynonType {
Null, Null,
@ -69,6 +67,29 @@ open class LynonEncoder(val bout: BitOutput,val settings: LynonSettings = LynonS
} }
} }
} }
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 -> { else -> {
TODO() TODO()
} }

View File

@ -6,7 +6,12 @@ import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjNull
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
open class LynonSettings() { open class LynonSettings {
enum class InstantTruncateMode {
Second,
Millisecond,
Microsecond
}
open fun shouldCache(obj: Any): Boolean = when (obj) { open fun shouldCache(obj: Any): Boolean = when (obj) {
is ObjChar -> false is ObjChar -> false

View File

@ -52,8 +52,10 @@ class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
return result.toString() return result.toString()
} }
@Suppress("unused")
fun asByteArray(): ByteArray = bytes.asByteArray() fun asByteArray(): ByteArray = bytes.asByteArray()
@Suppress("unused")
fun asUbyteArray(): UByteArray = bytes fun asUbyteArray(): UByteArray = bytes
companion object { companion object {
@ -82,10 +84,10 @@ class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
* added by [putBit] will be stored in the bit 0x01 of the first byte, the second bit * added by [putBit] will be stored in the bit 0x01 of the first byte, the second bit
* in the bit 0x02 of the first byte, etc. * in the bit 0x02 of the first byte, etc.
* *
* This allow automatic fill of the last byte with zeros. This is important when * This allows automatic fill of the last byte with zeros. This is important when
* using bytes stored from [asByteArray] or [asUbyteArray]. When converting to * using bytes stored from [asByteArray] or [asUbyteArray]. When converting to
* bytes, automatic padding to byte size is applied. With such bit order, constrinting * bytes, automatic padding to byte size is applied. With such bit order, constructing
* [BitInput] to read from [asByteArray] result only provides 0 to 7 extra zeroes bits * [BitInput] to read from [ByteArray.toUByteArray] result only provides 0 to 7 extra zeroes bits
* at teh end which is often acceptable. To avoid this, use [toBitArray]; the [BitArray] * at teh end which is often acceptable. To avoid this, use [toBitArray]; the [BitArray]
* stores exact number of bits and [BitArray.toBitInput] provides [BitInput] that * stores exact number of bits and [BitArray.toBitInput] provides [BitInput] that
* decodes exactly same bits. * decodes exactly same bits.

View File

@ -2649,4 +2649,12 @@ class ScriptTest {
""".trimIndent()) """.trimIndent())
} }
@Test
fun testRangeToList() = runTest {
val x = eval("""(1..10).toList()""") as ObjList
assertEquals(listOf(1,2,3,4,5,6,7,8,9,10), x.list.map { it.toInt() })
val y = eval("""(-2..3).toList()""") as ObjList
println(y.list)
}
} }

View File

@ -303,10 +303,29 @@ class LynonTests {
@Test @Test
fun testIntsNulls() = runTest{ fun testUnaryMinus() = runTest{
eval(""" eval("""
import lyng.serialization assertEquals( -1 * π, 0 - π )
assertEquals( null, Lynon.decode(Lynon.encode(null)) ) assertEquals( -1 * π, -π )
""".trimIndent())
}
@Test
fun testIntsNulls() = runTest{
testScope().eval("""
testEncode(null)
testEncode(0)
testEncode(47)
testEncode(-21)
testEncode(true)
testEncode(false)
testEncode(1.22345)
testEncode(-π)
import lyng.time
testEncode(Instant.now().truncateToSecond())
testEncode(Instant.now().truncateToMillisecond())
testEncode(Instant.now().truncateToMicrosecond())
""".trimIndent()) """.trimIndent())
} }