Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de71c7c8ec | ||
|
|
be370dfdef | ||
|
|
53a6a88924 | ||
|
|
2c2dae4d85 | ||
|
|
63b2808109 | ||
|
|
4165da5e81 | ||
|
|
af463163bb | ||
|
|
6df06a6911 | ||
|
|
f805e1ee82 | ||
|
|
790cce0d24 | ||
|
|
2339130241 | ||
|
|
d7bd159fcb | ||
|
|
12b209c724 | ||
|
|
20181c63a1 | ||
|
|
405ff2ec2b | ||
|
|
a9f65bdbe3 | ||
|
|
6ab438b1f6 | ||
|
|
cffe4eaffc | ||
|
|
7aee25ffef | ||
|
|
f3d766d1b1 | ||
|
|
34bc7297bd | ||
|
|
23dafff453 | ||
|
|
77f9191387 | ||
|
|
f26ee7cd7c | ||
|
|
d969993997 | ||
|
|
987b80e44d | ||
|
|
5848adca61 | ||
|
|
f1ae4b2d23 | ||
|
|
30b6ef235b | ||
|
|
9771b40c98 | ||
|
|
230cb0a067 | ||
|
|
732d8f3877 | ||
|
|
23006b5caa | ||
|
|
26282d3e22 | ||
|
|
ce4ed5c819 | ||
|
|
612c0fb7b9 | ||
|
|
ef6bc5c468 | ||
|
|
1e2cb5e420 | ||
|
|
868379ce22 | ||
|
|
f37354f382 | ||
|
|
ddbcbf9e4e | ||
|
|
6cf99fbd13 | ||
|
|
950e8301c3 | ||
|
|
ea2cf1f373 | ||
|
|
eee6d75587 | ||
|
|
a8067d0a6b | ||
|
|
75a6f20150 | ||
|
|
d969d6d572 | ||
|
|
2d4c4d345d | ||
|
|
f9416105ec | ||
|
|
c002204420 | ||
|
|
a4448ab2ff | ||
|
|
8a4363bd84 | ||
|
|
19eae213ec | ||
|
|
1db1f12be3 | ||
|
|
dcde11d722 | ||
|
|
83e79f47c7 | ||
|
|
e0bb183929 | ||
|
|
b961296425 | ||
|
|
bd2b6bf06e | ||
|
|
253480e32a | ||
|
|
eb8110cbf0 | ||
|
|
8c6a1979ed | ||
|
|
185aa4e0cf | ||
|
|
ef266b73a2 | ||
|
|
89427de5cd | ||
|
|
cfb2f7f128 | ||
|
|
be4f2c7f45 | ||
|
|
7cc80e2433 | ||
|
|
dacdcd7faa | ||
|
|
59a76efdce | ||
|
|
bb862e6cb5 | ||
|
|
aea819b89a | ||
|
|
88974e0f2d |
No files matched your search
@@ -14,3 +14,4 @@ xcuserdata
|
||||
.gigaide
|
||||
/kotlin-js-store/yarn.lock
|
||||
/test.lyng
|
||||
/sample_texts/1.txt.gz
|
||||
@@ -9,6 +9,10 @@ class Point(x,y) {
|
||||
fun dist() { sqrt(x*x + y*y) }
|
||||
}
|
||||
Point(3,4).dist() //< 5
|
||||
|
||||
fun swapEnds(first, args..., last, f) {
|
||||
f( last, ...args, first)
|
||||
}
|
||||
```
|
||||
|
||||
- extremely simple Kotlin integration on any platform
|
||||
@@ -31,26 +35,56 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
|
||||
|
||||
## Integration in Kotlin multiplatform
|
||||
|
||||
### Add library
|
||||
### Add dependency to your project
|
||||
|
||||
TBD
|
||||
```kotlin
|
||||
// update to current please:
|
||||
val lyngVersion = "0.6.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
// ...
|
||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
}
|
||||
```
|
||||
|
||||
And add dependency to the proper place in your project, it could look like:
|
||||
|
||||
```kotlin
|
||||
comminMain by getting {
|
||||
dependencies {
|
||||
// ...
|
||||
implementation("net.sergeych:lynglib:$lyngVersion")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can import lyng and use it:
|
||||
|
||||
### Execute script:
|
||||
|
||||
```kotlin
|
||||
assertEquals("hello, world", eval("""
|
||||
"hello, " + "world"
|
||||
""").toString())
|
||||
import net.sergeyh.lyng.*
|
||||
|
||||
// we need a coroutine to start, as Lyng
|
||||
// is a coroutine based language, async topdown
|
||||
runBlocking {
|
||||
assert(5 == eval(""" 3*3 - 4 """).toInt())
|
||||
eval(""" println("Hello, Lyng!") """)
|
||||
}
|
||||
```
|
||||
|
||||
### 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()
|
||||
@@ -67,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?
|
||||
|
||||
@@ -83,22 +117,51 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
|
||||
|
||||
# Language
|
||||
|
||||
- dynamic
|
||||
- async
|
||||
- multithreaded (coroutines could be dispatched using threads on appropriate platforms, automatically)
|
||||
- Javascript, WasmJS, native, JVM, android - batteries included.
|
||||
- dynamic types in most elegant and concise way
|
||||
- async, 100% coroutines, supports multiple cores where platofrm supports thread
|
||||
- good for functional an object-oriented style
|
||||
|
||||
## By-stage
|
||||
# Language Roadmap
|
||||
|
||||
Here are plans to develop it:
|
||||
## v1.0.0
|
||||
|
||||
### First stage
|
||||
Planned autumn 2025. Complete dynamic language with sufficient standard library:
|
||||
|
||||
Interpreted, precompiled into threaded code, actually. Dynamic types.
|
||||
Ready features:
|
||||
|
||||
### Second stage
|
||||
- [x] Language platform and independent command-line launcher
|
||||
- [x] Integral types and user classes, variables and constants, functions
|
||||
- [x] lambdas and closures, coroutines for all callables
|
||||
- [x] while-else, do-while-else, for-else loops with break-continue returning values and labels support
|
||||
- [x] ranges, lists, strings, interfaces: Iterable, Iterator, Collection, Array
|
||||
- [x] when(value), if-then-else
|
||||
- [x] exception handling: throw, try-catch-finally, exception classes.
|
||||
- [x] multiplatform maven publication
|
||||
- [x] documentation for the current state
|
||||
|
||||
Will add:
|
||||
Under way:
|
||||
|
||||
- optimizations
|
||||
- p-code serialization
|
||||
- static typing
|
||||
- [ ] maps, sets and sequences (flows?)
|
||||
- [ ] regular exceptions
|
||||
- [ ] modules
|
||||
- [ ] string interpolation and more string tools
|
||||
- [ ] multiple inheritance for user classes
|
||||
- [ ] launch, deferred, coroutineScope, mutex, etc.
|
||||
- [ ] site with integrated interpreter to give a try
|
||||
- [ ] kotlin part public API good docs, integration focused
|
||||
- [ ] better stack reporting
|
||||
|
||||
## v1.1+
|
||||
|
||||
Planned features.
|
||||
|
||||
- [ ] type specifications
|
||||
- [ ] source docs and maybe lyng.md to a standard
|
||||
- [ ] moacro-style kotlin integration or something else to simplify it
|
||||
|
||||
Further
|
||||
|
||||
- [ ] client with GUI support based on compose multiplatform somehow
|
||||
- [ ] notebook - style workbooks with graphs, formulaes, etc.
|
||||
- [ ] language server or compose-based lyng-aware editor
|
||||
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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env lyng
|
||||
|
||||
println("Hello from lyng!")
|
||||
+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
|
||||
@@ -0,0 +1,13 @@
|
||||
# Collection
|
||||
|
||||
Is a [Iterable] with known `size`, a finite [Iterable]:
|
||||
|
||||
class Collection : Iterable {
|
||||
val size
|
||||
}
|
||||
|
||||
See [List], [Set] and [Iterable]
|
||||
|
||||
[Iterable]: Iterable.md
|
||||
[List]: List.md
|
||||
[Set]: Set.md
|
||||
+35
-9
@@ -1,13 +1,17 @@
|
||||
# Iterable interface
|
||||
|
||||
The inteface which requires iterator to be implemented:
|
||||
Iterable is a class that provides function that creates _the iterator_:
|
||||
|
||||
fun iterator(): Iterator
|
||||
class Iterable {
|
||||
abstract fun iterator()
|
||||
}
|
||||
|
||||
Note that each call of `iterator()` must provide an independent iterator.
|
||||
|
||||
Iterator itself is a simple interface that should provide only to method:
|
||||
|
||||
interface Iterable {
|
||||
fun hasNext(): Bool
|
||||
class Iterator {
|
||||
abstract fun hasNext(): Bool
|
||||
fun next(): Obj
|
||||
}
|
||||
|
||||
@@ -15,19 +19,41 @@ Just remember at this stage typed declarations are not yet supported.
|
||||
|
||||
Having `Iterable` in base classes allows to use it in for loop. Also, each `Iterable` has some utility functions available:
|
||||
|
||||
## 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
|
||||
fun Iterable.contains(element): Bool
|
||||
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
|
||||
|
||||
fun iterator(): Iterator
|
||||
|
||||
## Instance methods
|
||||
|
||||
### toList()
|
||||
|
||||
Creates a list by iterating to the end. So, the Iterator should be finite to be used with it.
|
||||
|
||||
## Included in interfaces:
|
||||
|
||||
- Collection, Array, [List]
|
||||
- [Collection], Array, [List]
|
||||
|
||||
## Implemented in classes:
|
||||
|
||||
|
||||
+59
-12
@@ -20,11 +20,11 @@ indexing is zero-based, as in C/C++/Java/Kotlin, etc.
|
||||
list[1]
|
||||
>>> 20
|
||||
|
||||
Using negative indexes has a special meaning: _offset from the end of the list_:
|
||||
There is a shortcut for the last:
|
||||
|
||||
val list = [10, 20, 30]
|
||||
list[-1]
|
||||
>>> 30
|
||||
[list.last, list.lastIndex]
|
||||
>>> [30, 2]
|
||||
|
||||
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
|
||||
|
||||
@@ -38,7 +38,8 @@ You can concatenate lists or iterable objects:
|
||||
|
||||
## Appending
|
||||
|
||||
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`:
|
||||
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
|
||||
concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`:
|
||||
|
||||
var list = [1, 2]
|
||||
val other = [3, 4]
|
||||
@@ -57,7 +58,28 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances,
|
||||
|
||||
>>> void
|
||||
|
||||
## Removing elements
|
||||
|
||||
List is mutable, so it is possible to remove its contents. To remove a single element
|
||||
by index use:
|
||||
|
||||
assertEquals( [1,2,3].removeAt(1), [1,3] )
|
||||
assertEquals( [1,2,3].removeAt(0), [2,3] )
|
||||
assertEquals( [1,2,3].removeLast(), [1,2] )
|
||||
>>> void
|
||||
|
||||
There is a way to remove a range (see [Range] for more on ranges):
|
||||
|
||||
assertEquals( [1, 4], [1,2,3,4].removeRange(1..2))
|
||||
assertEquals( [1, 4], [1,2,3,4].removeRange(1..<3))
|
||||
>>> void
|
||||
|
||||
Open end ranges remove head and tail elements:
|
||||
|
||||
assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..1))
|
||||
assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..<2))
|
||||
assertEquals( [1, 2], [1,2,3,4,5].removeRange( (2..) ))
|
||||
>>> void
|
||||
|
||||
## Comparisons
|
||||
|
||||
@@ -73,18 +95,43 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances,
|
||||
## Members
|
||||
|
||||
| name | meaning | type |
|
||||
|-----------------------------------|-------------------------------------|----------|
|
||||
|-------------------------------|---------------------------------------|-------------|
|
||||
| `size` | current size | Int |
|
||||
| `add(elements...)` | add one or more elements to the end | Any |
|
||||
| `addAt(index,elements...)` | insert elements at position | Int, Any |
|
||||
| `insertAt(index,elements...)` | insert elements at position | Int, Any |
|
||||
| `removeAt(index)` | remove element at position | Int |
|
||||
| `removeRangeInclusive(start,end)` | remove range, inclusive (1) | Int, Int |
|
||||
| | | |
|
||||
| `remove(from,toNonInclusive)` | remove range from (incl) to (nonincl) | Int, Int |
|
||||
| `remove(Range)` | remove range | Range |
|
||||
| `removeLast()` | remove last element | |
|
||||
| `removeLast(n)` | remove n last elements | Int |
|
||||
| `contains(element)` | check the element is in the list (1) | |
|
||||
| `[index]` | get or set element at index | Int |
|
||||
| `[Range]` | get slice of the array (copy) | Range |
|
||||
| `+=` | append element(s) | List or Obj |
|
||||
|
||||
(1)
|
||||
: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like `list.removeRangeInclusive(-2, -1)` will remove two last elements.
|
||||
: optimized implementation that override `Array` one
|
||||
|
||||
(2)
|
||||
: `+=` append either a single element, or all elements if the List or other Iterable
|
||||
instance is appended. If you want to append an Iterable object itself, use `add` instead.
|
||||
|
||||
It inherits from [Iterable] too.
|
||||
|
||||
## Member inherited from Array
|
||||
|
||||
| name | meaning | type |
|
||||
|------------------|--------------------------------|-------|
|
||||
| `last` | last element (throws if empty) | |
|
||||
| `lastOrNull` | last element or null | |
|
||||
| `lastIndex` | | Int |
|
||||
| `indices` | range of indexes | Range |
|
||||
| `contains(item)` | test that item is in the list | |
|
||||
|
||||
(1)
|
||||
: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like
|
||||
`list.removeRangeInclusive(-2, -1)` will remove two last elements.
|
||||
|
||||
|
||||
# Notes
|
||||
|
||||
Could be rewritten using array as a class but List as the interface
|
||||
[Range]: Range.md
|
||||
[Iterable]: Iterable.md
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Map
|
||||
|
||||
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" )
|
||||
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" )
|
||||
assertEquals( null, map["nonexisting"])
|
||||
assert( map.getOrNull(101) == null )
|
||||
assert( map.getOrPut(911) { "nine-eleven" } == "nine-eleven" )
|
||||
// now 91 entry is set:
|
||||
assert( map[911] == "nine-eleven" )
|
||||
map["foo"] = -1
|
||||
assert( map["foo"] == -1)
|
||||
>>> void
|
||||
|
||||
To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you
|
||||
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"] )
|
||||
assertEquals( 1, map.remove("foo") )
|
||||
assert( map.getOrNull("foo") == null)
|
||||
assert( map.size == 2 )
|
||||
map.clear()
|
||||
assert( map.size == 0 )
|
||||
>>> void
|
||||
|
||||
Map implements [contains] method that checks _the presence of the key_ in the map:
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
assert( "foo" in map )
|
||||
assert( "answer" !in map )
|
||||
>>> void
|
||||
|
||||
To iterate maps it is convenient to use `keys` method that returns [Set] of keys (keys are unique:
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
for( k in map.keys ) println(map[k])
|
||||
>>> 1
|
||||
>>> buzz
|
||||
>>> answer
|
||||
>>> void
|
||||
|
||||
Or iterate its key-value pairs that are instances of [MapEntry] class:
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
for( entry in map ) {
|
||||
println("map[%s] = %s"(entry.key, entry.value))
|
||||
}
|
||||
void
|
||||
>>> map[foo] = 1
|
||||
>>> map[bar] = buzz
|
||||
>>> map[42] = answer
|
||||
>>> void
|
||||
|
||||
There is a shortcut to use `MapEntry` to create maps: operator `=>` which creates `MapEntry`:
|
||||
|
||||
val entry = "answer" => 42
|
||||
assert( entry is MapEntry )
|
||||
>>> void
|
||||
|
||||
And you can use it to construct maps:
|
||||
|
||||
val map = Map( "foo" => 1, "bar" => 22)
|
||||
assertEquals(1, map["foo"])
|
||||
assertEquals(22, map["bar"])
|
||||
>>> void
|
||||
|
||||
Or use `.toMap` on anything that implements [Iterable] and which elements implements [Array] with 2 elements size, for example, `MapEntry`:
|
||||
|
||||
val map = ["foo" => 1, "bar" => 22].toMap()
|
||||
assert( map is Map )
|
||||
assertEquals(1, map["foo"])
|
||||
assertEquals(22, map["bar"])
|
||||
>>> void
|
||||
|
||||
|
||||
It is possible also to get values as [List] (values are not unique):
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
assertEquals(map.values, [1, "buzz", "answer"] )
|
||||
>>> void
|
||||
|
||||
Map could be tested to be equal: when all it key-value pairs are equal, the map
|
||||
is equal.
|
||||
|
||||
val m1 = Map(["foo", 1])
|
||||
val m2 = Map(["foo", 1])
|
||||
val m3 = Map(["foo", 2])
|
||||
assert( m1 == m2 )
|
||||
// but the references are different:
|
||||
assert( m1 !== m2 )
|
||||
// different maps:
|
||||
assert( m1 != m3 )
|
||||
>>> void
|
||||
|
||||
[Collection](Collection.md)
|
||||
+78
-4
@@ -147,7 +147,7 @@ In many cases it is necessary to implement custom comparison and `toString`, sti
|
||||
each class is provided with default implementations:
|
||||
|
||||
- default toString outputs class name and its _public_ fields.
|
||||
- default comparison compares compares all public fields in order of appearance.
|
||||
- default comparison compares all fields in order of appearance.
|
||||
|
||||
For example, for our class Point:
|
||||
|
||||
@@ -160,6 +160,81 @@ For example, for our class Point:
|
||||
Point(1,1+1)
|
||||
>>> Point(x=1,y=2)
|
||||
|
||||
## Statics: class fields and class methods
|
||||
|
||||
You can mark a field or a method as static. This is borrowed from Java as more plain version of a kotlin's companion object or Scala's object. Static field and functions is one for a class, not for an instance. From inside the class, e.g. from the class method, it is a regular var. From outside, it is accessible as `ClassName.field` or method:
|
||||
|
||||
|
||||
class Value(x) {
|
||||
static var foo = Value("foo")
|
||||
|
||||
static fun exclamation() {
|
||||
// here foo is a regular var:
|
||||
foo.x + "!"
|
||||
}
|
||||
}
|
||||
assertEquals( Value.foo.x, "foo" )
|
||||
assertEquals( "foo!", Value.exclamation() )
|
||||
|
||||
// we can access foo from outside like this:
|
||||
Value.foo = Value("bar")
|
||||
assertEquals( "bar!", Value.exclamation() )
|
||||
>>> void
|
||||
|
||||
As usual, private statics are not accessible from the outside:
|
||||
|
||||
class Test {
|
||||
// private, inacessible from outside protected data:
|
||||
private static var data = null
|
||||
|
||||
// the interface to access and change it:
|
||||
static fun getData() { data }
|
||||
static fun setData(value) { data = value }
|
||||
}
|
||||
|
||||
// no direct access:
|
||||
assertThrows { Test.data }
|
||||
|
||||
// accessible with the interface:
|
||||
assertEquals( null, Test.getData() )
|
||||
Test.setData("fubar")
|
||||
assertEquals("fubar", Test.getData() )
|
||||
>>> void
|
||||
|
||||
# Extending classes
|
||||
|
||||
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
|
||||
that would test if some object of unknown type contains something that can be interpreted
|
||||
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
|
||||
|
||||
fun Object.isInteger() {
|
||||
when(this) {
|
||||
// already Int?
|
||||
is Int -> true
|
||||
|
||||
// real, but with no declimal part?
|
||||
is Real -> toInt() == this
|
||||
|
||||
// string with int or real reuusig code above
|
||||
is String -> toReal().isInteger()
|
||||
|
||||
// otherwise, no:
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Let's test:
|
||||
assert( 12.isInteger() == true )
|
||||
assert( 12.1.isInteger() == false )
|
||||
assert( "5".isInteger() )
|
||||
assert( ! "5.2".isInteger() )
|
||||
>>> void
|
||||
|
||||
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created,
|
||||
as they are modifying the type, not the context.
|
||||
|
||||
Beware of it. We might need to reconsider it later.
|
||||
|
||||
# Theory
|
||||
|
||||
## Basic principles:
|
||||
@@ -216,11 +291,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 |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# List built-in class
|
||||
|
||||
Mutable set of any objects: a group of different objects, no repetitions.
|
||||
Sets are not ordered, order of appearance does not matter.
|
||||
|
||||
val set = Set(1,2,3, "foo")
|
||||
assert( 1 in set )
|
||||
assert( "foo" in set)
|
||||
assert( "bar" !in set)
|
||||
>>> void
|
||||
|
||||
## Set is collection and therefore [Iterable]:
|
||||
|
||||
assert( Set(1,2) is Set)
|
||||
assert( Set(1,2) is Iterable)
|
||||
assert( Set(1,2) is Collection)
|
||||
>>> void
|
||||
|
||||
So it supports all methods from [Iterable]; set is not, though, an [Array] and has
|
||||
no indexing. Use [set.toList] as needed.
|
||||
|
||||
## Set operations
|
||||
|
||||
// Union
|
||||
assertEquals( Set(1,2,3,4), Set(3, 1) + Set(2, 4))
|
||||
|
||||
// intersection
|
||||
assertEquals( Set(1,4), Set(3, 1, 4).intersect(Set(2, 4, 1)) )
|
||||
// or simple
|
||||
assertEquals( Set(1,4), Set(3, 1, 4) * Set(2, 4, 1) )
|
||||
|
||||
// To find collection elements not present in another collection, use the
|
||||
// subtract() or `-`:
|
||||
assertEquals( Set( 1, 2), Set(1, 2, 4, 3) - Set(3, 4))
|
||||
|
||||
>>> void
|
||||
|
||||
## Adding elements
|
||||
|
||||
var s = Set()
|
||||
s += 1
|
||||
assertEquals( Set(1), s)
|
||||
|
||||
s += [3, 3, 4]
|
||||
assertEquals( Set(3, 4, 1), s)
|
||||
>>> void
|
||||
|
||||
## Removing elements
|
||||
|
||||
List is mutable, so it is possible to remove its contents. To remove a single element
|
||||
by index use:
|
||||
|
||||
var s = Set(1,2,3)
|
||||
s.remove(2)
|
||||
assertEquals( s, Set(1,3) )
|
||||
|
||||
s = Set(1,2,3)
|
||||
s.remove(2,1)
|
||||
assertEquals( s, Set(3) )
|
||||
>>> void
|
||||
|
||||
Note that `remove` returns true if at least one element was actually removed and false
|
||||
if the set has not been changed.
|
||||
|
||||
## Comparisons and inclusion
|
||||
|
||||
Sets are only equal when contains exactly same elements, order, as was said, is not significant:
|
||||
|
||||
assert( Set(1, 2) == Set(2, 1) )
|
||||
assert( Set(1, 2, 2) == Set(2, 1) )
|
||||
assert( Set(1, 3) != Set(2, 1) )
|
||||
assert( 1 in Set(5,1))
|
||||
assert( 10 !in Set(5,1))
|
||||
>>> void
|
||||
|
||||
## Members
|
||||
|
||||
| name | meaning | type |
|
||||
|---------------------|--------------------------------------|-------|
|
||||
| `size` | current size | Int |
|
||||
| `+=` | add one or more elements | Any |
|
||||
| `+`, `union` | union sets | Any |
|
||||
| `-`, `subtract` | subtract sets | Any |
|
||||
| `*`, `intersect` | subtract sets | Any |
|
||||
| `remove(items...)` | remove one or more items | Range |
|
||||
| `contains(element)` | check the element is in the list (1) | |
|
||||
|
||||
(1)
|
||||
: optimized implementation that override `Iterable` one
|
||||
|
||||
Also, it inherits methods from [Iterable].
|
||||
|
||||
|
||||
[Range]: Range.md
|
||||
Whitespace-only changes.
@@ -87,3 +87,27 @@ Lambda functions remember their scopes, so it will work the same as previous:
|
||||
println(c)
|
||||
>> 1
|
||||
>> void
|
||||
|
||||
# Elements of functional programming
|
||||
|
||||
With ellipsis and splats you can create partial functions, manipulate
|
||||
arguments list in almost arbitrary ways. For example:
|
||||
|
||||
// Swap first and last arguments for call
|
||||
|
||||
fun swap_args(first, others..., last, f) {
|
||||
f(last, ...others, first)
|
||||
}
|
||||
|
||||
fun glue(args...) {
|
||||
var result = ""
|
||||
for( a in args ) result += a
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"4231",
|
||||
swap_args( 1, 2, 3, 4, glue)
|
||||
)
|
||||
>>> void
|
||||
|
||||
,
|
||||
@@ -81,6 +81,22 @@ to catch all exceptions to, then you can write it even shorter:
|
||||
You can even check the type of the `it` and create more convenient and sophisticated processing logic. Such approach is
|
||||
used, for example, in Scala.
|
||||
|
||||
## finally block
|
||||
|
||||
If `finally` block present, it will be executed after body (until first exception)
|
||||
and catch block, if any will match. finally statement is executed even if the
|
||||
exception will be thrown and not caught locally. It does not alter try/catch block result:
|
||||
|
||||
try {
|
||||
}
|
||||
finally {
|
||||
println("called finally")
|
||||
}
|
||||
>>> called finally
|
||||
>>> void
|
||||
|
||||
- and yes, there could be try-finally block, no catching, but perform some guaranteed cleanup.
|
||||
|
||||
# Conveying data with exceptions
|
||||
|
||||
The simplest way is to provide exception string and `Exception` class:
|
||||
@@ -115,7 +131,7 @@ _this functionality is not yet released_
|
||||
| class | notes |
|
||||
|----------------------------|-------------------------------------------------------|
|
||||
| Exception | root of al throwable objects |
|
||||
| NullPointerException | |
|
||||
| NullReferenceException | |
|
||||
| AssertionFailedException | |
|
||||
| ClassCastException | |
|
||||
| IndexOutOfBoundsException | |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+318
-24
@@ -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], [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
|
||||
@@ -118,6 +118,109 @@ These operators return rvalue, unmodifiable.
|
||||
|
||||
## Assignment return r-value!
|
||||
|
||||
Naturally, assignment returns its value:
|
||||
|
||||
var x
|
||||
x = 11
|
||||
>>> 11
|
||||
|
||||
rvalue means you cant assign the result if the assignment
|
||||
|
||||
var x
|
||||
assertThrows { (x = 11) = 5 }
|
||||
void
|
||||
>>> void
|
||||
|
||||
This also prevents chain assignments so use parentheses:
|
||||
|
||||
var x
|
||||
var y
|
||||
x = (y = 1)
|
||||
>>> 1
|
||||
|
||||
## Nullability
|
||||
|
||||
When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it
|
||||
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
|
||||
the operation won't be performed and the result will be null. Here is the difference:
|
||||
|
||||
val ref = null
|
||||
assertThrows { ref.field }
|
||||
assertThrows { ref.method() }
|
||||
assertThrows { ref.array[1] }
|
||||
assertThrows { ref[1] }
|
||||
assertThrows { ref() }
|
||||
|
||||
assert( ref?.field == null )
|
||||
assert( ref?.method() == null )
|
||||
assert( ref?.array?[1] == null )
|
||||
assert( ref?[1] == null )
|
||||
assert( ref?() == null )
|
||||
>>> void
|
||||
|
||||
There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`:
|
||||
|
||||
null ?: "nothing"
|
||||
>>> "nothing"
|
||||
|
||||
## Utility functions
|
||||
|
||||
The following functions simplify nullable values processing and
|
||||
allow to improve code look and readability. There are borrowed from Kotlin:
|
||||
|
||||
### let
|
||||
|
||||
`value.let {}` passes to the block value as the single parameter (by default it is assigned to `it`) and return block's returned value. It is useful dealing with null or to
|
||||
get a snapshot of some externally varying value, or with `?.` to process nullable value in a safe manner:
|
||||
|
||||
// this state is changed from parallel processes
|
||||
class GlobalState(nullableParam)
|
||||
|
||||
val state = GlobalState(null)
|
||||
|
||||
fun sample() {
|
||||
state.nullableParam?.let { "it's not null: "+it} ?: "it's null"
|
||||
}
|
||||
assertEquals(sample(), "it's null")
|
||||
state.nullableParam = 5
|
||||
assertEquals(sample(), "it's not null: 5")
|
||||
>>> void
|
||||
|
||||
This is the same as:
|
||||
|
||||
fun sample() {
|
||||
val it = state.nullableParam
|
||||
if( it != null ) "it's not null: "+it else "it's null"
|
||||
}
|
||||
|
||||
The important is that nullableParam got a local copy that can't be changed from any
|
||||
parallel thread/coroutine. Remember: Lyng _is __not__ a single-threaded language_.
|
||||
|
||||
## Also
|
||||
|
||||
Much like let, but it does not alter returned value:
|
||||
|
||||
assert( "test".also { println( it + "!") } == "test" )
|
||||
>>> test!
|
||||
>>> void
|
||||
|
||||
While it is not altering return value, the source object could be changed:
|
||||
|
||||
class Point(x,y)
|
||||
val p = Point(1,2).also { it.x++ }
|
||||
assertEquals(p.x, 2)
|
||||
>>> void
|
||||
|
||||
## apply
|
||||
|
||||
It works much like `also`, but is executed in the context of the source object:
|
||||
|
||||
class Point(x,y)
|
||||
// see the difference: apply changes this to newly created Point:
|
||||
val p = Point(1,2).apply { x++; y++ }
|
||||
assertEquals(p, Point(2,3))
|
||||
>>> void
|
||||
|
||||
## Math
|
||||
|
||||
It is rather simple, like everywhere else:
|
||||
@@ -427,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:
|
||||
@@ -469,64 +573,101 @@ To add elements to the list:
|
||||
assert( x == [1, 2, 3, "the", "end"])
|
||||
>>> void
|
||||
|
||||
Self-modifying concatenation by `+=` also works:
|
||||
Self-modifying concatenation by `+=` also works (also with single elements):
|
||||
|
||||
val x = [1, 2]
|
||||
x += [3, 4]
|
||||
assert( x == [1, 2, 3, 4])
|
||||
x += 5
|
||||
assert( x == [1, 2, 3, 4, 5])
|
||||
>>> void
|
||||
|
||||
You can insert elements at any position using `addAt`:
|
||||
You can insert elements at any position using `insertAt`:
|
||||
|
||||
val x = [1,2,3]
|
||||
x.addAt(1, "foo", "bar")
|
||||
x.insertAt(1, "foo", "bar")
|
||||
assert( x == [1, "foo", "bar", 2, 3])
|
||||
>>> void
|
||||
|
||||
Using splat arguments can simplify inserting list in list:
|
||||
|
||||
val x = [1, 2, 3]
|
||||
x.addAt( 1, ...[0,100,0])
|
||||
x.insertAt( 1, ...[0,100,0])
|
||||
x
|
||||
>>> [1, 0, 100, 0, 2, 3]
|
||||
|
||||
Using negative indexes can insert elements as offset from the end, for example:
|
||||
|
||||
val x = [1,2,3]
|
||||
x.addAt(-1, 10)
|
||||
x
|
||||
>>> [1, 2, 10, 3]
|
||||
|
||||
Note that to add to the end you still need to use `add` or positive index of the after-last element:
|
||||
|
||||
val x = [1,2,3]
|
||||
x.addAt(3, 10)
|
||||
x.insertAt(3, 10)
|
||||
x
|
||||
>>> [1, 2, 3, 10]
|
||||
|
||||
but it is much simpler, and we recommend to use '+='
|
||||
|
||||
val x = [1,2,3]
|
||||
x += 10
|
||||
>>> [1, 2, 3, 10]
|
||||
|
||||
## Removing list items
|
||||
|
||||
val x = [1, 2, 3, 4, 5]
|
||||
x.removeAt(2)
|
||||
assert( x == [1, 2, 4, 5])
|
||||
// or remove range (start inclusive, end exclusive):
|
||||
x.removeRangeInclusive(1,2)
|
||||
x.removeRange(1..2)
|
||||
assert( x == [1, 5])
|
||||
>>> void
|
||||
|
||||
Again, you can use negative indexes. For example, removing last elements like:
|
||||
There is a shortcut to remove the last elements:
|
||||
|
||||
val x = [1, 2, 3, 4, 5]
|
||||
|
||||
// remove last:
|
||||
x.removeAt(-1)
|
||||
x.removeLast()
|
||||
assert( x == [1, 2, 3, 4])
|
||||
|
||||
// remove 2 last:
|
||||
x.removeRangeInclusive(-2,-1)
|
||||
assert( x == [1, 2])
|
||||
x.removeLast(2)
|
||||
assertEquals( [1, 2], x)
|
||||
>>> void
|
||||
|
||||
You can get ranges to extract a portion from a list:
|
||||
|
||||
val list = [1, 2, 3, 4, 5]
|
||||
assertEquals( [1,2,3], list[..2])
|
||||
assertEquals( [1,2,], list[..<2])
|
||||
assertEquals( [4,5], list[3..])
|
||||
assertEquals( [2,3], list[1..2])
|
||||
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.
|
||||
|
||||
assertEquals( Set(3, 2, 1), Set( 1, 2, 3))
|
||||
assert( 5 !in Set(1, 2, 6) )
|
||||
>>> void
|
||||
|
||||
Please see [Set] for detailed description.
|
||||
|
||||
# Maps
|
||||
|
||||
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
|
||||
are [Iterable]:
|
||||
|
||||
val m = Map( "foo" => 77, "bar" => "buzz" )
|
||||
assertEquals( m["foo"], 77 )
|
||||
>>> void
|
||||
|
||||
Please see [Map] reference for detailed description on using Maps.
|
||||
|
||||
# Flow control operators
|
||||
|
||||
## if-then-else
|
||||
@@ -552,6 +693,92 @@ Or, more neat:
|
||||
>>> just 3
|
||||
>>> void
|
||||
|
||||
## When
|
||||
|
||||
It is very much like the kotlin's:
|
||||
|
||||
fun type(x) {
|
||||
when(x) {
|
||||
in 'a'..'z', in 'A'..'Z' -> "letter"
|
||||
in '0'..'9' -> "digit"
|
||||
'$' -> "dollar"
|
||||
"EUR" -> "crap"
|
||||
in ['@', '#', '^'] -> "punctuation1"
|
||||
in "*&.," -> "punctuation2"
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
assertEquals("digit", type('3'))
|
||||
assertEquals("dollar", type('$'))
|
||||
assertEquals("crap", type("EUR"))
|
||||
>>> void
|
||||
|
||||
Notice, several conditions can be grouped with a comma.
|
||||
Also, you can check the type too:
|
||||
|
||||
fun type(x) {
|
||||
when(x) {
|
||||
"42", 42 -> "answer to the great question"
|
||||
is Real, is Int -> "number"
|
||||
is String -> {
|
||||
for( d in x ) {
|
||||
if( d !in '0'..'9' )
|
||||
break "unknown"
|
||||
}
|
||||
else "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals("number", type(5))
|
||||
assertEquals("number", type("153"))
|
||||
assertEquals("number", type(π/2))
|
||||
assertEquals("unknown", type("12%"))
|
||||
assertEquals("answer to the great question", type(42))
|
||||
assertEquals("answer to the great question", type("42"))
|
||||
>>> void
|
||||
|
||||
### supported when conditions:
|
||||
|
||||
#### Contains:
|
||||
|
||||
You can thest that _when expression_ is _contained_, or not contained, in some object using `in container` and `!in container`. The container is any object that provides `contains` method, otherwise the runtime exception will be thrown.
|
||||
|
||||
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 (3) |
|
||||
| Range | object is included in the range (2) |
|
||||
|
||||
(1)
|
||||
: Iterable is not the container as it can be infinite
|
||||
|
||||
(2)
|
||||
: Depending on the inclusivity and open/closed range parameters. BE careful here: String range is allowed, but it is usually not what you expect of it:
|
||||
|
||||
assert( "more" in "a".."z") // string range ok
|
||||
assert( 'x' !in "a".."z") // char in string range: probably error
|
||||
assert( 'x' in 'a'..'z') // character range: ok
|
||||
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:
|
||||
|
||||
"foo" in "foobar"
|
||||
>>> true
|
||||
|
||||
and also character inclusion:
|
||||
|
||||
'o' in "foobar"
|
||||
>>> true
|
||||
|
||||
## while
|
||||
|
||||
Regular pre-condition while loop, as expression, loop returns the last expression as everything else:
|
||||
@@ -873,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
|
||||
@@ -923,15 +1161,56 @@ Are the same as in string literals with little difference:
|
||||
|
||||
## String details
|
||||
|
||||
Strings are much like Kotlin ones:
|
||||
Strings are arrays of Unicode characters. It can be indexed, and indexing will
|
||||
return a valid Unicode character at position. No utf hassle:
|
||||
|
||||
"Hello".length
|
||||
"Парашют"[5]
|
||||
>>> 'ю'
|
||||
|
||||
Its length is, of course, in characters:
|
||||
|
||||
"разум".length
|
||||
>>> 5
|
||||
|
||||
And supports growing set of kotlin-borrowed operations, see below, for example:
|
||||
|
||||
assertEquals("Hell", "Hello".dropLast(1))
|
||||
>>> void
|
||||
|
||||
To format a string use sprintf-style modifiers like:
|
||||
|
||||
val a = "hello"
|
||||
val b = 11
|
||||
assertEquals( "hello:11", "%s:%d"(a, b) )
|
||||
assertEquals( " hello: 11", "%6s:%6d"(a, b) )
|
||||
assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) )
|
||||
>>> void
|
||||
|
||||
List of format specifiers closely resembles C sprintf() one. See [format specifiers](https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary), this is doe using [mp_stools kotlin multiplatform library](https://github.com/sergeych/mp_stools). Currently supported Lyng types are `String`, `Int`, `Real`, `Bool`, the rest are displayed using their `toString()` representation.
|
||||
|
||||
This list will be extended.
|
||||
|
||||
To get the substring use:
|
||||
|
||||
assertEquals("pult", "catapult".takeLast(4))
|
||||
assertEquals("cat", "catapult".take(3))
|
||||
assertEquals("cat", "catapult".dropLast(5))
|
||||
assertEquals("pult", "catapult".drop(4))
|
||||
>>> void
|
||||
|
||||
And to get a portion you can slice it with range as the index:
|
||||
|
||||
assertEquals( "tap", "catapult"[ 2 .. 4 ])
|
||||
assertEquals( "tap", "catapult"[ 2 ..< 5 ])
|
||||
>>> void
|
||||
|
||||
Open-ended ranges could be used to get start and end too:
|
||||
|
||||
assertEquals( "cat", "catapult"[ ..< 3 ])
|
||||
assertEquals( "cat", "catapult"[ .. 2 ])
|
||||
assertEquals( "pult", "catapult"[ 4.. ])
|
||||
>>> void
|
||||
|
||||
### String operations
|
||||
|
||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
||||
@@ -939,16 +1218,28 @@ 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 |
|
||||
| endsWith(prefix) | true if ends with a prefix |
|
||||
| take(n) | get a new string from up to n first characters |
|
||||
| takeLast(n) | get a new string from up to n last characters |
|
||||
| drop(n) | get a new string dropping n first chars, or empty string |
|
||||
| dropLast(n) | get a new string dropping n last chars, or empty string |
|
||||
| size | size in characters like `length` because String is [Array] |
|
||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||
| [index] | character at index |
|
||||
| [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.
|
||||
|
||||
|
||||
|
||||
@@ -983,5 +1274,8 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
[Iterator]: Iterator.md
|
||||
[Real]: Real.md
|
||||
[Range]: Range.md
|
||||
|
||||
|
||||
[String]: String.md
|
||||
[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,11 +0,0 @@
|
||||
count = 0
|
||||
for n1 in range(10):
|
||||
for n2 in range(10):
|
||||
for n3 in range(10):
|
||||
for n4 in range(10):
|
||||
for n5 in range(10):
|
||||
for n6 in range(10):
|
||||
if n1 + n2 + n3 == n4 + n5 + n6:
|
||||
count += 1
|
||||
|
||||
print(count)
|
||||
@@ -1,123 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
class Context(
|
||||
val parent: Context?,
|
||||
val args: Arguments = Arguments.EMPTY,
|
||||
var pos: Pos = Pos.builtIn,
|
||||
val thisObj: Obj = ObjVoid,
|
||||
var skipContextCreation: Boolean = false,
|
||||
) {
|
||||
constructor(
|
||||
args: Arguments = Arguments.EMPTY,
|
||||
pos: Pos = Pos.builtIn,
|
||||
)
|
||||
: this(Script.defaultContext, args, pos)
|
||||
|
||||
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
||||
raiseError(ObjIndexOutOfBoundsException(this, message))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseArgumentError(message: String = "Illegal argument error"): 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 val objects = mutableMapOf<String, ObjRecord>()
|
||||
|
||||
operator fun get(name: String): ObjRecord? =
|
||||
objects[name]
|
||||
?: parent?.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,428 +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 {
|
||||
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
|
||||
|
||||
fun isInstanceOf(someClass: Obj) = someClass === objClass || objClass.allParentsSet.contains(someClass)
|
||||
|
||||
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 {
|
||||
context.raiseNotImplemented()
|
||||
}
|
||||
|
||||
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 by lazy {
|
||||
ObjClass("Obj").apply {
|
||||
addFn("toString") {
|
||||
thisObj.asStr
|
||||
}
|
||||
addFn("contains") {
|
||||
ObjBool(thisObj.contains(this, args.firstAndOnly()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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: Int): 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 {
|
||||
inline fun <reified T> from(obj: T): Obj {
|
||||
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)
|
||||
Unit -> ObjVoid
|
||||
null -> ObjNull
|
||||
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 fun toString(): String = "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(
|
||||
"NullPointerException",
|
||||
"AssertionFailedException",
|
||||
"ClassCastException",
|
||||
"IndexOutOfBoundsException",
|
||||
"IllegalArgumentException",
|
||||
"IllegalAssignmentException",
|
||||
"SymbolNotDefinedException",
|
||||
"IterationEndException",
|
||||
"AccessException",
|
||||
"UnknownException",
|
||||
)) {
|
||||
context.addConst(name, getOrCreateExceptionClass(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObjNullPointerException(context: Context) : ObjException("NullPointerException", 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)
|
||||
|
||||
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
|
||||
ObjException("IllegalAssignmentException", 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,27 +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())
|
||||
|
||||
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,151 +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
|
||||
}.toSet()
|
||||
|
||||
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
|
||||
) {
|
||||
if (name in members || allParentsSet.any { name in it.members })
|
||||
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 null
|
||||
}
|
||||
|
||||
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
|
||||
getInstanceMemberOrNull(name)
|
||||
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class that must provide `iterator` method that returns [ObjIterator] instance.
|
||||
*/
|
||||
val ObjIterable by lazy {
|
||||
ObjClass("Iterable").apply {
|
||||
|
||||
addFn("toList") {
|
||||
val result = mutableListOf<Obj>()
|
||||
val iterator = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
|
||||
while (iterator.invokeInstanceMethod(this, "hasNext").toBool())
|
||||
result += iterator.invokeInstanceMethod(this, "next")
|
||||
|
||||
|
||||
// val next = iterator.getMemberOrNull("next")!!
|
||||
// val hasNext = iterator.getMemberOrNull("hasNext")!!
|
||||
// while( hasNext.invoke(this, iterator).toBool() )
|
||||
// result += next.invoke(this, iterator)
|
||||
ObjList(result)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection is an iterator with `size`]
|
||||
*/
|
||||
val ObjCollection by lazy {
|
||||
val i: ObjClass = ObjIterable
|
||||
ObjClass("Collection", i)
|
||||
}
|
||||
|
||||
val ObjIterator by lazy { ObjClass("Iterator") }
|
||||
|
||||
class ObjArrayIterator(val array: Obj) : Obj() {
|
||||
|
||||
override val objClass: ObjClass by lazy { type }
|
||||
|
||||
private var nextIndex = 0
|
||||
private var lastIndex = 0
|
||||
|
||||
suspend fun init(context: Context) {
|
||||
nextIndex = 0
|
||||
lastIndex = array.invokeInstanceMethod(context, "size").toInt()
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type by lazy {
|
||||
ObjClass("ArrayIterator", ObjIterator).apply {
|
||||
addFn("next") {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
if (self.nextIndex < self.lastIndex) {
|
||||
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
|
||||
} else raiseError(ObjIterationFinishedException(this))
|
||||
}
|
||||
addFn("hasNext") {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val ObjArray by lazy {
|
||||
|
||||
/**
|
||||
* Array abstract class is a [ObjCollection] with `getAt` method.
|
||||
*/
|
||||
ObjClass("Array", ObjCollection).apply {
|
||||
// we can create iterators using size/getat:
|
||||
|
||||
addFn("iterator") {
|
||||
ObjArrayIterator(thisObj).also { it.init(this) }
|
||||
}
|
||||
addFn("isample") { "ok".toObj() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,80 +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 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
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("Int")
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toObj() = ObjInt(this.toLong())
|
||||
@@ -1,137 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
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() }
|
||||
}]"
|
||||
|
||||
fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int {
|
||||
val i = if (index < 0) list.size + index else index
|
||||
if (allowisEndInclusive && i == list.size) return i
|
||||
if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}")
|
||||
return i
|
||||
}
|
||||
|
||||
override suspend fun getAt(context: Context, index: Int): Obj {
|
||||
val i = normalize(context, index)
|
||||
return list[i]
|
||||
}
|
||||
|
||||
override suspend fun putAt(context: Context, index: Int, newValue: Obj) {
|
||||
val i = normalize(context, index)
|
||||
list[i] = newValue
|
||||
}
|
||||
|
||||
override suspend fun compareTo(context: Context, 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])
|
||||
}
|
||||
}
|
||||
// equal so far, longer is greater:
|
||||
return when {
|
||||
mySize < otherSize -> -1
|
||||
mySize > otherSize -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun plus(context: Context, other: Obj): Obj =
|
||||
when {
|
||||
other is ObjList ->
|
||||
ObjList((list + other.list).toMutableList())
|
||||
|
||||
other.isInstanceOf(ObjIterable) -> {
|
||||
val l = other.callMethod<ObjList>(context, "toList")
|
||||
ObjList((list + l.list).toMutableList())
|
||||
}
|
||||
|
||||
else ->
|
||||
context.raiseError("'+': can't concatenate $this with $other")
|
||||
}
|
||||
|
||||
|
||||
override suspend fun plusAssign(context: Context, 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
|
||||
list += otherList.list
|
||||
} else
|
||||
list += other
|
||||
return this
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("List", ObjArray).apply {
|
||||
|
||||
createField("size",
|
||||
statement {
|
||||
(thisObj as ObjList).list.size.toObj()
|
||||
}
|
||||
)
|
||||
addFn("getAt") {
|
||||
requireExactCount(1)
|
||||
thisAs<ObjList>().getAt(this, requiredArg<ObjInt>(0).value.toInt())
|
||||
}
|
||||
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
|
||||
for (a in args) l.add(a)
|
||||
ObjVoid
|
||||
}
|
||||
)
|
||||
createField("addAt",
|
||||
statement {
|
||||
if (args.size < 2) raiseError("addAt takes 2+ arguments")
|
||||
val l = thisAs<ObjList>()
|
||||
var index = l.normalize(
|
||||
this, requiredArg<ObjInt>(0).value.toInt(),
|
||||
allowisEndInclusive = true
|
||||
)
|
||||
for (i in 1..<args.size) l.list.add(index++, args[i])
|
||||
ObjVoid
|
||||
}
|
||||
)
|
||||
addFn("removeAt") {
|
||||
val self = thisAs<ObjList>()
|
||||
val start = self.normalize(this, requiredArg<ObjInt>(0).value.toInt())
|
||||
if (args.size == 2) {
|
||||
val end = requireOnlyArg<ObjInt>().value.toInt()
|
||||
self.list.subList(start, self.normalize(this, end)).clear()
|
||||
} else
|
||||
self.list.removeAt(start)
|
||||
self
|
||||
}
|
||||
addFn("removeRangeInclusive") {
|
||||
val self = thisAs<ObjList>()
|
||||
val start = self.normalize(this, requiredArg<ObjInt>(0).value.toInt())
|
||||
val end = self.normalize(this, requiredArg<ObjInt>(1).value.toInt()) + 1
|
||||
self.list.subList(start, end).clear()
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
override val longValue: Long by lazy { floor(value).toLong() }
|
||||
override val doubleValue: Double by lazy { value }
|
||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||
|
||||
override fun byValueCopy(): Obj = ObjReal(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 =
|
||||
ObjReal(this.value + other.toDouble())
|
||||
|
||||
override suspend fun minus(context: Context, other: Obj): Obj =
|
||||
ObjReal(this.value - other.toDouble())
|
||||
|
||||
override suspend fun mul(context: Context, other: Obj): Obj =
|
||||
ObjReal(this.value * other.toDouble())
|
||||
|
||||
override suspend fun div(context: Context, other: Obj): Obj =
|
||||
ObjReal(this.value / other.toDouble())
|
||||
|
||||
override suspend fun mod(context: Context, other: Obj): Obj =
|
||||
ObjReal(this.value % other.toDouble())
|
||||
|
||||
companion object {
|
||||
val type: ObjClass = ObjClass("Real").apply {
|
||||
createField(
|
||||
"roundToInt",
|
||||
statement(Pos.builtIn) {
|
||||
(it.thisObj as ObjReal).value.roundToLong().toObj()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("string")
|
||||
data class ObjString(val value: String) : Obj() {
|
||||
|
||||
override suspend fun compareTo(context: Context, other: Obj): Int {
|
||||
if (other !is ObjString) return -2
|
||||
return this.value.compareTo(other.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
|
||||
override val asStr: ObjString by lazy { this }
|
||||
|
||||
override fun inspect(): String {
|
||||
return "\"$value\""
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
override suspend fun plus(context: Context, other: Obj): Obj {
|
||||
return ObjString(value + other.asStr.value)
|
||||
}
|
||||
|
||||
override suspend fun getAt(context: Context, index: Int): Obj {
|
||||
return ObjChar(value[index])
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("String").apply {
|
||||
addConst("startsWith",
|
||||
statement {
|
||||
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
|
||||
})
|
||||
addConst("length",
|
||||
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
)
|
||||
addFn("takeLast") {
|
||||
thisAs<ObjString>().value.takeLast(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("take") {
|
||||
thisAs<ObjString>().value.take(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("drop") {
|
||||
thisAs<ObjString>().value.drop(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("dropLast") {
|
||||
thisAs<ObjString>().value.dropLast(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("lower") {
|
||||
thisAs<ObjString>().value.lowercase().let(::ObjString)
|
||||
}
|
||||
addFn("upper") {
|
||||
thisAs<ObjString>().value.uppercase().let(::ObjString)
|
||||
}
|
||||
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
class Source(val fileName: String, text: String) {
|
||||
|
||||
val lines = text.lines().map { it.trimEnd() }
|
||||
|
||||
companion object {
|
||||
val builtIn: Source by lazy { Source("built-in", "") }
|
||||
val UNKNOWN: Source by lazy { Source("UNKNOWN", "") }
|
||||
}
|
||||
|
||||
val startPos: Pos = Pos(this, 0, 0)
|
||||
|
||||
fun posAt(line: Int, column: Int): Pos = Pos(this, line, column)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
|
||||
|
||||
class Symbols(
|
||||
unitType: UnitType,
|
||||
val name: String,
|
||||
val x: TypeDecl
|
||||
) {
|
||||
enum class UnitType {
|
||||
Module, Function, Lambda
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
sealed class TypeDecl {
|
||||
// ??
|
||||
data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
|
||||
object Obj : TypeDecl()
|
||||
}
|
||||
|
||||
/*
|
||||
To use in the compiler, we need symbol information when:
|
||||
|
||||
- declaring a class: the only way to export its public/protected symbols is to know it in compiler time
|
||||
- importing a module: actually, we cam try to do it in a more efficient way.
|
||||
|
||||
Importing module:
|
||||
|
||||
The moudule is efficiently a statement, that initializes it with all its symbols modifying some context.
|
||||
|
||||
The thing is, we need only
|
||||
|
||||
*/
|
||||
@@ -29,11 +29,11 @@ kotlin {
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(project(":library"))
|
||||
implementation(kotlin("stdlib-common"))
|
||||
implementation(project(":lynglib"))
|
||||
implementation(libs.okio)
|
||||
|
||||
implementation(libs.clikt)
|
||||
|
||||
implementation(kotlin("stdlib-common"))
|
||||
// optional support for rendering markdown in help messages
|
||||
// implementation(libs.clikt.markdown)
|
||||
}
|
||||
@@ -42,9 +42,15 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(kotlin("test-common"))
|
||||
implementation(kotlin("test-annotations-common"))
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.okio.fakefilesystem)
|
||||
}
|
||||
}
|
||||
// val nativeMain by getting {
|
||||
// dependencies {
|
||||
// implementation(kotlin("stdlib-common"))
|
||||
// }
|
||||
// }
|
||||
val linuxX64Main by getting {
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -17,14 +24,47 @@ import okio.use
|
||||
|
||||
expect fun exit(code: Int)
|
||||
|
||||
val baseContext = Context().apply {
|
||||
expect class ShellCommandExecutor {
|
||||
fun executeCommand(command: String): CommandResult
|
||||
|
||||
companion object {
|
||||
fun create(): ShellCommandExecutor
|
||||
}
|
||||
}
|
||||
|
||||
data class CommandResult(
|
||||
val exitCode: Int,
|
||||
val output: String,
|
||||
val error: String
|
||||
)
|
||||
|
||||
val baseScope = Scope().apply {
|
||||
addFn("exit") {
|
||||
exit(requireOnlyArg<ObjInt>().toInt())
|
||||
ObjVoid
|
||||
}
|
||||
// ObjString.type.addFn("shell") {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -38,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.
|
||||
|
||||
@@ -57,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()
|
||||
)
|
||||
@@ -65,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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,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!!) }
|
||||
}
|
||||
}
|
||||
@@ -89,14 +129,26 @@ 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) {
|
||||
val text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
||||
fileSource.buffer().use { bs ->
|
||||
bs.readUtf8()
|
||||
}
|
||||
}
|
||||
if( text.startsWith("#!") ) {
|
||||
// skip shebang
|
||||
val pos = text.indexOf('\n')
|
||||
text = text.substring(pos + 1)
|
||||
}
|
||||
processErrors {
|
||||
Compiler().compile(Source(fileName, text)).execute(baseContext)
|
||||
baseScope.eval(Source(fileName, text))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
|
||||
package net.sergeych
|
||||
|
||||
// Alternative implementation for native targets
|
||||
actual class ShellCommandExecutor() {
|
||||
actual fun executeCommand(command: String): CommandResult {
|
||||
val process = ProcessBuilder("/bin/sh", "-c", command).start()
|
||||
val exitCode = process.waitFor()
|
||||
val output = process.inputStream.bufferedReader().readText()
|
||||
val error = process.errorStream.bufferedReader().readText()
|
||||
|
||||
return CommandResult(
|
||||
exitCode = exitCode,
|
||||
output = output.trim(),
|
||||
error = error.trim()
|
||||
)
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun create(): ShellCommandExecutor = ShellCommandExecutor()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,7 +1,48 @@
|
||||
@file:OptIn(ExperimentalForeignApi::class, ExperimentalForeignApi::class, ExperimentalForeignApi::class)
|
||||
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.fgets
|
||||
import platform.posix.pclose
|
||||
import platform.posix.popen
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
actual class ShellCommandExecutor() {
|
||||
actual fun executeCommand(command: String): CommandResult {
|
||||
val outputBuilder = StringBuilder()
|
||||
val errorBuilder = StringBuilder()
|
||||
|
||||
val fp = popen(command, "r") ?: return CommandResult(
|
||||
exitCode = -1,
|
||||
output = "",
|
||||
error = "Failed to execute command"
|
||||
)
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
while (true) {
|
||||
val bytesRead = buffer.usePinned { pinned ->
|
||||
fgets(pinned.addressOf(0), buffer.size.convert(), fp)
|
||||
}
|
||||
if (bytesRead == null) break
|
||||
outputBuilder.append(bytesRead.toKString())
|
||||
}
|
||||
|
||||
val status = pclose(fp)
|
||||
val exitCode = if (status == 0) 0 else 1
|
||||
|
||||
return CommandResult(
|
||||
exitCode = exitCode,
|
||||
output = outputBuilder.toString().trim(),
|
||||
error = errorBuilder.toString().trim()
|
||||
)
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun create(): ShellCommandExecutor = ShellCommandExecutor()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exit(code: Int) {
|
||||
exitProcess(code)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.5.1-SNAPSHOT"
|
||||
version = "0.8.1-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
@@ -20,9 +19,10 @@ buildscript {
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.vanniktech.mavenPublish)
|
||||
kotlin("plugin.serialization") version "2.1.20"
|
||||
// alias(libs.plugins.vanniktech.mavenPublish)
|
||||
kotlin("plugin.serialization") version "2.2.0"
|
||||
id("com.codingfeline.buildkonfig") version "0.17.1"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
buildkonfig {
|
||||
@@ -62,7 +62,7 @@ kotlin {
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
languageSettings.optIn("kotlin.contracts.ExperimentalContracts::class")
|
||||
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
|
||||
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
|
||||
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
|
||||
}
|
||||
@@ -99,75 +99,57 @@ android {
|
||||
}
|
||||
dependencies {
|
||||
implementation(libs.firebase.crashlytics.buildtools)
|
||||
implementation(libs.compiler)
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||
publishing {
|
||||
val mavenToken by lazy {
|
||||
File("${System.getProperty("user.home")}/.gitea_token").readText()
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
credentials(HttpHeaderCredentials::class) {
|
||||
name = "Authorization"
|
||||
value = mavenToken
|
||||
}
|
||||
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||
authentication {
|
||||
create("Authorization", HttpHeaderAuthentication::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signAllPublications()
|
||||
|
||||
coordinates(group.toString(), "library", version.toString())
|
||||
|
||||
pom {
|
||||
name = "Lyng language"
|
||||
description = "Kotlin-bound scripting loanguage"
|
||||
inceptionYear = "2025"
|
||||
// url = "https://sergeych.net"
|
||||
licenses {
|
||||
license {
|
||||
name = "XXX"
|
||||
url = "YYY"
|
||||
distribution = "ZZZ"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "XXX"
|
||||
name = "YYY"
|
||||
url = "ZZZ"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url = "XXX"
|
||||
connection = "YYY"
|
||||
developerConnection = "ZZZ"
|
||||
}
|
||||
}
|
||||
}
|
||||
//mavenPublishing {
|
||||
// publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||
//
|
||||
//val projectVersion by project.extra(provider {
|
||||
// // Compute value lazily
|
||||
// (version as String)
|
||||
//})
|
||||
// signAllPublications()
|
||||
//
|
||||
//val generateBuildConfig by tasks.registering {
|
||||
// // Declare outputs safely
|
||||
// val outputDir = layout.buildDirectory.dir("generated/buildConfig/commonMain/kotlin")
|
||||
// outputs.dir(outputDir)
|
||||
// coordinates(group.toString(), "library", version.toString())
|
||||
//
|
||||
// val version = projectVersion.get()
|
||||
//
|
||||
// // Inputs: Version is tracked as an input
|
||||
// inputs.property("version", version)
|
||||
//
|
||||
// doLast {
|
||||
// val packageName = "net.sergeych.lyng.buildconfig"
|
||||
// val packagePath = packageName.replace('.', '/')
|
||||
// val buildConfigFile = outputDir.get().file("$packagePath/BuildConfig.kt").asFile
|
||||
//
|
||||
// buildConfigFile.parentFile?.mkdirs()
|
||||
// buildConfigFile.writeText(
|
||||
// """
|
||||
// |package $packageName
|
||||
// |
|
||||
// |object BuildConfig {
|
||||
// | const val VERSION = "$version"
|
||||
// |}
|
||||
// """.trimMargin()
|
||||
// )
|
||||
// pom {
|
||||
// name = "Lyng language"
|
||||
// description = "Kotlin-bound scripting loanguage"
|
||||
// inceptionYear = "2025"
|
||||
//// url = "https://sergeych.net"
|
||||
// licenses {
|
||||
// license {
|
||||
// name = "XXX"
|
||||
// url = "YYY"
|
||||
// distribution = "ZZZ"
|
||||
// }
|
||||
// }
|
||||
// developers {
|
||||
// developer {
|
||||
// id = "XXX"
|
||||
// name = "YYY"
|
||||
// url = "ZZZ"
|
||||
// }
|
||||
// }
|
||||
// scm {
|
||||
// url = "XXX"
|
||||
// connection = "YYY"
|
||||
// developerConnection = "ZZZ"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
// dependsOn(generateBuildConfig)
|
||||
//}
|
||||
File renamed without changes.
@@ -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]
|
||||
}
|
||||
+21
-14
@@ -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.raiseArgumentError("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.raiseArgumentError("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.raiseArgumentError("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,
|
||||
/**
|
||||
+20
-4
@@ -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)
|
||||
@@ -28,11 +32,23 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
|
||||
|
||||
data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
|
||||
|
||||
constructor(vararg values: Obj) : this(values.toList())
|
||||
|
||||
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
|
||||
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
|
||||
return list.first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to list of kotlin objects, see [Obj.toKotlin].
|
||||
*/
|
||||
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 renamed without changes.
+623
-299
File diff suppressed because it is too large.
Load diff
+60
-32
@@ -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
|
||||
@@ -21,7 +19,11 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
|
||||
fun hasNext() = currentIndex < tokens.size
|
||||
fun hasPrevious() = currentIndex > 0
|
||||
fun next() = tokens.getOrElse(currentIndex) { throw IllegalStateException("No next token") }.also { currentIndex++ }
|
||||
fun next() =
|
||||
if( currentIndex < tokens.size ) tokens[currentIndex++]
|
||||
else Token("", tokens.last().pos, Token.Type.EOF)
|
||||
// throw IllegalStateException("No more tokens")
|
||||
|
||||
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
|
||||
|
||||
fun savePos() = currentIndex
|
||||
@@ -47,9 +49,25 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
throw ScriptError(at, message)
|
||||
}
|
||||
|
||||
fun currentPos() =
|
||||
if (hasNext()) next().pos.also { previous() }
|
||||
else previous().pos.also { next() }
|
||||
fun syntaxError(message: String = "Syntax error"): Nothing {
|
||||
throw ScriptError(currentPos(), message)
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -83,7 +101,7 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
}
|
||||
|
||||
|
||||
fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
|
||||
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
|
||||
val t = next()
|
||||
return if (t.type == typeId) {
|
||||
f(t)
|
||||
@@ -102,6 +120,7 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
* Return value of the next token if it is an identifier, null otherwise.
|
||||
* Does not change position.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun nextIdValue(): String? {
|
||||
return if (hasNext()) {
|
||||
val nt = tokens[currentIndex]
|
||||
@@ -117,36 +136,45 @@ internal class CompilerContext(val tokens: List<Token>) {
|
||||
/**
|
||||
* If the token at current position plus offset (could be negative) exists, returns it, otherwise returns null.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun atOffset(offset: Int): Token? =
|
||||
if (currentIndex + offset in tokens.indices) tokens[currentIndex + offset] else null
|
||||
|
||||
fun matchQualifiers(keyword: String, vararg qualifiers: String): Boolean {
|
||||
val pos = savePos()
|
||||
var count = 0
|
||||
while( count < qualifiers.size) {
|
||||
val t = next()
|
||||
when(t.type) {
|
||||
Token.Type.ID -> {
|
||||
if( t.value in qualifiers ) count++
|
||||
else { restorePos(pos); return false }
|
||||
}
|
||||
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {}
|
||||
else -> { restorePos(pos); return false }
|
||||
}
|
||||
}
|
||||
val t = next()
|
||||
if( t.type == Token.Type.ID && t.value == keyword ) {
|
||||
return true
|
||||
} else {
|
||||
restorePos(pos)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan backwards as deep as specified looking for visibility token. Does not change position.
|
||||
* Skip newlines and comments. Returns (and reads) first non-whitespace token.
|
||||
* Note that [Token.Type.EOF] is not considered a whitespace token.
|
||||
*/
|
||||
fun getVisibility(default: Visibility = Visibility.Public, depths: Int = 2): Visibility {
|
||||
for( i in -depths .. -1) {
|
||||
when( atOffset(i)?.type) {
|
||||
Token.Type.PROTECTED -> return Visibility.Protected
|
||||
Token.Type.PRIVATE -> return Visibility.Private
|
||||
else -> {}
|
||||
fun skipWsTokens(): Token {
|
||||
while( current().type in wstokens ) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
return default
|
||||
return next()
|
||||
}
|
||||
|
||||
// fun expectKeyword(vararg keyword: String): String {
|
||||
// val t = next()
|
||||
// if (t.type != Token.Type.ID && t.value !in keyword) {
|
||||
// throw ScriptError(t.pos, "expected one of ${keyword.joinToString()}")
|
||||
//
|
||||
// }
|
||||
|
||||
// data class ReturnScope(val needCatch: Boolean = false)
|
||||
|
||||
// private val
|
||||
|
||||
// fun startReturnScope(): ReturnScope {
|
||||
// return ReturnScope()
|
||||
// }
|
||||
|
||||
companion object {
|
||||
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT)
|
||||
}
|
||||
}
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Accessor
|
||||
|
||||
sealed class ListEntry {
|
||||
data class Element(val accessor: Accessor) : ListEntry()
|
||||
|
||||
+3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-18
@@ -45,12 +45,18 @@ private class Parser(fromPos: Pos) {
|
||||
'=' -> {
|
||||
if (pos.currentChar == '=') {
|
||||
pos.advance()
|
||||
if (currentChar == '=') {
|
||||
when (currentChar) {
|
||||
'=' -> {
|
||||
pos.advance()
|
||||
Token("===", from, Token.Type.REF_EQ)
|
||||
} else
|
||||
Token("==", from, Token.Type.EQ)
|
||||
} else
|
||||
}
|
||||
else -> Token("==", from, Token.Type.EQ)
|
||||
}
|
||||
} else if( currentChar == '>' ) {
|
||||
pos.advance()
|
||||
Token("=>", from, Token.Type.EQARROW)
|
||||
}
|
||||
else
|
||||
Token("=", from, Token.Type.ASSIGN)
|
||||
}
|
||||
|
||||
@@ -103,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)
|
||||
}
|
||||
|
||||
'*' -> {
|
||||
@@ -137,15 +143,21 @@ private class Parser(fromPos: Pos) {
|
||||
if (currentChar == '.') {
|
||||
pos.advance()
|
||||
// .. already parsed:
|
||||
if (currentChar == '.') {
|
||||
when (currentChar) {
|
||||
'.' -> {
|
||||
pos.advance()
|
||||
Token("...", from, Token.Type.ELLIPSIS)
|
||||
} else if (currentChar == '<') {
|
||||
}
|
||||
|
||||
'<' -> {
|
||||
pos.advance()
|
||||
Token("..<", from, Token.Type.DOTDOTLT)
|
||||
} else {
|
||||
}
|
||||
|
||||
else -> {
|
||||
Token("..", from, Token.Type.DOTDOT)
|
||||
}
|
||||
}
|
||||
} else
|
||||
Token(".", from, Token.Type.DOT)
|
||||
}
|
||||
@@ -156,8 +168,7 @@ private class Parser(fromPos: Pos) {
|
||||
if (currentChar == '>') {
|
||||
pos.advance()
|
||||
Token("<=>", from, Token.Type.SHUTTLE)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Token("<=", from, Token.Type.LTE)
|
||||
}
|
||||
} else
|
||||
@@ -236,9 +247,10 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
|
||||
'"' -> loadStringToken()
|
||||
|
||||
in digitsSet -> {
|
||||
pos.back()
|
||||
decodeNumber(loadChars(digits), from)
|
||||
decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
|
||||
}
|
||||
|
||||
'\'' -> {
|
||||
@@ -261,6 +273,21 @@ private class Parser(fromPos: Pos) {
|
||||
Token(value.toString(), start, Token.Type.CHAR)
|
||||
}
|
||||
|
||||
'?' -> {
|
||||
when(currentChar.also { pos.advance() }) {
|
||||
':' -> Token("??", from, Token.Type.ELVIS)
|
||||
'?' -> Token("??", from, Token.Type.ELVIS)
|
||||
'.' -> Token("?.", from, Token.Type.NULL_COALESCE)
|
||||
'[' -> Token("?(", from, Token.Type.NULL_COALESCE_INDEX)
|
||||
'(' -> Token("?(", from, Token.Type.NULL_COALESCE_INVOKE)
|
||||
'{' -> Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE)
|
||||
else -> {
|
||||
pos.back()
|
||||
Token("?", from, Token.Type.QUESTION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// text infix operators:
|
||||
// Labels processing is complicated!
|
||||
@@ -280,8 +307,6 @@ private class Parser(fromPos: Pos) {
|
||||
when (text) {
|
||||
"in" -> Token("in", from, Token.Type.IN)
|
||||
"is" -> Token("is", from, Token.Type.IS)
|
||||
"protected" -> Token("protected", from, Token.Type.PROTECTED)
|
||||
"private" -> Token("private", from, Token.Type.PRIVATE)
|
||||
else -> Token(text, from, Token.Type.ID)
|
||||
}
|
||||
} else
|
||||
@@ -338,7 +363,7 @@ private class Parser(fromPos: Pos) {
|
||||
// could be integer, also hex:
|
||||
if (currentChar == 'x' && p1 == "0") {
|
||||
pos.advance()
|
||||
Token(loadChars({ it in hexDigits }), start, Token.Type.HEX).also {
|
||||
Token(loadChars { it in hexDigits }, start, Token.Type.HEX).also {
|
||||
if (currentChar.isLetter())
|
||||
raise("invalid hex literal")
|
||||
}
|
||||
@@ -351,10 +376,11 @@ private class Parser(fromPos: Pos) {
|
||||
private val currentChar: Char get() = pos.currentChar
|
||||
|
||||
private fun loadStringToken(): Token {
|
||||
var start = currentPos
|
||||
val start = currentPos
|
||||
|
||||
if (currentChar == '"') pos.advance()
|
||||
else start = start.back()
|
||||
// if (currentChar == '"') pos.advance()
|
||||
// else start = start.back()
|
||||
// start = start.back()
|
||||
|
||||
val sb = StringBuilder()
|
||||
while (currentChar != '"') {
|
||||
@@ -419,7 +445,7 @@ private class Parser(fromPos: Pos) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadToEnd(): String {
|
||||
private fun loadToEndOfLine(): String {
|
||||
val result = StringBuilder()
|
||||
val l = pos.line
|
||||
do {
|
||||
@@ -453,4 +479,10 @@ private class Parser(fromPos: Pos) {
|
||||
return null
|
||||
}
|
||||
|
||||
init {
|
||||
// skip shebang
|
||||
if( pos.readFragment("#!") )
|
||||
loadToEndOfLine()
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -5,6 +5,7 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
|
||||
return "${source.fileName}:${line+1}:${column}"
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun back(): Pos =
|
||||
if( column > 0) Pos(source, line, column-1)
|
||||
else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1)
|
||||
@@ -0,0 +1,201 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.pacman.ImportProvider
|
||||
|
||||
/**
|
||||
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
|
||||
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
|
||||
*
|
||||
* To create default scope, use default `Scope()` constructor, it will create a scope with a parent
|
||||
* module scope with default [ImportManager], you can access with [currentImportProvider] as needed.
|
||||
*
|
||||
* If you want to create [ModuleScope] by hand, try [currentImportProvider] and [ImportManager.newModule],
|
||||
* or [ImportManager.newModuleAt].
|
||||
*
|
||||
* There are special types of scopes:
|
||||
*
|
||||
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
|
||||
*/
|
||||
open class Scope(
|
||||
val parent: Scope?,
|
||||
val args: Arguments = Arguments.EMPTY,
|
||||
var pos: Pos = Pos.builtIn,
|
||||
var thisObj: Obj = ObjVoid,
|
||||
var skipScopeCreation: Boolean = false,
|
||||
) {
|
||||
open val packageName: String = "<anonymous package>"
|
||||
|
||||
constructor(
|
||||
args: Arguments = Arguments.EMPTY,
|
||||
pos: Pos = Pos.builtIn,
|
||||
)
|
||||
: this(Script.defaultImportManager.copy().newModuleAt(pos), args, pos)
|
||||
|
||||
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
||||
raiseError(ObjIndexOutOfBoundsException(this, message))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
|
||||
raiseError(ObjIllegalArgumentException(this, message))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing =
|
||||
raiseError(ObjIllegalStateException(this, message))
|
||||
|
||||
fun raiseIllegalAssignment(message: String): Nothing =
|
||||
raiseError(ObjIllegalAssignmentException(this, message))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
|
||||
raiseError(ObjIllegalArgumentException(this, message))
|
||||
|
||||
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
|
||||
|
||||
@Suppress("unused")
|
||||
fun raiseSymbolNotFound(name: String): Nothing =
|
||||
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
|
||||
|
||||
fun raiseError(message: String): Nothing {
|
||||
throw ExecutionError(ObjException(this, message))
|
||||
}
|
||||
|
||||
fun raiseError(obj: ObjException): Nothing {
|
||||
throw ExecutionError(obj)
|
||||
}
|
||||
|
||||
inline fun <reified T : Obj> requiredArg(index: Int): T {
|
||||
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
|
||||
return (args.list[index] as? T)
|
||||
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
|
||||
}
|
||||
|
||||
inline fun <reified T : Obj> requireOnlyArg(): T {
|
||||
if (args.list.size != 1) raiseError("Expected exactly 1 argument, got ${args.list.size}")
|
||||
return requiredArg(0)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun requireExactCount(count: Int) {
|
||||
if (args.list.size != count) {
|
||||
raiseError("Expected exactly $count arguments, got ${args.list.size}")
|
||||
}
|
||||
}
|
||||
|
||||
fun requireNoArgs() {
|
||||
if( args.list.isNotEmpty())
|
||||
raiseError("This function does not accept any arguments")
|
||||
}
|
||||
|
||||
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
|
||||
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
|
||||
|
||||
internal val objects = mutableMapOf<String, ObjRecord>()
|
||||
|
||||
open operator fun get(name: String): ObjRecord? =
|
||||
if (name == "this") thisObj.asReadonly
|
||||
else {
|
||||
objects[name]
|
||||
?: parent?.get(name)
|
||||
?: thisObj.objClass.getInstanceMemberOrNull(name)
|
||||
}
|
||||
|
||||
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||
|
||||
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||
|
||||
fun copy() = Scope(this, args, pos, thisObj)
|
||||
|
||||
fun addItem(
|
||||
name: String,
|
||||
isMutable: Boolean,
|
||||
value: Obj,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||
): ObjRecord {
|
||||
return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
|
||||
}
|
||||
|
||||
fun getOrCreateNamespace(name: String): ObjClass {
|
||||
val ns = objects.getOrPut(name) { ObjRecord(ObjNamespace(name), isMutable = false) }.value
|
||||
return ns.objClass
|
||||
}
|
||||
|
||||
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
|
||||
addFn<ObjVoid>(*names) {
|
||||
fn(this)
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Scope.() -> T) {
|
||||
val newFn = object : Statement() {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj = scope.fn()
|
||||
|
||||
}
|
||||
for (name in names) {
|
||||
addItem(
|
||||
name,
|
||||
false,
|
||||
newFn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addConst(name: String, value: Obj) = addItem(name, false, value)
|
||||
|
||||
suspend fun eval(code: String): Obj =
|
||||
Compiler.compile(code.toSource(), currentImportProvider).execute(this)
|
||||
|
||||
suspend fun eval(source: Source): Obj =
|
||||
Compiler.compile(
|
||||
source,
|
||||
currentImportProvider
|
||||
).execute(this)
|
||||
|
||||
fun containsLocal(name: String): Boolean = name in objects
|
||||
|
||||
/**
|
||||
* Some scopes can be imported into other scopes, like [ModuleScope]. Those must correctly implement this method.
|
||||
* @param scope where to copy symbols from this module
|
||||
* @param symbols symbols to import, ir present, only symbols keys will be imported renamed to corresponding values
|
||||
*/
|
||||
open suspend fun importInto(scope: Scope, symbols: Map<String, String>? = null) {
|
||||
scope.raiseError(ObjIllegalOperationException(scope, "Import is not allowed here: import $packageName"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a first [ImportManager] in this Scope hierarchy. Normally there should be one. Found instance is cached.
|
||||
*
|
||||
* Use it to register your package sources, see [ImportManager] features.
|
||||
*
|
||||
* @throws IllegalStateException if there is no such manager (if you create some specific scope with no manager,
|
||||
* then you knew what you did)
|
||||
*/
|
||||
val currentImportProvider: ImportProvider by lazy {
|
||||
if (this is ModuleScope)
|
||||
importProvider.getActualProvider()
|
||||
else
|
||||
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
|
||||
}
|
||||
|
||||
val importManager by lazy { (currentImportProvider as? ImportManager)
|
||||
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
|
||||
|
||||
companion object {
|
||||
|
||||
fun new(): Scope =
|
||||
Script.defaultImportManager.copy().newModuleAt(Pos.builtIn)
|
||||
}
|
||||
}
|
||||
+47
-5
@@ -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 {
|
||||
@@ -146,20 +156,24 @@ class Script(
|
||||
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
|
||||
}
|
||||
|
||||
addConst("Object", rootObjectType)
|
||||
addConst("Real", ObjReal.type)
|
||||
addConst("String", ObjString.type)
|
||||
addConst("Int", ObjInt.type)
|
||||
addConst("Bool", ObjBool.type)
|
||||
addConst("Char", ObjChar.type)
|
||||
addConst("List", ObjList.type)
|
||||
addConst("Set", ObjSet.type)
|
||||
addConst("Range", ObjRange.type)
|
||||
addConst("Map", ObjMap.type)
|
||||
addConst("MapEntry", ObjMapEntry.type)
|
||||
@Suppress("RemoveRedundantQualifierName")
|
||||
addConst("Callable", Statement.type)
|
||||
// interfaces
|
||||
addConst("Iterable", ObjIterable)
|
||||
addConst("Collection", ObjCollection)
|
||||
addConst("Array", ObjArray)
|
||||
addConst("Class", ObjClassType)
|
||||
addConst("Object", Obj().objClass)
|
||||
|
||||
val pi = ObjReal(PI)
|
||||
addConst("π", pi)
|
||||
@@ -167,5 +181,33 @@ class Script(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
class Source(val fileName: String, text: String) {
|
||||
|
||||
val lines = text.lines().map { it.trimEnd() }
|
||||
|
||||
companion object {
|
||||
val builtIn: Source by lazy { Source("built-in", "") }
|
||||
val UNKNOWN: Source by lazy { Source("UNKNOWN", "") }
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -14,14 +14,18 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||
IN, NOTIN, IS, NOTIS,
|
||||
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ,
|
||||
SHUTTLE,
|
||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, QUESTION, COLONCOLON,
|
||||
AND, BITAND, OR, BITOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
|
||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||
LABEL, ATLABEL, // label@ at@label
|
||||
PRIVATE, PROTECTED,
|
||||
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
||||
ELLIPSIS, DOTDOT, DOTDOTLT,
|
||||
NEWLINE,
|
||||
EOF,
|
||||
NULL_COALESCE,
|
||||
ELVIS,
|
||||
NULL_COALESCE_INDEX,
|
||||
NULL_COALESCE_INVOKE,
|
||||
NULL_COALESCE_BLOCKINVOKE,
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -0,0 +1,14 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
// this is highly experimental and subject to complete redesign
|
||||
// very soon
|
||||
sealed class TypeDecl(val isNullable:Boolean = false) {
|
||||
// ??
|
||||
// data class Fn(val argTypes: List<ArgsDeclaration.Item>, val retType: TypeDecl) : TypeDecl()
|
||||
object TypeAny : TypeDecl()
|
||||
object TypeNullableAny : TypeDecl(true)
|
||||
|
||||
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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)
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
val ObjArray by lazy {
|
||||
|
||||
/**
|
||||
* Array abstract class is a [ObjCollection] with `getAt` method.
|
||||
*/
|
||||
ObjClass("Array", ObjCollection).apply {
|
||||
// we can create iterators using size/getat:
|
||||
|
||||
addFn("iterator") {
|
||||
ObjArrayIterator(thisObj).also { it.init(this) }
|
||||
}
|
||||
|
||||
addFn("contains", isOpen = true) {
|
||||
val obj = args.firstAndOnly()
|
||||
for (i in 0..<thisObj.invokeInstanceMethod(this, "size").toInt()) {
|
||||
if (thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0) return@addFn ObjTrue
|
||||
}
|
||||
ObjFalse
|
||||
}
|
||||
|
||||
addFn("last") {
|
||||
thisObj.invokeInstanceMethod(
|
||||
this,
|
||||
"getAt",
|
||||
(thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj()
|
||||
)
|
||||
}
|
||||
|
||||
addFn("lastIndex") { (thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() }
|
||||
|
||||
addFn("indices") {
|
||||
ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
|
||||
class ObjArrayIterator(val array: Obj) : Obj() {
|
||||
|
||||
override val objClass: ObjClass by lazy { type }
|
||||
|
||||
private var nextIndex = 0
|
||||
private var lastIndex = 0
|
||||
|
||||
suspend fun init(scope: Scope) {
|
||||
nextIndex = 0
|
||||
lastIndex = array.invokeInstanceMethod(scope, "size").toInt()
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type by lazy {
|
||||
ObjClass("ArrayIterator", ObjIterator).apply {
|
||||
addFn("next") {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
if (self.nextIndex < self.lastIndex) {
|
||||
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
|
||||
} else raiseError(ObjIterationFinishedException(this))
|
||||
}
|
||||
addFn("hasNext") {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.bintools.toDump
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.BitArray
|
||||
|
||||
class ObjBitBuffer(val bitArray: BitArray) : Obj() {
|
||||
|
||||
override val objClass = type
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return bitArray[index.toLong()].toObj()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = object: ObjClass("BitBuffer", ObjArray) {
|
||||
|
||||
}.apply {
|
||||
addFn("toBuffer") {
|
||||
requireNoArgs()
|
||||
ObjBuffer(thisAs<ObjBitBuffer>().bitArray.asUbyteArray())
|
||||
}
|
||||
addFn("toDump") {
|
||||
requireNoArgs()
|
||||
ObjString(
|
||||
thisAs<ObjBitBuffer>().bitArray.asUbyteArray().toDump()
|
||||
)
|
||||
}
|
||||
addFn("size") {
|
||||
thisAs<ObjBitBuffer>().bitArray.size.toObj()
|
||||
}
|
||||
addFn("sizeInBytes") {
|
||||
ObjInt((thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
data class ObjBool(val value: Boolean) : Obj() {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjBool) return -2
|
||||
return value.compareTo(other.value)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override suspend fun logicalNot(scope: Scope): Obj = ObjBool(!value)
|
||||
|
||||
override suspend fun logicalAnd(scope: Scope, other: Obj): Obj = ObjBool(value && other.toBool())
|
||||
|
||||
override suspend fun logicalOr(scope: Scope, other: Obj): Obj = ObjBool(value || other.toBool())
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.Bool
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.encodeBoolean(value)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjBool
|
||||
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("Bool") {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder,lynonType: LynonType?): Obj {
|
||||
return ObjBool(decoder.unpackBoolean())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ObjTrue = ObjBool(true)
|
||||
val ObjFalse = ObjBool(false)
|
||||
@@ -0,0 +1,166 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import net.sergeych.bintools.encodeToHex
|
||||
import net.sergeych.bintools.toDump
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.statement
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
import kotlin.math.min
|
||||
|
||||
open class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
fun checkIndex(scope: Scope, index: Obj): Int {
|
||||
if (index !is ObjInt)
|
||||
scope.raiseIllegalArgument("index must be Int")
|
||||
val i = index.value.toInt()
|
||||
if (i < 0) scope.raiseIllegalArgument("index must be positive")
|
||||
if (i >= byteArray.size)
|
||||
scope.raiseIndexOutOfBounds("index $i is out of bounds 0..<${byteArray.size}")
|
||||
return i
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
// notice: we create a copy if content, so we don't want it
|
||||
// to be treated as modifiable, or putAt will not be called:
|
||||
return if (index is ObjRange) {
|
||||
val start: Int = index.startInt(scope)
|
||||
val end: Int = index.exclusiveIntEnd(scope) ?: size
|
||||
ObjBuffer(byteArray.sliceArray(start..<end))
|
||||
} else ObjInt(byteArray[checkIndex(scope, index)].toLong(), true)
|
||||
}
|
||||
|
||||
val size by byteArray::size
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return byteArray.hashCode()
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjBuffer) return super.compareTo(scope, other)
|
||||
val limit = min(size, other.size)
|
||||
for (i in 0..<limit) {
|
||||
val own = byteArray[i]
|
||||
val their = other.byteArray[i]
|
||||
if (own < their) return -1
|
||||
else if (own > their) return 1
|
||||
}
|
||||
if (size < other.size) return -1
|
||||
if (size > other.size) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return if (other is ObjBuffer)
|
||||
ObjBuffer(byteArray + other.byteArray)
|
||||
else if (other.isInstanceOf(ObjIterable)) {
|
||||
ObjBuffer(
|
||||
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
|
||||
.toUByteArray()
|
||||
)
|
||||
} else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect()}")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Buffer(${byteArray.encodeToHex()})"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjBuffer
|
||||
|
||||
return byteArray contentEquals other.byteArray
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.Buffer
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.encodeCachedBytes(byteArray.asByteArray())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private suspend fun createBufferFrom(scope: Scope, obj: Obj): ObjBuffer =
|
||||
when (obj) {
|
||||
is ObjBuffer -> ObjBuffer(obj.byteArray.copyOf())
|
||||
is ObjInt -> {
|
||||
if (obj.value < 0)
|
||||
scope.raiseIllegalArgument("buffer size must be positive")
|
||||
val data = UByteArray(obj.value.toInt())
|
||||
ObjBuffer(data)
|
||||
}
|
||||
|
||||
is ObjString -> ObjBuffer(obj.value.encodeToByteArray().asUByteArray())
|
||||
else -> {
|
||||
if (obj.isInstanceOf(ObjIterable)) {
|
||||
ObjBuffer(
|
||||
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
|
||||
.toUByteArray()
|
||||
)
|
||||
} else
|
||||
scope.raiseIllegalArgument(
|
||||
"can't construct buffer from ${obj.inspect()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val type = object : ObjClass("Buffer", ObjArray) {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val args = scope.args.list
|
||||
return when (args.size) {
|
||||
// empty buffer
|
||||
0 -> ObjBuffer(ubyteArrayOf())
|
||||
1 -> createBufferFrom(scope, args[0])
|
||||
else -> {
|
||||
// create buffer from array, each argument should be a byte then:
|
||||
val data = UByteArray(args.size)
|
||||
for ((i, b) in args.withIndex()) {
|
||||
val code = when (b) {
|
||||
is ObjChar -> b.value.code.toUByte()
|
||||
is ObjInt -> b.value.toUByte()
|
||||
else -> scope.raiseIllegalArgument(
|
||||
"invalid byte value for buffer constructor at index $i: ${b.inspect()}"
|
||||
)
|
||||
}
|
||||
data[i] = code
|
||||
}
|
||||
ObjBuffer(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
ObjBuffer( decoder.decodeCached {
|
||||
decoder.decompress().asUByteArray()
|
||||
})
|
||||
|
||||
}.apply {
|
||||
createField("size",
|
||||
statement {
|
||||
(thisObj as ObjBuffer).byteArray.size.toObj()
|
||||
}
|
||||
)
|
||||
addFn("decodeUtf8") {
|
||||
ObjString(
|
||||
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
|
||||
)
|
||||
}
|
||||
addFn("toMutable") {
|
||||
requireNoArgs()
|
||||
ObjMutableBuffer(thisAs<ObjBuffer>().byteArray.copyOf())
|
||||
}
|
||||
addFn("toDump") {
|
||||
requireNoArgs()
|
||||
ObjString(
|
||||
thisAs<ObjBuffer>().byteArray.toByteArray().toDump()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
|
||||
class ObjChar(val value: Char): Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int =
|
||||
(other as? ObjChar)?.let { value.compareTo(it.value) } ?: -1
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
|
||||
override fun inspect(): String = "'$value'"
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjChar
|
||||
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("Char").apply {
|
||||
addFn("code") { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
val ObjClassType by lazy { ObjClass("Class") }
|
||||
|
||||
open class ObjClass(
|
||||
val className: String,
|
||||
vararg parents: ObjClass,
|
||||
) : Obj() {
|
||||
|
||||
val classNameObj by lazy { ObjString(className) }
|
||||
|
||||
var constructorMeta: ArgsDeclaration? = null
|
||||
var instanceConstructor: Statement? = null
|
||||
|
||||
/**
|
||||
* the scope for class methods, initialize class vars, etc.
|
||||
*
|
||||
* Important notice. When create a user class, e.g. from Lyng source, it should
|
||||
* be set to a scope by compiler, so it could access local closure, etc. Otherwise,
|
||||
* it will be initialized to default scope on first necessity, e.g. when used in
|
||||
* external, kotlin classes with [addClassConst] and [addClassFn], etc.
|
||||
*/
|
||||
var classScope: Scope? = null
|
||||
|
||||
val allParentsSet: Set<ObjClass> =
|
||||
parents.flatMap {
|
||||
listOf(it) + it.allParentsSet
|
||||
}.toMutableSet()
|
||||
|
||||
override val objClass: ObjClass by lazy { ObjClassType }
|
||||
|
||||
/**
|
||||
* members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass]
|
||||
*/
|
||||
private val members = mutableMapOf<String, ObjRecord>()
|
||||
|
||||
override fun toString(): String = className
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val instance = ObjInstance(this)
|
||||
instance.instanceScope = scope.copy(newThisObj = instance,args = scope.args)
|
||||
if (instanceConstructor != null) {
|
||||
instanceConstructor!!.execute(instance.instanceScope)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
|
||||
fun createField(
|
||||
name: String,
|
||||
initialValue: Obj,
|
||||
isMutable: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
pos: Pos = Pos.builtIn
|
||||
) {
|
||||
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
|
||||
if( existing?.isMutable == false)
|
||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||
members[name] = ObjRecord(initialValue, isMutable, visibility)
|
||||
}
|
||||
|
||||
private fun initClassScope(): Scope {
|
||||
if( classScope == null ) classScope = Scope()
|
||||
return classScope!!
|
||||
}
|
||||
|
||||
fun createClassField(
|
||||
name: String,
|
||||
initialValue: Obj,
|
||||
isMutable: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
pos: Pos = Pos.builtIn
|
||||
) {
|
||||
initClassScope()
|
||||
val existing = classScope!!.objects[name]
|
||||
if( existing != null)
|
||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
||||
classScope!!.addItem(name, isMutable, initialValue, visibility)
|
||||
}
|
||||
|
||||
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
||||
createField(name, statement { code() }, isOpen)
|
||||
}
|
||||
|
||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
|
||||
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
||||
createClassField(name, statement { code() }, isOpen)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
|
||||
*/
|
||||
fun getInstanceMemberOrNull(name: String): ObjRecord? {
|
||||
members[name]?.let { return it }
|
||||
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
|
||||
return rootObjectType.members[name]
|
||||
}
|
||||
|
||||
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
|
||||
getInstanceMemberOrNull(name)
|
||||
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
||||
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
classScope?.objects?.get(name)?.let {
|
||||
if( it.visibility.isPublic ) return it
|
||||
}
|
||||
return super.readField(scope, name)
|
||||
}
|
||||
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
initClassScope().objects[name]?.let {
|
||||
if( it.isMutable) it.value = newValue
|
||||
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
|
||||
}
|
||||
?: super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: Obj?): Obj {
|
||||
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
}
|
||||
|
||||
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
/**
|
||||
* Collection is an iterator with `size`]
|
||||
*/
|
||||
val ObjCollection = ObjClass("Collection", ObjIterable).apply {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
class ObjDuration(val duration: Duration) : Obj() {
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override fun toString(): String {
|
||||
return duration.toString()
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
return if( other is ObjDuration)
|
||||
duration.compareTo(other.duration)
|
||||
else -1
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return duration.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjDuration
|
||||
|
||||
return duration == other.duration
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("Duration") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val args = scope.args
|
||||
if( args.list.size > 1 )
|
||||
scope.raiseIllegalArgument("can't construct Duration(${args.inspect()})")
|
||||
val a0 = args.list.getOrNull(0)
|
||||
|
||||
return ObjDuration(
|
||||
when (a0) {
|
||||
null -> Duration.ZERO
|
||||
is ObjInt -> a0.value.seconds
|
||||
is ObjReal -> a0.value.seconds
|
||||
else -> {
|
||||
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}.apply {
|
||||
addFn("days") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj()
|
||||
}
|
||||
addFn("hours") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj()
|
||||
}
|
||||
addFn("minutes") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj()
|
||||
}
|
||||
addFn("seconds") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj()
|
||||
}
|
||||
addFn("milliseconds") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj()
|
||||
}
|
||||
addFn("microseconds") {
|
||||
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj()
|
||||
}
|
||||
// extensions
|
||||
|
||||
ObjInt.type.addFn("seconds") {
|
||||
ObjDuration(thisAs<ObjInt>().value.seconds)
|
||||
}
|
||||
|
||||
ObjInt.type.addFn("second") {
|
||||
ObjDuration(thisAs<ObjInt>().value.seconds)
|
||||
}
|
||||
ObjInt.type.addFn("milliseconds") {
|
||||
ObjDuration(thisAs<ObjInt>().value.milliseconds)
|
||||
}
|
||||
|
||||
ObjInt.type.addFn("millisecond") {
|
||||
ObjDuration(thisAs<ObjInt>().value.milliseconds)
|
||||
}
|
||||
ObjReal.type.addFn("seconds") {
|
||||
ObjDuration(thisAs<ObjReal>().value.seconds)
|
||||
}
|
||||
|
||||
ObjReal.type.addFn("second") {
|
||||
ObjDuration(thisAs<ObjReal>().value.seconds)
|
||||
}
|
||||
|
||||
ObjReal.type.addFn("milliseconds") {
|
||||
ObjDuration(thisAs<ObjReal>().value.milliseconds)
|
||||
}
|
||||
ObjReal.type.addFn("millisecond") {
|
||||
ObjDuration(thisAs<ObjReal>().value.milliseconds)
|
||||
}
|
||||
|
||||
ObjInt.type.addFn("minutes") {
|
||||
ObjDuration(thisAs<ObjInt>().value.minutes)
|
||||
}
|
||||
ObjReal.type.addFn("minutes") {
|
||||
ObjDuration(thisAs<ObjReal>().value.minutes)
|
||||
}
|
||||
ObjInt.type.addFn("minute") {
|
||||
ObjDuration(thisAs<ObjInt>().value.minutes)
|
||||
}
|
||||
ObjReal.type.addFn("minute") {
|
||||
ObjDuration(thisAs<ObjReal>().value.minutes)
|
||||
}
|
||||
ObjInt.type.addFn("hours") {
|
||||
ObjDuration(thisAs<ObjInt>().value.hours)
|
||||
}
|
||||
ObjReal.type.addFn("hours") {
|
||||
ObjDuration(thisAs<ObjReal>().value.hours)
|
||||
}
|
||||
ObjInt.type.addFn("hour") {
|
||||
ObjDuration(thisAs<ObjInt>().value.hours)
|
||||
}
|
||||
ObjReal.type.addFn("hour") {
|
||||
ObjDuration(thisAs<ObjReal>().value.hours)
|
||||
}
|
||||
ObjInt.type.addFn("days") {
|
||||
ObjDuration(thisAs<ObjInt>().value.days)
|
||||
}
|
||||
ObjReal.type.addFn("days") {
|
||||
ObjDuration(thisAs<ObjReal>().value.days)
|
||||
}
|
||||
ObjInt.type.addFn("day") {
|
||||
ObjDuration(thisAs<ObjInt>().value.days)
|
||||
}
|
||||
ObjReal.type.addFn("day") {
|
||||
ObjDuration(thisAs<ObjReal>().value.days)
|
||||
}
|
||||
|
||||
|
||||
// addFn("epochSeconds") {
|
||||
// val instant = thisAs<ObjInstant>().instant
|
||||
// ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
|
||||
// }
|
||||
// addFn("epochMilliseconds") {
|
||||
// ObjInt(instant.toEpochMilliseconds())
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
|
||||
internal lateinit var instanceScope: Scope
|
||||
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
return instanceScope[name]?.let {
|
||||
if (it.visibility.isPublic)
|
||||
it
|
||||
else
|
||||
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
|
||||
}
|
||||
?: super.readField(scope, name)
|
||||
}
|
||||
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
instanceScope[name]?.let { f ->
|
||||
if (!f.visibility.isPublic)
|
||||
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (f.value.assign(scope, newValue) == null)
|
||||
f.value = newValue
|
||||
} ?: super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: Obj?): Obj =
|
||||
instanceScope[name]?.let {
|
||||
if (it.visibility.isPublic)
|
||||
it.value.invoke(scope, this, args)
|
||||
else
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
|
||||
}
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
|
||||
private val publicFields: Map<String, ObjRecord>
|
||||
get() = instanceScope.objects.filter { it.value.visibility.isPublic }
|
||||
|
||||
override fun toString(): String {
|
||||
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
|
||||
return "${objClass.className}($fields)"
|
||||
}
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
val meta = objClass.constructorMeta
|
||||
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
||||
// actual constructor can vary, for example, adding new fields with default
|
||||
// values, so we save size of the construction:
|
||||
|
||||
// using objlist allow for some optimizations:
|
||||
val params = meta.params.map { readField(scope, it.name).value }
|
||||
encoder.encodeAnyList(scope, params)
|
||||
serializeStateVars(scope, encoder)
|
||||
}
|
||||
|
||||
protected val instanceVars: Map<String, ObjRecord> by lazy {
|
||||
instanceScope.objects.filter { it.value.type.serializable }
|
||||
}
|
||||
|
||||
protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) {
|
||||
val vars = instanceVars.values.map { it.value }
|
||||
if( vars.isNotEmpty()) {
|
||||
encoder.encodeAnyList(scope, vars)
|
||||
println("serialized state vars $vars")
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
|
||||
val localVars = instanceVars.values.toList()
|
||||
if( localVars.isNotEmpty() ) {
|
||||
println("gonna read vars")
|
||||
val vars = decoder.decodeAnyList(scope)
|
||||
if (vars.size > instanceVars.size)
|
||||
scope.raiseIllegalArgument("serialized vars has bigger size than instance vars")
|
||||
println("deser state vars $vars")
|
||||
for ((i, v) in vars.withIndex()) {
|
||||
localVars[i].value = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected val comparableVars: Map<String, ObjRecord> by lazy {
|
||||
instanceScope.objects.filter {
|
||||
it.value.type.comparable
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjInstance) return -1
|
||||
if (other.objClass != objClass) return -1
|
||||
for (f in comparableVars) {
|
||||
val a = f.value.value
|
||||
val b = other.instanceScope[f.key]!!.value
|
||||
val d = a.compareTo(scope, b)
|
||||
if (d != 0) return d
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
/**
|
||||
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
|
||||
*/
|
||||
class ObjInstanceClass(val name: String) : ObjClass(name) {
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
val args = decoder.decodeAnyList(scope)
|
||||
println("deserializing constructor $name, $args params")
|
||||
val actualSize = constructorMeta?.params?.size ?: 0
|
||||
if( args.size > actualSize )
|
||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
||||
val newScope = scope.copy(args = Arguments(args))
|
||||
return (callOn(newScope) as ObjInstance).apply {
|
||||
deserializeStateVars(scope,decoder)
|
||||
invokeInstanceMethod(scope, "onDeserialized", onNotFoundResult = ObjVoid)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.isDistantFuture
|
||||
import kotlinx.datetime.isDistantPast
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonSettings
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() {
|
||||
override val objClass: ObjClass get() = type
|
||||
|
||||
override fun toString(): String {
|
||||
return instant.toString()
|
||||
}
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return when (other) {
|
||||
is ObjDuration -> ObjInstant(instant + other.duration)
|
||||
else -> super.plus(scope, other)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj {
|
||||
return when (other) {
|
||||
is ObjDuration -> ObjInstant(instant - other.duration)
|
||||
is ObjInstant -> ObjDuration(instant - other.instant)
|
||||
else -> super.plus(scope, other)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if( other is ObjInstant) {
|
||||
return instant.compareTo(other.instant)
|
||||
}
|
||||
return super.compareTo(scope, other)
|
||||
}
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return instant
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return instant.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjInstant
|
||||
|
||||
return instant == other.instant
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.Instant
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.putBits(truncateMode.ordinal, 2)
|
||||
when(truncateMode) {
|
||||
LynonSettings.InstantTruncateMode.Millisecond ->
|
||||
encoder.encodeSigned(instant.toEpochMilliseconds())
|
||||
LynonSettings.InstantTruncateMode.Second ->
|
||||
encoder.encodeSigned(instant.epochSeconds)
|
||||
LynonSettings.InstantTruncateMode.Microsecond -> {
|
||||
encoder.encodeSigned(instant.epochSeconds)
|
||||
encoder.encodeUnsigned(instant.nanosecondsOfSecond.toULong() / 1000UL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val distantFuture by lazy {
|
||||
ObjInstant(Instant.DISTANT_FUTURE)
|
||||
}
|
||||
|
||||
val distantPast by lazy {
|
||||
ObjInstant(Instant.DISTANT_PAST)
|
||||
}
|
||||
|
||||
val type = object : ObjClass("Instant") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val args = scope.args
|
||||
val a0 = args.list.getOrNull(0)
|
||||
return ObjInstant(
|
||||
when (a0) {
|
||||
null -> {
|
||||
val t = Clock.System.now()
|
||||
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond)
|
||||
}
|
||||
is ObjInt -> Instant.fromEpochSeconds(a0.value)
|
||||
is ObjReal -> {
|
||||
val seconds = a0.value.toLong()
|
||||
val nanos = (a0.value - seconds) * 1e9
|
||||
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
||||
}
|
||||
is ObjInstant -> a0.instant
|
||||
|
||||
else -> {
|
||||
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
val mode = LynonSettings.InstantTruncateMode.entries[decoder.getBitsAsInt(2)]
|
||||
return when (mode) {
|
||||
LynonSettings.InstantTruncateMode.Microsecond -> ObjInstant(
|
||||
Instant.fromEpochSeconds(
|
||||
decoder.unpackSigned(), decoder.unpackUnsignedInt() * 1000
|
||||
)
|
||||
)
|
||||
LynonSettings.InstantTruncateMode.Millisecond -> ObjInstant(
|
||||
Instant.fromEpochMilliseconds(
|
||||
decoder.unpackSigned()
|
||||
)
|
||||
)
|
||||
LynonSettings.InstantTruncateMode.Second -> ObjInstant(
|
||||
Instant.fromEpochSeconds(decoder.unpackSigned())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}.apply {
|
||||
addFn("epochSeconds") {
|
||||
val instant = thisAs<ObjInstant>().instant
|
||||
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
|
||||
}
|
||||
addFn("isDistantFuture") {
|
||||
thisAs<ObjInstant>().instant.isDistantFuture.toObj()
|
||||
}
|
||||
addFn("isDistantPast") {
|
||||
thisAs<ObjInstant>().instant.isDistantPast.toObj()
|
||||
}
|
||||
addFn("epochWholeSeconds") {
|
||||
ObjInt(thisAs<ObjInstant>().instant.epochSeconds)
|
||||
}
|
||||
addFn("nanosecondsOfSecond") {
|
||||
ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong())
|
||||
}
|
||||
addFn("truncateToSecond") {
|
||||
val t = thisAs<ObjInstant>().instant
|
||||
ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
|
||||
}
|
||||
addFn("truncateToMillisecond") {
|
||||
val t = thisAs<ObjInstant>().instant
|
||||
ObjInstant(
|
||||
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
|
||||
LynonSettings.InstantTruncateMode.Millisecond
|
||||
)
|
||||
}
|
||||
addFn("truncateToMicrosecond") {
|
||||
val t = thisAs<ObjInstant>().instant
|
||||
ObjInstant(
|
||||
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),
|
||||
LynonSettings.InstantTruncateMode.Microsecond
|
||||
)
|
||||
}
|
||||
// class members
|
||||
|
||||
addClassConst("distantFuture", distantFuture)
|
||||
addClassConst("distantPast", distantPast)
|
||||
addClassFn("now") {
|
||||
ObjInstant(Clock.System.now())
|
||||
}
|
||||
// addFn("epochMilliseconds") {
|
||||
// ObjInt(instant.toEpochMilliseconds())
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
|
||||
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
|
||||
override val asStr get() = ObjString(value.toString())
|
||||
override val longValue get() = value
|
||||
override val doubleValue get() = value.toDouble()
|
||||
override val toObjInt get() = this
|
||||
override val toObjReal = ObjReal(doubleValue)
|
||||
|
||||
override fun byValueCopy(): Obj = ObjInt(value)
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override suspend fun getAndIncrement(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(value).also { value++ }
|
||||
}
|
||||
|
||||
override suspend fun getAndDecrement(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(value).also { value-- }
|
||||
}
|
||||
|
||||
override suspend fun incrementAndGet(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(++value)
|
||||
}
|
||||
|
||||
override suspend fun decrementAndGet(scope: Scope): Obj {
|
||||
ensureNotConst(scope)
|
||||
return ObjInt(--value)
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is Numeric) return -2
|
||||
return value.compareTo(other.doubleValue)
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value + other.value)
|
||||
else
|
||||
ObjReal(this.doubleValue + other.toDouble())
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value - other.value)
|
||||
else
|
||||
ObjReal(this.doubleValue - other.toDouble())
|
||||
|
||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt) {
|
||||
ObjInt(this.value * other.value)
|
||||
} else ObjReal(this.value * other.toDouble())
|
||||
|
||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value / other.value)
|
||||
else ObjReal(this.value / other.toDouble())
|
||||
|
||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
ObjInt(this.value % other.value)
|
||||
else ObjReal(this.value.toDouble() % other.toDouble())
|
||||
|
||||
/**
|
||||
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
|
||||
* assignment
|
||||
*/
|
||||
override suspend fun assign(scope: Scope, other: Obj): Obj? {
|
||||
return if (!isConst && other is ObjInt) {
|
||||
value = other.value
|
||||
this
|
||||
} else null
|
||||
}
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjInt
|
||||
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override suspend fun negate(scope: Scope): Obj {
|
||||
return ObjInt(-value)
|
||||
}
|
||||
|
||||
override suspend fun lynonType(): LynonType = when (value) {
|
||||
0L -> LynonType.Int0
|
||||
else -> {
|
||||
if (value > 0) LynonType.IntPositive
|
||||
else LynonType.IntNegative
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
when (lynonType) {
|
||||
null -> encoder.encodeSigned(value)
|
||||
LynonType.Int0 -> {}
|
||||
LynonType.IntPositive -> encoder.encodeUnsigned(value.toULong())
|
||||
LynonType.IntNegative -> encoder.encodeUnsigned((-value).toULong())
|
||||
LynonType.IntSigned -> encoder.encodeSigned(value)
|
||||
else -> scope.raiseIllegalArgument("Unsupported lynon type code for Int: $lynonType")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Zero = ObjInt(0, true)
|
||||
val One = ObjInt(1, true)
|
||||
val type = object : ObjClass("Int") {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
when (lynonType) {
|
||||
null -> ObjInt(decoder.unpackSigned())
|
||||
LynonType.Int0 -> Zero
|
||||
LynonType.IntPositive -> ObjInt(decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntNegative -> ObjInt(-decoder.unpackUnsigned().toLong())
|
||||
LynonType.IntSigned -> ObjInt(decoder.unpackSigned())
|
||||
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toObj() = ObjInt(this.toLong())
|
||||
@@ -0,0 +1,98 @@
|
||||
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.
|
||||
*/
|
||||
val ObjIterable by lazy {
|
||||
ObjClass("Iterable").apply {
|
||||
|
||||
addFn("toList") {
|
||||
val result = mutableListOf<Obj>()
|
||||
val iterator = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
|
||||
while (iterator.invokeInstanceMethod(this, "hasNext").toBool())
|
||||
result += iterator.invokeInstanceMethod(this, "next")
|
||||
ObjList(result)
|
||||
}
|
||||
|
||||
// it is not effective, but it is open:
|
||||
addFn("contains", isOpen = true) {
|
||||
val obj = args.firstAndOnly()
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0)
|
||||
return@addFn ObjTrue
|
||||
}
|
||||
ObjFalse
|
||||
}
|
||||
|
||||
addFn("indexOf", isOpen = true) {
|
||||
val obj = args.firstAndOnly()
|
||||
var index = 0
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0)
|
||||
return@addFn ObjInt(index.toLong())
|
||||
index++
|
||||
}
|
||||
ObjInt(-1L)
|
||||
}
|
||||
|
||||
addFn("toSet") {
|
||||
val result = mutableSetOf<Obj>()
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
result += it.invokeInstanceMethod(this, "next")
|
||||
}
|
||||
ObjSet(result)
|
||||
}
|
||||
|
||||
addFn("toMap") {
|
||||
val result = ObjMap()
|
||||
thisObj.toFlow(this).collect { pair ->
|
||||
result.map[pair.getAt(this,0)] = pair.getAt(this, 1)
|
||||
}
|
||||
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) {
|
||||
val it = thisObj.invokeInstanceMethod(this, "iterator")
|
||||
val fn = requiredArg<Statement>(0)
|
||||
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
|
||||
val x = it.invokeInstanceMethod(this, "next")
|
||||
fn.execute(this.copy(Arguments(listOf(x))))
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
addFn("map", isOpen = true) {
|
||||
val fn = requiredArg<Statement>(0)
|
||||
val result = mutableListOf<Obj>()
|
||||
thisObj.toFlow(this).collect {
|
||||
result += fn.call(this, it)
|
||||
}
|
||||
ObjList(result)
|
||||
}
|
||||
|
||||
addFn("isEmpty") {
|
||||
ObjBool(
|
||||
thisObj.invokeInstanceMethod(this, "iterator")
|
||||
.invokeInstanceMethod(this, "hasNext").toBool()
|
||||
.not()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
val ObjIterator by lazy { ObjClass("Iterator") }
|
||||
@@ -0,0 +1,58 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
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;
|
||||
* each object is converted to a Lyng object.
|
||||
*/
|
||||
class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
|
||||
|
||||
override val objClass = type
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("KotlinIterator", ObjIterator).apply {
|
||||
addFn("next") { thisAs<ObjKotlinIterator>().iterator.next().toObj() }
|
||||
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate kotlin iterator that already produces Lyng objects, no conversion
|
||||
* is applied
|
||||
*/
|
||||
class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
|
||||
|
||||
override val objClass = type
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("KotlinIterator", ObjIterator).apply {
|
||||
addFn("next") {
|
||||
thisAs<ObjKotlinObjIterator>().iterator.next()
|
||||
}
|
||||
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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() {
|
||||
|
||||
override fun toString(): String = "[${
|
||||
list.joinToString(separator = ", ") { it.inspect() }
|
||||
}]"
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return when (index) {
|
||||
is ObjInt -> {
|
||||
list[index.toInt()]
|
||||
}
|
||||
|
||||
is ObjRange -> {
|
||||
when {
|
||||
index.start is ObjInt && index.end is ObjInt -> {
|
||||
if (index.isEndInclusive)
|
||||
ObjList(list.subList(index.start.toInt(), index.end.toInt() + 1).toMutableList())
|
||||
else
|
||||
ObjList(list.subList(index.start.toInt(), index.end.toInt()).toMutableList())
|
||||
}
|
||||
|
||||
index.isOpenStart && !index.isOpenEnd -> {
|
||||
if (index.isEndInclusive)
|
||||
ObjList(list.subList(0, index.end!!.toInt() + 1).toMutableList())
|
||||
else
|
||||
ObjList(list.subList(0, index.end!!.toInt()).toMutableList())
|
||||
}
|
||||
|
||||
index.isOpenEnd && !index.isOpenStart -> {
|
||||
ObjList(list.subList(index.start!!.toInt(), list.size).toMutableList())
|
||||
}
|
||||
|
||||
index.isOpenStart && index.isOpenEnd -> {
|
||||
ObjList(list.toMutableList())
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw RuntimeException("Can't apply range for index: $index")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> scope.raiseIllegalArgument("Illegal index object for a list: ${index.inspect()}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
list[index.toInt()] = newValue
|
||||
}
|
||||
|
||||
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(scope, other.list[i]) != 0) {
|
||||
return list[i].compareTo(scope, other.list[i])
|
||||
}
|
||||
}
|
||||
// equal so far, longer is greater:
|
||||
return when {
|
||||
mySize < otherSize -> -1
|
||||
mySize > otherSize -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
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>(scope, "toList")
|
||||
ObjList((list + l.list).toMutableList())
|
||||
}
|
||||
|
||||
else ->
|
||||
scope.raiseError("'+': can't concatenate $this with $other")
|
||||
}
|
||||
|
||||
|
||||
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(scope, "toList") as ObjList
|
||||
list += otherList.list
|
||||
} else
|
||||
list += other
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun contains(scope: Scope, other: Obj): Boolean {
|
||||
return list.contains(other)
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any {
|
||||
return list.map { it.toKotlin(scope) }
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
// check?
|
||||
return list.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjList
|
||||
|
||||
return list == other.list
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
)
|
||||
createField("add",
|
||||
statement {
|
||||
val l = thisAs<ObjList>().list
|
||||
for (a in args) l.add(a)
|
||||
ObjVoid
|
||||
}
|
||||
)
|
||||
addFn("insertAt") {
|
||||
if (args.size < 2) raiseError("addAt takes 2+ arguments")
|
||||
val l = thisAs<ObjList>()
|
||||
var index = requiredArg<ObjInt>(0).value.toInt()
|
||||
for (i in 1..<args.size) l.list.add(index++, args[i])
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
addFn("removeAt") {
|
||||
val self = thisAs<ObjList>()
|
||||
val start = requiredArg<ObjInt>(0).value.toInt()
|
||||
if (args.size == 2) {
|
||||
val end = requireOnlyArg<ObjInt>().value.toInt()
|
||||
self.list.subList(start, end).clear()
|
||||
} else
|
||||
self.list.removeAt(start)
|
||||
self
|
||||
}
|
||||
|
||||
addFn("removeLast") {
|
||||
val self = thisAs<ObjList>()
|
||||
if (args.isNotEmpty()) {
|
||||
val count = requireOnlyArg<ObjInt>().value.toInt()
|
||||
val size = self.list.size
|
||||
if (count >= size) self.list.clear()
|
||||
else self.list.subList(size - count, size).clear()
|
||||
} else self.list.removeLast()
|
||||
self
|
||||
}
|
||||
|
||||
addFn("removeRange") {
|
||||
val self = thisAs<ObjList>()
|
||||
val list = self.list
|
||||
val range = requiredArg<Obj>(0)
|
||||
if (range is ObjRange) {
|
||||
val index = range
|
||||
when {
|
||||
index.start is ObjInt && index.end is ObjInt -> {
|
||||
if (index.isEndInclusive)
|
||||
list.subList(index.start.toInt(), index.end.toInt() + 1)
|
||||
else
|
||||
list.subList(index.start.toInt(), index.end.toInt())
|
||||
}
|
||||
|
||||
index.isOpenStart && !index.isOpenEnd -> {
|
||||
if (index.isEndInclusive)
|
||||
list.subList(0, index.end!!.toInt() + 1)
|
||||
else
|
||||
list.subList(0, index.end!!.toInt())
|
||||
}
|
||||
|
||||
index.isOpenEnd && !index.isOpenStart -> {
|
||||
list.subList(index.start!!.toInt(), list.size)
|
||||
}
|
||||
|
||||
index.isOpenStart && index.isOpenEnd -> {
|
||||
list
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw RuntimeException("Can't apply range for index: $index")
|
||||
}
|
||||
}.clear()
|
||||
} else {
|
||||
val start = range.toInt()
|
||||
val end = requiredArg<ObjInt>(1).value.toInt() + 1
|
||||
self.list.subList(start, end).clear()
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
-12
@@ -1,7 +1,12 @@
|
||||
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() {
|
||||
|
||||
val isOpenStart by lazy { start == null || start.isNull }
|
||||
val isOpenEnd by lazy { end == null || end.isNull }
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -12,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:
|
||||
@@ -23,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")
|
||||
}
|
||||
@@ -40,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
|
||||
@@ -64,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 {
|
||||
@@ -0,0 +1,91 @@
|
||||
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
|
||||
|
||||
data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
override val longValue: Long by lazy { floor(value).toLong() }
|
||||
override val doubleValue: Double by lazy { value }
|
||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
override fun byValueCopy(): Obj = ObjReal(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 fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||
ObjReal(this.value + other.toDouble())
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||
ObjReal(this.value - other.toDouble())
|
||||
|
||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||
ObjReal(this.value * other.toDouble())
|
||||
|
||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||
ObjReal(this.value / other.toDouble())
|
||||
|
||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||
ObjReal(this.value % other.toDouble())
|
||||
|
||||
/**
|
||||
* Returns unboxed Double value
|
||||
*/
|
||||
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 ObjReal
|
||||
|
||||
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 = 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"
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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(scope: Scope, other: Obj): Boolean {
|
||||
return set.contains(other)
|
||||
}
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return ObjSet(
|
||||
if (other is ObjSet)
|
||||
(set + other.set).toMutableSet()
|
||||
else
|
||||
(set + other).toMutableSet()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
|
||||
when (other) {
|
||||
is ObjSet -> {
|
||||
set += other.set
|
||||
}
|
||||
|
||||
is ObjList -> {
|
||||
set += other.list
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (other.isInstanceOf(ObjIterable)) {
|
||||
val i = other.invokeInstanceMethod(scope, "iterable")
|
||||
while (i.invokeInstanceMethod(scope, "hasNext").toBool()) {
|
||||
set += i.invokeInstanceMethod(scope, "next")
|
||||
}
|
||||
}
|
||||
set += other
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun mul(scope: Scope, other: Obj): Obj {
|
||||
return if (other is ObjSet) {
|
||||
ObjSet(set.intersect(other.set).toMutableSet())
|
||||
} else
|
||||
scope.raiseIllegalArgument("set operator * requires another set")
|
||||
}
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj {
|
||||
if (other !is ObjSet)
|
||||
scope.raiseIllegalArgument("set operator - requires another set")
|
||||
return ObjSet(set.minus(other.set).toMutableSet())
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Set(${set.joinToString(", ")})"
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
return if (other !is ObjSet) -1
|
||||
else {
|
||||
if (set == other.set) 0
|
||||
else -1
|
||||
}
|
||||
}
|
||||
|
||||
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(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()
|
||||
}
|
||||
addFn("intersect") {
|
||||
thisAs<ObjSet>().mul(this, args.firstAndOnly())
|
||||
}
|
||||
addFn("iterator") {
|
||||
thisAs<ObjSet>().set.iterator().toObj()
|
||||
}
|
||||
addFn("union") {
|
||||
thisAs<ObjSet>().plus(this, args.firstAndOnly())
|
||||
}
|
||||
addFn("subtract") {
|
||||
thisAs<ObjSet>().minus(this, args.firstAndOnly())
|
||||
}
|
||||
addFn("remove") {
|
||||
val set = thisAs<ObjSet>().set
|
||||
val n = set.size
|
||||
for( x in args.list ) set -= x
|
||||
if( n == set.size ) ObjFalse else ObjTrue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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
|
||||
@SerialName("string")
|
||||
data class ObjString(val value: String) : Obj() {
|
||||
|
||||
// fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
|
||||
// val i = if (index < 0) value.length + index else index
|
||||
// if (allowsEndInclusive && i == value.length) return i
|
||||
// if (i !in value.indices) context.raiseError("index $index out of bounds for length ${value.length} of \"$value\"")
|
||||
// return i
|
||||
// }
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjString) return -2
|
||||
return this.value.compareTo(other.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
|
||||
override val asStr: ObjString by lazy { this }
|
||||
|
||||
override fun inspect(): String {
|
||||
return "\"$value\""
|
||||
}
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return ObjString(value + other.asStr.value)
|
||||
}
|
||||
|
||||
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()
|
||||
val end = if (index.end == null || index.end.isNull) value.length else {
|
||||
val e = index.end.toInt()
|
||||
if (index.isEndInclusive) e + 1 else e
|
||||
}
|
||||
return ObjString(value.substring(start, end))
|
||||
}
|
||||
scope.raiseIllegalArgument("String index must be Int or Range")
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
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(scope: Scope, other: Obj): Boolean {
|
||||
return if (other is ObjString)
|
||||
value.contains(other.value)
|
||||
else if (other is ObjChar)
|
||||
value.contains(other.value)
|
||||
else scope.raiseIllegalArgument("String.contains can't take $other")
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as ObjString
|
||||
|
||||
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 = 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))
|
||||
}
|
||||
addFn("endsWith") {
|
||||
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
|
||||
}
|
||||
addConst("length",
|
||||
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
)
|
||||
addFn("takeLast") {
|
||||
thisAs<ObjString>().value.takeLast(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("take") {
|
||||
thisAs<ObjString>().value.take(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("drop") {
|
||||
thisAs<ObjString>().value.drop(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("dropLast") {
|
||||
thisAs<ObjString>().value.dropLast(
|
||||
requiredArg<ObjInt>(0).toInt()
|
||||
).let(::ObjString)
|
||||
}
|
||||
addFn("lower") {
|
||||
thisAs<ObjString>().value.lowercase().let(::ObjString)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -1,31 +1,38 @@
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
sealed class ObjType {
|
||||
object Any : ObjType()
|
||||
object Int : ObjType()
|
||||
object Void: ObjType()
|
||||
|
||||
companion object {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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 +41,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 +54,20 @@ 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)
|
||||
}
|
||||
|
||||
object NopStatement: Statement(true, true, ObjType.Void) {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj = ObjVoid
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
interface BitInput {
|
||||
|
||||
|
||||
fun getBitOrNull(): Int?
|
||||
|
||||
fun getBitsOrNull(count: Int): ULong? {
|
||||
var result = 0UL
|
||||
var resultMask = 1UL
|
||||
for( i in 0 ..< count) {
|
||||
when(getBitOrNull()) {
|
||||
null -> return null
|
||||
1 -> result = result or resultMask
|
||||
0 -> {}
|
||||
}
|
||||
resultMask = resultMask shl 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getBits(count: Int): ULong {
|
||||
return getBitsOrNull(count) ?: throw IllegalStateException("Unexpected end of stream")
|
||||
}
|
||||
|
||||
fun getBit(): Int {
|
||||
return getBitOrNull() ?: throw IllegalStateException("Unexpected end of stream")
|
||||
}
|
||||
|
||||
fun unpackUnsigned(): ULong =
|
||||
unpackUnsignedOrNull() ?: throw IllegalStateException("Unexpected end of stream")
|
||||
|
||||
fun unpackUnsignedOrNull(): ULong? {
|
||||
val tetrades = getBitsOrNull(4)?.toInt() ?: return null
|
||||
var result = 0UL
|
||||
var shift = 0
|
||||
for (i in 0.. tetrades) {
|
||||
result = result or (getBits(4) shl shift)
|
||||
shift += 4
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun unpackSigned(): Long {
|
||||
val isNegative = getBit()
|
||||
val value = unpackUnsigned().toLong()
|
||||
return if( isNegative == 1) -value else value
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getBool(): Boolean {
|
||||
return getBit() == 1
|
||||
}
|
||||
|
||||
fun getBytes(count: Int): ByteArray? {
|
||||
val result = ByteArray(count)
|
||||
for (i in 0..<count) {
|
||||
val b = getBitsOrNull(8) ?: return null
|
||||
result[i] = b.toByte()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
fun decompress(): ByteArray = decompressOrNull() ?: throw DecompressionException("Unexpected end of stream")
|
||||
|
||||
fun decompressOrNull(): ByteArray? {
|
||||
val originalSize = unpackUnsignedOrNull()?.toInt() ?: return null
|
||||
return if( getBit() == 1) {
|
||||
// data is compressed
|
||||
// val expectedCRC = getBits(32).toUInt()
|
||||
val method = getBits(2).toInt()
|
||||
if( method != 0) throw DecompressionException("Unknown compression method")
|
||||
LZW.decompress(this, originalSize).asByteArray()
|
||||
}
|
||||
else {
|
||||
getBytes(originalSize) ?: throw DecompressionException("Unexpected end of stream in uncompressed data")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun decompressStringOrNull(): String? = decompressOrNull()?.decodeToString()
|
||||
|
||||
fun decompressString(): String = decompress().decodeToString()
|
||||
fun unpackDouble(): Double {
|
||||
val bits = getBits(64)
|
||||
return Double.fromBits(bits.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
@Suppress("unused")
|
||||
interface BitList {
|
||||
operator fun get(bitIndex: Long): Int
|
||||
operator fun set(bitIndex: Long,value: Int)
|
||||
val size: Long
|
||||
val indices: LongRange
|
||||
|
||||
fun toInput(): BitInput = object : BitInput {
|
||||
private var index = 0L
|
||||
|
||||
override fun getBitOrNull(): Int? =
|
||||
if( index < size) this@BitList[index++]
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
fun bitListOf(vararg bits: Int): BitList {
|
||||
return if( bits.size > 64) {
|
||||
BitArray.ofBits(*bits)
|
||||
}
|
||||
else
|
||||
TinyBits.of(*bits)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun bitListOfSize(sizeInBits: Long): BitList {
|
||||
return if( sizeInBits > 64) {
|
||||
BitArray.withBitSize(sizeInBits)
|
||||
}
|
||||
else
|
||||
TinyBits()
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
interface BitOutput {
|
||||
|
||||
fun putBits(bits: ULong, count: Int) {
|
||||
require(count <= 64)
|
||||
var x = bits
|
||||
for (i in 0..<count) {
|
||||
putBit((x and 1u).toInt())
|
||||
x = x shr 1
|
||||
}
|
||||
}
|
||||
|
||||
fun putBits(bits: Int, count: Int) {
|
||||
require(count <= 32)
|
||||
var x = bits
|
||||
for (i in 0..<count) {
|
||||
putBit((x and 1))
|
||||
x = x shr 1
|
||||
}
|
||||
}
|
||||
|
||||
fun putBit(bit: Int)
|
||||
|
||||
fun putBits(bitList: BitList) {
|
||||
for (i in bitList.indices)
|
||||
putBit(bitList[i])
|
||||
}
|
||||
|
||||
fun packUnsigned(value: ULong) {
|
||||
val tetrades = sizeInTetrades(value)
|
||||
putBits(tetrades - 1, 4)
|
||||
var rest = value
|
||||
for (i in 0..<tetrades) {
|
||||
putBits(rest and 0xFu, 4)
|
||||
rest = rest shr 4
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun packSigned(value: Long) {
|
||||
if (value < 0) {
|
||||
putBit(1)
|
||||
packUnsigned((-value).toULong())
|
||||
} else {
|
||||
putBit(0)
|
||||
packUnsigned(value.toULong())
|
||||
}
|
||||
}
|
||||
|
||||
fun putBytes(data: ByteArray) {
|
||||
for (b in data) {
|
||||
putBits(b.toULong(), 8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create compressed record with content and size check. Compression works with _bytes_.
|
||||
*
|
||||
* Structure:
|
||||
*
|
||||
* | size | meaning |
|
||||
* |------|--------------------------------------------------|
|
||||
* | packed unsigned | size of uncompressed content in bytes |
|
||||
* | 1 | 0 - not compressed, 1 - compressed |
|
||||
*
|
||||
* __If compressed__, then:
|
||||
*
|
||||
* | size | meaning |
|
||||
* |------|--------------------------------------|
|
||||
* | 2 | 00 - LZW, other combinations reserved|
|
||||
*
|
||||
* After this header compressed bits follow.
|
||||
*
|
||||
* __If not compressed,__ then source data follows as bit stream.
|
||||
*
|
||||
* Compressed block overhead is 3 bits, uncompressed 1.
|
||||
*/
|
||||
fun compress(source: ByteArray) {
|
||||
// size
|
||||
packUnsigned(source.size.toULong())
|
||||
// check compression is effective?
|
||||
val compressed = LZW.compress(source.asUByteArray())
|
||||
// check that compression is effective including header bits size:
|
||||
if( compressed.size + 2 < source.size * 8L) {
|
||||
println("write compressed")
|
||||
putBit(1)
|
||||
// LZW algorithm
|
||||
putBits(0, 2)
|
||||
// compressed data
|
||||
putBits(compressed)
|
||||
}
|
||||
else {
|
||||
putBit(0)
|
||||
putBytes(source)
|
||||
}
|
||||
}
|
||||
|
||||
fun compress(source: String) {
|
||||
compress(source.encodeToByteArray())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
class DecompressionException(message: String) : IllegalArgumentException(message) {}
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
|
||||
|
||||
fun getBitsAsInt(bitsSize: Int): Int {
|
||||
return bin.getBits(bitsSize).toInt()
|
||||
}
|
||||
|
||||
fun unpackUnsignedInt(): Int = bin.unpackUnsigned().toInt()
|
||||
|
||||
fun decompress() = bin.decompress()
|
||||
|
||||
val cache = mutableListOf<Any>()
|
||||
|
||||
|
||||
inline fun <reified T : Any> decodeCached(f: LynonDecoder.() -> T): T {
|
||||
return if (bin.getBit() == 0) {
|
||||
// unpack and cache
|
||||
f().also {
|
||||
// println("decode: cache miss: ${cache.size}: $it:${it::class.simpleName}")
|
||||
if (settings.shouldCache(it)) cache.add(it)
|
||||
}
|
||||
} else {
|
||||
// get cache reference
|
||||
val size = sizeInBits(cache.size)
|
||||
val id = bin.getBitsOrNull(size)?.toInt()
|
||||
?: throw RuntimeException("Invalid object id: unexpected end of stream")
|
||||
if (id >= cache.size) throw RuntimeException("Invalid object id: $id should be in 0..<${cache.size}")
|
||||
// println("decode: cache hit ${id}: ${cache[id]}:${cache[id]::class.simpleName}")
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
cache[id] as T
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decodeAny(scope: Scope): Obj = decodeCached {
|
||||
val type = LynonType.entries[bin.getBits(4).toInt()]
|
||||
if (type != LynonType.Other) {
|
||||
type.objClass.deserialize(scope, this, type)
|
||||
} else {
|
||||
decodeClassObj(scope).deserialize(scope, this, null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decodeClassObj(scope: Scope): ObjClass {
|
||||
val className = decodeObject(scope, ObjString.type, null) as ObjString
|
||||
println("expected class name $className")
|
||||
return scope.get(className.value)?.value?.let {
|
||||
if (it !is ObjClass)
|
||||
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
|
||||
it
|
||||
} ?: scope.raiseSymbolNotFound("can't deserialize: not found type $className")
|
||||
}
|
||||
|
||||
suspend fun decodeAnyList(scope: Scope,fixedSize: Int?=null): MutableList<Obj> {
|
||||
return if (bin.getBit() == 1) {
|
||||
// homogenous
|
||||
val type = LynonType.entries[getBitsAsInt(4)]
|
||||
val list = mutableListOf<Obj>()
|
||||
val objClass = if (type == LynonType.Other)
|
||||
decodeClassObj(scope).also { println("detected class obj: $it") }
|
||||
else type.objClass
|
||||
val size = fixedSize ?: bin.unpackUnsigned().toInt()
|
||||
println("detected homogenous list type $type, $size items")
|
||||
for (i in 0..<size) {
|
||||
list += decodeObject(scope, objClass, type).also {
|
||||
println("decoded: $it")
|
||||
}
|
||||
}
|
||||
list
|
||||
} else {
|
||||
val size = fixedSize ?: unpackUnsigned().toInt()
|
||||
(0..<size).map { decodeAny(scope) }.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decodeObject(scope: Scope, type: ObjClass, overrideType: LynonType? = null): Obj {
|
||||
return decodeCached { type.deserialize(scope, this, overrideType) }
|
||||
}
|
||||
|
||||
fun unpackBinaryData(): ByteArray = bin.decompress()
|
||||
|
||||
@Suppress("unused")
|
||||
fun unpackBinaryDataOrNull(): ByteArray? = bin.decompressOrNull()
|
||||
|
||||
fun unpackBoolean(): Boolean {
|
||||
return bin.getBit() == 1
|
||||
}
|
||||
|
||||
fun unpackDouble(): Double {
|
||||
return Double.fromBits(bin.getBits(64).toLong())
|
||||
}
|
||||
|
||||
fun unpackSigned(): Long {
|
||||
return bin.unpackSigned()
|
||||
}
|
||||
|
||||
fun unpackUnsigned(): ULong {
|
||||
return bin.unpackUnsigned()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
import net.sergeych.bintools.ByteChunk
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.*
|
||||
|
||||
enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
|
||||
Null(ObjNull.objClass, 80),
|
||||
Int0(ObjInt.type, 70),
|
||||
IntNegative(ObjInt.type, 50),
|
||||
IntPositive(ObjInt.type, 100),
|
||||
IntSigned(ObjInt.type, 30),
|
||||
String(ObjString.type, 100),
|
||||
Real(ObjReal.type),
|
||||
Bool(ObjBool.type, 80),
|
||||
List(ObjList.type, 70),
|
||||
Map(ObjMap.type, 40),
|
||||
Set(ObjSet.type),
|
||||
Buffer(ObjBuffer.type, 50),
|
||||
Instant(ObjInstant.type, 30),
|
||||
Duration(ObjDuration.type),
|
||||
Other(Obj.rootObjectType, 60);
|
||||
|
||||
fun generalizeTo(other: LynonType): LynonType? {
|
||||
if (this == other) return this
|
||||
return (if (this.isInt && other.isInt) {
|
||||
when {
|
||||
this == Int0 -> other // upgrade 0 to some other int
|
||||
other == Int0 -> this // 0 is member of our class, ignore
|
||||
// different signum propagate to signed
|
||||
else -> IntSigned
|
||||
}
|
||||
} else
|
||||
// impossible to generalize
|
||||
null
|
||||
).also { println("Gen $this + $other -> $it") }
|
||||
}
|
||||
|
||||
val isInt by lazy {
|
||||
when (this) {
|
||||
Int0, IntSigned, IntPositive, IntNegative -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open class LynonEncoder(val bout: BitOutput, val settings: LynonSettings = LynonSettings.default) {
|
||||
|
||||
val cache = mutableMapOf<Any, Int>()
|
||||
|
||||
suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) {
|
||||
|
||||
suspend fun serializeAndCache(key: Any = item) {
|
||||
cache[key]?.let { cacheId ->
|
||||
// println("encode: Cache hit: ${cacheId}: $item: ${item::class.simpleName}")
|
||||
val size = sizeInBits(cache.size)
|
||||
bout.putBit(1)
|
||||
bout.putBits(cacheId.toULong(), size)
|
||||
} ?: run {
|
||||
bout.putBit(0)
|
||||
if (settings.shouldCache(item)) {
|
||||
// println("encode add cache: ${cache.size}: $item: ${item::class.simpleName}")
|
||||
packer()
|
||||
cache[key] = cache.size
|
||||
} else {
|
||||
// println("encode but not cache $item")
|
||||
packer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (item) {
|
||||
is ByteArray -> serializeAndCache(ByteChunk(item.asUByteArray()))
|
||||
is UByteArray -> serializeAndCache(ByteChunk(item))
|
||||
else -> serializeAndCache(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode any Lyng object [Obj], which can be serialized, using type record. This allow to
|
||||
* encode any object with the overhead of type record.
|
||||
*
|
||||
* Caching is used automatically.
|
||||
*/
|
||||
suspend fun encodeAny(scope: Scope, obj: Obj) {
|
||||
encodeCached(obj) {
|
||||
val type = putTypeRecord(scope, obj, obj.lynonType())
|
||||
obj.serialize(scope, this, type)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun putTypeRecord(scope: Scope, obj: Obj, type: LynonType): LynonType {
|
||||
putType(type)
|
||||
if( type == LynonType.Other) {
|
||||
encodeObject(scope, obj.objClass.classNameObj)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
private fun putType(type: LynonType) {
|
||||
bout.putBits(type.ordinal.toULong(), 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* AnyList could be homogenous (first bit=1) and heterogeneous (first bit=0). Homogenous list
|
||||
* has a single type record that precedes the list, heterogeneous hash typed record
|
||||
* for each item.
|
||||
*
|
||||
*/
|
||||
suspend fun encodeAnyList(scope: Scope, list: List<Obj>,fixedSize: Boolean = false) {
|
||||
if( list.isEmpty()) {
|
||||
// any-typed, empty, save space
|
||||
putBit(0)
|
||||
encodeUnsigned(0u)
|
||||
}
|
||||
else {
|
||||
val objClass = list[0].objClass
|
||||
var type = list[0].lynonType()
|
||||
var isHomogeneous = true
|
||||
for (i in list.drop(1))
|
||||
if (i.objClass != objClass) {
|
||||
isHomogeneous = false
|
||||
break
|
||||
} else {
|
||||
// same class but type might need generalization
|
||||
type = type.generalizeTo(i.lynonType())
|
||||
?: scope.raiseError("inner error: can't generalize lynon type $type to ${i.lynonType()}")
|
||||
}
|
||||
if (isHomogeneous) {
|
||||
putBit(1)
|
||||
putTypeRecord(scope, list[0], type)
|
||||
if (!fixedSize)
|
||||
encodeUnsigned(list.size.toULong())
|
||||
for (i in list) encodeObject(scope, i, type)
|
||||
} else {
|
||||
putBit(0)
|
||||
if (!fixedSize)
|
||||
encodeUnsigned(list.size.toULong())
|
||||
for (i in list) encodeAny(scope, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write object _with no type record_: type is known
|
||||
*/
|
||||
suspend fun encodeObject(scope: Scope, obj: Obj,overrideType: LynonType? = null) {
|
||||
encodeCached(obj) {
|
||||
obj.serialize(scope, this, overrideType)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun encodeCachedBytes(bytes: ByteArray) {
|
||||
encodeCached(bytes) {
|
||||
bout.compress(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeBinaryData(data: ByteArray) {
|
||||
bout.compress(data)
|
||||
}
|
||||
|
||||
fun encodeSigned(value: Long) {
|
||||
bout.packSigned(value)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun encodeUnsigned(value: ULong) {
|
||||
bout.packUnsigned(value)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun encodeBool(value: Boolean) {
|
||||
bout.putBit(if (value) 1 else 0)
|
||||
}
|
||||
|
||||
fun encodeReal(value: Double) {
|
||||
bout.putBits(value.toRawBits().toULong(), 64)
|
||||
}
|
||||
|
||||
fun encodeBoolean(value: Boolean) {
|
||||
bout.putBit(if (value) 1 else 0)
|
||||
}
|
||||
|
||||
fun putBits(value: Int, sizeInBits: Int) {
|
||||
bout.putBits(value.toULong(), sizeInBits)
|
||||
}
|
||||
|
||||
fun putBit(bit: Int) {
|
||||
bout.putBit(bit)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjChar
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
open class LynonSettings {
|
||||
enum class InstantTruncateMode {
|
||||
Second,
|
||||
Millisecond,
|
||||
Microsecond
|
||||
}
|
||||
|
||||
open fun shouldCache(obj: Any): Boolean = when (obj) {
|
||||
is ObjChar -> false
|
||||
is ObjInt -> obj.value.absoluteValue > 0x10000FF
|
||||
is ObjBool -> false
|
||||
is ObjNull -> false
|
||||
is ByteArray -> obj.size > 2
|
||||
is UByteArray -> obj.size > 2
|
||||
else -> true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val default = LynonSettings()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.sergeych.lynon
|
||||
|
||||
|
||||
class MemoryBitInput(val packedBits: UByteArray, val lastByteBits: Int) : BitInput {
|
||||
|
||||
constructor(ba: BitArray) : this(ba.bytes, ba.lastByteBits) {}
|
||||
constructor(mba: MemoryBitOutput) : this(mba.toBitArray()) {}
|
||||
|
||||
private var index = 0
|
||||
|
||||
private var isEndOfStream: Boolean = packedBits.isEmpty() || (packedBits.size == 1 && lastByteBits == 0)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Return next byte, int in 0..255 range, or -1 if end of stream reached
|
||||
*/
|
||||
private var accumulator = if( isEndOfStream ) 0 else packedBits[0].toInt()
|
||||
|
||||
private var bitCounter = 0
|
||||
|
||||
override fun getBitOrNull(): Int? {
|
||||
if (isEndOfStream) return null
|
||||
val result = accumulator and 1
|
||||
accumulator = accumulator shr 1
|
||||
bitCounter++
|
||||
// is end?
|
||||
if( index == packedBits.lastIndex && bitCounter == lastByteBits ) {
|
||||
isEndOfStream = true
|
||||
}
|
||||
else {
|
||||
if( bitCounter == 8 ) {
|
||||
bitCounter = 0
|
||||
accumulator = packedBits[++index].toInt()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user