Compare commits

..
20 Commits
Author SHA1 Message Date
sergeych 804087f16d version bump 2025-08-12 00:16:09 +03:00
sergeych 6d8eed7b8c added "dynamic" fields (get/set/call fields by name using dynamic standard function 2025-08-12 00:15:45 +03:00
sergeych e916d9805a correct end-block token processing (detect more errors) 2025-08-11 22:48:43 +03:00
sergeych c398496ee0 refining the readme/2 2025-08-11 14:41:35 +03:00
sergeych 299738cffd refining the readme 2025-08-11 10:36:29 +03:00
sergeych 62461c09cc readme fix 2025-08-10 23:41:57 +03:00
sergeych 63de82393a multiline string literals 2025-08-10 23:32:03 +03:00
sergeych ed0a21cb06 added test config 2025-08-10 09:51:04 +03:00
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
44 changed files with 1774 additions and 211 deletions

No files matched your search

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tests in 'lyng.lynglib.jvmTest'" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":lynglib:cleanJvmTest" />
<option value=":lynglib:jvmTest" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
</component>
+34 -18
View File
@@ -2,7 +2,7 @@
A KMP library and a standalone interpreter A KMP library and a standalone interpreter
- simple, compact, intuitive and elegant modern code style: - simple, compact, intuitive and elegant modern code:
``` ```
class Point(x,y) { class Point(x,y) {
@@ -15,23 +15,31 @@ fun swapEnds(first, args..., last, f) {
} }
``` ```
- extremely simple Kotlin integration on any platform - extremely simple Kotlin integration on any platform (JVM, JS, WasmJS, Lunux, MacOS, iOS, Windows)
- 100% secure: no access to any API you didn't explicitly provide - 100% secure: no access to any API you didn't explicitly provide
- 100% coroutines! Every function/script is a coroutine, it does not block the thread, no async/await/suspend keyword garbage: - 100% coroutines! Every function/script is a coroutine, it does not block the thread, no async/await/suspend keyword garbage, see [parallelism]
``` ```
val deferred = launch {
delay(1.5) // coroutine is delayed for 1.5s, thread is not blocked! delay(1.5) // coroutine is delayed for 1.5s, thread is not blocked!
"done"
}
// ...
// suspend current coroutine, no thread is blocked again,
// and wait for deferred to return something:
assertEquals("donw", deferred.await())
``` ```
and it is multithreaded on platforms supporting it (automatically, no code changes required, just 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. - 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 )`. - Any Unicode letters can be used as identifiers: `assert( sin(π/2) == 1 )`.
## Resources: ## Resources:
- [introduction and tutorial](docs/tutorial.md) - start here please - [introduction and tutorial](docs/tutorial.md) - start here please
- [Samples directory](docs/samples) - [Samples directory](docs/samples)
- [Books directory](docs)
## Integration in Kotlin multiplatform ## Integration in Kotlin multiplatform
@@ -84,8 +92,8 @@ import com.sun.source.tree.Scope
import new.sergeych.lyng.* import new.sergeych.lyng.*
// simple function // simple function
val scope = Scope().apply { val scope = Script.newScope().apply {
addFn("addArgs") { addFn("sumOf") {
var sum = 0.0 var sum = 0.0
for (a in args) sum += a.toDouble() for (a in args) sum += a.toDouble()
ObjReal(sum) ObjReal(sum)
@@ -96,12 +104,14 @@ val scope = Scope().apply {
// suspend fun doSomeWork(text: String): Int // suspend fun doSomeWork(text: String): Int
addFn("doSomeWork") { addFn("doSomeWork") {
// this _is_ a suspend lambda, we can call suspend function, // this _is_ a suspend lambda, we can call suspend function,
// and it won't consume the thread: // and it won't consume the thread.
doSomeWork(args[0].toString()).toObj() // note that in kotlin handler, `args` is a list of `Obj` arguments
// and return value from this lambda should be Obj too:
doSomeWork(args[0]).toObj()
} }
} }
// adding constant: // adding constant:
scope.eval("addArgs(1,2,3)") // <- 6 scope.eval("sumOf(1,2,3)") // <- 6
``` ```
Note that the scope stores all changes in it so you can make calls on a single scope 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.
@@ -119,7 +129,7 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
- Javascript, WasmJS, native, JVM, android - batteries included. - Javascript, WasmJS, native, JVM, android - batteries included.
- dynamic types in most elegant and concise way - dynamic types in most elegant and concise way
- async, 100% coroutines, supports multiple cores where platofrm supports thread - async, 100% coroutines, supports multiple cores where platform supports thread
- good for functional an object-oriented style - good for functional an object-oriented style
# Language Roadmap # Language Roadmap
@@ -139,15 +149,19 @@ Ready features:
- [x] exception handling: throw, try-catch-finally, exception classes. - [x] exception handling: throw, try-catch-finally, exception classes.
- [x] multiplatform maven publication - [x] multiplatform maven publication
- [x] documentation for the current state - [x] documentation for the current state
- [x] maps, sets and sequences (flows?)
- [x] modules
- [x] string formatting and tools
- [x] launch, deferred, CompletableDeferred, Mutex, etc.
- [x] multiline strings
- [x] typesafe bit-effective serialization
- [x] compression/decompression (integrated in serialization)
- [x] dynamic fields
Under way: ### Under way:
- [ ] maps, sets and sequences (flows?)
- [ ] regular exceptions - [ ] regular exceptions
- [ ] modules
- [ ] string interpolation and more string tools
- [ ] multiple inheritance for user classes - [ ] multiple inheritance for user classes
- [ ] launch, deferred, coroutineScope, mutex, etc.
- [ ] site with integrated interpreter to give a try - [ ] site with integrated interpreter to give a try
- [ ] kotlin part public API good docs, integration focused - [ ] kotlin part public API good docs, integration focused
- [ ] better stack reporting - [ ] better stack reporting
@@ -158,10 +172,12 @@ Planned features.
- [ ] type specifications - [ ] type specifications
- [ ] source docs and maybe lyng.md to a standard - [ ] source docs and maybe lyng.md to a standard
- [ ] moacro-style kotlin integration or something else to simplify it - [ ] metadata first class access from lyng
Further Further
- [ ] client with GUI support based on compose multiplatform somehow - [ ] client with GUI support based on compose multiplatform somehow
- [ ] notebook - style workbooks with graphs, formulaes, etc. - [ ] notebook - style workbooks with graphs, formulae, etc.
- [ ] language server or compose-based lyng-aware editor - [ ] language server or compose-based lyng-aware editor
[parallelism]: docs/parallelism.md
+27 -4
View File
@@ -1,5 +1,11 @@
# Iterable interface # 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_: Iterable is a class that provides function that creates _the iterator_:
class Iterable { 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. 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 | | 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 | | 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 | | 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 | | 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.toList(): List
fun Iterable.toSet(): Set 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 fun Iterable.associateBy( keyMaker: (Any?)->Any): Map
## Abstract methods ## Abstract methods:
fun iterator(): Iterator 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: ## Implemented in classes:
- [List], [Range] - [List], [Range], [Buffer](Buffer.md), [BitBuffer], [Buffer], [Set], [RingBuffer]
[List]: List.md [List]: List.md
[Range]: Range.md [Range]: Range.md
[Set]: Set.md
[RingBuffer]: RingBuffer.md
+57
View File
@@ -235,6 +235,63 @@ as they are modifying the type, not the context.
Beware of it. We might need to reconsider it later. Beware of it. We might need to reconsider it later.
## dynamic symbols
Sometimes it is convenient to provide methods and variables whose names are not known at compile time. For example, it could be external interfaces not known to library code, user-defined data fields, etc. You can use `dynamic` function to create such:
// val only dynamic object
val accessor = dynamic {
// all symbol reads are redirected here:
get { name ->
// lets provide one dynamic symbol:
if( name == "foo" ) "bar" else null
// consider also throw SymbolNotDefinedException
}
}
// now we can access dynamic "fields" of accessor:
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
>>> void
The same we can provide writable dynamic fields (var-type), adding set method:
// store one dynamic field here
var storedValueForBar = null
// create dynamic object with 2 fields:
val accessor = dynamic {
get { name ->
when(name) {
// constant field
"foo" -> "bar"
// mutable field
"bar" -> setValueForBar
else -> throw SymbolNotFoundException()
}
}
set { name, value ->
// only 'bar' is mutable:
if( name == "bar" )
storedValueForBar = value
// the rest is immotable. consider throw also
// SymbolNotFoundException when needed.
else throw IllegalAssignmentException("Can't assign "+name)
}
}
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
accessor.bar = "buzz"
assertEquals("buzz", accessor.bar)
assertThrows {
accessor.bad = "!23"
}
Of course, you can return any object from dynamic fields; returning lambdas let create _dynamic methods_ - the callable method. It is very convenient to implement libraries with dynamic remote interfaces, etc.
# Theory # Theory
## Basic principles: ## Basic principles:
+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 # Advanced topics
__See also:__ [parallelism].
## Closures/scopes isolation ## Closures/scopes isolation
Each block has own scope, in which it can safely use closures and override 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 >>> 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
+3 -2
View File
@@ -48,6 +48,7 @@ $$ \ln(1+x)=x-{\dfrac {x^{2}}{2}}+{\dfrac {x^{3}}{3}}-\cdots =\sum \limits _{n=0
} }
assert( почти_равны( 0.0005, 0.000501 ) ) assert( почти_равны( 0.0005, 0.000501 ) )
Во многих случаях вычисление $n+1$ члена значительно проще cчитается от предыдущего члена, в нашем случае это можно было бы записать через итератор, что мы вскоре добавим. Во многих случаях вычисление $n+1$ члена значительно проще cчитается от предыдущего члена, в нашем случае это можно было бы записать через итератор, см [Iterable] и `flow` в [parallelism].
(продолжение следует) [Iterable]: ../Iterable.md
[parallelism]: ../parallelism.md
+95 -54
View File
@@ -14,8 +14,11 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md) - [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md) - [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md) - [math in Lyng](math.md)
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md) - [time](time.md) and [parallelism](parallelism.md)
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples) - [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 # Expressions
@@ -170,7 +173,8 @@ allow to improve code look and readability. There are borrowed from Kotlin:
### let ### 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: 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 // 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) assert( [11, 21, 31] == mapped)
>>> void >>> void
# Lists (aka arrays) # Lists (aka arrays)
Lyng has built-in mutable array class `List` with simple literals: 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
>>> 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] var list = [1, 2]
val other = [3, 4] val other = [3, 4]
@@ -562,7 +566,6 @@ Use `list.add` to avoid confusion:
assert( list == [1, 2, [3, 4], (10..12)]) assert( list == [1, 2, [3, 4], (10..12)])
>>> void >>> void
To add elements to the list: To add elements to the list:
val x = [1,2] val x = [1,2]
@@ -595,7 +598,6 @@ Using splat arguments can simplify inserting list in list:
x x
>>> [1, 0, 100, 0, 2, 3] >>> [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: 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] val x = [1,2,3]
@@ -741,7 +743,9 @@ Also, you can check the type too:
#### Contains: #### 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`): 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 : Iterable is not the container as it can be infinite
(2) (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( "more" in "a".."z") // string range ok
assert( 'x' !in "a".."z") // char in string range: probably error 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 >>> void
(3) (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 So we recommend not to mix characters and string ranges; use `ch in str` that works
as expected: 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: " + countEven
>>> "found even numbers: 5" >>> "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 count = 0
var total = 0 var total = 0
@@ -890,21 +898,22 @@ test function (remember function return it's last expression result):
```mermaid ```mermaid
flowchart TD flowchart TD
S((start)) --> Cond{check} S((start)) --> Cond{check}
Cond--false, no else--->V((void)) Cond -- false, no else ---> V((void))
Cond--true-->E(["last = loop_body()" ]) Cond -- true --> E(["last = loop_body()"])
E--break value---->BV((value)) E -- break value ----> BV((value))
E--> Check2{check} E --> Check2{check}
E--break---->V E -- break ----> V
Check2 --false-->E Check2 -- false --> E
Check2 --true, no else--->L((last)) Check2 -- true, no else ---> L((last))
Check2 --true, else-->Else(["else_clause()"]) Check2 -- true, else --> Else(["else_clause()"])
Cond--false, else--->Else Cond -- false, else ---> Else
Else --> Ele4$nr((else)) Else --> Ele4$nr((else))
``` ```
So the returned value, as seen from diagram could be one of: 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 `break value' statement
- value returned from the `else` clause, of the loop was not broken - 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. - 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 ) } while( continueLoop )
>>> "OK" >>> "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" } fun readLine() { "done: result" }
val result = do { 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. Suppose readLine() here reads some stream of lines.
## For loops ## For loops
For loop are intended to traverse collections, and all other objects that supports For loop are intended to traverse collections, and all other objects that supports
@@ -982,7 +991,8 @@ We can use labels too:
# Exception handling # 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 error = "not caught"
var finallyCaught = false var finallyCaught = false
@@ -1035,7 +1045,6 @@ many more.
- see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference. - see [exception handling](exceptions_handling.md) for detailed exceptions tutorial and reference.
# Self-assignments in expression # Self-assignments in expression
There are auto-increments and auto-decrements: 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 | | code | Int | Unicode code for the character |
| | | | | | | |
## String details ## String details
Strings are arrays of Unicode characters. It can be indexed, and indexing will 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) ) assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) )
>>> void >>> 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. This list will be extended.
@@ -1217,33 +1228,30 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
Typical set of String functions includes: Typical set of String functions includes:
| fun/prop | description / notes | | fun/prop | description / notes |
|-------------------|------------------------------------------------------------| |--------------------|------------------------------------------------------------|
| lower() | change case to unicode upper | | lower() | change case to unicode upper |
| upper() | change case to unicode lower | | upper() | change case to unicode lower |
| startsWith(prefix) | true if starts with a prefix | | startsWith(prefix) | true if starts with a prefix |
| endsWith(prefix) | true if ends with a prefix | | endsWith(prefix) | true if ends with a prefix |
| take(n) | get a new string from up to n first characters | | take(n) | get a new string from up to n first characters |
| takeLast(n) | get a new string from up to n last characters | | takeLast(n) | get a new string from up to n last characters |
| drop(n) | get a new string dropping n first chars, or empty string | | drop(n) | get a new string dropping n first chars, or empty string |
| dropLast(n) | get a new string dropping n last chars, or empty string | | dropLast(n) | get a new string dropping n last chars, or empty string |
| size | size in characters like `length` because String is [Array] | | size | size in characters like `length` because String is [Array] |
| (args...) | sprintf-like formatting, see [string formatting] | | (args...) | sprintf-like formatting, see [string formatting] |
| [index] | character at index | | [index] | character at index |
| [Range] | substring at range | | [Range] | substring at range |
| s1 + s2 | concatenation | | s1 + s2 | concatenation |
| s1 += s2 | self-modifying concatenation | | s1 += s2 | self-modifying concatenation |
| toReal() | attempts to parse string as a Real value | | toReal() | attempts to parse string as a Real value |
| toInt() | parse string to Int value | | toInt() | parse string to Int value |
| characters() | create [List] of characters (1) | | characters() | create [List] of characters (1) |
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 | | encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
(1) (1)
: List is mutable therefore a new copy is created on each call. : List is mutable therefore a new copy is created on each call.
### Literals ### Literals
String literal could be multiline: String literal could be multiline:
@@ -1251,16 +1259,36 @@ String literal could be multiline:
"Hello "Hello
World" World"
though multiline literals is yet work in progress. In this case, it will be passed literally ot "hello\n World". But, if there are
several lines with common left indent, it will be removed, also, forst and last lines,
if blank, will be removed too, for example:
println("
This is a multiline text.
This is a second line.
")
>>> This is a multiline text.
>>> This is a second line.
>>> void
- as expected, empty lines and common indent were removed. It is much like kotlin's `""" ... """.trimIndent()` technique, but simpler ;)
# Built-in functions # Built-in functions
See [math functions](math.md). Other general purpose functions are: See [math functions](math.md). Other general purpose functions are:
| name | description | | name | description |
|----------------------------------------------|----------------------------------------------------------| |--------------------------------------------|-----------------------------------------------------------|
| assert(condition,message="assertion failed") | runtime code check. There will be an option to skip them | | 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 # Built-in constants
@@ -1270,12 +1298,25 @@ See [math functions](math.md). Other general purpose functions are:
| π | See [math](math.md) | | π | See [math](math.md) |
[List]: List.md [List]: List.md
[Iterable]: Iterable.md [Iterable]: Iterable.md
[Iterator]: Iterator.md [Iterator]: Iterator.md
[Real]: Real.md [Real]: Real.md
[Range]: Range.md [Range]: Range.md
[String]: String.md [String]: String.md
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary [string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
[Set]: Set.md [Set]: Set.md
[Map]: Map.md [Map]: Map.md
[Buffer]: Buffer.md [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" kotlin = "2.1.21"
android-minSdk = "24" android-minSdk = "24"
android-compileSdk = "34" android-compileSdk = "34"
kotlinx-coroutines = "1.10.1" kotlinx-coroutines = "1.9.0"
mp_bintools = "0.1.12" mp_bintools = "0.1.12"
firebaseCrashlyticsBuildtools = "3.0.3" firebaseCrashlyticsBuildtools = "3.0.3"
okioVersion = "3.10.2" okioVersion = "3.10.2"
+39 -36
View File
@@ -10,10 +10,11 @@ import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.LyngVersion import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Scope import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.mp_tools.globalDefer
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import okio.SYSTEM import okio.SYSTEM
@@ -38,14 +39,13 @@ data class CommandResult(
val error: String val error: String
) )
val baseScope = Scope().apply { val baseScopeDefer = globalDefer {
addFn("exit") { Script.newScope().apply {
exit(requireOnlyArg<ObjInt>().toInt()) addFn("exit") {
ObjVoid exit(requireOnlyArg<ObjInt>().toInt())
ObjVoid
}
} }
// ObjString.type.addFn("shell") {
//
// }
} }
fun runMain(args: Array<String>) { fun runMain(args: Array<String>) {
@@ -88,41 +88,44 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
""".trimIndent() """.trimIndent()
override fun run() { override fun run() {
when { runBlocking {
version -> { val baseScope = baseScopeDefer.await()
println("Lyng language version ${LyngVersion}") when {
} version -> {
println("Lyng language version ${LyngVersion}")
}
execute != null -> { execute != null -> {
val objargs = mutableListOf<String>() val objargs = mutableListOf<String>()
script?.let { objargs += it } script?.let { objargs += it }
objargs += args objargs += args
baseScope.addConst( baseScope.addConst(
"ARGV", ObjList( "ARGV", ObjList(
objargs.map { ObjString(it) }.toMutableList() objargs.map { ObjString(it) }.toMutableList()
)
) )
) launcher {
launcher { // there is no script name, it is a first argument instead:
// there is no script name, it is a first argument instead: processErrors {
processErrors { baseScope.eval(execute!!)
baseScope.eval(execute!!) }
} }
} }
}
else -> { else -> {
if (script == null) { if (script == null) {
println( println(
""" """
Error: no script specified. Error: no script specified.
""".trimIndent() """.trimIndent()
) )
echoFormattedHelp() echoFormattedHelp()
} else { } else {
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList())) baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
launcher { executeFile(script!!) } launcher { executeFile(script!!) }
}
} }
} }
} }
@@ -131,7 +134,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
fun executeFileWithArgs(fileName: String, args: List<String>) { fun executeFileWithArgs(fileName: String, args: List<String>) {
runBlocking { runBlocking {
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList())) baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
executeFile(fileName) executeFile(fileName)
} }
} }
@@ -148,7 +151,7 @@ suspend fun executeFile(fileName: String) {
text = text.substring(pos + 1) text = text.substring(pos + 1)
} }
processErrors { 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 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "0.8.1-SNAPSHOT" version = "0.8.6-SNAPSHOT"
buildscript { buildscript {
repositories { repositories {
@@ -65,6 +65,7 @@ kotlin {
languageSettings.optIn("kotlin.contracts.ExperimentalContracts") languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi") languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
} }
val commonMain by getting { 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 defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) { ) {
fun assign(a: Item, value: Obj) { 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, a.visibility ?: defaultVisibility,
recordType = defaultRecordType) recordType = defaultRecordType)
} }
@@ -36,7 +36,7 @@ data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) :
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") 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() } fun inspect(): String = list.joinToString(", ") { it.inspect() }
companion object { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList()) fun from(values: Collection<Obj>) = Arguments(values.toList())
@@ -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)
}
}
@@ -65,9 +65,17 @@ class Compiler(
} }
} }
} }
parseStatement(braceMeansLambda = true)?.also { val s = parseStatement(braceMeansLambda = true)?.also {
statements += it statements += it
} ?: break }
if (s == null) {
when( t.type ) {
Token.Type.RBRACE, Token.Type.EOF, Token.Type.SEMICOLON -> {}
else ->
throw ScriptError(t.pos, "unexpeced `${t.value}` here")
}
break
}
} while (true) } while (true)
return Script(start, statements)//returnScope.needCatch) return Script(start, statements)//returnScope.needCatch)
@@ -470,7 +478,7 @@ class Compiler(
val callStatement = statement { val callStatement = statement {
// and the source closure of the lambda which might have other thisObj. // 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) { if (argsDeclaration == null) {
// no args: automatic var 'it' // no args: automatic var 'it'
val l = args.list val l = args.list
@@ -833,8 +841,18 @@ class Compiler(
val isExtern = cc.skipId("extern") val isExtern = cc.skipId("extern")
when { when {
cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern) cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(Visibility.Private, isExtern, isStatic = true) cc.matchQualifiers("fun", "private", "static") -> parseFunctionDeclaration(
cc.matchQualifiers("fun", "static") -> parseFunctionDeclaration(Visibility.Public, isExtern, isStatic = true) 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("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern)
cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern)
@@ -1540,7 +1558,8 @@ class Compiler(
} }
} }
private suspend fun parseFunctionDeclaration( private suspend fun
parseFunctionDeclaration(
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false, isExtern: Boolean = false,
@@ -1586,9 +1605,11 @@ class Compiler(
val fnBody = statement(t.pos) { callerContext -> val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start callerContext.pos = start
// restore closure where the function was defined, and making a copy of it // restore closure where the function was defined, and making a copy of it
// for local space (otherwise it will write local stuff to closure!) // 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 // load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
@@ -1597,7 +1618,7 @@ class Compiler(
} }
fnStatements.execute(context) fnStatements.execute(context)
} }
val fnCreatestatement = statement(start) { context -> val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure // we added fn in the context. now we must save closure
// for the function // for the function
closure = context closure = context
@@ -1606,7 +1627,12 @@ class Compiler(
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance") if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) { 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 // regular function/method
@@ -1616,10 +1642,10 @@ class Compiler(
fnBody fnBody
} }
return if (isStatic) { return if (isStatic) {
currentInitScope += fnCreatestatement currentInitScope += fnCreateStatement
NopStatement NopStatement
} else } else
fnCreatestatement fnCreateStatement
} }
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement { private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {
@@ -375,6 +375,32 @@ private class Parser(fromPos: Pos) {
private val currentChar: Char get() = pos.currentChar private val currentChar: Char get() = pos.currentChar
private fun fixMultilineStringLiteral(source: String): String {
val sizes = mutableListOf<Int>()
val lines = source.lines().toMutableList()
if( lines.size == 0 ) return ""
if( lines[0].isBlank() ) lines.removeFirst()
if( lines.isEmpty()) return ""
if( lines.last().isBlank() ) lines.removeLast()
val normalized = lines.map { l ->
if( l.isBlank() ) {
sizes.add(-1)
""
}
else {
val margin = leftMargin(l)
sizes += margin
" ".repeat(margin) + l.trim()
}
}
val commonMargin = sizes.filter { it >= 0 }.min()
val fixed = if( commonMargin < 1 ) lines else normalized.map {
if( it.isBlank() ) "" else it.drop(commonMargin)
}
return fixed.joinToString("\n")
}
private fun loadStringToken(): Token { private fun loadStringToken(): Token {
val start = currentPos val start = currentPos
@@ -383,6 +409,7 @@ private class Parser(fromPos: Pos) {
// start = start.back() // start = start.back()
val sb = StringBuilder() val sb = StringBuilder()
var newlineDetected = false
while (currentChar != '"') { while (currentChar != '"') {
if (pos.end) throw ScriptError(start, "unterminated string started there") if (pos.end) throw ScriptError(start, "unterminated string started there")
when (currentChar) { when (currentChar) {
@@ -397,6 +424,12 @@ private class Parser(fromPos: Pos) {
} }
} }
'\n', '\r'-> {
newlineDetected = true
sb.append(currentChar)
pos.advance()
}
else -> { else -> {
sb.append(currentChar) sb.append(currentChar)
pos.advance() pos.advance()
@@ -404,7 +437,10 @@ private class Parser(fromPos: Pos) {
} }
} }
pos.advance() pos.advance()
return Token(sb.toString(), start, Token.Type.STRING)
val result = sb.toString().let { if( newlineDetected ) fixMultilineStringLiteral(it) else it }
return Token(result, start, Token.Type.STRING)
} }
/** /**
@@ -16,7 +16,7 @@ import net.sergeych.lyng.pacman.ImportProvider
* *
* There are special types of scopes: * 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( open class Scope(
val parent: Scope?, val parent: Scope?,
@@ -73,7 +73,7 @@ open class Scope(
inline fun <reified T : Obj> requiredArg(index: Int): T { 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}") 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}") ?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}")
} }
@@ -94,8 +94,15 @@ open class Scope(
raiseError("This function does not accept any arguments") raiseError("This function does not accept any arguments")
} }
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T) inline fun <reified T : Obj> thisAs(): T {
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") 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>() internal val objects = mutableMapOf<String, ObjRecord>()
@@ -156,13 +163,14 @@ open class Scope(
fun addConst(name: String, value: Obj) = addItem(name, false, value) fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj = suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource(), currentImportProvider).execute(this) eval(code.toSource())
suspend fun eval(source: Source): Obj = suspend fun eval(source: Source): Obj {
Compiler.compile( return Compiler.compile(
source, source,
currentImportProvider currentImportProvider
).execute(this) ).execute(this)
}
fun containsLocal(name: String): Boolean = name in objects fun containsLocal(name: String): Boolean = name in objects
@@ -193,6 +201,25 @@ open class Scope(
val importManager by lazy { (currentImportProvider as? ImportManager) val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") } ?: 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 { companion object {
fun new(): Scope = fun new(): Scope =
@@ -1,9 +1,12 @@
package net.sergeych.lyng package net.sergeych.lyng
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.stdlib_included.rootLyng
import net.sergeych.lynon.ObjLynonClass import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.* import kotlin.math.*
class Script( class Script(
@@ -20,12 +23,28 @@ class Script(
return lastResult return lastResult
} }
suspend fun execute() = execute(defaultImportManager.newModule()) suspend fun execute() = execute(
defaultImportManager.newStdScope()
)
companion object { companion object {
private val rootScope: Scope = Scope(null).apply { /**
* Create new scope using standard safe set of modules, using [defaultImportManager]. It is
* suspended as first time calls requires compilation of standard library or other
* asynchronous initialization.
*/
suspend fun newScope(pos: Pos = Pos.builtIn) = defaultImportManager.newStdScope(pos)
internal val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this) 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") { addFn("println") {
for ((i, a) in args.withIndex()) { for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value) if (i > 0) print(' ' + a.asStr.value)
@@ -152,6 +171,31 @@ class Script(
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown")) result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
} }
addFn("dynamic") {
ObjDynamic.create(this, requireOnlyArg())
}
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") { addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
} }
@@ -173,8 +217,31 @@ class Script(
addConst("Iterable", ObjIterable) addConst("Iterable", ObjIterable)
addConst("Collection", ObjCollection) addConst("Collection", ObjCollection)
addConst("Array", ObjArray) addConst("Array", ObjArray)
addConst("RingBuffer", ObjRingBuffer.type)
addConst("Class", ObjClassType) 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) val pi = ObjReal(PI)
addConst("π", pi) addConst("π", pi)
getOrCreateNamespace("Math").apply { getOrCreateNamespace("Math").apply {
@@ -184,6 +251,9 @@ class Script(
val defaultImportManager: ImportManager by lazy { val defaultImportManager: ImportManager by lazy {
ImportManager(rootScope, SecurityManager.allowAll).apply { ImportManager(rootScope, SecurityManager.allowAll).apply {
addTextPackages(
rootLyng
)
addPackage("lyng.buffer") { addPackage("lyng.buffer") {
it.addConst("Buffer", ObjBuffer.type) it.addConst("Buffer", ObjBuffer.type)
it.addConst("MutableBuffer", ObjMutableBuffer.type) it.addConst("MutableBuffer", ObjMutableBuffer.type)
@@ -14,6 +14,8 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
cause cause
) )
class ScriptFlowIsNoMoreCollected: Exception()
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message) class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message)
class ImportException(pos: Pos, message: String) : ScriptError(pos, message) class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
@@ -55,7 +55,8 @@ open class Obj {
scope: Scope, scope: Scope,
name: String, name: String,
args: Arguments = Arguments.EMPTY 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 * Invoke a method of the object if exists
@@ -68,7 +69,10 @@ open class Obj {
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null onNotFoundResult: Obj?=null
): Obj = ): Obj =
objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args) objClass.getInstanceMemberOrNull(name)?.value?.invoke(
scope,
this,
args)
?: onNotFoundResult ?: onNotFoundResult
?: scope.raiseSymbolNotFound(name) ?: scope.raiseSymbolNotFound(name)
@@ -251,6 +255,14 @@ open class Obj {
scope.raiseNotImplemented() 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 { companion object {
val rootObjectType = ObjClass("Obj").apply { val rootObjectType = ObjClass("Obj").apply {
@@ -36,7 +36,7 @@ open class ObjClass(
/** /**
* members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass] * members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass]
*/ */
private val members = mutableMapOf<String, ObjRecord>() internal val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className override fun toString(): String = className
@@ -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,65 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
override val objClass: ObjClass = type
companion object {
val type = ObjClass("DelegateContext").apply {
addFn("get") {
val d = thisAs<ObjDynamicContext>().delegate
if (d.readCallback != null)
raiseIllegalState("get already defined")
d.readCallback = requireOnlyArg()
ObjVoid
}
addFn("set") {
val d = thisAs<ObjDynamicContext>().delegate
if (d.writeCallback != null)
raiseIllegalState("set already defined")
d.writeCallback = requireOnlyArg()
ObjVoid
}
}
}
}
class ObjDynamic : Obj() {
internal var readCallback: Statement? = null
internal var writeCallback: Statement? = null
override suspend fun readField(scope: Scope, name: String): ObjRecord {
return readCallback?.execute(scope.copy(Arguments(ObjString(name))))?.let {
if (writeCallback != null)
it.asMutable
else
it.asReadonly
}
?: super.readField(scope, name)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
writeCallback?.execute(scope.copy(Arguments(ObjString(name), newValue)))
?: super.writeField(scope, name, newValue)
}
companion object {
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
val delegate = ObjDynamic()
val context = ObjDynamicContext(delegate)
builder.execute(scope.copy(newThisObj = context))
return delegate
}
val type = object : ObjClass("Delegate") {}
}
}
@@ -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 = onNotFoundResult: Obj?): Obj =
instanceScope[name]?.let { instanceScope[name]?.let {
if (it.visibility.isPublic) if (it.visibility.isPublic)
it.value.invoke(scope, this, args) it.value.invoke(
instanceScope,
this,
args)
else else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name")) scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
} }
@@ -53,7 +53,7 @@ val ObjIterable by lazy {
addFn("toMap") { addFn("toMap") {
val result = ObjMap() val result = ObjMap()
thisObj.toFlow(this).collect { pair -> 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 result
} }
@@ -86,6 +86,24 @@ val ObjIterable by lazy {
ObjList(result) 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") { addFn("isEmpty") {
ObjBool( ObjBool(
thisObj.invokeInstanceMethod(this, "iterator") thisObj.invokeInstanceMethod(this, "iterator")
@@ -93,6 +111,5 @@ val ObjIterable by lazy {
.not() .not()
) )
} }
} }
} }
@@ -1,3 +1,4 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
val ObjIterator by lazy { ObjClass("Iterator") } val ObjIterator by lazy { ObjClass("Iterator") }
@@ -36,7 +36,9 @@ class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
addFn("next") { addFn("next") {
thisAs<ObjKotlinObjIterator>().iterator.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)) 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()) ObjString(decoder.unpackBinaryData().decodeToString())
}.apply { }.apply {
addFn("toInt") { addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong()) ObjInt(thisAs<ObjString>().value.toLongOrNull()
?: raiseIllegalArgument("can't convert to int: $thisObj")
)
} }
addFn("startsWith") { addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value)) 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("encodeUtf8") { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) } addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble()) } addFn("toReal") {
ObjReal(thisAs<ObjString>().value.toDouble())
}
} }
} }
} }
@@ -1,6 +1,7 @@
package net.sergeych.lyng.pacman package net.sergeych.lyng.pacman
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.mptools.CachedExpression
/** /**
* Package manager INTERFACE (abstract class). Performs import routines * Package manager INTERFACE (abstract class). Performs import routines
@@ -45,6 +46,15 @@ abstract class ImportProvider(
fun newModuleAt(pos: Pos): ModuleScope = fun newModuleAt(pos: Pos): ModuleScope =
ModuleScope(this, pos, "unknown") 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()
} }
@@ -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,13 @@
package net.sergeych.lyng
fun leftMargin(s: String): Int {
var cnt = 0
for (c in s) {
when (c) {
' ' -> cnt++
'\t' -> cnt = (cnt / 4.0 + 0.9).toInt() * 4
else -> break
}
}
return cnt
}
@@ -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
@@ -43,4 +43,50 @@ class OOTest {
assertEquals( "foo!", Point.getData() ) assertEquals( "foo!", Point.getData() )
""".trimIndent()) """.trimIndent())
} }
@Test
fun testDynamicGet() = runTest {
eval("""
val accessor = dynamic {
get { name ->
if( name == "foo" ) "bar" else null
}
}
println("-- " + accessor.foo)
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
""".trimIndent())
}
@Test
fun testDelegateSet() = runTest {
eval("""
var setValueForBar = null
val accessor = dynamic {
get { name ->
when(name) {
"foo" -> "bar"
"bar" -> setValueForBar
else -> null
}
}
set { name, value ->
if( name == "bar" )
setValueForBar = value
else throw IllegalAssignmentException("Can't assign "+name)
}
}
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
accessor.bar = "buzz"
assertEquals("buzz", accessor.bar)
assertThrows {
accessor.bad = "!23"
}
""".trimIndent())
}
} }
+195 -29
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 @Test
fun testLambdaWithArgsEllipsis() = runTest { fun testLambdaWithArgsEllipsis() = runTest {
eval( eval(
@@ -2244,10 +2344,12 @@ class ScriptTest {
@Test @Test
fun testSet2() = runTest { fun testSet2() = runTest {
eval(""" eval(
"""
assertEquals( Set( ...[1,2,3]), Set(1,2,3) ) assertEquals( Set( ...[1,2,3]), Set(1,2,3) )
assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) ) assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@@ -2273,8 +2375,6 @@ class ScriptTest {
x++; y++ x++; y++
} }
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void
""".trimIndent() """.trimIndent()
) )
} }
@@ -2284,13 +2384,13 @@ class ScriptTest {
eval( eval(
""" """
class Point(x,y) class Point(x,y)
// see the difference: apply changes this to newly created Point: // see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply { val p = Point(1,2).apply {
this.x++; this.y++ this.x++
y++
} }
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void
""".trimIndent() """.trimIndent()
) )
} }
@@ -2305,6 +2405,10 @@ class ScriptTest {
} }
fun Object.isInteger() { fun Object.isInteger() {
println(this)
println(this is Int)
println(this is Real)
println(this is String)
when(this) { when(this) {
is Int -> true is Int -> true
is Real -> toInt() == this is Real -> toInt() == this
@@ -2319,7 +2423,7 @@ class ScriptTest {
assert( 12.isInteger() == true ) assert( 12.isInteger() == true )
assert( 12.1.isInteger() == false ) assert( 12.1.isInteger() == false )
assert( "5".isInteger() ) assert( "5".isInteger() )
assert( ! "5.2".isInteger() ) assert( !"5.2".isInteger() )
""".trimIndent() """.trimIndent()
) )
} }
@@ -2446,20 +2550,26 @@ class ScriptTest {
fun testDefaultImportManager() = runTest { fun testDefaultImportManager() = runTest {
val scope = Scope.new() val scope = Scope.new()
assertFails { assertFails {
scope.eval(""" scope.eval(
"""
import foo import foo
foo() foo()
""".trimIndent()) """.trimIndent()
)
} }
scope.importManager.addTextPackages(""" scope.importManager.addTextPackages(
"""
package foo package foo
fun foo() { "bar" } fun foo() { "bar" }
""".trimIndent()) """.trimIndent()
scope.eval(""" )
scope.eval(
"""
import foo import foo
assertEquals( "bar", foo()) assertEquals( "bar", foo())
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@@ -2478,7 +2588,8 @@ class ScriptTest {
@Test @Test
fun testBuffer() = runTest { fun testBuffer() = runTest {
eval(""" eval(
"""
import lyng.buffer import lyng.buffer
assertEquals( 0, Buffer().size ) assertEquals( 0, Buffer().size )
@@ -2497,12 +2608,14 @@ class ScriptTest {
assertEquals(101, buffer[2]) assertEquals(101, buffer[2])
assertEquals("Heelo", buffer.decodeUtf8()) assertEquals("Heelo", buffer.decodeUtf8())
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testBufferCompare() = runTest { fun testBufferCompare() = runTest {
eval(""" eval(
"""
import lyng.buffer import lyng.buffer
println("Hello".characters()) println("Hello".characters())
@@ -2520,7 +2633,8 @@ class ScriptTest {
assertEquals("foo", map[b2]) assertEquals("foo", map[b2])
assertEquals(null, map[b3]) assertEquals(null, map[b3])
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@@ -2548,6 +2662,7 @@ class ScriptTest {
) )
delay(1000) delay(1000)
} }
@Test @Test
fun testTimeStatics() = runTest { fun testTimeStatics() = runTest {
eval( eval(
@@ -2589,7 +2704,8 @@ class ScriptTest {
println(Script.defaultImportManager.packageNames) println(Script.defaultImportManager.packageNames)
println(s.importManager.packageNames) println(s.importManager.packageNames)
s.importManager.addTextPackages(""" s.importManager.addTextPackages(
"""
package foo package foo
import lyng.time import lyng.time
@@ -2597,8 +2713,10 @@ class ScriptTest {
fun foo() { fun foo() {
println("foo: %s"(Instant())) println("foo: %s"(Instant()))
} }
""".trimIndent()) """.trimIndent()
s.importManager.addTextPackages(""" )
s.importManager.addTextPackages(
"""
package bar package bar
import lyng.time import lyng.time
@@ -2606,24 +2724,28 @@ class ScriptTest {
fun bar() { fun bar() {
println("bar: %s"(Instant())) println("bar: %s"(Instant()))
} }
""".trimIndent()) """.trimIndent()
)
println(s.importManager.packageNames) println(s.importManager.packageNames)
s.eval(""" s.eval(
"""
import foo import foo
import bar import bar
foo() foo()
bar() bar()
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testIndexIntIncrements() = runTest { fun testIndexIntIncrements() = runTest {
eval(""" eval(
"""
val x = [1,2,3] val x = [1,2,3]
x[1]++ x[1]++
++x[0] ++x[0]
@@ -2636,12 +2758,14 @@ class ScriptTest {
assert( b == Buffer(1,3,3) ) assert( b == Buffer(1,3,3) )
++b[0] ++b[0]
assertEquals( b, Buffer(2,3,3) ) assertEquals( b, Buffer(2,3,3) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testIndexIntDecrements() = runTest { fun testIndexIntDecrements() = runTest {
eval(""" eval(
"""
val x = [1,2,3] val x = [1,2,3]
x[1]-- x[1]--
--x[0] --x[0]
@@ -2654,15 +2778,57 @@ class ScriptTest {
assert( b == Buffer(1,1,3) ) assert( b == Buffer(1,1,3) )
--b[0] --b[0]
assertEquals( b, Buffer(0,1,3) ) assertEquals( b, Buffer(0,1,3) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testRangeToList() = runTest { fun testRangeToList() = runTest {
val x = eval("""(1..10).toList()""") as ObjList 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 val y = eval("""(-2..3).toList()""") as ObjList
println(y.list) println(y.list)
} }
@Test
fun testMultilineStrings() = runTest {
assertEquals(
"""
This is a multiline text.
This is a second line.
""".trimIndent(), eval(
"""
"
This is a multiline text.
This is a second line.
"
""".trimIndent()
).toString()
)
assertEquals(
"""
This is a multiline text.
""".trimIndent(), eval(
"""
"
This is a multiline text.
"
""".trimIndent()
).toString()
)
assertEquals(
"""
This is a multiline text.
""".trimIndent(), eval(
"""
"
This is a multiline text.
"
""".trimIndent()
).toString()
)
}
} }
@@ -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())
}
}
+14 -13
View File
@@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Script
import net.sergeych.lyng.leftMargin
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Files.readAllLines import java.nio.file.Files.readAllLines
@@ -15,18 +17,6 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.fail import kotlin.test.fail
fun leftMargin(s: String): Int {
var cnt = 0
for (c in s) {
when (c) {
' ' -> cnt++
'\t' -> cnt = (cnt / 4.0 + 0.9).toInt() * 4
else -> break
}
}
return cnt
}
data class DocTest( data class DocTest(
val fileName: String, val fileName: String,
val line: Int, val line: Int,
@@ -163,9 +153,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
suspend fun DocTest.test(scope: Scope = Scope()) { suspend fun DocTest.test(_scope: Scope? = null) {
val collectedOutput = StringBuilder() val collectedOutput = StringBuilder()
val currentTest = this val currentTest = this
val scope = _scope ?: Script.newScope()
scope.apply { scope.apply {
addFn("println") { addFn("println") {
if( bookMode ) { if( bookMode ) {
@@ -299,4 +290,14 @@ class BookTest {
runDocTests("../docs/time.md") runDocTests("../docs/time.md")
} }
@Test
fun testParallelismBook() = runBlocking {
runDocTests("../docs/parallelism.md")
}
@Test
fun testRingBuffer() = runBlocking {
runDocTests("../docs/RingBuffer.md")
}
} }
-1
View File
@@ -13,7 +13,6 @@ dependencyResolutionManagement {
maven("https://maven.universablockchain.com/") maven("https://maven.universablockchain.com/")
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven") maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
mavenLocal() mavenLocal()
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
} }
} }