Compare commits

...
51 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
sergeych 868379ce22 v0.7.0: imports and stuff 2025-07-04 01:00:20 +03:00
sergeych f37354f382 fix #36 basic import support 2025-07-04 00:56:56 +03:00
sergeych ddbcbf9e4e renaming context to scope, as it is rather scp[e than the context 2025-07-03 21:46:25 +03:00
sergeych 6cf99fbd13 Compiler refactored: CompilerContext now is class member 2025-07-03 20:00:01 +03:00
sergeych 950e8301c3 associateBy and Lyng:Iterable .toFlow() for kotlin integration simplicity 2025-07-02 20:45:30 +03:00
sergeych ea2cf1f373 code cleaning 2025-07-01 12:31:40 +03:00
sergeych eee6d75587 extension methids 2025-06-20 03:43:58 +04:00
sergeych a8067d0a6b fix #31 type records for simple types and extern keyword for methods/functions 2025-06-19 01:06:45 +04:00
sergeych 75a6f20150 fix #31 type records for simple types and extern keyword for methods/functions 2025-06-17 18:42:37 +04:00
sergeych d969d6d572 fix #37 AppliedContext optimization 2025-06-17 09:49:05 +04:00
100 changed files with 9039 additions and 1635 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
+19 -15
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 )`.
@@ -75,21 +75,23 @@ runBlocking {
### Exchanging information ### Exchanging information
Script is executed over some `Context`. Create instance of the context, Script is executed over some `Scope`. Create instance,
add your specific vars and functions to it, an call over it: add your specific vars and functions to it, and call:
```kotlin ```kotlin
import new.sergeych.lyng.*
import com.sun.source.tree.Scope
import new.sergeych.lyng.*
// simple function // simple function
val context = Context().apply { val scope = Scope().apply {
addFn("addArgs") { addFn("addArgs") {
var sum = 0.0 var sum = 0.0
for( a in args) sum += a.toDouble() for (a in args) sum += a.toDouble()
ObjReal(sum) ObjReal(sum)
} }
addConst("LIGHT_SPEED", ObjReal(299_792_458.0)) addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
// callback back to kotlin to some suspend fn, for example:: // callback back to kotlin to some suspend fn, for example::
// suspend fun doSomeWork(text: String): Int // suspend fun doSomeWork(text: String): Int
addFn("doSomeWork") { addFn("doSomeWork") {
@@ -99,9 +101,9 @@ val context = Context().apply {
} }
} }
// adding constant: // adding constant:
context.eval("addArgs(1,2,3)") // <- 6 scope.eval("addArgs(1,2,3)") // <- 6
``` ```
Note that the context stores all changes in it so you can make calls on a single context to preserve state between calls. Note that the scope stores all changes in it so you can make calls on a single scope to preserve state between calls.
## Why? ## Why?
@@ -140,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
@@ -156,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
+15
View File
@@ -21,6 +21,20 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
## Instance methods ## Instance methods
| fun/method | description |
|-----------------|---------------------------------------------------------------------------------|
| toList() | create a list from iterable |
| toSet() | create a set from iterable |
| contains(i) | check that iterable contains `i` |
| `i in iterator` | same as `contains(i)` |
| isEmpty() | check iterable is empty |
| forEach(f) | call f for each element |
| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) |
| map(f) | create a list of values returned by `f` called for each element of the iterable |
| indexOf(i) | return index if the first encounter of i or a negative value if not found |
| associateBy(kf) | create a map where keys are returned by kf that will be called for each element |
fun Iterable.toList(): List fun Iterable.toList(): List
fun Iterable.toSet(): Set fun Iterable.toSet(): Set
fun Iterable.indexOf(element): Int fun Iterable.indexOf(element): Int
@@ -28,6 +42,7 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
fun Iterable.isEmpty(element): Bool fun Iterable.isEmpty(element): Bool
fun Iterable.forEach(block: (Any?)->Void ): Void fun Iterable.forEach(block: (Any?)->Void ): Void
fun Iterable.map(block: (Any?)->Void ): List fun Iterable.map(block: (Any?)->Void ): List
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
## Abstract methods ## Abstract methods
+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 )
+78 -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,81 @@ 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
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
that would test if some object of unknown type contains something that can be interpreted
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
fun Object.isInteger() {
when(this) {
// already Int?
is Int -> true
// real, but with no declimal part?
is Real -> toInt() == this
// string with int or real reuusig code above
is String -> toReal().isInteger()
// otherwise, no:
else -> false
}
}
// Let's test:
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
>>> void
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created,
as they are modifying the type, not the context.
Beware of it. We might need to reconsider it later.
# Theory # Theory
## Basic principles: ## Basic principles:
@@ -216,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.
+8 -8
View File
@@ -15,11 +15,11 @@ you can use it's class to ensure type:
## Member functions ## Member functions
| name | meaning | type | | name | meaning | type |
|-----------------|------------------------------------|------| |-----------------|-------------------------------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int | | `.roundToInt()` | round to nearest int like round(x) | Int |
| | | | | `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
+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 -27
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,24 +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 |
| 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.
@@ -1253,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" }
+38 -8
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
@@ -31,7 +38,7 @@ data class CommandResult(
val error: String val error: String
) )
val baseContext = Context().apply { val baseScope = Scope().apply {
addFn("exit") { addFn("exit") {
exit(requireOnlyArg<ObjInt>().toInt()) exit(requireOnlyArg<ObjInt>().toInt())
ObjVoid ObjVoid
@@ -41,7 +48,23 @@ val baseContext = Context().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.
@@ -74,7 +97,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
val objargs = mutableListOf<String>() val objargs = mutableListOf<String>()
script?.let { objargs += it } script?.let { objargs += it }
objargs += args objargs += args
baseContext.addConst( baseScope.addConst(
"ARGV", ObjList( "ARGV", ObjList(
objargs.map { ObjString(it) }.toMutableList() objargs.map { ObjString(it) }.toMutableList()
) )
@@ -82,7 +105,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
launcher { launcher {
// there is no script name, it is a first argument instead: // there is no script name, it is a first argument instead:
processErrors { processErrors {
baseContext.eval(execute!!) baseScope.eval(execute!!)
} }
} }
} }
@@ -98,7 +121,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
) )
echoFormattedHelp() echoFormattedHelp()
} else { } else {
baseContext.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList())) baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
launcher { executeFile(script!!) } launcher { executeFile(script!!) }
} }
} }
@@ -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(baseContext) 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.6.7-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 {
@@ -0,0 +1,19 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
/**
* Special version of the [Scope] used to `apply` new this object to
* _parent context property.
*
* @param _parent context to apply to
* @param args arguments for the new context
* @param appliedScope the new context to apply, it will have lower priority except for `this` which
* will be reset by appliedContext's `this`.
*/
class AppliedScope(_parent: Scope, args: Arguments, val appliedScope: Scope)
: Scope(_parent, args, appliedScope.pos, appliedScope.thisObj) {
override fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else super.get(name) ?: appliedScope[name]
}
@@ -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(
context: Context, scope: Scope,
arguments: Arguments = context.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) {
context.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
@@ -52,11 +59,11 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { val value = when {
i < callArgs.size -> callArgs[i] i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context) 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}")
context.raiseIllegalArgument("too few arguments for the call") scope.raiseIllegalArgument("too few arguments for the call")
} }
} }
assign(a, value) assign(a, value)
@@ -76,8 +83,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
callArgs[j--] callArgs[j--]
} }
a.defaultValue != null -> a.defaultValue.execute(context) a.defaultValue != null -> a.defaultValue.execute(scope)
else -> context.raiseIllegalArgument("too few arguments for the call") else -> scope.raiseIllegalArgument("too few arguments for the call")
} }
assign(a, value) assign(a, value)
i-- i--
@@ -98,7 +105,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
processEllipsis(leftIndex, end) processEllipsis(leftIndex, end)
} else { } else {
if (leftIndex < callArgs.size) if (leftIndex < callArgs.size)
context.raiseIllegalArgument("too many arguments for the call") scope.raiseIllegalArgument("too many arguments for the call")
} }
} }
@@ -110,7 +117,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
*/ */
data class Item( data class Item(
val name: String, val name: String,
val type: TypeDecl = TypeDecl.Obj, val type: TypeDecl = TypeDecl.TypeAny,
val pos: Pos = Pos.builtIn, val pos: Pos = Pos.builtIn,
val isEllipsis: Boolean = false, val isEllipsis: Boolean = false,
/** /**
@@ -1,12 +1,16 @@
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(context: Context,tailBlockMode: Boolean): Arguments { suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
val list = mutableListOf<Obj>() val list = mutableListOf<Obj>()
for (x in this) { for (x in this) {
val value = x.value.execute(context) val value = x.value.execute(scope)
if (x.isSplat) { if (x.isSplat) {
when { when {
value is ObjList -> { value is ObjList -> {
@@ -14,11 +18,11 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
} }
value.isInstanceOf(ObjIterable) -> { value.isInstanceOf(ObjIterable) -> {
val i = (value.invokeInstanceMethod(context, "toList") as ObjList).list val i = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
i.forEach { list.add(it) } i.forEach { list.add(it) }
} }
else -> context.raiseClassCastError("expected list of objects for splat argument") else -> scope.raiseClassCastError("expected list of objects for splat argument")
} }
} else } else
list.add(value) list.add(value)
@@ -26,7 +30,7 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
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())
@@ -38,10 +42,12 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
/** /**
* Convert to list of kotlin objects, see [Obj.toKotlin]. * Convert to list of kotlin objects, see [Obj.toKotlin].
*/ */
suspend fun toKotlinList(context: Context): List<Any?> { suspend fun toKotlinList(scope: Scope): List<Any?> {
return list.map { it.toKotlin(context) } 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())
File diff suppressed because it is too large. Load diff
@@ -1,13 +1,11 @@
package net.sergeych.lyng package net.sergeych.lyng
internal class CompilerContext(val tokens: List<Token>) { class CompilerContext(val tokens: List<Token>) {
val labels = mutableSetOf<String>() val labels = mutableSetOf<String>()
var breakFound = false var breakFound = false
private set
var loopLevel = 0 var loopLevel = 0
private set
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> { inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
if (++loopLevel == 0) breakFound = false if (++loopLevel == 0) breakFound = false
@@ -51,8 +49,26 @@ internal 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
/**
* If the next token is identifier `name`, skip it and return `true`.
* else leave where it is and return `false`
*/
fun skipId(name: String): Boolean {
current().let { t ->
if( t.type == Token.Type.ID && t.value == name ) {
next()
return true
}
}
return false
}
/** /**
* Skips next token if its type is `tokenType`, returns `true` if so. * Skips next token if its type is `tokenType`, returns `true` if so.
* @param errorMessage message to throw if next token is not `tokenType` * @param errorMessage message to throw if next token is not `tokenType`
@@ -85,7 +101,7 @@ internal class CompilerContext(val tokens: List<Token>) {
} }
fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean { inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
val t = next() val t = next()
return if (t.type == typeId) { return if (t.type == typeId) {
f(t) f(t)
@@ -96,7 +112,6 @@ internal class CompilerContext(val tokens: List<Token>) {
} }
} }
@Suppress("NOTHING_TO_INLINE")
inline fun addBreak() { inline fun addBreak() {
breakFound = true breakFound = true
} }
@@ -153,7 +168,9 @@ internal 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,141 +0,0 @@
package net.sergeych.lyng
class Context(
val parent: Context?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
var thisObj: Obj = ObjVoid,
var skipContextCreation: Boolean = false,
) {
constructor(
args: Arguments = Arguments.EMPTY,
pos: Pos = Pos.builtIn,
)
: this(Script.defaultContext, args, pos)
/**
* Making this context priority one
*/
fun applyContext(other: Context): Context {
if (other.thisObj != ObjVoid) thisObj = other.thisObj
appliedContext = other
return this
}
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}
inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
inline fun <reified T : Obj> requireOnlyArg(): T {
if (args.list.size != 1) raiseError("Expected exactly 1 argument, got ${args.list.size}")
return requiredArg(0)
}
@Suppress("unused")
fun requireExactCount(count: Int) {
if (args.list.size != count) {
raiseError("Expected exactly $count arguments, got ${args.list.size}")
}
}
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal var appliedContext: Context? = null
internal val objects = mutableMapOf<String, ObjRecord>()
operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
?: parent?.get(name)
?: appliedContext?.get(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Context =
Context(this, args, pos, newThisObj ?: thisObj)
fun copy() = Context(this, args, pos, thisObj)
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public
): ObjRecord {
return ObjRecord(value, isMutable, visibility).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {
val ns = objects.getOrPut(name) { ObjRecord(ObjNamespace(name), isMutable = false) }.value
return ns.objClass
}
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Context.() -> Unit) {
addFn<ObjVoid>(*names) {
fn(this)
ObjVoid
}
}
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj = context.fn()
}
for (name in names) {
addItem(
name,
false,
newFn
)
}
}
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
Compiler().compile(code.toSource()).execute(this)
fun containsLocal(name: String): Boolean = name in objects
}
@@ -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,502 +0,0 @@
package net.sergeych.lyng
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
/**
* Record to store object with access rules, e.g. [isMutable] and access level [visibility].
*/
data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
val visibility: Visibility = Visibility.Public
)
/**
* 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 (Context) -> ObjRecord,
val setterOrNull: (suspend (Context, Obj) -> Unit)?
) {
/**
* Simplified constructor for immutable stores.
*/
constructor(getter: suspend (Context) -> ObjRecord) : this(getter, null)
/**
* Get the setter or throw.
*/
fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos, "can't assign value")
}
open class Obj {
val isNull by lazy { this === ObjNull }
var isFrozen: Boolean = false
private val monitor = Mutex()
// private val memberMutex = Mutex()
internal var parentInstances: MutableList<Obj> = mutableListOf()
private val opInstances = ProtectedOp()
open fun inspect(): String = toString()
/**
* Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such.
* When initializing a var with it, by value objects must be copied. By-reference ones aren't.
*
* Almost all objects are by-reference.
*/
open fun byValueCopy(): Obj = this
@Suppress("SuspiciousEqualsCombination")
fun isInstanceOf(someClass: Obj) = someClass === objClass ||
objClass.allParentsSet.contains(someClass) ||
someClass == rootObjectType
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.toList()))
suspend inline fun <reified T : Obj> callMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(context, name, args) as T
open suspend fun invokeInstanceMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
): Obj =
// note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args)
fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
// methods that to override
open suspend fun compareTo(context: Context, other: Obj): Int {
context.raiseNotImplemented()
}
open suspend fun contains(context: Context, other: Obj): Boolean {
return invokeInstanceMethod(context, "contains", other).toBool()
}
open val asStr: ObjString by lazy {
if (this is ObjString) this else ObjString(this.toString())
}
/**
* Class of the object: definition of member functions (top-level), etc.
* Note that using lazy allows to avoid endless recursion here
*/
open val objClass: ObjClass = rootObjectType
open suspend fun plus(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun minus(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun mul(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun div(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun mod(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun logicalNot(context: Context): Obj {
context.raiseNotImplemented()
}
open suspend fun logicalAnd(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun logicalOr(context: Context, other: Obj): Obj {
context.raiseNotImplemented()
}
open suspend fun assign(context: Context, other: Obj): Obj? = null
/**
* a += b
* if( the operation is not defined, it returns null and the compiler would try
* to generate it as 'this = this + other', reassigning its variable
*/
open suspend fun plusAssign(context: Context, other: Obj): Obj? = null
/**
* `-=` operations, see [plusAssign]
*/
open suspend fun minusAssign(context: Context, other: Obj): Obj? = null
open suspend fun mulAssign(context: Context, other: Obj): Obj? = null
open suspend fun divAssign(context: Context, other: Obj): Obj? = null
open suspend fun modAssign(context: Context, other: Obj): Obj? = null
open suspend fun getAndIncrement(context: Context): Obj {
context.raiseNotImplemented()
}
open suspend fun incrementAndGet(context: Context): Obj {
context.raiseNotImplemented()
}
open suspend fun decrementAndGet(context: Context): Obj {
context.raiseNotImplemented()
}
open suspend fun getAndDecrement(context: Context): Obj {
context.raiseNotImplemented()
}
/**
* Convert Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(context: Context): Any? {
return toString()
}
fun willMutate(context: Context) {
if (isFrozen) context.raiseError("attempt to mutate frozen object")
}
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
open suspend fun readField(context: Context, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
}
// could be writable property naturally
// null -> ObjNull.asReadonly
else -> obj
}
}
open suspend fun writeField(context: Context, name: String, newValue: Obj) {
willMutate(context)
val field = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name")
}
open suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNotImplemented("indexing")
}
open suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNotImplemented("indexing")
}
open suspend fun callOn(context: Context): Obj {
context.raiseNotImplemented()
}
suspend fun invoke(context: Context, thisObj: Obj, args: Arguments): Obj =
callOn(context.copy(context.pos, args = args, newThisObj = thisObj))
suspend fun invoke(context: Context, thisObj: Obj, vararg args: Obj): Obj =
callOn(
context.copy(
context.pos,
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
suspend fun invoke(context: Context, thisObj: Obj): Obj =
callOn(
context.copy(
context.pos,
args = Arguments.EMPTY,
newThisObj = thisObj
)
)
suspend fun invoke(context: Context, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(context.copy(atPos, args = args, newThisObj = thisObj))
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
val asMutable: ObjRecord by lazy { ObjRecord(this, true) }
companion object {
val rootObjectType = ObjClass("Obj").apply {
addFn("toString") {
thisObj.asStr
}
addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
// utilities
addFn("let") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
}
addFn("apply") {
val newContext = ( thisObj as? ObjInstance)?.instanceContext ?: this
args.firstAndOnly()
.callOn(newContext)
thisObj
}
addFn("also") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
thisObj
}
}
inline fun from(obj: Any?): Obj {
@Suppress("UNCHECKED_CAST")
return when (obj) {
is Obj -> obj
is Double -> ObjReal(obj)
is Float -> ObjReal(obj.toDouble())
is Int -> ObjInt(obj.toLong())
is Long -> ObjInt(obj)
is String -> ObjString(obj)
is CharSequence -> ObjString(obj.toString())
is Boolean -> ObjBool(obj)
is Set<*> -> ObjSet((obj as Set<Obj>).toMutableSet())
Unit -> ObjVoid
null -> ObjNull
is Iterator<*> -> ObjKotlinIterator(obj)
is Map.Entry<*, *> -> {
obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value)
}
else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
}
}
}
}
@Suppress("unused")
inline fun <reified T> T.toObj(): Obj = Obj.from(this)
@Serializable
@SerialName("void")
object ObjVoid : Obj() {
override fun equals(other: Any?): Boolean {
return other is ObjVoid || other is Unit
}
override suspend fun compareTo(context: Context, other: Obj): Int {
return if (other === this) 0 else -1
}
override fun toString(): String = "void"
}
@Serializable
@SerialName("null")
object ObjNull : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
return if (other === this) 0 else -1
}
override fun equals(other: Any?): Boolean {
return other is ObjNull || other == null
}
override suspend fun readField(context: Context, name: String): ObjRecord {
context.raiseNPE()
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj {
context.raiseNPE()
}
override suspend fun getAt(context: Context, index: Obj): Obj {
context.raiseNPE()
}
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
context.raiseNPE()
}
override suspend fun callOn(context: Context): Obj {
context.raiseNPE()
}
override fun toString(): String = "null"
override suspend fun toKotlin(context: Context): Any? {
return null
}
}
interface Numeric {
val longValue: Long
val doubleValue: Double
val toObjInt: ObjInt
val toObjReal: ObjReal
}
fun Obj.toDouble(): Double =
(this as? Numeric)?.doubleValue
?: (this as? ObjString)?.value?.toDouble()
?: throw IllegalArgumentException("cannot convert to double $this")
@Suppress("unused")
fun Obj.toLong(): Long =
when (this) {
is Numeric -> longValue
is ObjString -> value.toLong()
is ObjChar -> value.code.toLong()
else -> throw IllegalArgumentException("cannot convert to double $this")
}
fun Obj.toInt(): Int = toLong().toInt()
fun Obj.toBool(): Boolean =
(this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this")
data class ObjNamespace(val name: String) : Obj() {
override val objClass by lazy { ObjClass(name) }
override fun inspect(): String = "Ns[$name]"
override fun toString(): String {
return "package $name"
}
}
open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
constructor(name: String, context: Context, message: String) : this(
getOrCreateExceptionClass(name),
context,
message
)
constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(context: Context): Obj {
val message = context.args.getOrNull(0)?.toString() ?: name
return ObjException(this, context, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
}
private val op = ProtectedOp()
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
@OptIn(ExperimentalContracts::class)
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
return op.withLock {
existingErrorClasses.getOrPut(name) {
ExceptionClass(name, Root)
}
}
}
/**
* Get [ObjClass] for error class by name if exists.
*/
@OptIn(ExperimentalContracts::class)
fun getErrorClass(name: String): ObjClass? = op.withLock {
existingErrorClasses[name]
}
fun addExceptionsToContext(context: Context) {
context.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
)) {
context.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullReferenceException(context: Context) : ObjException("NullReferenceException", context, "object is null")
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
class ObjClassCastException(context: Context, message: String) : ObjException("ClassCastException", context, message)
class ObjIndexOutOfBoundsException(context: Context, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", context, message)
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
@Suppress("unused")
class ObjNoSuchElementException(context: Context, message: String = "no such element") :
ObjException("IllegalArgumentException", context, message)
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
ObjException("NoSuchElementException", context, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", context, message)
class ObjIterationFinishedException(context: Context) :
ObjException("IterationEndException", context, "iteration finished")
class ObjAccessException(context: Context, message: String = "access not allowed error") :
ObjException("AccessException", context, message)
class ObjUnknownException(context: Context, message: String = "access not allowed error") :
ObjException("UnknownException", context, message)
@@ -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(context: Context, 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(context: Context): Obj = ObjBool(!value)
override suspend fun logicalAnd(context: Context, other: Obj): Obj = ObjBool(value && other.toBool())
override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool())
override suspend fun toKotlin(context: Context): Any {
return value
}
companion object {
val type = ObjClass("Bool")
}
}
val ObjTrue = ObjBool(true)
val ObjFalse = ObjBool(false)
@@ -1,19 +0,0 @@
package net.sergeych.lyng
class ObjChar(val value: Char): Obj() {
override val objClass: ObjClass = type
override suspend fun compareTo(context: Context, other: Obj): Int =
(other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1
override fun toString(): String = value.toString()
override fun inspect(): String = "'$value'"
companion object {
val type = ObjClass("Char").apply {
addFn("code") { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
}
}
}
@@ -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(context: Context, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(context: Context): Obj {
val instance = ObjInstance(this)
instance.instanceContext = context.copy(newThisObj = instance,args = context.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceContext)
}
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 Context.() -> 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 instanceContext: Context
override suspend fun readField(context: Context, name: String): ObjRecord {
return instanceContext[name]?.let {
if (it.visibility.isPublic)
it
else
context.raiseError(ObjAccessException(context, "can't access non-public field $name"))
}
?: super.readField(context, name)
}
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
instanceContext[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(context, "can't reassign val $name").raise()
if (f.value.assign(context, newValue) == null)
f.value = newValue
} ?: super.writeField(context, name, newValue)
}
override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj =
instanceContext[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(context, this, args)
else
context.raiseError(ObjAccessException(context, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(context, name, args)
private val publicFields: Map<String, ObjRecord>
get() = instanceContext.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(context: Context, 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.instanceContext[f.key]!!.value
val d = a.compareTo(context, b)
if (d != 0) return d
}
return 0
}
}
@@ -1,99 +0,0 @@
package net.sergeych.lyng
data class ObjInt(var value: Long) : Obj(), Numeric {
override val asStr get() = ObjString(value.toString())
override val longValue get() = value
override val doubleValue get() = value.toDouble()
override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value)
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun getAndIncrement(context: Context): Obj {
return ObjInt(value).also { value++ }
}
override suspend fun getAndDecrement(context: Context): Obj {
return ObjInt(value).also { value-- }
}
override suspend fun incrementAndGet(context: Context): Obj {
return ObjInt(++value)
}
override suspend fun decrementAndGet(context: Context): Obj {
return ObjInt(--value)
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is Numeric) return -2
return value.compareTo(other.doubleValue)
}
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override suspend fun plus(context: Context, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value + other.value)
else
ObjReal(this.doubleValue + other.toDouble())
override suspend fun minus(context: Context, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value - other.value)
else
ObjReal(this.doubleValue - other.toDouble())
override suspend fun mul(context: Context, other: Obj): Obj =
if (other is ObjInt) {
ObjInt(this.value * other.value)
} else ObjReal(this.value * other.toDouble())
override suspend fun div(context: Context, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value / other.value)
else ObjReal(this.value / other.toDouble())
override suspend fun mod(context: Context, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value % other.value)
else ObjReal(this.value.toDouble() % other.toDouble())
/**
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
* assignment
*/
override suspend fun assign(context: Context, other: Obj): Obj? {
return if (other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun toKotlin(context: Context): Any {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInt
return value == other.value
}
companion object {
val Zero = ObjInt(0)
val One = ObjInt(1)
val type = ObjClass("Int")
}
}
fun Int.toObj() = ObjInt(this.toLong())
@@ -1,3 +0,0 @@
package net.sergeych.lyng
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -1,114 +0,0 @@
package net.sergeych.lyng
class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
override suspend fun compareTo(context: Context, other: Obj): Int {
if (other !is ObjMapEntry) return -1
val c = key.compareTo(context, other.key)
if (c != 0) return c
return value.compareTo(context, other.value)
}
override suspend fun getAt(context: Context, index: Obj): Obj = when (index.toInt()) {
0 -> key
1 -> value
else -> context.raiseIndexOutOfBounds()
}
override fun toString(): String {
return "$key=>$value"
}
override val objClass = type
companion object {
val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(context: Context): Obj {
return ObjMapEntry(context.requiredArg<Obj>(0), context.requiredArg<Obj>(1))
}
}.apply {
addFn("key") { thisAs<ObjMapEntry>().key }
addFn("value") { thisAs<ObjMapEntry>().value }
addFn("size") { 2.toObj() }
}
}
}
class ObjMap(val map: MutableMap<Obj, Obj>) : Obj() {
override val objClass = type
override suspend fun getAt(context: Context, index: Obj): Obj =
map.getOrElse(index) { context.raiseNoSuchElement() }
override suspend fun contains(context: Context, other: Obj): Boolean {
return other in map
}
override suspend fun compareTo(context: Context, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0
return -1
}
override fun toString(): String = map.toString()
companion object {
suspend fun listToMap(context: Context, list: List<Obj>): MutableMap<Obj, Obj> {
val map = mutableMapOf<Obj, Obj>()
if (list.isEmpty()) return map
val first = list.first()
if (first.isInstanceOf(ObjArray)) {
if (first.invokeInstanceMethod(context, "size").toInt() != 2)
context.raiseIllegalArgument(
"list to construct map entry should exactly be 2 element Array like [key,value], got $list"
)
} else context.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
list.forEach {
map[it.getAt(context, ObjInt.Zero)] = it.getAt(context, ObjInt.One)
}
return map
}
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(context: Context): Obj {
return ObjMap(listToMap(context, context.args.list))
}
}.apply {
addFn("getOrNull") {
val key = args.firstAndOnly(pos)
thisAs<ObjMap>().map.getOrElse(key) { ObjNull }
}
addFn("getOrPut") {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {
val lambda = requiredArg<Statement>(1)
lambda.execute(this)
}
}
addFn("size") {
thisAs<ObjMap>().map.size.toObj()
}
addFn("remove") {
thisAs<ObjMap>().map.remove(requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
addFn("clear") {
thisAs<ObjMap>().map.clear()
thisObj
}
addFn("keys") {
thisAs<ObjMap>().map.keys.toObj()
}
addFn("values") {
ObjList(thisAs<ObjMap>().map.values.toMutableList())
}
addFn("iterator") {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.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()
}
} }
@@ -0,0 +1,201 @@
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.
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
*
* 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.
*
* If you want to create [ModuleScope] by hand, try [currentImportProvider] and [ImportManager.newModule],
* or [ImportManager.newModuleAt].
*
* There are special types of scopes:
*
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
*/
open class Scope(
val parent: Scope?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
var thisObj: Obj = ObjVoid,
var skipScopeCreation: Boolean = false,
) {
open val packageName: String = "<anonymous package>"
constructor(
args: Arguments = Arguments.EMPTY,
pos: Pos = Pos.builtIn,
)
: this(Script.defaultImportManager.copy().newModuleAt(pos), args, pos)
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalStateException(this, message))
fun raiseIllegalAssignment(message: String): Nothing =
raiseError(ObjIllegalAssignmentException(this, message))
@Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}
inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
inline fun <reified T : Obj> requireOnlyArg(): T {
if (args.list.size != 1) raiseError("Expected exactly 1 argument, got ${args.list.size}")
return requiredArg(0)
}
@Suppress("unused")
fun requireExactCount(count: Int) {
if (args.list.size != count) {
raiseError("Expected exactly $count arguments, got ${args.list.size}")
}
}
fun requireNoArgs() {
if( args.list.isNotEmpty())
raiseError("This function does not accept any arguments")
}
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal val objects = mutableMapOf<String, ObjRecord>()
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
?: parent?.get(name)
?: thisObj.objClass.getInstanceMemberOrNull(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy() = Scope(this, args, pos, thisObj)
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord {
return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {
val ns = objects.getOrPut(name) { ObjRecord(ObjNamespace(name), isMutable = false) }.value
return ns.objClass
}
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
addFn<ObjVoid>(*names) {
fn(this)
ObjVoid
}
}
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Scope.() -> T) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = scope.fn()
}
for (name in names) {
addItem(
name,
false,
newFn
)
}
}
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource(), currentImportProvider).execute(this)
suspend fun eval(source: Source): Obj =
Compiler.compile(
source,
currentImportProvider
).execute(this)
fun containsLocal(name: String): Boolean = name in objects
/**
* 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"))
}
/**
* 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(
@@ -9,18 +14,19 @@ class Script(
// private val catchReturn: Boolean = false, // private val catchReturn: Boolean = false,
) : Statement() { ) : Statement() {
override suspend fun execute(context: Context): Obj { override suspend fun execute(scope: Scope): Obj {
var lastResult: Obj = ObjVoid var lastResult: Obj = ObjVoid
for (s in statements) { for (s in statements) {
lastResult = s.execute(context) lastResult = s.execute(scope)
} }
return lastResult return lastResult
} }
suspend fun execute() = execute(defaultContext.copy(pos = pos)) suspend fun execute() = execute(defaultImportManager.newModule())
companion object { companion object {
val defaultContext: Context = Context().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,7 +2,9 @@
package net.sergeych.lyng package net.sergeych.lyng
open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=null) : Exception( import net.sergeych.lyng.obj.ObjException
open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? = null) : Exception(
""" """
$pos: Error: $errorMessage $pos: Error: $errorMessage
@@ -12,4 +14,6 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause cause
) )
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.context.pos, errorObject.message) class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message)
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
@@ -0,0 +1,22 @@
package net.sergeych.lyng
interface SecurityManager {
/**
* Check that any symbol from the corresponding module can be imported. If it returns false,
* the module will not be imported and no further processing will be done
*/
fun canImportModule(name: String): Boolean
/**\
* if [canImportModule] this method allows fine-grained control over symbols.
*/
fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true
companion object {
val allowAll: SecurityManager = object : SecurityManager {
override fun canImportModule(name: String): Boolean = true
override fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true
}
}
}
@@ -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")
}
} }
@@ -1,9 +1,14 @@
@file:Suppress("unused")
package net.sergeych.lyng package net.sergeych.lyng
// this is highly experimental and subject to complete redesign // this is highly experimental and subject to complete redesign
// very soon // very soon
sealed class TypeDecl { sealed class TypeDecl(val isNullable:Boolean = false) {
// ?? // ??
// data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl() // data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
object Obj : TypeDecl() object TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true)
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
} }
@@ -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")
}
@@ -0,0 +1,546 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
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 }
var isFrozen: Boolean = false
private val monitor = Mutex()
// private val memberMutex = Mutex()
internal var parentInstances: MutableList<Obj> = mutableListOf()
private val opInstances = ProtectedOp()
open fun inspect(): String = toString()
/**
* Some objects are by-value, historically [ObjInt] and [ObjReal] are usually treated as such.
* When initializing a var with it, by value objects must be copied. By-reference ones aren't.
*
* Almost all objects are by-reference.
*/
open fun byValueCopy(): Obj = this
@Suppress("SuspiciousEqualsCombination")
fun isInstanceOf(someClass: Obj) = someClass === objClass ||
objClass.allParentsSet.contains(someClass) ||
someClass == rootObjectType
suspend fun invokeInstanceMethod(scope: Scope, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(scope, name, Arguments(args.toList()))
suspend inline fun <reified T : Obj> callMethod(
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): 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(
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null
): Obj =
objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args)
?: onNotFoundResult
?: scope.raiseSymbolNotFound(name)
open suspend fun getInstanceMethod(
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): Obj =
// note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(scope.pos, name).value
fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value
// methods that to override
open suspend fun compareTo(scope: Scope, other: Obj): Int {
scope.raiseNotImplemented()
}
open suspend fun contains(scope: Scope, other: Obj): Boolean {
return invokeInstanceMethod(scope, "contains", other).toBool()
}
open val asStr: ObjString by lazy {
if (this is ObjString) this else ObjString(this.toString())
}
/**
* Class of the object: definition of member functions (top-level), etc.
* Note that using lazy allows to avoid endless recursion here
*/
open val objClass: ObjClass = rootObjectType
open suspend fun plus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun minus(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun negate(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun mul(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun div(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun mod(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalNot(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalAnd(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun logicalOr(scope: Scope, other: Obj): Obj {
scope.raiseNotImplemented()
}
open suspend fun assign(scope: Scope, other: Obj): Obj? = null
/**
* a += b
* if( the operation is not defined, it returns null and the compiler would try
* to generate it as 'this = this + other', reassigning its variable
*/
open suspend fun plusAssign(scope: Scope, other: Obj): Obj? = null
/**
* `-=` operations, see [plusAssign]
*/
open suspend fun minusAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun mulAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun divAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun modAssign(scope: Scope, other: Obj): Obj? = null
open suspend fun getAndIncrement(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun incrementAndGet(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun decrementAndGet(scope: Scope): Obj {
scope.raiseNotImplemented()
}
open suspend fun getAndDecrement(scope: Scope): Obj {
scope.raiseNotImplemented()
}
/**
* Convert Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(scope: Scope): Any? {
return toString()
}
fun willMutate(scope: Scope) {
if (isFrozen) scope.raiseError("attempt to mutate frozen object")
}
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
open suspend fun readField(scope: Scope, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(scope.copy(scope.pos, newThisObj = this)), obj.isMutable)
}
// could be writable property naturally
// null -> ObjNull.asReadonly
else -> obj
}
}
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
willMutate(scope)
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
}
open suspend fun getAt(scope: Scope, index: Obj): Obj {
scope.raiseNotImplemented("indexing")
}
suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong()))
open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
scope.raiseNotImplemented("indexing")
}
open suspend fun callOn(scope: Scope): Obj {
scope.raiseNotImplemented()
}
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(scope.pos, args = args, newThisObj = thisObj))
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn(
scope.copy(
scope.pos,
args = Arguments(args.toList()),
newThisObj = thisObj
)
)
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
callOn(
scope.copy(
scope.pos,
args = Arguments.EMPTY,
newThisObj = thisObj
)
)
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(scope.copy(atPos, args = args, newThisObj = thisObj))
val asReadonly: ObjRecord by lazy { ObjRecord(this, false) }
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 {
val rootObjectType = ObjClass("Obj").apply {
addFn("toString") {
thisObj.asStr
}
addFn("contains") {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
// utilities
addFn("let") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
}
addFn("apply") {
val newContext = (thisObj as? ObjInstance)?.instanceScope ?: this
args.firstAndOnly()
.callOn(newContext)
thisObj
}
addFn("also") {
args.firstAndOnly().callOn(copy(Arguments(thisObj)))
thisObj
}
addFn("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 {
@Suppress("UNCHECKED_CAST")
return when (obj) {
is Obj -> obj
is Double -> ObjReal(obj)
is Float -> ObjReal(obj.toDouble())
is Int -> ObjInt(obj.toLong())
is Long -> ObjInt(obj)
is String -> ObjString(obj)
is CharSequence -> ObjString(obj.toString())
is Boolean -> ObjBool(obj)
is Set<*> -> ObjSet((obj as Set<Obj>).toMutableSet())
Unit -> ObjVoid
null -> ObjNull
is Iterator<*> -> ObjKotlinIterator(obj)
is Map.Entry<*, *> -> {
obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value)
}
else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
}
}
}
}
fun Double.toObj(): Obj = ObjReal(this)
@Suppress("unused")
inline fun <reified T> T.toObj(): Obj = Obj.from(this)
@Serializable
@SerialName("void")
object ObjVoid : Obj() {
override fun equals(other: Any?): Boolean {
return other is ObjVoid || other is Unit
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other === this) 0 else -1
}
override fun toString(): String = "void"
}
@Serializable
@SerialName("null")
object ObjNull : Obj() {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other === this) 0 else -1
}
override fun equals(other: Any?): Boolean {
return other is ObjNull || other == null
}
override suspend fun readField(scope: Scope, name: String): ObjRecord {
scope.raiseNPE()
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: Obj?): Obj {
scope.raiseNPE()
}
override suspend fun getAt(scope: Scope, index: Obj): Obj {
scope.raiseNPE()
}
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
scope.raiseNPE()
}
override suspend fun callOn(scope: Scope): Obj {
scope.raiseNPE()
}
override fun toString(): String = "null"
override suspend fun toKotlin(scope: Scope): Any? {
return null
}
override suspend fun lynonType(): LynonType {
return LynonType.Null
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
if (lynonType == null) {
encoder.putBit(0)
}
}
override val objClass: ObjClass by lazy {
object : ObjClass("Null") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
if (lynonType == LynonType.Null)
return this@ObjNull
else
scope.raiseIllegalState("can't deserialize null directly or with wrong type: ${lynonType}")
}
}
}
}
interface Numeric {
val longValue: Long
val doubleValue: Double
val toObjInt: ObjInt
val toObjReal: ObjReal
}
fun Obj.toDouble(): Double =
(this as? Numeric)?.doubleValue
?: (this as? ObjString)?.value?.toDouble()
?: throw IllegalArgumentException("cannot convert to double $this")
@Suppress("unused")
fun Obj.toLong(): Long =
when (this) {
is Numeric -> longValue
is ObjString -> value.toLong()
is ObjChar -> value.code.toLong()
else -> throw IllegalArgumentException("cannot convert to double $this")
}
fun Obj.toInt(): Int = toLong().toInt()
fun Obj.toBool(): Boolean =
(this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this")
data class ObjNamespace(val name: String) : Obj() {
override val objClass by lazy { ObjClass(name) }
override fun inspect(): String = "Ns[$name]"
override fun toString(): String {
return "package $name"
}
}
open class ObjException(exceptionClass: ExceptionClass, val scope: Scope, val message: String) : Obj() {
constructor(name: String, scope: Scope, message: String) : this(
getOrCreateExceptionClass(name),
scope,
message
)
constructor(scope: Scope, message: String) : this(Root, scope, message)
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${scope.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(scope: Scope): Obj {
val message = scope.args.getOrNull(0)?.toString() ?: name
return ObjException(this, scope, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
}
private val op = ProtectedOp()
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
@OptIn(ExperimentalContracts::class)
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
return op.withLock {
existingErrorClasses.getOrPut(name) {
ExceptionClass(name, Root)
}
}
}
/**
* Get [ObjClass] for error class by name if exists.
*/
@OptIn(ExperimentalContracts::class)
fun getErrorClass(name: String): ObjClass? = op.withLock {
existingErrorClasses[name]
}
fun addExceptionsToContext(scope: Scope) {
scope.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
)) {
scope.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
class ObjAssertionFailedException(scope: Scope, message: String) :
ObjException("AssertionFailedException", scope, message)
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", scope, message)
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
ObjException("IllegalStateException", scope, message)
@Suppress("unused")
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
ObjException("NoSuchElementException", scope, message)
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", scope, message)
class ObjIterationFinishedException(scope: Scope) :
ObjException("IterationEndException", scope, "iteration finished")
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
ObjException("AccessException", scope, message)
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
ObjException("UnknownException", scope, message)
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
ObjException("IllegalOperationException", 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() {
@@ -7,9 +9,9 @@ class ObjArrayIterator(val array: Obj) : Obj() {
private var nextIndex = 0 private var nextIndex = 0
private var lastIndex = 0 private var lastIndex = 0
suspend fun init(context: Context) { suspend fun init(scope: Scope) {
nextIndex = 0 nextIndex = 0
lastIndex = array.invokeInstanceMethod(context, "size").toInt() lastIndex = array.invokeInstanceMethod(scope, "size").toInt()
ObjVoid ObjVoid
} }
@@ -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()
)
}
}
}
}
@@ -0,0 +1,34 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class ObjChar(val value: Char): Obj() {
override val objClass: ObjClass = type
override suspend fun compareTo(scope: Scope, other: Obj): Int =
(other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1
override fun toString(): String = value.toString()
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 {
val type = ObjClass("Char").apply {
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())
// }
}
}
}
@@ -0,0 +1,142 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
override val asStr get() = ObjString(value.toString())
override val longValue get() = value
override val doubleValue get() = value.toDouble()
override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value)
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun getAndIncrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value++ }
}
override suspend fun getAndDecrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value-- }
}
override suspend fun incrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(++value)
}
override suspend fun decrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(--value)
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2
return value.compareTo(other.doubleValue)
}
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override suspend fun plus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value + other.value)
else
ObjReal(this.doubleValue + other.toDouble())
override suspend fun minus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value - other.value)
else
ObjReal(this.doubleValue - other.toDouble())
override suspend fun mul(scope: Scope, other: Obj): Obj =
if (other is ObjInt) {
ObjInt(this.value * other.value)
} else ObjReal(this.value * other.toDouble())
override suspend fun div(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value / other.value)
else ObjReal(this.value / other.toDouble())
override suspend fun mod(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value % other.value)
else ObjReal(this.value.toDouble() % other.toDouble())
/**
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
* assignment
*/
override suspend fun assign(scope: Scope, other: Obj): Obj? {
return if (!isConst && other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun toKotlin(scope: Scope): Any {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInt
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 {
val Zero = ObjInt(0, true)
val One = ObjInt(1, true)
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")
}
}
}
}
fun Int.toObj() = ObjInt(this.toLong())
@@ -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.
@@ -48,12 +51,20 @@ val ObjIterable by lazy {
} }
addFn("toMap") { addFn("toMap") {
val result = mutableListOf<Obj>() val result = ObjMap()
val it = thisObj.invokeInstanceMethod(this, "iterator") thisObj.toFlow(this).collect { pair ->
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { result.map[pair.getAt(this,0)] = pair.getAt(this, 1)
result += it.invokeInstanceMethod(this, "next")
} }
ObjMap(ObjMap.listToMap(this, result)) result
}
addFn("associateBy") {
val association = requireOnlyArg<Statement>()
val result = ObjMap()
thisObj.toFlow(this).collect {
result.map[association.call(this, it)] = it
}
result
} }
addFn("forEach", isOpen = true) { addFn("forEach", isOpen = true) {
@@ -67,12 +78,10 @@ val ObjIterable by lazy {
} }
addFn("map", isOpen = true) { addFn("map", isOpen = true) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0) val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>() val result = mutableListOf<Obj>()
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { thisObj.toFlow(this).collect {
val x = it.invokeInstanceMethod(this, "next") result += fn.call(this, it)
result += fn.execute(this.copy(Arguments(listOf(x))))
} }
ObjList(result) ObjList(result)
} }
@@ -0,0 +1,3 @@
package net.sergeych.lyng.obj
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -1,6 +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 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;
@@ -36,4 +40,19 @@ class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
} }
} }
}
/**
* Convert Lyng's Iterable to Kotlin's Flow.
*
* As Lyng is totally asynchronous, its iterator can't be trivially converted to Kotlin's synchronous iterator.
* It is, though, trivially convertible to Kotlin's Flow.
*/
fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
val iterator = invokeInstanceMethod(scope, "iterator")
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
val next = iterator.getInstanceMethod(scope, "next")
while (hasNext.invoke(scope, iterator).toBool()) {
emit(next.invoke(scope, iterator))
}
} }
@@ -1,17 +1,18 @@
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() }
}]" }]"
override suspend fun getAt(context: Context, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
return when (index) { return when (index) {
is ObjInt -> { is ObjInt -> {
list[index.toInt()] list[index.toInt()]
@@ -47,23 +48,22 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
} }
else -> context.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}") else -> scope.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}")
} }
} }
override suspend fun putAt(context: Context, 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(context: Context, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjList) return -2 if (other !is ObjList) return -2
val mySize = list.size val mySize = list.size
val otherSize = other.list.size val otherSize = other.list.size
val commonSize = minOf(mySize, otherSize) val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) { for (i in 0..<commonSize) {
if (list[i].compareTo(context, other.list[i]) != 0) { if (list[i].compareTo(scope, other.list[i]) != 0) {
return list[i].compareTo(context, other.list[i]) return list[i].compareTo(scope, other.list[i])
} }
} }
// equal so far, longer is greater: // equal so far, longer is greater:
@@ -74,44 +74,44 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
} }
override suspend fun plus(context: Context, other: Obj): Obj = override suspend fun plus(scope: Scope, other: Obj): Obj =
when { when {
other is ObjList -> other is ObjList ->
ObjList((list + other.list).toMutableList()) ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) -> { other.isInstanceOf(ObjIterable) -> {
val l = other.callMethod<ObjList>(context, "toList") val l = other.callMethod<ObjList>(scope, "toList")
ObjList((list + l.list).toMutableList()) ObjList((list + l.list).toMutableList())
} }
else -> else ->
context.raiseError("'+': can't concatenate $this with $other") scope.raiseError("'+': can't concatenate $this with $other")
} }
override suspend fun plusAssign(context: Context, other: Obj): Obj { override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
// optimization // optimization
if (other is ObjList) { if (other is ObjList) {
list += other.list list += other.list
return this return this
} }
if (other.isInstanceOf(ObjIterable)) { if (other.isInstanceOf(ObjIterable)) {
val otherList = other.invokeInstanceMethod(context, "toList") as ObjList val otherList = other.invokeInstanceMethod(scope, "toList") as ObjList
list += otherList.list list += otherList.list
} else } else
list += other list += other
return this return this
} }
override suspend fun contains(context: Context, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
return list.contains(other) return list.contains(other)
} }
override val objClass: ObjClass override val objClass: ObjClass
get() = type get() = type
override suspend fun toKotlin(context: Context): Any { override suspend fun toKotlin(scope: Scope): Any {
return list.map { it.toKotlin(context) } return list.map { it.toKotlin(scope) }
} }
override fun hashCode(): Int { override fun hashCode(): 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
@@ -0,0 +1,173 @@
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() {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjMapEntry) return -1
val c = key.compareTo(scope, other.key)
if (c != 0) return c
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()) {
0 -> key
1 -> value
else -> scope.raiseIndexOutOfBounds()
}
override fun toString(): String {
return "$key=>$value"
}
override val objClass = type
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope,key)
encoder.encodeAny(scope,value)
}
companion object {
val type = object : ObjClass("MapEntry", ObjArray) {
override suspend fun callOn(scope: Scope): Obj {
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 {
addFn("key") { thisAs<ObjMapEntry>().key }
addFn("value") { thisAs<ObjMapEntry>().value }
addFn("size") { 2.toObj() }
}
}
}
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override val objClass = type
override suspend fun getAt(scope: Scope, index: Obj): Obj =
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 {
return other in map
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0
return -1
}
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 {
suspend fun listToMap(scope: Scope, list: List<Obj>): MutableMap<Obj, Obj> {
val map = mutableMapOf<Obj, Obj>()
if (list.isEmpty()) return map
val first = list.first()
if (first.isInstanceOf(ObjArray)) {
if (first.invokeInstanceMethod(scope, "size").toInt() != 2)
scope.raiseIllegalArgument(
"list to construct map entry should exactly be 2 element Array like [key,value], got $list"
)
} else scope.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first")
list.forEach {
map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One)
}
return map
}
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(scope: Scope): Obj {
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 {
addFn("getOrNull") {
val key = args.firstAndOnly(pos)
thisAs<ObjMap>().map.getOrElse(key) { ObjNull }
}
addFn("getOrPut") {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {
val lambda = requiredArg<Statement>(1)
lambda.execute(this)
}
}
addFn("size") {
thisAs<ObjMap>().map.size.toObj()
}
addFn("remove") {
thisAs<ObjMap>().map.remove(requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
addFn("clear") {
thisAs<ObjMap>().map.clear()
thisObj
}
addFn("keys") {
thisAs<ObjMap>().map.keys.toObj()
}
addFn("values") {
ObjList(thisAs<ObjMap>().map.values.toMutableList())
}
addFn("iterator") {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.iterator())
}
}
}
}
@@ -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,10 +17,35 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
return result.toString() return result.toString()
} }
suspend fun containsRange(context: Context, other: ObjRange): Boolean { /**
* 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 {
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:
if (other.start != null && start.compareTo(context, other.start) > 0) return false if (other.start != null && start.compareTo(scope, other.start) > 0) return false
} }
if (end != null) { if (end != null) {
// same with the end: if it is open, it can't be contained in ours: // same with the end: if it is open, it can't be contained in ours:
@@ -26,16 +53,16 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
// both exists, now there could be 4 cases: // both exists, now there could be 4 cases:
return when { return when {
other.isEndInclusive && isEndInclusive -> other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(scope, other.end) >= 0
!other.isEndInclusive && !isEndInclusive -> !other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(scope, other.end) >= 0
other.isEndInclusive && !isEndInclusive -> other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) > 0 end.compareTo(scope, other.end) > 0
!other.isEndInclusive && isEndInclusive -> !other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0 end.compareTo(scope, other.end) >= 0
else -> throw IllegalStateException("unknown comparison") else -> throw IllegalStateException("unknown comparison")
} }
@@ -43,17 +70,17 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
return true return true
} }
override suspend fun contains(context: Context, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
if (other is ObjRange) if (other is ObjRange)
return containsRange(context, other) return containsRange(scope, other)
if (start == null && end == null) return true if (start == null && end == null) return true
if (start != null) { if (start != null) {
if (start.compareTo(context, other) > 0) return false if (start.compareTo(scope, other) > 0) return false
} }
if (end != null) { if (end != null) {
val cmp = end.compareTo(context, other) val cmp = end.compareTo(scope, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
} }
return true return true
@@ -67,13 +94,34 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
start is ObjChar && end is ObjChar start is ObjChar && end is ObjChar
} }
override suspend fun compareTo(context: Context, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let { return (other as? ObjRange)?.let {
if( start == other.start && end == other.end ) 0 else -1 if( start == other.start && end == other.end ) 0 else -1
} }
?: -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() {
@@ -8,7 +10,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
override val objClass: ObjClass = type override val objClass: ObjClass = type
fun Context.init() { fun Scope.init() {
if (self.start == null || self.end == null) if (self.start == null || self.end == null)
raiseError("next is only available for finite ranges") raiseError("next is only available for finite ranges")
isCharRange = self.isCharRange isCharRange = self.isCharRange
@@ -24,7 +26,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
fun hasNext(): Boolean = nextIndex < lastIndex fun hasNext(): Boolean = nextIndex < lastIndex
fun next(context: Context): Obj = fun next(scope: Scope): Obj =
if (nextIndex < lastIndex) { if (nextIndex < lastIndex) {
val x = if (self.isEndInclusive) val x = if (self.isEndInclusive)
self.start!!.toLong() + nextIndex++ self.start!!.toLong() + nextIndex++
@@ -33,7 +35,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x) if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
} }
else { else {
context.raiseError(ObjIterationFinishedException(context)) scope.raiseError(ObjIterationFinishedException(scope))
} }
companion object { companion object {
@@ -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
@@ -14,7 +20,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
override fun byValueCopy(): Obj = ObjReal(value) override fun byValueCopy(): Obj = ObjReal(value)
override suspend fun compareTo(context: Context, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2 if (other !is Numeric) return -2
return value.compareTo(other.doubleValue) return value.compareTo(other.doubleValue)
} }
@@ -25,25 +31,25 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
return value.hashCode() return value.hashCode()
} }
override suspend fun plus(context: Context, other: Obj): Obj = override suspend fun plus(scope: Scope, other: Obj): Obj =
ObjReal(this.value + other.toDouble()) ObjReal(this.value + other.toDouble())
override suspend fun minus(context: Context, other: Obj): Obj = override suspend fun minus(scope: Scope, other: Obj): Obj =
ObjReal(this.value - other.toDouble()) ObjReal(this.value - other.toDouble())
override suspend fun mul(context: Context, other: Obj): Obj = override suspend fun mul(scope: Scope, other: Obj): Obj =
ObjReal(this.value * other.toDouble()) ObjReal(this.value * other.toDouble())
override suspend fun div(context: Context, other: Obj): Obj = override suspend fun div(scope: Scope, other: Obj): Obj =
ObjReal(this.value / other.toDouble()) ObjReal(this.value / other.toDouble())
override suspend fun mod(context: Context, other: Obj): Obj = override suspend fun mod(scope: Scope, other: Obj): Obj =
ObjReal(this.value % other.toDouble()) ObjReal(this.value % other.toDouble())
/** /**
* Returns unboxed Double value * Returns unboxed Double value
*/ */
override suspend fun toKotlin(context: Context): Any { override suspend fun toKotlin(scope: Scope): Any {
return value return value
} }
@@ -56,14 +62,30 @@ 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) {
(it.thisObj as ObjReal).value.roundToLong().toObj() (it.thisObj as ObjReal).value.roundToLong().toObj()
}, },
) )
addFn("toInt") {
ObjInt(thisAs<ObjReal>().value.toLong())
}
} }
} }
} }
@@ -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,14 +1,19 @@
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() {
override val objClass = type override val objClass = type
override suspend fun contains(context: Context, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
return set.contains(other) return set.contains(other)
} }
override suspend fun plus(context: Context, other: Obj): Obj { override suspend fun plus(scope: Scope, other: Obj): Obj {
return ObjSet( return ObjSet(
if (other is ObjSet) if (other is ObjSet)
(set + other.set).toMutableSet() (set + other.set).toMutableSet()
@@ -17,7 +22,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
) )
} }
override suspend fun plusAssign(context: Context, other: Obj): Obj { override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
when (other) { when (other) {
is ObjSet -> { is ObjSet -> {
set += other.set set += other.set
@@ -29,9 +34,9 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
else -> { else -> {
if (other.isInstanceOf(ObjIterable)) { if (other.isInstanceOf(ObjIterable)) {
val i = other.invokeInstanceMethod(context, "iterable") val i = other.invokeInstanceMethod(scope, "iterable")
while (i.invokeInstanceMethod(context, "hasNext").toBool()) { while (i.invokeInstanceMethod(scope, "hasNext").toBool()) {
set += i.invokeInstanceMethod(context, "next") set += i.invokeInstanceMethod(scope, "next")
} }
} }
set += other set += other
@@ -40,16 +45,16 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
return this return this
} }
override suspend fun mul(context: Context, other: Obj): Obj { override suspend fun mul(scope: Scope, other: Obj): Obj {
return if (other is ObjSet) { return if (other is ObjSet) {
ObjSet(set.intersect(other.set).toMutableSet()) ObjSet(set.intersect(other.set).toMutableSet())
} else } else
context.raiseIllegalArgument("set operator * requires another set") scope.raiseIllegalArgument("set operator * requires another set")
} }
override suspend fun minus(context: Context, other: Obj): Obj { override suspend fun minus(scope: Scope, other: Obj): Obj {
if (other !is ObjSet) if (other !is ObjSet)
context.raiseIllegalArgument("set operator - requires another set") scope.raiseIllegalArgument("set operator - requires another set")
return ObjSet(set.minus(other.set).toMutableSet()) return ObjSet(set.minus(other.set).toMutableSet())
} }
@@ -57,7 +62,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
return "Set(${set.joinToString(", ")})" return "Set(${set.joinToString(", ")})"
} }
override suspend fun compareTo(context: Context, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other !is ObjSet) -1 return if (other !is ObjSet) -1
else { else {
if (set == other.set) 0 if (set == other.set) 0
@@ -65,13 +70,35 @@ 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 {
val type = object : ObjClass("Set", ObjCollection) { val type = object : ObjClass("Set", ObjCollection) {
override suspend fun callOn(context: Context): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjSet(context.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,8 +20,7 @@ data class ObjString(val value: String) : Obj() {
// return i // return i
// } // }
override suspend fun compareTo(scope: Scope, other: Obj): Int {
override suspend fun compareTo(context: Context, 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)
} }
@@ -32,37 +36,40 @@ data class ObjString(val value: String) : Obj() {
override val objClass: ObjClass override val objClass: ObjClass
get() = type get() = type
override suspend fun plus(context: Context, other: Obj): Obj { override suspend fun plus(scope: Scope, other: Obj): Obj {
return ObjString(value + other.asStr.value) return ObjString(value + other.asStr.value)
} }
override suspend fun getAt(context: Context, 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))
} }
context.raiseIllegalArgument("String index must be Int or Range") scope.raiseIllegalArgument("String index must be Int or Range")
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return value.hashCode() return value.hashCode()
} }
override suspend fun callOn(context: Context): Obj { override suspend fun callOn(scope: Scope): Obj {
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray())) return ObjString(this.value.sprintf(*scope.args
.toKotlinList(scope)
.map { if (it == null) "null" else it }
.toTypedArray()))
} }
override suspend fun contains(context: Context, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
return if (other is ObjString) return if (other is ObjString)
value.contains(other.value) value.contains(other.value)
else if (other is ObjChar) else if (other is ObjChar)
value.contains(other.value) value.contains(other.value)
else context.raiseIllegalArgument("String.contains can't take $other") else scope.raiseIllegalArgument("String.contains can't take $other")
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -74,8 +81,20 @@ 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") {
ObjInt(thisAs<ObjString>().value.toLong())
}
addFn("startsWith") { addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value)) ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
} }
@@ -111,7 +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()) }
} }
} }
} }
@@ -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,31 +1,38 @@
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() {
override val objClass: ObjClass = type override val objClass: ObjClass = type
abstract val pos: Pos abstract val pos: Pos
abstract suspend fun execute(context: Context): Obj abstract suspend fun execute(scope: Scope): Obj
override suspend fun compareTo(context: Context,other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
throw UnsupportedOperationException("not comparable") throw UnsupportedOperationException("not comparable")
} }
override suspend fun callOn(context: Context): Obj { override suspend fun callOn(scope: Scope): Obj {
return execute(context) return execute(scope)
} }
override fun toString(): String = "Callable@${this.hashCode()}" override fun toString(): String = "Callable@${this.hashCode()}"
@@ -34,6 +41,8 @@ abstract class Statement(
val type = ObjClass("Callable") val type = ObjClass("Callable")
} }
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args)))
} }
fun Statement.raise(text: String): Nothing { fun Statement.raise(text: String): Nothing {
@@ -45,16 +54,20 @@ fun Statement.require(cond: Boolean, message: () -> String) {
if (!cond) raise(message()) if (!cond) raise(message())
} }
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Context) -> Obj): Statement = fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Scope) -> Obj): Statement =
object : Statement(isStaticConst, isConst) { object : Statement(isStaticConst, isConst) {
override val pos: Pos = pos override val pos: Pos = pos
override suspend fun execute(context: Context): Obj = f(context) override suspend fun execute(scope: Scope): Obj = f(scope)
} }
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Context.() -> Obj): Statement = fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Scope.() -> Obj): Statement =
object : Statement(isStaticConst, isConst) { object : Statement(isStaticConst, isConst) {
override val pos: Pos = Pos.builtIn override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj = f(context) 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())
}
}
+468 -88
View File
@@ -1,7 +1,10 @@
package io.github.kotlin.fibonacci import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
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 {
@@ -172,55 +175,55 @@ class ScriptTest {
@Test @Test
fun varsAndConstsTest() = runTest { fun varsAndConstsTest() = runTest {
val context = Context(pos = Pos.builtIn) val scope = Scope(pos = Pos.builtIn)
assertEquals( assertEquals(
ObjInt(3L), context.eval( ObjInt(3L), scope.eval(
""" """
val a = 17 val a = 17
var b = 3 var b = 3
""".trimIndent() """.trimIndent()
) )
) )
assertEquals(17, context.eval("a").toInt()) assertEquals(17, scope.eval("a").toInt())
assertEquals(20, context.eval("b + a").toInt()) assertEquals(20, scope.eval("b + a").toInt())
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
context.eval("a = 10") scope.eval("a = 10")
} }
assertEquals(17, context.eval("a").toInt()) assertEquals(17, scope.eval("a").toInt())
assertEquals(5, context.eval("b = a - 7 - 5").toInt()) assertEquals(5, scope.eval("b = a - 7 - 5").toInt())
assertEquals(5, context.eval("b").toInt()) assertEquals(5, scope.eval("b").toInt())
} }
@Test @Test
fun functionTest() = runTest { fun functionTest() = runTest {
val context = Context(pos = Pos.builtIn) val scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
fun foo(a, b) { fun foo(a, b) {
a + b a + b
} }
""".trimIndent() """.trimIndent()
) )
assertEquals(17, context.eval("foo(3,14)").toInt()) assertEquals(17, scope.eval("foo(3,14)").toInt())
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
assertEquals(17, context.eval("foo(3)").toInt()) assertEquals(17, scope.eval("foo(3)").toInt())
} }
context.eval( scope.eval(
""" """
fn bar(a, b=10) { fn bar(a, b=10) {
a + b + 1 a + b + 1
} }
""".trimIndent() """.trimIndent()
) )
assertEquals(10, context.eval("bar(3, 6)").toInt()) assertEquals(10, scope.eval("bar(3, 6)").toInt())
assertEquals(14, context.eval("bar(3)").toInt()) assertEquals(14, scope.eval("bar(3)").toInt())
} }
@Test @Test
fun simpleClosureTest() = runTest { fun simpleClosureTest() = runTest {
val context = Context(pos = Pos.builtIn) val scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
var global = 10 var global = 10
@@ -229,16 +232,16 @@ class ScriptTest {
} }
""".trimIndent() """.trimIndent()
) )
assertEquals(27, context.eval("foo(3,14)").toInt()) assertEquals(27, scope.eval("foo(3,14)").toInt())
context.eval("global = 20") scope.eval("global = 20")
assertEquals(37, context.eval("foo(3,14)").toInt()) assertEquals(37, scope.eval("foo(3,14)").toInt())
} }
@Test @Test
fun nullAndVoidTest() = runTest { fun nullAndVoidTest() = runTest {
val context = Context(pos = Pos.builtIn) val scope = Scope(pos = Pos.builtIn)
assertEquals(ObjVoid, context.eval("void")) assertEquals(ObjVoid, scope.eval("void"))
assertEquals(ObjNull, context.eval("null")) assertEquals(ObjNull, scope.eval("null"))
} }
@Test @Test
@@ -345,8 +348,8 @@ class ScriptTest {
@Test @Test
fun ifTest() = runTest { fun ifTest() = runTest {
// if - single line // if - single line
var context = Context(pos = Pos.builtIn) var scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
fn test1(n) { fn test1(n) {
var result = "more" var result = "more"
@@ -356,12 +359,12 @@ class ScriptTest {
} }
""".trimIndent() """.trimIndent()
) )
assertEquals("enough", context.eval("test1(11)").toString()) assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString()) assertEquals("more", scope.eval("test1(1)").toString())
// if - multiline (block) // if - multiline (block)
context = Context(pos = Pos.builtIn) scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
fn test1(n) { fn test1(n) {
var prefix = "answer: " var prefix = "answer: "
@@ -375,12 +378,12 @@ class ScriptTest {
} }
""".trimIndent() """.trimIndent()
) )
assertEquals("answer: enough", context.eval("test1(11)").toString()) assertEquals("answer: enough", scope.eval("test1(11)").toString())
assertEquals("answer: more", context.eval("test1(1)").toString()) assertEquals("answer: more", scope.eval("test1(1)").toString())
// else single line1 // else single line1
context = Context(pos = Pos.builtIn) scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
fn test1(n) { fn test1(n) {
if( n >= 10 ) if( n >= 10 )
@@ -390,12 +393,12 @@ class ScriptTest {
} }
""".trimIndent() """.trimIndent()
) )
assertEquals("enough", context.eval("test1(11)").toString()) assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString()) assertEquals("more", scope.eval("test1(1)").toString())
// if/else with blocks // if/else with blocks
context = Context(pos = Pos.builtIn) scope = Scope(pos = Pos.builtIn)
context.eval( scope.eval(
""" """
fn test1(n) { fn test1(n) {
if( n > 20 ) { if( n > 20 ) {
@@ -409,9 +412,9 @@ class ScriptTest {
} }
""".trimIndent() """.trimIndent()
) )
assertEquals("enough", context.eval("test1(11)").toString()) assertEquals("enough", scope.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString()) assertEquals("more", scope.eval("test1(1)").toString())
assertEquals("too much", context.eval("test1(100)").toString()) assertEquals("too much", scope.eval("test1(100)").toString())
} }
@Test @Test
@@ -511,13 +514,13 @@ class ScriptTest {
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd ), ttEnd
) )
var c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) var c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
assertEquals(ObjInt(1), c["a"]?.value) assertEquals(ObjInt(1), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value) assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(3), c["c"]?.value) assertEquals(ObjInt(3), c["c"]?.value)
// less args: error // less args: error
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2).map { it.toObj() })) c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(1, 2).map { it.toObj() }))
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
pa.assignToContext(c) pa.assignToContext(c)
} }
@@ -534,7 +537,7 @@ class ScriptTest {
assertEquals(ObjInt(2), c["b"]?.value) assertEquals(ObjInt(2), c["b"]?.value)
assertEquals(ObjInt(100), c["c"]?.value) assertEquals(ObjInt(100), c["c"]?.value)
// enough args. default value is ignored: // enough args. default value is ignored:
c = Context(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() })) c = Scope(pos = Pos.builtIn, args = Arguments.from(listOf(10, 2, 5).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
assertEquals(ObjInt(10), c["a"]?.value) assertEquals(ObjInt(10), c["a"]?.value)
assertEquals(ObjInt(2), c["b"]?.value) assertEquals(ObjInt(2), c["b"]?.value)
@@ -552,17 +555,17 @@ class ScriptTest {
ArgsDeclaration.Item("b", isEllipsis = true), ArgsDeclaration.Item("b", isEllipsis = true),
), ttEnd ), ttEnd
) )
var c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) var c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)") c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [2,3] )") c.eval("assert( b == [2,3] )")
c = Context(args = Arguments.from(listOf(1, 2).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(1, 2).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( a, 1 ); println(b)") c.eval("assertEquals( a, 1 ); println(b)")
c.eval("assertEquals( b, [2] )") c.eval("assertEquals( b, [2] )")
c = Context(args = Arguments.from(listOf(1).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(1).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assert( a == 1 ); println(b)") c.eval("assert( a == 1 ); println(b)")
c.eval("assert( b == [] )") c.eval("assert( b == [] )")
@@ -578,32 +581,32 @@ class ScriptTest {
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd ), ttEnd
) )
var c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() })) var c = Scope(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( a,[0,1] )") c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( a,[1] )") c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( a,[] )") c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(3).map { it.toObj() }))
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
pa.assignToContext(c) pa.assignToContext(c)
} }
} }
@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(
@@ -613,28 +616,28 @@ class ScriptTest {
ArgsDeclaration.Item("c"), ArgsDeclaration.Item("c"),
), ttEnd ), ttEnd
) )
var c = Context(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() })) var c = Scope(args = Arguments.from(listOf(-1, 0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( i, -1 )") c.eval("assertEquals( i, -1 )")
c.eval("assertEquals( a,[0,1] )") c.eval("assertEquals( a,[0,1] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(0, 1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( i, 0 )") c.eval("assertEquals( i, 0 )")
c.eval("assertEquals( a,[1] )") c.eval("assertEquals( a,[1] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(1, 2, 3).map { it.toObj() }))
pa.assignToContext(c) pa.assignToContext(c)
c.eval("assertEquals( i, 1)") c.eval("assertEquals( i, 1)")
c.eval("assertEquals( a,[] )") c.eval("assertEquals( a,[] )")
c.eval("assertEquals( b, 2 )") c.eval("assertEquals( b, 2 )")
c.eval("assertEquals( c, 3 )") c.eval("assertEquals( c, 3 )")
c = Context(args = Arguments.from(listOf(2, 3).map { it.toObj() })) c = Scope(args = Arguments.from(listOf(2, 3).map { it.toObj() }))
assertFailsWith<ExecutionError> { assertFailsWith<ExecutionError> {
pa.assignToContext(c) pa.assignToContext(c)
} }
@@ -705,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()
@@ -732,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()
) )
@@ -755,7 +750,7 @@ class ScriptTest {
@Test @Test
fun testIncr() = runTest { fun testIncr() = runTest {
val c = Context() val c = Scope()
c.eval("var x = 10") c.eval("var x = 10")
assertEquals(10, c.eval("x++").toInt()) assertEquals(10, c.eval("x++").toInt())
assertEquals(11, c.eval("x++").toInt()) assertEquals(11, c.eval("x++").toInt())
@@ -767,9 +762,9 @@ class ScriptTest {
@Test @Test
fun testDecr() = runTest { fun testDecr() = runTest {
val c = Context() 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())
@@ -778,7 +773,7 @@ class ScriptTest {
@Test @Test
fun testDecrIncr() = runTest { fun testDecrIncr() = runTest {
val c = Context() val c = Scope()
c.eval("var x = 9") c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt()) assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt()) assertEquals(10, c.eval("x++").toInt())
@@ -792,7 +787,7 @@ class ScriptTest {
@Test @Test
fun testDecrIncr2() = runTest { fun testDecrIncr2() = runTest {
val c = Context() val c = Scope()
c.eval("var x = 9") c.eval("var x = 9")
assertEquals(9, c.eval("x--").toInt()) assertEquals(9, c.eval("x--").toInt())
assertEquals(8, c.eval("x--").toInt()) assertEquals(8, c.eval("x--").toInt())
@@ -809,7 +804,7 @@ class ScriptTest {
@Test @Test
fun testDecrIncr3() = runTest { fun testDecrIncr3() = runTest {
val c = Context() val c = Scope()
c.eval("var x = 9") c.eval("var x = 9")
assertEquals(9, c.eval("x++").toInt()) assertEquals(9, c.eval("x++").toInt())
assertEquals(10, c.eval("x++").toInt()) assertEquals(10, c.eval("x++").toInt())
@@ -821,7 +816,7 @@ class ScriptTest {
@Test @Test
fun testIncrAndDecr() = runTest { fun testIncrAndDecr() = runTest {
val c = Context() val c = Scope()
assertEquals( assertEquals(
"8", c.eval( "8", c.eval(
""" """
@@ -856,7 +851,7 @@ class ScriptTest {
@Test @Test
fun testAssign1() = runTest { fun testAssign1() = runTest {
assertEquals(10, eval("var x = 5; x=10; x").toInt()) assertEquals(10, eval("var x = 5; x=10; x").toInt())
val ctx = Context() val ctx = Scope()
ctx.eval( ctx.eval(
""" """
var a = 1 var a = 1
@@ -870,7 +865,7 @@ class ScriptTest {
@Test @Test
fun testAssign2() = runTest { fun testAssign2() = runTest {
val ctx = Context() val ctx = Scope()
ctx.eval("var x = 10") ctx.eval("var x = 10")
assertEquals(14, ctx.eval("x += 4").toInt()) assertEquals(14, ctx.eval("x += 4").toInt())
assertEquals(14, ctx.eval("x").toInt()) assertEquals(14, ctx.eval("x").toInt())
@@ -888,7 +883,7 @@ class ScriptTest {
@Test @Test
fun testVals() = runTest { fun testVals() = runTest {
val cxt = Context() val cxt = Scope()
cxt.eval("val x = 11") cxt.eval("val x = 11")
assertEquals(11, cxt.eval("x").toInt()) assertEquals(11, cxt.eval("x").toInt())
assertFails { cxt.eval("x = 12") } assertFails { cxt.eval("x = 12") }
@@ -1435,7 +1430,7 @@ class ScriptTest {
@Test @Test
fun testSimpleStruct() = runTest { fun testSimpleStruct() = runTest {
val c = Context() val c = Scope()
c.eval( c.eval(
""" """
class Point(x,y) class Point(x,y)
@@ -1456,7 +1451,7 @@ class ScriptTest {
@Test @Test
fun testNonAssignalbeFieldInStruct() = runTest { fun testNonAssignalbeFieldInStruct() = runTest {
val c = Context() val c = Scope()
c.eval( c.eval(
""" """
class Point(x,y) class Point(x,y)
@@ -1473,7 +1468,7 @@ class ScriptTest {
@Test @Test
fun testStructBodyVal() = runTest { fun testStructBodyVal() = runTest {
val c = Context() val c = Scope()
c.eval( c.eval(
""" """
class Point(x,y) { class Point(x,y) {
@@ -1495,7 +1490,7 @@ class ScriptTest {
@Test @Test
fun testStructBodyFun() = runTest { fun testStructBodyFun() = runTest {
val c = Context() val c = Scope()
c.eval( c.eval(
""" """
class Point(x,y) { class Point(x,y) {
@@ -1515,7 +1510,7 @@ class ScriptTest {
@Test @Test
fun testPrivateConstructorParams() = runTest { fun testPrivateConstructorParams() = runTest {
val c = Context() val c = Scope()
c.eval( c.eval(
""" """
class Point(private var x,y) class Point(private var x,y)
@@ -1870,7 +1865,7 @@ class ScriptTest {
@Test @Test
fun testTryFinally() = runTest { fun testTryFinally() = runTest {
val c = Context() val c = Scope()
assertFails { assertFails {
c.eval( c.eval(
""" """
@@ -1893,7 +1888,7 @@ class ScriptTest {
@Test @Test
fun testThrowFromKotlin() = runTest { fun testThrowFromKotlin() = runTest {
val c = Context() val c = Scope()
c.addFn("callThrow") { c.addFn("callThrow") {
raiseIllegalArgument("fromKotlin") raiseIllegalArgument("fromKotlin")
} }
@@ -2248,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 {
@@ -2269,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 {
@@ -2283,6 +2291,378 @@ class ScriptTest {
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void >>> void
""".trimIndent()
)
}
@Test
fun testExtend() = runTest() {
eval(
"""
fun Int.isEven() {
this % 2 == 0
}
fun Object.isInteger() {
when(this) {
is Int -> true
is Real -> toInt() == this
is String -> toReal().isInteger()
else -> false
}
}
assert( 4.isEven() )
assert( !5.isEven() )
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
""".trimIndent()
)
}
@Test
fun testToFlow() = runTest() {
val c = Scope()
val arr = c.eval("[1,2,3]")
// array is iterable so we can:
assertEquals(listOf(1, 2, 3), arr.toFlow(c).map { it.toInt() }.toList())
}
@Test
fun testAssociateBy() = runTest() {
eval(
"""
val m = [123, 456].associateBy { "k:%s"(it) }
println(m)
assertEquals(123, m["k:123"])
assertEquals(456, m["k:456"])
"""
)
listOf(1, 2, 3).associateBy { it * 10 }
}
// @Test
// fun testImports1() = runTest() {
// val foosrc = """
// package lyng.foo
//
// fun foo() { "foo1" }
// """.trimIndent()
// val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc)))
// assertNotNull(pm.modules["lyng.foo"])
// assertIs<ModuleScope>(pm.modules["lyng.foo"]!!.deferredModule.await())
// assertEquals("foo1", pm.modules["lyng.foo"]!!.deferredModule.await().eval("foo()").toString())
// }
@Test
fun testImports2() = runTest() {
val foosrc = """
package lyng.foo
fun foo() { "foo1" }
""".trimIndent()
val pm = InlineSourcesImportProvider(listOf(Source("foosrc", foosrc)))
val src = """
import lyng.foo
foo()
""".trimIndent().toSource("test")
val scope = ModuleScope(pm, src)
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()) """.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)
}
} }
@@ -0,0 +1,76 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class TypesTest {
@Test
fun testTypeCollection1() = runTest {
eval("""
class Point(x: Real, y: Real)
assert(Point(1,2).x == 1)
assert(Point(1,2).y == 2)
assert(Point(1,2) is Point)
""".trimIndent())
}
@Test
fun testTypeCollection2() = runTest {
eval("""
fun fn1(x: Real, y: Real): Real { x + y }
""".trimIndent())
}
@Test
fun testTypeCollection3() = runTest {
eval("""
class Test(a: Int) {
fun fn1(x: Real, y: Real): Real { x + y }
}
""".trimIndent())
}
@Test
fun testExternDeclarations() = runTest {
eval("""
extern fun foo1(a: String): Void
assertThrows { foo1("1") }
class Test(a: Int) {
extern fun fn1(x: Real, y: Real): Real
// extern val b: Int
}
// println("1")
val t = Test(0)
// println(t.b)
// println("2")
assertThrows {
t.fn1(1,2)
}
// println("4")
""".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())
}
}
+28 -7
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.Context import net.sergeych.lyng.Scope
import net.sergeych.lyng.ObjVoid 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(">>> ")) {
@@ -158,10 +163,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
suspend fun DocTest.test(context: Context = Context()) { suspend fun DocTest.test(scope: Scope = Scope()) {
val collectedOutput = StringBuilder() val collectedOutput = StringBuilder()
val currentTest = this val currentTest = this
context.apply { scope.apply {
addFn("println") { addFn("println") {
if( bookMode ) { if( bookMode ) {
println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}") println("${currentTest.fileNamePart}:${currentTest.line}> ${args.joinToString(" "){it.asStr.value}}")
@@ -177,7 +182,7 @@ suspend fun DocTest.test(context: Context = Context()) {
} }
var error: Throwable? = null var error: Throwable? = null
val result = try { val result = try {
context.eval(code) scope.eval(code)
} catch (e: Throwable) { } catch (e: Throwable) {
error = e error = e
null null
@@ -204,10 +209,10 @@ suspend fun DocTest.test(context: Context = Context()) {
} }
suspend fun runDocTests(fileName: String, bookMode: Boolean = false) { suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
val bookContext = Context() val bookScope = Scope()
var count = 0 var count = 0
parseDocTests(fileName, bookMode).collect { dt -> parseDocTests(fileName, bookMode).collect { dt ->
if (bookMode) dt.test(bookContext) if (bookMode) dt.test(bookScope)
else dt.test() else dt.test()
count++ count++
} }
@@ -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
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import net.sergeych.lyng.Context import net.sergeych.lyng.Scope
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.extension import kotlin.io.path.extension
@@ -13,7 +13,7 @@ suspend fun executeSampleTests(fileName: String) {
Files.readString(Paths.get(fileName)) Files.readString(Paths.get(fileName))
} }
runBlocking { runBlocking {
val c = Context() val c = Scope()
val start = Clock.System.now() val start = Clock.System.now()
c.eval(sample) c.eval(sample)
val time = Clock.System.now() - start val time = Clock.System.now() - start
File diff suppressed because it is too large. Load diff