Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de71c7c8ec | ||
|
|
be370dfdef | ||
|
|
53a6a88924 | ||
|
|
2c2dae4d85 | ||
|
|
63b2808109 | ||
|
|
4165da5e81 | ||
|
|
af463163bb | ||
|
|
6df06a6911 | ||
|
|
f805e1ee82 | ||
|
|
790cce0d24 | ||
|
|
2339130241 | ||
|
|
d7bd159fcb | ||
|
|
12b209c724 | ||
|
|
20181c63a1 | ||
|
|
405ff2ec2b | ||
|
|
a9f65bdbe3 | ||
|
|
6ab438b1f6 | ||
|
|
cffe4eaffc | ||
|
|
7aee25ffef | ||
|
|
f3d766d1b1 | ||
|
|
34bc7297bd | ||
|
|
23dafff453 | ||
|
|
77f9191387 | ||
|
|
f26ee7cd7c | ||
|
|
d969993997 | ||
|
|
987b80e44d | ||
|
|
5848adca61 | ||
|
|
f1ae4b2d23 | ||
|
|
30b6ef235b | ||
|
|
9771b40c98 | ||
|
|
230cb0a067 | ||
|
|
732d8f3877 | ||
|
|
23006b5caa | ||
|
|
26282d3e22 | ||
|
|
ce4ed5c819 | ||
|
|
612c0fb7b9 | ||
|
|
ef6bc5c468 | ||
|
|
1e2cb5e420 | ||
|
|
868379ce22 | ||
|
|
f37354f382 | ||
|
|
ddbcbf9e4e | ||
|
|
6cf99fbd13 | ||
|
|
950e8301c3 | ||
|
|
ea2cf1f373 | ||
|
|
eee6d75587 | ||
|
|
a8067d0a6b | ||
|
|
75a6f20150 | ||
|
|
d969d6d572 |
No files matched your search
@@ -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
|
||||||
@@ -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?
|
||||||
|
|
||||||
|
|||||||
Executable
+14
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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 |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
Regular → Executable
+1
@@ -1,2 +1,3 @@
|
|||||||
|
#!/bin/env jlyng
|
||||||
|
|
||||||
println("Hello, world!");
|
println("Hello, world!");
|
||||||
+215
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
+52
-27
@@ -14,7 +14,7 @@ __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]
|
- 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 +530,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 +642,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 +668,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 +745,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 +765,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 +1100,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 +1217,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 +1277,5 @@ 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
|
||||||
@@ -8,6 +8,7 @@ kotlinx-coroutines = "1.10.1"
|
|||||||
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" }
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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.1-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,13 @@ 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,9 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import net.sergeych.lyng.obj.*
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
class Script(
|
class Script(
|
||||||
@@ -9,18 +12,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 +116,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 +131,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 {
|
||||||
@@ -170,8 +180,34 @@ class Script(
|
|||||||
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
-1
@@ -1,4 +1,4 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
val ObjArray by lazy {
|
val ObjArray by lazy {
|
||||||
|
|
||||||
+5
-3
@@ -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
-1
@@ -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,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())
|
||||||
+19
-10
@@ -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") }
|
||||||
+20
-1
@@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+33
-34
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-12
@@ -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") {
|
||||||
+6
-4
@@ -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 {
|
||||||
+31
-9
@@ -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"
|
||||||
|
}
|
||||||
+41
-14
@@ -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()
|
||||||
+42
-16
@@ -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,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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,10 @@ 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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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,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
Reference in new issue
Block a user