Compare commits

...
17 Commits
Author SHA1 Message Date
sergeych 1baf69f40f more docs on Iterable 2025-08-09 23:23:05 +03:00
sergeych ba8d543d87 more docs on Iterable 2025-08-09 23:20:25 +03:00
sergeych d3785afa6f adopt stdlib to CLI tool 2025-08-09 22:59:43 +03:00
sergeych 3948283481 0.8.4: stadlib in Lyng! added universal Iterable functions 2025-08-09 22:50:48 +03:00
sergeych f9198fe583 parallelism and flows + bugfixes release 2025-08-09 15:55:30 +03:00
sergeych 4917f99197 docs 2025-08-09 15:54:46 +03:00
sergeych e0ed27a01f fixed many bugs in closures processing also in flows 2025-08-09 15:48:41 +03:00
sergeych 9aae33d564 fix #48 flows 2025-08-08 19:33:17 +03:00
sergeych 1a90b25b1e fix #51 ref #48 flows started. bad closure-based bug fixed 2025-08-08 19:23:43 +03:00
sergeych f7f020f4d6 0.8.2-SNAPSHOT 2025-08-07 21:50:14 +03:00
sergeych 0f54e2f845 ref #47 yield 2025-08-07 21:46:45 +03:00
sergeych 19f8b6605b fix #47 parallelism: launch, Deferred, CompletableDeferred, Mutex (delay was already implemented) 2025-08-07 20:31:36 +03:00
sergeych de71c7c8ec 0.8.1 release version 2025-08-06 23:12:25 +03:00
sergeych be370dfdef fix #45 private statics and docs 2025-08-06 23:10:33 +03:00
sergeych 53a6a88924 fix #42 class static fields and methods 2025-08-06 22:48:32 +03:00
sergeych 2c2dae4d85 well redesigned class vals and vars, classScope introduced for class functions 2025-08-06 11:47:45 +03:00
sergeych 63b2808109 well redesigned class vals and vars, classScope introduced for class functions 2025-08-06 11:46:17 +03:00
41 changed files with 1563 additions and 137 deletions

No files matched your search

