Compare commits

...
41 Commits
Author SHA1 Message Date
sergeych f7f020f4d6 0.8.2-SNAPSHOT 2025-08-07 21:50:14 +03:00
sergeych 0f54e2f845 ref #47 yield 2025-08-07 21:46:45 +03:00
sergeych 19f8b6605b fix #47 parallelism: launch, Deferred, CompletableDeferred, Mutex (delay was already implemented) 2025-08-07 20:31:36 +03:00
sergeych de71c7c8ec 0.8.1 release version 2025-08-06 23:12:25 +03:00
sergeych be370dfdef fix #45 private statics and docs 2025-08-06 23:10:33 +03:00
sergeych 53a6a88924 fix #42 class static fields and methods 2025-08-06 22:48:32 +03:00
sergeych 2c2dae4d85 well redesigned class vals and vars, classScope introduced for class functions 2025-08-06 11:47:45 +03:00
sergeych 63b2808109 well redesigned class vals and vars, classScope introduced for class functions 2025-08-06 11:46:17 +03:00
sergeych 4165da5e81 published 0.8.0-SNAPSHOT 2025-08-04 16:52:43 +03:00
sergeych af463163bb published 0.8.0-SNAPSHOT 2025-08-04 16:52:25 +03:00
sergeych 6df06a6911 fix #35 serialization alsso user classes. Fixed user classes comparison 2025-08-04 16:49:55 +03:00
sergeych f805e1ee82 sets serialization 2025-08-03 15:11:15 +03:00
sergeych 790cce0d24 maps serialization 2025-08-03 15:06:46 +03:00
sergeych 2339130241 trivial implementation of class name = based serialization (LynonType.Other) 2025-08-03 14:42:23 +03:00
sergeych d7bd159fcb list serialization (optimized, homogenous and heterogeneous 2025-08-03 13:30:41 +03:00
sergeych 12b209c724 refs #35 generic implementation of Huffman compression for variable bit length source alphabet 2025-07-23 20:51:31 +03:00
sergeych 20181c63a1 refs #35 refac to get caching out of Obj; it is decoder/encoder matter now 2025-07-19 14:19:50 +03:00
sergeych 405ff2ec2b refs #35 hack to avoid cache clush for encodeAny and encode, double caching same object conflict. to refactor! 2025-07-19 14:12:41 +03:00
sergeych a9f65bdbe3 refs #35 serializatino framework refactored: implementation put in open methods of Obj/ObjClass for flexibility 2025-07-19 12:28:14 +03:00
sergeych 6ab438b1f6 refs #35 serialize any: null, int, real, boolean, Instant. Added unary minus as general operator, not only to numbers. Instant truncation (nice for serialization) 2025-07-17 23:23:03 +03:00
sergeych cffe4eaffc refs #35 bits in BitArray/input/output reordered for better performance; started typed serialization 2025-07-17 12:33:32 +03:00
sergeych 7aee25ffef refs #35 Buffer is not mutable, MutableBuffer added (to cache in serialized form) 2025-07-16 11:47:23 +03:00
sergeych f3d766d1b1 refs #35 Lynon builtin compression 2025-07-16 11:10:38 +03:00
sergeych 34bc7297bd refs #35 bit granularity for bitstreams; LZW done 2025-07-13 00:37:27 +03:00
sergeych 23dafff453 refs #35 started user class serialization; started MP compression 2025-07-12 22:15:00 +03:00
sergeych 77f9191387 refs #35 caching moved to level above objec serializing so we can cache strings, etc that are not Obj instances 2025-07-12 11:48:14 +03:00
sergeych f26ee7cd7c refs #35 minimal serialization of simple types in bit-effective format 2025-07-12 00:46:51 +03:00
sergeych d969993997 ref #35 bitwise pack/unpack for integers 2025-07-11 06:09:09 +03:00
sergeych 987b80e44d lynon started 2025-07-11 05:56:43 +03:00
sergeych 5848adca61 fix #39 correct implementation of ++ and -- with indexing access 2025-07-10 15:54:53 +03:00
sergeych f1ae4b2d23 fixed CLI for new compiler interface 2025-07-10 12:47:00 +03:00
sergeych 30b6ef235b each new scope with no parent now starts with a copy of import manager to isolate its imports 2025-07-10 12:42:39 +03:00
sergeych 9771b40c98 fixed double imports bug
added clean import scope Scope.new()
2025-07-10 12:41:10 +03:00
sergeych 230cb0a067 ref #34 time formatting and precision time access 2025-07-10 11:10:26 +03:00
sergeych 732d8f3877 fix #34 minimal time manipulation 2025-07-10 00:00:02 +03:00
sergeych 23006b5caa fix #33 minimal Buffer with docs, in a separate package 2025-07-09 17:56:08 +03:00
sergeych 26282d3e22 fix #38 ImportManager integrated into Scope tree and all systems 2025-07-09 13:15:28 +03:00
sergeych ce4ed5c819 migrated to kotlin 2.2.0
better support for shebangs in CLI lyng
added jlyng local release
2025-07-07 23:44:21 +03:00
sergeych 612c0fb7b9 v.0.7.1 improved imports 2025-07-07 15:02:10 +03:00
sergeych ef6bc5c468 refs #36 imports refined and optimized, circular imports are supported. 2025-07-07 15:01:27 +03:00
sergeych 1e2cb5e420 fixed import inside sources in InlineSourcesPacman 2025-07-04 02:23:45 +03:00
91 changed files with 7460 additions and 568 deletions

No files matched your search

+1
View File
@@ -14,3 +14,4 @@ xcuserdata
.gigaide .gigaide
/kotlin-js-store/yarn.lock /kotlin-js-store/yarn.lock
/test.lyng /test.lyng
/sample_texts/1.txt.gz
+9 -7
View File
@@ -23,7 +23,7 @@ fun swapEnds(first, args..., last, f) {
delay(1.5) // coroutine is delayed for 1.5s, thread is not blocked! delay(1.5) // coroutine is delayed for 1.5s, thread is not blocked!
``` ```
and it is multithreaded on platforms supporting it (automatically, no code changes required, just and it is multithreaded on platforms supporting it (automatically, no code changes required, just
`launch` more coroutines and they will be executed concurrently if possible)/ `launch` more coroutines and they will be executed concurrently if possible). See [parallelism]
- functional style and OOP together, multiple inheritance, implementing interfaces for existing classes, writing extensions. - functional style and OOP together, multiple inheritance, implementing interfaces for existing classes, writing extensions.
- Any unicode letters can be used as identifiers: `assert( sin(π/2) == 1 )`. - Any unicode letters can be used as identifiers: `assert( sin(π/2) == 1 )`.
@@ -142,12 +142,12 @@ Ready features:
Under way: Under way:
- [ ] maps, sets and sequences (flows?) - [x] maps, sets and sequences (flows?)
- [ ] regular exceptions - [ ] regular exceptions
- [ ] modules - [x] modules
- [ ] string interpolation and more string tools - [x] string formatting and tools
- [ ] multiple inheritance for user classes - [ ] multiple inheritance for user classes
- [ ] launch, deferred, coroutineScope, mutex, etc. - [x] launch, deferred, CompletableDeferred, Mutex, etc.
- [ ] site with integrated interpreter to give a try - [ ] site with integrated interpreter to give a try
- [ ] kotlin part public API good docs, integration focused - [ ] kotlin part public API good docs, integration focused
- [ ] better stack reporting - [ ] better stack reporting
@@ -158,10 +158,12 @@ Planned features.
- [ ] type specifications - [ ] type specifications
- [ ] source docs and maybe lyng.md to a standard - [ ] source docs and maybe lyng.md to a standard
- [ ] moacro-style kotlin integration or something else to simplify it - [ ] macro-style kotlin integration or something else to simplify it
Further Further
- [ ] client with GUI support based on compose multiplatform somehow - [ ] client with GUI support based on compose multiplatform somehow
- [ ] notebook - style workbooks with graphs, formulaes, etc. - [ ] notebook - style workbooks with graphs, formulaes, etc.
- [ ] language server or compose-based lyng-aware editor - [ ] language server or compose-based lyng-aware editor
[parallelism]: docs/parallelism.md
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
set -e
root=./lyng/build/install/lyng-jvm/
./gradlew :lyng:installJvmDist
#strip $file
#upx $file
rm -rf ~/bin/jlyng-jvm || true
rm ~/bin/jlyng 2>/dev/null || true
mkdir -p ~/bin/jlyng-jvm
cp -R $root ~/bin/jlyng-jvm
ln -s ~/bin/jlyng-jvm/lyng-jvm/bin/lyng ~/bin/jlyng
+121
View File
@@ -0,0 +1,121 @@
# Binary `Buffer`
Buffers are effective unsigned byte arrays of fixed size. Buffers content is mutable,
unlike its size. Buffers are comparable and implement [Array], thus [Collection] and [Iterable]. Buffer iterators return
its contents as unsigned bytes converted to `Int`
Buffers needs to be imported with `import lyng.buffer`:
import lyng.buffer
assertEquals(5, Buffer("Hello").size)
>>> void
Buffer is _immutable_, there is a `MutableBuffer` with same interface but mutable.
## Constructing
There are a lo of ways to construct a buffer:
import lyng.buffer
// from string using utf8 encoding:
assertEquals( 5, Buffer("hello").size )
// from bytes, e.g. integers in range 0..255
assertEquals( 255, Buffer(1,2,3,255).last() )
// from whatever iterable that produces bytes, e.g.
// integers in 0..255 range:
assertEquals( 129, Buffer([1,2,129]).last() )
// Empty buffer of fixed size:
assertEquals(100, Buffer(100).size)
assertEquals(0, Buffer(100)[0])
// Note that you can use list iteral to create buffer with 1 byte:
assertEquals(1, Buffer([100]).size)
assertEquals(100, Buffer([100])[0])
>>> void
## Accessing and modifying
Buffer implement [Array] and therefore can be accessed, and `MutableBuffers` also modified:
import lyng.buffer
val b1 = Buffer( 1, 2, 3)
assertEquals( 2, b1[1] )
val b2 = b1.toMutable()
assertEquals( 2, b1[1] )
b2[1]++
b2[0] = 100
assertEquals( Buffer(100, 3, 3), b2)
// b2 is a mutable copy so b1 has not been changed:
assertEquals( Buffer(1, 2, 3), b1)
>>> void
Buffer provides concatenation with another Buffer:
import lyng.buffer
val b = Buffer(101, 102)
assertEquals( Buffer(101, 102, 1, 2), b + [1,2])
>>> void
Please note that indexed bytes are _readonly projection_, e.g. you can't modify these with
## Comparing
Buffers are comparable with other buffers (and notice there are _mutable_ buffers, bu default buffers ar _immutable_):
import lyng.buffer
val b1 = Buffer(1, 2, 3)
val b2 = Buffer(1, 2, 3)
val b3 = MutableBuffer(b2)
b3[0] = 101
assert( b3 > b1 )
assert( b2 == b1 )
// longer string with same prefix is considered bigger:
assert( b2 + "!".characters() > b1 )
// note that characters() provide Iterable of characters that
// can be concatenated to Buffer
>>> void
## Slicing
As with [List], it is possible to use ranges as indexes to slice a Buffer:
import lyng.buffer
val a = Buffer( 100, 101, 102, 103, 104, 105 )
assertEquals( a[ 0..1 ], Buffer(100, 101) )
assertEquals( a[ 0 ..< 2 ], Buffer(100, 101) )
assertEquals( a[ ..< 2 ], Buffer(100, 101) )
assertEquals( a[ 4.. ], Buffer(104, 105) )
assertEquals( a[ 2..3 ], Buffer(102, 103) )
>>> void
## Members
| name | meaning | type |
|---------------|------------------------------------|---------------|
| `size` | size | Int |
| `decodeUtf8` | decodee to String using UTF8 rules | Any |
| `+` | buffer concatenation | Any |
| `toMutable()` | create a mutable copy | MutableBuffer |
(1)
: optimized implementation that override `Iterable` one
Also, it inherits methods from [Iterable] and [Array].
[Range]: Range.md
[Iterable]: Iterable.md
+7 -3
View File
@@ -3,20 +3,24 @@
Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with
constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List]. constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
Important thing is that maps can't contain `null`: it is used to return from missing elements.
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`) Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`)
val map = Map( ["foo", 1], ["bar", "buzz"] ) val map = Map( "foo" => 1, "bar" => "buzz" )
assert(map is Map) assert(map is Map)
assert(map.size == 2) assert(map.size == 2)
assert(map is Iterable) assert(map is Iterable)
>>> void >>> void
Notice usage of the `=>` operator that creates `MapEntry`, which implements also [Collection] of
two items, first, at index zero, is a key, second, at index 1, is the value. You can use lists too.
Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator: Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator:
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
assert( map["bar"] == "buzz") assert( map["bar"] == "buzz")
assert( map[42] == "answer" ) assert( map[42] == "answer" )
assertThrows { map["nonexistent"] } assertEquals( null, map["nonexisting"])
assert( map.getOrNull(101) == null ) assert( map.getOrNull(101) == null )
assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" ) assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" )
// now 91 entry is set: // now 91 entry is set:
@@ -29,7 +33,7 @@ To remove item from the collection. use `remove`. It returns last removed item o
hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()` hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()`
removes all. removes all.
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] ) val map = Map( "foo" => 1, "bar" => "buzz", [42, "answer"] )
assertEquals( 1, map.remove("foo") ) assertEquals( 1, map.remove("foo") )
assert( map.getOrNull("foo") == null) assert( map.getOrNull("foo") == null)
assert( map.size == 2 ) assert( map.size == 2 )
+44 -4
View File
@@ -147,7 +147,7 @@ In many cases it is necessary to implement custom comparison and `toString`, sti
each class is provided with default implementations: each class is provided with default implementations:
- default toString outputs class name and its _public_ fields. - default toString outputs class name and its _public_ fields.
- default comparison compares compares all public fields in order of appearance. - default comparison compares all fields in order of appearance.
For example, for our class Point: For example, for our class Point:
@@ -160,6 +160,47 @@ For example, for our class Point:
Point(1,1+1) Point(1,1+1)
>>> Point(x=1,y=2) >>> Point(x=1,y=2)
## Statics: class fields and class methods
You can mark a field or a method as static. This is borrowed from Java as more plain version of a kotlin's companion object or Scala's object. Static field and functions is one for a class, not for an instance. From inside the class, e.g. from the class method, it is a regular var. From outside, it is accessible as `ClassName.field` or method:
class Value(x) {
static var foo = Value("foo")
static fun exclamation() {
// here foo is a regular var:
foo.x + "!"
}
}
assertEquals( Value.foo.x, "foo" )
assertEquals( "foo!", Value.exclamation() )
// we can access foo from outside like this:
Value.foo = Value("bar")
assertEquals( "bar!", Value.exclamation() )
>>> void
As usual, private statics are not accessible from the outside:
class Test {
// private, inacessible from outside protected data:
private static var data = null
// the interface to access and change it:
static fun getData() { data }
static fun setData(value) { data = value }
}
// no direct access:
assertThrows { Test.data }
// accessible with the interface:
assertEquals( null, Test.getData() )
Test.setData("fubar")
assertEquals("fubar", Test.getData() )
>>> void
# Extending classes # Extending classes
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
@@ -250,11 +291,10 @@ Note `Real` class: it is global variable for Real class; there are such class in
assert('$'::class == Char) assert('$'::class == Char)
>>> void >>> void
More complex is singleton classes, because you don't need to compare their class Singleton classes also have class:
instances and generally don't need them at all, these are normally just Obj:
null::class null::class
>>> Obj >>> Null
At this time, `Obj` can't be accessed as a class. At this time, `Obj` can't be accessed as a class.
+4
View File
@@ -1,5 +1,7 @@
# Advanced topics # Advanced topics
__See also:__ [parallelism].
## Closures/scopes isolation ## Closures/scopes isolation
Each block has own scope, in which it can safely use closures and override Each block has own scope, in which it can safely use closures and override
@@ -111,3 +113,5 @@ arguments list in almost arbitrary ways. For example:
>>> void >>> void
, ,
[parallelism]: parallelism.md
+130
View File
@@ -0,0 +1,130 @@
# Parallelism in Lyng
Lyng is built to me multithreaded where possible (e.g. all targets byt JS and wasmJS as for now)
and cooperatively parallel (coroutine based) everywhere.
In Lyng, every function, every lambda are _coroutines_. It means, you can have as many of these as you want without risking running out of memory on threads stack, or get too many threads.
Depending on the platform, these coroutines may be executed on different CPU and cores, too, truly in parallel. Where not, like Javascript browser, they are still executed cooperatively. You should not care about the platform capabilities, just call `launch`:
// track coroutine call:
var xIsCalled = false
// launch coroutine in parallel
val x = launch {
// wait 10ms to let main code to be executed
delay(10)
// now set the flag
xIsCalled = true
// and return something useful:
"ok"
}
// corouine is launhed, but not yet executed
// due to delay call:
assert(!xIsCalled)
// now we wait for it to be executed:
assertEquals( x.await(), "ok")
// now glag should be set:
assert(xIsCalled)
>>> void
This example shows how to launch a coroutine with `launch` which returns [Deferred] instance, the latter have ways to await for the coroutine completion and retrieve possible result.
Launch has the only argument which should be a callable (lambda usually) that is run in parallel (or cooperatively in parallel), and return anything as the result.
## Synchronization: Mutex
Suppose we have a resource, that could be used concurrently, a coutner in our case. If we won'r protect it, concurrent usage cause RC, Race Condition, providing wrong result:
var counter = 0
(1..4).map {
launch {
// slow increment:
val c = counter
delay(10)
counter = c + 1
}
}.forEach { it.await() }
assert(counter < 4)
>>> void
The obviously wrong result is not 4, as all coroutines capture the counter value, which is 1, then sleep for 5ms, then save 1 + 1 as result. May some coroutines will pass, so it will be 1 or 2, most likely.
Using [Mutex] makes it all working:
var counter = 0
val mutex = Mutex()
(1..4).map {
launch {
// slow increment:
mutex.withLock {
val c = counter
delay(10)
counter = c + 1
}
}
}.forEach { it.await() }
assertEquals(4, counter)
>>> void
now everything works as expected: `mutex.withLock` makes them all be executed in sequence, not in parallel.
## Completable deferred
Sometimes it is convenient to manually set completion status of some deferred result. This is when [CompletableDeferred] is used:
// this variable will be completed later:
val done = CompletableDeferred()
// complete it ater delay
launch {
delay(10)
// complete it setting the result:
done.complete("ok")
}
// now it is still not completed: coroutine is delayed
// (ot not started on sinthe-threaded platforms):
assert(!done.isCompleted)
assert(done.isActive)
// then we can just await it as any other deferred:
assertEquals( done.await(), "ok")
// and as any other deferred it is now complete:
assert(done.isCompleted)
## True parallelism
Cooperative, coroutine-based parallelism is automatically available on all platforms. Depending on the platform, though, the coroutines could be dispatched also in different threads; where there are multiple cores and/or CPU available, it means the coroutines could be exuted truly in parallel, unless [Mutex] is used:
| platofrm | multithreaded |
|------------|---------------|
| JVM | yes |
| Android | yes |
| Javascript | NO |
| wasmJS | NO |
| IOS | yes |
| MacOSX | yes |
| Linux | yes |
| Windows | yes |
So it is important to always use [Mutex] where concurrent execution could be a problem (so called Race Conditions, or RC).
## Yield
When the coroutine is executed, on the single-threaded environment all other coroutines are suspended until active one will wait for something. Sometimes, it is undesirable; the coroutine may perform long calculations or some other CPU consuming task. The solution is to call `yield()` periodically. Unlike `delay()`, yield does not pauses the coroutine for some specified time, but it just makes all other coroutines to be executed. In other word, yield interrupts current coroutines and out it to the end of the dispatcher list of active coroutines. It is especially important on Javascript and wasmJS targets as otherwise UI thread could be blocked.
Usage example:
fun someLongTask() { // ...
do {
// execute step
if( done ) break
yield()
} while(true)
}
Regular → Executable
+1
View File
@@ -1,2 +1,3 @@
#!/bin/env jlyng
println("Hello, world!"); println("Hello, world!");
+215
View File
@@ -0,0 +1,215 @@
# Lyng time functions
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
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
hours, days)
## 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.
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`.
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:
import lyng.time
// default constructor returns time now:
val t1 = Instant()
val t2 = Instant()
assert( t2 - t1 < 1.millisecond )
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
>>> void
## Constructing
import lyng.time
// empty constructor gives current time instant using system clock:
val now = Instant()
// constructor with Instant instance makes a copy:
assertEquals( now, Instant(now) )
// constructing from a number is trated as seconds since unix epoch:
val copyOfNow = Instant( now.epochSeconds )
// note that instant resolution is higher that Real can hold
// so reconstructed from real slightly differs:
assert( abs( (copyOfNow - now).milliseconds ) < 0.01 )
>>> void
The resolution of system clock could be more precise and double precision real number of `Real`, keep it in mind.
## Comparing and calculating periods
import lyng.time
val now = Instant()
// you cam add or subtract periods, and compare
assert( now - 5.minutes < now )
val oneHourAgo = now - 1.hour
assertEquals( now, oneHourAgo + 1.hour)
>>> void
## Getting the max precision
Normally, subtracting instants gives precision to microseconds, which is well inside the jitter
the language VM adds. Still `Instant()` or `Instant.now()` capture most precise system timer at hand and provide inner
value of 12 bytes, up to nanoseconds (hopefully). To access it use:
import lyng.time
// capture time
val now = Instant.now()
// this is Int value, number of whole epoch
// milliseconds to the moment, it fits 8 bytes Int well
val seconds = now.epochWholeSeconds
assert(seconds is Int)
// and this is Int value of nanoseconds _since_ the epochMillis,
// it effectively add 4 more mytes int:
val nanos = now.nanosecondsOfSecond
assert(nanos is Int)
assert( nanos in 0..999_999_999 )
// we can construct epochSeconds from these parts:
assertEquals( now.epochSeconds, nanos * 1e-9 + seconds )
>>> 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):
// note that encoding return _bit array_ and this is a _bit size_:
val s0 = Lynon.encode(Instant.now()).size
// shorter: milliseconds only
val s1 = Lynon.encode(Instant.now().truncateToMillisecond()).size
// truncated to seconds, good for file mtime, etc:
val s2 = Lynon.encode(Instant.now().truncateToSecond()).size
assert( s1 < s0 )
assert( s2 < s1 )
>>> void
## Formatting instants
You can freely use `Instant` in string formatting. It supports usual sprintf-style formats:
import lyng.time
val now = Instant()
// will be something like "now: 12:10:05"
val currentTimeOnly24 = "now: %tT"(now)
// we can extract epoch second with formatting too,
// this was since early C time
// get epoch while seconds from formatting
val unixEpoch = "Now is %ts since unix epoch"(now)
// and it is the same as now.epochSeconds, int part:
assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) )
>>> 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...)`!
## Instant members
| member | description |
|--------------------------------|---------------------------------------------------------|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) |
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
| 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)
: 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
| member | description |
|--------------------------------|----------------------------------------------|
| Instant.now() | create new instance with current system time |
| Instant.distantPast: Instant | most distant instant in past |
| Instant.distantFuture: Instant | most distant instant in future |
# `Duraion` class
Represent absolute time distance between two `Instant`.
import lyng.time
val t1 = Instant()
// yes we can delay to period, and it is not blocking. is suspends!
delay(1.millisecond)
val t2 = Instant()
// be suspend, so actual time may vary:
assert( t2 - t1 >= 1.millisecond)
assert( t2 - t1 < 100.millisecond)
>>> void
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
- `n.millisecond`, `n.milliseconds`
- `n.second`, `n.seconds`
- `n.minute`, `n.minutes`
- `n.hour`, `n.hours`
- `n.day`, `n.days`
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:
- `d.microseconds`
- `d.milliseconds`
- `d.seconds`
- `d.minutes`
- `d.hours`
- `d.days`
for example
import lyng.time
assertEquals( 60, 1.minute.seconds )
assertEquals( 10.milliseconds, 0.01.seconds )
>>> void
# Utility functions
## delay(duration: Duration)
Suspends current coroutine for at least the specified duration.
+54 -30
View File
@@ -14,7 +14,8 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md) - [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md) - [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md) - [math in Lyng](math.md)
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator] - [parallelism] - multithreaded code, coroutines, etc.
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md)
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples) - Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions # Expressions
@@ -530,7 +531,8 @@ The simplest way to concatenate lists is using `+` and `+=`:
list += [2, 1] list += [2, 1]
// or you can append a single element: // or you can append a single element:
list += "end" list += "end"
assert( list == [1, 2, 2, 1, "end"]) assertEquals( list, [1, 2, 2, 1, "end"])
void
>>> void >>> void
***Important note***: the pitfall of using `+=` is that you can't append in [Iterable] instance as an object: it will always add all its contents. Use `list.add` to add a single iterable instance: ***Important note***: the pitfall of using `+=` is that you can't append in [Iterable] instance as an object: it will always add all its contents. Use `list.add` to add a single iterable instance:
@@ -641,6 +643,11 @@ You can get ranges to extract a portion from a list:
assertEquals( [2,3], list[1..<3]) assertEquals( [2,3], list[1..<3])
>>> void >>> void
# Buffers
[Buffer] is a special implementation of an [Array] of unsigned bytes, in the
[separate file](Buffer.md).
# Sets # Sets
Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] but have no indexing access. Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] but have no indexing access.
@@ -662,7 +669,6 @@ are [Iterable]:
Please see [Map] reference for detailed description on using Maps. Please see [Map] reference for detailed description on using Maps.
# Flow control operators # Flow control operators
## if-then-else ## if-then-else
@@ -740,13 +746,13 @@ You can thest that _when expression_ is _contained_, or not contained, in some o
Typical builtin types that are containers (e.g. support `conain`): Typical builtin types that are containers (e.g. support `conain`):
| class | notes | | class | notes |
|------------|--------------------------------------------| |------------|------------------------------------------------|
| Collection | contains an element (1) | | Collection | contains an element (1) |
| Array | faster maybe that Collection's | | Array | faster maybe that Collection's |
| List | faster than Array's | | List | faster than Array's |
| String | character in string or substring in string | | String | character in string or substring in string (3) |
| Range | object is included in the range (2) | | Range | object is included in the range (2) |
(1) (1)
: Iterable is not the container as it can be infinite : Iterable is not the container as it can be infinite
@@ -760,6 +766,9 @@ Typical builtin types that are containers (e.g. support `conain`):
assert( "x" !in 'a'..'z') // string in character range: could be error assert( "x" !in 'a'..'z') // string in character range: could be error
>>> void >>> void
(3)
: `String` also can provide array of characters directly with `str.characters()`, which is [Iterable] and [Array]. String itself is not iterable as otherwise it will interfere when adding strigns to lists (it will add _characters_ it it would be iterable).
So we recommend not to mix characters and string ranges; use `ch in str` that works So we recommend not to mix characters and string ranges; use `ch in str` that works
as expected: as expected:
@@ -1092,6 +1101,17 @@ and you can use ranges in for-loops:
See [Ranges](Range.md) for detailed documentation on it. See [Ranges](Range.md) for detailed documentation on it.
# Time routines
These should be imported from [lyng.time](time.md). For example:
import lyng.time
val now = Instant()
val hourAgo = now - 1.hour
See [more docs on time manipulation](time.md)
# Comments # Comments
// single line comment // single line comment
@@ -1198,27 +1218,29 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
Typical set of String functions includes: Typical set of String functions includes:
| fun/prop | description / notes | | fun/prop | description / notes |
|--------------------|------------------------------------------------------------| |-------------------|------------------------------------------------------------|
| lower() | change case to unicode upper | | lower() | change case to unicode upper |
| upper() | change case to unicode lower | | upper() | change case to unicode lower |
| startsWith(prefix) | true if starts with a prefix | | startsWith(prefix) | true if starts with a prefix |
| endsWith(prefix) | true if ends with a prefix | | endsWith(prefix) | true if ends with a prefix |
| take(n) | get a new string from up to n first characters | | take(n) | get a new string from up to n first characters |
| takeLast(n) | get a new string from up to n last 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 | | 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 | | dropLast(n) | get a new string dropping n last chars, or empty string |
| size | size in characters like `length` because String is [Array] | | size | size in characters like `length` because String is [Array] |
| (args...) | sprintf-like formatting, see [string formatting] | | (args...) | sprintf-like formatting, see [string formatting] |
| [index] | character at index | | [index] | character at index |
| [Range] | substring at range | | [Range] | substring at range |
| s1 + s2 | concatenation | | s1 + s2 | concatenation |
| s1 += s2 | self-modifying concatenation | | s1 += s2 | self-modifying concatenation |
| toReal() | attempts to parse string as a Real value | | toReal() | attempts to parse string as a Real value |
| toInt() | parse string to Int 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.
@@ -1256,4 +1278,6 @@ See [math functions](math.md). Other general purpose functions are:
[String]: String.md [String]: String.md
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary [string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
[Set]: Set.md [Set]: Set.md
[Map]: Map.md [Map]: Map.md
[Buffer]: Buffer.md
[parallelism]: parallelism.md
+3 -1
View File
@@ -4,10 +4,11 @@ clikt = "5.0.3"
kotlin = "2.1.21" kotlin = "2.1.21"
android-minSdk = "24" android-minSdk = "24"
android-compileSdk = "34" android-compileSdk = "34"
kotlinx-coroutines = "1.10.1" kotlinx-coroutines = "1.9.0"
mp_bintools = "0.1.12" mp_bintools = "0.1.12"
firebaseCrashlyticsBuildtools = "3.0.3" firebaseCrashlyticsBuildtools = "3.0.3"
okioVersion = "3.10.2" okioVersion = "3.10.2"
compiler = "3.2.0-alpha11"
[libraries] [libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
@@ -19,6 +20,7 @@ mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools"
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" } firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" } okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" }
[plugins] [plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }
+34 -4
View File
@@ -1,12 +1,19 @@
package net.sergeych package net.sergeych
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.arguments.optional import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import net.sergeych.lyng.* import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.*
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import okio.SYSTEM import okio.SYSTEM
@@ -41,7 +48,23 @@ val baseScope = Scope().apply {
// } // }
} }
class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() { fun runMain(args: Array<String>) {
if(args.isNotEmpty()) {
if( args.size >= 2 && args[0] == "--" ) {
// -- -file.lyng <args>
executeFileWithArgs(args[1], args.drop(2))
return
} else if( args[0][0] != '-') {
// file.lyng <args>
executeFileWithArgs(args[0], args.drop(1))
return
}
}
// normal processing
Lyng { runBlocking { it() } }.main(args)
}
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
override val printHelpOnEmptyArgs = true override val printHelpOnEmptyArgs = true
@@ -55,7 +78,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
val args by argument(help = "arguments for script").multiple() val args by argument(help = "arguments for script").multiple()
override fun help(context: com.github.ajalt.clikt.core.Context): String = override fun help(context: Context): String =
""" """
The Lyng script language interpreter, language version is $LyngVersion. The Lyng script language interpreter, language version is $LyngVersion.
@@ -106,6 +129,13 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
} }
} }
fun executeFileWithArgs(fileName: String, args: List<String>) {
runBlocking {
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
executeFile(fileName)
}
}
suspend fun executeFile(fileName: String) { suspend fun executeFile(fileName: String) {
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource -> var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
fileSource.buffer().use { bs -> fileSource.buffer().use { bs ->
@@ -118,7 +148,7 @@ suspend fun executeFile(fileName: String) {
text = text.substring(pos + 1) text = text.substring(pos + 1)
} }
processErrors { processErrors {
Compiler.compile(Source(fileName, text)).execute(baseScope) baseScope.eval(Source(fileName, text))
} }
} }
@@ -1,9 +1,7 @@
package net.sergeych.lyng_cli package net.sergeych.lyng_cli
import com.github.ajalt.clikt.core.main import net.sergeych.runMain
import kotlinx.coroutines.runBlocking
import net.sergeych.Lyng
fun main(args: Array<String>) { fun main(args: Array<String>) {
Lyng({ runBlocking { it() } }).main(args) runMain(args)
} }
+2 -4
View File
@@ -1,7 +1,5 @@
import com.github.ajalt.clikt.core.main import net.sergeych.runMain
import kotlinx.coroutines.runBlocking
import net.sergeych.Lyng
fun main(args: Array<String>) { fun main(args: Array<String>) {
Lyng( { runBlocking { it() } }).main(args) runMain(args)
} }
+3 -2
View File
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "0.7.0-SNAPSHOT" version = "0.8.2-SNAPSHOT"
buildscript { buildscript {
repositories { repositories {
@@ -20,7 +20,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidLibrary)
// alias(libs.plugins.vanniktech.mavenPublish) // alias(libs.plugins.vanniktech.mavenPublish)
kotlin("plugin.serialization") version "2.1.20" kotlin("plugin.serialization") version "2.2.0"
id("com.codingfeline.buildkonfig") version "0.17.1" id("com.codingfeline.buildkonfig") version "0.17.1"
`maven-publish` `maven-publish`
} }
@@ -99,6 +99,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.firebase.crashlytics.buildtools) implementation(libs.firebase.crashlytics.buildtools)
implementation(libs.compiler)
} }
publishing { publishing {
@@ -1,5 +1,7 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
/** /**
* Special version of the [Scope] used to `apply` new this object to * Special version of the [Scope] used to `apply` new this object to
* _parent context property. * _parent context property.
@@ -1,5 +1,9 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjRecord
/** /**
* List of argument declarations in the __definition__ of the lambda, class constructor, * List of argument declarations in the __definition__ of the lambda, class constructor,
* function, etc. It is created by [Compiler.parseArgsDeclaration] * function, etc. It is created by [Compiler.parseArgsDeclaration]
@@ -19,17 +23,20 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
} }
/** /**
* parse args and create local vars in a given context * parse args and create local vars in a given context properly interpreting
* ellipsis args and default values
*/ */
suspend fun assignToContext( suspend fun assignToContext(
scope: Scope, scope: Scope,
arguments: Arguments = scope.args, arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var, defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public defaultVisibility: Visibility = Visibility.Public,
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) { ) {
fun assign(a: Item, value: Obj) { fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value, scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility) a.visibility ?: defaultVisibility,
recordType = defaultRecordType)
} }
// will be used with last lambda arg fix // will be used with last lambda arg fix
@@ -54,8 +61,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
i < callArgs.size -> callArgs[i] i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(scope) a.defaultValue != null -> a.defaultValue.execute(scope)
else -> { else -> {
println("callArgs: ${callArgs.joinToString()}") // println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}") // println("tailBlockMode: ${arguments.tailBlockMode}")
scope.raiseIllegalArgument("too few arguments for the call") scope.raiseIllegalArgument("too few arguments for the call")
} }
} }
@@ -1,5 +1,9 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjList
data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false) data class ParsedArgument(val value: Statement, val pos: Pos, val isSplat: Boolean = false)
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments { suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
@@ -26,7 +30,7 @@ suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode:
return Arguments(list,tailBlockMode) return Arguments(list,tailBlockMode)
} }
data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list { data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList()) constructor(vararg values: Obj) : this(values.toList())
@@ -42,6 +46,8 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
return list.map { it.toKotlin(scope) } return list.map { it.toKotlin(scope) }
} }
fun inspect(): String = list.joinToString(", ") { it.inspect() }
companion object { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList()) fun from(values: Collection<Obj>) = Arguments(values.toList())
@@ -1,18 +1,32 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportProvider
/** /**
* The LYNG compiler. * The LYNG compiler.
*/ */
class Compiler( class Compiler(
val cc: CompilerContext, val cc: CompilerContext,
val pacman: Pacman = Pacman.emptyAllowAll, val importManager: ImportProvider,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
settings: Settings = Settings() settings: Settings = Settings()
) { ) {
var packageName: String? = null var packageName: String? = null
class Settings class Settings
private val initStack = mutableListOf<MutableList<Statement>>()
val currentInitScope: MutableList<Statement>
get() =
initStack.lastOrNull() ?: cc.syntaxError("no initialization scope exists here")
private fun pushInitScope(): MutableList<Statement> = mutableListOf<Statement>().also { initStack.add(it) }
private fun popInitScope(): MutableList<Statement> = initStack.removeLast()
private suspend fun parseScript(): Script { private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>() val statements = mutableListOf<Statement>()
val start = cc.currentPos() val start = cc.currentPos()
@@ -20,6 +34,10 @@ class Compiler(
// package level declarations // package level declarations
do { do {
val t = cc.current() val t = cc.current()
if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINLGE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
cc.next()
continue
}
if (t.type == Token.Type.ID) { if (t.type == Token.Type.ID) {
when (t.value) { when (t.value) {
"package" -> { "package" -> {
@@ -33,15 +51,17 @@ class Compiler(
packageName = name packageName = name
continue continue
} }
"import" -> { "import" -> {
cc.next() cc.next()
val pos = cc.currentPos() val pos = cc.currentPos()
val name = loadQualifiedName() val name = loadQualifiedName()
pacman.prepareImport(pos, name, null) val module = importManager.prepareImport(pos, name, null)
statements += statement { statements += statement {
pacman.performImport(this,name,null) module.importInto(this, null)
ObjVoid ObjVoid
} }
continue
} }
} }
} }
@@ -64,6 +84,7 @@ class Compiler(
t = cc.next() t = cc.next()
} }
} }
cc.previous()
return result.toString() return result.toString()
} }
@@ -266,9 +287,7 @@ class Compiler(
if (x == ObjNull && isOptional) ObjNull.asReadonly if (x == ObjNull && isOptional) ObjNull.asReadonly
else x.getAt(cxt, i).asMutable else x.getAt(cxt, i).asMutable
}) { cxt, newValue -> }) { cxt, newValue ->
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt() left.getter(cxt).value.putAt(cxt, index.execute(cxt), newValue)
?: cxt.raiseError("index must be integer")
left.getter(cxt).value.putAt(cxt, i, newValue)
} }
} ?: run { } ?: run {
// array literal // array literal
@@ -338,17 +357,26 @@ class Compiler(
left.setter(startPos) left.setter(startPos)
operand = Accessor { cxt -> operand = Accessor { cxt ->
val x = left.getter(cxt) val x = left.getter(cxt)
if (x.isMutable) if (x.isMutable) {
x.value.getAndIncrement(cxt).asReadonly if (x.value.isConst) {
else cxt.raiseError("Cannot increment immutable value") x.value.plus(cxt, ObjInt.One).also {
left.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.getAndIncrement(cxt).asReadonly
} else cxt.raiseError("Cannot increment immutable value")
} }
} ?: run { } ?: run {
// no lvalue means pre-increment, expression to increment follows // no lvalue means pre-increment, expression to increment follows
val next = parseAccessor() ?: throw ScriptError(t.pos, "Expecting expression") val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { ctx -> operand = Accessor { ctx ->
next.getter(ctx).also { val x = next.getter(ctx).also {
if (!it.isMutable) ctx.raiseError("Cannot increment immutable value") if (!it.isMutable) ctx.raiseError("Cannot increment immutable value")
}.value.incrementAndGet(ctx).asReadonly }.value
if (x.isConst) {
next.setter(startPos)(ctx, x.plus(ctx, ObjInt.One))
x.asReadonly
} else x.incrementAndGet(ctx).asReadonly
} }
} }
} }
@@ -358,18 +386,28 @@ class Compiler(
operand?.let { left -> operand?.let { left ->
// post decrement // post decrement
left.setter(startPos) left.setter(startPos)
operand = Accessor { ctx -> operand = Accessor { cxt ->
left.getter(ctx).also { val x = left.getter(cxt)
if (!it.isMutable) ctx.raiseError("Cannot decrement immutable value") if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
}.value.getAndDecrement(ctx).asReadonly if (x.value.isConst) {
x.value.minus(cxt, ObjInt.One).also {
left.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.getAndDecrement(cxt).asReadonly
} }
} ?: run { } ?: run {
// no lvalue means pre-decrement, expression to decrement follows // no lvalue means pre-decrement, expression to decrement follows
val next = parseAccessor() ?: throw ScriptError(t.pos, "Expecting expression") val next = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = Accessor { ctx -> operand = Accessor { cxt ->
next.getter(ctx).also { val x = next.getter(cxt)
if (!it.isMutable) ctx.raiseError("Cannot decrement immutable value") if (!x.isMutable) cxt.raiseError("Cannot decrement immutable value")
}.value.decrementAndGet(ctx).asReadonly if (x.value.isConst) {
x.value.minus(cxt, ObjInt.One).also {
next.setter(startPos)(cxt, it)
}.asReadonly
} else
x.value.decrementAndGet(cxt).asReadonly
} }
} }
} }
@@ -687,7 +725,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) {
@@ -709,8 +747,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 -> {
@@ -741,11 +785,12 @@ 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 -> {
val n = t.value.toLong(if (t.type == Token.Type.HEX) 16 else 10) val n = t.value.replace("_", "").toLong(if (t.type == Token.Type.HEX) 16 else 10)
if (isPlus) ObjInt(n) else ObjInt(-n) if (isPlus) ObjInt(n) else ObjInt(-n)
} }
@@ -755,11 +800,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
@@ -783,6 +833,8 @@ class Compiler(
val isExtern = cc.skipId("extern") val isExtern = cc.skipId("extern")
when { when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern) cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(Visibility.Private, isExtern, isStatic = true)
cc.matchQualifiers("fun", "static") -> parseFunctionDeclaration(Visibility.Public, isExtern, isStatic = true)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern) cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
@@ -790,7 +842,21 @@ class Compiler(
cc.matchQualifiers("fun") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern) cc.matchQualifiers("fun") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("fn") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern) cc.matchQualifiers("fn") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("val", "private", "static") -> parseVarDeclaration(
false,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("val", "static") -> parseVarDeclaration(false, Visibility.Public, isStatic = true)
cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private) cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private)
cc.matchQualifiers("var", "static") -> parseVarDeclaration(true, Visibility.Public, isStatic = true)
cc.matchQualifiers("var", "static", "private") -> parseVarDeclaration(
true,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private) cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private)
cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, true) cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, true)
cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, true) cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, true)
@@ -1050,6 +1116,8 @@ class Compiler(
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next() val t = cc.next()
pushInitScope()
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) { val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body // parse body
parseScript().also { parseScript().also {
@@ -1060,6 +1128,8 @@ class Compiler(
null null
} }
val initScope = popInitScope()
// create class // create class
val className = nameToken.value val className = nameToken.value
@@ -1082,14 +1152,22 @@ class Compiler(
thisObj thisObj
} }
// inheritance must alter this code: // inheritance must alter this code:
val newClass = ObjClass(className).apply { val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
} }
return statement { return statement {
// the main statement should create custom ObjClass instance with field // the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc. // accessors, constructor registration, etc.
addItem(className, false, newClass) addItem(className, false, newClass)
if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
s.execute(classScope)
.also { println("executed, ${classScope.objects}") }
}
newClass newClass
} }
} }
@@ -1465,7 +1543,8 @@ class Compiler(
private suspend fun parseFunctionDeclaration( private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false isExtern: Boolean = false,
isStatic: Boolean = false,
): Statement { ): Statement {
var t = cc.next() var t = cc.next()
val start = t.pos val start = t.pos
@@ -1518,7 +1597,7 @@ class Compiler(
} }
fnStatements.execute(context) fnStatements.execute(context)
} }
return statement(start) { context -> val fnCreatestatement = statement(start) { context ->
// we added fn in the context. now we must save closure // we added fn in the context. now we must save closure
// for the function // for the function
closure = context closure = context
@@ -1536,6 +1615,11 @@ class Compiler(
// saved the proper context in the closure // saved the proper context in the closure
fnBody fnBody
} }
return if (isStatic) {
currentInitScope += fnCreatestatement
NopStatement
} else
fnCreatestatement
} }
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement { private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {
@@ -1559,7 +1643,8 @@ class Compiler(
private suspend fun parseVarDeclaration( private suspend fun parseVarDeclaration(
isMutable: Boolean, isMutable: Boolean,
visibility: Visibility, visibility: Visibility,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isStatic: Boolean = false
): Statement { ): Statement {
val nameToken = cc.next() val nameToken = cc.next()
val start = nameToken.pos val start = nameToken.pos
@@ -1581,6 +1666,21 @@ class Compiler(
val initialExpression = if (setNull) null else parseStatement(true) val initialExpression = if (setNull) null else parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression") ?: throw ScriptError(eqToken.pos, "Expected initializer expression")
if (isStatic) {
// find objclass instance: this is tricky: this code executes in object initializer,
// when creating instance, but we need to execute it in the class initializer which
// is missing as for now. Add it to the compiler context?
// add there
// return
currentInitScope += statement {
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos)
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
ObjVoid
}
return NopStatement
}
return statement(nameToken.pos) { context -> return statement(nameToken.pos) { context ->
if (context.containsLocal(name)) if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined") throw ScriptError(nameToken.pos, "Variable $name is already defined")
@@ -1589,7 +1689,7 @@ class Compiler(
// create a separate copy: // create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility) context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
initValue initValue
} }
} }
@@ -1612,15 +1712,8 @@ class Compiler(
companion object { companion object {
suspend fun compile(source: Source,pacman: Pacman = Pacman.emptyAllowAll): Script { suspend fun compile(source: Source, importManager: ImportProvider): Script {
return Compiler(CompilerContext(parseLyng(source)),pacman).parseScript() return Compiler(CompilerContext(parseLyng(source)), importManager).parseScript()
}
suspend fun compilePackage(source: Source): Pair<String, Script> {
val c = Compiler(CompilerContext(parseLyng(source)))
val script = c.parseScript()
if (c.packageName == null) throw ScriptError(source.startPos, "package not set")
return c.packageName!! to script
} }
private var lastPriority = 0 private var lastPriority = 0
@@ -1743,7 +1836,7 @@ class Compiler(
allOps.filter { it.priority == l }.associateBy { it.tokenType } allOps.filter { it.priority == l }.associateBy { it.tokenType }
} }
suspend fun compile(code: String): Script = compile(Source("<eval>", code)) suspend fun compile(code: String): Script = compile(Source("<eval>", code), Script.defaultImportManager)
/** /**
* The keywords that stop processing of expression term * The keywords that stop processing of expression term
@@ -49,6 +49,10 @@ class CompilerContext(val tokens: List<Token>) {
throw ScriptError(at, message) throw ScriptError(at, message)
} }
fun syntaxError(message: String = "Syntax error"): Nothing {
throw ScriptError(currentPos(), message)
}
fun currentPos(): Pos = tokens[currentIndex].pos fun currentPos(): Pos = tokens[currentIndex].pos
/** /**
@@ -164,7 +168,9 @@ class CompilerContext(val tokens: List<Token>) {
* Note that [Token.Type.EOF] is not considered a whitespace token. * Note that [Token.Type.EOF] is not considered a whitespace token.
*/ */
fun skipWsTokens(): Token { fun skipWsTokens(): Token {
while( current().type in wstokens ) next() while( current().type in wstokens ) {
next()
}
return next() return next()
} }
@@ -1,130 +0,0 @@
package net.sergeych.lyng
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.mp_tools.globalDefer
/**
* Package manager. Chained manager, too simple. Override [createModuleScope] to return either
* valid [ModuleScope] or call [parent] - or return null.
*/
abstract class Pacman(
val parent: Pacman? = null,
val rootScope: Scope = parent!!.rootScope,
val securityManager: SecurityManager = parent!!.securityManager
) {
private val opScopes = Mutex()
private val cachedScopes = mutableMapOf<String, Scope>()
/**
* Create a new module scope if this pacman can import the module, return null otherwise so
* the manager can decide what to do
*/
abstract suspend fun createModuleScope(name: String): ModuleScope?
suspend fun prepareImport(pos: Pos, name: String, symbols: Map<String, String>?) {
if (!securityManager.canImportModule(name))
throw ImportException(pos, "Module $name is not allowed")
symbols?.keys?.forEach {
if (!securityManager.canImportSymbol(name, it)) throw ImportException(
pos,
"Symbol $name.$it is not allowed"
)
}
// if we can import the module, cache it, or go further
opScopes.withLock {
cachedScopes[name] ?: createModuleScope(name)?.let { cachedScopes[name] = it }
} ?: parent?.prepareImport(pos, name, symbols) ?: throw ImportException(pos, "Module $name is not found")
}
suspend fun performImport(scope: Scope, name: String, symbols: Map<String, String>?) {
val module = opScopes.withLock { cachedScopes[name] }
?: scope.raiseSymbolNotFound("module $name not found")
val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in module.objects) {
if (record.visibility.isPublic) {
println("import $name: $symbol: $record")
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(it) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
} ?: symbol
println("import $name.$symbol as $newName")
if (newName in scope.objects)
scope.raiseError("symbol $newName already exists, redefinition on import is not allowed")
scope.objects[newName] = record
}
}
if (!symbolsToImport.isNullOrEmpty())
scope.raiseSymbolNotFound("symbols $name.{$symbolsToImport} are.were not found")
}
companion object {
val emptyAllowAll = object : Pacman(rootScope = Script.defaultScope, securityManager = SecurityManager.allowAll) {
override suspend fun createModuleScope(name: String): ModuleScope? {
return null
}
}
}
}
/**
* Module scope supports importing and contains the [pacman]; it should be the same
* used in [Compiler];
*/
class ModuleScope(
val pacman: Pacman,
pos: Pos = Pos.builtIn,
val packageName: String
) : Scope(pacman.rootScope, Arguments.EMPTY, pos) {
constructor(pacman: Pacman,source: Source) : this(pacman, source.startPos, source.fileName)
override suspend fun checkImport(pos: Pos, name: String, symbols: Map<String, String>?) {
pacman.prepareImport(pos, name, symbols)
}
/**
* Import symbols into the scope. It _is called_ after the module is imported by [checkImport].
* If [checkImport] was not called, the symbols will not be imported with exception as module is not found.
*/
override suspend fun importInto(scope: Scope, name: String, symbols: Map<String, String>?) {
pacman.performImport(scope, name, symbols)
}
val packageNameObj by lazy { ObjString(packageName).asReadonly}
override fun get(name: String): ObjRecord? {
return if( name == "__PACKAGE__")
packageNameObj
else
super.get(name)
}
}
class InlineSourcesPacman(pacman: Pacman,val sources: List<Source>) : Pacman(pacman) {
val modules: Deferred<Map<String,Deferred<ModuleScope>>> = globalDefer {
val result = mutableMapOf<String, Deferred<ModuleScope>>()
for (source in sources) {
// retrieve the module name and script for deferred execution:
val (name, script) = Compiler.compilePackage(source)
// scope is created used pacman's root scope:
val scope = ModuleScope(this@InlineSourcesPacman, source.startPos, name)
// we execute scripts in parallel which allow cross-imports to some extent:
result[name] = globalDefer { script.execute(scope); scope }
}
result
}
override suspend fun createModuleScope(name: String): ModuleScope? =
modules.await()[name]?.await()
}
@@ -1,5 +1,7 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Accessor
sealed class ListEntry { sealed class ListEntry {
data class Element(val accessor: Accessor) : ListEntry() data class Element(val accessor: Accessor) : ListEntry()
@@ -1,5 +1,8 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjVoid
class LoopBreakContinueException( class LoopBreakContinueException(
val doContinue: Boolean, val doContinue: Boolean,
val result: Obj = ObjVoid, val result: Obj = ObjVoid,
@@ -0,0 +1,61 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.pacman.ImportProvider
/**
* Module scope supports importing and contains the [importProvider]; it should be the same
* used in [Compiler];
*/
class ModuleScope(
var importProvider: ImportProvider,
pos: Pos = Pos.builtIn,
override val packageName: String
) : Scope(importProvider.rootScope, Arguments.EMPTY, pos) {
constructor(importProvider: ImportProvider, source: Source) : this(importProvider, source.startPos, source.fileName)
/**
* Import symbols into the scope. It _is called_ after the module is imported by [ImportProvider.prepareImport]
* which checks symbol availability and accessibility prior to execution.
* @param scope where to copy symbols from this module
* @param symbols symbols to import, ir present, only symbols keys will be imported renamed to corresponding values
*/
override suspend fun importInto(scope: Scope, symbols: Map<String, String>?) {
val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in this.objects) {
if (record.visibility.isPublic) {
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(it) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
} ?: symbol
val existing = scope.objects[newName]
if (existing != null ) {
if (existing.importedFrom != record.importedFrom)
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
// already imported
}
else {
// when importing records, we keep track of its package (not otherwise needed)
if (record.importedFrom == null) record.importedFrom = this
scope.objects[newName] = record
}
}
}
if (!symbolsToImport.isNullOrEmpty())
scope.raiseSymbolNotFound("symbols $packageName.{$symbolsToImport} are.were not found")
}
val packageNameObj by lazy { ObjString(packageName).asReadonly }
override fun get(name: String): ObjRecord? {
return if (name == "__PACKAGE__")
packageNameObj
else
super.get(name)
}
}
@@ -1,31 +0,0 @@
package net.sergeych.lyng
data class ObjBool(val value: Boolean) : Obj() {
override val asStr by lazy { ObjString(value.toString()) }
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBool) return -2
return value.compareTo(other.value)
}
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override suspend fun logicalNot(scope: Scope): Obj = ObjBool(!value)
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = ObjBool(value && other.toBool())
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(scope: Scope): Any {
return value
}
companion object {
val type = ObjClass("Bool")
}
}
val ObjTrue = ObjBool(true)
val ObjFalse = ObjBool(false)
@@ -1,73 +0,0 @@
package net.sergeych.lyng
val ObjClassType by lazy { ObjClass("Class") }
open class ObjClass(
val className: String,
vararg val parents: ObjClass,
) : Obj() {
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toMutableSet()
override val objClass: ObjClass by lazy { ObjClassType }
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(scope: Scope): Obj {
val instance = ObjInstance(this)
instance.instanceScope = scope.copy(newThisObj = instance,args = scope.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceScope)
}
return instance
}
fun defaultInstance(): Obj = object : Obj() {
override val objClass: ObjClass = this@ObjClass
}
fun createField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if( existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
/**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
return rootObjectType.members[name]
}
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
}
@@ -1,55 +0,0 @@
package net.sergeych.lyng
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceScope: Scope
override suspend fun readField(scope: Scope, name: String): ObjRecord {
return instanceScope[name]?.let {
if (it.visibility.isPublic)
it
else
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
}
?: super.readField(scope, name)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
instanceScope[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
f.value = newValue
} ?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(scope, this, args)
else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(scope, name, args)
private val publicFields: Map<String, ObjRecord>
get() = instanceScope.objects.filter { it.value.visibility.isPublic }
override fun toString(): String {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
return "${objClass.className}($fields)"
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in publicFields) {
val a = f.value.value
val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b)
if (d != 0) return d
}
return 0
}
}
@@ -1,3 +0,0 @@
package net.sergeych.lyng
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -109,7 +109,7 @@ private class Parser(fromPos: Pos) {
'/' -> when (currentChar) { '/' -> when (currentChar) {
'/' -> { '/' -> {
pos.advance() pos.advance()
Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT) Token(loadToEndOfLine().trim(), from, Token.Type.SINLGE_LINE_COMMENT)
} }
'*' -> { '*' -> {
@@ -250,7 +250,7 @@ private class Parser(fromPos: Pos) {
in digitsSet -> { in digitsSet -> {
pos.back() pos.back()
decodeNumber(loadChars(digits), from) decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
} }
'\'' -> { '\'' -> {
@@ -445,7 +445,7 @@ private class Parser(fromPos: Pos) {
} }
} }
private fun loadToEnd(): String { private fun loadToEndOfLine(): String {
val result = StringBuilder() val result = StringBuilder()
val l = pos.line val l = pos.line
do { do {
@@ -479,4 +479,10 @@ private class Parser(fromPos: Pos) {
return null return null
} }
init {
// skip shebang
if( pos.readFragment("#!") )
loadToEndOfLine()
}
} }
@@ -1,13 +1,22 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
/** /**
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes. * Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope. * Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
* *
* There are special types of scopes: * To create default scope, use default `Scope()` constructor, it will create a scope with a parent
* module scope with default [ImportManager], you can access with [currentImportProvider] as needed.
* *
* - [Script.defaultScope] - root scope for a script, safe one * If you want to create [ModuleScope] by hand, try [currentImportProvider] and [ImportManager.newModule],
* - [AppliedScope] - scope used to apply a closure to some thisObj scope * or [ImportManager.newModuleAt].
*
* There are special types of scopes:
*
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
*/ */
open class Scope( open class Scope(
val parent: Scope?, val parent: Scope?,
@@ -16,11 +25,13 @@ open class Scope(
var thisObj: Obj = ObjVoid, var thisObj: Obj = ObjVoid,
var skipScopeCreation: Boolean = false, var skipScopeCreation: Boolean = false,
) { ) {
open val packageName: String = "<anonymous package>"
constructor( constructor(
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
pos: Pos = Pos.builtIn, pos: Pos = Pos.builtIn,
) )
: this(Script.defaultScope, args, pos) : this(Script.defaultImportManager.copy().newModuleAt(pos), args, pos)
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented") fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@@ -35,6 +46,13 @@ open class Scope(
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing = fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message)) raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalStateException(this, message))
fun raiseIllegalAssignment(message: String): Nothing =
raiseError(ObjIllegalAssignmentException(this, message))
@Suppress("unused") @Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing = fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message)) raiseError(ObjIllegalArgumentException(this, message))
@@ -71,6 +89,11 @@ open class Scope(
} }
} }
fun requireNoArgs() {
if( args.list.isNotEmpty())
raiseError("This function does not accept any arguments")
}
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T) inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") ?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
@@ -96,9 +119,10 @@ open class Scope(
name: String, name: String,
isMutable: Boolean, isMutable: Boolean,
value: Obj, value: Obj,
visibility: Visibility = Visibility.Public visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord { ): ObjRecord {
return ObjRecord(value, isMutable, visibility).also { objects[name] = it } return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
} }
fun getOrCreateNamespace(name: String): ObjClass { fun getOrCreateNamespace(name: String): ObjClass {
@@ -132,21 +156,46 @@ open class Scope(
fun addConst(name: String, value: Obj) = addItem(name, false, value) fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj = suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource()).execute(this) Compiler.compile(code.toSource(), currentImportProvider).execute(this)
suspend fun eval(source: Source): Obj = suspend fun eval(source: Source): Obj =
Compiler.compile( Compiler.compile(
source, source,
(this as? ModuleScope)?.pacman?.also { println("pacman found: $pacman")} ?: Pacman.emptyAllowAll currentImportProvider
).execute(this) ).execute(this)
fun containsLocal(name: String): Boolean = name in objects fun containsLocal(name: String): Boolean = name in objects
open suspend fun checkImport(pos: Pos, name: String, symbols: Map<String, String>? = null) { /**
throw ImportException(pos, "Import is not allowed here: $name") * Some scopes can be imported into other scopes, like [ModuleScope]. Those must correctly implement this method.
* @param scope where to copy symbols from this module
* @param symbols symbols to import, ir present, only symbols keys will be imported renamed to corresponding values
*/
open suspend fun importInto(scope: Scope, symbols: Map<String, String>? = null) {
scope.raiseError(ObjIllegalOperationException(scope, "Import is not allowed here: import $packageName"))
} }
open suspend fun importInto(scope: Scope, name: String, symbols: Map<String, String>? = null) { /**
scope.raiseError(ObjIllegalOperationException(scope,"Import is not allowed here: import $name")) * Find a first [ImportManager] in this Scope hierarchy. Normally there should be one. Found instance is cached.
*
* Use it to register your package sources, see [ImportManager] features.
*
* @throws IllegalStateException if there is no such manager (if you create some specific scope with no manager,
* then you knew what you did)
*/
val currentImportProvider: ImportProvider by lazy {
if (this is ModuleScope)
importProvider.getActualProvider()
else
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
}
val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
companion object {
fun new(): Scope =
Script.defaultImportManager.copy().newModuleAt(Pos.builtIn)
} }
} }
@@ -1,6 +1,11 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.* import kotlin.math.*
class Script( class Script(
@@ -17,10 +22,11 @@ class Script(
return lastResult return lastResult
} }
suspend fun execute() = execute(defaultScope.copy(pos = pos)) suspend fun execute() = execute(defaultImportManager.newModule())
companion object { companion object {
val defaultScope: Scope = Scope().apply {
private val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this) ObjException.addExceptionsToContext(this)
addFn("println") { addFn("println") {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
@@ -112,7 +118,7 @@ class Script(
} }
addFn( "abs" ) { addFn( "abs" ) {
val x = args.firstAndOnly() val x = args.firstAndOnly()
if( x is ObjInt ) ObjInt( x.value.absoluteValue ) else ObjReal( x.toDouble().absoluteValue ) if( x is ObjInt) ObjInt( x.value.absoluteValue ) else ObjReal( x.toDouble().absoluteValue )
} }
addVoidFn("assert") { addVoidFn("assert") {
@@ -127,6 +133,12 @@ class Script(
if( a.compareTo(this, b) != 0 ) if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}")) raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
} }
addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) == 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} != ${b.inspect()}"))
}
addFn("assertThrows") { addFn("assertThrows") {
val code = requireOnlyArg<Statement>() val code = requireOnlyArg<Statement>()
val result =try { val result =try {
@@ -165,13 +177,56 @@ class Script(
addConst("Array", ObjArray) addConst("Array", ObjArray)
addConst("Class", ObjClassType) addConst("Class", ObjClassType)
addConst("Deferred", ObjDeferred.type)
addConst("CompletableDeferred", ObjCompletableDeferred.type)
addConst("Mutex", ObjMutex.type)
addFn("launch") {
val callable = args.firstAndOnly() as Statement
ObjDeferred(globalDefer {
callable.execute(this@addFn)
})
}
addFn("yield") {
yield()
ObjVoid
}
val pi = ObjReal(PI) val pi = ObjReal(PI)
addConst("π", pi) addConst("π", pi)
getOrCreateNamespace("Math").apply { getOrCreateNamespace("Math").apply {
addConst("PI", pi) addConst("PI", pi)
} }
}
val defaultImportManager: ImportManager by lazy {
ImportManager(rootScope, SecurityManager.allowAll).apply {
addPackage("lyng.buffer") {
it.addConst("Buffer", ObjBuffer.type)
it.addConst("MutableBuffer", ObjMutableBuffer.type)
}
addPackage("lyng.serialization") {
it.addConst("Lynon", ObjLynonClass)
}
addPackage("lyng.time") {
it.addConst("Instant", ObjInstant.type)
it.addConst("Duration", ObjDuration.type)
it.addFn("delay") {
val a = args.firstAndOnly()
when(a) {
is ObjInt -> delay(a.value * 1000)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect()}")
}
ObjVoid
}
}
}
} }
} }
} }
@@ -2,6 +2,8 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjException
open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? = null) : Exception( open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? = null) : Exception(
""" """
$pos: Error: $errorMessage $pos: Error: $errorMessage
@@ -12,4 +12,15 @@ class Source(val fileName: String, text: String) {
val startPos: Pos = Pos(this, 0, 0) val startPos: Pos = Pos(this, 0, 0)
fun posAt(line: Int, column: Int): Pos = Pos(this, line, column) fun posAt(line: Int, column: Int): Pos = Pos(this, line, column)
fun extractPackageName(): String {
for ((n,line) in lines.withIndex()) {
if( line.isBlank() || line.isEmpty() )
continue
if( line.startsWith("package ") )
return line.substring(8).trim()
else throw ScriptError(Pos(this, n, 0),"package declaration expected")
}
throw ScriptError(Pos(this, 0, 0),"package declaration expected")
}
} }
@@ -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")
}
@@ -1,48 +1,26 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex 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.ProtectedOp
import net.sergeych.synctools.withLock import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts 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
)
/**
* 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 class Obj {
open val isConst: Boolean = false
fun ensureNotConst(scope: Scope) {
if (isConst) scope.raiseError("can't assign to constant")
}
val isNull by lazy { this === ObjNull } val isNull by lazy { this === ObjNull }
var isFrozen: Boolean = false var isFrozen: Boolean = false
@@ -79,13 +57,20 @@ open class Obj {
args: Arguments = Arguments.EMPTY args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(scope, name, args) as T ): T = invokeInstanceMethod(scope, name, args) as T
/**
* Invoke a method of the object if exists
* it [onNotFoundResult] is not null, it returns it when symbol is not found
* otherwise throws [ObjSymbolNotDefinedException] object exception
*/
open suspend fun invokeInstanceMethod( open suspend fun invokeInstanceMethod(
scope: Scope, scope: Scope,
name: String, name: String,
args: Arguments = Arguments.EMPTY args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null
): Obj = ): Obj =
// note that getInstanceMember traverses the hierarchy objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args)
objClass.getInstanceMember(scope.pos, name).value.invoke(scope, this, args) ?: onNotFoundResult
?: scope.raiseSymbolNotFound(name)
open suspend fun getInstanceMethod( open suspend fun getInstanceMethod(
scope: Scope, scope: Scope,
@@ -125,6 +110,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()
} }
@@ -220,7 +209,7 @@ open class Obj {
suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong())) suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong()))
open suspend fun putAt(scope: Scope, index: Int, newValue: Obj) { open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
scope.raiseNotImplemented("indexing") scope.raiseNotImplemented("indexing")
} }
@@ -256,31 +245,47 @@ open class Obj {
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) } val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
val asMutable: ObjRecord by lazy { ObjRecord(this, true) } val asMutable: ObjRecord by lazy { ObjRecord(this, true) }
open suspend fun lynonType(): LynonType = LynonType.Other
open suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
scope.raiseNotImplemented()
}
companion object { companion object {
val rootObjectType = ObjClass("Obj").apply { val rootObjectType = ObjClass("Obj").apply {
addFn("toString") { addFn("toString") {
thisObj.asStr 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("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<Obj>(0))
}
addFn("putAt") {
requireExactCount(2)
val newValue = args[1]
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
newValue
}
}
inline fun from(obj: Any?): Obj { inline fun from(obj: Any?): Obj {
@@ -309,6 +314,8 @@ open class Obj {
} }
} }
fun Double.toObj(): Obj = ObjReal(this)
@Suppress("unused") @Suppress("unused")
inline fun <reified T> T.toObj(): Obj = Obj.from(this) inline fun <reified T> T.toObj(): Obj = Obj.from(this)
@@ -341,7 +348,7 @@ object ObjNull : Obj() {
scope.raiseNPE() scope.raiseNPE()
} }
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj { override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: Obj?): Obj {
scope.raiseNPE() scope.raiseNPE()
} }
@@ -349,7 +356,7 @@ object ObjNull : Obj() {
scope.raiseNPE() scope.raiseNPE()
} }
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) { override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
scope.raiseNPE() scope.raiseNPE()
} }
@@ -362,6 +369,27 @@ object ObjNull : Obj() {
override suspend fun toKotlin(scope: Scope): Any? { override suspend fun toKotlin(scope: Scope): Any? {
return null 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 { interface Numeric {
@@ -492,6 +520,9 @@ class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") : class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message) ObjException("IllegalArgumentException", scope, message)
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
ObjException("IllegalStateException", scope, message)
@Suppress("unused") @Suppress("unused")
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") : class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
ObjException("IllegalArgumentException", scope, message) ObjException("IllegalArgumentException", scope, message)
@@ -1,4 +1,4 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
val ObjArray by lazy { val ObjArray by lazy {
@@ -1,4 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class ObjArrayIterator(val array: Obj) : Obj() { class ObjArrayIterator(val array: Obj) : Obj() {
@@ -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<ObjBitBuffer>().bitArray.asUbyteArray())
}
addFn("toDump") {
requireNoArgs()
ObjString(
thisAs<ObjBitBuffer>().bitArray.asUbyteArray().toDump()
)
}
addFn("size") {
thisAs<ObjBitBuffer>().bitArray.size.toObj()
}
addFn("sizeInBytes") {
ObjInt((thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3)
}
}
}
}
@@ -0,0 +1,59 @@
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()) }
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBool) return -2
return value.compareTo(other.value)
}
override fun hashCode(): Int {
return value.hashCode()
}
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override suspend fun logicalNot(scope: Scope): Obj = ObjBool(!value)
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = ObjBool(value && other.toBool())
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(scope: Scope): Any {
return value
}
override suspend fun lynonType(): LynonType = LynonType.Bool
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeBoolean(value)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjBool
return value == other.value
}
companion object {
val type = object : ObjClass("Bool") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder,lynonType: LynonType?): Obj {
return ObjBool(decoder.unpackBoolean())
}
}
}
}
val ObjTrue = ObjBool(true)
val ObjFalse = ObjBool(false)
@@ -0,0 +1,166 @@
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() {
override val objClass: ObjClass = type
fun checkIndex(scope: Scope, index: Obj): Int {
if (index !is ObjInt)
scope.raiseIllegalArgument("index must be Int")
val i = index.value.toInt()
if (i < 0) scope.raiseIllegalArgument("index must be positive")
if (i >= byteArray.size)
scope.raiseIndexOutOfBounds("index $i is out of bounds 0..<${byteArray.size}")
return i
}
override suspend fun getAt(scope: Scope, index: Obj): Obj {
// notice: we create a copy if content, so we don't want it
// to be treated as modifiable, or putAt will not be called:
return if (index is ObjRange) {
val start: Int = index.startInt(scope)
val end: Int = index.exclusiveIntEnd(scope) ?: size
ObjBuffer(byteArray.sliceArray(start..<end))
} else ObjInt(byteArray[checkIndex(scope, index)].toLong(), true)
}
val size by byteArray::size
override fun hashCode(): Int {
return byteArray.hashCode()
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBuffer) return super.compareTo(scope, other)
val limit = min(size, other.size)
for (i in 0..<limit) {
val own = byteArray[i]
val their = other.byteArray[i]
if (own < their) return -1
else if (own > their) return 1
}
if (size < other.size) return -1
if (size > other.size) return 1
return 0
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return if (other is ObjBuffer)
ObjBuffer(byteArray + other.byteArray)
else if (other.isInstanceOf(ObjIterable)) {
ObjBuffer(
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray()
)
} else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect()}")
}
override fun toString(): String {
return "Buffer(${byteArray.encodeToHex()})"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjBuffer
return byteArray contentEquals other.byteArray
}
override suspend fun lynonType(): LynonType = LynonType.Buffer
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeCachedBytes(byteArray.asByteArray())
}
companion object {
private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer =
when (obj) {
is ObjBuffer -> ObjBuffer(obj.byteArray.copyOf())
is ObjInt -> {
if (obj.value < 0)
scope.raiseIllegalArgument("buffer size must be positive")
val data = UByteArray(obj.value.toInt())
ObjBuffer(data)
}
is ObjString -> ObjBuffer(obj.value.encodeToByteArray().asUByteArray())
else -> {
if (obj.isInstanceOf(ObjIterable)) {
ObjBuffer(
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray()
)
} else
scope.raiseIllegalArgument(
"can't construct buffer from ${obj.inspect()}"
)
}
}
val type = object : ObjClass("Buffer", ObjArray) {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args.list
return when (args.size) {
// empty buffer
0 -> ObjBuffer(ubyteArrayOf())
1 -> createBufferFrom(scope, args[0])
else -> {
// create buffer from array, each argument should be a byte then:
val data = UByteArray(args.size)
for ((i, b) in args.withIndex()) {
val code = when (b) {
is ObjChar -> b.value.code.toUByte()
is ObjInt -> b.value.toUByte()
else -> scope.raiseIllegalArgument(
"invalid byte value for buffer constructor at index $i: ${b.inspect()}"
)
}
data[i] = code
}
ObjBuffer(data)
}
}
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjBuffer( decoder.decodeCached {
decoder.decompress().asUByteArray()
})
}.apply {
createField("size",
statement {
(thisObj as ObjBuffer).byteArray.size.toObj()
}
)
addFn("decodeUtf8") {
ObjString(
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
)
}
addFn("toMutable") {
requireNoArgs()
ObjMutableBuffer(thisAs<ObjBuffer>().byteArray.copyOf())
}
addFn("toDump") {
requireNoArgs()
ObjString(
thisAs<ObjBuffer>().byteArray.toByteArray().toDump()
)
}
}
}
}
@@ -1,4 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class ObjChar(val value: Char): Obj() { class ObjChar(val value: Char): Obj() {
@@ -11,6 +13,19 @@ class ObjChar(val value: Char): Obj() {
override fun inspect(): String = "'$value'" override fun inspect(): String = "'$value'"
override fun hashCode(): Int {
return value.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjChar
return value == other.value
}
companion object { companion object {
val type = ObjClass("Char").apply { val type = ObjClass("Char").apply {
addFn("code") { ObjInt(thisAs<ObjChar>().value.code.toLong()) } addFn("code") { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
@@ -0,0 +1,136 @@
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") }
open class ObjClass(
val className: String,
vararg parents: ObjClass,
) : Obj() {
val classNameObj by lazy { ObjString(className) }
var constructorMeta: ArgsDeclaration? = null
var instanceConstructor: Statement? = null
/**
* the scope for class methods, initialize class vars, etc.
*
* Important notice. When create a user class, e.g. from Lyng source, it should
* be set to a scope by compiler, so it could access local closure, etc. Otherwise,
* it will be initialized to default scope on first necessity, e.g. when used in
* external, kotlin classes with [addClassConst] and [addClassFn], etc.
*/
var classScope: Scope? = null
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toMutableSet()
override val objClass: ObjClass by lazy { ObjClassType }
/**
* members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass]
*/
private val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(scope: Scope): Obj {
val instance = ObjInstance(this)
instance.instanceScope = scope.copy(newThisObj = instance,args = scope.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceScope)
}
return instance
}
fun createField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if( existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
private fun initClassScope(): Scope {
if( classScope == null ) classScope = Scope()
return classScope!!
}
fun createClassField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
initClassScope()
val existing = classScope!!.objects[name]
if( existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
classScope!!.addItem(name, isMutable, initialValue, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createClassField(name, statement { code() }, isOpen)
}
/**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
return rootObjectType.members[name]
}
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
override suspend fun readField(scope: Scope, name: String): ObjRecord {
classScope?.objects?.get(name)?.let {
if( it.visibility.isPublic ) return it
}
return super.readField(scope, name)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
initClassScope().objects[name]?.let {
if( it.isMutable) it.value = newValue
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
}
?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj {
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented()
}
@@ -1,4 +1,4 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
/** /**
* Collection is an iterator with `size`] * Collection is an iterator with `size`]
@@ -0,0 +1,22 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.lyng.Scope
class ObjCompletableDeferred(val completableDeferred: CompletableDeferred<Obj>): ObjDeferred(completableDeferred) {
override val objClass = type
companion object {
val type = object: ObjClass("CompletableDeferred", ObjDeferred.type){
override suspend fun callOn(scope: Scope): Obj {
return ObjCompletableDeferred(CompletableDeferred())
}
}.apply {
addFn("complete") {
thisAs<ObjCompletableDeferred>().completableDeferred.complete(args.firstAndOnly())
ObjVoid
}
}
}
}
@@ -0,0 +1,32 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.Deferred
import net.sergeych.lyng.Scope
open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
override val objClass = type
companion object {
val type = object: ObjClass("Deferred"){
override suspend fun callOn(scope: Scope): Obj {
scope.raiseError("Deferred constructor is not directly callable")
}
}.apply {
addFn("await") {
thisAs<ObjDeferred>().deferred.await()
}
addFn("isCompleted") {
thisAs<ObjDeferred>().deferred.isCompleted.toObj()
}
addFn("isActive") {
thisAs<ObjDeferred>().deferred.isActive.toObj()
}
addFn("isCancelled") {
thisAs<ObjDeferred>().deferred.isCancelled.toObj()
}
}
}
}
@@ -0,0 +1,154 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
class ObjDuration(val duration: Duration) : Obj() {
override val objClass: ObjClass = type
override fun toString(): String {
return duration.toString()
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if( other is ObjDuration)
duration.compareTo(other.duration)
else -1
}
override fun hashCode(): Int {
return duration.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjDuration
return duration == other.duration
}
companion object {
val type = object : ObjClass("Duration") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
if( args.list.size > 1 )
scope.raiseIllegalArgument("can't construct Duration(${args.inspect()})")
val a0 = args.list.getOrNull(0)
return ObjDuration(
when (a0) {
null -> Duration.ZERO
is ObjInt -> a0.value.seconds
is ObjReal -> a0.value.seconds
else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
}
}
)
}
}.apply {
addFn("days") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj()
}
addFn("hours") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj()
}
addFn("minutes") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj()
}
addFn("seconds") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj()
}
addFn("milliseconds") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj()
}
addFn("microseconds") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj()
}
// extensions
ObjInt.type.addFn("seconds") {
ObjDuration(thisAs<ObjInt>().value.seconds)
}
ObjInt.type.addFn("second") {
ObjDuration(thisAs<ObjInt>().value.seconds)
}
ObjInt.type.addFn("milliseconds") {
ObjDuration(thisAs<ObjInt>().value.milliseconds)
}
ObjInt.type.addFn("millisecond") {
ObjDuration(thisAs<ObjInt>().value.milliseconds)
}
ObjReal.type.addFn("seconds") {
ObjDuration(thisAs<ObjReal>().value.seconds)
}
ObjReal.type.addFn("second") {
ObjDuration(thisAs<ObjReal>().value.seconds)
}
ObjReal.type.addFn("milliseconds") {
ObjDuration(thisAs<ObjReal>().value.milliseconds)
}
ObjReal.type.addFn("millisecond") {
ObjDuration(thisAs<ObjReal>().value.milliseconds)
}
ObjInt.type.addFn("minutes") {
ObjDuration(thisAs<ObjInt>().value.minutes)
}
ObjReal.type.addFn("minutes") {
ObjDuration(thisAs<ObjReal>().value.minutes)
}
ObjInt.type.addFn("minute") {
ObjDuration(thisAs<ObjInt>().value.minutes)
}
ObjReal.type.addFn("minute") {
ObjDuration(thisAs<ObjReal>().value.minutes)
}
ObjInt.type.addFn("hours") {
ObjDuration(thisAs<ObjInt>().value.hours)
}
ObjReal.type.addFn("hours") {
ObjDuration(thisAs<ObjReal>().value.hours)
}
ObjInt.type.addFn("hour") {
ObjDuration(thisAs<ObjInt>().value.hours)
}
ObjReal.type.addFn("hour") {
ObjDuration(thisAs<ObjReal>().value.hours)
}
ObjInt.type.addFn("days") {
ObjDuration(thisAs<ObjInt>().value.days)
}
ObjReal.type.addFn("days") {
ObjDuration(thisAs<ObjReal>().value.days)
}
ObjInt.type.addFn("day") {
ObjDuration(thisAs<ObjInt>().value.days)
}
ObjReal.type.addFn("day") {
ObjDuration(thisAs<ObjReal>().value.days)
}
// addFn("epochSeconds") {
// val instant = thisAs<ObjInstant>().instant
// ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
// }
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
}
}
}
@@ -0,0 +1,106 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceScope: Scope
override suspend fun readField(scope: Scope, name: String): ObjRecord {
return instanceScope[name]?.let {
if (it.visibility.isPublic)
it
else
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
}
?: super.readField(scope, name)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
instanceScope[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
f.value = newValue
} ?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(scope, this, args)
else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
private val publicFields: Map<String, ObjRecord>
get() = instanceScope.objects.filter { it.value.visibility.isPublic }
override fun toString(): String {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
return "${objClass.className}($fields)"
}
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)")
// actual constructor can vary, for example, adding new fields with default
// values, so we save size of the construction:
// using objlist allow for some optimizations:
val params = meta.params.map { readField(scope, it.name).value }
encoder.encodeAnyList(scope, params)
serializeStateVars(scope, encoder)
}
protected val instanceVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter { it.value.type.serializable }
}
protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) {
val vars = instanceVars.values.map { it.value }
if( vars.isNotEmpty()) {
encoder.encodeAnyList(scope, vars)
println("serialized state vars $vars")
}
}
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
val localVars = instanceVars.values.toList()
if( localVars.isNotEmpty() ) {
println("gonna read vars")
val vars = decoder.decodeAnyList(scope)
if (vars.size > instanceVars.size)
scope.raiseIllegalArgument("serialized vars has bigger size than instance vars")
println("deser state vars $vars")
for ((i, v) in vars.withIndex()) {
localVars[i].value = v
}
}
}
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in comparableVars) {
val a = f.value.value
val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b)
if (d != 0) return d
}
return 0
}
}
@@ -0,0 +1,26 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonType
/**
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
*/
class ObjInstanceClass(val name: String) : ObjClass(name) {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val args = decoder.decodeAnyList(scope)
println("deserializing constructor $name, $args params")
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.copy(args = Arguments(args))
return (callOn(newScope) as ObjInstance).apply {
deserializeStateVars(scope,decoder)
invokeInstanceMethod(scope, "onDeserialized", onNotFoundResult = ObjVoid)
}
}
}
@@ -0,0 +1,178 @@
package net.sergeych.lyng.obj
import kotlinx.datetime.Clock
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
override fun toString(): String {
return instant.toString()
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant + other.duration)
else -> super.plus(scope, other)
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant - other.duration)
is ObjInstant -> ObjDuration(instant - other.instant)
else -> super.plus(scope, other)
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjInstant) {
return instant.compareTo(other.instant)
}
return super.compareTo(scope, other)
}
override suspend fun toKotlin(scope: Scope): Any {
return instant
}
override fun hashCode(): Int {
return instant.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInstant
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)
}
val distantPast by lazy {
ObjInstant(Instant.DISTANT_PAST)
}
val type = object : ObjClass("Instant") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
val a0 = args.list.getOrNull(0)
return ObjInstant(
when (a0) {
null -> {
val t = Clock.System.now()
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond)
}
is ObjInt -> Instant.fromEpochSeconds(a0.value)
is ObjReal -> {
val seconds = a0.value.toLong()
val nanos = (a0.value - seconds) * 1e9
Instant.fromEpochSeconds(seconds, nanos.toLong())
}
is ObjInstant -> a0.instant
else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
}
}
)
}
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<ObjInstant>().instant
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
}
addFn("isDistantFuture") {
thisAs<ObjInstant>().instant.isDistantFuture.toObj()
}
addFn("isDistantPast") {
thisAs<ObjInstant>().instant.isDistantPast.toObj()
}
addFn("epochWholeSeconds") {
ObjInt(thisAs<ObjInstant>().instant.epochSeconds)
}
addFn("nanosecondsOfSecond") {
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
addClassConst("distantFuture", distantFuture)
addClassConst("distantPast", distantPast)
addClassFn("now") {
ObjInstant(Clock.System.now())
}
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
}
}
}
@@ -1,6 +1,11 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
data class ObjInt(var value: Long) : Obj(), Numeric { 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 {
override val asStr get() = ObjString(value.toString()) override val asStr get() = ObjString(value.toString())
override val longValue get() = value override val longValue get() = value
override val doubleValue get() = value.toDouble() override val doubleValue get() = value.toDouble()
@@ -14,18 +19,22 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
} }
override suspend fun getAndIncrement(scope: Scope): Obj { override suspend fun getAndIncrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value++ } return ObjInt(value).also { value++ }
} }
override suspend fun getAndDecrement(scope: Scope): Obj { override suspend fun getAndDecrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value-- } return ObjInt(value).also { value-- }
} }
override suspend fun incrementAndGet(scope: Scope): Obj { override suspend fun incrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(++value) return ObjInt(++value)
} }
override suspend fun decrementAndGet(scope: Scope): Obj { override suspend fun decrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(--value) return ObjInt(--value)
} }
@@ -70,7 +79,7 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
* assignment * assignment
*/ */
override suspend fun assign(scope: Scope, other: Obj): Obj? { override suspend fun assign(scope: Scope, other: Obj): Obj? {
return if (other is ObjInt) { return if (!isConst && other is ObjInt) {
value = other.value value = other.value
this this
} else null } else null
@@ -89,10 +98,44 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
return value == other.value return value == other.value
} }
override suspend fun negate(scope: Scope): Obj {
return ObjInt(-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())
LynonType.IntSigned -> encoder.encodeSigned(value)
else -> scope.raiseIllegalArgument("Unsupported lynon type code for Int: $lynonType")
}
}
companion object { companion object {
val Zero = ObjInt(0) val Zero = ObjInt(0, true)
val One = ObjInt(1) val One = ObjInt(1, true)
val type = ObjClass("Int") 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())
LynonType.IntSigned -> ObjInt(decoder.unpackSigned())
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
}
}
} }
} }
@@ -1,4 +1,7 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Statement
/** /**
* Abstract class that must provide `iterator` method that returns [ObjIterator] instance. * Abstract class that must provide `iterator` method that returns [ObjIterator] instance.
@@ -0,0 +1,3 @@
package net.sergeych.lyng.obj
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -1,9 +1,10 @@
@file:Suppress("unused") @file:Suppress("unused")
package net.sergeych.lyng package net.sergeych.lyng.obj
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import net.sergeych.lyng.Scope
/** /**
* Iterator wrapper to allow Kotlin collections to be returned from Lyng objects; * Iterator wrapper to allow Kotlin collections to be returned from Lyng objects;
@@ -1,12 +1,13 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
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
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() { class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
init {
for (p in objClass.parents)
parentInstances.add(p.defaultInstance())
}
override fun toString(): String = "[${ override fun toString(): String = "[${
list.joinToString(separator = ", ") { it.inspect() } list.joinToString(separator = ", ") { it.inspect() }
}]" }]"
@@ -51,9 +52,8 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
} }
override suspend fun putAt(scope: Scope, index: Int, newValue: Obj) { override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
val i = index list[index.toInt()] = newValue
list[i] = newValue
} }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
@@ -128,24 +128,23 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return list == other.list return list == other.list
} }
companion object { override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
val type = ObjClass("List", ObjArray).apply { encoder.encodeAnyList(scope,list)
}
override suspend fun lynonType(): LynonType = LynonType.List
companion object {
val type = object : ObjClass("List", ObjArray) {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return ObjList(decoder.decodeAnyList(scope))
}
}.apply {
createField("size", createField("size",
statement { statement {
(thisObj as ObjList).list.size.toObj() (thisObj as ObjList).list.size.toObj()
} }
) )
addFn("getAt") {
requireExactCount(1)
thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
}
addFn("putAt") {
requireExactCount(2)
val newValue = args[1]
thisAs<ObjList>().putAt(this, requiredArg<ObjInt>(0).value.toInt(), newValue)
newValue
}
createField("add", createField("add",
statement { statement {
val l = thisAs<ObjList>().list val l = thisAs<ObjList>().list
@@ -1,4 +1,10 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
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
class ObjMapEntry(val key: Obj, val value: Obj) : Obj() { class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
@@ -9,6 +15,14 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
return value.compareTo(scope, other.value) return value.compareTo(scope, other.value)
} }
override fun hashCode(): Int {
return key.hashCode() + value.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is ObjMapEntry && key == other.key && value == other.value
}
override suspend fun getAt(scope: Scope, index: Obj): Obj = when (index.toInt()) { override suspend fun getAt(scope: Scope, index: Obj): Obj = when (index.toInt()) {
0 -> key 0 -> key
1 -> value 1 -> value
@@ -21,11 +35,23 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
override val objClass = type override val objClass = type
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope,key)
encoder.encodeAny(scope,value)
}
companion object { companion object {
val type = object : ObjClass("MapEntry", ObjArray) { val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjMapEntry(scope.requiredArg<Obj>(0), scope.requiredArg<Obj>(1)) return ObjMapEntry(scope.requiredArg<Obj>(0), scope.requiredArg<Obj>(1))
} }
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return ObjMapEntry(
decoder.decodeAny(scope),
decoder.decodeAny(scope)
)
}
}.apply { }.apply {
addFn("key") { thisAs<ObjMapEntry>().key } addFn("key") { thisAs<ObjMapEntry>().key }
addFn("value") { thisAs<ObjMapEntry>().value } addFn("value") { thisAs<ObjMapEntry>().value }
@@ -39,7 +65,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override val objClass = type override val objClass = type
override suspend fun getAt(scope: Scope, index: Obj): Obj = override suspend fun getAt(scope: Scope, index: Obj): Obj =
map.getOrElse(index) { scope.raiseNoSuchElement() } map.get(index) ?: ObjNull
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
map[index] = newValue
}
override suspend fun contains(scope: Scope, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
return other in map return other in map
@@ -51,6 +81,28 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
} }
override fun toString(): String = map.toString() override fun toString(): String = map.toString()
override fun hashCode(): Int {
return map.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjMap
return map == other.map
}
override suspend fun lynonType(): LynonType = LynonType.Map
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
val keys = map.keys.map { it.toObj() }
val values = map.values.map { it.toObj() }
encoder.encodeAnyList(scope, keys)
encoder.encodeAnyList(scope, values, fixedSize = true)
}
companion object { companion object {
suspend fun listToMap(scope: Scope, list: List<Obj>): MutableMap<Obj, Obj> { suspend fun listToMap(scope: Scope, list: List<Obj>): MutableMap<Obj, Obj> {
@@ -78,6 +130,13 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjMap(listToMap(scope, scope.args.list)) return ObjMap(listToMap(scope, scope.args.list))
} }
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val keys = decoder.decodeAnyList(scope)
val values = decoder.decodeAnyList(scope,fixedSize = keys.size)
if( keys.size != values.size) scope.raiseIllegalArgument("map keys and values should be same size")
return ObjMap(keys.zip(values).toMap().toMutableMap())
}
}.apply { }.apply {
addFn("getOrNull") { addFn("getOrNull") {
val key = args.firstAndOnly(pos) val key = args.firstAndOnly(pos)
@@ -0,0 +1,71 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import net.sergeych.lyng.Scope
class ObjMutableBuffer(byteArray: UByteArray) : ObjBuffer(byteArray) {
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
byteArray[checkIndex(scope, index.toObj())] = when (newValue) {
is ObjInt -> newValue.value.toUByte()
is ObjChar -> newValue.value.code.toUByte()
else -> scope.raiseIllegalArgument(
"invalid byte value for buffer at index ${index.inspect()}: ${newValue.inspect()}"
)
}
}
companion object {
private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer =
when (obj) {
is ObjBuffer -> ObjMutableBuffer(obj.byteArray.copyOf())
is ObjInt -> {
if (obj.value < 0)
scope.raiseIllegalArgument("buffer size must be positive")
val data = UByteArray(obj.value.toInt())
ObjMutableBuffer(data)
}
is ObjString -> ObjMutableBuffer(obj.value.encodeToByteArray().asUByteArray())
else -> {
if (obj.isInstanceOf(ObjIterable)) {
ObjMutableBuffer(
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray()
)
} else
scope.raiseIllegalArgument(
"can't construct buffer from ${obj.inspect()}"
)
}
}
val type = object : ObjClass("MutableBuffer", ObjBuffer.type) {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args.list
return when (args.size) {
// empty buffer
0 -> ObjMutableBuffer(ubyteArrayOf())
1 -> createBufferFrom(scope, args[0])
else -> {
// create buffer from array, each argument should be a byte then:
val data = UByteArray(args.size)
for ((i, b) in args.withIndex()) {
val code = when (b) {
is ObjChar -> b.value.code.toUByte()
is ObjInt -> b.value.toUByte()
else -> scope.raiseIllegalArgument(
"invalid byte value for buffer constructor at index $i: ${b.inspect()}"
)
}
data[i] = code
}
ObjMutableBuffer(data)
}
}
}
}
}
}
@@ -0,0 +1,23 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
class ObjMutex(val mutex: Mutex): Obj() {
override val objClass = type
companion object {
val type = object: ObjClass("Mutex") {
override suspend fun callOn(scope: Scope): Obj {
return ObjMutex(Mutex())
}
}.apply {
addFn("withLock") {
val f = requiredArg<Statement>(0)
thisAs<ObjMutex>().mutex.withLock { f.execute(this) }
}
}
}
}
@@ -1,4 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() { class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
@@ -15,6 +17,31 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
return result.toString() return result.toString()
} }
/**
* IF end is open (null/ObjNull), returns null
* Otherwise, return correct value for the exclusive end
* raises [ObjIllegalArgumentException] if end is not ObjInt
*/
fun exclusiveIntEnd(scope: Scope): Int? =
if (end == null || end is ObjNull) null
else {
if (end !is ObjInt) scope.raiseIllegalArgument("end is not int")
if (isEndInclusive) end.value.toInt() + 1 else end.value.toInt()
}
/**
* If start is null/ObjNull, returns 0
* if start is not ObjInt, raises [ObjIllegalArgumentException]
* otherwise returns start.value.toInt()
*/
fun startInt(scope: Scope): Int =
if( start == null || start is ObjNull) 0
else {
if( start is ObjInt) start.value.toInt()
else scope.raiseIllegalArgument("start is not Int: ${start.inspect()}")
}
suspend fun containsRange(scope: Scope, other: ObjRange): Boolean { suspend fun containsRange(scope: Scope, other: ObjRange): Boolean {
if (start != null) { if (start != null) {
// our start is not -∞ so other start should be GTE or is not contained: // our start is not -∞ so other start should be GTE or is not contained:
@@ -74,6 +101,27 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
?: -1 ?: -1
} }
override fun hashCode(): Int {
var result = start?.hashCode() ?: 0
result = 31 * result + (end?.hashCode() ?: 0)
result = 31 * result + isEndInclusive.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjRange
if (start != other.start) return false
if (end != other.end) return false
if (isEndInclusive != other.isEndInclusive) return false
return true
}
companion object { companion object {
val type = ObjClass("Range", ObjIterable).apply { val type = ObjClass("Range", ObjIterable).apply {
addFn("start") { addFn("start") {
@@ -1,4 +1,6 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class ObjRangeIterator(val self: ObjRange) : Obj() { class ObjRangeIterator(val self: ObjRange) : Obj() {
@@ -1,5 +1,11 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import net.sergeych.lyng.Pos
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.floor
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -56,8 +62,21 @@ 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 lynonType(): LynonType = LynonType.Real
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeReal(value)
}
companion object { companion object {
val type: ObjClass = ObjClass("Real").apply { val type: ObjClass = object : ObjClass("Real") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjReal(decoder.unpackDouble())
}.apply {
createField( createField(
"roundToInt", "roundToInt",
statement(Pos.builtIn) { statement(Pos.builtIn) {
@@ -0,0 +1,27 @@
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,
val isTransient: Boolean = false,
val type: Type = Type.Other
) {
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
Field(true, true),
@Suppress("unused")
Fun,
ConstructorField(true, true),
Other
}
@Suppress("unused")
fun qualifiedName(name: String): String =
"${importedFrom?.packageName ?: "anonymous"}.$name"
}
@@ -1,4 +1,9 @@
package net.sergeych.lyng 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 ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() { class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
@@ -65,6 +70,25 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
} }
} }
override fun hashCode(): Int {
return set.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjSet
return set == other.set
}
override suspend fun lynonType(): LynonType = LynonType.Set
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAnyList(scope, set.toList())
}
companion object { companion object {
@@ -72,6 +96,9 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjSet(scope.args.list.toMutableSet()) return ObjSet(scope.args.list.toMutableSet())
} }
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjSet(decoder.decodeAnyList(scope).toMutableSet())
}.apply { }.apply {
addFn("size") { addFn("size") {
thisAs<ObjSet>().set.size.toObj() thisAs<ObjSet>().set.size.toObj()
@@ -1,7 +1,12 @@
package net.sergeych.lyng package net.sergeych.lyng.obj
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
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 import net.sergeych.sprintf.sprintf
@Serializable @Serializable
@@ -15,7 +20,6 @@ data class ObjString(val value: String) : Obj() {
// return i // return i
// } // }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjString) return -2 if (other !is ObjString) return -2
return this.value.compareTo(other.value) return this.value.compareTo(other.value)
@@ -37,12 +41,12 @@ data class ObjString(val value: String) : Obj() {
} }
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
if( index is ObjInt ) return ObjChar(value[index.toInt()]) if (index is ObjInt) return ObjChar(value[index.toInt()])
if( index is ObjRange ) { if (index is ObjRange) {
val start = if(index.start == null || index.start.isNull) 0 else index.start.toInt() val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
val end = if( index.end == null || index.end.isNull ) value.length else { val end = if (index.end == null || index.end.isNull) value.length else {
val e = index.end.toInt() val e = index.end.toInt()
if( index.isEndInclusive) e + 1 else e if (index.isEndInclusive) e + 1 else e
} }
return ObjString(value.substring(start, end)) return ObjString(value.substring(start, end))
} }
@@ -54,7 +58,10 @@ data class ObjString(val value: String) : Obj() {
} }
override suspend fun callOn(scope: Scope): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjString(this.value.sprintf(*scope.args.toKotlinList(scope).toTypedArray())) return ObjString(this.value.sprintf(*scope.args
.toKotlinList(scope)
.map { if (it == null) "null" else it }
.toTypedArray()))
} }
override suspend fun contains(scope: Scope, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
@@ -74,8 +81,17 @@ data class ObjString(val value: String) : Obj() {
return value == other.value return value == other.value
} }
override suspend fun lynonType(): LynonType = LynonType.String
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeBinaryData(value.encodeToByteArray())
}
companion object { companion object {
val type = ObjClass("String").apply { val type = object : ObjClass("String") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjString(decoder.unpackBinaryData().decodeToString())
}.apply {
addFn("toInt") { addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong()) ObjInt(thisAs<ObjString>().value.toLong())
} }
@@ -114,8 +130,14 @@ data class ObjString(val value: String) : Obj() {
addFn("upper") { addFn("upper") {
thisAs<ObjString>().value.uppercase().let(::ObjString) thisAs<ObjString>().value.uppercase().let(::ObjString)
} }
addFn("characters") {
ObjList(
thisAs<ObjString>().value.map { ObjChar(it) }.toMutableList()
)
}
addFn("encodeUtf8") { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) } addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble())} addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble()) }
} }
} }
} }
@@ -0,0 +1,141 @@
package net.sergeych.lyng.pacman
import net.sergeych.lyng.*
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
/**
* Import manager allow to register packages with builder lambdas and act as an
* [ImportProvider]. Note that packages _must be registered_ first with [addPackage],
* [addSourcePackages] or [addTextPackages]. Registration is cheap, actual package
* building is lazily performed on [createModuleScope], when the package will
* be first imported.
*
* It is possible to register new packages at any time, but it is not allowed to override
* packages already registered.
*/
class ImportManager(
rootScope: Scope = Script.defaultImportManager.newModule(),
securityManager: SecurityManager = SecurityManager.allowAll
) : ImportProvider(rootScope, securityManager) {
val packageNames: List<String> get() = imports.keys.toList()
private inner class Entry(
val packageName: String,
val builder: suspend (ModuleScope) -> Unit,
var cachedScope: ModuleScope? = null
) {
suspend fun getScope(pos: Pos): ModuleScope {
cachedScope?.let { return it }
return ModuleScope(inner, pos, packageName).apply {
cachedScope = this
builder(this)
}
}
}
/**
* Inner provider does not lock [op], the only difference; it is meant to be used
* exclusively by the coroutine that starts actual import chain
*/
private inner class InternalProvider : ImportProvider(rootScope) {
override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope {
return doImport(packageName, pos)
}
override fun getActualProvider(): ImportProvider {
return this@ImportManager
}
}
/**
* Inner module import provider used to prepare lazily prepared modules
*/
private val inner = InternalProvider()
private val imports = mutableMapOf<String, Entry>()
val op = ProtectedOp()
/**
* Register new package that can be imported. It is not possible to unregister or
* update package already registered.
*
* Packages are lazily created when first imported somewhere, so the registration is
* cheap; the recommended procedure is to register all available packages prior to
* compile with this.
*
* @param name package name
* @param builder lambda to create actual package using the given [ModuleScope]
*/
fun addPackage(name: String, builder: suspend (ModuleScope) -> Unit) {
op.withLock {
if (name in imports)
throw IllegalArgumentException("Package $name already exists")
imports[name] = Entry(name, builder)
}
}
/**
* Bulk [addPackage] with slightly better performance
*/
@Suppress("unused")
fun addPackages(registrationData: List<Pair<String, suspend (ModuleScope) -> Unit>>) {
op.withLock {
for (pp in registrationData) {
if (pp.first in imports)
throw IllegalArgumentException("Package ${pp.first} already exists")
imports[pp.first] = Entry(pp.first, pp.second)
}
}
}
/**
* Perform actual import or return ready scope. __It must only be called when
* [op] is locked__, e.g. only internally
*/
private suspend fun doImport(packageName: String, pos: Pos): ModuleScope {
val entry = imports[packageName] ?: throw ImportException(pos, "package not found: $packageName")
return entry.getScope(pos)
}
override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope =
doImport(packageName, pos)
/**
* Add packages that only need to compile [Source].
*/
fun addSourcePackages(vararg sources: Source) {
for (s in sources) {
addPackage(s.extractPackageName()) {
it.eval(s)
}
}
}
/**
* Add source packages using package name as [Source.fileName], for simplicity
*/
fun addTextPackages(vararg sourceTexts: String) {
for (s in sourceTexts) {
var source = Source("tmp", s)
val packageName = source.extractPackageName()
source = Source(packageName, s)
addPackage(packageName) { it.eval(source) }
}
}
fun copy(): ImportManager =
op.withLock {
ImportManager(rootScope, securityManager).apply {
imports.putAll(this@ImportManager.imports)
}
}
}
@@ -0,0 +1,51 @@
package net.sergeych.lyng.pacman
import net.sergeych.lyng.*
/**
* Package manager INTERFACE (abstract class). Performs import routines
* using abstract [createModuleScope] method ot be implemented by heirs.
*
* Notice that [createModuleScope] is responsible for caching the modules;
* base class relies on caching. This is not implemented here as the correct
* caching strategy depends on the import provider
*/
abstract class ImportProvider(
val rootScope: Scope,
val securityManager: SecurityManager = SecurityManager.allowAll
) {
open fun getActualProvider() = this
/**
* Find an import and create a scope for it. This method must implement caching so repeated
* imports are not repeatedly loaded and parsed and should be cheap.
*
* @throws ImportException if the module is not found
*/
abstract suspend fun createModuleScope(pos: Pos,packageName: String): ModuleScope
/**
* Check that the import is possible and allowed. This method is called on compile time by [Compiler];
* actual module loading is performed by [ModuleScope.importInto]
*/
suspend fun prepareImport(pos: Pos, name: String, symbols: Map<String, String>?): ModuleScope {
if (!securityManager.canImportModule(name))
throw ImportException(pos, "Module $name is not allowed")
symbols?.keys?.forEach {
if (!securityManager.canImportSymbol(name, it)) throw ImportException(
pos,
"Symbol $name.$it is not allowed"
)
}
return createModuleScope(pos, name)
}
fun newModule() = newModuleAt(Pos.builtIn)
fun newModuleAt(pos: Pos): ModuleScope =
ModuleScope(this, pos, "unknown")
}
@@ -0,0 +1,35 @@
package net.sergeych.lyng.pacman
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.lyng.*
import net.sergeych.mp_tools.globalLaunch
/**
* The sample import provider that imports sources available in memory.
* on construction time.
*
* Actually it is left here only as a demo.
*/
class InlineSourcesImportProvider(sources: List<Source>,
rootScope: ModuleScope = Script.defaultImportManager.newModule(),
securityManager: SecurityManager = SecurityManager.allowAll
) : ImportProvider(rootScope) {
private val manager = ImportManager(rootScope, securityManager)
private val readyManager = CompletableDeferred<ImportManager>()
/**
* This implementation only
*/
override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope {
return readyManager.await().createModuleScope(pos, packageName)
}
init {
globalLaunch {
manager.addSourcePackages(*sources.toTypedArray())
readyManager.complete(manager)
}
}
}
@@ -1,17 +1,24 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjVoid
fun String.toSource(name: String = "eval"): Source = Source(name, this) fun String.toSource(name: String = "eval"): Source = Source(name, this)
sealed class ObjType { sealed class ObjType {
object Any : ObjType() object Any : ObjType()
object Int : ObjType() object Void: ObjType()
companion object {
}
} }
@Suppress("unused") @Suppress("unused")
abstract class Statement( abstract class Statement(
val isStaticConst: Boolean = false, val isStaticConst: Boolean = false,
val isConst: Boolean = false, override val isConst: Boolean = false,
val returnType: ObjType = ObjType.Any val returnType: ObjType = ObjType.Any
) : Obj() { ) : Obj() {
@@ -59,4 +66,8 @@ fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspe
override suspend fun execute(scope: Scope): Obj = f(scope) override suspend fun execute(scope: Scope): Obj = f(scope)
} }
object NopStatement: Statement(true, true, ObjType.Void) {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = ObjVoid
}
@@ -0,0 +1,90 @@
package net.sergeych.lynon
interface BitInput {
fun getBitOrNull(): Int?
fun getBitsOrNull(count: Int): ULong? {
var result = 0UL
var resultMask = 1UL
for( i in 0 ..< count) {
when(getBitOrNull()) {
null -> return null
1 -> result = result or resultMask
0 -> {}
}
resultMask = resultMask shl 1
}
return result
}
fun getBits(count: Int): ULong {
return getBitsOrNull(count) ?: throw IllegalStateException("Unexpected end of stream")
}
fun getBit(): Int {
return getBitOrNull() ?: throw IllegalStateException("Unexpected end of stream")
}
fun unpackUnsigned(): ULong =
unpackUnsignedOrNull() ?: throw IllegalStateException("Unexpected end of stream")
fun unpackUnsignedOrNull(): ULong? {
val tetrades = getBitsOrNull(4)?.toInt() ?: return null
var result = 0UL
var shift = 0
for (i in 0.. tetrades) {
result = result or (getBits(4) shl shift)
shift += 4
}
return result
}
fun unpackSigned(): Long {
val isNegative = getBit()
val value = unpackUnsigned().toLong()
return if( isNegative == 1) -value else value
}
@Suppress("unused")
fun getBool(): Boolean {
return getBit() == 1
}
fun getBytes(count: Int): ByteArray? {
val result = ByteArray(count)
for (i in 0..<count) {
val b = getBitsOrNull(8) ?: return null
result[i] = b.toByte()
}
return result
}
fun decompress(): ByteArray = decompressOrNull() ?: throw DecompressionException("Unexpected end of stream")
fun decompressOrNull(): ByteArray? {
val originalSize = unpackUnsignedOrNull()?.toInt() ?: return null
return if( getBit() == 1) {
// data is compressed
// val expectedCRC = getBits(32).toUInt()
val method = getBits(2).toInt()
if( method != 0) throw DecompressionException("Unknown compression method")
LZW.decompress(this, originalSize).asByteArray()
}
else {
getBytes(originalSize) ?: throw DecompressionException("Unexpected end of stream in uncompressed data")
}
}
@Suppress("unused")
fun decompressStringOrNull(): String? = decompressOrNull()?.decodeToString()
fun decompressString(): String = decompress().decodeToString()
fun unpackDouble(): Double {
val bits = getBits(64)
return Double.fromBits(bits.toLong())
}
}
@@ -0,0 +1,34 @@
package net.sergeych.lynon
@Suppress("unused")
interface BitList {
operator fun get(bitIndex: Long): Int
operator fun set(bitIndex: Long,value: Int)
val size: Long
val indices: LongRange
fun toInput(): BitInput = object : BitInput {
private var index = 0L
override fun getBitOrNull(): Int? =
if( index < size) this@BitList[index++]
else null
}
}
fun bitListOf(vararg bits: Int): BitList {
return if( bits.size > 64) {
BitArray.ofBits(*bits)
}
else
TinyBits.of(*bits)
}
@Suppress("unused")
fun bitListOfSize(sizeInBits: Long): BitList {
return if( sizeInBits > 64) {
BitArray.withBitSize(sizeInBits)
}
else
TinyBits()
}
@@ -0,0 +1,104 @@
package net.sergeych.lynon
interface BitOutput {
fun putBits(bits: ULong, count: Int) {
require(count <= 64)
var x = bits
for (i in 0..<count) {
putBit((x and 1u).toInt())
x = x shr 1
}
}
fun putBits(bits: Int, count: Int) {
require(count <= 32)
var x = bits
for (i in 0..<count) {
putBit((x and 1))
x = x shr 1
}
}
fun putBit(bit: Int)
fun putBits(bitList: BitList) {
for (i in bitList.indices)
putBit(bitList[i])
}
fun packUnsigned(value: ULong) {
val tetrades = sizeInTetrades(value)
putBits(tetrades - 1, 4)
var rest = value
for (i in 0..<tetrades) {
putBits(rest and 0xFu, 4)
rest = rest shr 4
}
}
@Suppress("unused")
fun packSigned(value: Long) {
if (value < 0) {
putBit(1)
packUnsigned((-value).toULong())
} else {
putBit(0)
packUnsigned(value.toULong())
}
}
fun putBytes(data: ByteArray) {
for (b in data) {
putBits(b.toULong(), 8)
}
}
/**
* Create compressed record with content and size check. Compression works with _bytes_.
*
* Structure:
*
* | size | meaning |
* |------|--------------------------------------------------|
* | packed unsigned | size of uncompressed content in bytes |
* | 1 | 0 - not compressed, 1 - compressed |
*
* __If compressed__, then:
*
* | size | meaning |
* |------|--------------------------------------|
* | 2 | 00 - LZW, other combinations reserved|
*
* After this header compressed bits follow.
*
* __If not compressed,__ then source data follows as bit stream.
*
* Compressed block overhead is 3 bits, uncompressed 1.
*/
fun compress(source: ByteArray) {
// size
packUnsigned(source.size.toULong())
// check compression is effective?
val compressed = LZW.compress(source.asUByteArray())
// check that compression is effective including header bits size:
if( compressed.size + 2 < source.size * 8L) {
println("write compressed")
putBit(1)
// LZW algorithm
putBits(0, 2)
// compressed data
putBits(compressed)
}
else {
putBit(0)
putBytes(source)
}
}
fun compress(source: String) {
compress(source.encodeToByteArray())
}
}
@@ -0,0 +1,3 @@
package net.sergeych.lynon
class DecompressionException(message: String) : IllegalArgumentException(message) {}
@@ -0,0 +1,106 @@
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.ObjString
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
fun getBitsAsInt(bitsSize: Int): Int {
return bin.getBits(bitsSize).toInt()
}
fun unpackUnsignedInt(): Int = bin.unpackUnsigned().toInt()
fun decompress() = bin.decompress()
val cache = mutableListOf<Any>()
inline fun <reified T : Any> decodeCached(f: LynonDecoder.() -> T): T {
return if (bin.getBit() == 0) {
// unpack and cache
f().also {
// println("decode: cache miss: ${cache.size}: $it:${it::class.simpleName}")
if (settings.shouldCache(it)) cache.add(it)
}
} else {
// get cache reference
val size = sizeInBits(cache.size)
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}")
// println("decode: cache hit ${id}: ${cache[id]}:${cache[id]::class.simpleName}")
// @Suppress("UNCHECKED_CAST")
cache[id] as T
}
}
suspend fun decodeAny(scope: Scope): Obj = decodeCached {
val type = LynonType.entries[bin.getBits(4).toInt()]
if (type != LynonType.Other) {
type.objClass.deserialize(scope, this, type)
} else {
decodeClassObj(scope).deserialize(scope, this, null)
}
}
private suspend fun decodeClassObj(scope: Scope): ObjClass {
val className = decodeObject(scope, ObjString.type, null) as ObjString
println("expected class name $className")
return scope.get(className.value)?.value?.let {
if (it !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
it
} ?: scope.raiseSymbolNotFound("can't deserialize: not found type $className")
}
suspend fun decodeAnyList(scope: Scope,fixedSize: Int?=null): MutableList<Obj> {
return if (bin.getBit() == 1) {
// homogenous
val type = LynonType.entries[getBitsAsInt(4)]
val list = mutableListOf<Obj>()
val objClass = if (type == LynonType.Other)
decodeClassObj(scope).also { println("detected class obj: $it") }
else type.objClass
val size = fixedSize ?: bin.unpackUnsigned().toInt()
println("detected homogenous list type $type, $size items")
for (i in 0..<size) {
list += decodeObject(scope, objClass, type).also {
println("decoded: $it")
}
}
list
} else {
val size = fixedSize ?: unpackUnsigned().toInt()
(0..<size).map { decodeAny(scope) }.toMutableList()
}
}
suspend fun decodeObject(scope: Scope, type: ObjClass, overrideType: LynonType? = null): Obj {
return decodeCached { type.deserialize(scope, this, overrideType) }
}
fun unpackBinaryData(): ByteArray = bin.decompress()
@Suppress("unused")
fun unpackBinaryDataOrNull(): ByteArray? = bin.decompressOrNull()
fun unpackBoolean(): Boolean {
return bin.getBit() == 1
}
fun unpackDouble(): Double {
return Double.fromBits(bin.getBits(64).toLong())
}
fun unpackSigned(): Long {
return bin.unpackSigned()
}
fun unpackUnsigned(): ULong {
return bin.unpackUnsigned()
}
}
@@ -0,0 +1,194 @@
package net.sergeych.lynon
import net.sergeych.bintools.ByteChunk
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.*
enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
Null(ObjNull.objClass, 80),
Int0(ObjInt.type, 70),
IntNegative(ObjInt.type, 50),
IntPositive(ObjInt.type, 100),
IntSigned(ObjInt.type, 30),
String(ObjString.type, 100),
Real(ObjReal.type),
Bool(ObjBool.type, 80),
List(ObjList.type, 70),
Map(ObjMap.type, 40),
Set(ObjSet.type),
Buffer(ObjBuffer.type, 50),
Instant(ObjInstant.type, 30),
Duration(ObjDuration.type),
Other(Obj.rootObjectType, 60);
fun generalizeTo(other: LynonType): LynonType? {
if (this == other) return this
return (if (this.isInt && other.isInt) {
when {
this == Int0 -> other // upgrade 0 to some other int
other == Int0 -> this // 0 is member of our class, ignore
// different signum propagate to signed
else -> IntSigned
}
} else
// impossible to generalize
null
).also { println("Gen $this + $other -> $it") }
}
val isInt by lazy {
when (this) {
Int0, IntSigned, IntPositive, IntNegative -> true
else -> false
}
}
}
open class LynonEncoder(val bout: BitOutput, val settings: LynonSettings = LynonSettings.default) {
val cache = mutableMapOf<Any, Int>()
suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) {
suspend fun serializeAndCache(key: Any = item) {
cache[key]?.let { cacheId ->
// println("encode: Cache hit: ${cacheId}: $item: ${item::class.simpleName}")
val size = sizeInBits(cache.size)
bout.putBit(1)
bout.putBits(cacheId.toULong(), size)
} ?: run {
bout.putBit(0)
if (settings.shouldCache(item)) {
// println("encode add cache: ${cache.size}: $item: ${item::class.simpleName}")
packer()
cache[key] = cache.size
} else {
// println("encode but not cache $item")
packer()
}
}
}
when (item) {
is ByteArray -> serializeAndCache(ByteChunk(item.asUByteArray()))
is UByteArray -> serializeAndCache(ByteChunk(item))
else -> serializeAndCache(item)
}
}
/**
* Encode any Lyng object [Obj], which can be serialized, using type record. This allow to
* encode any object with the overhead of type record.
*
* Caching is used automatically.
*/
suspend fun encodeAny(scope: Scope, obj: Obj) {
encodeCached(obj) {
val type = putTypeRecord(scope, obj, obj.lynonType())
obj.serialize(scope, this, type)
}
}
private suspend fun putTypeRecord(scope: Scope, obj: Obj, type: LynonType): LynonType {
putType(type)
if( type == LynonType.Other) {
encodeObject(scope, obj.objClass.classNameObj)
}
return type
}
private fun putType(type: LynonType) {
bout.putBits(type.ordinal.toULong(), 4)
}
/**
* AnyList could be homogenous (first bit=1) and heterogeneous (first bit=0). Homogenous list
* has a single type record that precedes the list, heterogeneous hash typed record
* for each item.
*
*/
suspend fun encodeAnyList(scope: Scope, list: List<Obj>,fixedSize: Boolean = false) {
if( list.isEmpty()) {
// any-typed, empty, save space
putBit(0)
encodeUnsigned(0u)
}
else {
val objClass = list[0].objClass
var type = list[0].lynonType()
var isHomogeneous = true
for (i in list.drop(1))
if (i.objClass != objClass) {
isHomogeneous = false
break
} else {
// same class but type might need generalization
type = type.generalizeTo(i.lynonType())
?: scope.raiseError("inner error: can't generalize lynon type $type to ${i.lynonType()}")
}
if (isHomogeneous) {
putBit(1)
putTypeRecord(scope, list[0], type)
if (!fixedSize)
encodeUnsigned(list.size.toULong())
for (i in list) encodeObject(scope, i, type)
} else {
putBit(0)
if (!fixedSize)
encodeUnsigned(list.size.toULong())
for (i in list) encodeAny(scope, i)
}
}
}
/**
* Write object _with no type record_: type is known
*/
suspend fun encodeObject(scope: Scope, obj: Obj,overrideType: LynonType? = null) {
encodeCached(obj) {
obj.serialize(scope, this, overrideType)
}
}
suspend fun encodeCachedBytes(bytes: ByteArray) {
encodeCached(bytes) {
bout.compress(bytes)
}
}
fun encodeBinaryData(data: ByteArray) {
bout.compress(data)
}
fun encodeSigned(value: Long) {
bout.packSigned(value)
}
@Suppress("unused")
fun encodeUnsigned(value: ULong) {
bout.packUnsigned(value)
}
@Suppress("unused")
fun encodeBool(value: Boolean) {
bout.putBit(if (value) 1 else 0)
}
fun encodeReal(value: Double) {
bout.putBits(value.toRawBits().toULong(), 64)
}
fun encodeBoolean(value: Boolean) {
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)
}
}
@@ -0,0 +1,29 @@
package net.sergeych.lynon
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjChar
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
import kotlin.math.absoluteValue
open class LynonSettings {
enum class InstantTruncateMode {
Second,
Millisecond,
Microsecond
}
open fun shouldCache(obj: Any): Boolean = when (obj) {
is ObjChar -> false
is ObjInt -> obj.value.absoluteValue > 0x10000FF
is ObjBool -> false
is ObjNull -> false
is ByteArray -> obj.size > 2
is UByteArray -> obj.size > 2
else -> true
}
companion object {
val default = LynonSettings()
}
}
@@ -0,0 +1,40 @@
package net.sergeych.lynon
class MemoryBitInput(val packedBits: UByteArray, val lastByteBits: Int) : BitInput {
constructor(ba: BitArray) : this(ba.bytes, ba.lastByteBits) {}
constructor(mba: MemoryBitOutput) : this(mba.toBitArray()) {}
private var index = 0
private var isEndOfStream: Boolean = packedBits.isEmpty() || (packedBits.size == 1 && lastByteBits == 0)
private set
/**
* Return next byte, int in 0..255 range, or -1 if end of stream reached
*/
private var accumulator = if( isEndOfStream ) 0 else packedBits[0].toInt()
private var bitCounter = 0
override fun getBitOrNull(): Int? {
if (isEndOfStream) return null
val result = accumulator and 1
accumulator = accumulator shr 1
bitCounter++
// is end?
if( index == packedBits.lastIndex && bitCounter == lastByteBits ) {
isEndOfStream = true
}
else {
if( bitCounter == 8 ) {
bitCounter = 0
accumulator = packedBits[++index].toInt()
}
}
return result
}
}
@@ -0,0 +1,157 @@
package net.sergeych.lynon
import kotlin.math.min
/**
* BitList implementation as fixed suze array of bits; indexing works exactly same as if
* [MemoryBitInput] is used with [MemoryBitInput.getBit]. See [MemoryBitOutput] for
* bits order and more information.
*/
class BitArray(val bytes: UByteArray, val lastByteBits: Int) : BitList {
val bytesSize: Int get() = bytes.size
override val size by lazy { bytes.size * 8L - (8 - lastByteBits) }
override val indices by lazy { 0..<size }
/**
* @return [BitInput] that can be used to read from this array
*/
fun toBitInput(): BitInput = MemoryBitInput(bytes, lastByteBits)
private fun getIndexAndMask(bitIndex: Long): Pair<Int, Int> {
val byteIndex = (bitIndex / 8).toInt()
if (byteIndex !in bytes.indices)
throw IndexOutOfBoundsException("$bitIndex is out of bounds")
val i = (bitIndex % 8).toInt()
if (byteIndex == bytes.lastIndex && i >= lastByteBits)
throw IndexOutOfBoundsException("$bitIndex is out of bounds (last)")
return byteIndex to (1 shl i)
}
override operator fun get(bitIndex: Long): Int =
getIndexAndMask(bitIndex).let { (byteIndex, mask) ->
if (bytes[byteIndex].toInt() and mask == 0) 0 else 1
}
override operator fun set(bitIndex: Long, value: Int) {
require(value == 0 || value == 1)
val (byteIndex, mask) = getIndexAndMask(bitIndex)
if (value == 1)
bytes[byteIndex] = bytes[byteIndex] or mask.toUByte()
else
bytes[byteIndex] = bytes[byteIndex] and mask.inv().toUByte()
}
override fun toString(): String {
val result = StringBuilder()
val s = min(size, 64)
for (i in 0..<s) result.append(this[i])
if (s < size) result.append("…")
return result.toString()
}
@Suppress("unused")
fun asByteArray(): ByteArray = bytes.asByteArray()
@Suppress("unused")
fun asUbyteArray(): UByteArray = bytes
companion object {
fun withBitSize(size: Long): BitArray {
val byteSize = ((size + 7) / 8).toInt()
val lastByteBits = size % 8
return BitArray(UByteArray(byteSize), lastByteBits.toInt())
}
fun ofBits(vararg bits: Int): BitArray {
return withBitSize(bits.size.toLong()).apply {
for (i in bits.indices) {
this[i.toLong()] = bits[i]
}
}
}
}
}
/**
* [BitOutput] implementation that writes to a memory buffer, LSB first.
*
* Bits are stored in the least significant bits of the bytes. E.g. the first 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.
*
* This allows automatic fill of the last byte with zeros. This is important when
* using bytes stored from [asByteArray] or [asUbyteArray]. When converting to
* bytes, automatic padding to byte size is applied. With such bit order, constructing
* [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]
* stores exact number of bits and [BitArray.toBitInput] provides [BitInput] that
* decodes exactly same bits.
*
*/
class MemoryBitOutput : BitOutput {
private val buffer = mutableListOf<UByte>()
private var accumulator = 0
private var mask = 1
override fun putBit(bit: Int) {
when (bit) {
0 -> {}
1 -> accumulator = accumulator or mask
else -> throw IllegalArgumentException("Bit must be 0 or 1")
}
mask = mask shl 1
if(mask == 0x100) {
mask = 1
outputByte(accumulator.toUByte())
accumulator = accumulator shr 8
}
}
var isClosed = false
private set
fun close(): BitArray {
if (!isClosed) {
if (mask != 0x01) {
outputByte(accumulator.toUByte())
}
isClosed = true
}
return toBitArray()
}
fun lastBits(): Int {
check(isClosed)
return when(mask) {
0x01 -> 8 // means that all bits of the last byte are in use
0x02 -> 1
0x04 -> 2
0x08 -> 3
0x10 -> 4
0x20 -> 5
0x40 -> 6
0x80 -> 7
else -> throw IllegalStateException("Invalid state, mask=${mask.toString(16)}")
}
}
fun toBitArray(): BitArray {
if (!isClosed) {
close()
}
return BitArray(buffer.toTypedArray().toUByteArray(), lastBits())
}
fun toBitInput(): BitInput = toBitArray().toBitInput()
private fun outputByte(byte: UByte) {
buffer.add(byte)
}
}
@@ -0,0 +1,71 @@
package net.sergeych.lynon
/**
* Bit size-aware code, short [BitList] implementation, up to 64 bits (efficiency tradeoff).
* E.g `Bits(0, 3) != Bits(0, 2). For longer, use [BitArray].
*
* Note that [bitListOf] creates [TinyBits] when possible.
*/
class TinyBits(initValue: ULong = 0U, override val size: Long = 0): BitList {
private var bits: ULong = initValue
constructor(value: ULong, size: Int): this(value, size.toLong()) {}
override val indices: LongRange by lazy { 0..<size }
override operator fun get(bitIndex: Long): Int {
if( bitIndex !in indices) throw IndexOutOfBoundsException("index out of bounds: $bitIndex")
val mask = 1UL shl (size - bitIndex - 1).toInt()
return if (bits and mask != 0UL) 1 else 0
}
override fun set(bitIndex: Long, value: Int) {
val mask = 1UL shl (size - bitIndex - 1).toInt()
if( value == 1)
bits = bits or mask
else
bits = bits and mask.inv()
}
override fun toString(): String {
val result = StringBuilder()
for (i in 0..<size) result.append(this[i])
return result.toString()
}
val value by ::bits
/**
* Add bit shifting value to the left and return _new instance_
*/
fun insertBit(bit: Int): TinyBits {
return TinyBits((bits shl 1) or bit.toULong(), size + 1)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as TinyBits
if (size != other.size) return false
if (bits != other.bits) return false
return true
}
override fun hashCode(): Int {
var result = size.hashCode()
result = 31 * result + bits.hashCode()
return result
}
companion object {
fun of(vararg bits: Int): TinyBits {
return TinyBits(0UL, bits.size).apply { bits.forEachIndexed { i, v -> this[i.toLong()] = v } }
}
}
}
@@ -0,0 +1,35 @@
package net.sergeych.lynon
/**
* Hoq many tetrades needed to store the value. It is faster to use this function
* than to use sizeInBits
*
* Size for 0 is 1
*/
fun sizeInTetrades(value: ULong): Int {
if( value == 0UL ) return 1
var size = 0
var rest = value
while( rest != 0UL ) {
size++
rest = rest shr 4
}
return size
}
/**
* How many bits needed to store the value. Size for 0 is 1,
*/
@Suppress("unused")
fun sizeInBits(value: ULong): Int {
if( value == 0UL ) return 1
var size = 0
var rest = value
while( rest != 0UL ) {
size++
rest = rest shr 1
}
return size
}
fun sizeInBits(value: Int): Int = sizeInBits(value.toULong())
@@ -0,0 +1,274 @@
package net.sergeych.lynon
import net.sergeych.collections.SortedList
import net.sergeych.lynon.Huffman.Alphabet
/**
* Generic huffman encoding implementation using bits input/output and abstract [Alphabet].
*/
object Huffman {
/**
* Alphabet interface: source can be variable bit size codes, not just bytes,
* so the Huffman encoding is not limited to bytes. It works with any alphabet
* using its _ordinals_; encoding between source symbols and ordinals are
* performed by the alphabet. See [byteAlphabet] for example.
*/
interface Alphabet<T> {
val maxOrdinal: Int
/**
* Write correct symbol for the [ordinal] to the [bout]. This is
* the inverse of [ordinalOf] but as [T] could be variable bit size,
* we provide output bit stream.
*/
fun decodeOrdinalTo(bout: BitOutput, ordinal: Int)
/**
* Find the ordinal of the source symbol
*/
fun ordinalOf(value: T): Int
operator fun get(ordinal: Int): T
}
/**
* Alphabet for unsigned bytes, allows to encode bytes easily
*/
val byteAlphabet = object : Alphabet<UByte> {
override val maxOrdinal: Int
get() = 256
override fun decodeOrdinalTo(bout: BitOutput, ordinal: Int) {
bout.putBits(ordinal, 8)
}
override fun ordinalOf(value: UByte): Int = value.toInt()
override operator fun get(ordinal: Int): UByte = ordinal.toUByte()
}
sealed class Node(val freq: Int) : Comparable<Node> {
override fun compareTo(other: Node): Int {
return freq.compareTo(other.freq)
}
abstract fun decodeOrdinal(bin: BitInput): Int?
class Leaf(val ordinal: Int, freq: Int) : Node(freq) {
override fun toString(): String {
return "[$ordinal:$freq]"
}
override fun decodeOrdinal(bin: BitInput): Int {
return ordinal//.also { println(": ${Char(value)}") }
}
}
class Internal(val left: Node, val right: Node) : Node(left.freq + right.freq) {
override fun toString(): String {
return "[${left.freq}<- :<$freq>: ->${right.freq}]"
}
override fun decodeOrdinal(bin: BitInput): Int? {
return when (bin.getBitOrNull().also { print("$it") }) {
1 -> left.decodeOrdinal(bin)
0 -> right.decodeOrdinal(bin)
else -> null
}
}
}
}
data class Code(val ordinal: Int, val bits: TinyBits) {
val size by bits::size
override fun toString(): String {
return "[$ordinal:$size:$bits]"
}
}
private fun generateCanonicCodes(tree: Node, alphabet: Alphabet<*>): List<Code?> {
val codes = MutableList<Code?>(alphabet.maxOrdinal) { null }
fun traverse(node: Node, code: TinyBits) {
when (node) {
is Node.Leaf ->
codes[node.ordinal] = (Code(node.ordinal, code))
is Node.Internal -> {
traverse(node.left, code.insertBit(1))
traverse(node.right, code.insertBit(0))
}
}
}
traverse(tree, TinyBits())
return makeCanonical(codes, alphabet)
}
private fun makeCanonical(source: List<Code?>,alphabet: Alphabet<*>): List<Code?> {
val sorted = source.filterNotNull().sortedWith(canonicComparator)
val canonical = MutableList<Code?>(alphabet.maxOrdinal) { null }
val first = sorted[0]
val prevValue = first.copy(bits = TinyBits(0UL, first.bits.size))
canonical[first.ordinal] = prevValue
var prev = prevValue.bits
for (i in 1..<sorted.size) {
var bits = TinyBits(prev.value + 1U, prev.size)
val code = sorted[i]
while (code.bits.size > bits.size) {
bits = bits.insertBit(0)
}
canonical[code.ordinal] = code.copy(bits = bits)//.also { println("$it") }
prev = bits
}
return canonical
}
private val canonicComparator = { a: Code, b: Code ->
if (a.bits.size == b.bits.size) {
a.ordinal.compareTo(b.ordinal)
} else {
a.bits.size.compareTo(b.bits.size)
}
}
private fun buildTree(data: Iterable<Int>,alphabet: Alphabet<*>): Node {
val frequencies = buildFrequencies(alphabet, data)
return buildTree(frequencies)
}
private fun buildTree(frequencies: Array<Int>): Node {
// println(data.toDump())
val list: SortedList<Node> = SortedList(*frequencies.mapIndexed { index, frequency -> Node.Leaf(index, frequency) }.filter { it.freq > 0 }
.toTypedArray())
// build the tree
while (list.size > 1) {
val left = list.removeAt(0)
val right = list.removeAt(0)
list.add(Node.Internal(left, right))
}
return list[0]
}
private fun buildFrequencies(
alphabet: Alphabet<*>,
data: Iterable<Int>
): Array<Int> {
val maxOrdinal = alphabet.maxOrdinal
val frequencies = Array(maxOrdinal) { 0 }
data.forEach { frequencies[it]++ }
return frequencies
}
fun decompressUsingCodes(bin: BitInput, codes: List<Code?>, alphabet: Alphabet<*>): BitArray {
val result = MemoryBitOutput()
val table = codes.filterNotNull().associateBy { it.bits }
outer@ while (true) {
var input = TinyBits()
while (true) {
bin.getBitOrNull()?.let { input = input.insertBit(it) }
?: break@outer
val data = table[input]
if (data != null) {
// println("Code found: ${data.bits} -> [${data.symbol.toChar()}]")
alphabet.decodeOrdinalTo(result,data.ordinal)
break
}
}
}
return result.toBitArray()
}
private fun serializeCanonicCodes(bout: BitOutput, codes: List<Code?>) {
var minSize: Int? = null
var maxSize: Int? = null
for (i in 1..<codes.size) {
val s = codes[i]?.size?.toInt() ?: continue
if (minSize == null || s < minSize) minSize = s
if (maxSize == null || s > maxSize) maxSize = s
}
val size = maxSize!! - minSize!! + 1
val sizeInBits = sizeInBits(size)
bout.packUnsigned(minSize.toULong())
bout.packUnsigned(sizeInBits.toULong())
for (c in codes) {
if (c != null)
bout.putBits(c.bits.size.toInt() - minSize + 1, sizeInBits)
else
bout.putBits(0, sizeInBits)
}
}
fun deserializeCanonicCodes(bin: BitInput, alphabet: Alphabet<*>): List<Code?> {
val minSize = bin.unpackUnsigned().toInt()
val sizeInBits = bin.unpackUnsigned().toInt()
val sorted = mutableListOf<Code>().also { codes ->
for (i in 0..<alphabet.maxOrdinal) {
val s = bin.getBits(sizeInBits).toInt()
if (s > 0) {
codes.add(Code(i, TinyBits(0U, s - 1 + minSize)))
}
}
}.sortedWith(canonicComparator)
val result = MutableList<Code?>(alphabet.maxOrdinal) { null }
var prev = sorted[0].copy(bits = TinyBits(0U, sorted[0].bits.size))
result[prev.ordinal] = prev
for (i in 1..<sorted.size) {
val code = sorted[i]
var bits = TinyBits(prev.bits.value + 1u, prev.bits.size)
while (bits.size < code.bits.size) bits = bits.insertBit(0)
result[code.ordinal] = code.copy(bits = bits).also {
prev = it
}
}
return result
}
// fun generateCanonicalCodes(frequencies: Iterable<Int>): List<Code?> {
//
// }
fun generateCanonicalCodes(frequencies: Array<Int>,alphabet: Alphabet<*>): List<Code?> =
generateCanonicCodes(buildTree(frequencies), alphabet)
fun <T>compress(plain: Iterable<T>,alphabet: Alphabet<T>): BitArray {
val source = plain.map { alphabet.ordinalOf(it) }
val root = buildTree(source,alphabet)
val codes = generateCanonicCodes(root, alphabet)
// serializa table
// test encode:
val bout = MemoryBitOutput()
serializeCanonicCodes(bout, codes)
for (i in source) {
val code = codes[i]!!
// println(">> $code")
bout.putBits(code.bits)
}
// println(bout.toBitArray().bytes.toDump())
val compressed = bout.toBitArray()
return compressed
}
fun <T>decompress(bin: BitInput,alphabet: Alphabet<T>): UByteArray {
val codes = deserializeCanonicCodes(bin, alphabet)
return decompressUsingCodes(bin, codes, alphabet).asUbyteArray()
}
}
@@ -0,0 +1,128 @@
package net.sergeych.lynon
import net.sergeych.bintools.ByteChunk
import kotlin.math.roundToInt
/**
* LZW lightweight pure kotlin compression.
*/
object LZW {
val MAX_CODE_SIZE = 17
val STOP_CODE = (1 shl MAX_CODE_SIZE) - 1
val MAX_DICT_SIZE = (STOP_CODE * 0.92).roundToInt()
fun compress(input: ByteArray, bitOutput: BitOutput)
= compress(input.asUByteArray(), bitOutput)
/**
* Compresses the input string using LZW algorithm
* @param input The string to compress
* @return List of compressed codes
*/
fun compress(input: UByteArray, bitOutput: BitOutput) {
// Initialize dictionary with all possible single characters
val dictionary = mutableMapOf<ByteChunk, Int>()
for (i in 0..255) {
// 23
dictionary[ByteChunk(ubyteArrayOf(i.toUByte()))] = i
}
var nextCode = 256
var current = ByteChunk(ubyteArrayOf())
// val result = mutableListOf<Int>()
for (char in input) {
val combined = current + char
if (dictionary.containsKey(combined)) {
current = combined
} else {
val size = sizeInBits(dictionary.size)
bitOutput.putBits(dictionary[current]!!, size)
if (dictionary.size >= MAX_DICT_SIZE) {
bitOutput.putBits(STOP_CODE, size)
dictionary.clear()
nextCode = 256
for (i in 0..255) {
dictionary[ByteChunk(ubyteArrayOf(i.toUByte()))] = i
}
} else
dictionary[combined] = nextCode++
current = ByteChunk(ubyteArrayOf(char))
}
}
if (current.size > 0) {
val size = sizeInBits(dictionary.size)
bitOutput.putBits(dictionary[current]!!, size)
}
}
fun compress(input: UByteArray): BitArray {
return MemoryBitOutput().apply {
compress(input, this)
}.toBitArray()
}
/**
* Decompresses a list of LZW codes back to the original string. Note that usage of apriori existing
* size is crucial: it let repeal explosion style attacks.
*
* @param compressed The list of compressed codes
* @param resultSize The expected size of the decompressed string
*
* @throws DecompressionException if something goes wrong
* @return The decompressed string
*/
fun decompress(compressed: BitInput, resultSize: Int): UByteArray {
// Initialize dictionary with all possible single characters
val dictionary = mutableMapOf<Int, UByteArray>()
for (i in 0..255) {
dictionary[i] = ubyteArrayOf(i.toUByte())
}
var nextCode = 256
val firstCode = compressed.getBits(9).toInt()
var previous = dictionary[firstCode]
?: throw DecompressionException("Invalid first compressed code: $firstCode")
val result = mutableListOf<UByte>()
result += previous
while (result.size < resultSize) {
val codeSize = sizeInBits(nextCode + 1)
val code = compressed.getBitsOrNull(codeSize)?.toInt() ?: break
if (code == STOP_CODE) {
nextCode = 256
dictionary.clear()
for (i in 0..255)
dictionary[i] = ubyteArrayOf(i.toUByte())
previous = dictionary[compressed.getBits(9).toInt()]!!
} else {
val current = if (code in dictionary) {
dictionary[code]!!
} else if (code == nextCode) {
// Special case for pattern like cScSc
previous + previous[0]
} else {
throw DecompressionException("Invalid compressed code: $code")
}
result += current
dictionary[nextCode++] = previous + current[0]
previous = current
}
}
if (result.size != resultSize)
throw DecompressionException("Decompressed size is not equal to expected: real/expected = ${result.size}/$resultSize")
return result.toTypedArray().toUByteArray()
}
}
private operator fun ByteChunk.plus(byte: UByte): ByteChunk {
return ByteChunk(data + byte)
}
@@ -0,0 +1,36 @@
package net.sergeych.lynon
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjBitBuffer
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjString
// Most often used types:
val ObjLynonClass = object : ObjClass("Lynon") {
suspend fun Scope.encodeAny(obj: Obj): Obj {
val bout = MemoryBitOutput()
val serializer = LynonEncoder(bout)
serializer.encodeAny(this, obj)
return ObjBitBuffer(bout.toBitArray())
}
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)
}
}.apply {
addClassConst("test", ObjString("test_const"))
addClassFn("encode") {
encodeAny(requireOnlyArg<Obj>())
}
addClassFn("decode") {
decodeAny(requireOnlyArg<Obj>())
}
}
@@ -0,0 +1,15 @@
package net.sergeych.lynon
/**
* Variant of [LynonEncoder] that writes to embedded [MemoryBitOutput]
*/
class LynonPacker(bout: MemoryBitOutput = MemoryBitOutput(), settings: LynonSettings = LynonSettings.default)
: LynonEncoder(bout, settings) {
}
/**
* Variant of [LynonDecoder] that reads from a given `source` using [MemoryBitInput]
*/
class LynonUnpacker(source: BitInput) : LynonDecoder(source) {
constructor(packer: LynonPacker) : this((packer.bout as MemoryBitOutput).toBitInput())
}
@@ -0,0 +1,66 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class TestCoroutines {
@Test
fun testLaunch() = runTest {
eval(
"""
var passed = false
val x = launch {
delay(10)
passed = true
"ok"
}
assert(!passed)
assertEquals( x.await(), "ok")
assert(passed)
assert(x.isCompleted)
""".trimIndent()
)
}
@Test
fun testCompletableDeferred() = runTest {
eval(
"""
val done = CompletableDeferred()
launch {
delay(10)
done.complete("ok")
}
assert(!done.isCompleted)
assert(done.isActive)
assertEquals( done.await(), "ok")
assert(done.isCompleted)
""".trimIndent()
)
}
@Test
fun testMutex() = runTest {
eval(
"""
var counter = 0
val mutex = Mutex()
(1..4).map {
launch {
// mutex.withLock {
val c = counter
delay(5)
counter = c + 1
// }
}
}.forEach { it.await() }
println(counter)
assert( counter < 10 )
""".trimIndent()
)
}
}
+46
View File
@@ -0,0 +1,46 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class OOTest {
@Test
fun testClassProps() = runTest {
eval("""
import lyng.time
class Point(x,y) {
static val origin = Point(0,0)
static var center = origin
}
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(0,0), Point.center)
Point.center = Point(1,2)
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(1,2), Point.center)
""".trimIndent())
}
@Test
fun testClassMethods() = runTest {
eval("""
import lyng.time
class Point(x,y) {
private static var data = null
static fun getData() { data }
static fun setData(value) {
data = value
callFrom()
}
static fun callFrom() {
data = data + "!"
}
}
assertEquals(Point(0,0), Point(0,0) )
assertEquals(null, Point.getData() )
Point.setData("foo")
assertEquals( "foo!", Point.getData() )
""".trimIndent())
}
}
+337 -37
View File
@@ -1,8 +1,10 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
import kotlin.test.* import kotlin.test.*
class ScriptTest { class ScriptTest {
@@ -604,7 +606,7 @@ class ScriptTest {
} }
@Test @Test
fun testAssignArgumentsmiddleEllipsis() = runTest { fun testAssignArgumentsMiddleEllipsis() = runTest {
val ttEnd = Token.Type.RBRACE val ttEnd = Token.Type.RBRACE
val pa = ArgsDeclaration( val pa = ArgsDeclaration(
listOf( listOf(
@@ -706,19 +708,13 @@ class ScriptTest {
var t1 = 10 var t1 = 10
outer@ while( t1 > 0 ) { outer@ while( t1 > 0 ) {
var t2 = 10 var t2 = 10
println("starting t2 = " + t2)
while( t2 > 0 ) { while( t2 > 0 ) {
t2 = t2 - 1 t2 = t2 - 1
println("t2 " + t2 + " t1 " + t1)
if( t2 == 3 && t1 == 7) { if( t2 == 3 && t1 == 7) {
println("will break")
break@outer "ok2:"+t2+":"+t1 break@outer "ok2:"+t2+":"+t1
} }
} }
println("next t1") --t1
t1 = t1 - 1
println("t1 now "+t1)
t1
} }
""".trimIndent() """.trimIndent()
).toString() ).toString()
@@ -733,8 +729,6 @@ class ScriptTest {
""" """
val count = 3 val count = 3
val res = if( count > 10 ) "too much" else "just " + count val res = if( count > 10 ) "too much" else "just " + count
println(count)
println(res)
res res
""".trimIndent() """.trimIndent()
) )
@@ -770,7 +764,7 @@ class ScriptTest {
fun testDecr() = runTest { fun testDecr() = runTest {
val c = Scope() val c = Scope()
c.eval("var x = 9") c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt()) assertEquals(9, c.eval("println(x); val a = x--; println(x); println(a); a").toInt())
assertEquals(8, c.eval("x--").toInt()) assertEquals(8, c.eval("x--").toInt())
assertEquals(7, c.eval("x--").toInt()) assertEquals(7, c.eval("x--").toInt())
assertEquals(6, c.eval("x--").toInt()) assertEquals(6, c.eval("x--").toInt())
@@ -2249,19 +2243,30 @@ class ScriptTest {
} }
@Test @Test
fun testLet() = runTest { fun testSet2() = runTest {
eval(""" eval("""
assertEquals( Set( ...[1,2,3]), Set(1,2,3) )
assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) )
""".trimIndent())
}
@Test
fun testLet() = runTest {
eval(
"""
class Point(x=0,y=0) class Point(x=0,y=0)
assert( Point() is Object) assert( Point() is Object)
Point().let { println(it.x, it.y) } Point().let { println(it.x, it.y) }
val x = null val x = null
x?.let { println(it.x, it.y) } x?.let { println(it.x, it.y) }
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testApply() = runTest { fun testApply() = runTest {
eval(""" eval(
"""
class Point(x,y) class Point(x,y)
// see the difference: apply changes this to newly created Point: // see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply { val p = Point(1,2).apply {
@@ -2270,12 +2275,14 @@ class ScriptTest {
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void >>> void
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testApplyThis() = runTest { fun testApplyThis() = runTest {
eval(""" eval(
"""
class Point(x,y) class Point(x,y)
// see the difference: apply changes this to newly created Point: // see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply { val p = Point(1,2).apply {
@@ -2284,12 +2291,14 @@ class ScriptTest {
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void >>> void
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testExtend() = runTest() { fun testExtend() = runTest() {
eval(""" eval(
"""
fun Int.isEven() { fun Int.isEven() {
this % 2 == 0 this % 2 == 0
@@ -2311,7 +2320,8 @@ class ScriptTest {
assert( 12.1.isInteger() == false ) assert( 12.1.isInteger() == false )
assert( "5".isInteger() ) assert( "5".isInteger() )
assert( ! "5.2".isInteger() ) assert( ! "5.2".isInteger() )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@@ -2319,33 +2329,35 @@ class ScriptTest {
val c = Scope() val c = Scope()
val arr = c.eval("[1,2,3]") val arr = c.eval("[1,2,3]")
// array is iterable so we can: // array is iterable so we can:
assertEquals(listOf(1,2,3), arr.toFlow(c).map { it.toInt() }.toList()) assertEquals(listOf(1, 2, 3), arr.toFlow(c).map { it.toInt() }.toList())
} }
@Test @Test
fun testAssociateBy() = runTest() { fun testAssociateBy() = runTest() {
eval(""" eval(
"""
val m = [123, 456].associateBy { "k:%s"(it) } val m = [123, 456].associateBy { "k:%s"(it) }
println(m) println(m)
assertEquals(123, m["k:123"]) assertEquals(123, m["k:123"])
assertEquals(456, m["k:456"]) assertEquals(456, m["k:456"])
""") """
listOf(1,2,3).associateBy { it * 10 } )
listOf(1, 2, 3).associateBy { it * 10 }
} }
@Test // @Test
fun testImports1() = runTest() { // fun testImports1() = runTest() {
val foosrc = """ // val foosrc = """
package lyng.foo // package lyng.foo
//
fun foo() { "foo1" } // fun foo() { "foo1" }
""".trimIndent() // """.trimIndent()
val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc))) // val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc)))
assertNotNull(pm.modules.await()["lyng.foo"]) // assertNotNull(pm.modules["lyng.foo"])
assertIs<ModuleScope>(pm.modules.await()["lyng.foo"]!!.await()) // assertIs<ModuleScope>(pm.modules["lyng.foo"]!!.deferredModule.await())
assertEquals("foo1", pm.modules.await()["lyng.foo"]!!.await().eval("foo()").toString()) // assertEquals("foo1", pm.modules["lyng.foo"]!!.deferredModule.await().eval("foo()").toString())
} // }
@Test @Test
fun testImports2() = runTest() { fun testImports2() = runTest() {
@@ -2354,7 +2366,7 @@ class ScriptTest {
fun foo() { "foo1" } fun foo() { "foo1" }
""".trimIndent() """.trimIndent()
val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc))) val pm = InlineSourcesImportProvider(listOf(Source("foosrc", foosrc)))
val src = """ val src = """
import lyng.foo import lyng.foo
@@ -2365,4 +2377,292 @@ class ScriptTest {
val scope = ModuleScope(pm, src) val scope = ModuleScope(pm, src)
assertEquals("foo1", scope.eval(src).toString()) assertEquals("foo1", scope.eval(src).toString())
} }
@Test
fun testImports3() = runTest {
val foosrc = """
package lyng.foo
import lyng.bar
fun foo() { "foo1" }
""".trimIndent()
val barsrc = """
package lyng.bar
fun bar() { "bar1" }
""".trimIndent()
val pm = InlineSourcesImportProvider(
listOf(
Source("barsrc", barsrc),
Source("foosrc", foosrc),
)
)
val src = """
import lyng.foo
foo() + " / " + bar()
""".trimIndent().toSource("test")
val scope = ModuleScope(pm, src)
assertEquals("foo1 / bar1", scope.eval(src).toString())
}
@Test
fun testImportsCircular() = runTest {
val foosrc = """
package lyng.foo
import lyng.bar
fun foo() { "foo1" }
""".trimIndent()
val barsrc = """
package lyng.bar
import lyng.foo
fun bar() { "bar1" }
""".trimIndent()
val pm = InlineSourcesImportProvider(
listOf(
Source("barsrc", barsrc),
Source("foosrc", foosrc),
)
)
val src = """
import lyng.bar
foo() + " / " + bar()
""".trimIndent().toSource("test")
val scope = ModuleScope(pm, src)
assertEquals("foo1 / bar1", scope.eval(src).toString())
}
@Test
fun testDefaultImportManager() = runTest {
val scope = Scope.new()
assertFails {
scope.eval("""
import foo
foo()
""".trimIndent())
}
scope.importManager.addTextPackages("""
package foo
fun foo() { "bar" }
""".trimIndent())
scope.eval("""
import foo
assertEquals( "bar", foo())
""".trimIndent())
}
@Test
fun testMaps() = runTest {
eval(
"""
val map = Map( "a" => 1, "b" => 2 )
assertEquals( 1, map["a"] )
assertEquals( 2, map["b"] )
assertEquals( null, map["c"] )
map["c"] = 3
assertEquals( 3, map["c"] )
""".trimIndent()
)
}
@Test
fun testBuffer() = runTest {
eval("""
import lyng.buffer
assertEquals( 0, Buffer().size )
assertEquals( 3, Buffer(1, 2, 3).size )
assertEquals( 5, Buffer("hello").size )
var buffer = Buffer("Hello")
assertEquals( 5, buffer.size)
assertEquals('l'.code, buffer[2] )
assertEquals('l'.code, buffer[3] )
assertEquals("Hello", buffer.decodeUtf8())
buffer = buffer.toMutable()
buffer[2] = 101
assertEquals(101, buffer[2])
assertEquals("Heelo", buffer.decodeUtf8())
""".trimIndent())
}
@Test
fun testBufferCompare() = runTest {
eval("""
import lyng.buffer
println("Hello".characters())
val b1 = Buffer("Hello")
val b2 = Buffer("Hello".characters())
assertEquals( b1, b2 )
val b3 = b1 + Buffer("!")
assertEquals( "Hello!", b3.decodeUtf8())
assert( b3 > b1 )
assert( b1 !== b2)
val map = Map( b1 => "foo")
assertEquals("foo", map[b1])
assertEquals("foo", map[b2])
assertEquals(null, map[b3])
""".trimIndent())
}
@Test
fun testInstant() = runTest {
eval(
"""
import lyng.time
val now = Instant()
// assertEquals( now.epochSeconds, Instant(now.epochSeconds).epochSeconds )
assert( 10.seconds is Duration )
assertEquals( 10.seconds, Duration(10) )
assertEquals( 10.milliseconds, Duration(0.01) )
assertEquals( 10.milliseconds, 0.01.seconds )
assertEquals( 1001.5.milliseconds, 1.0015.seconds )
val n1 = now + 7.seconds
assert( n1 is Instant )
assertEquals( n1 - now, 7.seconds )
assertEquals( now - n1, -7.seconds )
""".trimIndent()
)
delay(1000)
}
@Test
fun testTimeStatics() = runTest {
eval(
"""
import lyng.time
assert( 100.minutes is Duration )
assert( 100.days is Duration )
assert( 1.day == 24.hours )
assert( 1.day.hours == 24 )
assert( 1.hour.seconds == 3600 )
assert( 1.minute.milliseconds == 60_000 )
assert(Instant.distantFuture is Instant)
assert(Instant.distantPast is Instant)
assert( Instant.distantFuture - Instant.distantPast > 70_000_000.days)
val maxRange = Instant.distantFuture - Instant.distantPast
println("всего лет %g"(maxRange.days/365.2425))
""".trimIndent()
)
}
@Test
fun testInstantFormatting() = runTest {
eval(
"""
import lyng.time
val now = Instant()
val unixEpoch = "%ts"(now)
println("current seconds is %s"(unixEpoch))
println("current time is %tT"(now))
assertEquals( unixEpoch.toInt(), now.epochSeconds.toInt() )
""".trimIndent()
)
}
@Test
fun testDoubleImports() = runTest {
val s = Scope.new()
println(Script.defaultImportManager.packageNames)
println(s.importManager.packageNames)
s.importManager.addTextPackages("""
package foo
import lyng.time
fun foo() {
println("foo: %s"(Instant()))
}
""".trimIndent())
s.importManager.addTextPackages("""
package bar
import lyng.time
fun bar() {
println("bar: %s"(Instant()))
}
""".trimIndent())
println(s.importManager.packageNames)
s.eval("""
import foo
import bar
foo()
bar()
""".trimIndent())
}
@Test
fun testIndexIntIncrements() = runTest {
eval("""
val x = [1,2,3]
x[1]++
++x[0]
assertEquals( [2,3,3], x )
import lyng.buffer
val b = MutableBuffer(1,2,3)
b[1]++
assert( b == Buffer(1,3,3) )
++b[0]
assertEquals( b, Buffer(2,3,3) )
""".trimIndent())
}
@Test
fun testIndexIntDecrements() = runTest {
eval("""
val x = [1,2,3]
x[1]--
--x[0]
assertEquals( [0,1,3], x )
import lyng.buffer
val b = Buffer(1,2,3).toMutable()
b[1]--
assert( b == Buffer(1,1,3) )
--b[0]
assertEquals( b, Buffer(0,1,3) )
""".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)
}
} }
@@ -50,4 +50,27 @@ class TypesTest {
""".trimIndent()) """.trimIndent())
} }
@Test
fun testUserClassCompareTo() = runTest {
eval("""
class Point(val a,b)
assertEquals(Point(0,1), Point(0,1) )
assertNotEquals(Point(0,1), Point(1,1) )
""".trimIndent())
}
@Test
fun testUserClassCompareTo2() = runTest {
eval("""
class Point(val a,b) {
var c = 0
}
assertEquals(Point(0,1), Point(0,1) )
assertEquals(Point(0,1).apply { c = 2 }, Point(0,1).apply { c = 2 } )
assertNotEquals(Point(0,1), Point(1,1) )
assertNotEquals(Point(0,1), Point(0,1).apply { c = 1 } )
""".trimIndent())
}
} }
+22 -1
View File
@@ -2,9 +2,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.ObjVoid
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.ObjVoid
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Files.readAllLines import java.nio.file.Files.readAllLines
import java.nio.file.Paths import java.nio.file.Paths
@@ -105,6 +106,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
} else { } else {
var isValid = true var isValid = true
val result = mutableListOf<String>() val result = mutableListOf<String>()
// remove empty trails:
while( block.last().isEmpty() ) block.removeLast()
while (block.size > outStart) { while (block.size > outStart) {
val line = block.removeAt(outStart) val line = block.removeAt(outStart)
if (!line.startsWith(">>> ")) { if (!line.startsWith(">>> ")) {
@@ -265,6 +270,11 @@ class BookTest {
runDocTests("../docs/Map.md") runDocTests("../docs/Map.md")
} }
@Test
fun testBuffer() = runTest {
runDocTests("../docs/Buffer.md")
}
@Test @Test
fun testSampleBooks() = runTest { fun testSampleBooks() = runTest {
for (bt in Files.list(Paths.get("../docs/samples")).toList()) { for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
@@ -283,4 +293,15 @@ class BookTest {
fun testExceptionsBooks() = runTest { fun testExceptionsBooks() = runTest {
runDocTests("../docs/exceptions_handling.md") runDocTests("../docs/exceptions_handling.md")
} }
@Test
fun testTimeBooks() = runBlocking {
runDocTests("../docs/time.md")
}
@Test
fun testParallelismBook() = runBlocking {
runDocTests("../docs/parallelism.md")
}
} }
+638
View File
@@ -0,0 +1,638 @@
import junit.framework.TestCase.assertNotSame
import junit.framework.TestCase.assertSame
import kotlinx.coroutines.test.runTest
import net.sergeych.bintools.encodeToHex
import net.sergeych.lyng.Scope
import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.*
import net.sergeych.lynon.*
import java.nio.file.Files
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class LynonTests {
@Test
fun testSizeInTetrades() {
assertEquals(1, sizeInTetrades(0u))
assertEquals(1, sizeInTetrades(1u))
assertEquals(1, sizeInTetrades(15u))
assertEquals(2, sizeInTetrades(16u))
assertEquals(2, sizeInTetrades(254u))
assertEquals(2, sizeInTetrades(255u))
assertEquals(3, sizeInTetrades(256u))
assertEquals(3, sizeInTetrades(257u))
}
@Test
fun testSizeInBits() {
assertEquals(1, sizeInBits(0u))
assertEquals(1, sizeInBits(1u))
assertEquals(2, sizeInBits(2u))
assertEquals(2, sizeInBits(3u))
assertEquals(4, sizeInBits(15u))
}
@Test
fun testBitOutputSmall() {
val bout = MemoryBitOutput()
bout.putBit(1)
bout.putBit(1)
bout.putBit(0)
bout.putBit(1)
val x = bout.toBitArray()
assertEquals(1, x[0])
assertEquals(1, x[1])
assertEquals(0, x[2])
assertEquals(1, x[3])
assertEquals(4, x.size)
assertEquals("1101", x.toString())
val bin = MemoryBitInput(x)
assertEquals(1, bin.getBit())
assertEquals(1, bin.getBit())
assertEquals(0, bin.getBit())
assertEquals(1, bin.getBit())
assertEquals(null, bin.getBitOrNull())
}
@Test
fun testBitOutputMedium() {
val bout = MemoryBitOutput()
bout.putBit(1)
bout.putBit(1)
bout.putBit(0)
bout.putBit(1)
bout.putBits(0, 7)
bout.putBits(3, 2)
val x = bout.toBitArray()
assertEquals(1, x[0])
assertEquals(1, x[1])
assertEquals(0, x[2])
assertEquals(1, x[3])
assertEquals(13, x.size)
assertEquals("1101000000011", x.toString())
println(x.bytes.encodeToHex())
val bin = MemoryBitInput(x)
assertEquals(1, bin.getBit())
assertEquals(1, bin.getBit())
assertEquals(0, bin.getBit())
assertEquals(1, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
// assertEquals(0, bin.getBit())
assertEquals(0UL, bin.getBits(7))
assertEquals(3UL, bin.getBits(2))
assertEquals(null, bin.getBitOrNull())
}
@Test
fun testBitStreams() {
val bout = MemoryBitOutput()
bout.putBits(2, 3)
bout.putBits(1, 7)
bout.putBits(197, 8)
bout.putBits(3, 4)
bout.close()
val bin = MemoryBitInput(bout)
assertEquals(2UL, bin.getBits(3))
assertEquals(1UL, bin.getBits(7))
assertEquals(197UL, bin.getBits(8))
assertEquals(3UL, bin.getBits(4))
}
@Test
fun testUnsignedPackInteger() {
val bout = MemoryBitOutput()
bout.packUnsigned(1471792UL)
bout.close()
val bin = MemoryBitInput(bout)
assertEquals(1471792UL, bin.unpackUnsigned())
}
@Test
fun testUnsignedPackLongInteger() {
val bout = MemoryBitOutput()
bout.packUnsigned(ULong.MAX_VALUE)
bout.close()
val bin = MemoryBitInput(bout)
assertEquals(ULong.MAX_VALUE, bin.unpackUnsigned())
}
@Test
fun testUnsignedPackLongSmallInteger() {
val bout = MemoryBitOutput()
bout.packUnsigned(7UL)
bout.close()
val bin = MemoryBitInput(bout)
assertEquals(7UL, bin.unpackUnsigned())
}
@Test
fun testSignedPackInteger() {
val bout = MemoryBitOutput()
bout.packSigned(-1471792L)
bout.packSigned(1471792L)
// bout.packSigned(147179L)
bout.close()
val bin = MemoryBitInput(bout)
assertEquals(-1471792L, bin.unpackSigned())
assertEquals(1471792L, bin.unpackSigned())
}
@Test
fun testObjStringAndStringKeys() = runTest {
val s = "foo"
val sobj = ObjString("foo")
val map = mutableMapOf(s to 1, sobj to 2)
assertEquals(1, map[s])
assertEquals(2, map[sobj])
}
@Test
fun testCache1() = runTest {
val bout = MemoryBitOutput()
val encoder = LynonEncoder(bout)
val s = "Hello, World!".toObj()
val scope = Scope()
encoder.encodeObject(scope, s) // 1
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s)
encoder.encodeObject(scope, s) // 8
val decoder = LynonDecoder(MemoryBitInput(bout))
val s1 = decoder.decodeObject(scope, ObjString.type) // 1
assertEquals(s, s1)
assertNotSame(s, s1)
val s2 = decoder.decodeObject(scope, ObjString.type)
assertEquals(s, s2)
assertSame(s1, s2)
assertSame(s1, decoder.decodeObject(scope, ObjString.type))
assertSame(s1, decoder.decodeObject(scope, ObjString.type))
assertSame(s1, decoder.decodeObject(scope, ObjString.type))
assertSame(s1, decoder.decodeObject(scope, ObjString.type))
assertSame(s1, decoder.decodeObject(scope, ObjString.type))
assertSame(s1, decoder.decodeObject(scope, ObjString.type)) // 8
}
@Test
fun testCache2() = runTest {
val variants = (100..500).map { "Sample $it".toObj() }.shuffled()
var source = variants.shuffled()
for (i in 0..300) source += variants.shuffled()
val encoder = LynonPacker()
val scope = Scope()
for (s in source) {
encoder.encodeObject(scope, s)
}
val decoder = LynonUnpacker(encoder)
val restored = mutableListOf<Obj>()
for (i in source.indices) {
restored.add(decoder.decodeObject(scope, ObjString.type))
}
assertEquals(restored, source)
}
@Test
fun testUnpackBoolean() = runTest {
val scope = Scope()
val decoder = LynonUnpacker(LynonPacker().apply {
encodeObject(scope, ObjBool(true))
encodeObject(scope, ObjBool(false))
encodeObject(scope, ObjBool(true))
encodeObject(scope, ObjBool(true))
})
assertEquals(ObjTrue, decoder.decodeObject(scope, ObjBool.type))
assertEquals(ObjFalse, decoder.decodeObject(scope, ObjBool.type))
assertEquals(ObjTrue, decoder.decodeObject(scope, ObjBool.type))
assertEquals(ObjTrue, decoder.decodeObject(scope, ObjBool.type))
}
@Test
fun testUnpackReal() = runTest {
val scope = Scope()
val decoder = LynonUnpacker(LynonPacker().apply {
encodeObject(scope, ObjReal(-Math.PI))
encodeObject(scope, ObjReal(Math.PI))
encodeObject(scope, ObjReal(-Math.PI))
encodeObject(scope, ObjReal(Math.PI))
encodeObject(scope, ObjReal(Double.NaN))
encodeObject(scope, ObjReal(Double.NEGATIVE_INFINITY))
encodeObject(scope, ObjReal(Double.POSITIVE_INFINITY))
encodeObject(scope, ObjReal(Double.MIN_VALUE))
encodeObject(scope, ObjReal(Double.MAX_VALUE))
})
assertEquals(ObjReal(-Math.PI), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(Math.PI), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(-Math.PI), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(Math.PI), decoder.decodeObject(scope, ObjReal.type))
assert((decoder.decodeObject(scope, ObjReal.type)).toDouble().isNaN())
assertEquals(ObjReal(Double.NEGATIVE_INFINITY), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(Double.POSITIVE_INFINITY), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(Double.MIN_VALUE), decoder.decodeObject(scope, ObjReal.type))
assertEquals(ObjReal(Double.MAX_VALUE), decoder.decodeObject(scope, ObjReal.type))
}
@Test
fun testUnpackInt() = runTest {
val scope = Scope()
val decoder = LynonUnpacker(LynonPacker().apply {
encodeObject(scope, ObjInt(0))
encodeObject(scope, ObjInt(-1))
encodeObject(scope, ObjInt(23))
encodeObject(scope, ObjInt(Long.MIN_VALUE))
encodeObject(scope, ObjInt(Long.MAX_VALUE))
encodeObject(scope, ObjInt(Long.MAX_VALUE))
})
assertEquals(ObjInt(0), decoder.decodeObject(scope, ObjInt.type))
assertEquals(ObjInt(-1), decoder.decodeObject(scope, ObjInt.type))
assertEquals(ObjInt(23), decoder.decodeObject(scope, ObjInt.type))
assertEquals(ObjInt(Long.MIN_VALUE), decoder.decodeObject(scope, ObjInt.type))
assertEquals(ObjInt(Long.MAX_VALUE), decoder.decodeObject(scope, ObjInt.type))
assertEquals(ObjInt(Long.MAX_VALUE), decoder.decodeObject(scope, ObjInt.type))
}
@Test
fun testLastvalue() {
var bin = MemoryBitInput(MemoryBitOutput().apply {
putBits(5, 3)
})
assertEquals(5UL, bin.getBits(3))
assertEquals(null, bin.getBitsOrNull(3))
bin = MemoryBitInput(MemoryBitOutput().apply {
putBits(5, 3)
putBits(1024, 11)
putBits(2, 2)
})
assertEquals(5UL, bin.getBits(3))
assertEquals(1024UL, bin.getBits(11))
assertEquals(2UL, bin.getBits(2))
assertEquals(null, bin.getBitsOrNull(3))
}
val original = Files.readString(Path.of("../sample_texts/dikkens_hard_times.txt"))
@Test
fun testEncodeNullsAndInts() = runTest {
testScope().eval(
"""
testEncode(null)
testEncode(0)
""".trimIndent()
)
}
@Test
fun testBufferEncoderInterop() = runTest {
val bout = MemoryBitOutput()
bout.putBits(0, 1)
bout.putBits(1, 4)
val bin = MemoryBitInput(bout.toBitArray().bytes, 8)
assertEquals(0UL, bin.getBits(1))
assertEquals(1UL, bin.getBits(4))
}
suspend fun testScope() =
Scope().apply {
eval(
"""
import lyng.serialization
fun testEncode(value) {
val encoded = Lynon.encode(value)
println(encoded.toDump())
println("Encoded size %d: %s"(encoded.size, value))
assertEquals( value, Lynon.decode(encoded) )
}
""".trimIndent()
)
}
@Test
fun testUnaryMinus() = runTest {
eval(
"""
assertEquals( -1 * π, 0 - π )
assertEquals( -1 * π, -π )
""".trimIndent()
)
}
@Test
fun testSimpleTypes() = 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())
testEncode("Hello, world".encodeUtf8())
testEncode("Hello, world")
""".trimIndent()
)
}
@Test
fun testLzw() {
// Example usage
// val original = "TOBEORNOTTOBEORTOBEORNOT"
// println("Original: $original")
println("Length: ${original.length}")
// Compress
val out = MemoryBitOutput()
LZW.compress(original.encodeToByteArray().toUByteArray(), out)
// println("\nCompressed codes: ${out.toUByteArray().toDump()}")
println("Number of codes: ${out.toBitArray().bytesSize}")
println("Copression rate: ${out.toBitArray().bytesSize.toDouble() / original.length.toDouble()}")
// // Decompress
val decompressed = LZW.decompress(MemoryBitInput(out), original.length).toByteArray().decodeToString()
// println("\nDecompressed: $decompressed")
println("Length: ${decompressed.length}")
// Verification
println("\nOriginal and decompressed match: ${original == decompressed}")
assertEquals(original, decompressed)
}
@Test
fun testTinyBits() {
var a0 = TinyBits()
assertEquals(a0, a0)
a0 = a0.insertBit(0)
a0 = a0.insertBit(1)
a0 = a0.insertBit(1)
a0 = a0.insertBit(1)
a0 = a0.insertBit(0)
a0 = a0.insertBit(1)
// println(a0)
assertEquals("011101", a0.toString())
val bin = MemoryBitInput(MemoryBitOutput().apply { putBits(a0) })
var result = TinyBits()
for (i in a0.indices) result = result.insertBit(bin.getBit())
assertEquals(a0, result)
}
@Test
fun testHuffman() {
val x = original.encodeToByteArray().toUByteArray()
// val x ="hello, world!".toByteArray().asUByteArray()// original.encodeToByteArray().toUByteArray()
println("Original : ${x.size}")
val lzw = LZW.compress(x).bytes
println("LZW : ${lzw.size}")
val ba = Huffman.compress(x, Huffman.byteAlphabet)
val huff = ba.bytes
println("Huffman : ${huff.size}")
val lzwhuff = Huffman.compress(lzw, Huffman.byteAlphabet).bytes
println("LZW+HUFF : ${lzwhuff.size}")
val compressed = Huffman.compress(x, Huffman.byteAlphabet)
val decompressed = Huffman.decompress(compressed.toBitInput(), Huffman.byteAlphabet)
assertContentEquals(x, decompressed)
}
@Test
fun testGenerateCanonicalHuffmanCodes() {
val frequencies = LynonType.entries.map { it.defaultFrequency }.toTypedArray()
val alphabet = object : Huffman.Alphabet<LynonType> {
override val maxOrdinal = LynonType.entries.size
override fun decodeOrdinalTo(bout: BitOutput, ordinal: Int) {
throw NotImplementedError()
}
override fun get(ordinal: Int): LynonType {
return LynonType.entries[ordinal]
}
override fun ordinalOf(value: LynonType): Int = value.ordinal
}
for (code in Huffman.generateCanonicalCodes(frequencies, alphabet)) {
println("${code?.bits}: ${code?.ordinal?.let { LynonType.entries[it] }}")
}
}
@Test
fun testBitListSmall() {
var t = TinyBits()
for (i in listOf(1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1))
t = t.insertBit(i)
assertEquals(1, t[0])
assertEquals(1, t[1])
assertEquals(0, t[2])
assertEquals("1101000111101", t.toString())
t[0] = 0
t[1] = 0
t[2] = 1
assertEquals("0011000111101", t.toString())
t[12] = 0
t[11] = 1
assertEquals("0011000111110", t.toString())
}
@Test
fun testBitListSerialization() {
// this also tests bitArray with first and last bytes
val bout = MemoryBitOutput()
assertEquals("1101", bitListOf(1, 1, 0, 1).toString())
bout.putBits(bitListOf(1, 1, 0, 1))
bout.putBits(bitListOf(0, 0))
bout.putBits(bitListOf(0, 1, 1, 1, 1, 0, 1))
val x = bout.toBitArray()
assertEquals("1101000111101", x.toString())
}
@Test
fun testCompressionWithOffsets() {
val src = "to be or not to be or not to be or not to be or not to be"
val bout = MemoryBitOutput()
bout.packUnsigned(1571UL)
LZW.compress(src.encodeToByteArray(), bout)
bout.packUnsigned(157108UL)
val bin = bout.toBitInput()
assertEquals(1571UL, bin.unpackUnsigned())
assertEquals(src, LZW.decompress(bin, src.length).asByteArray().decodeToString())
assertEquals(157108UL, bin.unpackUnsigned())
}
@Test
fun testCompressionRecord() {
val bout = MemoryBitOutput()
val src = "to be or not to be or not to be or not to be or not to be"
val src2 = "to be or not to be"
val src3 = "ababababab"
bout.compress(src)
bout.compress(src2)
bout.compress(src3)
val bin = bout.toBitInput()
assertEquals(src, bin.decompressString())
assertEquals(src2, bin.decompressString())
assertEquals(src3, bin.decompressString())
}
@Test
fun testIntList() = runTest {
testScope().eval(
"""
testEncode([1,2,3])
testEncode([-1,-2,-3])
testEncode([1,-2,-3])
testEncode([0,1])
testEncode([0,0,0])
testEncode(["the", "the", "wall", "the", "wall", "wall"])
testEncode([1,2,3, "the", "wall", "wall"])
testEncode([false, false, false, true,true])
testEncode([])
""".trimIndent()
)
}
@Test
fun testNamedObject() = runTest {
val s = testScope()
val x = s.eval("""5 => 6""")
assert(x is ObjMapEntry)
val bout = MemoryBitOutput()
val e = LynonEncoder(bout)
e.encodeAny(s, x)
val bin = bout.toBitInput()
val d = LynonDecoder(bin)
val x2 = d.decodeAny(s)
println(x)
println(x2)
assertTrue { x.compareTo(s, x2) == 0}
assertEquals(x, x2)
s.eval("""
testEncode( 1 => "one" )
testEncode( [1 => "one", 1 => "one"] )
testEncode( [1 => "one", 1 => "one", "foo" => "MapEntry"] )
""".trimIndent())
}
@Test
fun testHomogenousMap() = runTest {
val s = testScope()
s.eval("""
testEncode( Map("one" => 1, "two" => 2) )
testEncode( Map() )
""".trimIndent())
}
@Test
fun testHeterogeneousMap() = runTest {
val s = testScope()
s.eval("""
testEncode(["one", 2])
testEncode([1, "2"])
testEncode( Map("one" => 1, 2 => 2) )
testEncode( Map("one" => 1, 2 => "2") )
""".trimIndent())
}
@Test
fun testSetSerialization() = runTest {
testScope().eval("""
testEncode( Set("one", "two") )
testEncode( Set() )
testEncode( Set(1, "one", false) )
testEncode( Set(true, true, false) )
""".trimIndent())
}
@Test
fun testClassSerializationNoInstanceVars() = runTest {
testScope().eval("""
import lyng.serialization
class Point(x,y)
// println( Lynon.encode(Point(0,0)).toDump() )
testEncode(Point(0,0))
testEncode(Point(10,11))
testEncode(Point(-1,2))
testEncode(Point(-1,-2))
testEncode(Point("point!",-2))
""".trimIndent())
}
@Test
fun testClassSerializationWithInstanceVars() = runTest {
testScope().eval("""
import lyng.serialization
class Point(x=0) {
var y = 0
}
testEncode(Point())
testEncode(Point(1))
testEncode(Point(1).apply { y = 2 })
testEncode(Point(10).also { it.y = 11 })
""".trimIndent())
}
@Test
fun testClassSerializationWithInstanceVars2() = runTest {
testScope().eval("""
import lyng.serialization
var onInitComment = null
class Point(x=0) {
var y = 0
var comment = null
fun onDeserialized() {
onInitComment = comment
}
}
testEncode(Point())
testEncode(Point(1))
testEncode(Point(1).apply { y = 2 })
testEncode(Point(10).also { it.y = 11 })
// important: class init is called before setting non-constructor fields
// this is decessary, so deserialized fields are only available
// after onDeserialized() call (if exists):
// deserialized:
testEncode(Point(10).also { it.y = 11; it.comment = "comment" })
println("-- on init comment "+onInitComment)
assertEquals("comment", onInitComment)
""".trimIndent())
}
}
+53
View File
@@ -0,0 +1,53 @@
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Source
import net.sergeych.lyng.eval
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
import net.sergeych.lyng.toSource
import kotlin.test.Test
class OtherTests {
@Test
fun testImports3() = runBlocking {
val foosrc = """
package lyng.foo
import lyng.bar
fun foo() { "foo1" }
""".trimIndent()
val barsrc = """
package lyng.bar
fun bar() { "bar1" }
""".trimIndent()
val pm = InlineSourcesImportProvider(
listOf(
Source("foosrc", foosrc),
Source("barsrc", barsrc),
))
val src = """
import lyng.foo
foo() + " / " + bar()
""".trimIndent().toSource("test")
val scope = ModuleScope(pm, src)
assertEquals("foo1 / bar1", scope.eval(src).toString())
}
@Test
fun testInstantTruncation() = runBlocking {
eval("""
import lyng.time
val t1 = Instant()
val t2 = Instant()
// assert( t1 != t2 )
println(t1 - t2)
""".trimIndent())
Unit
}
}
File diff suppressed because it is too large. Load diff