Compare commits

..
76 Commits
Author SHA1 Message Date
sergeych 4165da5e81 published 0.8.0-SNAPSHOT 2025-08-04 16:52:43 +03:00
sergeych af463163bb published 0.8.0-SNAPSHOT 2025-08-04 16:52:25 +03:00
sergeych 6df06a6911 fix #35 serialization alsso user classes. Fixed user classes comparison 2025-08-04 16:49:55 +03:00
sergeych f805e1ee82 sets serialization 2025-08-03 15:11:15 +03:00
sergeych 790cce0d24 maps serialization 2025-08-03 15:06:46 +03:00
sergeych 2339130241 trivial implementation of class name = based serialization (LynonType.Other) 2025-08-03 14:42:23 +03:00
sergeych d7bd159fcb list serialization (optimized, homogenous and heterogeneous 2025-08-03 13:30:41 +03:00
sergeych 12b209c724 refs #35 generic implementation of Huffman compression for variable bit length source alphabet 2025-07-23 20:51:31 +03:00
sergeych 20181c63a1 refs #35 refac to get caching out of Obj; it is decoder/encoder matter now 2025-07-19 14:19:50 +03:00
sergeych 405ff2ec2b refs #35 hack to avoid cache clush for encodeAny and encode, double caching same object conflict. to refactor! 2025-07-19 14:12:41 +03:00
sergeych a9f65bdbe3 refs #35 serializatino framework refactored: implementation put in open methods of Obj/ObjClass for flexibility 2025-07-19 12:28:14 +03:00
sergeych 6ab438b1f6 refs #35 serialize any: null, int, real, boolean, Instant. Added unary minus as general operator, not only to numbers. Instant truncation (nice for serialization) 2025-07-17 23:23:03 +03:00
sergeych cffe4eaffc refs #35 bits in BitArray/input/output reordered for better performance; started typed serialization 2025-07-17 12:33:32 +03:00
sergeych 7aee25ffef refs #35 Buffer is not mutable, MutableBuffer added (to cache in serialized form) 2025-07-16 11:47:23 +03:00
sergeych f3d766d1b1 refs #35 Lynon builtin compression 2025-07-16 11:10:38 +03:00
sergeych 34bc7297bd refs #35 bit granularity for bitstreams; LZW done 2025-07-13 00:37:27 +03:00
sergeych 23dafff453 refs #35 started user class serialization; started MP compression 2025-07-12 22:15:00 +03:00
sergeych 77f9191387 refs #35 caching moved to level above objec serializing so we can cache strings, etc that are not Obj instances 2025-07-12 11:48:14 +03:00
sergeych f26ee7cd7c refs #35 minimal serialization of simple types in bit-effective format 2025-07-12 00:46:51 +03:00
sergeych d969993997 ref #35 bitwise pack/unpack for integers 2025-07-11 06:09:09 +03:00
sergeych 987b80e44d lynon started 2025-07-11 05:56:43 +03:00
sergeych 5848adca61 fix #39 correct implementation of ++ and -- with indexing access 2025-07-10 15:54:53 +03:00
sergeych f1ae4b2d23 fixed CLI for new compiler interface 2025-07-10 12:47:00 +03:00
sergeych 30b6ef235b each new scope with no parent now starts with a copy of import manager to isolate its imports 2025-07-10 12:42:39 +03:00
sergeych 9771b40c98 fixed double imports bug
added clean import scope Scope.new()
2025-07-10 12:41:10 +03:00
sergeych 230cb0a067 ref #34 time formatting and precision time access 2025-07-10 11:10:26 +03:00
sergeych 732d8f3877 fix #34 minimal time manipulation 2025-07-10 00:00:02 +03:00
sergeych 23006b5caa fix #33 minimal Buffer with docs, in a separate package 2025-07-09 17:56:08 +03:00
sergeych 26282d3e22 fix #38 ImportManager integrated into Scope tree and all systems 2025-07-09 13:15:28 +03:00
sergeych ce4ed5c819 migrated to kotlin 2.2.0
better support for shebangs in CLI lyng
added jlyng local release
2025-07-07 23:44:21 +03:00
sergeych 612c0fb7b9 v.0.7.1 improved imports 2025-07-07 15:02:10 +03:00
sergeych ef6bc5c468 refs #36 imports refined and optimized, circular imports are supported. 2025-07-07 15:01:27 +03:00
sergeych 1e2cb5e420 fixed import inside sources in InlineSourcesPacman 2025-07-04 02:23:45 +03:00
sergeych 868379ce22 v0.7.0: imports and stuff 2025-07-04 01:00:20 +03:00
sergeych f37354f382 fix #36 basic import support 2025-07-04 00:56:56 +03:00
sergeych ddbcbf9e4e renaming context to scope, as it is rather scp[e than the context 2025-07-03 21:46:25 +03:00
sergeych 6cf99fbd13 Compiler refactored: CompilerContext now is class member 2025-07-03 20:00:01 +03:00
sergeych 950e8301c3 associateBy and Lyng:Iterable .toFlow() for kotlin integration simplicity 2025-07-02 20:45:30 +03:00
sergeych ea2cf1f373 code cleaning 2025-07-01 12:31:40 +03:00
sergeych eee6d75587 extension methids 2025-06-20 03:43:58 +04:00
sergeych a8067d0a6b fix #31 type records for simple types and extern keyword for methods/functions 2025-06-19 01:06:45 +04:00
sergeych 75a6f20150 fix #31 type records for simple types and extern keyword for methods/functions 2025-06-17 18:42:37 +04:00
sergeych d969d6d572 fix #37 AppliedContext optimization 2025-06-17 09:49:05 +04:00
sergeych 2d4c4d345d fix #30: let, apply, also. Fix in context combining for lambda calls. 2025-06-16 15:44:22 +04:00
sergeych f9416105ec fix #29 mapentries, map iterators, => operator and .toMap() 2025-06-16 02:14:53 +04:00
sergeych c002204420 fix #28 basic map supports (still no iterators) 2025-06-16 01:12:04 +04:00
sergeych a4448ab2ff fix #10 set
+collection functions (map, forEach, toList, toSet, isEmpty, etc,)
2025-06-15 18:01:44 +04:00
sergeych 8a4363bd84 fix #26 lists redesigned for ranges, less chaotic and serpentic 2025-06-14 14:51:41 +04:00
sergeych 19eae213ec fix #23 string formatting and manipulations 2025-06-14 11:53:18 +04:00
sergeych 1db1f12be3 refs #25 backbone of multiplatform shell 2025-06-14 10:18:38 +04:00
sergeych dcde11d722 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	lyng/src/nativeMain/kotlin/Common.native.kt
2025-06-14 01:21:44 +04:00
sergeych 83e79f47c7 lyng CLI: support for shebang, started shell KMP code 2025-06-14 01:20:26 +04:00
sergeych e0bb183929 lyng CLI: support for shebang, started shell KMP code 2025-06-14 01:20:00 +04:00
sergeych b961296425 fix #19 set of null-coalesce operators 2025-06-13 22:25:18 +04:00
sergeych bd2b6bf06e readme actualized 2025-06-13 21:20:45 +04:00
sergeych 253480e32a readme actualized 2025-06-13 20:15:39 +04:00
sergeych eb8110cbf0 removed unnecessary 2025-06-13 19:12:51 +04:00
sergeych 8c6a1979ed published to our maven 2025-06-13 19:11:28 +04:00
sergeych 185aa4e0cf better docs/2 2025-06-13 18:40:14 +04:00
sergeych ef266b73a2 better docs/2 2025-06-13 18:03:51 +04:00
sergeych 89427de5cd better docs 2025-06-13 18:02:34 +04:00
sergeych cfb2f7f128 more docs, fixed parsing of an empty string 2025-06-13 17:59:40 +04:00
sergeych be4f2c7f45 fix #14 when(value) with blows and whistles. Collection now is container (has Contains). greatly improved container properties of builtin classes. 2025-06-13 17:27:23 +04:00
sergeych 7cc80e2433 fix #20 do-while tests for labels, values, else 2025-06-13 11:05:30 +04:00
sergeych dacdcd7faa ref #13 get rid pf tokens for private/protected, added open attribute parsing 2025-06-13 10:31:32 +04:00
sergeych 59a76efdce fix #22 test throw from kotlin code 2025-06-13 07:51:42 +04:00
sergeych bb862e6cb5 refs #22 test for try-free 2025-06-13 01:52:44 +04:00
sergeych aea819b89a refs #22 docs reordered chapters 2025-06-13 01:45:33 +04:00
sergeych 88974e0f2d refs #22 try without catch but finally 2025-06-13 01:44:13 +04:00
sergeych 0981d8370e v0.5.1-SNAPSHOT: Exceptions Handling! 2025-06-13 01:36:21 +04:00
sergeych c4122e8243 refs #22 more docs 2025-06-13 01:32:40 +04:00
sergeych c3bf536bab refs #22 more docs, fixed EH bug
+shortcut throw "message"
2025-06-13 01:30:26 +04:00
sergeych 5ed8b2f123 refs #22 @35m 2 shorter forms for catch block, fixed catch class instance type checks 2025-06-13 00:12:21 +04:00
sergeych 6c71f0a2e6 fix #18 basic exceptions handling 2025-06-12 19:17:35 +04:00
sergeych 95aae0b231 fix #21 fixed loop break with label behavior 2025-06-12 12:34:03 +04:00
sergeych b3f08b4cac fix #1 for-continue test 2025-06-12 12:23:50 +04:00
116 changed files with 11308 additions and 1885 deletions

No files matched your search

+1
View File
@@ -14,3 +14,4 @@ xcuserdata
.gigaide
/kotlin-js-store/yarn.lock
/test.lyng
/sample_texts/1.txt.gz
+86 -23
View File
@@ -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,29 +35,59 @@ 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()
for (a in args) sum += a.toDouble()
ObjReal(sum)
}
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
@@ -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
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
set -e
root=./lyng/build/install/lyng-jvm/
./gradlew :lyng:installJvmDist
#strip $file
#upx $file
rm -rf ~/bin/jlyng-jvm || true
rm ~/bin/jlyng 2>/dev/null || true
mkdir -p ~/bin/jlyng-jvm
cp -R $root ~/bin/jlyng-jvm
ln -s ~/bin/jlyng-jvm/lyng-jvm/bin/lyng ~/bin/jlyng
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env lyng
println("Hello from lyng!")
+121
View File
@@ -0,0 +1,121 @@
# Binary `Buffer`
Buffers are effective unsigned byte arrays of fixed size. Buffers content is mutable,
unlike its size. Buffers are comparable and implement [Array], thus [Collection] and [Iterable]. Buffer iterators return
its contents as unsigned bytes converted to `Int`
Buffers needs to be imported with `import lyng.buffer`:
import lyng.buffer
assertEquals(5, Buffer("Hello").size)
>>> void
Buffer is _immutable_, there is a `MutableBuffer` with same interface but mutable.
## Constructing
There are a lo of ways to construct a buffer:
import lyng.buffer
// from string using utf8 encoding:
assertEquals( 5, Buffer("hello").size )
// from bytes, e.g. integers in range 0..255
assertEquals( 255, Buffer(1,2,3,255).last() )
// from whatever iterable that produces bytes, e.g.
// integers in 0..255 range:
assertEquals( 129, Buffer([1,2,129]).last() )
// Empty buffer of fixed size:
assertEquals(100, Buffer(100).size)
assertEquals(0, Buffer(100)[0])
// Note that you can use list iteral to create buffer with 1 byte:
assertEquals(1, Buffer([100]).size)
assertEquals(100, Buffer([100])[0])
>>> void
## Accessing and modifying
Buffer implement [Array] and therefore can be accessed, and `MutableBuffers` also modified:
import lyng.buffer
val b1 = Buffer( 1, 2, 3)
assertEquals( 2, b1[1] )
val b2 = b1.toMutable()
assertEquals( 2, b1[1] )
b2[1]++
b2[0] = 100
assertEquals( Buffer(100, 3, 3), b2)
// b2 is a mutable copy so b1 has not been changed:
assertEquals( Buffer(1, 2, 3), b1)
>>> void
Buffer provides concatenation with another Buffer:
import lyng.buffer
val b = Buffer(101, 102)
assertEquals( Buffer(101, 102, 1, 2), b + [1,2])
>>> void
Please note that indexed bytes are _readonly projection_, e.g. you can't modify these with
## Comparing
Buffers are comparable with other buffers (and notice there are _mutable_ buffers, bu default buffers ar _immutable_):
import lyng.buffer
val b1 = Buffer(1, 2, 3)
val b2 = Buffer(1, 2, 3)
val b3 = MutableBuffer(b2)
b3[0] = 101
assert( b3 > b1 )
assert( b2 == b1 )
// longer string with same prefix is considered bigger:
assert( b2 + "!".characters() > b1 )
// note that characters() provide Iterable of characters that
// can be concatenated to Buffer
>>> void
## Slicing
As with [List], it is possible to use ranges as indexes to slice a Buffer:
import lyng.buffer
val a = Buffer( 100, 101, 102, 103, 104, 105 )
assertEquals( a[ 0..1 ], Buffer(100, 101) )
assertEquals( a[ 0 ..< 2 ], Buffer(100, 101) )
assertEquals( a[ ..< 2 ], Buffer(100, 101) )
assertEquals( a[ 4.. ], Buffer(104, 105) )
assertEquals( a[ 2..3 ], Buffer(102, 103) )
>>> void
## Members
| name | meaning | type |
|---------------|------------------------------------|---------------|
| `size` | size | Int |
| `decodeUtf8` | decodee to String using UTF8 rules | Any |
| `+` | buffer concatenation | Any |
| `toMutable()` | create a mutable copy | MutableBuffer |
(1)
: optimized implementation that override `Iterable` one
Also, it inherits methods from [Iterable] and [Array].
[Range]: Range.md
[Iterable]: Iterable.md
+13
View File
@@ -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
View File
@@ -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:
+63 -16
View File
@@ -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
@@ -72,19 +94,44 @@ 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 |
| `removeAt(index)` | remove element at position | Int |
| `removeRangeInclusive(start,end)` | remove range, inclusive (1) | Int, Int |
| | | |
| name | meaning | type |
|-------------------------------|---------------------------------------|-------------|
| `size` | current size | Int |
| `add(elements...)` | add one or more elements to the end | Any |
| `insertAt(index,elements...)` | insert elements at position | Int, Any |
| `removeAt(index)` | remove element at position | 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
View File
@@ -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)
+36 -3
View File
@@ -160,6 +160,40 @@ For example, for our class Point:
Point(1,1+1)
>>> Point(x=1,y=2)
# Extending classes
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
that would test if some object of unknown type contains something that can be interpreted
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
fun Object.isInteger() {
when(this) {
// already Int?
is Int -> true
// real, but with no declimal part?
is Real -> toInt() == this
// string with int or real reuusig code above
is String -> toReal().isInteger()
// otherwise, no:
else -> false
}
}
// Let's test:
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
>>> void
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created,
as they are modifying the type, not the context.
Beware of it. We might need to reconsider it later.
# Theory
## Basic principles:
@@ -216,11 +250,10 @@ Note `Real` class: it is global variable for Real class; there are such class in
assert('$'::class == Char)
>>> void
More complex is singleton classes, because you don't need to compare their class
instances and generally don't need them at all, these are normally just Obj:
Singleton classes also have class:
null::class
>>> Obj
>>> Null
At this time, `Obj` can't be accessed as a class.
+8 -8
View File
@@ -15,11 +15,11 @@ you can use it's class to ensure type:
## Member functions
| name | meaning | type |
|-----------------|------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int |
| | | |
| | | |
| | | |
| | | |
| | | |
| 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
View File
@@ -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
View File
Whitespace-only changes.
+24
View File
@@ -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
,
+145
View File
@@ -0,0 +1,145 @@
# Exceptions handling
Exceptions are widely used in modern programming languages, so
they are implemented also in Lyng and in the most complete way.
# Exception classes
Exceptions are throwing instances of some class that inherits `Exception`
across the code. Below is the list of built-in exceptions. Note that
only objects that inherit `Exception` can be thrown. For example:
assert( IllegalArgumentException() is Exception)
>>> void
# Try statement: catching exceptions
There general pattern is:
```
try_statement = try_clause, [catch_clause, ...], [finally_clause]
try_clause = "try", "{", statements, "}"
catch_clause = "catch", [(full_catch | shorter_catch)], "{", statements "}"
full_catch = "(", catch_var, ":", exception_class [, excetpion_class...], ")
shorter_catch = "(", catch_var, ")"
finally_clause = "{", statements, "}"
```
Let's in details.
## Full catch block:
val result = try {
throw IllegalArgumentException("the test")
}
catch( x: IndexOutOfBoundsException, IllegalArgumentException) {
x.message
}
catch(x: Exception) {
"bad"
}
assertEquals(result, "the test")
>>> void
Because our exception is listed in a first catch block, it is processed there.
The full form allow a single catch block to process exceptions with specified classes and bind actual caught object to
the given variable. This is most common and well known form, implemented like this or similar in many other languages,
like Kotlin, Java or C++.
## Shorter form
When you want to catch _all_ the exceptions, you should write `catch(e: Exception)`,
but it is somewhat redundant, so there is simpler variant:
val sample2 = try {
throw IllegalArgumentException("sample 2")
}
catch(x) {
x.message
}
assertEquals( sample2, "sample 2" )
>>> void
But well most likely you will find default variable `it`, like in Kotlin, more than enough
to catch all exceptions to, then you can write it even shorter:
val sample2 = try {
throw IllegalArgumentException("sample 3")
}
catch {
it.message
}
assertEquals( sample2, "sample 3" )
>>> void
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:
try {
throw Exception("this is my exception")
}
catch {
it.message
}
>>> "this is my exception"
This way, in turn, can also be shortened, as it is overly popular:
try {
throw "this is my exception"
}
catch {
it.message
}
>>> "this is my exception"
The trick, though, works with strings only, and always provide `Exception` instances, which is good for debugging but
most often not enough.
# Custom error classes
_this functionality is not yet released_
# Standard exception classes
| class | notes |
|----------------------------|-------------------------------------------------------|
| Exception | root of al throwable objects |
| NullReferenceException | |
| AssertionFailedException | |
| ClassCastException | |
| IndexOutOfBoundsException | |
| IllegalArgumentException | |
| IllegalAssignmentException | assigning to val, etc. |
| SymbolNotDefinedException | |
| IterationEndException | attempt to read iterator past end, `hasNext == false` |
| AccessException | attempt to access private members or like |
| UnknownException | unexpected kotlin exception caught |
| | |
Regular → Executable
+1
View File
@@ -1,2 +1,3 @@
#!/bin/env jlyng
println("Hello, world!");
+215
View File
@@ -0,0 +1,215 @@
# Lyng time functions
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
- `Instant` class for time stamps with platform-dependent resolution
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
hours, days)
## Time instant: `Instant`
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time
can be for example introduced or dropped). It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin. Some moment of
time; not the calendar date.
Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on
the calendar, e.g. absolute time period.
It is possible to add or subtract `Duration` to and from `Instant`, that gives another `Instant`.
Instants are converted to and from `Real` number of seconds before or after Unix Epoch, 01.01.1970. Constructor with
single number parameter constructs from such number of seconds,
and any instance provide `.epochSeconds` member:
import lyng.time
// default constructor returns time now:
val t1 = Instant()
val t2 = Instant()
assert( t2 - t1 < 1.millisecond )
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
>>> void
## Constructing
import lyng.time
// empty constructor gives current time instant using system clock:
val now = Instant()
// constructor with Instant instance makes a copy:
assertEquals( now, Instant(now) )
// constructing from a number is trated as seconds since unix epoch:
val copyOfNow = Instant( now.epochSeconds )
// note that instant resolution is higher that Real can hold
// so reconstructed from real slightly differs:
assert( abs( (copyOfNow - now).milliseconds ) < 0.01 )
>>> void
The resolution of system clock could be more precise and double precision real number of `Real`, keep it in mind.
## Comparing and calculating periods
import lyng.time
val now = Instant()
// you cam add or subtract periods, and compare
assert( now - 5.minutes < now )
val oneHourAgo = now - 1.hour
assertEquals( now, oneHourAgo + 1.hour)
>>> void
## Getting the max precision
Normally, subtracting instants gives precision to microseconds, which is well inside the jitter
the language VM adds. Still `Instant()` or `Instant.now()` capture most precise system timer at hand and provide inner
value of 12 bytes, up to nanoseconds (hopefully). To access it use:
import lyng.time
// capture time
val now = Instant.now()
// this is Int value, number of whole epoch
// milliseconds to the moment, it fits 8 bytes Int well
val seconds = now.epochWholeSeconds
assert(seconds is Int)
// and this is Int value of nanoseconds _since_ the epochMillis,
// it effectively add 4 more mytes int:
val nanos = now.nanosecondsOfSecond
assert(nanos is Int)
assert( nanos in 0..999_999_999 )
// we can construct epochSeconds from these parts:
assertEquals( now.epochSeconds, nanos * 1e-9 + seconds )
>>> void
## Truncating to more realistic precision
Full precision Instant is way too long and impractical to store, especially when serializing,
so it is possible to truncate it to milliseconds, microseconds or seconds:
import lyng.time
import lyng.serialization
// max supported size (now microseconds for serialized value):
// note that encoding return _bit array_ and this is a _bit size_:
val s0 = Lynon.encode(Instant.now()).size
// shorter: milliseconds only
val s1 = Lynon.encode(Instant.now().truncateToMillisecond()).size
// truncated to seconds, good for file mtime, etc:
val s2 = Lynon.encode(Instant.now().truncateToSecond()).size
assert( s1 < s0 )
assert( s2 < s1 )
>>> void
## Formatting instants
You can freely use `Instant` in string formatting. It supports usual sprintf-style formats:
import lyng.time
val now = Instant()
// will be something like "now: 12:10:05"
val currentTimeOnly24 = "now: %tT"(now)
// we can extract epoch second with formatting too,
// this was since early C time
// get epoch while seconds from formatting
val unixEpoch = "Now is %ts since unix epoch"(now)
// and it is the same as now.epochSeconds, int part:
assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) )
>>> void
See
the [complete list of available formats](https://github.com/sergeych/mp_stools?tab=readme-ov-file#datetime-formatting)
and the [formatting reference](https://github.com/sergeych/mp_stools?tab=readme-ov-file#printf--sprintf): it all works
in Lyng as `"format"(args...)`!
## Instant members
| member | description |
|--------------------------------|---------------------------------------------------------|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) |
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
| isDistantPast: Bool | true if it `Instant.distantPast` |
| truncateToSecond: Intant | create new instnce truncated to second |
| truncateToMillisecond: Instant | truncate new instance with to millisecond |
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
(1)
: The value of nanoseconds is to be added to `epochWholeSeconds` to get exact time point. It is in 0..999_999_999 range.
The precise time instant value therefore needs as for now 12 bytes integer; we might use bigint later (it is planned to
be added)
## Class members
| member | description |
|--------------------------------|----------------------------------------------|
| Instant.now() | create new instance with current system time |
| Instant.distantPast: Instant | most distant instant in past |
| Instant.distantFuture: Instant | most distant instant in future |
# `Duraion` class
Represent absolute time distance between two `Instant`.
import lyng.time
val t1 = Instant()
// yes we can delay to period, and it is not blocking. is suspends!
delay(1.millisecond)
val t2 = Instant()
// be suspend, so actual time may vary:
assert( t2 - t1 >= 1.millisecond)
assert( t2 - t1 < 100.millisecond)
>>> void
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
- `n.millisecond`, `n.milliseconds`
- `n.second`, `n.seconds`
- `n.minute`, `n.minutes`
- `n.hour`, `n.hours`
- `n.day`, `n.days`
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration`
instance:
- `d.microseconds`
- `d.milliseconds`
- `d.seconds`
- `d.minutes`
- `d.hours`
- `d.days`
for example
import lyng.time
assertEquals( 60, 1.minute.seconds )
assertEquals( 10.milliseconds, 0.01.seconds )
>>> void
# Utility functions
## delay(duration: Duration)
Suspends current coroutine for at least the specified duration.
+383 -33
View File
@@ -12,9 +12,9 @@ In other word, the code usually works as expected when you see it. So, nothing u
__Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.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:
@@ -753,6 +980,62 @@ We can use labels too:
assert( search(["hello", "world"], 'z') == null)
>>> void
# Exception handling
Very much like in Kotlin. Try block returns its body block result, if no exception was cauht, or the result from the catch block that caught the exception:
var error = "not caught"
var finallyCaught = false
val result = try {
throw IllegalArgumentException()
"invalid"
}
catch(nd: SymbolNotDefinedException) {
error = "bad catch"
}
catch(x: IllegalArgumentException) {
error = "no error"
"OK"
}
finally {
// finally does not affect returned value
"too bad"
}
assertEquals( "no error", error)
assertEquals( "OK", result)
>>> void
There is shorter form of catch block when you want to catch any exception:
var caught = null
try {
throw IllegalArgumentException()
}
catch(t) { // same as catch(t: Exception), but simpler
caught = t
}
assert( caught is IllegalArgumentException )
>>> void
And even shortest, for the Lying lang tradition, missing var is `it`:
var caught = null
try {
throw IllegalArgumentException()
}
catch { // same as catch(it: Exception), but simpler
caught = it
}
assert( caught is IllegalArgumentException )
>>> void
It is possible to catch several exceptions in the same block too, use
`catch( varName: ExceptionClass1, ExceptionClass2)`, etc, use short form of throw and
many more.
- see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference.
# Self-assignments in expression
There are auto-increments and auto-decrements:
@@ -817,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
@@ -867,32 +1161,85 @@ 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.
Typical set of String functions includes:
| fun/prop | description / notes |
|------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| fun/prop | description / notes |
|-------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| startsWith(prefix) | true if starts 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] |
| 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.
@@ -927,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
+2
View File
@@ -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" }
-11
View File
@@ -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(ObjNullPointerError(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsError(this, message))
@Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentError(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message))
}
fun raiseError(obj: ObjError): 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,347 +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.synctools.ProtectedOp
/**
* 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()))
inline suspend 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() }
suspend open fun readField(context: Context, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val value = obj.value
return when (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 ObjError(val context: Context, val message: String) : Obj() {
override val asStr: ObjString by lazy { ObjString("Error: $message") }
fun raise(): Nothing {
throw ExecutionError(this)
}
}
class ObjNullPointerError(context: Context) : ObjError(context, "object is null")
class ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(context: Context, message: String) : ObjError(context, message)
class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message)
class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message)
class ObjIllegalAssignmentError(context: Context, message: String = "illegal assignment") : ObjError(context, message)
class ObjSymbolNotDefinedError(context: Context, message: String = "symbol is not defined") : ObjError(context, message)
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished")
class ObjAccessError(context: Context, message: String = "access not allowed error") : ObjError(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") }
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(ObjIterationFinishedError(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(ObjAccessError(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)
ObjIllegalAssignmentError(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentError(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(ObjAccessError(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 @@
@file:Suppress("CanBeParameter")
package net.sergeych.lyng
open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=null) : Exception(
"""
$pos: Error: $errorMessage
${pos.currentLine}
${"-".repeat(pos.column)}^
""".trimIndent(),
cause
)
class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message)
@@ -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
*/
+9 -3
View File
@@ -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 {
}
+61 -9
View File
@@ -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))
}
}
+23
View File
@@ -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)
}
+2 -4
View File
@@ -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.4.0-SNAPSHOT"
version = "0.8.0-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,6 +62,7 @@ kotlin {
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}
@@ -98,75 +99,57 @@ android {
}
dependencies {
implementation(libs.firebase.crashlytics.buildtools)
implementation(libs.compiler)
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
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"
publishing {
val mavenToken by lazy {
File("${System.getProperty("user.home")}/.gitea_token").readText()
}
repositories {
maven {
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = mavenToken
}
}
developers {
developer {
id = "XXX"
name = "YYY"
url = "ZZZ"
url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
authentication {
create("Authorization", HttpHeaderAuthentication::class)
}
}
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)
//}
@@ -0,0 +1,19 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
/**
* Special version of the [Scope] used to `apply` new this object to
* _parent context property.
*
* @param _parent context to apply to
* @param args arguments for the new context
* @param appliedScope the new context to apply, it will have lower priority except for `this` which
* will be reset by appliedContext's `this`.
*/
class AppliedScope(_parent: Scope, args: Arguments, val appliedScope: Scope)
: Scope(_parent, args, appliedScope.pos, appliedScope.thisObj) {
override fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else super.get(name) ?: appliedScope[name]
}
@@ -1,5 +1,9 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjRecord
/**
* List of argument declarations in the __definition__ of the lambda, class constructor,
* function, etc. It is created by [Compiler.parseArgsDeclaration]
@@ -19,17 +23,20 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
/**
* parse args and create local vars in a given context
* parse args and create local vars in a given context properly interpreting
* ellipsis args and default values
*/
suspend fun assignToContext(
context: Context,
arguments: Arguments = context.args,
scope: Scope,
arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
defaultVisibility: Visibility = Visibility.Public,
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) {
fun assign(a: Item, value: Obj) {
context.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility)
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility,
recordType = defaultRecordType)
}
// will be used with last lambda arg fix
@@ -52,11 +59,11 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
if (a.isEllipsis) break
val value = when {
i < callArgs.size -> callArgs[i]
a.defaultValue != null -> a.defaultValue.execute(context)
a.defaultValue != null -> a.defaultValue.execute(scope)
else -> {
println("callArgs: ${callArgs.joinToString()}")
println("tailBlockMode: ${arguments.tailBlockMode}")
context.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,
/**
@@ -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)
@@ -26,13 +30,25 @@ suspend fun Collection<ParsedArgument>.toArguments(context: Context,tailBlockMod
return Arguments(list,tailBlockMode)
}
data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : List<Obj> by list {
data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) : List<Obj> by list {
constructor(vararg values: Obj) : this(values.toList())
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())
@@ -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,21 @@ 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 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 +97,7 @@ internal class CompilerContext(val tokens: List<Token>) {
}
fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
val t = next()
return if (t.type == typeId) {
f(t)
@@ -102,6 +116,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 +132,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
/**
* Scan backwards as deep as specified looking for visibility token. Does not change position.
*/
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 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 }
}
}
return default
val t = next()
if( t.type == Token.Type.ID && t.value == keyword ) {
return true
} else {
restorePos(pos)
return false
}
}
// 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()
// }
/**
* Skip newlines and comments. Returns (and reads) first non-whitespace token.
* Note that [Token.Type.EOF] is not considered a whitespace token.
*/
fun skipWsTokens(): Token {
while( current().type in wstokens ) {
next()
}
return next()
}
companion object {
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT)
}
}
@@ -1,5 +1,7 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Accessor
sealed class ListEntry {
data class Element(val accessor: Accessor) : ListEntry()
@@ -1,5 +1,8 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjVoid
class LoopBreakContinueException(
val doContinue: Boolean,
val result: Obj = ObjVoid,
@@ -0,0 +1,61 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.pacman.ImportProvider
/**
* Module scope supports importing and contains the [importProvider]; it should be the same
* used in [Compiler];
*/
class ModuleScope(
var importProvider: ImportProvider,
pos: Pos = Pos.builtIn,
override val packageName: String
) : Scope(importProvider.rootScope, Arguments.EMPTY, pos) {
constructor(importProvider: ImportProvider, source: Source) : this(importProvider, source.startPos, source.fileName)
/**
* Import symbols into the scope. It _is called_ after the module is imported by [ImportProvider.prepareImport]
* which checks symbol availability and accessibility prior to execution.
* @param scope where to copy symbols from this module
* @param symbols symbols to import, ir present, only symbols keys will be imported renamed to corresponding values
*/
override suspend fun importInto(scope: Scope, symbols: Map<String, String>?) {
val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in this.objects) {
if (record.visibility.isPublic) {
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(it) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
} ?: symbol
val existing = scope.objects[newName]
if (existing != null ) {
if (existing.importedFrom != record.importedFrom)
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
// already imported
}
else {
// when importing records, we keep track of its package (not otherwise needed)
if (record.importedFrom == null) record.importedFrom = this
scope.objects[newName] = record
}
}
}
if (!symbolsToImport.isNullOrEmpty())
scope.raiseSymbolNotFound("symbols $packageName.{$symbolsToImport} are.were not found")
}
val packageNameObj by lazy { ObjString(packageName).asReadonly }
override fun get(name: String): ObjRecord? {
return if (name == "__PACKAGE__")
packageNameObj
else
super.get(name)
}
}
@@ -45,12 +45,18 @@ private class Parser(fromPos: Pos) {
'=' -> {
if (pos.currentChar == '=') {
pos.advance()
if (currentChar == '=') {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
} else
Token("==", from, Token.Type.EQ)
} else
when (currentChar) {
'=' -> {
pos.advance()
Token("===", from, Token.Type.REF_EQ)
}
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,14 +143,20 @@ private class Parser(fromPos: Pos) {
if (currentChar == '.') {
pos.advance()
// .. already parsed:
if (currentChar == '.') {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
} else if (currentChar == '<') {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
} else {
Token("..", from, Token.Type.DOTDOT)
when (currentChar) {
'.' -> {
pos.advance()
Token("...", from, Token.Type.ELLIPSIS)
}
'<' -> {
pos.advance()
Token("..<", from, Token.Type.DOTDOTLT)
}
else -> {
Token("..", from, Token.Type.DOTDOT)
}
}
} else
Token(".", from, Token.Type.DOT)
@@ -153,11 +165,10 @@ private class Parser(fromPos: Pos) {
'<' -> {
if (currentChar == '=') {
pos.advance()
if( currentChar == '>' ) {
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
@@ -293,7 +318,7 @@ private class Parser(fromPos: Pos) {
private fun decodeNumber(p1: String, start: Pos): Token =
if (pos.end)
Token(p1, start, Token.Type.INT)
else if( currentChar == 'e' || currentChar == 'E' ) {
else if (currentChar == 'e' || currentChar == 'E') {
pos.advance()
var negative = false
if (currentChar == '+')
@@ -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()
}
}
@@ -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,198 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
/**
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
*
* To create default scope, use default `Scope()` constructor, it will create a scope with a parent
* module scope with default [ImportManager], you can access with [currentImportProvider] as needed.
*
* If you want to create [ModuleScope] by hand, try [currentImportProvider] and [ImportManager.newModule],
* or [ImportManager.newModuleAt].
*
* There are special types of scopes:
*
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
*/
open class Scope(
val parent: Scope?,
val args: Arguments = Arguments.EMPTY,
var pos: Pos = Pos.builtIn,
var thisObj: Obj = ObjVoid,
var skipScopeCreation: Boolean = false,
) {
open val packageName: String = "<anonymous package>"
constructor(
args: Arguments = Arguments.EMPTY,
pos: Pos = Pos.builtIn,
)
: this(Script.defaultImportManager.copy().newModuleAt(pos), args, pos)
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseIllegalArgument(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@Suppress("unused")
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalStateException(this, message))
@Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}
inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index] as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
inline fun <reified T : Obj> requireOnlyArg(): T {
if (args.list.size != 1) raiseError("Expected exactly 1 argument, got ${args.list.size}")
return requiredArg(0)
}
@Suppress("unused")
fun requireExactCount(count: Int) {
if (args.list.size != count) {
raiseError("Expected exactly $count arguments, got ${args.list.size}")
}
}
fun requireNoArgs() {
if( args.list.isNotEmpty())
raiseError("This function does not accept any arguments")
}
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
internal val objects = mutableMapOf<String, ObjRecord>()
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
objects[name]
?: parent?.get(name)
?: thisObj.objClass.getInstanceMemberOrNull(name)
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
Scope(this, args, pos, newThisObj ?: thisObj)
fun copy() = Scope(this, args, pos, thisObj)
fun addItem(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord {
return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {
val ns = objects.getOrPut(name) { ObjRecord(ObjNamespace(name), isMutable = false) }.value
return ns.objClass
}
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
addFn<ObjVoid>(*names) {
fn(this)
ObjVoid
}
}
inline fun <reified T : Obj> addFn(vararg names: String, crossinline fn: suspend Scope.() -> T) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = scope.fn()
}
for (name in names) {
addItem(
name,
false,
newFn
)
}
}
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource(), currentImportProvider).execute(this)
suspend fun eval(source: Source): Obj =
Compiler.compile(
source,
currentImportProvider
).execute(this)
fun containsLocal(name: String): Boolean = name in objects
/**
* Some scopes can be imported into other scopes, like [ModuleScope]. Those must correctly implement this method.
* @param scope where to copy symbols from this module
* @param symbols symbols to import, ir present, only symbols keys will be imported renamed to corresponding values
*/
open suspend fun importInto(scope: Scope, symbols: Map<String, String>? = null) {
scope.raiseError(ObjIllegalOperationException(scope, "Import is not allowed here: import $packageName"))
}
/**
* Find a first [ImportManager] in this Scope hierarchy. Normally there should be one. Found instance is cached.
*
* Use it to register your package sources, see [ImportManager] features.
*
* @throws IllegalStateException if there is no such manager (if you create some specific scope with no manager,
* then you knew what you did)
*/
val currentImportProvider: ImportProvider by lazy {
if (this is ModuleScope)
importProvider.getActualProvider()
else
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
}
val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
companion object {
fun new(): Scope =
Script.defaultImportManager.copy().newModuleAt(Pos.builtIn)
}
}
@@ -1,6 +1,9 @@
package net.sergeych.lyng
import kotlinx.coroutines.delay
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lynon.ObjLynonClass
import kotlin.math.*
class Script(
@@ -9,18 +12,20 @@ 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()) {
if (i > 0) print(' ' + a.asStr.value)
@@ -111,20 +116,26 @@ class Script(
}
addFn( "abs" ) {
val x = args.firstAndOnly()
if( x is ObjInt ) ObjInt( x.value.absoluteValue ) else ObjReal( x.toDouble().absoluteValue )
if( x is ObjInt) ObjInt( x.value.absoluteValue ) else ObjReal( x.toDouble().absoluteValue )
}
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
if( !cond.value == true )
raiseError(ObjAssertionError(this,"Assertion failed"))
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
}
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) == 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} != ${b.inspect()}"))
}
addFn("assertThrows") {
val code = requireOnlyArg<Statement>()
@@ -138,27 +149,31 @@ class Script(
catch (e: ScriptError) {
ObjNull
}
result ?: raiseError(ObjAssertionError(this,"Expected exception but nothing was thrown"))
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
}
addVoidFn("delay") {
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)
@@ -166,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
}
}
}
}
}
}
@@ -0,0 +1,19 @@
@file:Suppress("CanBeParameter")
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
${pos.currentLine}
${"-".repeat(pos.column)}^
""".trimIndent(),
cause
)
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")
}
}
@@ -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)
}
@@ -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,110 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonType
val ObjClassType by lazy { ObjClass("Class") }
open class ObjClass(
val className: String,
vararg parents: ObjClass,
) : Obj() {
val classNameObj by lazy { ObjString(className) }
var constructorMeta: ArgsDeclaration? = null
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toMutableSet()
override val objClass: ObjClass by lazy { ObjClassType }
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
private val classMembers = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
override suspend fun callOn(scope: Scope): Obj {
val instance = ObjInstance(this)
instance.instanceScope = scope.copy(newThisObj = instance,args = scope.args)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceScope)
}
return instance
}
fun createField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if( existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun createClassField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = classMembers[name]
if( existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
classMembers[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createClassField(name, statement { code() }, isOpen)
}
/**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
return rootObjectType.members[name]
}
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
override suspend fun readField(scope: Scope, name: String): ObjRecord {
classMembers[name]?.let {
return it
}
return super.readField(scope, name)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj {
return classMembers[name]?.value?.invoke(scope, this, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented()
}
@@ -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 = vars[i]
}
}
}
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in comparableVars) {
val a = f.value.value
val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b)
if (d != 0) return d
}
return 0
}
}
@@ -0,0 +1,28 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonType
/**
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
*/
class ObjInstanceClass(val name: String) : ObjClass(name) {
// val onDeserilaized =
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val args = decoder.decodeAnyList(scope)
println("deserializing constructor $name, $args params")
val actualSize = constructorMeta?.params?.size ?: 0
if( args.size > actualSize )
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
val newScope = scope.copy(args = Arguments(args))
return (callOn(newScope) as ObjInstance).apply {
deserializeStateVars(scope,decoder)
invokeInstanceMethod(scope, "onDeserialized", onNotFoundResult = ObjVoid)
}
}
}
@@ -0,0 +1,178 @@
package net.sergeych.lyng.obj
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.isDistantFuture
import kotlinx.datetime.isDistantPast
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonSettings
import net.sergeych.lynon.LynonType
class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTruncateMode=LynonSettings.InstantTruncateMode.Microsecond) : Obj() {
override val objClass: ObjClass get() = type
override fun toString(): String {
return instant.toString()
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant + other.duration)
else -> super.plus(scope, other)
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant - other.duration)
is ObjInstant -> ObjDuration(instant - other.instant)
else -> super.plus(scope, other)
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjInstant) {
return instant.compareTo(other.instant)
}
return super.compareTo(scope, other)
}
override suspend fun toKotlin(scope: Scope): Any {
return instant
}
override fun hashCode(): Int {
return instant.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInstant
return instant == other.instant
}
override suspend fun lynonType(): LynonType = LynonType.Instant
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.putBits(truncateMode.ordinal, 2)
when(truncateMode) {
LynonSettings.InstantTruncateMode.Millisecond ->
encoder.encodeSigned(instant.toEpochMilliseconds())
LynonSettings.InstantTruncateMode.Second ->
encoder.encodeSigned(instant.epochSeconds)
LynonSettings.InstantTruncateMode.Microsecond -> {
encoder.encodeSigned(instant.epochSeconds)
encoder.encodeUnsigned(instant.nanosecondsOfSecond.toULong() / 1000UL)
}
}
}
companion object {
val distantFuture by lazy {
ObjInstant(Instant.DISTANT_FUTURE)
}
val distantPast by lazy {
ObjInstant(Instant.DISTANT_PAST)
}
val type = object : ObjClass("Instant") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
val a0 = args.list.getOrNull(0)
return ObjInstant(
when (a0) {
null -> {
val t = Clock.System.now()
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond)
}
is ObjInt -> Instant.fromEpochSeconds(a0.value)
is ObjReal -> {
val seconds = a0.value.toLong()
val nanos = (a0.value - seconds) * 1e9
Instant.fromEpochSeconds(seconds, nanos.toLong())
}
is ObjInstant -> a0.instant
else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
}
}
)
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val mode = LynonSettings.InstantTruncateMode.entries[decoder.getBitsAsInt(2)]
return when (mode) {
LynonSettings.InstantTruncateMode.Microsecond -> ObjInstant(
Instant.fromEpochSeconds(
decoder.unpackSigned(), decoder.unpackUnsignedInt() * 1000
)
)
LynonSettings.InstantTruncateMode.Millisecond -> ObjInstant(
Instant.fromEpochMilliseconds(
decoder.unpackSigned()
)
)
LynonSettings.InstantTruncateMode.Second -> ObjInstant(
Instant.fromEpochSeconds(decoder.unpackSigned())
)
}
}
}.apply {
addFn("epochSeconds") {
val instant = thisAs<ObjInstant>().instant
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
}
addFn("isDistantFuture") {
thisAs<ObjInstant>().instant.isDistantFuture.toObj()
}
addFn("isDistantPast") {
thisAs<ObjInstant>().instant.isDistantPast.toObj()
}
addFn("epochWholeSeconds") {
ObjInt(thisAs<ObjInstant>().instant.epochSeconds)
}
addFn("nanosecondsOfSecond") {
ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong())
}
addFn("truncateToSecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
}
addFn("truncateToMillisecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
LynonSettings.InstantTruncateMode.Millisecond
)
}
addFn("truncateToMicrosecond") {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),
LynonSettings.InstantTruncateMode.Microsecond
)
}
// class members
addClassConst("distantFuture", distantFuture)
addClassConst("distantPast", distantPast)
addClassFn("now") {
ObjInstant(Clock.System.now())
}
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
}
}
}
@@ -0,0 +1,142 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Numeric {
override val asStr get() = ObjString(value.toString())
override val longValue get() = value
override val doubleValue get() = value.toDouble()
override val toObjInt get() = this
override val toObjReal = ObjReal(doubleValue)
override fun byValueCopy(): Obj = ObjInt(value)
override fun hashCode(): Int {
return value.hashCode()
}
override suspend fun getAndIncrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value++ }
}
override suspend fun getAndDecrement(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(value).also { value-- }
}
override suspend fun incrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(++value)
}
override suspend fun decrementAndGet(scope: Scope): Obj {
ensureNotConst(scope)
return ObjInt(--value)
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is Numeric) return -2
return value.compareTo(other.doubleValue)
}
override fun toString(): String = value.toString()
override val objClass: ObjClass = type
override suspend fun plus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value + other.value)
else
ObjReal(this.doubleValue + other.toDouble())
override suspend fun minus(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value - other.value)
else
ObjReal(this.doubleValue - other.toDouble())
override suspend fun mul(scope: Scope, other: Obj): Obj =
if (other is ObjInt) {
ObjInt(this.value * other.value)
} else ObjReal(this.value * other.toDouble())
override suspend fun div(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value / other.value)
else ObjReal(this.value / other.toDouble())
override suspend fun mod(scope: Scope, other: Obj): Obj =
if (other is ObjInt)
ObjInt(this.value % other.value)
else ObjReal(this.value.toDouble() % other.toDouble())
/**
* We are by-value type ([byValueCopy] is implemented) so we can do in-place
* assignment
*/
override suspend fun assign(scope: Scope, other: Obj): Obj? {
return if (!isConst && other is ObjInt) {
value = other.value
this
} else null
}
override suspend fun toKotlin(scope: Scope): Any {
return value
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as ObjInt
return value == other.value
}
override suspend fun negate(scope: Scope): Obj {
return ObjInt(-value)
}
override suspend fun lynonType(): LynonType = when (value) {
0L -> LynonType.Int0
else -> {
if (value > 0) LynonType.IntPositive
else LynonType.IntNegative
}
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
when (lynonType) {
null -> encoder.encodeSigned(value)
LynonType.Int0 -> {}
LynonType.IntPositive -> encoder.encodeUnsigned(value.toULong())
LynonType.IntNegative -> encoder.encodeUnsigned((-value).toULong())
LynonType.IntSigned -> encoder.encodeSigned(value)
else -> scope.raiseIllegalArgument("Unsupported lynon type code for Int: $lynonType")
}
}
companion object {
val Zero = ObjInt(0, true)
val One = ObjInt(1, true)
val type = object : ObjClass("Int") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
when (lynonType) {
null -> ObjInt(decoder.unpackSigned())
LynonType.Int0 -> Zero
LynonType.IntPositive -> ObjInt(decoder.unpackUnsigned().toLong())
LynonType.IntNegative -> ObjInt(-decoder.unpackUnsigned().toLong())
LynonType.IntSigned -> ObjInt(decoder.unpackSigned())
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
}
}
}
}
fun Int.toObj() = ObjInt(this.toLong())
@@ -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)
}
}
}
}
}
}
@@ -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") {
@@ -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(ObjIterationFinishedError(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)
}
}
}
@@ -1,5 +1,8 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
fun String.toSource(name: String = "eval"): Source = Source(name, this)
sealed class ObjType {
@@ -11,21 +14,21 @@ sealed class ObjType {
@Suppress("unused")
abstract class Statement(
val isStaticConst: Boolean = false,
val isConst: Boolean = false,
override val isConst: Boolean = false,
val returnType: ObjType = ObjType.Any
) : Obj() {
override val objClass: ObjClass = type
abstract val pos: Pos
abstract suspend fun execute(context: Context): Obj
abstract suspend fun execute(scope: Scope): Obj
override suspend fun compareTo(context: Context,other: Obj): Int {
override suspend fun compareTo(scope: Scope, other: Obj): Int {
throw UnsupportedOperationException("not comparable")
}
override suspend fun callOn(context: Context): Obj {
return execute(context)
override suspend fun callOn(scope: Scope): Obj {
return execute(scope)
}
override fun toString(): String = "Callable@${this.hashCode()}"
@@ -34,6 +37,8 @@ abstract class Statement(
val type = ObjClass("Callable")
}
suspend fun call(scope: Scope, vararg args: Obj) = execute(scope.copy(args = Arguments(*args)))
}
fun Statement.raise(text: String): Nothing {
@@ -45,16 +50,16 @@ fun Statement.require(cond: Boolean, message: () -> String) {
if (!cond) raise(message())
}
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Context) -> Obj): Statement =
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Scope) -> Obj): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = pos
override suspend fun execute(context: Context): Obj = f(context)
override suspend fun execute(scope: Scope): Obj = f(scope)
}
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Context.() -> Obj): Statement =
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Scope.() -> Obj): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj = f(context)
override suspend fun execute(scope: Scope): Obj = f(scope)
}
@@ -0,0 +1,90 @@
package net.sergeych.lynon
interface BitInput {
fun getBitOrNull(): Int?
fun getBitsOrNull(count: Int): ULong? {
var result = 0UL
var resultMask = 1UL
for( i in 0 ..< count) {
when(getBitOrNull()) {
null -> return null
1 -> result = result or resultMask
0 -> {}
}
resultMask = resultMask shl 1
}
return result
}
fun getBits(count: Int): ULong {
return getBitsOrNull(count) ?: throw IllegalStateException("Unexpected end of stream")
}
fun getBit(): Int {
return getBitOrNull() ?: throw IllegalStateException("Unexpected end of stream")
}
fun unpackUnsigned(): ULong =
unpackUnsignedOrNull() ?: throw IllegalStateException("Unexpected end of stream")
fun unpackUnsignedOrNull(): ULong? {
val tetrades = getBitsOrNull(4)?.toInt() ?: return null
var result = 0UL
var shift = 0
for (i in 0.. tetrades) {
result = result or (getBits(4) shl shift)
shift += 4
}
return result
}
fun unpackSigned(): Long {
val isNegative = getBit()
val value = unpackUnsigned().toLong()
return if( isNegative == 1) -value else value
}
@Suppress("unused")
fun getBool(): Boolean {
return getBit() == 1
}
fun getBytes(count: Int): ByteArray? {
val result = ByteArray(count)
for (i in 0..<count) {
val b = getBitsOrNull(8) ?: return null
result[i] = b.toByte()
}
return result
}
fun decompress(): ByteArray = decompressOrNull() ?: throw DecompressionException("Unexpected end of stream")
fun decompressOrNull(): ByteArray? {
val originalSize = unpackUnsignedOrNull()?.toInt() ?: return null
return if( getBit() == 1) {
// data is compressed
// val expectedCRC = getBits(32).toUInt()
val method = getBits(2).toInt()
if( method != 0) throw DecompressionException("Unknown compression method")
LZW.decompress(this, originalSize).asByteArray()
}
else {
getBytes(originalSize) ?: throw DecompressionException("Unexpected end of stream in uncompressed data")
}
}
@Suppress("unused")
fun decompressStringOrNull(): String? = decompressOrNull()?.decodeToString()
fun decompressString(): String = decompress().decodeToString()
fun unpackDouble(): Double {
val bits = getBits(64)
return Double.fromBits(bits.toLong())
}
}
@@ -0,0 +1,34 @@
package net.sergeych.lynon
@Suppress("unused")
interface BitList {
operator fun get(bitIndex: Long): Int
operator fun set(bitIndex: Long,value: Int)
val size: Long
val indices: LongRange
fun toInput(): BitInput = object : BitInput {
private var index = 0L
override fun getBitOrNull(): Int? =
if( index < size) this@BitList[index++]
else null
}
}
fun bitListOf(vararg bits: Int): BitList {
return if( bits.size > 64) {
BitArray.ofBits(*bits)
}
else
TinyBits.of(*bits)
}
@Suppress("unused")
fun bitListOfSize(sizeInBits: Long): BitList {
return if( sizeInBits > 64) {
BitArray.withBitSize(sizeInBits)
}
else
TinyBits()
}
@@ -0,0 +1,104 @@
package net.sergeych.lynon
interface BitOutput {
fun putBits(bits: ULong, count: Int) {
require(count <= 64)
var x = bits
for (i in 0..<count) {
putBit((x and 1u).toInt())
x = x shr 1
}
}
fun putBits(bits: Int, count: Int) {
require(count <= 32)
var x = bits
for (i in 0..<count) {
putBit((x and 1))
x = x shr 1
}
}
fun putBit(bit: Int)
fun putBits(bitList: BitList) {
for (i in bitList.indices)
putBit(bitList[i])
}
fun packUnsigned(value: ULong) {
val tetrades = sizeInTetrades(value)
putBits(tetrades - 1, 4)
var rest = value
for (i in 0..<tetrades) {
putBits(rest and 0xFu, 4)
rest = rest shr 4
}
}
@Suppress("unused")
fun packSigned(value: Long) {
if (value < 0) {
putBit(1)
packUnsigned((-value).toULong())
} else {
putBit(0)
packUnsigned(value.toULong())
}
}
fun putBytes(data: ByteArray) {
for (b in data) {
putBits(b.toULong(), 8)
}
}
/**
* Create compressed record with content and size check. Compression works with _bytes_.
*
* Structure:
*
* | size | meaning |
* |------|--------------------------------------------------|
* | packed unsigned | size of uncompressed content in bytes |
* | 1 | 0 - not compressed, 1 - compressed |
*
* __If compressed__, then:
*
* | size | meaning |
* |------|--------------------------------------|
* | 2 | 00 - LZW, other combinations reserved|
*
* After this header compressed bits follow.
*
* __If not compressed,__ then source data follows as bit stream.
*
* Compressed block overhead is 3 bits, uncompressed 1.
*/
fun compress(source: ByteArray) {
// size
packUnsigned(source.size.toULong())
// check compression is effective?
val compressed = LZW.compress(source.asUByteArray())
// check that compression is effective including header bits size:
if( compressed.size + 2 < source.size * 8L) {
println("write compressed")
putBit(1)
// LZW algorithm
putBits(0, 2)
// compressed data
putBits(compressed)
}
else {
putBit(0)
putBytes(source)
}
}
fun compress(source: String) {
compress(source.encodeToByteArray())
}
}
@@ -0,0 +1,3 @@
package net.sergeych.lynon
class DecompressionException(message: String) : IllegalArgumentException(message) {}
@@ -0,0 +1,106 @@
package net.sergeych.lynon
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjString
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
fun getBitsAsInt(bitsSize: Int): Int {
return bin.getBits(bitsSize).toInt()
}
fun unpackUnsignedInt(): Int = bin.unpackUnsigned().toInt()
fun decompress() = bin.decompress()
val cache = mutableListOf<Any>()
inline fun <reified T : Any> decodeCached(f: LynonDecoder.() -> T): T {
return if (bin.getBit() == 0) {
// unpack and cache
f().also {
// println("decode: cache miss: ${cache.size}: $it:${it::class.simpleName}")
if (settings.shouldCache(it)) cache.add(it)
}
} else {
// get cache reference
val size = sizeInBits(cache.size)
val id = bin.getBitsOrNull(size)?.toInt()
?: throw RuntimeException("Invalid object id: unexpected end of stream")
if (id >= cache.size) throw RuntimeException("Invalid object id: $id should be in 0..<${cache.size}")
// println("decode: cache hit ${id}: ${cache[id]}:${cache[id]::class.simpleName}")
// @Suppress("UNCHECKED_CAST")
cache[id] as T
}
}
suspend fun decodeAny(scope: Scope): Obj = decodeCached {
val type = LynonType.entries[bin.getBits(4).toInt()]
if (type != LynonType.Other) {
type.objClass.deserialize(scope, this, type)
} else {
decodeClassObj(scope).deserialize(scope, this, null)
}
}
private suspend fun decodeClassObj(scope: Scope): ObjClass {
val className = decodeObject(scope, ObjString.type, null) as ObjString
println("expected class name $className")
return scope.get(className.value)?.value?.let {
if (it !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
it
} ?: scope.raiseSymbolNotFound("can't deserialize: not found type $className")
}
suspend fun decodeAnyList(scope: Scope,fixedSize: Int?=null): MutableList<Obj> {
return if (bin.getBit() == 1) {
// homogenous
val type = LynonType.entries[getBitsAsInt(4)]
val list = mutableListOf<Obj>()
val objClass = if (type == LynonType.Other)
decodeClassObj(scope).also { println("detected class obj: $it") }
else type.objClass
val size = fixedSize ?: bin.unpackUnsigned().toInt()
println("detected homogenous list type $type, $size items")
for (i in 0..<size) {
list += decodeObject(scope, objClass, type).also {
println("decoded: $it")
}
}
list
} else {
val size = fixedSize ?: unpackUnsigned().toInt()
(0..<size).map { decodeAny(scope) }.toMutableList()
}
}
suspend fun decodeObject(scope: Scope, type: ObjClass, overrideType: LynonType? = null): Obj {
return decodeCached { type.deserialize(scope, this, overrideType) }
}
fun unpackBinaryData(): ByteArray = bin.decompress()
@Suppress("unused")
fun unpackBinaryDataOrNull(): ByteArray? = bin.decompressOrNull()
fun unpackBoolean(): Boolean {
return bin.getBit() == 1
}
fun unpackDouble(): Double {
return Double.fromBits(bin.getBits(64).toLong())
}
fun unpackSigned(): Long {
return bin.unpackSigned()
}
fun unpackUnsigned(): ULong {
return bin.unpackUnsigned()
}
}
@@ -0,0 +1,194 @@
package net.sergeych.lynon
import net.sergeych.bintools.ByteChunk
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.*
enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
Null(ObjNull.objClass, 80),
Int0(ObjInt.type, 70),
IntNegative(ObjInt.type, 50),
IntPositive(ObjInt.type, 100),
IntSigned(ObjInt.type, 30),
String(ObjString.type, 100),
Real(ObjReal.type),
Bool(ObjBool.type, 80),
List(ObjList.type, 70),
Map(ObjMap.type, 40),
Set(ObjSet.type),
Buffer(ObjBuffer.type, 50),
Instant(ObjInstant.type, 30),
Duration(ObjDuration.type),
Other(Obj.rootObjectType, 60);
fun generalizeTo(other: LynonType): LynonType? {
if (this == other) return this
return (if (this.isInt && other.isInt) {
when {
this == Int0 -> other // upgrade 0 to some other int
other == Int0 -> this // 0 is member of our class, ignore
// different signum propagate to signed
else -> IntSigned
}
} else
// impossible to generalize
null
).also { println("Gen $this + $other -> $it") }
}
val isInt by lazy {
when (this) {
Int0, IntSigned, IntPositive, IntNegative -> true
else -> false
}
}
}
open class LynonEncoder(val bout: BitOutput, val settings: LynonSettings = LynonSettings.default) {
val cache = mutableMapOf<Any, Int>()
suspend fun encodeCached(item: Any, packer: suspend LynonEncoder.() -> Unit) {
suspend fun serializeAndCache(key: Any = item) {
cache[key]?.let { cacheId ->
// println("encode: Cache hit: ${cacheId}: $item: ${item::class.simpleName}")
val size = sizeInBits(cache.size)
bout.putBit(1)
bout.putBits(cacheId.toULong(), size)
} ?: run {
bout.putBit(0)
if (settings.shouldCache(item)) {
// println("encode add cache: ${cache.size}: $item: ${item::class.simpleName}")
packer()
cache[key] = cache.size
} else {
// println("encode but not cache $item")
packer()
}
}
}
when (item) {
is ByteArray -> serializeAndCache(ByteChunk(item.asUByteArray()))
is UByteArray -> serializeAndCache(ByteChunk(item))
else -> serializeAndCache(item)
}
}
/**
* Encode any Lyng object [Obj], which can be serialized, using type record. This allow to
* encode any object with the overhead of type record.
*
* Caching is used automatically.
*/
suspend fun encodeAny(scope: Scope, obj: Obj) {
encodeCached(obj) {
val type = putTypeRecord(scope, obj, obj.lynonType())
obj.serialize(scope, this, type)
}
}
private suspend fun putTypeRecord(scope: Scope, obj: Obj, type: LynonType): LynonType {
putType(type)
if( type == LynonType.Other) {
encodeObject(scope, obj.objClass.classNameObj)
}
return type
}
private fun putType(type: LynonType) {
bout.putBits(type.ordinal.toULong(), 4)
}
/**
* AnyList could be homogenous (first bit=1) and heterogeneous (first bit=0). Homogenous list
* has a single type record that precedes the list, heterogeneous hash typed record
* for each item.
*
*/
suspend fun encodeAnyList(scope: Scope, list: List<Obj>,fixedSize: Boolean = false) {
if( list.isEmpty()) {
// any-typed, empty, save space
putBit(0)
encodeUnsigned(0u)
}
else {
val objClass = list[0].objClass
var type = list[0].lynonType()
var isHomogeneous = true
for (i in list.drop(1))
if (i.objClass != objClass) {
isHomogeneous = false
break
} else {
// same class but type might need generalization
type = type.generalizeTo(i.lynonType())
?: scope.raiseError("inner error: can't generalize lynon type $type to ${i.lynonType()}")
}
if (isHomogeneous) {
putBit(1)
putTypeRecord(scope, list[0], type)
if (!fixedSize)
encodeUnsigned(list.size.toULong())
for (i in list) encodeObject(scope, i, type)
} else {
putBit(0)
if (!fixedSize)
encodeUnsigned(list.size.toULong())
for (i in list) encodeAny(scope, i)
}
}
}
/**
* Write object _with no type record_: type is known
*/
suspend fun encodeObject(scope: Scope, obj: Obj,overrideType: LynonType? = null) {
encodeCached(obj) {
obj.serialize(scope, this, overrideType)
}
}
suspend fun encodeCachedBytes(bytes: ByteArray) {
encodeCached(bytes) {
bout.compress(bytes)
}
}
fun encodeBinaryData(data: ByteArray) {
bout.compress(data)
}
fun encodeSigned(value: Long) {
bout.packSigned(value)
}
@Suppress("unused")
fun encodeUnsigned(value: ULong) {
bout.packUnsigned(value)
}
@Suppress("unused")
fun encodeBool(value: Boolean) {
bout.putBit(if (value) 1 else 0)
}
fun encodeReal(value: Double) {
bout.putBits(value.toRawBits().toULong(), 64)
}
fun encodeBoolean(value: Boolean) {
bout.putBit(if (value) 1 else 0)
}
fun putBits(value: Int, sizeInBits: Int) {
bout.putBits(value.toULong(), sizeInBits)
}
fun putBit(bit: Int) {
bout.putBit(bit)
}
}
@@ -0,0 +1,29 @@
package net.sergeych.lynon
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjChar
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
import kotlin.math.absoluteValue
open class LynonSettings {
enum class InstantTruncateMode {
Second,
Millisecond,
Microsecond
}
open fun shouldCache(obj: Any): Boolean = when (obj) {
is ObjChar -> false
is ObjInt -> obj.value.absoluteValue > 0x10000FF
is ObjBool -> false
is ObjNull -> false
is ByteArray -> obj.size > 2
is UByteArray -> obj.size > 2
else -> true
}
companion object {
val default = LynonSettings()
}
}
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more