+8 -6
View File
@@ -23,7 +23,7 @@ fun swapEnds(first, args..., last, f) {
delay(1.5) // coroutine is delayed for 1.5s, thread is not blocked!
```
and it is multithreaded on platforms supporting it (automatically, no code changes required, just
`launch` more coroutines and they will be executed concurrently if possible)/
`launch` more coroutines and they will be executed concurrently if possible). See [parallelism]
- functional style and OOP together, multiple inheritance, implementing interfaces for existing classes, writing extensions.
- Any unicode letters can be used as identifiers: `assert( sin(π/2) == 1 )`.
@@ -142,12 +142,12 @@ Ready features:
Under way:
- [ ] maps, sets and sequences (flows?)
- [x] maps, sets and sequences (flows?)
- [ ] regular exceptions
- [ ] modules
- [ ] string interpolation and more string tools
- [x] modules
- [x] string formatting and tools
- [ ] multiple inheritance for user classes
- [ ] launch, deferred, coroutineScope, mutex, etc.
- [x] launch, deferred, CompletableDeferred, Mutex, etc.
- [ ] site with integrated interpreter to give a try
- [ ] kotlin part public API good docs, integration focused
- [ ] better stack reporting
@@ -158,10 +158,12 @@ Planned features.
- [ ] type specifications
- [ ] source docs and maybe lyng.md to a standard
- [ ] moacro-style kotlin integration or something else to simplify it
- [ ] macro-style kotlin integration or something else to simplify it
Further
- [ ] client with GUI support based on compose multiplatform somehow
- [ ] notebook - style workbooks with graphs, formulaes, etc.
- [ ] language server or compose-based lyng-aware editor
[parallelism]: docs/parallelism.md
+27 -4
View File
@@ -1,5 +1,11 @@
# Iterable interface
The interface for anything that can be iterated, e.g. finite or infinite ordered set of data that can be accessed sequentially. Almost any data container in `Lyng` implements it: `List`, `Set`, `Buffer`, `RingBuffer`, `BitBuffer`, `Range` and many others are `Iterable`, also `Collection` and `Array` interfaces inherit it.
`Map` and `String` have `Iterable` members to access its contents too.
## Definition:
Iterable is a class that provides function that creates _the iterator_:
class Iterable {
@@ -17,9 +23,16 @@ Iterator itself is a simple interface that should provide only to method:
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:
Having `Iterable` in base classes allows to use it in for loop. Also, each `Iterable` has some utility functions available, for example
## Instance methods
val r = 1..10 // Range is Iterable!
assertEquals( [9,10] r.takeLast(2) )
assertEquals( [1,2,3] r.take(3) )
assertEquals( [9,10] r.drop(8) )
assertEquals( [1,2] r.dropLast(8) )
>>> void
## Instance methods:
| fun/method | description |
|-----------------|---------------------------------------------------------------------------------|
@@ -33,7 +46,15 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
| 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 |
| first | first element (1) |
| last | last element (1) |
| take(n) | return [Iterable] of up to n first elements |
| taleLast(n) | return [Iterable] of up to n last elements |
| drop(n) | return new [Iterable] without first n elements |
| dropLast(n) | return new [Iterable] without last n elements |
(1)
: throws `NoSuchElementException` if there is no such element
fun Iterable.toList(): List
fun Iterable.toSet(): Set
@@ -45,7 +66,7 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter
fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
## Abstract methods
## Abstract methods:
fun iterator(): Iterator
@@ -57,7 +78,9 @@ Creates a list by iterating to the end. So, the Iterator should be finite to be
## Implemented in classes:
- [List], [Range]
- [List], [Range], [Buffer](Buffer.md), [BitBuffer], [Buffer], [Set], [RingBuffer]
[List]: List.md
[Range]: Range.md
[Set]: Set.md
[RingBuffer]: RingBuffer.md
+42 -1
View File
@@ -147,7 +147,7 @@ In many cases it is necessary to implement custom comparison and `toString`, sti
each class is provided with default implementations:
- default toString outputs class name and its _public_ fields.
- default comparison compares compares all public fields in order of appearance.
- default comparison compares all fields in order of appearance.
For example, for our class Point:
@@ -160,6 +160,47 @@ For example, for our class Point:
Point(1,1+1)
>>> Point(x=1,y=2)
## Statics: class fields and class methods
You can mark a field or a method as static. This is borrowed from Java as more plain version of a kotlin's companion object or Scala's object. Static field and functions is one for a class, not for an instance. From inside the class, e.g. from the class method, it is a regular var. From outside, it is accessible as `ClassName.field` or method:
class Value(x) {
static var foo = Value("foo")
static fun exclamation() {
// here foo is a regular var:
foo.x + "!"
}
}
assertEquals( Value.foo.x, "foo" )
assertEquals( "foo!", Value.exclamation() )
// we can access foo from outside like this:
Value.foo = Value("bar")
assertEquals( "bar!", Value.exclamation() )
>>> void
As usual, private statics are not accessible from the outside:
class Test {
// private, inacessible from outside protected data:
private static var data = null
// the interface to access and change it:
static fun getData() { data }
static fun setData(value) { data = value }
}
// no direct access:
assertThrows { Test.data }
// accessible with the interface:
assertEquals( null, Test.getData() )
Test.setData("fubar")
assertEquals("fubar", Test.getData() )
>>> void
# Extending classes
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method
+52
View File
@@ -0,0 +1,52 @@
# RingBuffer
This is a fixed size buffer that allow to store N last elements with _O(1)_ effectiveness (no data shifting).
Here is the sample:
val r = RingBuffer(3)
assert( r is RingBuffer )
assertEquals(0, r.size)
assertEquals(3, r.capacity)
r += 10
assertEquals(1, r.size)
assertEquals(10, r.first)
r += 20
assertEquals(2, r.size)
assertEquals( [10, 20], r.toList() )
r += 30
assertEquals(3, r.size)
assertEquals( [10, 20, 30], r.toList() )
// now first value is lost:
r += 40
assertEquals(3, r.size)
assertEquals( [20, 30, 40], r.toList() )
assertEquals(3, r.capacity)
>>> void
Ring buffer implements [Iterable], so any of its methods are available for `RingBuffer`, e.g. `first`, `last`, `toList`,
`take`, `drop`, `takelast`, `dropLast`, etc.
## Constructor
RinbBuffer(capacity: Int)
## Instance methods
| method | description | remarks |
|-------------|------------------------|---------|
| capacity | max size of the buffer | |
| size | current size | (1) |
| operator += | add new item | (1) |
| add(item) | add new item | (1) |
| iterator() | return iterator | (1) |
(1)
: Ringbuffer is not threadsafe, protect it with a mutex to avoid RC where necessary.
[Iterable]: Iterable.md
+4
View File
@@ -1,5 +1,7 @@
# Advanced topics
__See also:__ [parallelism].
## Closures/scopes isolation
Each block has own scope, in which it can safely use closures and override
@@ -111,3 +113,5 @@ arguments list in almost arbitrary ways. For example:
>>> void
,
[parallelism]: parallelism.md
+207
View File
@@ -0,0 +1,207 @@
# Parallelism in Lyng
Lyng is built to me multithreaded where possible (e.g. all targets byt JS and wasmJS as for now)
and cooperatively parallel (coroutine based) everywhere.
In Lyng, every function, every lambda are _coroutines_. It means, you can have as many of these as you want without risking running out of memory on threads stack, or get too many threads.
Depending on the platform, these coroutines may be executed on different CPU and cores, too, truly in parallel. Where not, like Javascript browser, they are still executed cooperatively. You should not care about the platform capabilities, just call `launch`:
// track coroutine call:
var xIsCalled = false
// launch coroutine in parallel
val x = launch {
// wait 10ms to let main code to be executed
delay(10)
// now set the flag
xIsCalled = true
// and return something useful:
"ok"
}
// corouine is launhed, but not yet executed
// due to delay call:
assert(!xIsCalled)
// now we wait for it to be executed:
assertEquals( x.await(), "ok")
// now glag should be set:
assert(xIsCalled)
>>> void
This example shows how to launch a coroutine with `launch` which returns [Deferred] instance, the latter have ways to await for the coroutine completion and retrieve possible result.
Launch has the only argument which should be a callable (lambda usually) that is run in parallel (or cooperatively in parallel), and return anything as the result.
## Synchronization: Mutex
Suppose we have a resource, that could be used concurrently, a coutner in our case. If we won'r protect it, concurrent usage cause RC, Race Condition, providing wrong result:
var counter = 0
(1..4).map {
launch {
// slow increment:
val c = counter
delay(10)
counter = c + 1
}
}.forEach { it.await() }
assert(counter < 4)
>>> void
The obviously wrong result is not 4, as all coroutines capture the counter value, which is 1, then sleep for 5ms, then save 1 + 1 as result. May some coroutines will pass, so it will be 1 or 2, most likely.
Using [Mutex] makes it all working:
var counter = 0
val mutex = Mutex()
(1..4).map {
launch {
// slow increment:
mutex.withLock {
val c = counter
delay(10)
counter = c + 1
}
}
}.forEach { it.await() }
assertEquals(4, counter)
>>> void
now everything works as expected: `mutex.withLock` makes them all be executed in sequence, not in parallel.
## Completable deferred
Sometimes it is convenient to manually set completion status of some deferred result. This is when [CompletableDeferred] is used:
// this variable will be completed later:
val done = CompletableDeferred()
// complete it ater delay
launch {
delay(10)
// complete it setting the result:
done.complete("ok")
}
// now it is still not completed: coroutine is delayed
// (ot not started on sinthe-threaded platforms):
assert(!done.isCompleted)
assert(done.isActive)
// then we can just await it as any other deferred:
assertEquals( done.await(), "ok")
// and as any other deferred it is now complete:
assert(done.isCompleted)
## True parallelism
Cooperative, coroutine-based parallelism is automatically available on all platforms. Depending on the platform, though, the coroutines could be dispatched also in different threads; where there are multiple cores and/or CPU available, it means the coroutines could be exuted truly in parallel, unless [Mutex] is used:
| platofrm | multithreaded |
|------------|---------------|
| JVM | yes |
| Android | yes |
| Javascript | NO |
| wasmJS | NO |
| IOS | yes |
| MacOSX | yes |
| Linux | yes |
| Windows | yes |
So it is important to always use [Mutex] where concurrent execution could be a problem (so called Race Conditions, or RC).
## Yield
When the coroutine is executed, on the single-threaded environment all other coroutines are suspended until active one will wait for something. Sometimes, it is undesirable; the coroutine may perform long calculations or some other CPU consuming task. The solution is to call `yield()` periodically. Unlike `delay()`, yield does not pauses the coroutine for some specified time, but it just makes all other coroutines to be executed. In other word, yield interrupts current coroutines and out it to the end of the dispatcher list of active coroutines. It is especially important on Javascript and wasmJS targets as otherwise UI thread could be blocked.
Usage example:
fun someLongTask() { // ...
do {
// execute step
if( done ) break
yield()
} while(true)
}
# Data exchange for coroutines
## Flow
Flow is an async cold sequence; it is named after kotlin's Flow as it resembles it closely. The cold means the flow is only evaluated when iterated (collected, in Kotlin terms), before it is inactive. Sequence means that it is potentially unlimited, as in our example of glorious Fibonacci number generator:
// Fibonacch numbers flow!
val f = flow {
println("Starting generator")
var n1 = 0
var n2 = 1
emit(n1)
emit(n2)
while(true) {
val n = n1 + n2
emit(n)
n1 = n2
n2 = n
}
}
val correctFibs = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
println("Generation starts")
assertEquals( correctFibs, f.take(correctFibs.size))
>>> Generation starts
>>> Starting generator
>>> void
Great: the generator is not executed until collected bu the `f.take()` call, which picks specified number of elements from the flow, can cancel it.
Important difference from the channels or like, every time you collect the flow, you collect it anew:
var isStarted = false
val f = flow {
emit("start")
isStarted = true
(1..4).forEach { emit(it) }
}
// flow is not yet started, e.g. not got execited,
// that is called 'cold':
assertEquals( false, isStarted )
// let's collect flow:
val result = []
for( x in f ) result += x
println(result)
assertEquals( true, isStarted)
// let's collect it once again, it should be the same:
println(f.toList())
// and again:
assertEquals( result, f.toList() )
>>> ["start", 1, 2, 3, 4]
>>> ["start", 1, 2, 3, 4]
>>> void
Notice that flow's lambda is not called until actual collection is started. Cold flows are
better in terms of resource consumption.
Flows allow easy transforming of any [Iterable]. See how the standard Lyng library functions use it:
fun Iterable.filter(predicate) {
val list = this
flow {
for( item in list ) {
if( predicate(item) ) {
emit(item)
}
}
}
}
[Iterable]: Iterable.md
+62 -33
View File
@@ -14,8 +14,11 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md)
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
- [time](time.md) and [parallelism](parallelism.md)
- [parallelism] - multithreaded code, coroutines, etc.
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md), [RingBuffer], [Buffer].
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and
loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions
@@ -170,7 +173,8 @@ 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
`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
@@ -464,7 +468,6 @@ after function call, it is treated as a last argument to the call, e.g.:
assert( [11, 21, 31] == mapped)
>>> void
# Lists (aka arrays)
Lyng has built-in mutable array class `List` with simple literals:
@@ -534,7 +537,8 @@ The simplest way to concatenate lists is using `+` and `+=`:
void
>>> void
***Important note***: the pitfall of using `+=` is that you can't append in [Iterable] instance as an object: it will always add all its contents. Use `list.add` to add a single iterable instance:
***Important note***: the pitfall of using `+=` is that you can't append in [Iterable] instance as an object: it will
always add all its contents. Use `list.add` to add a single iterable instance:
var list = [1, 2]
val other = [3, 4]
@@ -562,7 +566,6 @@ Use `list.add` to avoid confusion:
assert( list == [1, 2, [3, 4], (10..12)])
>>> void
To add elements to the list:
val x = [1,2]
@@ -595,7 +598,6 @@ Using splat arguments can simplify inserting list in list:
x
>>> [1, 0, 100, 0, 2, 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]
@@ -741,7 +743,9 @@ Also, you can check the type too:
#### 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.
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`):
@@ -757,7 +761,8 @@ Typical builtin types that are containers (e.g. support `conain`):
: 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:
: 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
@@ -766,7 +771,9 @@ Typical builtin types that are containers (e.g. support `conain`):
>>> 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).
: `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:
@@ -844,7 +851,8 @@ We can skip the rest of the loop and restart it, as usual, with `continue` opera
"found even numbers: " + countEven
>>> "found even numbers: 5"
`continue` can't "return" anything: it just restarts the loop. It can use labeled loops to restart outer ones (we intentionally avoid using for loops here):
`continue` can't "return" anything: it just restarts the loop. It can use labeled loops to restart outer ones (we
intentionally avoid using for loops here):
var count = 0
var total = 0
@@ -890,21 +898,22 @@ test function (remember function return it's last expression result):
```mermaid
flowchart TD
S((start)) --> Cond{check}
Cond--false, no else--->V((void))
Cond--true-->E(["last = loop_body()" ])
E--break value---->BV((value))
E--> Check2{check}
E--break---->V
Check2 --false-->E
Check2 --true, no else--->L((last))
Check2 --true, else-->Else(["else_clause()"])
Cond--false, else--->Else
Cond -- false, no else ---> V((void))
Cond -- true --> E(["last = loop_body()"])
E -- break value ----> BV((value))
E --> Check2{check}
E -- break ----> V
Check2 -- false --> E
Check2 -- true, no else ---> L((last))
Check2 -- true, else --> Else(["else_clause()"])
Cond -- false, else ---> Else
Else --> Ele4$nr((else))
```
So the returned value, as seen from diagram could be one of:
- `void`, if the loop was not executed, e.g. `condition` was initially false, and there was no `else` clause, or if the empty break was executed.
- `void`, if the loop was not executed, e.g. `condition` was initially false, and there was no `else` clause, or if the
empty break was executed.
- value returned from `break value' statement
- value returned from the `else` clause, of the loop was not broken
- value returned from the last execution of loop body, if there was no `break` and no `else` clause.
@@ -928,7 +937,8 @@ available in the condition:
} while( continueLoop )
>>> "OK"
This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value returning by the loop:
This is sometimes convenient when condition is complex and has to be calculated inside the loop body. Notice the value
returning by the loop:
fun readLine() { "done: result" }
val result = do {
@@ -939,7 +949,6 @@ This is sometimes convenient when condition is complex and has to be calculated
Suppose readLine() here reads some stream of lines.
## For loops
For loop are intended to traverse collections, and all other objects that supports
@@ -982,7 +991,8 @@ We can use labels too:
# 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:
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
@@ -1035,7 +1045,6 @@ 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:
@@ -1158,7 +1167,6 @@ Are the same as in string literals with little difference:
| code | Int | Unicode code for the character |
| | | |
## String details
Strings are arrays of Unicode characters. It can be indexed, and indexing will
@@ -1186,7 +1194,10 @@ To format a string use sprintf-style modifiers like:
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.
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.
@@ -1218,7 +1229,7 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
Typical set of String functions includes:
| fun/prop | description / notes |
|-------------------|------------------------------------------------------------|
|--------------------|------------------------------------------------------------|
| lower() | change case to unicode upper |
| upper() | change case to unicode lower |
| startsWith(prefix) | true if starts with a prefix |
@@ -1241,9 +1252,6 @@ Typical set of String functions includes:
(1)
: List is mutable therefore a new copy is created on each call.
### Literals
String literal could be multiline:
@@ -1258,9 +1266,17 @@ though multiline literals is yet work in progress.
See [math functions](math.md). Other general purpose functions are:
| name | description |
|----------------------------------------------|----------------------------------------------------------|
|--------------------------------------------|-----------------------------------------------------------|
| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them |
| println(args...) | Open for overriding, it prints to stdout. |
| assertEquals(a,b) | |
| assertNotEquals(a,b) | |
| assertTrows { /* block */ } | |
| check(condition, message=<default>) | throws IllegalStateException" of condition isn't met |
| require(condition, message=<default>) | throws IllegalArgumentException" of condition isn't met |
| println(args...) | Open for overriding, it prints to stdout with newline. |
| print(args...) | Open for overriding, it prints to stdout without newline. |
| flow {} | create flow sequence, see [parallelism] |
| delay, launch, yield | see [parallelism] |
# Built-in constants
@@ -1270,12 +1286,25 @@ See [math functions](math.md). Other general purpose functions are:
| π | See [math](math.md) |
[List]: List.md
[Iterable]: Iterable.md
[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
[parallelism]: parallelism.md
[RingBuffer]: RingBuffer.md
+1 -1
View File
@@ -4,7 +4,7 @@ clikt = "5.0.3"
kotlin = "2.1.21"
android-minSdk = "24"
android-compileSdk = "34"
kotlinx-coroutines = "1.10.1"
kotlinx-coroutines = "1.9.0"
mp_bintools = "0.1.12"
firebaseCrashlyticsBuildtools = "3.0.3"
okioVersion = "3.10.2"
+10 -7
View File
@@ -10,10 +10,11 @@ import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.*
import net.sergeych.mp_tools.globalDefer
import okio.FileSystem
import okio.Path.Companion.toPath
import okio.SYSTEM
@@ -38,14 +39,13 @@ data class CommandResult(
val error: String
)
val baseScope = Scope().apply {
val baseScopeDefer = globalDefer {
Script.newScope().apply {
addFn("exit") {
exit(requireOnlyArg<ObjInt>().toInt())
ObjVoid
}
// ObjString.type.addFn("shell") {
//
// }
}
}
fun runMain(args: Array<String>) {
@@ -88,6 +88,8 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
""".trimIndent()
override fun run() {
runBlocking {
val baseScope = baseScopeDefer.await()
when {
version -> {
println("Lyng language version ${LyngVersion}")
@@ -127,11 +129,12 @@ private 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()))
baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
executeFile(fileName)
}
}
@@ -148,7 +151,7 @@ suspend fun executeFile(fileName: String) {
text = text.substring(pos + 1)
}
processErrors {
baseScope.eval(Source(fileName, text))
baseScopeDefer.await().eval(Source(fileName, text))
}
}
+2 -1
View File
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.8.0-SNAPSHOT"
version = "0.8.4-SNAPSHOT"
buildscript {
repositories {
@@ -65,6 +65,7 @@ kotlin {
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
}
val commonMain by getting {
@@ -1,19 +0,0 @@
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]
}
@@ -34,7 +34,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) {
fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = defaultRecordType)
}
@@ -36,7 +36,7 @@ data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) :
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
return list.first()
return list.first().byValueCopy()
}
/**
@@ -48,7 +48,6 @@ data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) :
fun inspect(): String = list.joinToString(", ") { it.inspect() }
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList())
@@ -0,0 +1,16 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.ObjRecord
/**
* Scope that adds a "closure" to caller; most often it is used to apply class instance to caller scope.
* Inherits [Scope.args] and [Scope.thisObj] from [callScope] and adds lookup for symbols
* from [closureScope] with proper precedence
*/
class ClosureScope(val callScope: Scope,val closureScope: Scope) : Scope(callScope, callScope.args, thisObj = callScope.thisObj) {
override fun get(name: String): ObjRecord? {
// closure should be treated below callScope
return super.get(name) ?: closureScope.get(name)
}
}
@@ -17,6 +17,16 @@ class Compiler(
class Settings
private val initStack = mutableListOf<MutableList<Statement>>()
val currentInitScope: MutableList<Statement>
get() =
initStack.lastOrNull() ?: cc.syntaxError("no initialization scope exists here")
private fun pushInitScope(): MutableList<Statement> = mutableListOf<Statement>().also { initStack.add(it) }
private fun popInitScope(): MutableList<Statement> = initStack.removeLast()
private suspend fun parseScript(): Script {
val statements = mutableListOf<Statement>()
val start = cc.currentPos()
@@ -460,7 +470,7 @@ class Compiler(
val callStatement = statement {
// and the source closure of the lambda which might have other thisObj.
val context = AppliedScope(closure!!, args, this)
val context = ClosureScope(this, closure!!) //AppliedScope(closure!!, args, this)
if (argsDeclaration == null) {
// no args: automatic var 'it'
val l = args.list
@@ -823,6 +833,8 @@ class Compiler(
val isExtern = cc.skipId("extern")
when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(Visibility.Private, isExtern, isStatic = true)
cc.matchQualifiers("fun", "static") -> parseFunctionDeclaration(Visibility.Public, isExtern, isStatic = true)
cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
@@ -830,7 +842,21 @@ class Compiler(
cc.matchQualifiers("fun") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("fn") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern)
cc.matchQualifiers("val", "private", "static") -> parseVarDeclaration(
false,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("val", "static") -> parseVarDeclaration(false, Visibility.Public, isStatic = true)
cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private)
cc.matchQualifiers("var", "static") -> parseVarDeclaration(true, Visibility.Public, isStatic = true)
cc.matchQualifiers("var", "static", "private") -> parseVarDeclaration(
true,
Visibility.Private,
isStatic = true
)
cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private)
cc.matchQualifiers("val", "open") -> parseVarDeclaration(false, Visibility.Private, true)
cc.matchQualifiers("var", "open") -> parseVarDeclaration(true, Visibility.Private, true)
@@ -1090,6 +1116,8 @@ class Compiler(
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next()
pushInitScope()
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body
parseScript().also {
@@ -1100,6 +1128,8 @@ class Compiler(
null
}
val initScope = popInitScope()
// create class
val className = nameToken.value
@@ -1131,6 +1161,13 @@ class Compiler(
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
addItem(className, false, newClass)
if (initScope.isNotEmpty()) {
val classScope = copy(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
s.execute(classScope)
.also { println("executed, ${classScope.objects}") }
}
newClass
}
}
@@ -1503,10 +1540,12 @@ class Compiler(
}
}
private suspend fun parseFunctionDeclaration(
private suspend fun
parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false
isExtern: Boolean = false,
isStatic: Boolean = false,
): Statement {
var t = cc.next()
val start = t.pos
@@ -1548,9 +1587,11 @@ class Compiler(
val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start
// restore closure where the function was defined, and making a copy of it
// for local space (otherwise it will write local stuff to closure!)
val context = closure?.copy() ?: callerContext.raiseError("bug: closure not set")
val context = closure?.let { ClosureScope(callerContext, it) }
?: callerContext.raiseError("bug: closure not set")
// load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
@@ -1559,7 +1600,7 @@ class Compiler(
}
fnStatements.execute(context)
}
return statement(start) { context ->
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// for the function
closure = context
@@ -1568,7 +1609,12 @@ class Compiler(
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) {
fnBody.execute(this)
// ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i ->
fnBody.execute(ClosureScope(this, i.instanceScope))
}
// other classes can create one-time scope for this rare case:
?: fnBody.execute(thisObj.autoInstanceScope(this))
}
}
// regular function/method
@@ -1577,6 +1623,11 @@ class Compiler(
// saved the proper context in the closure
fnBody
}
return if (isStatic) {
currentInitScope += fnCreateStatement
NopStatement
} else
fnCreateStatement
}
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {
@@ -1600,7 +1651,8 @@ class Compiler(
private suspend fun parseVarDeclaration(
isMutable: Boolean,
visibility: Visibility,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isStatic: Boolean = false
): Statement {
val nameToken = cc.next()
val start = nameToken.pos
@@ -1622,6 +1674,21 @@ class Compiler(
val initialExpression = if (setNull) null else parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression")
if (isStatic) {
// find objclass instance: this is tricky: this code executes in object initializer,
// when creating instance, but we need to execute it in the class initializer which
// is missing as for now. Add it to the compiler context?
// add there
// return
currentInitScope += statement {
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, pos)
addItem(name, isMutable, initValue, visibility, ObjRecord.Type.Field)
ObjVoid
}
return NopStatement
}
return statement(nameToken.pos) { context ->
if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined")
@@ -49,6 +49,10 @@ class CompilerContext(val tokens: List<Token>) {
throw ScriptError(at, message)
}
fun syntaxError(message: String = "Syntax error"): Nothing {
throw ScriptError(currentPos(), message)
}
fun currentPos(): Pos = tokens[currentIndex].pos
/**
@@ -16,7 +16,7 @@ import net.sergeych.lyng.pacman.ImportProvider
*
* There are special types of scopes:
*
* - [AppliedScope] - scope used to apply a closure to some thisObj scope
* - [ClosureScope] - scope used to apply a closure to some thisObj scope
*/
open class Scope(
val parent: Scope?,
@@ -50,6 +50,9 @@ open class Scope(
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalStateException(this, message))
fun raiseIllegalAssignment(message: String): Nothing =
raiseError(ObjIllegalAssignmentException(this, message))
@Suppress("unused")
fun raiseNoSuchElement(message: String = "No such element"): Nothing =
raiseError(ObjIllegalArgumentException(this, message))
@@ -70,7 +73,7 @@ open class Scope(
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)
return (args.list[index].byValueCopy() as? T)
?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
}
@@ -91,8 +94,15 @@ open class Scope(
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}")
inline fun <reified T : Obj> thisAs(): T {
var s: Scope? = this
do {
val t = s!!.thisObj
if (t is T) return t
s = s.parent
} while(s != null)
raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
}
internal val objects = mutableMapOf<String, ObjRecord>()
@@ -153,13 +163,14 @@ open class Scope(
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource(), currentImportProvider).execute(this)
eval(code.toSource())
suspend fun eval(source: Source): Obj =
Compiler.compile(
suspend fun eval(source: Source): Obj {
return Compiler.compile(
source,
currentImportProvider
).execute(this)
}
fun containsLocal(name: String): Boolean = name in objects
@@ -190,6 +201,25 @@ open class Scope(
val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
override fun toString(): String {
val contents = objects.entries.joinToString { "${if( it.value.isMutable ) "var" else "val" } ${it.key}=${it.value.value}" }
return "S[this=$thisObj $contents]"
}
fun trace(text: String="") {
println("trace Scope: $text ------------------")
var p = this.parent
var level = 0
while (p != null) {
println(" parent#${++level}: $p")
println(" ( ${p.args.list} )")
p = p.parent
}
println("--------------------")
ObjVoid
}
companion object {
fun new(): Scope =
@@ -1,9 +1,12 @@
package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.stdlib_included.rootLyng
import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.*
class Script(
@@ -20,12 +23,23 @@ class Script(
return lastResult
}
suspend fun execute() = execute(defaultImportManager.newModule())
suspend fun execute() = execute(
defaultImportManager.newStdScope()
)
companion object {
private val rootScope: Scope = Scope(null).apply {
suspend fun newScope(pos: Pos = Pos.builtIn) = defaultImportManager.newStdScope(pos)
internal val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this)
addFn("print") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value)
else print(a.asStr.value)
}
ObjVoid
}
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value)
@@ -151,6 +165,26 @@ class Script(
}
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
}
addFn("require") {
val condition = requiredArg<ObjBool>(0)
if( !condition.value ) {
val message = args.list.getOrNull(1)?.toString() ?: "requirement not met"
raiseIllegalArgument(message)
}
ObjVoid
}
addFn("check") {
val condition = requiredArg<ObjBool>(0)
if( !condition.value ) {
val message = args.list.getOrNull(1)?.toString() ?: "check failed"
raiseIllegalState(message)
}
ObjVoid
}
addFn("traceScope") {
this.trace(args.getOrNull(0)?.toString() ?: "")
ObjVoid
}
addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
@@ -173,8 +207,31 @@ class Script(
addConst("Iterable", ObjIterable)
addConst("Collection", ObjCollection)
addConst("Array", ObjArray)
addConst("RingBuffer", ObjRingBuffer.type)
addConst("Class", ObjClassType)
addConst("Deferred", ObjDeferred.type)
addConst("CompletableDeferred", ObjCompletableDeferred.type)
addConst("Mutex", ObjMutex.type)
addFn("launch") {
val callable = requireOnlyArg<Statement>()
ObjDeferred(globalDefer {
callable.execute(this@addFn)
})
}
addFn("yield") {
yield()
ObjVoid
}
addFn("flow") {
// important is: current context contains closure often used in call;
// we'll need it for the producer
ObjFlow(requireOnlyArg<Statement>(), this)
}
val pi = ObjReal(PI)
addConst("π", pi)
getOrCreateNamespace("Math").apply {
@@ -184,6 +241,9 @@ class Script(
val defaultImportManager: ImportManager by lazy {
ImportManager(rootScope, SecurityManager.allowAll).apply {
addTextPackages(
rootLyng
)
addPackage("lyng.buffer") {
it.addConst("Buffer", ObjBuffer.type)
it.addConst("MutableBuffer", ObjMutableBuffer.type)
@@ -14,6 +14,8 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
cause
)
class ScriptFlowIsNoMoreCollected: Exception()
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message)
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
@@ -55,7 +55,8 @@ open class Obj {
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(scope, name, args) as T
): T =
invokeInstanceMethod(scope, name, args) as T
/**
* Invoke a method of the object if exists
@@ -68,7 +69,10 @@ open class Obj {
args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null
): Obj =
objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args)
objClass.getInstanceMemberOrNull(name)?.value?.invoke(
scope,
this,
args)
?: onNotFoundResult
?: scope.raiseSymbolNotFound(name)
@@ -251,6 +255,14 @@ open class Obj {
scope.raiseNotImplemented()
}
fun autoInstanceScope(parent: Scope): Scope {
val scope = parent.copy(newThisObj = this, args = parent.args)
for( m in objClass.members) {
scope.objects[m.key] = m.value
}
return scope
}
companion object {
val rootObjectType = ObjClass("Obj").apply {
@@ -16,6 +16,16 @@ open class ObjClass(
var constructorMeta: ArgsDeclaration? = null
var instanceConstructor: Statement? = null
/**
* the scope for class methods, initialize class vars, etc.
*
* Important notice. When create a user class, e.g. from Lyng source, it should
* be set to a scope by compiler, so it could access local closure, etc. Otherwise,
* it will be initialized to default scope on first necessity, e.g. when used in
* external, kotlin classes with [addClassConst] and [addClassFn], etc.
*/
var classScope: Scope? = null
val allParentsSet: Set<ObjClass> =
parents.flatMap {
listOf(it) + it.allParentsSet
@@ -23,9 +33,10 @@ open class ObjClass(
override val objClass: ObjClass by lazy { ObjClassType }
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
private val classMembers = mutableMapOf<String, ObjRecord>()
/**
* members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass]
*/
internal val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
@@ -40,6 +51,7 @@ open class ObjClass(
return instance
}
fun createField(
name: String,
initialValue: Obj,
@@ -53,6 +65,11 @@ open class ObjClass(
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
private fun initClassScope(): Scope {
if( classScope == null ) classScope = Scope()
return classScope!!
}
fun createClassField(
name: String,
initialValue: Obj,
@@ -60,10 +77,11 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = classMembers[name]
initClassScope()
val existing = classScope!!.objects[name]
if( existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
classMembers[name] = ObjRecord(initialValue, isMutable, visibility)
classScope!!.addItem(name, isMutable, initialValue, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
@@ -91,15 +109,23 @@ open class ObjClass(
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
override suspend fun readField(scope: Scope, name: String): ObjRecord {
classMembers[name]?.let {
return it
classScope?.objects?.get(name)?.let {
if( it.visibility.isPublic ) return it
}
return super.readField(scope, name)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
initClassScope().objects[name]?.let {
if( it.isMutable) it.value = newValue
else scope.raiseIllegalAssignment("can't assign $name is not mutable")
}
?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj {
return classMembers[name]?.value?.invoke(scope, this, args)
return classScope?.objects?.get(name)?.value?.invoke(scope, this, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
@@ -0,0 +1,22 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.lyng.Scope
class ObjCompletableDeferred(val completableDeferred: CompletableDeferred<Obj>): ObjDeferred(completableDeferred) {
override val objClass = type
companion object {
val type = object: ObjClass("CompletableDeferred", ObjDeferred.type){
override suspend fun callOn(scope: Scope): Obj {
return ObjCompletableDeferred(CompletableDeferred())
}
}.apply {
addFn("complete") {
thisAs<ObjCompletableDeferred>().completableDeferred.complete(args.firstAndOnly())
ObjVoid
}
}
}
}
@@ -0,0 +1,32 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.Deferred
import net.sergeych.lyng.Scope
open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
override val objClass = type
companion object {
val type = object: ObjClass("Deferred"){
override suspend fun callOn(scope: Scope): Obj {
scope.raiseError("Deferred constructor is not directly callable")
}
}.apply {
addFn("await") {
thisAs<ObjDeferred>().deferred.await()
}
addFn("isCompleted") {
thisAs<ObjDeferred>().deferred.isCompleted.toObj()
}
addFn("isActive") {
thisAs<ObjDeferred>().deferred.isActive.toObj()
}
addFn("isCancelled") {
thisAs<ObjDeferred>().deferred.isCancelled.toObj()
}
}
}
}
@@ -0,0 +1,136 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ChannelResult
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.lyng.*
import net.sergeych.mp_tools.globalLaunch
import kotlin.coroutines.cancellation.CancellationException
class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
override val objClass = type
companion object {
@OptIn(DelicateCoroutinesApi::class)
val type = object : ObjClass("FlowBuilder") {}.apply {
addFn("emit") {
val data = requireOnlyArg<Obj>()
try {
val channel = thisAs<ObjFlowBuilder>().output
if( !channel.isClosedForSend )
channel.send(data)
else
throw ScriptFlowIsNoMoreCollected()
} catch (x: Exception) {
if( x !is CancellationException )
x.printStackTrace()
throw ScriptFlowIsNoMoreCollected()
}
ObjVoid
}
}
}
}
private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel<Obj> {
val channel = Channel<Obj>(Channel.RENDEZVOUS)
val builder = ObjFlowBuilder(channel)
val builderScope = scope.copy(newThisObj = builder)
globalLaunch {
try {
producer.execute(builderScope)
}
catch(x: ScriptFlowIsNoMoreCollected) {
x.printStackTrace()
// premature flow closing, OK
}
catch(x: Exception) {
x.printStackTrace()
}
channel.close()
}
return channel
}
class ObjFlow(val producer: Statement, val scope: Scope) : Obj() {
override val objClass = type
companion object {
val type = object : ObjClass("Flow", ObjIterable) {
override suspend fun callOn(scope: Scope): Obj {
scope.raiseError("Flow constructor is not available")
}
}.apply {
addFn("iterator") {
val objFlow = thisAs<ObjFlow>()
ObjFlowIterator( statement { objFlow.producer.execute(ClosureScope(this,objFlow.scope)) } )
}
}
}
}
class ObjFlowIterator(val producer: Statement) : Obj() {
override val objClass: ObjClass = type
private var channel: ReceiveChannel<Obj>? = null
private var nextItem: ChannelResult<Obj>? = null
private var isCancelled = false
private fun checkNotCancelled(scope: Scope) {
if( isCancelled )
scope.raiseIllegalState("iteration is cancelled")
}
suspend fun hasNext(scope: Scope): ObjBool {
checkNotCancelled(scope)
// cold start:
if (channel == null) channel = createLyngFlowInput(scope, producer)
if (nextItem == null) nextItem = channel!!.receiveCatching()
return ObjBool(nextItem!!.isSuccess)
}
suspend fun next(scope: Scope): Obj {
checkNotCancelled(scope)
if (hasNext(scope).value == false) scope.raiseIllegalState("iteration is done")
return nextItem!!.getOrThrow().also { nextItem = null }
}
private val access = Mutex()
suspend fun cancel() {
access.withLock {
if (!isCancelled) {
isCancelled = true
channel?.cancel()
}
}
}
companion object {
val type = object : ObjClass("FlowIterator", ObjIterator) {
}.apply {
addFn("hasNext") {
thisAs<ObjFlowIterator>().hasNext(this).toObj()
}
addFn("next") {
val x = thisAs<ObjFlowIterator>()
x.next(this)
}
addFn("cancelIteration") {
val x = thisAs<ObjFlowIterator>()
x.cancel()
ObjVoid
}
}
}
}
@@ -34,7 +34,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
onNotFoundResult: Obj?): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(scope, this, args)
it.value.invoke(
instanceScope,
this,
args)
else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
}
@@ -81,7 +84,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
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]
localVars[i].value = v
}
}
}
@@ -10,8 +10,6 @@ import net.sergeych.lynon.LynonType
*/
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")
@@ -53,7 +53,7 @@ val ObjIterable by lazy {
addFn("toMap") {
val result = ObjMap()
thisObj.toFlow(this).collect { pair ->
result.map[pair.getAt(this,0)] = pair.getAt(this, 1)
result.map[pair.getAt(this, 0)] = pair.getAt(this, 1)
}
result
}
@@ -86,6 +86,24 @@ val ObjIterable by lazy {
ObjList(result)
}
addFn("take") {
var n = requireOnlyArg<ObjInt>().value.toInt()
val result = mutableListOf<Obj>()
if (n > 0) {
thisObj.enumerate(this) {
result += it
--n > 0
}
}
ObjList(result)
}
// addFn("drop" ) {
// var n = requireOnlyArg<ObjInt>().value.toInt()
// if( n < 0 ) raiseIllegalArgument("drop($n): should be positive")
// val it = callMethod<>()
// }
addFn("isEmpty") {
ObjBool(
thisObj.invokeInstanceMethod(this, "iterator")
@@ -93,6 +111,5 @@ val ObjIterable by lazy {
.not()
)
}
}
}
@@ -1,3 +1,4 @@
package net.sergeych.lyng.obj
val ObjIterator by lazy { ObjClass("Iterator") }
@@ -36,7 +36,9 @@ class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
addFn("next") {
thisAs<ObjKotlinObjIterator>().iterator.next()
}
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
addFn("hasNext") {
thisAs<ObjKotlinObjIterator>().iterator.hasNext().toObj()
}
}
}
@@ -56,3 +58,25 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
emit(next.invoke(scope, iterator))
}
}
/**
* Call [callback] for each element of this obj considering it provides [Iterator]
* methods `hasNext` and `next`.
*
* IF callback returns false, iteration is stopped.
*/
suspend fun Obj.enumerate(scope: Scope,callback: suspend (Obj)->Boolean) {
val iterator = invokeInstanceMethod(scope, "iterator")
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
val next = iterator.getInstanceMethod(scope, "next")
var closeIt = false
while (hasNext.invoke(scope, iterator).toBool()) {
val nextValue = next.invoke(scope, iterator)
if( !callback(nextValue) ) {
closeIt = true
break
}
}
if( closeIt )
iterator.invokeInstanceMethod(scope, "cancelIteration", onNotFoundResult = ObjVoid)
}
@@ -0,0 +1,23 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
class ObjMutex(val mutex: Mutex): Obj() {
override val objClass = type
companion object {
val type = object: ObjClass("Mutex") {
override suspend fun callOn(scope: Scope): Obj {
return ObjMutex(Mutex())
}
}.apply {
addFn("withLock") {
val f = requiredArg<Statement>(0)
thisAs<ObjMutex>().mutex.withLock { f.execute(this) }
}
}
}
}
@@ -0,0 +1,91 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
class RingBuffer<T>(val maxSize: Int) : Iterable<T> {
private val data = arrayOfNulls<Any>(maxSize)
var size = 0
private set
private var start = 0
init {
check(maxSize > 0) { "Max size should be a positive number: $maxSize" }
}
fun add(item: T) {
if (size < maxSize)
size++
else
start = (start + 1) % maxSize
data[(start + size - 1) % maxSize] = item
}
@Suppress("unused")
fun addAll(vararg items: T) {
for (i in items) add(i)
}
@Suppress("unused")
fun addAll(elements: Iterable<T>) {
elements.forEach { add(it) }
}
@Suppress("unused")
fun clear() {
start = 0
size = 0
for (i in data.indices) {
data[i] = null
}
}
override fun iterator(): Iterator<T> =
object : Iterator<T> {
private var i = 0
override fun hasNext(): Boolean = i < size
override fun next(): T {
if (!hasNext()) throw NoSuchElementException()
@Suppress("UNCHECKED_CAST")
return data[(start + i++) % maxSize] as T
}
}
}
class ObjRingBuffer(val capacity: Int) : Obj() {
val buffer = RingBuffer<Obj>(capacity)
override val objClass: ObjClass = type
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
buffer.add(other.byValueCopy())
return this
}
companion object {
val type = object : ObjClass("RingBuffer", ObjIterable) {
override suspend fun callOn(scope: Scope): Obj {
return ObjRingBuffer(scope.requireOnlyArg<ObjInt>().toInt())
}
}.apply {
addFn("capacity") {
thisAs<ObjRingBuffer>().capacity.toObj()
}
addFn("size") {
thisAs<ObjRingBuffer>().buffer.size.toObj()
}
addFn("iterator") {
val buffer = thisAs<ObjRingBuffer>().buffer
ObjKotlinObjIterator(buffer.iterator())
}
addFn("add") {
thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) }
}
}
}
}
@@ -93,7 +93,9 @@ data class ObjString(val value: String) : Obj() {
ObjString(decoder.unpackBinaryData().decodeToString())
}.apply {
addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong())
ObjInt(thisAs<ObjString>().value.toLongOrNull()
?: raiseIllegalArgument("can't convert to int: $thisObj")
)
}
addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
@@ -137,7 +139,9 @@ data class ObjString(val value: String) : Obj() {
}
addFn("encodeUtf8") { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble()) }
addFn("toReal") {
ObjReal(thisAs<ObjString>().value.toDouble())
}
}
}
}
@@ -1,6 +1,7 @@
package net.sergeych.lyng.pacman
import net.sergeych.lyng.*
import net.sergeych.mptools.CachedExpression
/**
* Package manager INTERFACE (abstract class). Performs import routines
@@ -45,6 +46,15 @@ abstract class ImportProvider(
fun newModuleAt(pos: Pos): ModuleScope =
ModuleScope(this, pos, "unknown")
private var cachedStdScope = CachedExpression<Scope>()
suspend fun newStdScope(pos: Pos = Pos.builtIn): Scope =
cachedStdScope.get {
newModuleAt(pos).also {
it.eval("import lyng.stdlib\n")
}
}.copy()
}
@@ -2,12 +2,16 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjVoid
fun String.toSource(name: String = "eval"): Source = Source(name, this)
sealed class ObjType {
object Any : ObjType()
object Int : ObjType()
object Void: ObjType()
companion object {
}
}
@@ -62,4 +66,8 @@ fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspe
override suspend fun execute(scope: Scope): Obj = f(scope)
}
object NopStatement: Statement(true, true, ObjType.Void) {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = ObjVoid
}
@@ -0,0 +1,58 @@
package net.sergeych.lyng.stdlib_included
internal val rootLyng = """
package lyng.stdlib
fun Iterable.filter(predicate) {
val list = this
flow {
for( item in list ) {
if( predicate(item) ) {
emit(item)
}
}
}
}
fun Iterable.drop(n) {
var cnt = 0
filter { cnt++ >= n }
}
fun Iterable.first() {
val i = iterator()
if( !i.hasNext() ) throw NoSuchElementException()
i.next()
}
fun Iterable.last() {
var found = false
var element = null
for( i in this ) {
element = i
found = true
}
if( !found ) throw NoSuchElementException()
element
}
fun Iterable.dropLast(n) {
val list = this
val buffer = RingBuffer(n)
flow {
for( item in list ) {
if( buffer.size == n )
emit( buffer.first() )
buffer += item
}
}
}
fun Iterable.takeLast(n) {
val list = this
val buffer = RingBuffer(n)
for( item in list ) buffer += item
buffer
}
""".trimIndent()
@@ -0,0 +1,16 @@
fun Iterable.filter( predicate ) {
flow {
for( item in this )
if( predicate(item) )
emit(item)
}
}
fun Iterable.drop(n) {
require( n >= 0, "drop amount must be non-negative")
var count = 0
filter {
count++ < N
}
}
@@ -0,0 +1,171 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class TestCoroutines {
@Test
fun testLaunch() = runTest {
eval(
"""
var passed = false
val x = launch {
delay(10)
passed = true
"ok"
}
assert(!passed)
assertEquals( x.await(), "ok")
assert(passed)
assert(x.isCompleted)
""".trimIndent()
)
}
@Test
fun testCompletableDeferred() = runTest {
eval(
"""
val done = CompletableDeferred()
launch {
delay(30)
done.complete("ok")
}
assert(!done.isCompleted)
assert(done.isActive)
assertEquals( done.await(), "ok")
assert(done.isCompleted)
""".trimIndent()
)
}
@Test
fun testMutex() = runTest {
eval(
"""
var counter = 0
val mutex = Mutex()
(1..4).map {
launch {
// mutex.withLock {
val c = counter
delay(5)
counter = c + 1
// }
}
}.forEach { it.await() }
println(counter)
assert( counter < 10 )
""".trimIndent()
)
}
@Test
fun testFlows() = runTest {
eval("""
val f = flow {
println("Starting generator")
var n1 = 0
var n2 = 1
emit(n1)
emit(n2)
while(true) {
val n = n1 + n2
emit(n)
n1 = n2
n2 = n
}
}
val correctFibs = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
assertEquals( correctFibs, f.take(correctFibs.size))
""".trimIndent())
}
@Test
fun testFlow2() = runTest {
eval("""
val f = flow {
emit("start")
emit("start2")
(1..4).forEach {
emit(it)
}
}
// let's collect flow:
val result = []
for( x in f ) result += x
println(result)
// let's collect it once again:
assertEquals( result, f.toList())
assertEquals( result, f.toList())
""".trimIndent())
}
@Test
fun testFlowClosures() = runTest {
eval("""
fun filter( a, b ) {
println("filter: %s, %s"(a,b))
flow {
emit(a)
emit(b)
}
}
assertEquals( [5, 1], filter(5,1).toList() )
assertEquals( [2, 3], filter(2,3).toList() )
""".trimIndent())
}
@Test
fun testFilterFlow() = runTest {
eval("""
fun filter( list, predicate ) {
val p = predicate
println("predicate "+predicate+" / "+p)
flow {
// here p is captured only once and does not change!
for( item in list ) {
print("filter "+p+" "+item+": ")
if( p(item) ) {
println("OK")
emit(item)
}
else println("NO")
}
}
}
// fun drop(i, n) {
// require( n >= 0, "drop amount must be non-negative")
// var count = 0
// println("drop %d"(n))
// filter(i) {
// count++ >= n
// }
// }
val src = (1..1).toList()
assertEquals( 1, filter(src) { true }.toList().size )
println("----------------------------------------------------------")
println("----------------------------------------------------------")
println("----------------------------------------------------------")
println("----------------------------------------------------------")
assertEquals( 0, filter(src) { false }.toList().size )
// assertEquals( 3, filter(src) { true }.size() )
// assertEquals( [7,8], drop((1..8).toList(),6).toList())
// assertEquals( [1,3,5,7], filter((1..8).toList()) {
// println("call2")
// it % 2 == 1
// }.toList())
""".trimIndent())
}
}
+46
View File
@@ -0,0 +1,46 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class OOTest {
@Test
fun testClassProps() = runTest {
eval("""
import lyng.time
class Point(x,y) {
static val origin = Point(0,0)
static var center = origin
}
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(0,0), Point.center)
Point.center = Point(1,2)
assertEquals(Point(0,0), Point.origin)
assertEquals(Point(1,2), Point.center)
""".trimIndent())
}
@Test
fun testClassMethods() = runTest {
eval("""
import lyng.time
class Point(x,y) {
private static var data = null
static fun getData() { data }
static fun setData(value) {
data = value
callFrom()
}
static fun callFrom() {
data = data + "!"
}
}
assertEquals(Point(0,0), Point(0,0) )
assertEquals(null, Point.getData() )
Point.setData("foo")
assertEquals( "foo!", Point.getData() )
""".trimIndent())
}
}
+154 -25
View File
@@ -1274,6 +1274,106 @@ class ScriptTest {
)
}
@Test
fun testCaptureLocals() = runTest {
eval(
"""
fun outer(prefix) {
val p1 = "0" + prefix
{
p1 + "2" + it
}
}
fun outer2(prefix) {
val p1 = "*" + prefix
{
p1 + "2" + it
}
}
val x = outer("1")
val y = outer2("1")
println(x("!"))
assertEquals( "0123", x("3") )
assertEquals( "*123", y("3") )
""".trimIndent()
)
}
@Test
fun testInstanceCallScopeIsCorrect() = runTest {
eval(
"""
val prefix = ":"
class T(text) {
fun getText() {
println(text)
prefix + text + "!"
}
}
val text = "invalid"
val t1 = T("foo")
val t2 = T("bar")
// get inside the block
for( i in 1..3 ) {
assertEquals( "foo", t1.text )
assertEquals( ":foo!", t1.getText() )
assertEquals( "bar", t2.text )
assertEquals( ":bar!", t2.getText() )
}
""".trimIndent()
)
}
@Test
fun testAppliedScopes() = runTest {
eval(
"""
class T(text) {
fun getText() {
println(text)
text + "!"
}
}
val prefix = ":"
val lambda = {
prefix + getText() + "!"
}
val text = "invalid"
val t1 = T("foo")
val t2 = T("bar")
t1.apply {
// it must take "text" from class t1:
assertEquals("foo", text)
assertEquals( "foo!", getText() )
assertEquals( ":foo!!", lambda() )
}
t2.apply {
assertEquals("bar", text)
assertEquals( "bar!", getText() )
assertEquals( ":bar!!", lambda() )
}
// worst case: names clash
fun badOne() {
val prefix = "&"
t1.apply {
assertEquals( ":foo!!", lambda() )
}
}
badOne()
""".trimIndent()
)
}
@Test
fun testLambdaWithArgsEllipsis() = runTest {
eval(
@@ -2244,10 +2344,12 @@ class ScriptTest {
@Test
fun testSet2() = runTest {
eval("""
eval(
"""
assertEquals( Set( ...[1,2,3]), Set(1,2,3) )
assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) )
""".trimIndent())
""".trimIndent()
)
}
@Test
@@ -2284,9 +2386,11 @@ class ScriptTest {
eval(
"""
class Point(x,y)
// see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply {
this.x++; this.y++
this.x++
y++
}
assertEquals(p, Point(2,3))
>>> void
@@ -2305,6 +2409,10 @@ class ScriptTest {
}
fun Object.isInteger() {
println(this)
println(this is Int)
println(this is Real)
println(this is String)
when(this) {
is Int -> true
is Real -> toInt() == this
@@ -2319,7 +2427,7 @@ class ScriptTest {
assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false )
assert( "5".isInteger() )
assert( ! "5.2".isInteger() )
assert( !"5.2".isInteger() )
""".trimIndent()
)
}
@@ -2446,20 +2554,26 @@ class ScriptTest {
fun testDefaultImportManager() = runTest {
val scope = Scope.new()
assertFails {
scope.eval("""
scope.eval(
"""
import foo
foo()
""".trimIndent())
""".trimIndent()
)
}
scope.importManager.addTextPackages("""
scope.importManager.addTextPackages(
"""
package foo
fun foo() { "bar" }
""".trimIndent())
scope.eval("""
""".trimIndent()
)
scope.eval(
"""
import foo
assertEquals( "bar", foo())
""".trimIndent())
""".trimIndent()
)
}
@Test
@@ -2478,7 +2592,8 @@ class ScriptTest {
@Test
fun testBuffer() = runTest {
eval("""
eval(
"""
import lyng.buffer
assertEquals( 0, Buffer().size )
@@ -2497,12 +2612,14 @@ class ScriptTest {
assertEquals(101, buffer[2])
assertEquals("Heelo", buffer.decodeUtf8())
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testBufferCompare() = runTest {
eval("""
eval(
"""
import lyng.buffer
println("Hello".characters())
@@ -2520,7 +2637,8 @@ class ScriptTest {
assertEquals("foo", map[b2])
assertEquals(null, map[b3])
""".trimIndent())
""".trimIndent()
)
}
@Test
@@ -2548,6 +2666,7 @@ class ScriptTest {
)
delay(1000)
}
@Test
fun testTimeStatics() = runTest {
eval(
@@ -2589,7 +2708,8 @@ class ScriptTest {
println(Script.defaultImportManager.packageNames)
println(s.importManager.packageNames)
s.importManager.addTextPackages("""
s.importManager.addTextPackages(
"""
package foo
import lyng.time
@@ -2597,8 +2717,10 @@ class ScriptTest {
fun foo() {
println("foo: %s"(Instant()))
}
""".trimIndent())
s.importManager.addTextPackages("""
""".trimIndent()
)
s.importManager.addTextPackages(
"""
package bar
import lyng.time
@@ -2606,24 +2728,28 @@ class ScriptTest {
fun bar() {
println("bar: %s"(Instant()))
}
""".trimIndent())
""".trimIndent()
)
println(s.importManager.packageNames)
s.eval("""
s.eval(
"""
import foo
import bar
foo()
bar()
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testIndexIntIncrements() = runTest {
eval("""
eval(
"""
val x = [1,2,3]
x[1]++
++x[0]
@@ -2636,12 +2762,14 @@ class ScriptTest {
assert( b == Buffer(1,3,3) )
++b[0]
assertEquals( b, Buffer(2,3,3) )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testIndexIntDecrements() = runTest {
eval("""
eval(
"""
val x = [1,2,3]
x[1]--
--x[0]
@@ -2654,13 +2782,14 @@ class ScriptTest {
assert( b == Buffer(1,1,3) )
--b[0]
assertEquals( b, Buffer(0,1,3) )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testRangeToList() = runTest {
val x = eval("""(1..10).toList()""") as ObjList
assertEquals(listOf(1,2,3,4,5,6,7,8,9,10), x.list.map { it.toInt() })
assertEquals(listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), x.list.map { it.toInt() })
val y = eval("""(-2..3).toList()""") as ObjList
println(y.list)
}
@@ -0,0 +1,65 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class StdlibTest {
@Test
fun testIterableFilter() = runTest {
eval("""
assertEquals([1,3,5,7], (1..8).filter{ it % 2 == 1 }.toList() )
assertEquals([2,4,6,8], (1..8).filter{ it % 2 == 0 }.toList() )
""".trimIndent())
}
@Test
fun testFirstLast() = runTest {
eval("""
assertEquals(1, (1..8).first )
assertEquals(8, (1..8).last )
""".trimIndent())
}
@Test
fun testTake() = runTest {
eval("""
assertEquals([1,2,3], (1..8).take(3).toList() )
assertEquals([7,8], (1..8).takeLast(2).toList() )
""".trimIndent())
}
@Test
fun testRingBuffer() = runTest {
eval("""
val r = RingBuffer(3)
assert( r is RingBuffer )
assertEquals(0, r.size)
assertEquals(3, r.capacity)
r += 10
assertEquals(1, r.size)
assertEquals(10, r.first)
r += 20
assertEquals(2, r.size)
assertEquals( [10, 20], r.toList() )
r += 30
assertEquals(3, r.size)
assertEquals( [10, 20, 30], r.toList() )
r += 40
assertEquals(3, r.size)
assertEquals( [20, 30, 40], r.toList() )
""".trimIndent())
}
@Test
fun testDrop() = runTest {
eval("""
assertEquals([7,8], (1..8).drop(6).toList() )
assertEquals([1,2], (1..8).dropLast(6).toList() )
""".trimIndent())
}
}
+13 -1
View File
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Script
import net.sergeych.lyng.obj.ObjVoid
import java.nio.file.Files
import java.nio.file.Files.readAllLines
@@ -163,9 +164,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
}
.flowOn(Dispatchers.IO)
suspend fun DocTest.test(scope: Scope = Scope()) {
suspend fun DocTest.test(_scope: Scope? = null) {
val collectedOutput = StringBuilder()
val currentTest = this
val scope = _scope ?: Script.newScope()
scope.apply {
addFn("println") {
if( bookMode ) {
@@ -299,4 +301,14 @@ class BookTest {
runDocTests("../docs/time.md")
}
@Test
fun testParallelismBook() = runBlocking {
runDocTests("../docs/parallelism.md")
}
@Test
fun testRingBuffer() = runBlocking {
runDocTests("../docs/RingBuffer.md")
}
}