Compare commits
43
Commits
0.6.7
...
0.8.0-SNAPSHOT
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
/kotlin-js-store/yarn.lock
|
||||
/test.lyng
|
||||
/sample_texts/1.txt.gz
|
||||
@@ -75,14 +75,16 @@ runBlocking {
|
||||
|
||||
### Exchanging information
|
||||
|
||||
Script is executed over some `Context`. Create instance of the context,
|
||||
add your specific vars and functions to it, an call over it:
|
||||
Script is executed over some `Scope`. Create instance,
|
||||
add your specific vars and functions to it, and call:
|
||||
|
||||
```kotlin
|
||||
|
||||
import com.sun.source.tree.Scope
|
||||
import new.sergeych.lyng.*
|
||||
|
||||
// simple function
|
||||
val context = Context().apply {
|
||||
val scope = Scope().apply {
|
||||
addFn("addArgs") {
|
||||
var sum = 0.0
|
||||
for (a in args) sum += a.toDouble()
|
||||
@@ -99,9 +101,9 @@ val context = Context().apply {
|
||||
}
|
||||
}
|
||||
// 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?
|
||||
|
||||
|
||||
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
|
||||
|
||||
| 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.toSet(): Set
|
||||
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.forEach(block: (Any?)->Void ): Void
|
||||
fun Iterable.map(block: (Any?)->Void ): List
|
||||
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
|
||||
|
||||
|
||||
## 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
|
||||
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`)
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"] )
|
||||
val map = Map( "foo" => 1, "bar" => "buzz" )
|
||||
assert(map is Map)
|
||||
assert(map.size == 2)
|
||||
assert(map is Iterable)
|
||||
>>> 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:
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
assert( map["bar"] == "buzz")
|
||||
assert( map[42] == "answer" )
|
||||
assertThrows { map["nonexistent"] }
|
||||
assertEquals( null, map["nonexisting"])
|
||||
assert( map.getOrNull(101) == null )
|
||||
assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" )
|
||||
// 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()`
|
||||
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") )
|
||||
assert( map.getOrNull("foo") == null)
|
||||
assert( map.size == 2 )
|
||||
|
||||
+36
-3
@@ -160,6 +160,40 @@ For example, for our class Point:
|
||||
Point(1,1+1)
|
||||
>>> Point(x=1,y=2)
|
||||
|
||||
# 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
|
||||
|
||||
## Basic principles:
|
||||
@@ -216,11 +250,10 @@ Note `Real` class: it is global variable for Real class; there are such class in
|
||||
assert('$'::class == Char)
|
||||
>>> void
|
||||
|
||||
More complex is singleton classes, because you don't need to compare their class
|
||||
instances and generally don't need them at all, these are normally just Obj:
|
||||
Singleton classes also have class:
|
||||
|
||||
null::class
|
||||
>>> Obj
|
||||
>>> Null
|
||||
|
||||
At this time, `Obj` can't be accessed as a class.
|
||||
|
||||
|
||||
+2
-2
@@ -16,9 +16,9 @@ you can use it's class to ensure type:
|
||||
## Member functions
|
||||
|
||||
| name | meaning | type |
|
||||
|-----------------|------------------------------------|------|
|
||||
|-----------------|-------------------------------------------------------------|------|
|
||||
| `.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!");
|
||||
+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.
|
||||
|
||||
|
||||
+32
-7
@@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one:
|
||||
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
|
||||
- [OOP notes](OOP.md), [exception handling](exceptions_handling.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)
|
||||
|
||||
# Expressions
|
||||
@@ -530,7 +530,8 @@ The simplest way to concatenate lists is using `+` and `+=`:
|
||||
list += [2, 1]
|
||||
// or you can append a single element:
|
||||
list += "end"
|
||||
assert( list == [1, 2, 2, 1, "end"])
|
||||
assertEquals( list, [1, 2, 2, 1, "end"])
|
||||
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:
|
||||
@@ -641,6 +642,11 @@ You can get ranges to extract a portion from a list:
|
||||
assertEquals( [2,3], list[1..<3])
|
||||
>>> void
|
||||
|
||||
# Buffers
|
||||
|
||||
[Buffer] is a special implementation of an [Array] of unsigned bytes, in the
|
||||
[separate file](Buffer.md).
|
||||
|
||||
# Sets
|
||||
|
||||
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.
|
||||
|
||||
|
||||
# Flow control operators
|
||||
|
||||
## if-then-else
|
||||
@@ -741,11 +746,11 @@ You can thest that _when expression_ is _contained_, or not contained, in some o
|
||||
Typical builtin types that are containers (e.g. support `conain`):
|
||||
|
||||
| class | notes |
|
||||
|------------|--------------------------------------------|
|
||||
|------------|------------------------------------------------|
|
||||
| Collection | contains an element (1) |
|
||||
| Array | faster maybe that Collection'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) |
|
||||
|
||||
(1)
|
||||
@@ -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
|
||||
>>> 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
|
||||
as expected:
|
||||
|
||||
@@ -1092,6 +1100,17 @@ and you can use ranges in for-loops:
|
||||
|
||||
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
|
||||
|
||||
// single line comment
|
||||
@@ -1199,7 +1218,7 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
||||
Typical set of String functions includes:
|
||||
|
||||
| fun/prop | description / notes |
|
||||
|--------------------|------------------------------------------------------------|
|
||||
|-------------------|------------------------------------------------------------|
|
||||
| lower() | change case to unicode upper |
|
||||
| upper() | change case to unicode lower |
|
||||
| startsWith(prefix) | true if starts with a prefix |
|
||||
@@ -1214,8 +1233,13 @@ Typical set of String functions includes:
|
||||
| [Range] | substring at range |
|
||||
| s1 + s2 | concatenation |
|
||||
| s1 += s2 | self-modifying concatenation |
|
||||
| toReal() | attempts to parse string as a Real value |
|
||||
| toInt() | parse string to Int value |
|
||||
| characters() | create [List] of characters (1) |
|
||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||
|
||||
|
||||
(1)
|
||||
: List is mutable therefore a new copy is created on each call.
|
||||
|
||||
|
||||
|
||||
@@ -1254,3 +1278,4 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
|
||||
[Set]: Set.md
|
||||
[Map]: Map.md
|
||||
[Buffer]: Buffer.md
|
||||
@@ -8,6 +8,7 @@ kotlinx-coroutines = "1.10.1"
|
||||
mp_bintools = "0.1.12"
|
||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||
okioVersion = "3.10.2"
|
||||
compiler = "3.2.0-alpha11"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||
compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" }
|
||||
|
||||
[plugins]
|
||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package net.sergeych
|
||||
|
||||
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.multiple
|
||||
import com.github.ajalt.clikt.parameters.arguments.optional
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
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.Path.Companion.toPath
|
||||
import okio.SYSTEM
|
||||
@@ -31,7 +38,7 @@ data class CommandResult(
|
||||
val error: String
|
||||
)
|
||||
|
||||
val baseContext = Context().apply {
|
||||
val baseScope = Scope().apply {
|
||||
addFn("exit") {
|
||||
exit(requireOnlyArg<ObjInt>().toInt())
|
||||
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
|
||||
|
||||
@@ -55,7 +78,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
||||
|
||||
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.
|
||||
|
||||
@@ -74,7 +97,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
||||
val objargs = mutableListOf<String>()
|
||||
script?.let { objargs += it }
|
||||
objargs += args
|
||||
baseContext.addConst(
|
||||
baseScope.addConst(
|
||||
"ARGV", ObjList(
|
||||
objargs.map { ObjString(it) }.toMutableList()
|
||||
)
|
||||
@@ -82,7 +105,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
||||
launcher {
|
||||
// there is no script name, it is a first argument instead:
|
||||
processErrors {
|
||||
baseContext.eval(execute!!)
|
||||
baseScope.eval(execute!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +121,7 @@ class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
||||
)
|
||||
echoFormattedHelp()
|
||||
} else {
|
||||
baseContext.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
||||
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
||||
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) {
|
||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
||||
fileSource.buffer().use { bs ->
|
||||
@@ -118,7 +148,7 @@ suspend fun executeFile(fileName: String) {
|
||||
text = text.substring(pos + 1)
|
||||
}
|
||||
processErrors {
|
||||
Compiler().compile(Source(fileName, text)).execute(baseContext)
|
||||
baseScope.eval(Source(fileName, text))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.Lyng
|
||||
import net.sergeych.runMain
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Lyng({ runBlocking { it() } }).main(args)
|
||||
runMain(args)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.Lyng
|
||||
import net.sergeych.runMain
|
||||
|
||||
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
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.6.7-SNAPSHOT"
|
||||
version = "0.8.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
@@ -20,7 +20,7 @@ plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
// 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"
|
||||
`maven-publish`
|
||||
}
|
||||
@@ -99,6 +99,7 @@ android {
|
||||
}
|
||||
dependencies {
|
||||
implementation(libs.firebase.crashlytics.buildtools)
|
||||
implementation(libs.compiler)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
* 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(
|
||||
context: Context,
|
||||
arguments: Arguments = context.args,
|
||||
scope: Scope,
|
||||
arguments: Arguments = scope.args,
|
||||
defaultAccessType: AccessType = AccessType.Var,
|
||||
defaultVisibility: Visibility = Visibility.Public
|
||||
defaultVisibility: Visibility = Visibility.Public,
|
||||
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
|
||||
) {
|
||||
fun assign(a: Item, value: Obj) {
|
||||
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
|
||||
a.visibility ?: defaultVisibility)
|
||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
|
||||
a.visibility ?: defaultVisibility,
|
||||
recordType = defaultRecordType)
|
||||
}
|
||||
|
||||
// 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
|
||||
val value = when {
|
||||
i < callArgs.size -> callArgs[i]
|
||||
a.defaultValue != null -> a.defaultValue.execute(context)
|
||||
a.defaultValue != null -> a.defaultValue.execute(scope)
|
||||
else -> {
|
||||
println("callArgs: ${callArgs.joinToString()}")
|
||||
println("tailBlockMode: ${arguments.tailBlockMode}")
|
||||
context.raiseIllegalArgument("too few arguments for the call")
|
||||
// println("callArgs: ${callArgs.joinToString()}")
|
||||
// println("tailBlockMode: ${arguments.tailBlockMode}")
|
||||
scope.raiseIllegalArgument("too few arguments for the call")
|
||||
}
|
||||
}
|
||||
assign(a, value)
|
||||
@@ -76,8 +83,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
||||
callArgs[j--]
|
||||
}
|
||||
|
||||
a.defaultValue != null -> a.defaultValue.execute(context)
|
||||
else -> context.raiseIllegalArgument("too few arguments for the call")
|
||||
a.defaultValue != null -> a.defaultValue.execute(scope)
|
||||
else -> scope.raiseIllegalArgument("too few arguments for the call")
|
||||
}
|
||||
assign(a, value)
|
||||
i--
|
||||
@@ -98,7 +105,7 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
||||
processEllipsis(leftIndex, end)
|
||||
} else {
|
||||
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(
|
||||
val name: String,
|
||||
val type: TypeDecl = TypeDecl.Obj,
|
||||
val type: TypeDecl = TypeDecl.TypeAny,
|
||||
val pos: Pos = Pos.builtIn,
|
||||
val isEllipsis: Boolean = false,
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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)
|
||||
|
||||
suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMode: Boolean): Arguments {
|
||||
suspend fun Collection<ParsedArgument>.toArguments(scope: Scope, tailBlockMode: Boolean): Arguments {
|
||||
val list = mutableListOf<Obj>()
|
||||
|
||||
for (x in this) {
|
||||
val value = x.value.execute(context)
|
||||
val value = x.value.execute(scope)
|
||||
if (x.isSplat) {
|
||||
when {
|
||||
value is ObjList -> {
|
||||
@@ -14,11 +18,11 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
else -> context.raiseClassCastError("expected list of objects for splat argument")
|
||||
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||
}
|
||||
} else
|
||||
list.add(value)
|
||||
@@ -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].
|
||||
*/
|
||||
suspend fun toKotlinList(context: Context): List<Any?> {
|
||||
return list.map { it.toKotlin(context) }
|
||||
suspend fun toKotlinList(scope: Scope): List<Any?> {
|
||||
return list.map { it.toKotlin(scope) }
|
||||
}
|
||||
|
||||
fun inspect(): String = list.joinToString(", ") { it.inspect() }
|
||||
|
||||
|
||||
companion object {
|
||||
val EMPTY = Arguments(emptyList())
|
||||
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
|
||||
|
||||
internal class CompilerContext(val tokens: List<Token>) {
|
||||
class CompilerContext(val tokens: List<Token>) {
|
||||
val labels = mutableSetOf<String>()
|
||||
|
||||
var breakFound = false
|
||||
private set
|
||||
|
||||
var loopLevel = 0
|
||||
private set
|
||||
|
||||
inline fun <T> parseLoop(f: () -> T): Pair<Boolean, T> {
|
||||
if (++loopLevel == 0) breakFound = false
|
||||
@@ -53,6 +51,20 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
|
||||
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.
|
||||
* @param errorMessage message to throw if next token is not `tokenType`
|
||||
@@ -85,7 +97,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()
|
||||
return if (t.type == typeId) {
|
||||
f(t)
|
||||
@@ -96,7 +108,6 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun addBreak() {
|
||||
breakFound = true
|
||||
}
|
||||
@@ -153,7 +164,9 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
* Note that [Token.Type.EOF] is not considered a whitespace token.
|
||||
*/
|
||||
fun skipWsTokens(): Token {
|
||||
while( current().type in wstokens ) next()
|
||||
while( current().type in wstokens ) {
|
||||
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
|
||||
|
||||
import net.sergeych.lyng.obj.Accessor
|
||||
|
||||
sealed class ListEntry {
|
||||
data class Element(val accessor: Accessor) : ListEntry()
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
|
||||
class LoopBreakContinueException(
|
||||
val doContinue: Boolean,
|
||||
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) {
|
||||
'/' -> {
|
||||
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 -> {
|
||||
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 l = pos.line
|
||||
do {
|
||||
@@ -479,4 +479,10 @@ private class Parser(fromPos: Pos) {
|
||||
return null
|
||||
}
|
||||
|
||||
init {
|
||||
// skip shebang
|
||||
if( pos.readFragment("#!") )
|
||||
loadToEndOfLine()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
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))
|
||||
|
||||
@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
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lynon.ObjLynonClass
|
||||
import kotlin.math.*
|
||||
|
||||
class Script(
|
||||
@@ -9,18 +12,19 @@ class Script(
|
||||
// private val catchReturn: Boolean = false,
|
||||
) : Statement() {
|
||||
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
var lastResult: Obj = ObjVoid
|
||||
for (s in statements) {
|
||||
lastResult = s.execute(context)
|
||||
lastResult = s.execute(scope)
|
||||
}
|
||||
return lastResult
|
||||
}
|
||||
|
||||
suspend fun execute() = execute(defaultContext.copy(pos = pos))
|
||||
suspend fun execute() = execute(defaultImportManager.newModule())
|
||||
|
||||
companion object {
|
||||
val defaultContext: Context = Context().apply {
|
||||
|
||||
private val rootScope: Scope = Scope(null).apply {
|
||||
ObjException.addExceptionsToContext(this)
|
||||
addFn("println") {
|
||||
for ((i, a) in args.withIndex()) {
|
||||
@@ -127,6 +131,12 @@ class Script(
|
||||
if( a.compareTo(this, b) != 0 )
|
||||
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") {
|
||||
val code = requireOnlyArg<Statement>()
|
||||
val result =try {
|
||||
@@ -170,8 +180,34 @@ class Script(
|
||||
getOrCreateNamespace("Math").apply {
|
||||
addConst("PI", pi)
|
||||
}
|
||||
}
|
||||
|
||||
val defaultImportManager: ImportManager by lazy {
|
||||
ImportManager(rootScope, SecurityManager.allowAll).apply {
|
||||
addPackage("lyng.buffer") {
|
||||
it.addConst("Buffer", ObjBuffer.type)
|
||||
it.addConst("MutableBuffer", ObjMutableBuffer.type)
|
||||
}
|
||||
addPackage("lyng.serialization") {
|
||||
it.addConst("Lynon", ObjLynonClass)
|
||||
}
|
||||
addPackage("lyng.time") {
|
||||
it.addConst("Instant", ObjInstant.type)
|
||||
it.addConst("Duration", ObjDuration.type)
|
||||
it.addFn("delay") {
|
||||
val a = args.firstAndOnly()
|
||||
when(a) {
|
||||
is ObjInt -> delay(a.value * 1000)
|
||||
is ObjReal -> delay((a.value * 1000).roundToLong())
|
||||
is ObjDuration -> delay(a.duration)
|
||||
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect()}")
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.ObjException
|
||||
|
||||
open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? = null) : Exception(
|
||||
"""
|
||||
$pos: Error: $errorMessage
|
||||
@@ -12,4 +14,6 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
// this is highly experimental and subject to complete redesign
|
||||
// very soon
|
||||
sealed class TypeDecl {
|
||||
sealed class TypeDecl(val isNullable:Boolean = false) {
|
||||
// ??
|
||||
// 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 {
|
||||
|
||||
+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() {
|
||||
|
||||
@@ -7,9 +9,9 @@ class ObjArrayIterator(val array: Obj) : Obj() {
|
||||
private var nextIndex = 0
|
||||
private var lastIndex = 0
|
||||
|
||||
suspend fun init(context: Context) {
|
||||
suspend fun init(scope: Scope) {
|
||||
nextIndex = 0
|
||||
lastIndex = array.invokeInstanceMethod(context, "size").toInt()
|
||||
lastIndex = array.invokeInstanceMethod(scope, "size").toInt()
|
||||
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,110 @@
|
||||
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
|
||||
|
||||
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>()
|
||||
private val classMembers = 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)
|
||||
}
|
||||
|
||||
fun createClassField(
|
||||
name: String,
|
||||
initialValue: Obj,
|
||||
isMutable: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
pos: Pos = Pos.builtIn
|
||||
) {
|
||||
val existing = classMembers[name]
|
||||
if( existing != null)
|
||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||
classMembers[name] = ObjRecord(initialValue, isMutable, visibility)
|
||||
}
|
||||
|
||||
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
||||
createField(name, statement { code() }, isOpen)
|
||||
}
|
||||
|
||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||
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 {
|
||||
classMembers[name]?.let {
|
||||
return it
|
||||
}
|
||||
return super.readField(scope, name)
|
||||
}
|
||||
|
||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: Obj?): Obj {
|
||||
return classMembers[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`]
|
||||
@@ -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 = vars[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,28 @@
|
||||
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) {
|
||||
|
||||
// val onDeserilaized =
|
||||
|
||||
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.
|
||||
@@ -48,12 +51,20 @@ val ObjIterable by lazy {
|
||||
}
|
||||
|
||||
addFn("toMap") {
|
||||
val result = mutableListOf<Obj>()
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
result += it.invokeInstanceMethod(this, "next")
|
||||
val result = ObjMap()
|
||||
thisObj.toFlow(this).collect { pair ->
|
||||
result.map[pair.getAt(this,0)] = pair.getAt(this, 1)
|
||||
}
|
||||
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) {
|
||||
@@ -67,12 +78,10 @@ val ObjIterable by lazy {
|
||||
}
|
||||
|
||||
addFn("map", isOpen = true) {
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
val fn = requiredArg<Statement>(0)
|
||||
val result = mutableListOf<Obj>()
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
val x = it.invokeInstanceMethod(this, "next")
|
||||
result += fn.execute(this.copy(Arguments(listOf(x))))
|
||||
thisObj.toFlow(this).collect {
|
||||
result += fn.call(this, it)
|
||||
}
|
||||
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")
|
||||
|
||||
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;
|
||||
@@ -37,3 +41,18 @@ 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() {
|
||||
|
||||
init {
|
||||
for (p in objClass.parents)
|
||||
parentInstances.add(p.defaultInstance())
|
||||
}
|
||||
|
||||
override fun toString(): String = "[${
|
||||
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) {
|
||||
is ObjInt -> {
|
||||
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) {
|
||||
val i = index
|
||||
list[i] = newValue
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
list[index.toInt()] = 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
|
||||
val mySize = list.size
|
||||
val otherSize = other.list.size
|
||||
val commonSize = minOf(mySize, otherSize)
|
||||
for (i in 0..<commonSize) {
|
||||
if (list[i].compareTo(context, other.list[i]) != 0) {
|
||||
return list[i].compareTo(context, other.list[i])
|
||||
if (list[i].compareTo(scope, other.list[i]) != 0) {
|
||||
return list[i].compareTo(scope, other.list[i])
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
other is ObjList ->
|
||||
ObjList((list + other.list).toMutableList())
|
||||
|
||||
other.isInstanceOf(ObjIterable) -> {
|
||||
val l = other.callMethod<ObjList>(context, "toList")
|
||||
val l = other.callMethod<ObjList>(scope, "toList")
|
||||
ObjList((list + l.list).toMutableList())
|
||||
}
|
||||
|
||||
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
|
||||
if (other is ObjList) {
|
||||
list += other.list
|
||||
return this
|
||||
}
|
||||
if (other.isInstanceOf(ObjIterable)) {
|
||||
val otherList = other.invokeInstanceMethod(context, "toList") as ObjList
|
||||
val otherList = other.invokeInstanceMethod(scope, "toList") as ObjList
|
||||
list += otherList.list
|
||||
} else
|
||||
list += other
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun contains(context: Context, other: Obj): Boolean {
|
||||
override suspend fun contains(scope: Scope, other: Obj): Boolean {
|
||||
return list.contains(other)
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
return list.map { it.toKotlin(context) }
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return list.map { it.toKotlin(scope) }
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -128,24 +128,23 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
return list == other.list
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("List", ObjArray).apply {
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
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",
|
||||
statement {
|
||||
(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",
|
||||
statement {
|
||||
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() {
|
||||
|
||||
@@ -15,10 +17,35 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
||||
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) {
|
||||
// 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) {
|
||||
// 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:
|
||||
return when {
|
||||
other.isEndInclusive && isEndInclusive ->
|
||||
end.compareTo(context, other.end) >= 0
|
||||
end.compareTo(scope, other.end) >= 0
|
||||
|
||||
!other.isEndInclusive && !isEndInclusive ->
|
||||
end.compareTo(context, other.end) >= 0
|
||||
end.compareTo(scope, other.end) >= 0
|
||||
|
||||
other.isEndInclusive && !isEndInclusive ->
|
||||
end.compareTo(context, other.end) > 0
|
||||
end.compareTo(scope, other.end) > 0
|
||||
|
||||
!other.isEndInclusive && isEndInclusive ->
|
||||
end.compareTo(context, other.end) >= 0
|
||||
end.compareTo(scope, other.end) >= 0
|
||||
|
||||
else -> throw IllegalStateException("unknown comparison")
|
||||
}
|
||||
@@ -43,17 +70,17 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun contains(context: Context, other: Obj): Boolean {
|
||||
override suspend fun contains(scope: Scope, other: Obj): Boolean {
|
||||
|
||||
if (other is ObjRange)
|
||||
return containsRange(context, other)
|
||||
return containsRange(scope, other)
|
||||
|
||||
if (start == null && end == null) return true
|
||||
if (start != null) {
|
||||
if (start.compareTo(context, other) > 0) return false
|
||||
if (start.compareTo(scope, other) > 0) return false
|
||||
}
|
||||
if (end != null) {
|
||||
val cmp = end.compareTo(context, other)
|
||||
val cmp = end.compareTo(scope, other)
|
||||
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
|
||||
}
|
||||
return true
|
||||
@@ -67,13 +94,34 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
|
||||
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 {
|
||||
if( start == other.start && end == other.end ) 0 else -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 {
|
||||
val type = ObjClass("Range", ObjIterable).apply {
|
||||
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() {
|
||||
|
||||
@@ -8,7 +10,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
fun Context.init() {
|
||||
fun Scope.init() {
|
||||
if (self.start == null || self.end == null)
|
||||
raiseError("next is only available for finite ranges")
|
||||
isCharRange = self.isCharRange
|
||||
@@ -24,7 +26,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
|
||||
|
||||
fun hasNext(): Boolean = nextIndex < lastIndex
|
||||
|
||||
fun next(context: Context): Obj =
|
||||
fun next(scope: Scope): Obj =
|
||||
if (nextIndex < lastIndex) {
|
||||
val x = if (self.isEndInclusive)
|
||||
self.start!!.toLong() + nextIndex++
|
||||
@@ -33,7 +35,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
|
||||
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
|
||||
}
|
||||
else {
|
||||
context.raiseError(ObjIterationFinishedException(context))
|
||||
scope.raiseError(ObjIterationFinishedException(scope))
|
||||
}
|
||||
|
||||
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.roundToLong
|
||||
|
||||
@@ -14,7 +20,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
|
||||
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
|
||||
return value.compareTo(other.doubleValue)
|
||||
}
|
||||
@@ -25,25 +31,25 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
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())
|
||||
|
||||
override suspend fun minus(context: Context, other: Obj): Obj =
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||
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())
|
||||
|
||||
override suspend fun div(context: Context, other: Obj): Obj =
|
||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||
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())
|
||||
|
||||
/**
|
||||
* Returns unboxed Double value
|
||||
*/
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -56,14 +62,30 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
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 {
|
||||
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(
|
||||
"roundToInt",
|
||||
statement(Pos.builtIn) {
|
||||
(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() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun plus(context: Context, other: Obj): Obj {
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return ObjSet(
|
||||
if (other is ObjSet)
|
||||
(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) {
|
||||
is ObjSet -> {
|
||||
set += other.set
|
||||
@@ -29,9 +34,9 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
||||
|
||||
else -> {
|
||||
if (other.isInstanceOf(ObjIterable)) {
|
||||
val i = other.invokeInstanceMethod(context, "iterable")
|
||||
while (i.invokeInstanceMethod(context, "hasNext").toBool()) {
|
||||
set += i.invokeInstanceMethod(context, "next")
|
||||
val i = other.invokeInstanceMethod(scope, "iterable")
|
||||
while (i.invokeInstanceMethod(scope, "hasNext").toBool()) {
|
||||
set += i.invokeInstanceMethod(scope, "next")
|
||||
}
|
||||
}
|
||||
set += other
|
||||
@@ -40,16 +45,16 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
||||
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) {
|
||||
ObjSet(set.intersect(other.set).toMutableSet())
|
||||
} 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)
|
||||
context.raiseIllegalArgument("set operator - requires another set")
|
||||
scope.raiseIllegalArgument("set operator - requires another set")
|
||||
return ObjSet(set.minus(other.set).toMutableSet())
|
||||
}
|
||||
|
||||
@@ -57,7 +62,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
|
||||
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
|
||||
else {
|
||||
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 {
|
||||
|
||||
|
||||
val type = object : ObjClass("Set", ObjCollection) {
|
||||
override suspend fun callOn(context: Context): Obj {
|
||||
return ObjSet(context.args.list.toMutableSet())
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
return ObjSet(scope.args.list.toMutableSet())
|
||||
}
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
ObjSet(decoder.decodeAnyList(scope).toMutableSet())
|
||||
}.apply {
|
||||
addFn("size") {
|
||||
thisAs<ObjSet>().set.size.toObj()
|
||||
+37
-11
@@ -1,7 +1,12 @@
|
||||
package net.sergeych.lyng
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
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
|
||||
|
||||
@Serializable
|
||||
@@ -15,8 +20,7 @@ data class ObjString(val value: String) : Obj() {
|
||||
// return i
|
||||
// }
|
||||
|
||||
|
||||
override suspend fun compareTo(context: Context, other: Obj): Int {
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjString) return -2
|
||||
return this.value.compareTo(other.value)
|
||||
}
|
||||
@@ -32,11 +36,11 @@ data class ObjString(val value: String) : Obj() {
|
||||
override val objClass: ObjClass
|
||||
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)
|
||||
}
|
||||
|
||||
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 ObjRange) {
|
||||
val start = if (index.start == null || index.start.isNull) 0 else index.start.toInt()
|
||||
@@ -46,23 +50,26 @@ data class ObjString(val value: String) : Obj() {
|
||||
}
|
||||
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 {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override suspend fun callOn(context: Context): Obj {
|
||||
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray()))
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
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)
|
||||
value.contains(other.value)
|
||||
else if (other is ObjChar)
|
||||
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 {
|
||||
@@ -74,8 +81,20 @@ data class ObjString(val value: String) : Obj() {
|
||||
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 {
|
||||
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") {
|
||||
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
|
||||
}
|
||||
@@ -111,7 +130,14 @@ data class ObjString(val value: String) : Obj() {
|
||||
addFn("upper") {
|
||||
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("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,5 +1,8 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
sealed class ObjType {
|
||||
@@ -11,21 +14,21 @@ sealed class ObjType {
|
||||
@Suppress("unused")
|
||||
abstract class Statement(
|
||||
val isStaticConst: Boolean = false,
|
||||
val isConst: Boolean = false,
|
||||
override val isConst: Boolean = false,
|
||||
val returnType: ObjType = ObjType.Any
|
||||
) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
override suspend fun callOn(context: Context): Obj {
|
||||
return execute(context)
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
return execute(scope)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Callable@${this.hashCode()}"
|
||||
@@ -34,6 +37,8 @@ abstract class Statement(
|
||||
val type = ObjClass("Callable")
|
||||
}
|
||||
|
||||
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args)))
|
||||
|
||||
}
|
||||
|
||||
fun Statement.raise(text: String): Nothing {
|
||||
@@ -45,16 +50,16 @@ fun Statement.require(cond: Boolean, message: () -> String) {
|
||||
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) {
|
||||
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) {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
override suspend fun execute(context: Context): Obj = f(context)
|
||||
override suspend fun execute(scope: Scope): Obj = f(scope)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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 net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||
import kotlin.test.*
|
||||
|
||||
class ScriptTest {
|
||||
@@ -172,55 +175,55 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun varsAndConstsTest() = runTest {
|
||||
val context = Context(pos = Pos.builtIn)
|
||||
val scope = Scope(pos = Pos.builtIn)
|
||||
assertEquals(
|
||||
ObjInt(3L), context.eval(
|
||||
ObjInt(3L), scope.eval(
|
||||
"""
|
||||
val a = 17
|
||||
var b = 3
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
assertEquals(17, context.eval("a").toInt())
|
||||
assertEquals(20, context.eval("b + a").toInt())
|
||||
assertEquals(17, scope.eval("a").toInt())
|
||||
assertEquals(20, scope.eval("b + a").toInt())
|
||||
assertFailsWith<ScriptError> {
|
||||
context.eval("a = 10")
|
||||
scope.eval("a = 10")
|
||||
}
|
||||
assertEquals(17, context.eval("a").toInt())
|
||||
assertEquals(5, context.eval("b = a - 7 - 5").toInt())
|
||||
assertEquals(5, context.eval("b").toInt())
|
||||
assertEquals(17, scope.eval("a").toInt())
|
||||
assertEquals(5, scope.eval("b = a - 7 - 5").toInt())
|
||||
assertEquals(5, scope.eval("b").toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun functionTest() = runTest {
|
||||
val context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
val scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
fun foo(a, b) {
|
||||
a + b
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(17, context.eval("foo(3,14)").toInt())
|
||||
assertEquals(17, scope.eval("foo(3,14)").toInt())
|
||||
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) {
|
||||
a + b + 1
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(10, context.eval("bar(3, 6)").toInt())
|
||||
assertEquals(14, context.eval("bar(3)").toInt())
|
||||
assertEquals(10, scope.eval("bar(3, 6)").toInt())
|
||||
assertEquals(14, scope.eval("bar(3)").toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun simpleClosureTest() = runTest {
|
||||
val context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
val scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
var global = 10
|
||||
|
||||
@@ -229,16 +232,16 @@ class ScriptTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(27, context.eval("foo(3,14)").toInt())
|
||||
context.eval("global = 20")
|
||||
assertEquals(37, context.eval("foo(3,14)").toInt())
|
||||
assertEquals(27, scope.eval("foo(3,14)").toInt())
|
||||
scope.eval("global = 20")
|
||||
assertEquals(37, scope.eval("foo(3,14)").toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullAndVoidTest() = runTest {
|
||||
val context = Context(pos = Pos.builtIn)
|
||||
assertEquals(ObjVoid, context.eval("void"))
|
||||
assertEquals(ObjNull, context.eval("null"))
|
||||
val scope = Scope(pos = Pos.builtIn)
|
||||
assertEquals(ObjVoid, scope.eval("void"))
|
||||
assertEquals(ObjNull, scope.eval("null"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -345,8 +348,8 @@ class ScriptTest {
|
||||
@Test
|
||||
fun ifTest() = runTest {
|
||||
// if - single line
|
||||
var context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
var scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
fn test1(n) {
|
||||
var result = "more"
|
||||
@@ -356,12 +359,12 @@ class ScriptTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals("enough", context.eval("test1(11)").toString())
|
||||
assertEquals("more", context.eval("test1(1)").toString())
|
||||
assertEquals("enough", scope.eval("test1(11)").toString())
|
||||
assertEquals("more", scope.eval("test1(1)").toString())
|
||||
|
||||
// if - multiline (block)
|
||||
context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
fn test1(n) {
|
||||
var prefix = "answer: "
|
||||
@@ -375,12 +378,12 @@ class ScriptTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals("answer: enough", context.eval("test1(11)").toString())
|
||||
assertEquals("answer: more", context.eval("test1(1)").toString())
|
||||
assertEquals("answer: enough", scope.eval("test1(11)").toString())
|
||||
assertEquals("answer: more", scope.eval("test1(1)").toString())
|
||||
|
||||
// else single line1
|
||||
context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
fn test1(n) {
|
||||
if( n >= 10 )
|
||||
@@ -390,12 +393,12 @@ class ScriptTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals("enough", context.eval("test1(11)").toString())
|
||||
assertEquals("more", context.eval("test1(1)").toString())
|
||||
assertEquals("enough", scope.eval("test1(11)").toString())
|
||||
assertEquals("more", scope.eval("test1(1)").toString())
|
||||
|
||||
// if/else with blocks
|
||||
context = Context(pos = Pos.builtIn)
|
||||
context.eval(
|
||||
scope = Scope(pos = Pos.builtIn)
|
||||
scope.eval(
|
||||
"""
|
||||
fn test1(n) {
|
||||
if( n > 20 ) {
|
||||
@@ -409,9 +412,9 @@ class ScriptTest {
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals("enough", context.eval("test1(11)").toString())
|
||||
assertEquals("more", context.eval("test1(1)").toString())
|
||||
assertEquals("too much", context.eval("test1(100)").toString())
|
||||
assertEquals("enough", scope.eval("test1(11)").toString())
|
||||
assertEquals("more", scope.eval("test1(1)").toString())
|
||||
assertEquals("too much", scope.eval("test1(100)").toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -511,13 +514,13 @@ class ScriptTest {
|
||||
ArgsDeclaration.Item("c"),
|
||||
), 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)
|
||||
assertEquals(ObjInt(1), c["a"]?.value)
|
||||
assertEquals(ObjInt(2), c["b"]?.value)
|
||||
assertEquals(ObjInt(3), c["c"]?.value)
|
||||
// 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> {
|
||||
pa.assignToContext(c)
|
||||
}
|
||||
@@ -534,7 +537,7 @@ class ScriptTest {
|
||||
assertEquals(ObjInt(2), c["b"]?.value)
|
||||
assertEquals(ObjInt(100), c["c"]?.value)
|
||||
// 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)
|
||||
assertEquals(ObjInt(10), c["a"]?.value)
|
||||
assertEquals(ObjInt(2), c["b"]?.value)
|
||||
@@ -552,17 +555,17 @@ class ScriptTest {
|
||||
ArgsDeclaration.Item("b", isEllipsis = true),
|
||||
), 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)
|
||||
c.eval("assert( a == 1 ); println(b)")
|
||||
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)
|
||||
c.eval("assertEquals( a, 1 ); println(b)")
|
||||
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)
|
||||
c.eval("assert( a == 1 ); println(b)")
|
||||
c.eval("assert( b == [] )")
|
||||
@@ -578,32 +581,32 @@ class ScriptTest {
|
||||
ArgsDeclaration.Item("c"),
|
||||
), 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)
|
||||
c.eval("assertEquals( a,[0,1] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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)
|
||||
c.eval("assertEquals( a,[1] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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)
|
||||
c.eval("assertEquals( a,[] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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> {
|
||||
pa.assignToContext(c)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAssignArgumentsmiddleEllipsis() = runTest {
|
||||
fun testAssignArgumentsMiddleEllipsis() = runTest {
|
||||
val ttEnd = Token.Type.RBRACE
|
||||
val pa = ArgsDeclaration(
|
||||
listOf(
|
||||
@@ -613,28 +616,28 @@ class ScriptTest {
|
||||
ArgsDeclaration.Item("c"),
|
||||
), 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)
|
||||
c.eval("assertEquals( i, -1 )")
|
||||
c.eval("assertEquals( a,[0,1] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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)
|
||||
c.eval("assertEquals( i, 0 )")
|
||||
c.eval("assertEquals( a,[1] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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)
|
||||
c.eval("assertEquals( i, 1)")
|
||||
c.eval("assertEquals( a,[] )")
|
||||
c.eval("assertEquals( b, 2 )")
|
||||
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> {
|
||||
pa.assignToContext(c)
|
||||
}
|
||||
@@ -705,19 +708,13 @@ class ScriptTest {
|
||||
var t1 = 10
|
||||
outer@ while( t1 > 0 ) {
|
||||
var t2 = 10
|
||||
println("starting t2 = " + t2)
|
||||
while( t2 > 0 ) {
|
||||
t2 = t2 - 1
|
||||
println("t2 " + t2 + " t1 " + t1)
|
||||
if( t2 == 3 && t1 == 7) {
|
||||
println("will break")
|
||||
break@outer "ok2:"+t2+":"+t1
|
||||
}
|
||||
}
|
||||
println("next t1")
|
||||
t1 = t1 - 1
|
||||
println("t1 now "+t1)
|
||||
t1
|
||||
--t1
|
||||
}
|
||||
""".trimIndent()
|
||||
).toString()
|
||||
@@ -732,8 +729,6 @@ class ScriptTest {
|
||||
"""
|
||||
val count = 3
|
||||
val res = if( count > 10 ) "too much" else "just " + count
|
||||
println(count)
|
||||
println(res)
|
||||
res
|
||||
""".trimIndent()
|
||||
)
|
||||
@@ -755,7 +750,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testIncr() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval("var x = 10")
|
||||
assertEquals(10, c.eval("x++").toInt())
|
||||
assertEquals(11, c.eval("x++").toInt())
|
||||
@@ -767,9 +762,9 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testDecr() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
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(7, c.eval("x--").toInt())
|
||||
assertEquals(6, c.eval("x--").toInt())
|
||||
@@ -778,7 +773,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testDecrIncr() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval("var x = 9")
|
||||
assertEquals(9, c.eval("x++").toInt())
|
||||
assertEquals(10, c.eval("x++").toInt())
|
||||
@@ -792,7 +787,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testDecrIncr2() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval("var x = 9")
|
||||
assertEquals(9, c.eval("x--").toInt())
|
||||
assertEquals(8, c.eval("x--").toInt())
|
||||
@@ -809,7 +804,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testDecrIncr3() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval("var x = 9")
|
||||
assertEquals(9, c.eval("x++").toInt())
|
||||
assertEquals(10, c.eval("x++").toInt())
|
||||
@@ -821,7 +816,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testIncrAndDecr() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
assertEquals(
|
||||
"8", c.eval(
|
||||
"""
|
||||
@@ -856,7 +851,7 @@ class ScriptTest {
|
||||
@Test
|
||||
fun testAssign1() = runTest {
|
||||
assertEquals(10, eval("var x = 5; x=10; x").toInt())
|
||||
val ctx = Context()
|
||||
val ctx = Scope()
|
||||
ctx.eval(
|
||||
"""
|
||||
var a = 1
|
||||
@@ -870,7 +865,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testAssign2() = runTest {
|
||||
val ctx = Context()
|
||||
val ctx = Scope()
|
||||
ctx.eval("var x = 10")
|
||||
assertEquals(14, ctx.eval("x += 4").toInt())
|
||||
assertEquals(14, ctx.eval("x").toInt())
|
||||
@@ -888,7 +883,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testVals() = runTest {
|
||||
val cxt = Context()
|
||||
val cxt = Scope()
|
||||
cxt.eval("val x = 11")
|
||||
assertEquals(11, cxt.eval("x").toInt())
|
||||
assertFails { cxt.eval("x = 12") }
|
||||
@@ -1435,7 +1430,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testSimpleStruct() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval(
|
||||
"""
|
||||
class Point(x,y)
|
||||
@@ -1456,7 +1451,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testNonAssignalbeFieldInStruct() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval(
|
||||
"""
|
||||
class Point(x,y)
|
||||
@@ -1473,7 +1468,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testStructBodyVal() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval(
|
||||
"""
|
||||
class Point(x,y) {
|
||||
@@ -1495,7 +1490,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testStructBodyFun() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval(
|
||||
"""
|
||||
class Point(x,y) {
|
||||
@@ -1515,7 +1510,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testPrivateConstructorParams() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.eval(
|
||||
"""
|
||||
class Point(private var x,y)
|
||||
@@ -1870,7 +1865,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testTryFinally() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
assertFails {
|
||||
c.eval(
|
||||
"""
|
||||
@@ -1893,7 +1888,7 @@ class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun testThrowFromKotlin() = runTest {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
c.addFn("callThrow") {
|
||||
raiseIllegalArgument("fromKotlin")
|
||||
}
|
||||
@@ -2248,19 +2243,30 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLet() = runTest {
|
||||
fun testSet2() = runTest {
|
||||
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)
|
||||
assert( Point() is Object)
|
||||
Point().let { println(it.x, it.y) }
|
||||
val x = null
|
||||
x?.let { println(it.x, it.y) }
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testApply() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
class Point(x,y)
|
||||
// see the difference: apply changes this to newly created Point:
|
||||
val p = Point(1,2).apply {
|
||||
@@ -2269,12 +2275,14 @@ class ScriptTest {
|
||||
assertEquals(p, Point(2,3))
|
||||
>>> void
|
||||
|
||||
""".trimIndent())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testApplyThis() = runTest {
|
||||
eval("""
|
||||
eval(
|
||||
"""
|
||||
class Point(x,y)
|
||||
// see the difference: apply changes this to newly created Point:
|
||||
val p = Point(1,2).apply {
|
||||
@@ -2283,6 +2291,378 @@ class ScriptTest {
|
||||
assertEquals(p, Point(2,3))
|
||||
>>> 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())
|
||||
}
|
||||
|
||||
@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.flowOn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Context
|
||||
import net.sergeych.lyng.ObjVoid
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Files.readAllLines
|
||||
import java.nio.file.Paths
|
||||
@@ -105,6 +106,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
|
||||
} else {
|
||||
var isValid = true
|
||||
val result = mutableListOf<String>()
|
||||
|
||||
// remove empty trails:
|
||||
while( block.last().isEmpty() ) block.removeLast()
|
||||
|
||||
while (block.size > outStart) {
|
||||
val line = block.removeAt(outStart)
|
||||
if (!line.startsWith(">>> ")) {
|
||||
@@ -158,10 +163,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
suspend fun DocTest.test(context: Context = Context()) {
|
||||
suspend fun DocTest.test(scope: Scope = Scope()) {
|
||||
val collectedOutput = StringBuilder()
|
||||
val currentTest = this
|
||||
context.apply {
|
||||
scope.apply {
|
||||
addFn("println") {
|
||||
if( bookMode ) {
|
||||
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
|
||||
val result = try {
|
||||
context.eval(code)
|
||||
scope.eval(code)
|
||||
} catch (e: Throwable) {
|
||||
error = e
|
||||
null
|
||||
@@ -204,10 +209,10 @@ suspend fun DocTest.test(context: Context = Context()) {
|
||||
}
|
||||
|
||||
suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
|
||||
val bookContext = Context()
|
||||
val bookScope = Scope()
|
||||
var count = 0
|
||||
parseDocTests(fileName, bookMode).collect { dt ->
|
||||
if (bookMode) dt.test(bookContext)
|
||||
if (bookMode) dt.test(bookScope)
|
||||
else dt.test()
|
||||
count++
|
||||
}
|
||||
@@ -265,6 +270,11 @@ class BookTest {
|
||||
runDocTests("../docs/Map.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuffer() = runTest {
|
||||
runDocTests("../docs/Buffer.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSampleBooks() = runTest {
|
||||
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||
@@ -283,4 +293,10 @@ class BookTest {
|
||||
fun testExceptionsBooks() = runTest {
|
||||
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.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import net.sergeych.lyng.Context
|
||||
import net.sergeych.lyng.Scope
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.extension
|
||||
@@ -13,7 +13,7 @@ suspend fun executeSampleTests(fileName: String) {
|
||||
Files.readString(Paths.get(fileName))
|
||||
}
|
||||
runBlocking {
|
||||
val c = Context()
|
||||
val c = Scope()
|
||||
val start = Clock.System.now()
|
||||
c.eval(sample)
|
||||
val time = Clock.System.now() - start
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user