Compare commits
No commits in common. "master" and "bigdecimals" have entirely different histories.
master
...
bigdecimal
4
.gitignore
vendored
4
.gitignore
vendored
@ -27,7 +27,3 @@ debug.log
|
||||
/compile_jvm_output.txt
|
||||
/compile_metadata_output.txt
|
||||
test_output*.txt
|
||||
/site/src/version-template/lyng-version.js
|
||||
/bugcontents.db
|
||||
/bugs/
|
||||
contents.db
|
||||
|
||||
@ -13,13 +13,11 @@
|
||||
- Prefer defining Lyng entities (enums/classes/type shapes) in `.lyng` files; only define them in Kotlin when there is Kotlin/platform-specific implementation detail that cannot be expressed in Lyng.
|
||||
- Avoid hardcoding Lyng API documentation in Kotlin registrars when it can be declared in `.lyng`; Kotlin-side docs should be fallback/bridge only.
|
||||
- For mixed pluggable modules (Lyng + Kotlin), embed module `.lyng` sources as generated Kotlin string literals, evaluate them into module scope during registration, then attach Kotlin implementations/bindings.
|
||||
- When a change adds or changes Lyng-visible runtime/module behavior, update the corresponding `.lyng` declaration in the same change, including declaration-level docs/comments for new API surface.
|
||||
|
||||
## Kotlin/Wasm generation guardrails
|
||||
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
||||
- Do not use `statement { ... }` or other inline suspend lambdas in compiler hot paths (e.g., parsing/var declarations, initializer thunks).
|
||||
- If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas.
|
||||
- For any code in `commonMain`, verify it is Kotlin Multiplatform compatible before finishing. Do not use JVM-only APIs or Java-backed convenience methods such as `Map.putIfAbsent`; prefer stdlib/common equivalents and run at least the relevant compile/test task that exercises the `commonMain` source set.
|
||||
- If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed.
|
||||
- Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead.
|
||||
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@ -7,70 +7,6 @@ History note:
|
||||
- Entries below are synchronized and curated for `1.5.x`.
|
||||
- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- No unreleased entries yet.
|
||||
|
||||
## 1.5.5 (2026-04-23)
|
||||
|
||||
### Concurrency and collections
|
||||
- Added coroutine coordination primitives and helpers for everyday parallel code:
|
||||
- `Channel` for coroutine-to-coroutine communication
|
||||
- `LaunchPool` for bounded-concurrency task execution
|
||||
- `Iterable<Deferred>.joinAll()` to await a whole collection of deferreds in input order
|
||||
- `CompletableDeferred.completeExceptionally(...)` and `Deferred.cancelAndJoin()`
|
||||
- Added docs and examples for the new concurrency APIs, including `joinAll()` coverage in iterable and parallelism references.
|
||||
|
||||
### Database and time APIs
|
||||
- Added the portable `lyng.io.db` SQL contract and the first concrete providers:
|
||||
- `lyng.io.db.sqlite` on JVM and Linux Native
|
||||
- `lyng.io.db.jdbc` on JVM
|
||||
- Added SQLite/JDBC release hardening:
|
||||
- nested transactions via savepoints
|
||||
- detached materialized rows
|
||||
- generated-key support through `ExecutionResult.getGeneratedKeys()`
|
||||
- schema-driven value conversion for `Bool`, `Decimal`, `Date`, `DateTime`, and `Instant`
|
||||
- portable SQLite linker/deployment fixes and documented runtime options
|
||||
- Added `Date` to `lyng.time` and the core runtime as a first-class calendar-date type, plus conversions and arithmetic across `Instant`, `DateTime`, and `Date`.
|
||||
|
||||
### Language, stdlib, and tooling
|
||||
- Added extensions on singleton `object` declarations, including object-scoped indexer overrides for bracket syntax.
|
||||
- Added backtick string literals and formatter support.
|
||||
- Added `lyng.legacy_digest` for SHA-1 compatibility work, `String.replace`, and `buffer.base64std`.
|
||||
- Improved CLI/runtime behavior with `atExit` shutdown handlers, native release-binary work, and follow-up CLI packaging/import fixes.
|
||||
- Expanded docs across the tutorial, stdlib references, database docs, networking docs, and release notes.
|
||||
|
||||
### Runtime/compiler stability and performance
|
||||
- Extended exact-call and higher-order lambda inlining through the bytecode compiler, including compiled fast paths for simple lambdas, wrappers, captures, and common higher-order helpers.
|
||||
- Fixed import caching and class/object bytecode dispatch on JVM.
|
||||
- Fixed immutable `val` compound assignments so true mutating `*Assign` operations continue to work while fallback reassignments report the correct read-only error.
|
||||
- Fixed closure/capture and import regressions across launched loops, singleton/object extensions, aliasing, transitive re-exports, and immutable capture escaping.
|
||||
- Improved list-fill/list-append fast paths, nullable-let inference, Decimal/Complex interop, and related regression coverage.
|
||||
|
||||
### Release notes
|
||||
- Release metadata, homepage samples, docs, and README now point to `1.5.5`.
|
||||
|
||||
## 1.5.4 (2026-04-03)
|
||||
|
||||
### Runtime and compiler stability
|
||||
- Stabilized the recent `piSpigot` benchmark/compiler work for release.
|
||||
- Fixed numeric-mix regressions introduced by overly broad int-coercion in bytecode compilation.
|
||||
- Restored correct behavior for decimal arithmetic, mixed real/int flows, list literals, list size checks, and national-character script cases.
|
||||
- Fixed plain-list index fast paths so they no longer bypass subclass behavior such as `ObservableList` hooks and flow notifications.
|
||||
- Hardened local numeric compare fast paths to correctly handle primitive-coded frame slots.
|
||||
|
||||
### Performance and examples
|
||||
- Added `piSpigot` benchmark/example coverage:
|
||||
- `examples/pi-test.lyng`
|
||||
- `examples/pi-bench.lyng`
|
||||
- JVM benchmark test for release-baseline verification
|
||||
- Kept the safe list/index/runtime wins that improve the optimized `piSpigot` path without reintroducing type-unsound coercions.
|
||||
- Changed the default `RVAL_FASTPATH` setting off on JVM/Android and in the benchmark preset after verification that it no longer helps the stabilized `piSpigot` workload.
|
||||
|
||||
### Release notes
|
||||
- Full JVM and wasm test gates pass on the release tree.
|
||||
- Benchmark findings and remaining post-release optimization targets are documented in `notes/pi_spigot_benchmark_baseline_2026-04-03.md`.
|
||||
|
||||
## 1.5.1 (2026-03-25)
|
||||
|
||||
### Language
|
||||
|
||||
80
README.md
80
README.md
@ -48,12 +48,9 @@ assertEquals(A.E.One, A.One)
|
||||
|
||||
- [Language home](https://lynglang.com)
|
||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||
- [Latest release notes (1.5.5)](docs/whats_new.md)
|
||||
- [What's New in 1.5](docs/whats_new_1_5.md)
|
||||
- [Testing and Assertions](docs/Testing.md)
|
||||
- [Filesystem and Processes (lyngio)](docs/lyngio.md)
|
||||
- [SQL Databases (lyng.io.db)](docs/lyng.io.db.md)
|
||||
- [Time and Calendar Types](docs/time.md)
|
||||
- [Return Statement](docs/return_statement.md)
|
||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||
- [Samples directory](docs/samples)
|
||||
@ -66,7 +63,8 @@ assertEquals(A.E.One, A.One)
|
||||
### Add dependency to your project
|
||||
|
||||
```kotlin
|
||||
val lyngVersion = "1.5.5"
|
||||
// update to current please:
|
||||
val lyngVersion = "1.5.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
// ...
|
||||
@ -95,49 +93,42 @@ import net.sergeych.lyng.*
|
||||
// we need a coroutine to start, as Lyng
|
||||
// is a coroutine based language, async topdown
|
||||
runBlocking {
|
||||
val session = EvalSession()
|
||||
assert(5 == session.eval(""" 3*3 - 4 """).toInt())
|
||||
session.eval(""" println("Hello, Lyng!") """)
|
||||
assert(5 == eval(""" 3*3 - 4 """).toInt())
|
||||
eval(""" println("Hello, Lyng!") """)
|
||||
}
|
||||
```
|
||||
|
||||
### Exchanging information
|
||||
|
||||
The preferred host runtime is `EvalSession`. It owns the script scope and any coroutines
|
||||
started with `launch { ... }`. Create a session, grab its scope when you need low-level
|
||||
binding APIs, then execute scripts through the session:
|
||||
Script is executed over some `Scope`. Create instance,
|
||||
add your specific vars and functions to it, and call:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.*
|
||||
|
||||
runBlocking {
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope().apply {
|
||||
// simple function
|
||||
addFn("sumOf") {
|
||||
var sum = 0.0
|
||||
for (a in args) sum += a.toDouble()
|
||||
ObjReal(sum)
|
||||
}
|
||||
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
||||
|
||||
// callback back to kotlin to some suspend fn, for example::
|
||||
// suspend fun doSomeWork(text: String): Int
|
||||
addFn("doSomeWork") {
|
||||
// this _is_ a suspend lambda, we can call suspend function,
|
||||
// and it won't consume the thread.
|
||||
// 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()
|
||||
}
|
||||
// simple function
|
||||
val scope = Script.newScope().apply {
|
||||
addFn("sumOf") {
|
||||
var sum = 0.0
|
||||
for (a in args) sum += a.toDouble()
|
||||
ObjReal(sum)
|
||||
}
|
||||
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
||||
|
||||
// execute through the session:
|
||||
session.eval("sumOf(1,2,3)") // <- 6
|
||||
// callback back to kotlin to some suspend fn, for example::
|
||||
// suspend fun doSomeWork(text: String): Int
|
||||
addFn("doSomeWork") {
|
||||
// this _is_ a suspend lambda, we can call suspend function,
|
||||
// and it won't consume the thread.
|
||||
// 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:
|
||||
scope.eval("sumOf(1,2,3)") // <- 6
|
||||
```
|
||||
Note that the session reuses one scope, so state persists across `session.eval(...)` calls.
|
||||
Use raw `Scope.eval(...)` only when you intentionally want low-level control without session-owned coroutine lifecycle.
|
||||
Note that the scope stores all changes in it so you can make calls on a single scope to preserve state between calls.
|
||||
|
||||
## IntelliJ IDEA plugin: Lightweight autocompletion (experimental)
|
||||
|
||||
@ -186,7 +177,8 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
|
||||
|
||||
# Language Roadmap
|
||||
|
||||
The current stable release is **v1.5.5**: the 1.5 cycle now includes the database/date/concurrency additions as well as the latest compiler/runtime stabilization work, and the language, tooling, and site are aligned around this release.
|
||||
We are now at **v1.5.0-SNAPSHOT** (stable development cycle): basic optimization performed, battery included: standard library is 90% here, initial
|
||||
support in HTML, popular editors, and IDEA; tools to syntax highlight and format code are ready. It was released closed to schedule.
|
||||
|
||||
Ready features:
|
||||
|
||||
@ -223,21 +215,23 @@ Ready features:
|
||||
- [x] assign-if-null operator `?=`
|
||||
- [x] user-defined exception classes
|
||||
|
||||
All of this is documented on the [language site](https://lynglang.com) and locally in [docs/tutorial.md](docs/tutorial.md). The site reflects the current release, while development snapshots continue in the private Maven repository.
|
||||
All of this is documented in the [language site](https://lynglang.com) and locally [docs/language.md](docs/tutorial.md). the current nightly builds published on the site and in the private maven repository.
|
||||
|
||||
## plan: towards v2.0 Next Generation
|
||||
|
||||
- [x] site with integrated interpreter to give a try
|
||||
- [x] kotlin part public API good docs, integration focused
|
||||
- [x] type specifications
|
||||
- [ ] type specifications
|
||||
- [x] Textmate Bundle
|
||||
- [x] IDEA plugin
|
||||
- [x] source docs and maybe lyng.md to a standard
|
||||
- [ ] source docs and maybe lyng.md to a standard
|
||||
- [ ] metadata first class access from lyng
|
||||
- [x] aggressive optimizations
|
||||
- [ ] compile to JVM bytecode optimization
|
||||
|
||||
## After 1.5 "Ideal scripting"
|
||||
|
||||
* __we are here now ;)__
|
||||
Estimated summer 2026
|
||||
|
||||
- propose your feature!
|
||||
|
||||
@ -245,12 +239,8 @@ All of this is documented on the [language site](https://lynglang.com) and local
|
||||
|
||||
@-links are for contacting authors on [project home](https://gitea.sergeych.net/SergeychWorks/lyng): this simplest s to open issue for the person you need to convey any information about this project.
|
||||
|
||||
<img src="https://www.gravatar.com/avatar/7e3a56ff8a090fc9ffbd1909dea94904?s=32&d=identicon" alt="Sergey Chernov" width="32" height="32" style="vertical-align: middle; margin-right: 0.5em;" /> <b>Sergey Chernov</b> @sergeych, real.sergeych@gmail.com: Initial idea and architecture, language concept, design, implementation.
|
||||
|
||||
<br/>
|
||||
|
||||
<img src="https://www.gravatar.com/avatar/53a90bca30c85a81db8f0c0d8dea43a1?s=32&d=identicon" alt="Yulia Nezhinskaya" width="32" height="32" style="vertical-align: middle; margin-right: 0.5em;" /> <b>Yulia Nezhinskaya</b> @AlterEgoJuliaN, neleka88@gmail.com: System analysis, math and feature design.
|
||||
|
||||
__Sergey Chernov__ @sergeych: Initial idea and architecture, language concept, design, implementation.
|
||||
|
||||
__Yulia Nezhinskaya__ @AlterEgoJuliaN: System analysis, math and features design.
|
||||
|
||||
[parallelism]: docs/parallelism.md
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
set -e
|
||||
echo "publishing all artifacts"
|
||||
echo
|
||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel --no-configuration-cache
|
||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel
|
||||
|
||||
#echo
|
||||
#echo "Creating plugin"
|
||||
|
||||
@ -18,12 +18,10 @@
|
||||
#
|
||||
|
||||
upload_only=false
|
||||
target=vps # default: new server; use --old for d.lynglang.com
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "-u" || "$arg" == "--upload-only" ]]; then
|
||||
upload_only=true
|
||||
elif [[ "$arg" == "--old" ]]; then
|
||||
target=com
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
@ -90,20 +88,19 @@ function updateIdeaPluginDownloadLink() {
|
||||
fi
|
||||
}
|
||||
|
||||
# target settings (-t com | -t vps)
|
||||
case "$target" in
|
||||
# default target settings
|
||||
case "com" in
|
||||
com)
|
||||
SSH_HOST=sergeych@d.lynglang.com
|
||||
SSH_PORT=22
|
||||
ROOT=/bigstore/sergeych_pub/lyng
|
||||
;;
|
||||
vps)
|
||||
SSH_HOST=sergeych@94.130.36.94
|
||||
SSH_PORT=22
|
||||
ROOT=/var/www/lynglang
|
||||
SSH_HOST=sergeych@d.lynglang.com # host to deploy to
|
||||
SSH_PORT=22 # ssh port on it
|
||||
ROOT=/bigstore/sergeych_pub/lyng # directory to rsync to
|
||||
;;
|
||||
# com)
|
||||
# SSH_HOST=vvk@front-01.neurodatalab.com
|
||||
# ROOT=/home/vvk
|
||||
# ;;
|
||||
*)
|
||||
echo "*** ERROR: unknown target '$target' (use -t com | -t vps)"
|
||||
echo "*** ERROR: target not specified (use deploy com | dev)"
|
||||
echo "*** stop"
|
||||
exit 101
|
||||
esac
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
# Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -19,15 +19,13 @@
|
||||
|
||||
set -e
|
||||
|
||||
archive=./lyng/build/distributions/lyng-jvm.zip
|
||||
install_root="$HOME/bin/jlyng-jvm"
|
||||
launcher="$install_root/lyng-jvm/bin/lyng"
|
||||
root=./lyng/build/install/lyng-jvm/
|
||||
|
||||
./gradlew :lyng:jvmDistZip
|
||||
mkdir -p ./distributables
|
||||
cp "$archive" ./distributables/lyng-jvm.zip
|
||||
rm -rf "$install_root" || true
|
||||
rm "$HOME/bin/jlyng" 2>/dev/null || true
|
||||
mkdir -p "$install_root"
|
||||
unzip -q ./distributables/lyng-jvm.zip -d "$install_root"
|
||||
ln -s "$launcher" "$HOME/bin/jlyng"
|
||||
./gradlew :lyng:installJvmDist
|
||||
#strip $file
|
||||
#upx $file
|
||||
rm -rf ~/bin/jlyng-jvm || true
|
||||
rm ~/bin/jlyng 2>/dev/null || true
|
||||
mkdir -p ~/bin/jlyng-jvm
|
||||
cp -R $root ~/bin/jlyng-jvm
|
||||
ln -s ~/bin/jlyng-jvm/lyng-jvm/bin/lyng ~/bin/jlyng
|
||||
|
||||
7
bytecode_migration_plan.md
Normal file
7
bytecode_migration_plan.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Bytecode Migration Plan (Archived)
|
||||
|
||||
Status: completed.
|
||||
|
||||
Historical reference:
|
||||
- `notes/archive/bytecode_migration_plan.md` (full plan)
|
||||
- `notes/archive/bytecode_migration_plan_completed.md` (summary)
|
||||
@ -4,13 +4,6 @@ It's an interface if the [Collection] that provides indexing access, like `array
|
||||
Array therefore implements [Iterable] too. Well known implementations of `Array` are
|
||||
[List] and [ImmutableList].
|
||||
|
||||
The language-level bracket syntax supports one or more selectors:
|
||||
|
||||
- `value[i]`
|
||||
- `value[i, j]`
|
||||
|
||||
Concrete array-like types decide what selectors they accept. Built-in list-like arrays use one selector at a time; custom types such as matrices may interpret multiple selectors.
|
||||
|
||||
Array adds the following methods:
|
||||
|
||||
## Binary search
|
||||
|
||||
@ -120,18 +120,17 @@ which is used in `toString`) and hex encoding:
|
||||
|
||||
## Members
|
||||
|
||||
| name | meaning | type |
|
||||
|----------------------------|------------------------------------------------|---------------|
|
||||
| `size` | size | Int |
|
||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||
| `+` | buffer concatenation | Any |
|
||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||
| `hex` | encode to hex strign | String |
|
||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
||||
| `base64std` | encode to base64 (default vocabulary, filling) | String |
|
||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||
| name | meaning | type |
|
||||
|----------------------------|-----------------------------------------|---------------|
|
||||
| `size` | size | Int |
|
||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||
| `+` | buffer concatenation | Any |
|
||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||
| `hex` | encode to hex strign | String |
|
||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||
|
||||
(1)
|
||||
: optimized implementation that override `Iterable` one
|
||||
|
||||
211
docs/Channel.md
211
docs/Channel.md
@ -1,211 +0,0 @@
|
||||
# Channel
|
||||
|
||||
A `Channel` is a **hot, bidirectional pipe** for passing values between concurrently running coroutines.
|
||||
Unlike a [Flow], which is cold and replayed on every collection, a `Channel` is stateful: each value
|
||||
sent is consumed by exactly one receiver.
|
||||
|
||||
Channels model the classic _producer / consumer_ pattern and are the right tool when:
|
||||
|
||||
- two or more coroutines need to exchange individual values at their own pace;
|
||||
- you want back-pressure (rendezvous) or explicit buffering control;
|
||||
- you need a push-based, hot data source (opposite of the pull-based, cold [Flow]).
|
||||
|
||||
## Constructors
|
||||
|
||||
```
|
||||
Channel() // rendezvous — sender and receiver must meet
|
||||
Channel(n: Int) // buffered — sender may run n items ahead of the receiver
|
||||
Channel(Channel.UNLIMITED) // no limit on buffered items
|
||||
```
|
||||
|
||||
**Rendezvous** (`Channel()`, capacity 0): `send` suspends until a matching `receive` is ready,
|
||||
and vice-versa. This gives the tightest synchronisation and the smallest memory footprint.
|
||||
|
||||
**Buffered** (`Channel(n)`): `send` only suspends when the internal buffer is full. Allows the
|
||||
producer to get up to _n_ items ahead of the consumer.
|
||||
|
||||
**Unlimited** (`Channel(Channel.UNLIMITED)`): `send` never suspends. Useful when the producer is
|
||||
bursty and you do not want it blocked, but be careful not to grow the buffer without bound.
|
||||
|
||||
## Sending and receiving
|
||||
|
||||
```lyng
|
||||
val ch = Channel() // rendezvous channel
|
||||
|
||||
val producer = launch {
|
||||
ch.send("hello") // suspends until the receiver is ready
|
||||
ch.send("world")
|
||||
ch.close() // signal: no more values
|
||||
}
|
||||
|
||||
val a = ch.receive() // suspends until "hello" arrives
|
||||
val b = ch.receive() // suspends until "world" arrives
|
||||
val c = ch.receive() // channel is closed and drained → null
|
||||
assertEquals("hello", a)
|
||||
assertEquals("world", b)
|
||||
assertEquals(null, c)
|
||||
```
|
||||
|
||||
`receive()` returns `null` when the channel is both **closed** _and_ **fully drained** — that is
|
||||
the idiomatic loop termination condition:
|
||||
|
||||
```lyng
|
||||
val ch = Channel(4)
|
||||
|
||||
launch {
|
||||
for (i in 1..5) ch.send(i)
|
||||
ch.close()
|
||||
}
|
||||
|
||||
var item = ch.receive()
|
||||
while (item != null) {
|
||||
println(item)
|
||||
item = ch.receive()
|
||||
}
|
||||
```
|
||||
|
||||
## Non-suspending poll
|
||||
|
||||
`tryReceive()` never suspends. It returns the next buffered value, or `null` if the buffer is
|
||||
empty or the channel is closed.
|
||||
|
||||
```lyng
|
||||
val ch = Channel(8)
|
||||
ch.send(42)
|
||||
println(ch.tryReceive()) // 42
|
||||
println(ch.tryReceive()) // null — nothing buffered right now
|
||||
```
|
||||
|
||||
Use `tryReceive` for _polling_ patterns where blocking would be unacceptable, for example when
|
||||
combining channel checks with other work inside a coroutine loop.
|
||||
|
||||
## Closing a channel
|
||||
|
||||
`close()` marks the channel so that no further `send` calls are accepted. Any items already in the
|
||||
buffer can still be received. Once the buffer is drained, `receive()` returns `null` and
|
||||
`isClosedForReceive` becomes `true`.
|
||||
|
||||
```lyng
|
||||
val ch = Channel(2)
|
||||
ch.send(1)
|
||||
ch.send(2)
|
||||
ch.close()
|
||||
|
||||
assert(ch.isClosedForSend)
|
||||
assert(!ch.isClosedForReceive) // still has 2 buffered items
|
||||
|
||||
ch.receive() // 1
|
||||
ch.receive() // 2
|
||||
assert(ch.isClosedForReceive) // drained
|
||||
```
|
||||
|
||||
Calling `send` after `close()` throws `IllegalStateException`.
|
||||
|
||||
## Properties
|
||||
|
||||
| property | type | description |
|
||||
|---------------------|--------|----------------------------------------------------------|
|
||||
| `isClosedForSend` | `Bool` | `true` after `close()` is called |
|
||||
| `isClosedForReceive`| `Bool` | `true` when closed _and_ every buffered item is consumed |
|
||||
|
||||
## Methods
|
||||
|
||||
| method | suspends | description |
|
||||
|-----------------|----------|----------------------------------------------------------------------------------|
|
||||
| `send(value)` | yes | send a value; suspends when buffer full (rendezvous: always until partner ready) |
|
||||
| `receive()` | yes | receive next value; suspends when empty; returns `null` when closed + drained |
|
||||
| `tryReceive()` | no | return next buffered value or `null`; never suspends |
|
||||
| `close()` | no | signal end of production; existing buffer items are still receivable |
|
||||
|
||||
## Static constants
|
||||
|
||||
| constant | value | description |
|
||||
|---------------------|------------------|-------------------------------------|
|
||||
| `Channel.UNLIMITED` | `Int.MAX_VALUE` | capacity for an unlimited-buffer channel |
|
||||
|
||||
## Common patterns
|
||||
|
||||
### Producer / consumer
|
||||
|
||||
```lyng
|
||||
val ch = Channel()
|
||||
val results = []
|
||||
val mu = Mutex()
|
||||
|
||||
val consumer = launch {
|
||||
var item = ch.receive()
|
||||
while (item != null) {
|
||||
mu.withLock { results += item }
|
||||
item = ch.receive()
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
for (i in 1..5) ch.send("msg:$i")
|
||||
ch.close()
|
||||
}.await()
|
||||
|
||||
consumer.await()
|
||||
println(results)
|
||||
```
|
||||
|
||||
### Fan-out: one channel, many consumers
|
||||
|
||||
```lyng
|
||||
val ch = Channel(16)
|
||||
|
||||
// multiple consumers
|
||||
val workers = (1..4).map { id ->
|
||||
launch {
|
||||
var task = ch.receive()
|
||||
while (task != null) {
|
||||
println("worker $id handles $task")
|
||||
task = ch.receive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// single producer
|
||||
for (i in 1..20) ch.send(i)
|
||||
ch.close()
|
||||
|
||||
workers.forEach { it.await() }
|
||||
```
|
||||
|
||||
### Ping-pong between two coroutines
|
||||
|
||||
```lyng
|
||||
val ping = Channel()
|
||||
val pong = Channel()
|
||||
|
||||
launch {
|
||||
repeat(3) {
|
||||
val msg = ping.receive()
|
||||
println("got: $msg → sending pong")
|
||||
pong.send("pong")
|
||||
}
|
||||
}
|
||||
|
||||
repeat(3) {
|
||||
ping.send("ping")
|
||||
println(pong.receive())
|
||||
}
|
||||
```
|
||||
|
||||
## Channel vs Flow
|
||||
|
||||
| | [Flow] | Channel |
|
||||
|---|---|---|
|
||||
| **temperature** | cold (lazy) | hot (eager) |
|
||||
| **replay** | every collector gets a fresh run | each item is consumed once |
|
||||
| **consumers** | any number; each gets all items | one receiver per item |
|
||||
| **back-pressure** | built-in via rendezvous | configurable (rendezvous / buffered / unlimited) |
|
||||
| **typical use** | transform pipelines, sequences | producer–consumer, fan-out |
|
||||
|
||||
## See also
|
||||
|
||||
- [parallelism] — `launch`, `Deferred`, `Mutex`, `Flow`, and the full concurrency picture
|
||||
- [Flow] — cold async sequences
|
||||
|
||||
[Flow]: parallelism.md#flow
|
||||
[parallelism]: parallelism.md
|
||||
@ -1,82 +0,0 @@
|
||||
# Complex Numbers (`lyng.complex`)
|
||||
|
||||
`lyng.complex` adds a pure-Lyng `Complex` type backed by `Real` components.
|
||||
|
||||
Import it when you want ordinary complex arithmetic:
|
||||
|
||||
```lyng
|
||||
import lyng.complex
|
||||
```
|
||||
|
||||
## Construction
|
||||
|
||||
Use any of these:
|
||||
|
||||
```lyng
|
||||
import lyng.complex
|
||||
|
||||
val a = Complex(1.0, 2.0)
|
||||
val b = complex(1.0, 2.0)
|
||||
val c = 2.i
|
||||
val d = 3.re
|
||||
|
||||
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
|
||||
```
|
||||
|
||||
Convenience extensions:
|
||||
|
||||
- `Int.re`, `Real.re`: embed a real value into the complex plane
|
||||
- `Int.i`, `Real.i`: create a pure imaginary value
|
||||
- `cis(angle)`: shorthand for `cos(angle) + i sin(angle)`
|
||||
|
||||
## Core Operations
|
||||
|
||||
`Complex` supports:
|
||||
|
||||
- `+`
|
||||
- `-`
|
||||
- `*`
|
||||
- `/`
|
||||
- unary `-`
|
||||
- `conjugate`
|
||||
- `magnitude`
|
||||
- `phase`
|
||||
|
||||
Mixed arithmetic with `Int` and `Real` is enabled through `lyng.operators`, so both sides work naturally:
|
||||
|
||||
```lyng
|
||||
import lyng.complex
|
||||
|
||||
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
|
||||
assertEquals(Complex(1.5, 2.0), 1.5 + 2.i)
|
||||
assertEquals(Complex(2.0, 2.0), 2.i + 2)
|
||||
```
|
||||
|
||||
Mixed equality with built-in numeric types is intentionally not promised yet. Keep equality checks in the `Complex` domain for now.
|
||||
|
||||
## Transcendental Functions
|
||||
|
||||
For now, use member-style calls:
|
||||
|
||||
```lyng
|
||||
import lyng.complex
|
||||
|
||||
val z = 1 + π.i
|
||||
val w = z.exp()
|
||||
val s = z.sin()
|
||||
val r = z.sqrt()
|
||||
```
|
||||
|
||||
This is deliberate. Lyng already has built-in top-level real-valued functions such as `exp(x)` and `sin(x)`, and imported modules do not currently replace those root bindings. So plain `exp(z)` is not yet the right extension mechanism for complex math.
|
||||
|
||||
## Design Scope
|
||||
|
||||
This module intentionally uses `Complex` with `Real` parts, not `Complex<T>`.
|
||||
|
||||
Reasons:
|
||||
|
||||
- the existing math runtime is `Real`-centric
|
||||
- the operator interop registry works with concrete runtime classes
|
||||
- transcendental functions (`exp`, `sin`, `ln`, `sqrt`) are defined over the `Real` math backend here
|
||||
|
||||
If Lyng later gets a more general numeric-trait or callable-overload registry, a generic algebraic `Complex<T>` can be revisited on firmer ground.
|
||||
@ -8,9 +8,9 @@ Import it when you need decimal arithmetic that should not inherit `Real`'s bina
|
||||
import lyng.decimal
|
||||
```
|
||||
|
||||
## What `Decimal` Is For
|
||||
## What `BigDecimal` Is For
|
||||
|
||||
Use `Decimal` when values are fundamentally decimal:
|
||||
Use `BigDecimal` when values are fundamentally decimal:
|
||||
|
||||
- money
|
||||
- human-entered quantities
|
||||
@ -38,8 +38,8 @@ assertEquals("2.2", c.toStringExpanded())
|
||||
|
||||
The three forms mean different things:
|
||||
|
||||
- `1.d`: convert `Int -> Decimal`
|
||||
- `2.2.d`: convert `Real -> Decimal`
|
||||
- `1.d`: convert `Int -> BigDecimal`
|
||||
- `2.2.d`: convert `Real -> BigDecimal`
|
||||
- `"2.2".d`: parse exact decimal text
|
||||
|
||||
That distinction is intentional.
|
||||
@ -67,36 +67,16 @@ The explicit factory methods are:
|
||||
```lyng
|
||||
import lyng.decimal
|
||||
|
||||
Decimal.fromInt(10)
|
||||
Decimal.fromReal(2.5)
|
||||
Decimal.fromString("12.34")
|
||||
BigDecimal.fromInt(10)
|
||||
BigDecimal.fromReal(2.5)
|
||||
BigDecimal.fromString("12.34")
|
||||
```
|
||||
|
||||
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
||||
|
||||
## From Kotlin
|
||||
|
||||
If you already have an ionspin `BigDecimal` on the host side, the simplest supported way to create a Lyng `Decimal` is:
|
||||
|
||||
```kotlin
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.newDecimal
|
||||
|
||||
val scope = EvalSession().getScope()
|
||||
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `newDecimal(...)` loads `lyng.decimal` if needed
|
||||
- it returns a real Lyng `Decimal` object instance
|
||||
- this is the preferred Kotlin-side construction path when you already hold a host `BigDecimal`
|
||||
|
||||
## Core Operations
|
||||
|
||||
`Decimal` supports:
|
||||
`BigDecimal` supports:
|
||||
|
||||
- `+`
|
||||
- `-`
|
||||
@ -135,7 +115,7 @@ assert(2 == 2.d)
|
||||
assert(3 > 2.d)
|
||||
```
|
||||
|
||||
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + Decimal` and `Real + Decimal`.
|
||||
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + BigDecimal` and `Real + BigDecimal`.
|
||||
|
||||
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
||||
|
||||
@ -166,17 +146,6 @@ assertEquals(2.9, "2.9".d.toReal())
|
||||
|
||||
Use `toReal()` only when you are willing to return to binary floating-point semantics.
|
||||
|
||||
## Non-Finite Checks
|
||||
|
||||
`Decimal` values are always finite, so these helpers exist for API symmetry with `Real` and always return `false`:
|
||||
|
||||
```lyng
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals(false, "2.9".d.isInfinite())
|
||||
assertEquals(false, "2.9".d.isNaN())
|
||||
```
|
||||
|
||||
## Division Context
|
||||
|
||||
Division is the operation where precision and rounding matter most.
|
||||
@ -254,50 +223,6 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (
|
||||
|
||||
## Recommended Usage Rules
|
||||
|
||||
## Decimal With Stdlib Math Functions
|
||||
|
||||
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow`
|
||||
now also accept `Decimal`.
|
||||
|
||||
Current behavior is intentionally split:
|
||||
|
||||
- exact decimal implementation:
|
||||
- `abs(x)`
|
||||
- `floor(x)`
|
||||
- `ceil(x)`
|
||||
- `round(x)`
|
||||
- `pow(x, y)` when `x` is `Decimal` and `y` is an integral exponent
|
||||
- temporary bridge through `Real`:
|
||||
- `sin`, `cos`, `tan`
|
||||
- `asin`, `acos`, `atan`
|
||||
- `sinh`, `cosh`, `tanh`
|
||||
- `asinh`, `acosh`, `atanh`
|
||||
- `exp`, `ln`, `log10`, `log2`
|
||||
- `sqrt`
|
||||
- `pow` for the remaining non-integral decimal exponent cases
|
||||
|
||||
The temporary bridge is:
|
||||
|
||||
```lyng
|
||||
Decimal -> Real -> host math -> Decimal
|
||||
```
|
||||
|
||||
This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
|
||||
paths over time.
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals("2.5", (abs("-2.5".d) as Decimal).toStringExpanded())
|
||||
assertEquals("2", (floor("2.9".d) as Decimal).toStringExpanded())
|
||||
|
||||
// Temporary Real bridge:
|
||||
assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as Decimal).toStringExpanded())
|
||||
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as Decimal).toStringExpanded())
|
||||
```
|
||||
|
||||
If you care about exact decimal source text:
|
||||
|
||||
```lyng
|
||||
|
||||
@ -55,23 +55,6 @@ Here is the sample:
|
||||
assertEquals( (1..3).joinToString { it * 10 }, "10 20 30")
|
||||
>>> void
|
||||
|
||||
## joinAll
|
||||
|
||||
`joinAll()` is an `Iterable<Deferred>` helper that awaits every deferred in iteration order and returns a `List`
|
||||
with the collected results.
|
||||
|
||||
val jobs = (1..4).map { n ->
|
||||
launch { n * n }
|
||||
}
|
||||
assertEquals([1, 4, 9, 16], jobs.joinAll())
|
||||
>>> void
|
||||
|
||||
Notes:
|
||||
|
||||
- it does not start any task by itself; it only awaits the deferreds already present in the iterable.
|
||||
- awaiting happens in iteration order, so the result list keeps the same order as the input iterable.
|
||||
- if any deferred fails or was cancelled, that `await()` error is propagated from `joinAll()`.
|
||||
|
||||
## `sum` and `sumOf`
|
||||
|
||||
These, again, does the thing:
|
||||
@ -201,7 +184,6 @@ Search for the first element that satisfies the given predicate:
|
||||
| sortedWith(comparator) | sort using a comparator that compares elements (1) |
|
||||
| sortedBy(predicate) | sort by comparing results of the predicate function |
|
||||
| joinToString(s,t) | convert iterable to string, see (2) |
|
||||
| joinAll() | for `Iterable<Deferred>`, await all items in order and collect results to [List] |
|
||||
| reversed() | create a list containing items from this in reverse order |
|
||||
| shuffled() | create a list of shuffled elements |
|
||||
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
# LaunchPool
|
||||
|
||||
`LaunchPool` is a bounded-concurrency task pool: you submit lambdas with `launch`, and the pool runs them using a fixed number of worker coroutines.
|
||||
|
||||
## Constructor
|
||||
|
||||
```
|
||||
LaunchPool(maxWorkers, maxQueueSize = Channel.UNLIMITED)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `maxWorkers` | Maximum number of tasks that run in parallel. |
|
||||
| `maxQueueSize` | Maximum number of tasks that may wait in the queue. When the queue is full, `launch` suspends the caller until space becomes available. Defaults to `Channel.UNLIMITED` (no bound). |
|
||||
|
||||
## Methods
|
||||
|
||||
### `launch(lambda): Deferred`
|
||||
|
||||
Schedules `lambda` for execution and returns a `Deferred` for its result.
|
||||
|
||||
- Suspends if the queue is full (`maxQueueSize` reached).
|
||||
- Throws `IllegalStateException` if the pool is already closed or cancelled.
|
||||
- Any exception thrown by `lambda` is captured in the returned `Deferred` and **does not escape the pool**.
|
||||
|
||||
```lyng
|
||||
val pool = LaunchPool(4)
|
||||
val d1 = pool.launch { computeSomething() }
|
||||
val d2 = pool.launch { computeOther() }
|
||||
pool.closeAndJoin()
|
||||
println(d1.await())
|
||||
println(d2.await())
|
||||
```
|
||||
|
||||
### `closeAndJoin()`
|
||||
|
||||
Stops accepting new tasks and suspends until all queued and running tasks complete normally. After this call, any further `launch` throws `IllegalStateException`. Idempotent — safe to call multiple times.
|
||||
|
||||
### `cancel()`
|
||||
|
||||
Immediately closes the queue and cancels all worker coroutines. Queued but unstarted tasks are discarded. After this call, `launch` throws `IllegalStateException`. Idempotent.
|
||||
|
||||
### `cancelAndJoin()`
|
||||
|
||||
Like `cancel()`, but also suspends until all worker coroutines have stopped. Useful when you need to be sure no worker code is still running before proceeding. Idempotent.
|
||||
|
||||
## Exception handling
|
||||
|
||||
Exceptions from submitted lambdas are captured per-task in the returned `Deferred`. The pool itself continues running after a task failure:
|
||||
|
||||
```lyng
|
||||
val pool = LaunchPool(2)
|
||||
val good = pool.launch { 42 }
|
||||
val bad = pool.launch { throw IllegalArgumentException("boom") }
|
||||
pool.closeAndJoin()
|
||||
|
||||
assertEquals(42, good.await())
|
||||
assertThrows(IllegalArgumentException) { bad.await() }
|
||||
```
|
||||
|
||||
## Bounded queue / back-pressure
|
||||
|
||||
When `maxQueueSize` is set, the producer suspends if the queue fills up, providing automatic back-pressure:
|
||||
|
||||
```lyng
|
||||
// 1 worker, queue of 2 — producer can be at most 2 tasks ahead of what's running
|
||||
val pool = LaunchPool(1, 2)
|
||||
val d1 = pool.launch { delay(10); "a" }
|
||||
val d2 = pool.launch { delay(10); "b" }
|
||||
val d3 = pool.launch { delay(10); "c" } // suspends until d1 is picked up by the worker
|
||||
pool.closeAndJoin()
|
||||
```
|
||||
|
||||
## Collecting all results
|
||||
|
||||
`launch` returns a `Deferred`, so you can collect results with `joinAll()`:
|
||||
|
||||
```lyng
|
||||
val pool = LaunchPool(4)
|
||||
val jobs = (1..10).map { n -> pool.launch { n * n } }
|
||||
pool.closeAndJoin()
|
||||
val results = jobs.joinAll()
|
||||
// results == [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
|
||||
```
|
||||
|
||||
## Concurrency limit in practice
|
||||
|
||||
With `maxWorkers = 2`, at most 2 tasks run simultaneously regardless of how many are queued:
|
||||
|
||||
```lyng
|
||||
val mu = Mutex()
|
||||
var active = 0
|
||||
var maxSeen = 0
|
||||
|
||||
val pool = LaunchPool(2)
|
||||
(1..8).map {
|
||||
pool.launch {
|
||||
mu.withLock { active++; if (active > maxSeen) maxSeen = active }
|
||||
delay(5)
|
||||
mu.withLock { active-- }
|
||||
}
|
||||
}
|
||||
pool.closeAndJoin()
|
||||
assert(maxSeen <= 2)
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [parallelism.md](parallelism.md) — `launch`, `Deferred`, `Mutex`, `Channel`, and coroutine basics
|
||||
- [Channel.md](Channel.md) — the underlying channel primitive used by `LaunchPool`
|
||||
@ -1,66 +0,0 @@
|
||||
# Legacy Digest Functions (`lyng.legacy_digest`)
|
||||
|
||||
> ⚠️ **Security warning:** The functions in this module use cryptographically broken
|
||||
> algorithms. Do **not** use them for passwords, digital signatures, integrity
|
||||
> verification against adversarial tampering, or any other security-sensitive
|
||||
> purpose. They exist solely for compatibility with legacy protocols and file
|
||||
> formats that require specific hash values.
|
||||
|
||||
Import when you need to produce a SHA-1 digest for an existing protocol or format:
|
||||
|
||||
```lyng
|
||||
import lyng.legacy_digest
|
||||
```
|
||||
|
||||
## `LegacyDigest` Object
|
||||
|
||||
### `sha1(data): String`
|
||||
|
||||
Computes the SHA-1 digest of `data` and returns it as a 40-character lowercase
|
||||
hex string.
|
||||
|
||||
`data` can be:
|
||||
|
||||
| Type | Behaviour |
|
||||
|----------|----------------------------------------|
|
||||
| `String` | Encoded as UTF-8, then hashed |
|
||||
| `Buffer` | Raw bytes hashed directly |
|
||||
| anything | Falls back to `toString()` then UTF-8 |
|
||||
|
||||
```lyng
|
||||
import lyng.legacy_digest
|
||||
|
||||
// String input
|
||||
val h = LegacyDigest.sha1("abc")
|
||||
assertEquals("a9993e364706816aba3e25717850c26c9cd0d89d", h)
|
||||
|
||||
// Empty string
|
||||
assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", LegacyDigest.sha1(""))
|
||||
```
|
||||
|
||||
```lyng
|
||||
import lyng.legacy_digest
|
||||
import lyng.buffer
|
||||
|
||||
// Buffer input (raw bytes)
|
||||
val buf = Buffer.decodeHex("616263") // 0x61 0x62 0x63 = "abc"
|
||||
assertEquals("a9993e364706816aba3e25717850c26c9cd0d89d", LegacyDigest.sha1(buf))
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Pure Kotlin/KMP — no native libraries or extra dependencies.
|
||||
- Follows FIPS 180-4.
|
||||
- The output is always lowercase hex, never uppercase or binary.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `lyng.legacy_digest` only when an external system you cannot change requires
|
||||
a SHA-1 value, for example:
|
||||
|
||||
- old git-style content addresses
|
||||
- some OAuth 1.0 / HMAC-SHA1 signature schemes
|
||||
- legacy file checksums defined in published specs
|
||||
|
||||
For any new design choose a current hash function (SHA-256 or better) once
|
||||
Lyng adds a `lyng.digest` module.
|
||||
25
docs/List.md
25
docs/List.md
@ -30,13 +30,6 @@ There is a shortcut for the last:
|
||||
|
||||
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
|
||||
|
||||
The language also allows multi-selector indexing syntax such as `value[i, j]`, but `List` itself uses a single selector only:
|
||||
|
||||
- `list[index]` for one element
|
||||
- `list[range]` for a slice copy
|
||||
|
||||
Multi-selector indexing is intended for custom indexers such as `Matrix`.
|
||||
|
||||
## Concatenation
|
||||
|
||||
You can concatenate lists or iterable objects:
|
||||
@ -45,16 +38,6 @@ You can concatenate lists or iterable objects:
|
||||
assert( [4,5] + (1..3) == [4, 5, 1, 2, 3])
|
||||
>>> void
|
||||
|
||||
## Constructing lists
|
||||
|
||||
Besides literals, you can build a list by size using `List.fill`:
|
||||
|
||||
val squares = List.fill(5) { i -> i * i }
|
||||
assertEquals([0, 1, 4, 9, 16], squares)
|
||||
>>> void
|
||||
|
||||
`List.fill(size) { ... }` calls the block once for each index from `0` to `size - 1` and returns a new mutable list.
|
||||
|
||||
## Appending
|
||||
|
||||
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
|
||||
@ -174,9 +157,6 @@ List could be sorted in place, just like [Collection] provide sorted copies, in
|
||||
| `[index]` | get or set element at index | Int |
|
||||
| `[Range]` | get slice of the array (copy) | Range |
|
||||
| `+=` | append element(s) (2) | List or Obj |
|
||||
| `List.fill(size, block)` | build a new list from indices `0..<size` | Int, Callable |
|
||||
| `List.fill(size,capacity,block)` | same, pre-allocating capacity slots | Int, Int, Callable |
|
||||
| `ensureCapacity(count)` | pre-allocate storage for at least `count` elements without reallocation (5) | Int |
|
||||
| `sort()` | in-place sort, natural order | void |
|
||||
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
||||
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
||||
@ -199,11 +179,6 @@ order, e.g. is same as `list.sortWith { a,b -> predicate(a) <=> predicate(b) }`
|
||||
positive if first is greater, and zero if they are equal. For example, the equvalent comparator
|
||||
for `sort()` will be `sort { a, b -> a <=> b }
|
||||
|
||||
(5)
|
||||
: if the current capacity is already ≥ `count`, this is a no-op. Otherwise the internal storage
|
||||
is reallocated to hold at least `count` elements. Use this before a bulk `+=` loop to avoid
|
||||
repeated reallocations. `List.fill(size, capacity, block)` calls this automatically.
|
||||
|
||||
It inherits from [Iterable] too and thus all iterable methods are applicable to any list.
|
||||
|
||||
## Observable list hooks
|
||||
|
||||
192
docs/Matrix.md
192
docs/Matrix.md
@ -1,192 +0,0 @@
|
||||
# Matrix (`lyng.matrix`)
|
||||
|
||||
`lyng.matrix` adds dense immutable `Matrix` and `Vector` types for linear algebra.
|
||||
|
||||
Import it when you need matrix or vector arithmetic:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
```
|
||||
|
||||
## Construction
|
||||
|
||||
Create vectors from a flat list and matrices from nested row lists:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val v: Vector = vector([1, 2, 3])
|
||||
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
assertEquals([1.0, 2.0, 3.0], v.toList())
|
||||
assertEquals([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], m.toList())
|
||||
```
|
||||
|
||||
Factory methods are also available:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val z: Vector = Vector.zeros(3)
|
||||
val i: Matrix = Matrix.identity(3)
|
||||
val m: Matrix = Matrix.zeros(2, 4)
|
||||
```
|
||||
|
||||
All elements are standard double-precision numeric values internally.
|
||||
|
||||
## Shapes
|
||||
|
||||
Matrices may have any rectangular geometry:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
assertEquals(2, a.rows)
|
||||
assertEquals(3, a.cols)
|
||||
assertEquals([2, 3], a.shape)
|
||||
assertEquals(false, a.isSquare)
|
||||
```
|
||||
|
||||
Vectors expose:
|
||||
|
||||
- `size`
|
||||
- `length` as an alias of `size`
|
||||
|
||||
## Matrix Operations
|
||||
|
||||
Supported matrix operations:
|
||||
|
||||
- `+` and `-` for element-wise matrix arithmetic
|
||||
- `*` for matrix-matrix product
|
||||
- `*` and `/` by a scalar
|
||||
- `transpose()`
|
||||
- `trace()`
|
||||
- `rank()`
|
||||
- `determinant()`
|
||||
- `inverse()`
|
||||
- `solve(rhs)` for `Vector` or `Matrix` right-hand sides
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]])
|
||||
val product: Matrix = a * b
|
||||
assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList())
|
||||
assertEquals([[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]], a.transpose().toList())
|
||||
```
|
||||
|
||||
Inverse and solve:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[4, 7], [2, 6]])
|
||||
val rhs: Vector = vector([1, 0])
|
||||
|
||||
val inv: Matrix = a.inverse()
|
||||
val x: Vector = a.solve(rhs)
|
||||
|
||||
assert(abs(a.determinant() - 10.0) < 1e-9)
|
||||
assert(abs(inv.get(0, 0) - 0.6) < 1e-9)
|
||||
assert(abs(x.get(0) - 0.6) < 1e-9)
|
||||
```
|
||||
|
||||
## Vector Operations
|
||||
|
||||
Supported vector operations:
|
||||
|
||||
- `+` and `-`
|
||||
- scalar `*` and `/`
|
||||
- `dot(other)`
|
||||
- `norm()`
|
||||
- `normalize()`
|
||||
- `cross(other)` for 3D vectors
|
||||
- `outer(other)` producing a matrix
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Vector = vector([1, 2, 3])
|
||||
val b: Vector = vector([2, 0, 0])
|
||||
|
||||
assertEquals(2.0, a.dot(b))
|
||||
assertEquals([0.2672612419124244, 0.5345224838248488, 0.8017837257372732], a.normalize().toList())
|
||||
```
|
||||
|
||||
## Indexing and Slicing
|
||||
|
||||
`Matrix` supports both method-style indexing and bracket syntax.
|
||||
|
||||
Scalar access:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
assertEquals(6.0, m.get(1, 2))
|
||||
assertEquals(6.0, m[1, 2])
|
||||
```
|
||||
|
||||
Bracket indexing accepts two selectors: `[row, col]`.
|
||||
Each selector may be either:
|
||||
|
||||
- an `Int`
|
||||
- a `Range`
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val m: Matrix = matrix([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
|
||||
|
||||
assertEquals(7.0, m[1, 2])
|
||||
val columnSlice: Matrix = m[0..2, 2]
|
||||
val topLeft: Matrix = m[0..1, 0..1]
|
||||
val tail: Matrix = m[1.., 1..]
|
||||
assertEquals([[3.0], [7.0], [11.0]], columnSlice.toList())
|
||||
assertEquals([[1.0, 2.0], [5.0, 6.0]], topLeft.toList())
|
||||
assertEquals([[6.0, 7.0, 8.0], [10.0, 11.0, 12.0]], tail.toList())
|
||||
```
|
||||
|
||||
Shape rules:
|
||||
|
||||
- `m[Int, Int]` returns a `Real`
|
||||
- `m[Range, Int]` returns an `Nx1` `Matrix`
|
||||
- `m[Int, Range]` returns a `1xM` `Matrix`
|
||||
- `m[Range, Range]` returns a submatrix
|
||||
|
||||
Open-ended ranges are supported:
|
||||
|
||||
- `m[..1, ..1]`
|
||||
- `m[1.., 1..]`
|
||||
- `m[.., 2]`
|
||||
|
||||
Stepped ranges are not supported in matrix slicing.
|
||||
|
||||
Slices currently return new matrices, not views.
|
||||
|
||||
## Rows and Columns
|
||||
|
||||
If you want plain lists instead of a sliced matrix:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
assertEquals([4.0, 5.0, 6.0], a.row(1))
|
||||
assertEquals([2.0, 5.0], a.column(1))
|
||||
```
|
||||
|
||||
## Backend Notes
|
||||
|
||||
The matrix module uses a platform-specific backend where available and falls back to pure Kotlin where needed.
|
||||
|
||||
The public Lyng API stays the same across platforms.
|
||||
192
docs/OOP.md
192
docs/OOP.md
@ -454,43 +454,6 @@ Key rules and features:
|
||||
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)`.
|
||||
- Qualified access does not relax visibility.
|
||||
|
||||
### Receiver-stack lambdas
|
||||
|
||||
Qualified `this@Type` is also used outside inheritance when a lambda has multiple visible receivers.
|
||||
This is common in DSL-style builders.
|
||||
|
||||
- `A & B` means one receiver value that implements both types.
|
||||
- `context(A, B) C.()->R` means a receiver stack:
|
||||
- primary `this` is `C`
|
||||
- outer/context receivers are `A`, then `B`
|
||||
- Unqualified lookup checks the primary receiver first.
|
||||
- If the primary receiver does not define a member and several outer/context receivers do, Lyng reports a compile-time ambiguity. Use `this@Type` to select one explicitly.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
class Html { fun title() = "html" }
|
||||
class Head { fun title() = "head" }
|
||||
class Body
|
||||
|
||||
val block: context(Html, Head) Body.()->String = {
|
||||
// title() // compile-time ambiguity: Html vs Head
|
||||
this@Html.title()
|
||||
}
|
||||
```
|
||||
|
||||
Context receivers can also constrain extension functions. The extension is visible only when the required receiver is
|
||||
already in the implicit receiver stack:
|
||||
|
||||
```lyng
|
||||
class Tag { fun addText(text: String) { /* ... */ } }
|
||||
|
||||
context(Tag)
|
||||
fun String.unaryPlus() {
|
||||
this@Tag.addText(this)
|
||||
}
|
||||
```
|
||||
|
||||
- Field inheritance (`val`/`var`) and collisions
|
||||
- Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base).
|
||||
- Qualified read/write (via `this@Type` or casts) targets the chosen ancestor’s storage.
|
||||
@ -662,14 +625,10 @@ Unary operators are overloaded by defining methods with no arguments:
|
||||
|
||||
| Operator | Method Name |
|
||||
| :--- | :--- |
|
||||
| `+a` | `unaryPlus()` |
|
||||
| `-a` | `negate()` |
|
||||
| `!a` | `logicalNot()` |
|
||||
| `~a` | `bitNot()` |
|
||||
|
||||
`unaryPlus()` is useful in DSL-style builders where `+"text"` should append text to
|
||||
the current receiver. See [samples/html_builder_dsl.lyng](samples/html_builder_dsl.lyng).
|
||||
|
||||
### Assignment Operators
|
||||
|
||||
Assignment operators like `+=` first attempt to call a specific assignment method. If that method is not defined, they fall back to a combination of the binary operator and a regular assignment (e.g., `a = a + b`).
|
||||
@ -1042,9 +1001,9 @@ Static fields can be accessed from static methods via the class qualifier:
|
||||
assertEquals("bar", Test.getData() )
|
||||
>>> void
|
||||
|
||||
# Extension members
|
||||
# Extending classes
|
||||
|
||||
Sometimes an existing type or named singleton object is missing some particular functionality that can be _added to it_ without rewriting its inner logic and without using its private state. In this case, _extension members_ can be used.
|
||||
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension members_ could be used.
|
||||
|
||||
## Extension methods
|
||||
|
||||
@ -1061,133 +1020,6 @@ For example, we want to create an extension method that would test if a value ca
|
||||
assert( ! "5.2".isInteger() )
|
||||
>>> void
|
||||
|
||||
Extension methods normally act like instance members. If declared as `static`, they are called on the type object itself:
|
||||
|
||||
```lyng
|
||||
static fun List<T>.fill(size: Int, block: (Int)->T): List<T> { ... }
|
||||
|
||||
val tens = List.fill(5) { it * 10 }
|
||||
```
|
||||
|
||||
## Extending singleton `object` declarations
|
||||
|
||||
Named singleton objects can also be extension receivers. Use the object name as the receiver type:
|
||||
|
||||
```lyng
|
||||
object Config {
|
||||
fun base() = "cfg"
|
||||
}
|
||||
|
||||
fun Config.describe(value) {
|
||||
this.base() + ":" + value.toString()
|
||||
}
|
||||
|
||||
val Config.tag get() = this.base() + ":tag"
|
||||
|
||||
assertEquals("cfg:42", Config.describe(42))
|
||||
assertEquals("cfg:tag", Config.tag)
|
||||
```
|
||||
|
||||
This differs from extending a class in one important way:
|
||||
|
||||
- `fun Point.foo()` adds a member-like extension for all `Point` instances.
|
||||
- `fun Config.foo()` adds a member-like extension only for the single named object `Config`.
|
||||
|
||||
The same rules still apply:
|
||||
|
||||
- Extensions on singleton objects are scope-isolated just like class extensions.
|
||||
- They cannot access the object's `private` members.
|
||||
- Inside the extension body, `this` is the singleton object itself.
|
||||
|
||||
## Extension indexers
|
||||
|
||||
Bracket syntax is just another form of member dispatch. When you write `value[i]` or `value[i] = x`, Lyng lowers it to `getAt(...)` and `putAt(...)`.
|
||||
|
||||
That means indexers can be extended in the same way as methods and properties.
|
||||
|
||||
### Extending indexers on classes
|
||||
|
||||
Use `override fun Type.getAt(...)` and `override fun Type.putAt(...)`:
|
||||
|
||||
```lyng
|
||||
class BoxStore {
|
||||
val data = {"name": "alice"}
|
||||
}
|
||||
|
||||
override fun BoxStore.getAt(key: String): Object? {
|
||||
data[key]
|
||||
}
|
||||
|
||||
override fun BoxStore.putAt(key: String, value: Object) {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
val store = BoxStore()
|
||||
assertEquals("alice", store["name"])
|
||||
store["name"] = "bob"
|
||||
assertEquals("bob", store["name"])
|
||||
```
|
||||
|
||||
As with other extension members, this does not modify the original class declaration. It adds indexer behavior only in the scope where the extension is visible.
|
||||
|
||||
### Extending indexers on singleton `object` declarations
|
||||
|
||||
Named singleton objects work the same way:
|
||||
|
||||
```lyng
|
||||
object Storage
|
||||
|
||||
var storageData = {}
|
||||
|
||||
override fun Storage.getAt(key: String): Object? {
|
||||
storageData[key]
|
||||
}
|
||||
|
||||
override fun Storage.putAt(key: String, value: Object) {
|
||||
storageData[key] = value
|
||||
}
|
||||
|
||||
Storage["name"] = "alice"
|
||||
val name: String? = Storage["name"]
|
||||
assertEquals("alice", name)
|
||||
```
|
||||
|
||||
This is the indexer equivalent of `fun Config.foo()`: the extension applies to that single named object, not to all instances of some class.
|
||||
|
||||
### Selector packing
|
||||
|
||||
Index syntax can contain more than one selector:
|
||||
|
||||
```lyng
|
||||
value[i]
|
||||
value[i, j]
|
||||
```
|
||||
|
||||
The same packing rules still apply for extension indexers:
|
||||
|
||||
- `value[i]` calls `getAt(i)` or `putAt(i, value)`
|
||||
- `value[i, j]` passes `[i, j]` as one list-like index object
|
||||
- `value[i, j, k]` passes `[i, j, k]`
|
||||
|
||||
So if you want multi-selector indexing, define the receiver to accept that packed index object.
|
||||
|
||||
### About types and generics
|
||||
|
||||
In practice, extension indexers are usually best declared with `Object?` for reads and `Object` for writes:
|
||||
|
||||
```lyng
|
||||
override fun Storage.getAt(key: String): Object? { ... }
|
||||
override fun Storage.putAt(key: String, value: Object) { ... }
|
||||
```
|
||||
|
||||
Then use the expected type at the call site:
|
||||
|
||||
```lyng
|
||||
val name: String? = Storage["name"]
|
||||
```
|
||||
|
||||
Explicit generic arguments do not fit naturally onto `[]` syntax, so typed assignment on read is usually the clearest approach.
|
||||
|
||||
## Extension properties
|
||||
|
||||
Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.
|
||||
@ -1351,24 +1183,8 @@ collection's sugar won't work with it:
|
||||
assertEquals("buzz", x[0])
|
||||
>>> void
|
||||
|
||||
Multiple selectors are packed into one list index object:
|
||||
|
||||
val x = dynamic {
|
||||
get {
|
||||
if( it == [1, 2] ) "hit"
|
||||
else null
|
||||
}
|
||||
}
|
||||
assertEquals("hit", x[1, 2])
|
||||
>>> void
|
||||
|
||||
So:
|
||||
|
||||
- `x[i]` passes `i`
|
||||
- `x[i, j]` passes `[i, j]`
|
||||
- `x[i, j, k]` passes `[i, j, k]`
|
||||
|
||||
This is the same rule used by Kotlin-backed `getAt` / `putAt` indexers in embedding.
|
||||
If you want dynamic to function like an array, create a [feature
|
||||
request](https://gitea.sergeych.net/SergeychWorks/lyng/issues).
|
||||
|
||||
# Theory
|
||||
|
||||
|
||||
@ -160,15 +160,15 @@ import lyng.decimal
|
||||
3 > 2.d
|
||||
```
|
||||
|
||||
work naturally even though `Int` and `Real` themselves were not edited to know `Decimal`.
|
||||
work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`.
|
||||
|
||||
The shape is:
|
||||
|
||||
- `leftClass = Int` or `Real`
|
||||
- `rightClass = Decimal`
|
||||
- `commonClass = Decimal`
|
||||
- convert built-ins into `Decimal`
|
||||
- leave `Decimal` values unchanged
|
||||
- `rightClass = BigDecimal`
|
||||
- `commonClass = BigDecimal`
|
||||
- convert built-ins into `BigDecimal`
|
||||
- leave `BigDecimal` values unchanged
|
||||
|
||||
## Step-By-Step Pattern For Your Own Type
|
||||
|
||||
|
||||
@ -25,23 +25,6 @@ Exclusive end ranges are adopted from kotlin either:
|
||||
assert(4 in r)
|
||||
>>> void
|
||||
|
||||
Descending finite ranges are explicit too:
|
||||
|
||||
val r = 5 downTo 1
|
||||
assert(r.isDescending)
|
||||
assert(r.toList() == [5,4,3,2,1])
|
||||
>>> void
|
||||
|
||||
Use `downUntil` when the lower bound should be excluded:
|
||||
|
||||
val r = 5 downUntil 1
|
||||
assert(r.toList() == [5,4,3,2])
|
||||
assert(1 !in r)
|
||||
>>> void
|
||||
|
||||
This is explicit by design: `5..1` is not treated as a reverse range. It is an
|
||||
ordinary ascending range with no values in it when iterated.
|
||||
|
||||
In any case, we can test an object to belong to using `in` and `!in` and
|
||||
access limits:
|
||||
|
||||
@ -90,23 +73,6 @@ but
|
||||
>>> 2
|
||||
>>> void
|
||||
|
||||
Descending ranges work in `for` loops exactly the same way:
|
||||
|
||||
for( i in 3 downTo 1 )
|
||||
println(i)
|
||||
>>> 3
|
||||
>>> 2
|
||||
>>> 1
|
||||
>>> void
|
||||
|
||||
And with an exclusive lower bound:
|
||||
|
||||
for( i in 3 downUntil 1 )
|
||||
println(i)
|
||||
>>> 3
|
||||
>>> 2
|
||||
>>> void
|
||||
|
||||
### Stepped ranges
|
||||
|
||||
Use `step` to change the iteration increment. The range bounds still define membership,
|
||||
@ -114,18 +80,9 @@ so iteration ends when the next value is no longer in the range.
|
||||
|
||||
assert( [1,3,5] == (1..5 step 2).toList() )
|
||||
assert( [1,3] == (1..<5 step 2).toList() )
|
||||
assert( [5,3,1] == (5 downTo 1 step 2).toList() )
|
||||
assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
|
||||
>>> void
|
||||
|
||||
Descending ranges still use a positive `step`; the direction comes from
|
||||
`downTo` / `downUntil`:
|
||||
|
||||
assert( ['e','c','a'] == ('e' downTo 'a' step 2).toList() )
|
||||
>>> void
|
||||
|
||||
A negative step with `downTo` / `downUntil` is invalid.
|
||||
|
||||
Real ranges require an explicit step:
|
||||
|
||||
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
|
||||
@ -140,7 +97,7 @@ Open-ended ranges require an explicit step to iterate:
|
||||
|
||||
You can use Char as both ends of the closed range:
|
||||
|
||||
val r = 'a'..'c'
|
||||
val r = 'a' .. 'c'
|
||||
assert( 'b' in r)
|
||||
assert( 'e' !in r)
|
||||
for( ch in r )
|
||||
@ -162,7 +119,6 @@ Exclusive end char ranges are supported too:
|
||||
|-----------------|------------------------------|---------------|
|
||||
| contains(other) | used in `in` | Range, or Any |
|
||||
| isEndInclusive | true for '..' | Bool |
|
||||
| isDescending | true for `downTo`/`downUntil`| Bool |
|
||||
| isOpen | at any end | Bool |
|
||||
| isIntRange | both start and end are Int | Bool |
|
||||
| step | explicit iteration step | Any? |
|
||||
|
||||
@ -19,8 +19,6 @@ you can use it's class to ensure type:
|
||||
|-----------------|-------------------------------------------------------------|------|
|
||||
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
||||
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
|
||||
| `.isInfinite()` | true when the value is `Infinity` or `-Infinity` | Bool |
|
||||
| `.isNaN()` | true when the value is `NaN` | Bool |
|
||||
| `.clamp(range)` | clamp value within range boundaries | Real |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
@ -64,18 +64,6 @@ Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!
|
||||
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
||||
>>> void
|
||||
|
||||
Regex replacement is exposed on `String.replace` and `String.replaceFirst`:
|
||||
|
||||
assertEquals( "v#.#.#", "v1.2.3".replace( "\d+".re, "#" ) )
|
||||
assertEquals( "v[1].[2].[3]", "v1.2.3".replace( "(\d+)".re ) { m -> "[" + m[1] + "]" } )
|
||||
assertEquals( "year-04-08", "2026-04-08".replaceFirst( "\d+".re, "year" ) )
|
||||
>>> void
|
||||
|
||||
When `replace` takes a plain `String`, it is treated literally, not as a regex pattern:
|
||||
|
||||
assertEquals( "a-b-c", "a.b.c".replace( ".", "-" ) )
|
||||
>>> void
|
||||
|
||||
|
||||
# Regex class reference
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
# Lyng Language Reference for AI Agents (Current Compiler State)
|
||||
|
||||
[//]: # (excludeFromIndex)
|
||||
|
||||
Purpose: dense, implementation-first reference for generating valid Lyng code.
|
||||
|
||||
Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,Token,Compiler,Script,TypeDecl}.kt`, `lynglib/stdlib/lyng/root.lyng`, tests in `lynglib/src/commonTest` and `lynglib/src/jvmTest`.
|
||||
@ -15,17 +13,15 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
|
||||
## 2. Lexical Syntax
|
||||
- Comments: `// line`, `/* block */`.
|
||||
- Strings: `"..."` or `` `...` `` (supports escapes). Multiline string content is normalized by indentation logic.
|
||||
- AI generation preference: use `"..."` by default, including multiline strings; `"` strings are also multiline-capable and should be preferred for ordinary code/doc/SQL text. Use backtick strings mainly when the content contains many double quotes and backticks would make the source clearer.
|
||||
- Shared escapes: `\n`, `\r`, `\t`, `\\`, `\uXXXX` (4 hex digits).
|
||||
- Delimiter escapes: `\"` inside `"..."`, ``\` `` inside `` `...` ``.
|
||||
- Strings: `"..."` (supports escapes). Multiline string content is normalized by indentation logic.
|
||||
- Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits).
|
||||
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
|
||||
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`).
|
||||
- String interpolation is supported:
|
||||
- identifier form: `"$name"` or `` `$name` ``
|
||||
- expression form: `"${expr}"` or `` `${expr}` ``
|
||||
- escaped dollar: `"\$"`, `"$$"`, `` `\$` ``, and `` `$$` `` all produce literal `$`.
|
||||
- `\\$x` means backslash + interpolated `x` in either delimiter form.
|
||||
- identifier form: `"$name"`
|
||||
- expression form: `"${expr}"`
|
||||
- escaped dollar: `"\$"` and `"$$"` both produce literal `$`.
|
||||
- `\\$x` means backslash + interpolated `x`.
|
||||
- Per-file opt-out is supported via leading comment directive:
|
||||
- `// feature: interpolation: off`
|
||||
- with this directive, `$...` stays literal text.
|
||||
@ -52,10 +48,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- Range literals:
|
||||
- inclusive: `a..b`
|
||||
- exclusive end: `a..<b`
|
||||
- descending inclusive: `a downTo b`
|
||||
- descending exclusive end: `a downUntil b`
|
||||
- open-ended forms are supported (`a..`, `..b`, `..`).
|
||||
- optional step: `a..b step 2`, `a downTo b step 2`
|
||||
- optional step: `a..b step 2`
|
||||
- Lambda literal:
|
||||
- with params: `{ x, y -> x + y }`
|
||||
- implicit `it`: `{ it + 1 }`
|
||||
@ -83,23 +77,16 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
## 4. Operators (implemented)
|
||||
- Assignment: `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `?=`.
|
||||
- Logical: `||`, `&&`, unary `!`.
|
||||
- Unary arithmetic/bitwise: unary `+`, unary `-`, `~`.
|
||||
- Bitwise: `|`, `^`, `&`, `~`, shifts `<<`, `>>`.
|
||||
- Equality/comparison: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`, `<=>`, `=~`, `!~`.
|
||||
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
||||
- Null-safe family:
|
||||
- member access: `?.`
|
||||
- safe index: `?[i]`, `?[i, j]`
|
||||
- safe index: `?[i]`
|
||||
- safe invoke: `?(...)`
|
||||
- safe block invoke: `?{ ... }`
|
||||
- elvis: `?:` and `??`.
|
||||
- Increment/decrement: prefix and postfix `++`, `--`.
|
||||
- Indexing syntax:
|
||||
- single selector: `a[i]`
|
||||
- multiple selectors: `a[i, j, k]`
|
||||
- language-level indexing with multiple selectors is passed to `getAt`/`putAt` as one list-like index object, not as multiple method arguments.
|
||||
- indexers can also be supplied by extension members, including named singleton `object` receivers via `override fun Storage.getAt(...)` / `putAt(...)`.
|
||||
- example: `m[0..2, 2]`.
|
||||
|
||||
## 5. Declarations
|
||||
- Variables:
|
||||
@ -120,9 +107,6 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- shorthand: `fun f(x) = expr`.
|
||||
- generics: `fun f<T>(x: T): T`.
|
||||
- extension functions: `fun Type.name(...) { ... }`.
|
||||
- context-aware extension functions: `context(Tag) fun String.unaryPlus() { this@Tag.addText(this) }`.
|
||||
- named singleton `object` declarations can be extension receivers too: `fun Config.describe(...) { ... }`, `val Config.tag get() = ...`.
|
||||
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
|
||||
- delegated callable: `fun f(...) by delegate`.
|
||||
- Type aliases:
|
||||
- `type Name = TypeExpr`
|
||||
@ -138,9 +122,6 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
|
||||
## 6. Control Flow
|
||||
- `if` is expression-like.
|
||||
- `compile if (cond) { ... } else { ... }` is a compile-time-only conditional.
|
||||
- current condition grammar is restricted to `defined(NameOr.Package)`, `!`, `&&`, `||`, and parentheses.
|
||||
- the untaken branch is skipped by the compiler and is not name-resolved or type-checked.
|
||||
- `when(value) { ... }` supported.
|
||||
- branch conditions support equality, `in`, `!in`, `is`, `!is`, and `nullable` predicate.
|
||||
- `when { ... }` (subject-less) is currently not implemented.
|
||||
@ -166,12 +147,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- unions `A | B`
|
||||
- intersections `A & B`
|
||||
- function types `(A, B)->R` and receiver form `Receiver.(A)->R`
|
||||
- receiver-stack function types via `context(A, B) Receiver.(P)->R`
|
||||
- variadics in function type via ellipsis (`T...`)
|
||||
- `A & B` means one value implementing both types.
|
||||
- `context(A, B) Receiver.(P)->R` is different: it declares an ordered implicit-receiver stack where `Receiver` is primary `this`, then `A`, then `B`.
|
||||
- Nested receiver lambdas keep outer receivers in scope; unqualified lookup prefers the innermost receiver, and `this@Type` can select an outer/context receiver explicitly.
|
||||
- If the primary receiver does not provide a member and multiple outer/context receivers do, the lookup is a compile-time ambiguity and must be disambiguated with `this@Type`.
|
||||
- Generics:
|
||||
- type params on classes/functions/type aliases
|
||||
- bounds via `:` with union/intersection expressions
|
||||
@ -224,7 +200,6 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
||||
- Disambiguation helpers are supported:
|
||||
- qualified this: `this@Base.member()`
|
||||
- cast view: `(obj as Base).member()`
|
||||
- In nested receiver lambdas, `this@Type` can target any receiver visible through the receiver stack, not just inheritance ancestors.
|
||||
- On unknown receiver types, compiler allows only Object-safe members:
|
||||
- `toString`, `toInspectString`, `let`, `also`, `apply`, `run`
|
||||
- Other members require known receiver type or explicit cast.
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
# AI notes: publish JVM CLI updates with `bin/local_jrelease`
|
||||
|
||||
[//]: # (excludeFromIndex)
|
||||
|
||||
When a change affects the JVM CLI launcher used as `jlyng`, refresh the installed local distribution with:
|
||||
|
||||
```bash
|
||||
bin/local_jrelease
|
||||
```
|
||||
|
||||
Why:
|
||||
- `jlyng` in this repo is installed from `~/bin/jlyng-jvm/lyng-jvm`, not directly from `lyng/build/install`.
|
||||
- Manual copying from Gradle build output can leave the actual launcher on `PATH` stale.
|
||||
- `bin/local_jrelease` rebuilds `lyng/build/distributions/lyng-jvm.zip`, reinstalls it under `~/bin/jlyng-jvm`, and recreates the `~/bin/jlyng` symlink.
|
||||
@ -1,10 +0,0 @@
|
||||
# AI notes: heading levels must be consecutive
|
||||
|
||||
[//]: # (excludeFromIndex)
|
||||
|
||||
When editing repository documentation:
|
||||
|
||||
- Use heading levels in order: `#`, then `##`, then `###`, and so on.
|
||||
- Do not skip levels, for example `#` directly to `###`.
|
||||
- Keep the heading tree balanced inside each document; sibling sections should use the same level.
|
||||
- If you add a subsection and the parent is `##`, the child must be `###`.
|
||||
@ -1,7 +1,5 @@
|
||||
# AI notes: avoid Kotlin/Wasm invalid IR with suspend lambdas
|
||||
|
||||
[//]: # (excludeFromIndex)
|
||||
|
||||
## Do
|
||||
- Prefer explicit `object : Statement()` with `override suspend fun execute(...)` when building compiler statements.
|
||||
- Keep `Statement` objects non-lambda, especially in compiler hot paths like parsing/var declarations.
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
# Lyng Stdlib Reference for AI Agents (Compact)
|
||||
|
||||
[//]: # (excludeFromIndex)
|
||||
|
||||
Purpose: fast overview of what is available by default and what must be imported.
|
||||
|
||||
Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/stdlib/lyng/root.lyng`, `lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/observable_lyng.kt`.
|
||||
@ -16,14 +14,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- Assertions/tests: `assert`, `assertEquals`/`assertEqual`, `assertNotEquals`, `assertThrows`.
|
||||
- Preconditions: `require`, `check`.
|
||||
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
|
||||
- `Deferred.cancel()` cancels an active task.
|
||||
- `Deferred.await()` throws `CancellationException` if that task was cancelled.
|
||||
- `Iterable<Deferred>.joinAll()` awaits every deferred in iteration order and returns a `List` of results.
|
||||
- Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`.
|
||||
- These helpers also accept `lyng.decimal.Decimal`.
|
||||
- Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent.
|
||||
- Temporary Decimal path for the rest: convert `Decimal -> Real`, compute, then convert back to `Decimal`.
|
||||
- Treat that bridge as temporary; prefer native Decimal implementations when they become available.
|
||||
|
||||
## 3. Core Global Constants/Types
|
||||
- Values: `Unset`, `π`.
|
||||
@ -31,14 +22,13 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- Collections/types: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `Range`, `RingBuffer`.
|
||||
- Random: singleton `Random` and class `SeededRandom`.
|
||||
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
|
||||
- Async exception: `CancellationException`.
|
||||
- Delegation types: `Delegate`, `DelegateContext`.
|
||||
- Regex types: `Regex`, `RegexMatch`.
|
||||
- Also present: `Math.PI` namespace constant.
|
||||
|
||||
## 4. `lyng.stdlib` Module Surface (from `root.lyng`)
|
||||
### 4.1 Extern class declarations
|
||||
- Exceptions/delegation base: `Exception`, `CancellationException`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`.
|
||||
- Exceptions/delegation base: `Exception`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`.
|
||||
- Collections and iterables: `Iterable<T>`, `Iterator<T>`, `Collection<T>`, `Array<T>`, `List<T>`, `ImmutableList<T>`, `Set<T>`, `ImmutableSet<T>`, `Map<K,V>`, `ImmutableMap<K,V>`, `MapEntry<K,V>`, `RingBuffer<T>`.
|
||||
- Host iterator bridge: `KotlinIterator<T>`.
|
||||
- Random APIs: `extern object Random`, `extern class SeededRandom`.
|
||||
@ -47,8 +37,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
|
||||
- Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`.
|
||||
- Mapping/aggregation: `map`, `flatMap`, `flatten`, `sum`, `sumOf`, `minOf`, `maxOf`.
|
||||
- Ordering and list building: `sorted`, `sortedBy`, `shuffled`, `List.sort`, `List.sortBy`, `List.fill`.
|
||||
- `List.fill(size) { index -> ... }` constructs a new `List<T>` by evaluating the block once per index from `0` to `size - 1`.
|
||||
- Ordering: `sorted`, `sortedBy`, `shuffled`, `List.sort`, `List.sortBy`.
|
||||
- String helper: `joinToString`, `String.re`.
|
||||
|
||||
### 4.3 Delegation helpers
|
||||
@ -67,39 +56,20 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
## 5. Additional Built-in Modules (import explicitly)
|
||||
- `import lyng.observable`
|
||||
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
||||
- `import lyng.decimal`
|
||||
- `Decimal`, `DecimalContext`, `DecimalRounding`, `withDecimalContext(...)`.
|
||||
- Kotlin host helper: `ScopeFacade.newDecimal(BigDecimal)` wraps an ionspin host decimal as a Lyng `Decimal`.
|
||||
- `import lyng.complex`
|
||||
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
|
||||
- `import lyng.matrix`
|
||||
- `Matrix`, `Vector`, `matrix(rows)`, `vector(values)`, dense linear algebra, inversion, solving, and matrix slicing with `m[row, col]`.
|
||||
- `import lyng.buffer`
|
||||
- `Buffer`, `MutableBuffer`.
|
||||
- `import lyng.legacy_digest`
|
||||
- `LegacyDigest.sha1(data): String` — SHA-1 hex digest; `data` may be `String` (UTF-8) or `Buffer` (raw bytes).
|
||||
- ⚠️ Cryptographically broken. Use only for legacy protocol / file-format compatibility.
|
||||
- `import lyng.serialization`
|
||||
- `Lynon` serialization utilities.
|
||||
- `import lyng.time`
|
||||
- `Instant`, `Date`, `DateTime`, `Duration`, and module `delay`.
|
||||
- `Instant`, `DateTime`, `Duration`, and module `delay`.
|
||||
|
||||
## 6. Optional (lyngio) Modules
|
||||
Requires installing `lyngio` into the import manager from host code.
|
||||
- `import lyng.io.fs` (filesystem `Path` API)
|
||||
- `import lyng.io.process` (process execution API)
|
||||
- `import lyng.io.console` (console capabilities, geometry, ANSI/output, events)
|
||||
- `import lyng.io.http` (HTTP/HTTPS client API)
|
||||
- `import lyng.io.http.server` (minimal HTTP/1.1 and WebSocket server API)
|
||||
- `import lyng.io.ws` (WebSocket client API; currently supported on JVM, capability-gated elsewhere)
|
||||
- `import lyng.io.net` (TCP/UDP transport API; currently supported on JVM, capability-gated elsewhere)
|
||||
- `import lyng.io.html` (pure Lyng HTML builder DSL: `html { body { h3 { +"text" } } }`)
|
||||
- Shared network value-type packages are also available when installed by host code:
|
||||
- `import lyng.io.http.types` (`HttpHeaders`)
|
||||
- `import lyng.io.ws.types` (`WsMessage`)
|
||||
- `import lyng.io.net.types` (`IpVersion`, `SocketAddress`, `Datagram`)
|
||||
|
||||
## 7. AI Generation Tips
|
||||
- Assume `lyng.stdlib` APIs exist in regular script contexts.
|
||||
- For platform-sensitive code (`fs`, `process`, `console`, `http`, `ws`, `net`), gate assumptions and mention required module install.
|
||||
- For platform-sensitive code (`fs`, `process`, `console`), gate assumptions and mention required module install.
|
||||
- Prefer extension-method style (`items.filter { ... }`) and standard scope helpers (`let`/`also`/`apply`/`run`).
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
# Some resources to download
|
||||
|
||||
## Lync CLI tool
|
||||
|
||||
- [lyng-linuxX64.zip](/distributables/lyng-linuxX64.zip) CLI tool for linuxX64: nodependencies, small monolith executable binary.
|
||||
- [lyng-jvm.zip](/distributables/lyng-jvm.zip) JVM CLI distribution: download, unpack, and run `lyng-jvm/bin/lyng`.
|
||||
|
||||
## IDE plugins
|
||||
|
||||
- [lyng-textmate.zip](../../lyng/distributables/lyng-textmate.zip) Texmate-compatible bundle with syntax coloring (could be outdated)
|
||||
|
||||
- [lyng-idea-0.0.5-SNAPSHOT.zip](/distributables/lyng-idea-0.0.5-SNAPSHOT.zip) - plugin for IntelliJ-compatible IDE
|
||||
@ -1,7 +1,5 @@
|
||||
# Embedding Lyng in your Kotlin project
|
||||
|
||||
[//]: # (topMenu)
|
||||
|
||||
Lyng is a tiny, embeddable, Kotlin‑first scripting language. This page shows, step by step, how to:
|
||||
|
||||
- add Lyng to your build
|
||||
@ -38,60 +36,21 @@ dependencies {
|
||||
|
||||
If you use Kotlin Multiplatform, add the dependency in the `commonMain` source set (and platform‑specific sets if you need platform APIs).
|
||||
|
||||
### 2) Preferred runtime: `EvalSession`
|
||||
### 2) Create a runtime (Scope) and execute scripts
|
||||
|
||||
For host applications, prefer `EvalSession` as the main way to run scripts.
|
||||
It owns one reusable Lyng scope, serializes `eval(...)` calls, and governs coroutines started from Lyng `launch { ... }`.
|
||||
|
||||
Main entrypoints:
|
||||
|
||||
- `session.eval(code)` / `session.eval(source)`
|
||||
- `session.getScope()` when you need low-level binding APIs
|
||||
- `session.cancel()` to cancel active session-owned coroutines
|
||||
- `session.join()` to wait for active session-owned coroutines
|
||||
|
||||
```kotlin
|
||||
fun main() = kotlinx.coroutines.runBlocking {
|
||||
val session = EvalSession()
|
||||
|
||||
// Evaluate a one‑liner
|
||||
val result = session.eval("1 + 2 * 3")
|
||||
println("Lyng result: $result") // ObjReal/ObjInt etc.
|
||||
|
||||
// Optional lifecycle management
|
||||
session.join()
|
||||
}
|
||||
```
|
||||
|
||||
The session creates its underlying scope lazily. If you need raw low-level APIs, get the scope explicitly:
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
```
|
||||
|
||||
Use `cancel()` / `join()` to govern async work started by scripts:
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
session.eval("""launch { delay(1000); println("done") }""")
|
||||
session.cancel()
|
||||
session.join()
|
||||
```
|
||||
|
||||
### 2.1) Low-level runtime: `Scope`
|
||||
|
||||
Use `Scope` directly when you intentionally want lower-level control.
|
||||
The easiest way to get a ready‑to‑use scope with standard packages is via `Script.newScope()`.
|
||||
|
||||
```kotlin
|
||||
fun main() = kotlinx.coroutines.runBlocking {
|
||||
val scope = Script.newScope() // suspends on first init
|
||||
|
||||
// Evaluate a one‑liner
|
||||
val result = scope.eval("1 + 2 * 3")
|
||||
println("Lyng result: $result")
|
||||
println("Lyng result: $result") // ObjReal/ObjInt etc.
|
||||
}
|
||||
```
|
||||
|
||||
You can also pre‑compile a script and execute it multiple times on the same scope:
|
||||
You can also pre‑compile a script and execute it multiple times:
|
||||
|
||||
```kotlin
|
||||
val script = Compiler.compile("""
|
||||
@ -104,8 +63,7 @@ val run1 = script.execute(scope)
|
||||
val run2 = script.execute(scope)
|
||||
```
|
||||
|
||||
`Scope.eval("...")` is the low-level shortcut that compiles and executes on the given scope.
|
||||
For most embedding use cases, prefer `session.eval("...")`.
|
||||
`Scope.eval("...")` is a shortcut that compiles and executes on the given scope.
|
||||
|
||||
### 3) Preferred: bind extern globals from Kotlin
|
||||
|
||||
@ -127,8 +85,6 @@ import net.sergeych.lyng.bridge.*
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val im = Script.defaultImportManager.copy()
|
||||
im.addPackage("my.api") { module ->
|
||||
module.eval("""
|
||||
@ -193,9 +149,6 @@ binder.bindGlobalFunRaw("echoRaw") { _, args ->
|
||||
Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
// A function returning value
|
||||
scope.addFn<ObjInt>("inc") {
|
||||
val x = args.firstAndOnly() as ObjInt
|
||||
@ -214,7 +167,7 @@ scope.addVoidFn("log") {
|
||||
// }
|
||||
|
||||
// Call them from Lyng
|
||||
session.eval("val y = inc(41); log('Answer:', y)")
|
||||
scope.eval("val y = inc(41); log('Answer:', y)")
|
||||
```
|
||||
|
||||
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
||||
@ -230,79 +183,11 @@ Scope-backed Kotlin lambdas receive a `ScopeFacade` (not a full `Scope`). For mi
|
||||
|
||||
If you truly need the full `Scope` (e.g., for low-level interop), use `requireScope()` explicitly.
|
||||
|
||||
### 4.5) Indexers from Kotlin: `getAt` and `putAt`
|
||||
|
||||
Lyng bracket syntax is dispatched through `getAt` and `putAt`.
|
||||
|
||||
That means:
|
||||
|
||||
- `x[i]` calls `getAt(index)`
|
||||
- `x[i] = value` calls `putAt(index, value)` or `setAt(index, value)`
|
||||
- field-like `x["name"]` also uses the same index path unless you expose a real field/property
|
||||
|
||||
For Kotlin-backed classes, bind indexers as ordinary methods named `getAt` and `putAt`:
|
||||
|
||||
```kotlin
|
||||
moduleScope.eval("""
|
||||
extern class Grid {
|
||||
override fun getAt(index: List<Int>): Int
|
||||
override fun putAt(index: List<Int>, value: Int): void
|
||||
}
|
||||
""".trimIndent())
|
||||
|
||||
moduleScope.bind("Grid") {
|
||||
init { _ -> data = IntArray(4) }
|
||||
|
||||
addFun("getAt") {
|
||||
val index = args.requiredArg<ObjList>(0)
|
||||
val row = (index.list[0] as ObjInt).value.toInt()
|
||||
val col = (index.list[1] as ObjInt).value.toInt()
|
||||
val data = (thisObj as ObjInstance).data as IntArray
|
||||
ObjInt.of(data[row * 2 + col].toLong())
|
||||
}
|
||||
|
||||
addFun("putAt") {
|
||||
val index = args.requiredArg<ObjList>(0)
|
||||
val value = args.requiredArg<ObjInt>(1).value.toInt()
|
||||
val row = (index.list[0] as ObjInt).value.toInt()
|
||||
val col = (index.list[1] as ObjInt).value.toInt()
|
||||
val data = (thisObj as ObjInstance).data as IntArray
|
||||
data[row * 2 + col] = value
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage from Lyng:
|
||||
|
||||
```lyng
|
||||
val g = Grid()
|
||||
g[0, 1] = 42
|
||||
assertEquals(42, g[0, 1])
|
||||
```
|
||||
|
||||
Important rule: multiple selectors inside brackets are packed into one index object.
|
||||
So:
|
||||
|
||||
- `x[i]` passes `i`
|
||||
- `x[i, j]` passes a `List` containing `[i, j]`
|
||||
- `x[i, j, k]` passes `[i, j, k]`
|
||||
|
||||
This applies equally to:
|
||||
|
||||
- Kotlin-backed classes
|
||||
- Lyng classes overriding `getAt`
|
||||
- `dynamic { get { ... } set { ... } }`
|
||||
|
||||
If you want multi-axis slicing semantics, decode that list yourself in `getAt`.
|
||||
|
||||
### 5) Add Kotlin‑backed fields
|
||||
|
||||
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val myClass = ObjClass("MyClass")
|
||||
|
||||
// Add a read-only field (constant)
|
||||
@ -330,8 +215,6 @@ println(instance.count) // -> 5
|
||||
Properties in Lyng are pure accessors (getters and setters) and do not have automatic backing fields. You can add them to a class using `addProperty`.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val myClass = ObjClass("MyClass")
|
||||
var internalValue: Long = 10
|
||||
|
||||
@ -498,9 +381,8 @@ For Kotlin code that needs dynamic access to Lyng variables, functions, or membe
|
||||
It provides explicit, cached handles and predictable lookup rules.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
session.eval("""
|
||||
val scope = Script.newScope()
|
||||
scope.eval("""
|
||||
val x = 40
|
||||
fun add(a, b) = a + b
|
||||
class Box { var value = 1 }
|
||||
@ -515,7 +397,7 @@ val x = resolver.resolveVal("x").get(scope)
|
||||
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
|
||||
|
||||
// Member access
|
||||
val box = session.eval("Box()")
|
||||
val box = scope.eval("Box()")
|
||||
val valueHandle = resolver.resolveMemberVar(box, "value")
|
||||
valueHandle.set(scope, ObjInt(10))
|
||||
val value = valueHandle.get(scope)
|
||||
@ -526,14 +408,12 @@ val value = valueHandle.get(scope)
|
||||
The simplest approach: evaluate an expression that yields the value and convert it.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val kotlinAnswer = session.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
||||
val kotlinAnswer = scope.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
||||
|
||||
// After scripts manipulate your vars:
|
||||
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
||||
session.eval("name = name + ' rocks!'")
|
||||
val kotlinName = session.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
||||
scope.eval("name = name + ' rocks!'")
|
||||
val kotlinName = scope.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
||||
```
|
||||
|
||||
Advanced: you can also grab a variable record directly via `scope.get(name)` and work with its `Obj` value, but evaluating `"name"` is often clearer and enforces Lyng semantics consistently.
|
||||
@ -546,20 +426,16 @@ There are two convenient patterns.
|
||||
|
||||
```kotlin
|
||||
// Suppose Lyng defines: fun add(a, b) = a + b
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
session.eval("fun add(a, b) = a + b")
|
||||
scope.eval("fun add(a, b) = a + b")
|
||||
|
||||
val sum = session.eval("add(20, 22)").toKotlin(scope) // -> 42
|
||||
val sum = scope.eval("add(20, 22)").toKotlin(scope) // -> 42
|
||||
```
|
||||
|
||||
2) Call a Lyng function by name via a prepared call scope:
|
||||
|
||||
```kotlin
|
||||
// Ensure the function exists in the scope
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
session.eval("fun add(a, b) = a + b")
|
||||
scope.eval("fun add(a, b) = a + b")
|
||||
|
||||
// Look up the function object
|
||||
val addFn = scope.get("add")!!.value as Statement
|
||||
@ -590,8 +466,7 @@ Register a Kotlin‑built package:
|
||||
import net.sergeych.lyng.bridge.*
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val scope = Script.newScope()
|
||||
|
||||
// Access the import manager behind this scope
|
||||
val im: ImportManager = scope.importManager
|
||||
@ -622,12 +497,12 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
||||
}
|
||||
|
||||
// Use it from Lyng
|
||||
session.eval("""
|
||||
scope.eval("""
|
||||
import my.tools.*
|
||||
val v = triple(14)
|
||||
status = "busy"
|
||||
""")
|
||||
val v = session.eval("v").toKotlin(scope) // -> 42
|
||||
val v = scope.eval("v").toKotlin(scope) // -> 42
|
||||
```
|
||||
|
||||
Register a package from Lyng source text:
|
||||
@ -641,27 +516,24 @@ val pkgText = """
|
||||
|
||||
scope.importManager.addTextPackages(pkgText)
|
||||
|
||||
session.eval("""
|
||||
scope.eval("""
|
||||
import math.extra.*
|
||||
val s = sqr(12)
|
||||
""")
|
||||
val s = session.eval("s").toKotlin(scope) // -> 144
|
||||
val s = scope.eval("s").toKotlin(scope) // -> 144
|
||||
```
|
||||
|
||||
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
||||
|
||||
### 10) Executing from files, security, and isolation
|
||||
|
||||
- To run code from a file, read it and pass to `session.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
||||
- To run code from a file, read it and pass to `scope.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
||||
- `ImportManager` takes an optional `SecurityManager` if you need to restrict what packages or operations are available. By default, `Script.defaultImportManager` allows everything suitable for embedded use; clamp it down in sandboxed environments.
|
||||
- For isolation, prefer a fresh `EvalSession()` per request. Use `Scope.new()` / `Script.newScope()` when you specifically need low-level raw scopes or modules.
|
||||
- For isolation, create fresh modules/scopes via `Scope.new()` or `Script.newScope()` when you need a clean environment per request.
|
||||
|
||||
```kotlin
|
||||
// Preferred per-request runtime:
|
||||
val isolatedSession = EvalSession()
|
||||
|
||||
// Low-level fresh module based on the default manager, without the standard prelude:
|
||||
val isolatedScope = net.sergeych.lyng.Scope.new()
|
||||
// Fresh module based on the default manager, without the standard prelude
|
||||
val isolated = net.sergeych.lyng.Scope.new()
|
||||
```
|
||||
|
||||
### 11) Tips and troubleshooting
|
||||
@ -696,11 +568,8 @@ To simplify handling these objects from Kotlin, several extension methods are pr
|
||||
You can serialize Lyng exception objects using `Lynon` to transmit them across boundaries and then rethrow them.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
try {
|
||||
session.eval("throw MyUserException(404, \"Not Found\")")
|
||||
scope.eval("throw MyUserException(404, \"Not Found\")")
|
||||
} catch (e: ExecutionError) {
|
||||
// 1. Serialize the Lyng exception object
|
||||
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
||||
|
||||
@ -27,6 +27,6 @@ See `docs/lyng_d_files.md` for `.lyng.d` syntax and examples.
|
||||
- Alternatively, if/when the plugin is published to a marketplace, you will be able to install it
|
||||
directly from the “Marketplace” tab (not yet available).
|
||||
|
||||
### [Download plugin v0.0.5-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.5-SNAPSHOT.zip)
|
||||
### [Download plugin v0.0.2-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.2-SNAPSHOT.zip)
|
||||
|
||||
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
||||
|
||||
@ -1,32 +1,9 @@
|
||||
# Json support
|
||||
|
||||
Lyng now has two distinct JSON-facing layers:
|
||||
Since 1.0.5 we start adding JSON support. Versions 1,0,6* support serialization of the basic types, including lists and
|
||||
maps, and simple classes. Multiple inheritance may produce incorrect results, it is work in progress.
|
||||
|
||||
- plain JSON projection:
|
||||
- `Obj.toJson()`
|
||||
- `Obj.toJsonString()`
|
||||
- canonical JSON round-trip format:
|
||||
- `Json.encode(value)`
|
||||
- `Json.decode(text)`
|
||||
- typed canonical JSON round-trip format:
|
||||
- `Json.encodeAs(Type, value)`
|
||||
- `Json.decodeAs(Type, text)`
|
||||
|
||||
Use the first when you need ordinary JSON for interop.
|
||||
|
||||
Use the second when you need Lyng value round-trip semantics through JSON text with no schema.
|
||||
|
||||
Use the third when both sides already know the Lyng type and you want the same round-trip semantics with fewer type
|
||||
tags in the JSON.
|
||||
|
||||
This distinction is intentional:
|
||||
|
||||
- plain JSON projection is optimized for compatibility with ordinary JSON tooling
|
||||
- canonical `Json.encode()` is optimized for semantic fidelity to Lyng and Lynon and stays self-describing
|
||||
- typed canonical `Json.encodeAs()` is optimized for the same fidelity when the schema is provided externally
|
||||
- these goals conflict for values such as sets, exceptions, singleton objects, buffers, and maps with non-string keys
|
||||
|
||||
## Plain JSON projection in Lyng
|
||||
## Serialization in Lyng
|
||||
|
||||
// in lyng
|
||||
assertEquals("{\"a\":1}", {a: 1}.toJsonString())
|
||||
@ -43,8 +20,7 @@ Simple classes serialization is supported:
|
||||
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
|
||||
>>> void
|
||||
|
||||
Note that mutable members are serialized by default. You can exclude any member (including constructor parameters) from
|
||||
JSON serialization using the `@Transient` attribute:
|
||||
Note that mutable members are serialized by default. You can exclude any member (including constructor parameters) from JSON serialization using the `@Transient` attribute:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
@ -55,7 +31,7 @@ JSON serialization using the `@Transient` attribute:
|
||||
assertEquals( "{\"bar\":2,\"visible\":100}", Point2(1,2).toJsonString() )
|
||||
>>> void
|
||||
|
||||
Note that if you override plain JSON serialization:
|
||||
Note that if you override json serialization:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
@ -70,8 +46,8 @@ Note that if you override plain JSON serialization:
|
||||
assertEquals( "{\"custom\":true}", Point2(1,2).toJsonString() )
|
||||
>>> void
|
||||
|
||||
Custom serialization of user classes is possible by overriding `toJsonObject`. It must return an object which is
|
||||
serializable to JSON. Most often it is a map, but any object is accepted:
|
||||
Custom serialization of user classes is possible by overriding `toJsonObject` method. It must return an object which is
|
||||
serializable to Json. Most often it is a map, but any object is accepted, that makes it very flexible:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
@ -94,87 +70,12 @@ serializable to JSON. Most often it is a map, but any object is accepted:
|
||||
Please note that `toJsonString` should be used to get serialized string representation of the object. Don't call
|
||||
`toJsonObject` directly, it is not intended to be used outside the serialization library.
|
||||
|
||||
## Canonical Json round-trip format
|
||||
|
||||
`Json.encode()` and `Json.decode()` are now the JSON equivalents of `Lynon.encode()` and `Lynon.decode()`.
|
||||
|
||||
They still use JSON text, but they add Lyng-specific type tags where plain JSON would otherwise lose information.
|
||||
|
||||
When a map already fits ordinary JSON object rules, canonical JSON keeps that traditional object shape. In particular,
|
||||
maps with string keys are still serialized as JSON objects, not as tagged entry lists.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.serialization
|
||||
import lyng.time
|
||||
|
||||
enum Color { Red, Green }
|
||||
class Point(x,y) { var z = 42 }
|
||||
|
||||
val p = Point(1,2)
|
||||
p.z = 99
|
||||
|
||||
val value = List(
|
||||
p,
|
||||
Map([1, "one"], ["two", 2]),
|
||||
Set(1,2,3),
|
||||
"hello".encodeUtf8(),
|
||||
Date(2026,4,15),
|
||||
Color.Green
|
||||
)
|
||||
|
||||
assertEquals(value, Json.decode(Json.encode(value)))
|
||||
```
|
||||
|
||||
The canonical `Json` format is intended for Lyng-to-Lyng transfer through JSON text.
|
||||
|
||||
The plain `toJson()` projection is intended for ordinary JSON interop.
|
||||
|
||||
Canonical `Json.encode()` should be read as the JSON analogue of `Lynon.encode()`: when Lynon already preserves a
|
||||
Lyng distinction, canonical JSON tries to preserve it too, using tags only where ordinary JSON is insufficient.
|
||||
|
||||
## Typed canonical Json round-trip format
|
||||
|
||||
`Json.encodeAs(Type, value)` and `Json.decodeAs(Type, text)` use the same canonical rules, but with a declared target
|
||||
type available during the whole traversal.
|
||||
|
||||
This changes one thing only: type tags may be omitted when the declared type is already exact enough to restore the
|
||||
value unambiguously.
|
||||
|
||||
The same map rule still applies here: `Map<String, T>` stays a normal JSON object, while non-string-key maps fall back
|
||||
to canonical entry encoding.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.serialization
|
||||
|
||||
closed class Point(x: Int, y: Int)
|
||||
closed class Segment(a: Point, b: Point)
|
||||
|
||||
val value = Segment(Point(0, 1), Point(2, 3))
|
||||
val encoded = Json.encodeAs(Segment, value)
|
||||
|
||||
assertEquals("{\"a\":{\"x\":0,\"y\":1},\"b\":{\"x\":2,\"y\":3}}", encoded)
|
||||
assertEquals(value, Json.decodeAs(Segment, encoded))
|
||||
```
|
||||
|
||||
Subtype information is still preserved when the declared type is wider than the runtime one. For example, if a field is
|
||||
declared as `Base` but contains `Derived`, canonical subtype tags remain in that field.
|
||||
|
||||
This is why the APIs are split:
|
||||
|
||||
- `toJson()` stays plain and interop-friendly
|
||||
- `Json.encode()` stays fully self-describing and safe to decode without a schema
|
||||
- `Json.encodeAs()` uses the supplied schema to reduce noise, but only where that schema is sufficient
|
||||
|
||||
## Kotlin side interfaces
|
||||
|
||||
The "Batteries included" principle is also applied to serialization.
|
||||
|
||||
- `Obj.toJson()` provides Kotlin `JsonElement` for the plain JSON projection
|
||||
- `Obj.toJsonString()` provides plain JSON string representation
|
||||
- `Obj.toJson()` provides Kotlin `JsonElement`
|
||||
- `Obj.toJsonString()` provides Json string representation
|
||||
- `Obj.decodeSerializableWith()` and `Obj.decodeSerializable()` allows to decode Lyng classes as Kotlin objects using
|
||||
`kotlinx.serialization`:
|
||||
|
||||
@ -203,9 +104,10 @@ suspend inline fun <reified T> Obj.decodeSerializable(scope: Scope = Scope()) =
|
||||
decodeSerializableWith<T>(serializer<T>(), scope)
|
||||
```
|
||||
|
||||
Note that Lyng-to-Kotlin deserialization with `kotlinx.serialization` is based on the plain JSON projection,
|
||||
not the canonical `Json.encode()` format. It uses `JsonElement` as the information carrier without formatting and
|
||||
parsing actual JSON strings. This is why we use `Json.decodeFromJsonElement` instead of `Json.decodeFromString`.
|
||||
Note that lyng-2-kotlin deserialization with `kotlinx.serialization` uses JsonElement as information carrier without
|
||||
formatting and parsing actual Json strings. This is why we use `Json.decodeFromJsonElement` instead of
|
||||
`Json.decodeFromString`. Such an approach gives satisfactory performance without writing and supporting custom
|
||||
`kotlinx.serialization` codecs.
|
||||
|
||||
### Pitfall: JSON objects and Map<String, Any?>
|
||||
|
||||
@ -220,8 +122,7 @@ data class TestJson2(
|
||||
|
||||
@Test
|
||||
fun deserializeMapWithJsonTest() = runTest {
|
||||
val session = EvalSession()
|
||||
val x = session.eval("""
|
||||
val x = eval("""
|
||||
import lyng.serialization
|
||||
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
||||
""".trimIndent()).decodeSerializable<TestJson2>()
|
||||
@ -232,8 +133,7 @@ fun deserializeMapWithJsonTest() = runTest {
|
||||
|
||||
But what if your map has objects of different types? The approach of using polymorphism is partially applicable, but what to do with `{ one: 1, two: "two" }`?
|
||||
|
||||
The answer is simple: use `JsonObject` in your deserializable object. This class is capable of holding any JSON types
|
||||
and structures:
|
||||
The answer is pretty simple: use `JsonObject` in your deserializable object. This class is capable of holding any JSON types and structures and is sort of a silver bullet for such cases:
|
||||
|
||||
~~~kotlin
|
||||
@Serializable
|
||||
@ -243,8 +143,7 @@ data class TestJson3(
|
||||
)
|
||||
@Test
|
||||
fun deserializeAnyMapWithJsonTest() = runTest {
|
||||
val session = EvalSession()
|
||||
val x = session.eval("""
|
||||
val x = eval("""
|
||||
import lyng.serialization
|
||||
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
||||
""".trimIndent()).decodeSerializable<TestJson3>()
|
||||
@ -253,71 +152,27 @@ fun deserializeAnyMapWithJsonTest() = runTest {
|
||||
~~~
|
||||
|
||||
|
||||
## Supported shapes
|
||||
|
||||
### Plain JSON projection
|
||||
# List of supported types
|
||||
|
||||
| Lyng type | JSON type | notes |
|
||||
|-----------|-----------|-------------|
|
||||
| `Int` | number | |
|
||||
| `Real` | number | finite values only as plain numbers |
|
||||
| `Real` | number | |
|
||||
| `String` | string | |
|
||||
| `Bool` | boolean | |
|
||||
| `null` | null | |
|
||||
| `Instant` | string | ISO8601 (1) |
|
||||
| `List` | array | (2) |
|
||||
| `Map` | object | string keys only |
|
||||
| simple class instance | object | constructor fields + mutable vars |
|
||||
| enum | string | entry name |
|
||||
| `Map` | object | (2) |
|
||||
|
||||
### Canonical `Json.encode`
|
||||
|
||||
This format can also round-trip:
|
||||
|
||||
- maps with non-string keys
|
||||
- sets
|
||||
- immutable collections
|
||||
- buffers and bit buffers
|
||||
- class instances
|
||||
- singleton objects
|
||||
- enums
|
||||
- exceptions
|
||||
- `Date`, `Instant`, `DateTime`
|
||||
- non-finite reals
|
||||
- `void`
|
||||
|
||||
### Typed canonical `Json.encodeAs`
|
||||
|
||||
This format round-trips the same value space as canonical `Json.encode`, but it can emit simpler JSON for:
|
||||
|
||||
- closed classes and other exactly-known class fields
|
||||
- enums when the enum type is known
|
||||
- typed collections whose element types are known
|
||||
- nested object graphs where declared field types are precise
|
||||
|
||||
It still falls back to canonical tagged encoding when exact runtime type information would otherwise be lost.
|
||||
|
||||
It does so by adding Lyng-specific type tags only when necessary.
|
||||
|
||||
## Kotlin-side extension point for more formats
|
||||
|
||||
Additional formats can be exported from Kotlin modules by subclassing `ObjSerializationFormatClass` and registering the
|
||||
format in module scope with `bindSerializationFormat(...)`.
|
||||
|
||||
```kotlin
|
||||
module.bindSerializationFormat(
|
||||
object : ObjSerializationFormatClass("MyFormat") {
|
||||
override suspend fun encodeValue(scope: Scope, value: Obj): Obj = ...
|
||||
override suspend fun decodeValue(scope: Scope, encoded: Obj): Obj = ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This makes `MyFormat.encode(...)` and `MyFormat.decode(...)` available from Lyng after importing the module.
|
||||
|
||||
(1)
|
||||
: ISO8601 flavor `1970-05-06T06:00:00.000Z` is used; number of fractional digits depends on truncation on
|
||||
`Instant`, see `Instant.truncateTo...` functions.
|
||||
: ISO8601 flavor 1970-05-06T06:00:00.000Z in used; number of fractional digits depends on the truncation
|
||||
on [Instant](time.md), see `Instant.truncateTo...` functions.
|
||||
|
||||
(2)
|
||||
: Lists may contain any values serializable by the selected JSON layer.
|
||||
: List may contain any objects serializable to Json.
|
||||
|
||||
(3)
|
||||
: Map keys must be strings, map values may be any objects serializable to Json.
|
||||
|
||||
|
||||
@ -9,13 +9,12 @@
|
||||
#### Install in host
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||
|
||||
suspend fun initScope() {
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val scope = Script.newScope()
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,565 +0,0 @@
|
||||
# lyng.io.db — SQL database access for Lyng scripts
|
||||
|
||||
This module provides the portable SQL database contract for Lyng. The current shipped providers are SQLite via `lyng.io.db.sqlite` and a JVM-only JDBC bridge via `lyng.io.db.jdbc`.
|
||||
|
||||
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||
|
||||
---
|
||||
|
||||
## Install the module into a Lyng session
|
||||
|
||||
For SQLite-backed database access, install both the generic DB module and the SQLite provider:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.db.createDbModule
|
||||
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||
|
||||
suspend fun bootstrapDb() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createDbModule(scope)
|
||||
createSqliteModule(scope)
|
||||
session.eval("""
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
""".trimIndent())
|
||||
}
|
||||
```
|
||||
|
||||
`createSqliteModule(...)` also registers the `sqlite:` scheme for generic `openDatabase(...)`.
|
||||
|
||||
For JVM JDBC-backed access, install the JDBC provider as well:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.db.createDbModule
|
||||
import net.sergeych.lyng.io.db.jdbc.createJdbcModule
|
||||
|
||||
suspend fun bootstrapJdbc() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createDbModule(scope)
|
||||
createJdbcModule(scope)
|
||||
session.eval("""
|
||||
import lyng.io.db
|
||||
import lyng.io.db.jdbc
|
||||
""".trimIndent())
|
||||
}
|
||||
```
|
||||
|
||||
`createJdbcModule(...)` registers `jdbc:`, `h2:`, `postgres:`, and `postgresql:` for `openDatabase(...)`.
|
||||
|
||||
---
|
||||
|
||||
## Using from Lyng scripts
|
||||
|
||||
Typed SQLite open helper:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
|
||||
val userCount = db.transaction { tx ->
|
||||
tx.execute("create table user(id integer primary key autoincrement, name text not null)")
|
||||
tx.execute("insert into user(name) values(?)", "Ada")
|
||||
tx.execute("insert into user(name) values(?)", "Linus")
|
||||
tx.select("select count(*) as count from user").toList()[0]["count"]
|
||||
}
|
||||
|
||||
assertEquals(2, userCount)
|
||||
```
|
||||
|
||||
Generic provider-based open:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
val db = openDatabase(
|
||||
"sqlite:./app.db",
|
||||
Map(
|
||||
"foreignKeys" => true,
|
||||
"busyTimeoutMillis" => 5000
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
JVM JDBC open with H2:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db.jdbc
|
||||
|
||||
val db = openH2("mem:demo;DB_CLOSE_DELAY=-1")
|
||||
|
||||
val names = db.transaction { tx ->
|
||||
tx.execute("create table person(id bigint auto_increment primary key, name varchar(120) not null)")
|
||||
tx.execute("insert into person(name) values(?)", "Ada")
|
||||
tx.execute("insert into person(name) values(?)", "Linus")
|
||||
tx.select("select name from person order by id").toList()
|
||||
}
|
||||
|
||||
assertEquals("Ada", names[0]["name"])
|
||||
assertEquals("Linus", names[1]["name"])
|
||||
```
|
||||
|
||||
Generic JDBC open through `openDatabase(...)`:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db
|
||||
import lyng.io.db.jdbc
|
||||
|
||||
val db = openDatabase(
|
||||
"jdbc:h2:mem:demo2;DB_CLOSE_DELAY=-1",
|
||||
Map()
|
||||
)
|
||||
|
||||
val answer = db.transaction { tx ->
|
||||
tx.select("select 42 as answer").toList()[0]["answer"]
|
||||
}
|
||||
|
||||
assertEquals(42, answer)
|
||||
```
|
||||
|
||||
PostgreSQL typed open:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db.jdbc
|
||||
|
||||
val db = openPostgres(
|
||||
"jdbc:postgresql://127.0.0.1/appdb",
|
||||
"appuser",
|
||||
"secret"
|
||||
)
|
||||
|
||||
val titles = db.transaction { tx ->
|
||||
tx.execute("create table if not exists task(id bigserial primary key, title text not null)")
|
||||
tx.execute("insert into task(title) values(?)", "Ship JDBC provider")
|
||||
tx.execute("insert into task(title) values(?)", "Test PostgreSQL path")
|
||||
tx.select("select title from task order by id").toList()
|
||||
}
|
||||
|
||||
assertEquals("Ship JDBC provider", titles[0]["title"])
|
||||
```
|
||||
|
||||
Nested transactions use real savepoint semantics:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
|
||||
tx.execute("insert into item(name) values(?)", "outer")
|
||||
|
||||
try {
|
||||
tx.transaction { inner ->
|
||||
inner.execute("insert into item(name) values(?)", "inner")
|
||||
throw IllegalStateException("rollback nested")
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
}
|
||||
|
||||
assertEquals(1, tx.select("select count(*) as count from item").toList()[0]["count"])
|
||||
}
|
||||
```
|
||||
|
||||
Intentional rollback without treating it as a backend failure:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
|
||||
assertThrows(RollbackException) {
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
|
||||
tx.execute("insert into item(name) values(?)", "temporary")
|
||||
throw RollbackException("stop here")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runnable serialization sample
|
||||
|
||||
A complete runnable example is in [examples/sqlite_serialization.lyng](../examples/sqlite_serialization.lyng).
|
||||
|
||||
It uses:
|
||||
|
||||
- `@DbJson`
|
||||
- `@DbLynon`
|
||||
- `@DbExcept`
|
||||
- `@cols(...)`, `@vals(...)`, `@set(...)`
|
||||
- `decodeAs<T>()`
|
||||
|
||||
The current direct read form that works under `jlyng` is:
|
||||
|
||||
```lyng
|
||||
tx.select("select * from item where id = ?", 1).decodeAs<Item>().first
|
||||
```
|
||||
|
||||
If we want a shorter form such as:
|
||||
|
||||
```lyng
|
||||
tx.selectAllAs<Item>("item where id = ?", 1).first
|
||||
```
|
||||
|
||||
it should be added as a built-in `SqlTransaction` API. A pure Lyng generic wrapper around `decodeAs<T>()` does not currently preserve `T` reliably enough under `jlyng`.
|
||||
|
||||
---
|
||||
|
||||
## Portable API
|
||||
|
||||
### `Database`
|
||||
|
||||
- `transaction(block)` — opens a transaction, commits on normal exit, rolls back on uncaught failure.
|
||||
|
||||
### `SqlTransaction`
|
||||
|
||||
- `select(clause, params...)` — execute a statement whose primary result is a row set.
|
||||
- `execute(clause, params...)` — execute a side-effect statement and return `ExecutionResult`.
|
||||
- `transaction(block)` — nested transaction with real savepoint semantics.
|
||||
|
||||
`select(...)` and `execute(...)` also support SQL object-expansion macros for declaration-driven writes:
|
||||
|
||||
- `@cols(?1)` — expand object argument `?1` to a comma-separated column list
|
||||
- `@vals(?1)` — expand object argument `?1` to matching placeholders and bind values
|
||||
- `@set(?1)` — expand object argument `?1` to `column = ?` pairs and bind values
|
||||
|
||||
Each macro also supports an optional clause-local exclusion list:
|
||||
|
||||
```lyng
|
||||
tx.execute("update item set @set(?1 except: \"id\", \"createdAt\") where id = ?2", item, item.id)
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
tx.execute("insert into item(@cols(?1)) values(@vals(?1))", item)
|
||||
tx.execute("update item set @set(?1) where id = ?2", item, item.id)
|
||||
```
|
||||
|
||||
When a clause uses any of these macros, non-expanded scalar parameters in the same SQL string must use explicit indexed placeholders such as `?2`, `?3`, and so on.
|
||||
|
||||
### `ResultSet`
|
||||
|
||||
- `columns` — positional `SqlColumn` metadata, available before iteration.
|
||||
- `size()` — result row count.
|
||||
- `isEmpty()` — fast emptiness check where possible.
|
||||
- `iterator()` — normal row iteration while the transaction is active.
|
||||
- `toList()` — materialize detached `SqlRow` snapshots that may be used after the transaction ends.
|
||||
- `decodeAs<T>()` — transaction-scoped iterable view that decodes each row into `T`.
|
||||
|
||||
### `SqlRow`
|
||||
|
||||
- `row[index]` — zero-based positional access.
|
||||
- `row["columnName"]` — case-insensitive lookup by output column label.
|
||||
- `row.decodeAs<T>()` — decode one row into a typed Lyng value.
|
||||
|
||||
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
|
||||
|
||||
### `DbFieldAdapter`
|
||||
|
||||
Custom DB field projection hook used by `@DbDecodeWith(...)` and `@DbSerializeWith(...)`.
|
||||
|
||||
- `decode(rawValue, column, row, targetType)` — adapt one raw DB field value to a Lyng value for the requested target type.
|
||||
- `encode(value, targetType)` — adapt one Lyng value to a direct DB-bindable value for SQL object expansion.
|
||||
|
||||
Use `@DbDecodeWith(adapter)` on class constructor parameters and class-body fields/properties that participate in `decodeAs<T>()`.
|
||||
|
||||
Use `@DbSerializeWith(adapter)` on constructor parameters and class-body fields/properties that participate in `@cols(...)`, `@vals(...)`, and `@set(...)` object expansion.
|
||||
|
||||
Annotation arguments are evaluated once when the declaration is created, and the resulting adapter instance is retained in declaration metadata.
|
||||
|
||||
### `ExecutionResult`
|
||||
|
||||
- `affectedRowsCount`
|
||||
- `getGeneratedKeys()`
|
||||
|
||||
Statements that return rows directly, such as `... returning ...`, should use `select(...)`, not `execute(...)`.
|
||||
|
||||
---
|
||||
|
||||
## Value mapping
|
||||
|
||||
Portable bind values:
|
||||
|
||||
- `null`
|
||||
- `Bool`
|
||||
- `Int`, `Double`, `Decimal`
|
||||
- `String`
|
||||
- `Buffer`
|
||||
- `Date`, `DateTime`, `Instant`
|
||||
|
||||
Unsupported parameter values fail with `SqlUsageException`.
|
||||
|
||||
SQL object-expansion write rules:
|
||||
|
||||
- constructor parameters participate in projection by declaration order
|
||||
- matching serializable class-body fields/properties also participate
|
||||
- `@Transient` fields are excluded automatically
|
||||
- `@DbExcept` fields are excluded automatically
|
||||
- `except:` excludes additional fields for one specific macro use
|
||||
- direct DB-bindable values are written as-is
|
||||
- `@DbJson` fields are encoded as canonical JSON text
|
||||
- `@DbLynon` fields are encoded as Lynon binary
|
||||
- `@DbSerializeWith(adapter)` fields are encoded through the adapter
|
||||
- unannotated non-bindable object fields fail with `SqlUsageException`
|
||||
|
||||
Write-side encoding is intentionally explicit. The runtime does not try to infer target DB column types from SQL text or backend metadata during statement preparation.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
class Payload(name: String, count: Int)
|
||||
|
||||
object TrimAdapter: DbFieldAdapter {
|
||||
override fun encode(value, targetType) =
|
||||
when(value) {
|
||||
null -> null
|
||||
else -> value.toString().trim()
|
||||
}
|
||||
}
|
||||
|
||||
class Item(
|
||||
id: Int,
|
||||
@DbSerializeWith(TrimAdapter) title: String,
|
||||
@DbJson meta: Payload,
|
||||
@DbLynon state: Payload
|
||||
) {
|
||||
var note: String = ""
|
||||
@DbExcept var cache: String = ""
|
||||
}
|
||||
|
||||
val db = openSqlite(":memory:")
|
||||
val restored = db.transaction { tx ->
|
||||
tx.execute(
|
||||
"create table item(id integer not null, title text not null, meta text not null, state blob not null, note text not null)"
|
||||
)
|
||||
|
||||
val item = Item(1, " first ", Payload("json", 10), Payload("bin", 20))
|
||||
item.note = "created"
|
||||
item.cache = "not stored"
|
||||
|
||||
tx.execute("insert into item(@cols(?1)) values(@vals(?1))", item)
|
||||
|
||||
item.title = " second "
|
||||
item.meta = Payload("json2", 11)
|
||||
item.state = Payload("bin2", 21)
|
||||
item.note = "updated"
|
||||
|
||||
tx.execute(
|
||||
"update item set @set(?1 except: \"id\") where id = ?2",
|
||||
item,
|
||||
item.id
|
||||
)
|
||||
|
||||
tx.select("select id, title, meta, state, note from item").decodeAs<Item>().first
|
||||
}
|
||||
|
||||
assertEquals("second", restored.title)
|
||||
assertEquals("json2", restored.meta.name)
|
||||
assertEquals(21, restored.state.count)
|
||||
assertEquals("updated", restored.note)
|
||||
```
|
||||
|
||||
This example shows:
|
||||
|
||||
- `@DbSerializeWith(...)` trimming a string before write
|
||||
- `@DbJson` storing structured data in a text column
|
||||
- `@DbLynon` storing structured data in a binary column
|
||||
- `@DbExcept` excluding a field from automatic projection
|
||||
- `@set(... except: "id")` skipping one field for an update clause
|
||||
- `decodeAs<Item>()` reconstructing the object on read
|
||||
|
||||
Portable result metadata categories:
|
||||
|
||||
- `Binary`
|
||||
- `String`
|
||||
- `Int`
|
||||
- `Double`
|
||||
- `Decimal`
|
||||
- `Bool`
|
||||
- `Date`
|
||||
- `DateTime`
|
||||
- `Instant`
|
||||
|
||||
Typed row decode rules:
|
||||
|
||||
- object/class targets map constructor parameters by column label, case-insensitively
|
||||
- remaining matching serializable mutable fields are assigned after constructor call
|
||||
- `@DbDecodeWith(adapter)` on a constructor parameter or class-body field/property takes precedence over built-in JSON/Lynon decoding
|
||||
- `@DbDecodeWith(adapter)` must receive exactly one adapter instance implementing `DbFieldAdapter`
|
||||
- adapter output must match the target member type or decoding fails with `SqlUsageException`
|
||||
- missing required non-null constructor fields fail
|
||||
- defaulted or nullable constructor fields may be omitted from the result
|
||||
- extra result columns currently fail in strict mode
|
||||
- if a row has exactly one column, that value may be decoded directly as the requested target type
|
||||
- JSON-like native column types (`json`, `jsonb`) are decoded through typed canonical `Json` when the target type is not `String`
|
||||
- binary columns are decoded through `Lynon` when the target type is not `Buffer`
|
||||
- `Buffer` targets keep the raw binary payload without Lynon decoding
|
||||
- plain text columns are not implicitly treated as JSON
|
||||
|
||||
For temporal types, see [time functions](time.md).
|
||||
|
||||
---
|
||||
|
||||
## SQLite provider
|
||||
|
||||
`lyng.io.db.sqlite` currently provides the first concrete backend.
|
||||
|
||||
Typed helper:
|
||||
|
||||
```lyng
|
||||
openSqlite(
|
||||
path: String,
|
||||
readOnly: Bool = false,
|
||||
createIfMissing: Bool = true,
|
||||
foreignKeys: Bool = true,
|
||||
busyTimeoutMillis: Int = 5000
|
||||
): Database
|
||||
```
|
||||
|
||||
Accepted generic URL forms:
|
||||
|
||||
- `sqlite::memory:`
|
||||
- `sqlite:relative/path.db`
|
||||
- `sqlite:/absolute/path.db`
|
||||
|
||||
Supported `openDatabase(..., extraParams)` keys for SQLite:
|
||||
|
||||
- `readOnly: Bool`
|
||||
- `createIfMissing: Bool`
|
||||
- `foreignKeys: Bool`
|
||||
- `busyTimeoutMillis: Int`
|
||||
|
||||
SQLite write/read policy in v1:
|
||||
|
||||
- `Bool` writes as `0` / `1`
|
||||
- `Decimal` writes as canonical text
|
||||
- `Date` writes as `YYYY-MM-DD`
|
||||
- `DateTime` writes as ISO local timestamp text without timezone
|
||||
- `Instant` writes as ISO UTC timestamp text with explicit timezone marker
|
||||
- `TIME*` values stay `String`
|
||||
- `TIMESTAMP` / `DATETIME` reject timezone-bearing stored text
|
||||
|
||||
Open-time validation failures:
|
||||
|
||||
- malformed URL or bad option shape -> `IllegalArgumentException`
|
||||
- runtime open failure -> `DatabaseException`
|
||||
|
||||
## JDBC provider
|
||||
|
||||
`lyng.io.db.jdbc` is currently implemented on the JVM target only. The `lyngio-jvm` artifact bundles and explicitly loads these JDBC drivers:
|
||||
|
||||
- SQLite
|
||||
- H2
|
||||
- PostgreSQL
|
||||
|
||||
Typed helpers:
|
||||
|
||||
```lyng
|
||||
openJdbc(
|
||||
connectionUrl: String,
|
||||
user: String? = null,
|
||||
password: String? = null,
|
||||
driverClass: String? = null,
|
||||
properties: Map<String, Object?>? = null
|
||||
): Database
|
||||
|
||||
openH2(
|
||||
connectionUrl: String,
|
||||
user: String? = null,
|
||||
password: String? = null,
|
||||
properties: Map<String, Object?>? = null
|
||||
): Database
|
||||
|
||||
openPostgres(
|
||||
connectionUrl: String,
|
||||
user: String? = null,
|
||||
password: String? = null,
|
||||
properties: Map<String, Object?>? = null
|
||||
): Database
|
||||
```
|
||||
|
||||
Accepted generic URL forms:
|
||||
|
||||
- `jdbc:h2:mem:test;DB_CLOSE_DELAY=-1`
|
||||
- `h2:mem:test;DB_CLOSE_DELAY=-1`
|
||||
- `jdbc:postgresql://localhost/app`
|
||||
- `postgres://localhost/app`
|
||||
- `postgresql://localhost/app`
|
||||
|
||||
Supported `openDatabase(..., extraParams)` keys for JDBC:
|
||||
|
||||
- `driverClass: String`
|
||||
- `user: String`
|
||||
- `password: String`
|
||||
- `properties: Map<String, Object?>`
|
||||
|
||||
Behavior notes for the JDBC bridge:
|
||||
|
||||
- the portable `Database` / `SqlTransaction` API stays the same as for SQLite
|
||||
- nested transactions use JDBC savepoints
|
||||
- JDBC connection properties are built from `user`, `password`, and `properties`
|
||||
- `properties` values are stringified before being passed to JDBC
|
||||
- statements with row-returning clauses still must use `select(...)`, not `execute(...)`
|
||||
|
||||
Platform support for this provider:
|
||||
|
||||
- `lyng.io.db.jdbc` — JVM only
|
||||
- `openH2(...)` — works out of the box with `lyngio-jvm`
|
||||
- `openPostgres(...)` — driver included, but an actual PostgreSQL server is still required
|
||||
|
||||
PostgreSQL-specific notes:
|
||||
|
||||
- `openPostgres(...)` accepts either a full JDBC URL or shorthand forms such as `//localhost/app`
|
||||
- local peer/trust setups may use an empty password string
|
||||
- generated keys work with PostgreSQL `bigserial` / identity columns through `ExecutionResult.getGeneratedKeys()`
|
||||
- for reproducible automated tests, prefer a disposable PostgreSQL instance such as Docker/Testcontainers instead of a long-lived shared server
|
||||
|
||||
---
|
||||
|
||||
## Lifetime rules
|
||||
|
||||
`ResultSet` is valid only while its owning transaction is active.
|
||||
|
||||
`SqlRow` values are detached snapshots once materialized, so this pattern is valid:
|
||||
|
||||
```lyng
|
||||
val rows = db.transaction { tx ->
|
||||
tx.select("select name from person order by id").toList()
|
||||
}
|
||||
|
||||
assertEquals("Ada", rows[0]["name"])
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- do not keep `ResultSet` objects after the transaction block returns
|
||||
- materialize rows with `toList()` inside the transaction when they must outlive it
|
||||
- the iterable returned by `decodeAs<T>()` is also transaction-scoped
|
||||
- decoded objects produced while iterating `decodeAs<T>()` are detached ordinary Lyng values
|
||||
|
||||
The same rule applies to generated keys from `ExecutionResult.getGeneratedKeys()`: the `ResultSet` is transaction-scoped, but rows returned by `toList()` are detached.
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
- `lyng.io.db` — generic contract, available when host code installs it
|
||||
- `lyng.io.db.sqlite` — implemented on JVM and Linux Native in the current release tree
|
||||
- `lyng.io.db.jdbc` — implemented on JVM in the current release tree
|
||||
|
||||
For the broader I/O overview, see [lyngio overview](lyngio.md).
|
||||
@ -39,27 +39,23 @@ This brings in:
|
||||
|
||||
---
|
||||
|
||||
#### Install the module into a Lyng session
|
||||
#### Install the module into a Lyng Scope
|
||||
|
||||
The filesystem module is not installed automatically. The preferred host runtime is `EvalSession`: create the session, get its underlying scope, install the module there, and execute scripts through the session. You can customize access control via `FsAccessPolicy`.
|
||||
The filesystem module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using the installer. You can customize access control via `FsAccessPolicy`.
|
||||
|
||||
Kotlin (host) bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||
|
||||
suspend fun bootstrapFs() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||
// installed == true on first registration in this ImportManager, false on repeats
|
||||
val scope: Scope = Scope.new()
|
||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||
// installed == true on first registration in this ImportManager, false on repeats
|
||||
|
||||
// In scripts (or via session.eval), import the module to use its symbols:
|
||||
session.eval("import lyng.io.fs")
|
||||
}
|
||||
// In scripts (or via scope.eval), import the module to use its symbols:
|
||||
scope.eval("import lyng.io.fs")
|
||||
```
|
||||
|
||||
You can install with a custom policy too (see Access policy below).
|
||||
@ -189,7 +185,7 @@ val denyWrites = object : FsAccessPolicy {
|
||||
}
|
||||
|
||||
createFs(denyWrites, scope)
|
||||
session.eval("import lyng.io.fs")
|
||||
scope.eval("import lyng.io.fs")
|
||||
```
|
||||
|
||||
Composite operations like `copy` and `move` are checked as a set of primitives (e.g., `OpenRead(src)` + `Delete(dst)` if overwriting + `CreateFile(dst)` + `OpenWrite(dst)`).
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
# lyng.io.html
|
||||
|
||||
`lyng.io.html` provides a pure Lyng HTML builder DSL. It uses Lyng context
|
||||
receiver extensions, so text can be appended with `+"text"` inside tag blocks
|
||||
without global builder state.
|
||||
|
||||
Host code installs the package from `lyngio` with `createHtmlModule(...)`:
|
||||
|
||||
```kotlin
|
||||
val scope = Script.newScope()
|
||||
createHtmlModule(scope.importManager)
|
||||
```
|
||||
|
||||
Lyng code can then import it:
|
||||
|
||||
```lyng
|
||||
import lyng.io.html
|
||||
|
||||
val page = html {
|
||||
head {
|
||||
title { +"Demo" }
|
||||
}
|
||||
body {
|
||||
nav {
|
||||
a(href: "/") { +"Home" }
|
||||
}
|
||||
h3 { +"Heading 3" }
|
||||
p {
|
||||
attr("data-id", 123)
|
||||
+"Text is escaped: <safe>"
|
||||
}
|
||||
img(src: "/logo.png", alt: "Logo")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`html { ... }` returns a `String` beginning with `<!doctype html>`.
|
||||
|
||||
## Escaping
|
||||
|
||||
Text appended with unary `+` is HTML-escaped:
|
||||
|
||||
```lyng
|
||||
html {
|
||||
body {
|
||||
p { +"Text & <more>" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
produces:
|
||||
|
||||
```html
|
||||
<!doctype html><html><body><p>Text & <more></p></body></html>
|
||||
```
|
||||
|
||||
Attribute values are escaped with HTML attribute rules:
|
||||
|
||||
```lyng
|
||||
p {
|
||||
attr("data-x", "\"quoted\" & <tag>")
|
||||
+"content"
|
||||
}
|
||||
```
|
||||
|
||||
Use `raw(...)` only for trusted markup:
|
||||
|
||||
```lyng
|
||||
div {
|
||||
raw("<span>already escaped or trusted</span>")
|
||||
}
|
||||
```
|
||||
|
||||
## Tag Helpers
|
||||
|
||||
Current tag helpers cover common structural tags (`head`, `body`, `main`,
|
||||
`section`, `article`, `header`, `footer`, `nav`, `div`, `span`, `p`), headings
|
||||
(`h1` through `h6`), lists (`ul`, `ol`, `li`), and text/code tags (`strong`,
|
||||
`em`, `code`, `pre`, `script`, `style`).
|
||||
|
||||
```lyng
|
||||
body {
|
||||
main {
|
||||
section {
|
||||
h2 { +"News" }
|
||||
p { +"First item" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common void tags are also available: `meta`, `link`, `img`, `br`, and `input`.
|
||||
|
||||
```lyng
|
||||
head {
|
||||
meta { attr("charset", "utf-8") }
|
||||
link {
|
||||
attr("rel", "stylesheet")
|
||||
attr("href", "/site.css")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
Use `attr(name, value)` inside a tag block to set an escaped attribute value.
|
||||
`id(...)` and `classes(...)` are small aliases:
|
||||
|
||||
```lyng
|
||||
div {
|
||||
id("root")
|
||||
classes("app shell")
|
||||
}
|
||||
```
|
||||
|
||||
Use `flag(name)` for boolean attributes:
|
||||
|
||||
```lyng
|
||||
input {
|
||||
attr("type", "checkbox")
|
||||
flag("checked")
|
||||
}
|
||||
```
|
||||
|
||||
## Convenience Helpers
|
||||
|
||||
Convenience helpers include `metaCharset()`, `stylesheet(href)`,
|
||||
`a(href) { ... }`, `img(src, alt)`, and `input(type, name, value)`.
|
||||
|
||||
```lyng
|
||||
head {
|
||||
metaCharset()
|
||||
stylesheet("/site.css")
|
||||
}
|
||||
|
||||
body {
|
||||
nav {
|
||||
a(href: "/home") { +"Home" }
|
||||
}
|
||||
img(src: "/logo.png", alt: "Logo & mark")
|
||||
input(type: "hidden", name: "token", value: "abc")
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Elements
|
||||
|
||||
Use `tag(name) { ... }` and `voidTag(name) { ... }` for elements that do not
|
||||
have dedicated helpers yet:
|
||||
|
||||
```lyng
|
||||
body {
|
||||
tag("custom-element") {
|
||||
flag("hidden")
|
||||
+"Secret"
|
||||
}
|
||||
voidTag("source") {
|
||||
attr("srcset", "/image.webp")
|
||||
attr("type", "image/webp")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These helpers are intentionally simple escape hatches. Prefer a dedicated helper
|
||||
when one exists because it can encode safer defaults and clearer parameter names.
|
||||
@ -1,181 +0,0 @@
|
||||
# lyng.io.http — HTTP/HTTPS client for Lyng scripts
|
||||
|
||||
This module provides a compact HTTP client API for Lyng scripts. It is implemented in `lyngio` and backed by Ktor on supported runtimes.
|
||||
|
||||
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||
>
|
||||
> **Shared type note:** `HttpHeaders` is also available from `lyng.io.http.types` when host code wants the reusable value type without relying on the HTTP client module itself.
|
||||
|
||||
---
|
||||
|
||||
## Add the library to your project (Gradle)
|
||||
|
||||
If you use this repository as a multi-module project, add a dependency on `:lyngio`:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
|
||||
}
|
||||
```
|
||||
|
||||
For external projects, ensure you also use the Lyng Maven repository described in `lyng.io.fs`.
|
||||
|
||||
---
|
||||
|
||||
## Install the module into a Lyng session
|
||||
|
||||
The HTTP module is not installed automatically. Install it into the session scope and provide a policy.
|
||||
|
||||
Kotlin (host) bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.http.createHttpModule
|
||||
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
||||
|
||||
suspend fun bootstrapHttp() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createHttpModule(PermitAllHttpAccessPolicy, scope)
|
||||
session.eval("import lyng.io.http")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using from Lyng scripts
|
||||
|
||||
Simple GET:
|
||||
|
||||
import lyng.io.http
|
||||
|
||||
val r = Http.get(HTTP_TEST_URL + "/hello")
|
||||
[r.status, r.text()]
|
||||
>>> [200,hello from test]
|
||||
|
||||
Headers and response header access:
|
||||
|
||||
import lyng.io.http
|
||||
|
||||
val r = Http.get(HTTP_TEST_URL + "/headers")
|
||||
[r.headers["X-Reply"], r.headers.getAll("X-Reply").size, r.text()]
|
||||
>>> [one,2,header demo]
|
||||
|
||||
Programmatic request object:
|
||||
|
||||
import lyng.io.http
|
||||
|
||||
val q = HttpRequest()
|
||||
q.method = "POST"
|
||||
q.url = HTTP_TEST_URL + "/echo"
|
||||
q.headers = Map("Content-Type" => "text/plain")
|
||||
q.bodyText = "ping"
|
||||
|
||||
val r = Http.request(q)
|
||||
r.text()
|
||||
>>> "POST:ping"
|
||||
|
||||
HTTPS GET:
|
||||
|
||||
import lyng.io.http
|
||||
|
||||
val r = Http.get(HTTPS_TEST_URL + "/hello")
|
||||
[r.status, r.text()]
|
||||
>>> [200,hello from test]
|
||||
|
||||
---
|
||||
|
||||
## API reference
|
||||
|
||||
### `Http` (static methods)
|
||||
|
||||
- `isSupported(): Bool` — Whether HTTP client support is available on the current runtime.
|
||||
- `request(req: HttpRequest): HttpResponse` — Execute a request described by a mutable request object.
|
||||
- `get(url: String, headers...): HttpResponse` — Convenience GET request.
|
||||
- `post(url: String, bodyText: String = "", contentType: String? = null, headers...): HttpResponse` — Convenience text POST request.
|
||||
- `postBytes(url: String, body: Buffer, contentType: String? = null, headers...): HttpResponse` — Convenience binary POST request.
|
||||
|
||||
For convenience methods, `headers...` accepts:
|
||||
|
||||
- `MapEntry`, e.g. `"Accept" => "text/plain"`
|
||||
- 2-item lists, e.g. `["Accept", "text/plain"]`
|
||||
|
||||
### `HttpRequest`
|
||||
|
||||
- `method: String`
|
||||
- `url: String`
|
||||
- `headers: Map<String, String>`
|
||||
- `bodyText: String?`
|
||||
- `bodyBytes: Buffer?`
|
||||
- `timeoutMillis: Int?`
|
||||
|
||||
Only one of `bodyText` and `bodyBytes` should be set.
|
||||
|
||||
### `HttpResponse`
|
||||
|
||||
- `status: Int`
|
||||
- `statusText: String`
|
||||
- `headers: HttpHeaders`
|
||||
- `text(): String`
|
||||
- `bytes(): Buffer`
|
||||
|
||||
Response body decoding is cached inside the response object.
|
||||
|
||||
### `HttpHeaders`
|
||||
|
||||
`HttpHeaders` behaves like `Map<String, String>` for the first value of each header name and additionally exposes:
|
||||
|
||||
- `get(name: String): String?`
|
||||
- `getAll(name: String): List<String>`
|
||||
- `names(): List<String>`
|
||||
|
||||
Header lookup is case-insensitive.
|
||||
|
||||
---
|
||||
|
||||
## Security policy
|
||||
|
||||
The module uses `HttpAccessPolicy` to authorize requests before they are sent.
|
||||
|
||||
- `HttpAccessPolicy` — interface for custom policies
|
||||
- `PermitAllHttpAccessPolicy` — allows all requests
|
||||
- `HttpAccessOp.Request(method, url)` — operation checked by the policy
|
||||
|
||||
Example restricted policy in Kotlin:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyngio.fs.security.AccessContext
|
||||
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||
import net.sergeych.lyngio.fs.security.Decision
|
||||
import net.sergeych.lyngio.http.security.HttpAccessOp
|
||||
import net.sergeych.lyngio.http.security.HttpAccessPolicy
|
||||
|
||||
val allowLocalOnly = object : HttpAccessPolicy {
|
||||
override suspend fun check(op: HttpAccessOp, ctx: AccessContext): AccessDecision =
|
||||
when (op) {
|
||||
is HttpAccessOp.Request ->
|
||||
if (
|
||||
op.url.startsWith("http://127.0.0.1:") ||
|
||||
op.url.startsWith("https://127.0.0.1:") ||
|
||||
op.url.startsWith("http://localhost:") ||
|
||||
op.url.startsWith("https://localhost:")
|
||||
)
|
||||
AccessDecision(Decision.Allow)
|
||||
else
|
||||
AccessDecision(Decision.Deny, "only local HTTP/HTTPS requests are allowed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
- **JVM:** supported
|
||||
- **Android:** supported via the Ktor CIO client backend
|
||||
- **JS:** supported via the Ktor JS client backend
|
||||
- **Linux native:** supported via the Ktor Curl client backend
|
||||
- **Windows native:** supported via the Ktor WinHttp client backend
|
||||
- **Apple native:** supported via the Ktor Darwin client backend
|
||||
- **Other targets:** may report unsupported; use `Http.isSupported()` before relying on it
|
||||
@ -1,446 +0,0 @@
|
||||
# `lyng.io.http.server` - Minimal HTTP/1.1 And WebSocket Server
|
||||
|
||||
This module provides a small server-side HTTP API for Lyng scripts. It is implemented in `lyngio` on top of the existing TCP layer and is intended for embedded tools, local services, test fixtures, and lightweight app backends.
|
||||
|
||||
It supports:
|
||||
- HTTP/1.1 request parsing
|
||||
- keep-alive
|
||||
- exact-path routing
|
||||
- regex routing
|
||||
- path-template routing with named parameters
|
||||
- websocket upgrade and server-side sessions
|
||||
|
||||
It does not aim to replace a full reverse proxy. Typical deployment is behind nginx, Caddy, or another frontend that handles TLS and public-facing edge concerns.
|
||||
|
||||
> **Security note:** this module uses the same `NetAccessPolicy` capability model as raw TCP sockets. If scripts are allowed to listen on TCP, they can host an HTTP server.
|
||||
|
||||
## Install The Module Into A Lyng Session
|
||||
|
||||
Kotlin bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.http.server.createHttpServerModule
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
|
||||
suspend fun bootstrapHttpServer() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
||||
session.eval("import lyng.io.http.server")
|
||||
}
|
||||
```
|
||||
|
||||
## RequestContext Sugar
|
||||
|
||||
Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as:
|
||||
|
||||
- `jsonBody<T>()`
|
||||
- `respondJson(...)`
|
||||
- `respondHtml { ... }`
|
||||
- `respondText(...)`
|
||||
- `setHeader(...)`
|
||||
- `request.path`
|
||||
- `routeParams["id"]`
|
||||
|
||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda.
|
||||
|
||||
## HTML Response Sugar
|
||||
|
||||
Use `respondHtml { ... }` to render an HTML document with the `lyng.io.html` DSL and send it as `text/html; charset=utf-8`.
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
import lyng.io.html
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.get("/") {
|
||||
respondHtml {
|
||||
head {
|
||||
title { +"Lyng status" }
|
||||
}
|
||||
body {
|
||||
h3 { +"Service is running" }
|
||||
p { +"Path: ${request.path}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
Pass `code:` when the route should return a non-200 status:
|
||||
|
||||
```lyng
|
||||
server.get("/accepted") {
|
||||
respondHtml(code: 202) {
|
||||
body { h3 { +"Accepted" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JSON API Sugar
|
||||
|
||||
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
||||
|
||||
- `jsonBody<T>()` decodes the request body with typed `Json.decodeAs(...)`
|
||||
- `respondJson(body, status = 200)` sets JSON content type and responds with plain `toJsonString()`
|
||||
|
||||
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
||||
|
||||
### Typed JSON POST With Route Params
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
closed class CreateResultRequest(title: String, score: Int)
|
||||
closed class CreateResultResponse(id: String, userId: String, title: String, score: Int)
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.postPath("/api/users/{userId}/results") {
|
||||
val req = jsonBody<CreateResultRequest>()
|
||||
|
||||
if (req.title.isBlank()) {
|
||||
respondJson({ error: "title must not be empty" }, 400)
|
||||
return
|
||||
}
|
||||
|
||||
respondJson(
|
||||
CreateResultResponse("r-101", routeParams["userId"], req.title, req.score),
|
||||
201
|
||||
)
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
### JSON Response With Route Params
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.getPath("/api/users/{id}") {
|
||||
respondJson({
|
||||
id: routeParams["id"],
|
||||
path: request.path,
|
||||
ok: true
|
||||
})
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
## Request And Route Data
|
||||
|
||||
`ServerRequest` exposes parsed HTTP request data:
|
||||
|
||||
- `method: String`
|
||||
- `target: String`
|
||||
- `path: String`
|
||||
- `pathParts: List<String>`
|
||||
- `queryString: String?`
|
||||
- `query: Map<String, String>`
|
||||
- `headers: HttpHeaders`
|
||||
- `body: Buffer`
|
||||
|
||||
`RequestContext` exposes routing context and response controls:
|
||||
|
||||
- `request: ServerRequest`
|
||||
- `routeMatch: RegexMatch?`
|
||||
- `routeParams: Map<String, String>`
|
||||
- `jsonBody<T>()`
|
||||
- `respond(...)`
|
||||
- `respondText(...)`
|
||||
- `respondJson(body, status = 200)`
|
||||
- `respondHtml(code: 200) { ... }`
|
||||
- `setHeader(...)`
|
||||
- `addHeader(...)`
|
||||
- `acceptWebSocket(...)`
|
||||
|
||||
For exact routes, `routeMatch` is `null` and `routeParams` is empty.
|
||||
For regex routes, `routeMatch` is set and `routeParams` is empty.
|
||||
For path-template routes, both `routeMatch` and `routeParams` are set.
|
||||
|
||||
## Reusable Routers
|
||||
|
||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
||||
Mount it into `HttpServer` or another `Router`.
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
val api = Router()
|
||||
api.get("/health") {
|
||||
respondText(200, "ok")
|
||||
}
|
||||
|
||||
val users = Router()
|
||||
users.getPath("/users/{id}") {
|
||||
respondJson({ id: routeParams["id"] })
|
||||
}
|
||||
|
||||
api.mount(users)
|
||||
|
||||
val server = HttpServer()
|
||||
server.mount(api)
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer.
|
||||
|
||||
## WebSocket Routes
|
||||
|
||||
You can route websocket upgrades by exact path, regex, or path template.
|
||||
|
||||
```lyng
|
||||
server.ws("/chat") { ws ->
|
||||
ws.sendText("hello")
|
||||
ws.close()
|
||||
}
|
||||
|
||||
server.wsPath("/ws/{room}") { ws ->
|
||||
ws.sendText("room=" + routeParams["room"])
|
||||
ws.close()
|
||||
}
|
||||
```
|
||||
|
||||
A websocket handler runs only for requests that actually ask for websocket upgrade. Ordinary HTTP requests to the same path are not treated as websocket sessions.
|
||||
|
||||
### Choosing Between `ws(...)` And `acceptWebSocket(...)`
|
||||
|
||||
Use `server.ws(...)` or `server.wsPath(...)` when the route is always a websocket endpoint.
|
||||
|
||||
Use `acceptWebSocket(...)` inside a normal HTTP handler when the same route may inspect the request first and then decide whether to upgrade.
|
||||
|
||||
```lyng
|
||||
server.get("/maybe-upgrade") {
|
||||
if (!request.isWebSocketUpgrade()) {
|
||||
respondText(400, "websocket upgrade required")
|
||||
return
|
||||
}
|
||||
|
||||
acceptWebSocket { ws ->
|
||||
ws.sendText("connected")
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reading Incoming Messages
|
||||
|
||||
Inside a websocket handler, call `ws.receive()` to wait for the next application message.
|
||||
|
||||
What `receive()` returns:
|
||||
- `WsMessage` for the next text or binary message.
|
||||
- `null` after the client sends a close frame.
|
||||
- `null` after the socket is already closed and no more frames can arrive.
|
||||
|
||||
What reaches Lyng code:
|
||||
- Text frames become `WsMessage(isText = true, text = ...)`.
|
||||
- Binary frames become `WsMessage(isText = false, data = ...)`.
|
||||
- Fragmented websocket messages are reassembled before they are returned.
|
||||
- Ping and pong control frames are handled internally and do not appear in Lyng.
|
||||
- A client close frame is answered by the server close handshake, then `receive()` returns `null`.
|
||||
|
||||
Typical server receive loop:
|
||||
|
||||
```lyng
|
||||
import lyng.buffer
|
||||
|
||||
server.ws("/echo") { ws ->
|
||||
while (true) {
|
||||
val msg = ws.receive() ?: break
|
||||
if (msg.isText) {
|
||||
ws.sendText("echo:" + msg.text)
|
||||
} else {
|
||||
ws.sendBytes(msg.data as Buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sending Outgoing Messages
|
||||
|
||||
Use:
|
||||
- `ws.sendText(text)` for text messages.
|
||||
- `ws.sendBytes(data)` for binary messages.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.buffer
|
||||
|
||||
server.ws("/push") { ws ->
|
||||
ws.sendText("ready")
|
||||
ws.sendBytes(Buffer(1, 2, 3))
|
||||
ws.close()
|
||||
}
|
||||
```
|
||||
|
||||
Send behavior:
|
||||
- Each call sends one websocket message.
|
||||
- The server API does not expose frame-by-frame streaming.
|
||||
- Once the session is closed, send calls fail with a websocket error.
|
||||
|
||||
### What Happens When The Connection Closes
|
||||
|
||||
There are three practical cases:
|
||||
|
||||
1. The client closes first.
|
||||
The runtime replies with a close frame, releases the socket, and `receive()` returns `null`.
|
||||
|
||||
2. Your handler closes first with `ws.close(...)`.
|
||||
The runtime sends a close frame and releases the socket locally.
|
||||
|
||||
3. The transport disappears unexpectedly.
|
||||
The session is released and no more messages can be received; subsequent sends fail.
|
||||
|
||||
What Lyng code should do:
|
||||
- Treat `receive() == null` as end-of-session.
|
||||
- Exit the handler or break the receive loop at that point.
|
||||
- Do not keep sending after close has been observed.
|
||||
|
||||
The current server-side API does not expose the peer close code or close reason to Lyng.
|
||||
|
||||
### Closing The Connection Yourself
|
||||
|
||||
Call `ws.close()` when you want to terminate the websocket session.
|
||||
|
||||
```lyng
|
||||
server.ws("/chat") { ws ->
|
||||
ws.sendText("server shutting down")
|
||||
ws.close(1000, "done")
|
||||
}
|
||||
```
|
||||
|
||||
Close semantics:
|
||||
- `close()` sends a websocket close frame with the given code and reason.
|
||||
- Defaults are `code = 1000` and `reason = ""`.
|
||||
- `close()` is idempotent; calling it again after close does nothing.
|
||||
- After local close, the session should be treated as unusable.
|
||||
- After close, `isOpen()` becomes false and further sends fail.
|
||||
|
||||
### WebSocket Handler Pattern
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.wsPath("/rooms/{room}") { ws ->
|
||||
val room = routeParams["room"] ?: "<unknown>"
|
||||
ws.sendText("joined:" + room)
|
||||
|
||||
while (true) {
|
||||
val msg = ws.receive() ?: break
|
||||
if (msg.isText) {
|
||||
ws.sendText(room + ":" + msg.text)
|
||||
}
|
||||
}
|
||||
|
||||
ws.close()
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
## Path-Template Routes
|
||||
|
||||
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`.
|
||||
|
||||
```lyng
|
||||
server.getPath("/users/{userId}/posts/{postId}") {
|
||||
respondText(
|
||||
200,
|
||||
routeParams["userId"] + ":" + routeParams["postId"]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Template rules:
|
||||
- template must start with `/`
|
||||
- a segment is either literal text or `{name}`
|
||||
- parameter names must be valid identifiers
|
||||
- parameter values match one path segment only
|
||||
- parameter values use path decoding rules:
|
||||
- valid percent-encoding is decoded
|
||||
- `+` stays `+`
|
||||
- malformed `%` stays literal
|
||||
|
||||
## Regex Routes
|
||||
|
||||
Regex routes match the whole request path, not a substring.
|
||||
|
||||
```lyng
|
||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
||||
val m = routeMatch!!
|
||||
respondText(200, "user=" + m[1] + ", post=" + m[2])
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Exact Route
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("/hello") {
|
||||
setHeader("Content-Type", "text/plain")
|
||||
respondText(200, "hello")
|
||||
}
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
## Route Precedence
|
||||
|
||||
Dispatch order is:
|
||||
|
||||
1. exact method route
|
||||
2. exact `any` route
|
||||
3. regex method route, registration order
|
||||
4. regex `any` route, registration order
|
||||
5. fallback
|
||||
|
||||
This means exact routes stay fast and always win over template or regex routes for the same path.
|
||||
|
||||
## API Surface
|
||||
|
||||
### `Router` Route Registration Methods
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
- `getPath(pathTemplate: String, handler)`
|
||||
- `post(path: String|Regex, handler)`
|
||||
- `postPath(pathTemplate: String, handler)`
|
||||
- `put(path: String|Regex, handler)`
|
||||
- `putPath(pathTemplate: String, handler)`
|
||||
- `delete(path: String|Regex, handler)`
|
||||
- `deletePath(pathTemplate: String, handler)`
|
||||
- `any(path: String|Regex, handler)`
|
||||
- `anyPath(pathTemplate: String, handler)`
|
||||
- `ws(path: String|Regex, handler)`
|
||||
- `wsPath(pathTemplate: String, handler)`
|
||||
- `fallback(handler)`
|
||||
- `mount(router)`
|
||||
|
||||
### `HttpServer` Route Registration Methods
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
- `getPath(pathTemplate: String, handler)`
|
||||
- `post(path: String|Regex, handler)`
|
||||
- `postPath(pathTemplate: String, handler)`
|
||||
- `put(path: String|Regex, handler)`
|
||||
- `putPath(pathTemplate: String, handler)`
|
||||
- `delete(path: String|Regex, handler)`
|
||||
- `deletePath(pathTemplate: String, handler)`
|
||||
- `any(path: String|Regex, handler)`
|
||||
- `anyPath(pathTemplate: String, handler)`
|
||||
- `ws(path: String|Regex, handler)`
|
||||
- `wsPath(pathTemplate: String, handler)`
|
||||
- `fallback(handler)`
|
||||
- `mount(router)`
|
||||
- `listen(port, host = null, backlog = 128)`
|
||||
@ -1,177 +0,0 @@
|
||||
### lyng.io.net — TCP and UDP sockets for Lyng scripts
|
||||
|
||||
This module provides minimal raw transport networking for Lyng scripts. It is implemented in `lyngio` and backed by Ktor sockets on the JVM and Linux Native, and by Node networking APIs on JS/Node runtimes.
|
||||
|
||||
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||
>
|
||||
> **Shared type note:** `IpVersion`, `SocketAddress`, and `Datagram` are also available from `lyng.io.net.types` when host code wants reusable transport value types without depending on the `Net` capability object itself.
|
||||
>
|
||||
> **Important native platform limit:** current native TCP/UDP support is backed by a selector with a per-process file descriptor ceiling. On Linux/macOS native targets this makes high-connection-count servers and same-process load tests unsuitable once the process approaches that limit.
|
||||
>
|
||||
> **Recommendation:** for serious HTTP/TCP servers, prefer the JVM target today. On native targets, keep concurrency bounded, batch local load tests in waves, and use multiple worker processes behind a reverse proxy if you need more throughput before the backend is reworked.
|
||||
>
|
||||
> **Need this fixed?** Please open or upvote an issue at <https://github.com/sergeych/lyng/issues> so native high-concurrency networking can be prioritized.
|
||||
|
||||
---
|
||||
|
||||
#### Install the module into a Lyng session
|
||||
|
||||
Kotlin (host) bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.net.createNetModule
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
|
||||
suspend fun bootstrapNet() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createNetModule(PermitAllNetAccessPolicy, scope)
|
||||
session.eval("import lyng.io.net")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Using from Lyng scripts
|
||||
|
||||
Capability checks and address resolution:
|
||||
|
||||
import lyng.io.net
|
||||
|
||||
val a: SocketAddress = Net.resolve("127.0.0.1", 4040)[0]
|
||||
[Net.isSupported(), a.toString(), a.resolved, a.ipVersion == IpVersion.IPV4]
|
||||
>>> [true,127.0.0.1:4040,true,true]
|
||||
|
||||
TCP client connect, write, read, and close:
|
||||
|
||||
import lyng.buffer
|
||||
import lyng.io.net
|
||||
|
||||
val socket = Net.tcpConnect("127.0.0.1", NET_TEST_TCP_PORT)
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
val reply = (socket.read(16) as Buffer).decodeUtf8()
|
||||
socket.close()
|
||||
reply
|
||||
>>> "reply:ping"
|
||||
|
||||
Lyng TCP server socket operations with `tcpListen()` and `accept()`:
|
||||
|
||||
import lyng.buffer
|
||||
import lyng.io.net
|
||||
|
||||
val server = Net.tcpListen(0, "127.0.0.1")
|
||||
val port = server.localAddress().port
|
||||
val accepted = launch {
|
||||
val client = server.accept()
|
||||
val line = (client.read(4) as Buffer).decodeUtf8()
|
||||
client.writeUtf8("echo:" + line)
|
||||
client.flush()
|
||||
client.close()
|
||||
server.close()
|
||||
line
|
||||
}
|
||||
|
||||
val socket = Net.tcpConnect("127.0.0.1", port)
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
val reply = (socket.read(16) as Buffer).decodeUtf8()
|
||||
socket.close()
|
||||
[accepted.await(), reply]
|
||||
>>> [ping,echo:ping]
|
||||
|
||||
UDP bind, send, receive, and inspect sender address:
|
||||
|
||||
import lyng.buffer
|
||||
import lyng.io.net
|
||||
|
||||
val server = Net.udpBind(0, "127.0.0.1")
|
||||
val client = Net.udpBind(0, "127.0.0.1")
|
||||
client.send(Buffer("ping"), "127.0.0.1", server.localAddress().port)
|
||||
val d = server.receive()
|
||||
client.close()
|
||||
server.close()
|
||||
[d.data.decodeUtf8(), d.address.port > 0]
|
||||
>>> [ping,true]
|
||||
|
||||
---
|
||||
|
||||
#### API reference
|
||||
|
||||
##### `Net` (static methods)
|
||||
|
||||
- `isSupported(): Bool` — Whether any raw networking support is available.
|
||||
- `isTcpAvailable(): Bool` — Whether outbound TCP sockets are available.
|
||||
- `isTcpServerAvailable(): Bool` — Whether listening TCP server sockets are available.
|
||||
- `isUdpAvailable(): Bool` — Whether UDP datagram sockets are available.
|
||||
- `resolve(host: String, port: Int): List<SocketAddress>` — Resolve a host and port into concrete addresses.
|
||||
- `tcpConnect(host: String, port: Int, timeoutMillis: Int? = null, noDelay: Bool = true): TcpSocket` — Open an outbound TCP socket.
|
||||
- `tcpListen(port: Int, host: String? = null, backlog: Int = 128, reuseAddress: Bool = true): TcpServer` — Start a listening TCP server socket.
|
||||
- `udpBind(port: Int = 0, host: String? = null, reuseAddress: Bool = true): UdpSocket` — Bind a UDP socket.
|
||||
|
||||
##### `SocketAddress`
|
||||
|
||||
- `host: String`
|
||||
- `port: Int`
|
||||
- `ipVersion: IpVersion`
|
||||
- `resolved: Bool`
|
||||
- `toString(): String`
|
||||
|
||||
##### `TcpSocket`
|
||||
|
||||
- `isOpen(): Bool`
|
||||
- `localAddress(): SocketAddress`
|
||||
- `remoteAddress(): SocketAddress`
|
||||
- `read(maxBytes: Int = 65536): Buffer?`
|
||||
- `readLine(): String?`
|
||||
- `write(data: Buffer): void`
|
||||
- `writeUtf8(text: String): void`
|
||||
- `flush(): void`
|
||||
- `close(): void`
|
||||
|
||||
##### `TcpServer`
|
||||
|
||||
- `isOpen(): Bool`
|
||||
- `localAddress(): SocketAddress`
|
||||
- `accept(): TcpSocket`
|
||||
- `close(): void`
|
||||
|
||||
##### `UdpSocket`
|
||||
|
||||
- `isOpen(): Bool`
|
||||
- `localAddress(): SocketAddress`
|
||||
- `receive(maxBytes: Int = 65536): Datagram?`
|
||||
- `send(data: Buffer, host: String, port: Int): void`
|
||||
- `close(): void`
|
||||
|
||||
##### `Datagram`
|
||||
|
||||
- `data: Buffer`
|
||||
- `address: SocketAddress`
|
||||
|
||||
---
|
||||
|
||||
#### Security policy
|
||||
|
||||
The module uses `NetAccessPolicy` to authorize network operations before they are executed.
|
||||
|
||||
- `NetAccessPolicy` — interface for custom policies
|
||||
- `PermitAllNetAccessPolicy` — allows all network operations
|
||||
- `NetAccessOp.Resolve(host, port)`
|
||||
- `NetAccessOp.TcpConnect(host, port)`
|
||||
- `NetAccessOp.TcpListen(host, port, backlog)`
|
||||
- `NetAccessOp.UdpBind(host, port)`
|
||||
|
||||
---
|
||||
|
||||
#### Platform support
|
||||
|
||||
- **JVM:** supported
|
||||
- **Android:** supported via the Ktor CIO and Ktor sockets backends
|
||||
- **JS/Node:** supported for `resolve`, TCP client/server, and UDP
|
||||
- **JS/browser:** unsupported; capability checks report unavailable
|
||||
- **Linux Native:** supported via Ktor sockets
|
||||
- **Apple Native:** enabled via the shared native Ktor sockets backend; compile-verified, runtime not yet host-verified
|
||||
- **Other native targets:** currently report unsupported; use capability checks before relying on raw sockets
|
||||
@ -20,26 +20,24 @@ For external projects, ensure you have the appropriate Maven repository configur
|
||||
|
||||
---
|
||||
|
||||
#### Install the module into a Lyng session
|
||||
#### Install the module into a Lyng Scope
|
||||
|
||||
The process module is not installed automatically. The preferred host runtime is `EvalSession`: create the session, get its underlying scope, install the module there, and execute scripts through the session. You can customize access control via `ProcessAccessPolicy`.
|
||||
The process module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using `createProcessModule`. You can customize access control via `ProcessAccessPolicy`.
|
||||
|
||||
Kotlin (host) bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.io.process.createProcessModule
|
||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||
|
||||
suspend fun bootstrapProcess() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||
// ... inside a suspend function or runBlocking
|
||||
val scope: Scope = Script.newScope()
|
||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||
|
||||
// In scripts (or via session.eval), import the module:
|
||||
session.eval("import lyng.io.process")
|
||||
}
|
||||
// In scripts (or via scope.eval), import the module:
|
||||
scope.eval("import lyng.io.process")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -1,279 +0,0 @@
|
||||
# `lyng.io.ws` - WebSocket client for Lyng scripts
|
||||
|
||||
This module provides a compact WebSocket client API for Lyng scripts. It is implemented in `lyngio` and currently backed by Ktor WebSockets on the JVM.
|
||||
|
||||
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||
>
|
||||
> **Shared type note:** `WsMessage` is also available from `lyng.io.ws.types` when host code wants the reusable message type without depending on the WebSocket client module itself.
|
||||
|
||||
## Install The Module Into A Lyng Session
|
||||
|
||||
Kotlin host bootstrap example:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.ws.createWsModule
|
||||
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
||||
|
||||
suspend fun bootstrapWs() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createWsModule(PermitAllWsAccessPolicy, scope)
|
||||
session.eval("import lyng.io.ws")
|
||||
}
|
||||
```
|
||||
|
||||
## Using From Lyng Scripts
|
||||
|
||||
### Text Exchange
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_TEST_URL)
|
||||
ws.sendText("ping")
|
||||
val m: WsMessage = ws.receive()
|
||||
ws.close()
|
||||
[ws.url() == WS_TEST_URL, m.isText, m.text]
|
||||
>>> [true,true,echo:ping]
|
||||
```
|
||||
|
||||
### Binary Exchange
|
||||
|
||||
```lyng
|
||||
import lyng.buffer
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_TEST_BINARY_URL)
|
||||
ws.sendBytes(Buffer(9, 8, 7))
|
||||
val m: WsMessage = ws.receive()
|
||||
ws.close()
|
||||
[m.isText, (m.data as Buffer).hex]
|
||||
>>> [false,010203090807]
|
||||
```
|
||||
|
||||
### Secure `wss` Exchange
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WSS_TEST_URL)
|
||||
ws.sendText("ping")
|
||||
val m: WsMessage = ws.receive()
|
||||
ws.close()
|
||||
[ws.url() == WSS_TEST_URL, m.text]
|
||||
>>> [true,secure:ping]
|
||||
```
|
||||
|
||||
## Message Flow And Session Lifecycle
|
||||
|
||||
### Reading Incoming Messages
|
||||
|
||||
Call `ws.receive()` to wait for the next application message.
|
||||
|
||||
What `receive()` returns:
|
||||
- `WsMessage` for the next text or binary message.
|
||||
- `null` after the peer closes the connection cleanly.
|
||||
- `null` after the transport has already been closed and no more messages can arrive.
|
||||
|
||||
What reaches Lyng code:
|
||||
- Text frames are exposed as `WsMessage(isText = true, text = ...)`.
|
||||
- Binary frames are exposed as `WsMessage(isText = false, data = ...)`.
|
||||
- Fragmented websocket messages are reassembled before they are returned.
|
||||
- Ping and pong control frames are handled internally and are not returned by `receive()`.
|
||||
- Incoming close frames are handled internally; after that `receive()` returns `null`.
|
||||
|
||||
Typical receive loop:
|
||||
|
||||
```lyng
|
||||
import lyng.buffer
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_URL)
|
||||
|
||||
while (true) {
|
||||
val msg = ws.receive() ?: break
|
||||
|
||||
if (msg.isText) {
|
||||
println("text=" + msg.text)
|
||||
} else {
|
||||
println("bytes=" + ((msg.data as Buffer).size))
|
||||
}
|
||||
}
|
||||
|
||||
println("peer closed the websocket")
|
||||
```
|
||||
|
||||
### Sending Outgoing Messages
|
||||
|
||||
Use:
|
||||
- `ws.sendText(text)` for UTF-8 text messages.
|
||||
- `ws.sendBytes(data)` for binary messages.
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.buffer
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_URL)
|
||||
ws.sendText("hello")
|
||||
ws.sendBytes(Buffer(1, 2, 3, 4))
|
||||
```
|
||||
|
||||
Send behavior:
|
||||
- Each call sends one websocket message.
|
||||
- The API does not expose partial-frame streaming; send the whole message in one call.
|
||||
- If the session is already closed, `sendText(...)` and `sendBytes(...)` fail with a websocket error.
|
||||
- If the transport breaks during send, the session is released and the send call fails.
|
||||
|
||||
### Detecting Closed Connections
|
||||
|
||||
Use both signals together:
|
||||
- `ws.isOpen()` tells you whether the session is still considered open right now.
|
||||
- `ws.receive() == null` tells you the receive side has reached the end of the websocket session.
|
||||
|
||||
Practical rule:
|
||||
- If `receive()` returns `null`, stop reading and treat the session as closed.
|
||||
- After close has been observed, do not attempt further sends.
|
||||
|
||||
The API does not currently expose the peer close code or close reason to Lyng code.
|
||||
|
||||
### Closing The Connection Yourself
|
||||
|
||||
Call `ws.close()` when you are done.
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_URL)
|
||||
ws.sendText("bye")
|
||||
ws.close(1000, "done")
|
||||
```
|
||||
|
||||
Close semantics:
|
||||
- `close()` sends a websocket close frame with the given code and reason.
|
||||
- Defaults are `code = 1000` and `reason = ""`.
|
||||
- After `close()`, the session is released locally and should be treated as closed immediately.
|
||||
- Calling `close()` on an already closed session is a no-op.
|
||||
- After local close, `receive()` returns `null` and further sends fail.
|
||||
|
||||
### Recommended Usage Pattern
|
||||
|
||||
For request-response style exchanges:
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_URL)
|
||||
try {
|
||||
ws.sendText("ping")
|
||||
val reply = ws.receive() ?: error("socket closed before reply")
|
||||
println(reply.text)
|
||||
} finally {
|
||||
ws.close()
|
||||
}
|
||||
```
|
||||
|
||||
For long-lived consumers:
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect(WS_URL)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
val msg = ws.receive() ?: break
|
||||
if (msg.isText) {
|
||||
println(msg.text)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ws.close()
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `Ws`
|
||||
|
||||
- `isSupported(): Bool` - whether WebSocket client support is available on the current runtime.
|
||||
- `connect(url: String, headers...): WsSession` - open a client websocket session.
|
||||
|
||||
`headers...` accepts:
|
||||
- `MapEntry`, for example `"Authorization" => "Bearer x"`
|
||||
- 2-item lists, for example `["Authorization", "Bearer x"]`
|
||||
|
||||
### `WsSession`
|
||||
|
||||
- `isOpen(): Bool`
|
||||
- `url(): String`
|
||||
- `sendText(text: String): void`
|
||||
- `sendBytes(data: Buffer): void`
|
||||
- `receive(): WsMessage?`
|
||||
- `close(code: Int = 1000, reason: String = ""): void`
|
||||
|
||||
Behavior summary:
|
||||
- `receive()` returns `null` after close.
|
||||
- `close()` is safe to call more than once.
|
||||
- send operations require an open session.
|
||||
|
||||
### `WsMessage`
|
||||
|
||||
- `isText: Bool`
|
||||
- `text: String?`
|
||||
- `data: Buffer?`
|
||||
|
||||
Payload rules:
|
||||
- Text messages populate `text` and leave `data == null`.
|
||||
- Binary messages populate `data` and leave `text == null`.
|
||||
|
||||
## Security Policy
|
||||
|
||||
The module uses `WsAccessPolicy` to authorize websocket operations.
|
||||
|
||||
- `WsAccessPolicy` - interface for custom policies.
|
||||
- `PermitAllWsAccessPolicy` - allows all websocket operations.
|
||||
- `WsAccessOp.Connect(url)`
|
||||
- `WsAccessOp.Send(url, bytes, isText)`
|
||||
- `WsAccessOp.Receive(url)`
|
||||
|
||||
Example restricted policy in Kotlin:
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyngio.fs.security.AccessContext
|
||||
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||
import net.sergeych.lyngio.fs.security.Decision
|
||||
import net.sergeych.lyngio.ws.security.WsAccessOp
|
||||
import net.sergeych.lyngio.ws.security.WsAccessPolicy
|
||||
|
||||
val allowLocalOnly = object : WsAccessPolicy {
|
||||
override suspend fun check(op: WsAccessOp, ctx: AccessContext): AccessDecision =
|
||||
when (op) {
|
||||
is WsAccessOp.Connect ->
|
||||
if (
|
||||
op.url.startsWith("ws://127.0.0.1:") ||
|
||||
op.url.startsWith("wss://127.0.0.1:") ||
|
||||
op.url.startsWith("ws://localhost:") ||
|
||||
op.url.startsWith("wss://localhost:")
|
||||
)
|
||||
AccessDecision(Decision.Allow)
|
||||
else
|
||||
AccessDecision(Decision.Deny, "only local ws/wss connections are allowed")
|
||||
|
||||
else -> AccessDecision(Decision.Allow)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
- **JVM:** supported.
|
||||
- **Android:** supported via the Ktor CIO websocket client backend.
|
||||
- **JS:** supported via the Ktor JS websocket client backend.
|
||||
- **Linux native:** supported via the Ktor Curl websocket client backend.
|
||||
- **Windows native:** supported via the Ktor WinHttp websocket client backend.
|
||||
- **Apple native:** supported via the Ktor Darwin websocket client backend.
|
||||
- **Other targets:** may report unsupported; use `Ws.isSupported()` before relying on websocket client access.
|
||||
122
docs/lyng_cli.md
122
docs/lyng_cli.md
@ -1,15 +1,13 @@
|
||||
# Lyng CLI (`lyng`)
|
||||
### Lyng CLI (`lyng`)
|
||||
|
||||
The Lyng CLI is the reference command-line tool for the Lyng language. It lets you:
|
||||
|
||||
- Run Lyng scripts from files or inline strings (shebangs accepted)
|
||||
- Use standard argument passing (`ARGV`) to your scripts.
|
||||
- Resolve local file imports from the executed script's directory tree.
|
||||
- Format Lyng source files via the built-in `fmt` subcommand.
|
||||
- Register synchronous process-exit handlers with `atExit(...)`.
|
||||
|
||||
|
||||
## Building on Linux
|
||||
#### Building on Linux
|
||||
|
||||
Requirements:
|
||||
- JDK 17+ (for Gradle and the JVM distribution)
|
||||
@ -21,7 +19,7 @@ The repository provides convenience scripts in `bin/` for local builds and insta
|
||||
Note: In this repository the scripts are named `bin/local_release` and `bin/local_jrelease`. In some environments these may be aliased as `bin/release` and `bin/jrelease`. The steps below use the actual file names present here.
|
||||
|
||||
|
||||
### Option A: Native linuxX64 executable (`lyng`)
|
||||
##### Option A: Native linuxX64 executable (`lyng`)
|
||||
|
||||
1) Build the native binary:
|
||||
|
||||
@ -40,27 +38,26 @@ What this does:
|
||||
- Produces `distributables/lyng-linuxX64.zip` containing the `lyng` executable.
|
||||
|
||||
|
||||
### Option B: JVM distribution (`jlyng` launcher)
|
||||
##### Option B: JVM distribution (`jlyng` launcher)
|
||||
|
||||
This creates a JVM distribution with a launcher script, packages it as a downloadable zip, and links it to `~/bin/jlyng`.
|
||||
This creates a JVM distribution with a launcher script and links it to `~/bin/jlyng`.
|
||||
|
||||
```
|
||||
bin/local_jrelease
|
||||
```
|
||||
|
||||
What this does:
|
||||
- Runs `./gradlew :lyng:jvmDistZip` to build the JVM app distribution archive at `lyng/build/distributions/lyng-jvm.zip`.
|
||||
- Copies the archive to `distributables/lyng-jvm.zip`.
|
||||
- Unpacks that distribution under `~/bin/jlyng-jvm`.
|
||||
- Runs `./gradlew :lyng:installJvmDist` to build the JVM app distribution to `lyng/build/install/lyng-jvm`.
|
||||
- Copies the distribution under `~/bin/jlyng-jvm`.
|
||||
- Creates a symlink `~/bin/jlyng` pointing to the launcher script.
|
||||
|
||||
|
||||
## Usage
|
||||
#### Usage
|
||||
|
||||
Once installed, ensure `~/bin` is on your `PATH`. You can then use either the native `lyng` or the JVM `jlyng` launcher (both have the same CLI surface).
|
||||
|
||||
|
||||
### Running scripts
|
||||
##### Running scripts
|
||||
|
||||
- Run a script by file name and pass arguments to `ARGV`:
|
||||
|
||||
@ -75,7 +72,6 @@ lyng -- -my-script.lyng arg1 arg2
|
||||
```
|
||||
|
||||
- Execute inline code with `-x/--execute` and pass positional args to `ARGV`:
|
||||
- Inline execution does not scan the filesystem for local modules; only file-based execution does.
|
||||
|
||||
```
|
||||
lyng -x "println(\"Hello\")" more args
|
||||
@ -88,101 +84,7 @@ lyng --version
|
||||
lyng --help
|
||||
```
|
||||
|
||||
### Exit handlers: `atExit(...)`
|
||||
|
||||
The CLI exposes a CLI-only builtin:
|
||||
|
||||
```lyng
|
||||
extern fun atExit(append: Bool=true, handler: ()->Void)
|
||||
```
|
||||
|
||||
Use it to register synchronous cleanup handlers that should run when the CLI process is leaving.
|
||||
|
||||
Semantics:
|
||||
- `append=true` appends the handler to the end of the queue.
|
||||
- `append=false` inserts the handler at the front of the queue.
|
||||
- Handlers run one by one.
|
||||
- Exceptions thrown by a handler are ignored, and the next handler still runs.
|
||||
- Handlers are best-effort and run on:
|
||||
- normal script completion
|
||||
- script failure
|
||||
- script `exit(code)`
|
||||
- process shutdown such as `SIGTERM`
|
||||
|
||||
Non-goals:
|
||||
- `SIGKILL`, hard crashes, and power loss cannot be intercepted.
|
||||
- `atExit` is currently a CLI feature only; it is not part of the general embedding/runtime surface.
|
||||
|
||||
Examples:
|
||||
|
||||
```lyng
|
||||
atExit {
|
||||
println("closing resources")
|
||||
}
|
||||
|
||||
atExit(false) {
|
||||
println("runs first")
|
||||
}
|
||||
```
|
||||
|
||||
### Local imports for file execution
|
||||
|
||||
When you execute a script file, the CLI builds a temporary local import manager rooted at the directory that contains the entry script.
|
||||
|
||||
Formal structure:
|
||||
|
||||
- Root directory: the parent directory of the script passed to `lyng`.
|
||||
- Scan scope: every `.lyng` file under that root directory, recursively.
|
||||
- Entry script: the executed file itself is not registered as an importable module.
|
||||
- Module name mapping: `relative/path/to/file.lyng` maps to import name `relative.path.to.file`.
|
||||
- Package declaration: if a scanned file starts with `package ...` as its first non-blank line, that package name must exactly match the relative path mapping.
|
||||
- Package omission: if there is no leading `package` declaration, the CLI uses the relative path mapping as the module name.
|
||||
- Duplicates: if two files resolve to the same module name, CLI execution fails before script execution starts.
|
||||
- Import visibility: only files inside the entry root subtree are considered. Parent directories and sibling projects are not searched.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
project/
|
||||
main.lyng
|
||||
util/answer.lyng
|
||||
math/add.lyng
|
||||
```
|
||||
|
||||
`util/answer.lyng` is imported as `import util.answer`.
|
||||
|
||||
`math/add.lyng` is imported as `import math.add`.
|
||||
|
||||
Example contents:
|
||||
|
||||
```lyng
|
||||
// util/answer.lyng
|
||||
package util.answer
|
||||
|
||||
import math.add
|
||||
|
||||
fun answer() = plus(40, 2)
|
||||
```
|
||||
|
||||
```lyng
|
||||
// math/add.lyng
|
||||
fun plus(a, b) = a + b
|
||||
```
|
||||
|
||||
```lyng
|
||||
// main.lyng
|
||||
import util.answer
|
||||
|
||||
println(answer())
|
||||
```
|
||||
|
||||
Rationale:
|
||||
|
||||
- The module name is deterministic from the filesystem layout.
|
||||
- Explicit `package` remains available as a consistency check instead of a second, conflicting naming system.
|
||||
- The import search space stays local to the executed script, which avoids accidental cross-project resolution.
|
||||
|
||||
## Use in shell scripts
|
||||
### Use in shell scripts
|
||||
|
||||
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
|
||||
|
||||
@ -190,7 +92,7 @@ Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts direct
|
||||
println("Hello, world!")
|
||||
|
||||
|
||||
### Formatting source: `fmt` subcommand
|
||||
##### Formatting source: `fmt` subcommand
|
||||
|
||||
Format Lyng files with the built-in formatter.
|
||||
|
||||
@ -232,7 +134,7 @@ lyng fmt --spacing --wrap src/file.lyng
|
||||
```
|
||||
|
||||
|
||||
## Notes
|
||||
#### Notes
|
||||
|
||||
- Both native and JVM distributions expose the same CLI interface. Use whichever best fits your environment.
|
||||
- When executing scripts, all positional arguments after the script name are available in Lyng as `ARGV`.
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
`lyngio` is a separate library that extends the Lyng core (`lynglib`) with powerful, multiplatform, and secure I/O capabilities.
|
||||
|
||||
> **Important native networking limit:** `lyng.io.net` on current native targets is suitable for modest workloads, local tools, and test servers, but not yet for high-connection-count production servers. For serious HTTP/TCP serving, prefer the JVM target for now. If native high-concurrency networking matters for your use case, please open or upvote an issue at <https://github.com/sergeych/lyng/issues>.
|
||||
|
||||
#### Why a separate module?
|
||||
|
||||
1. **Security:** I/O and process execution are sensitive operations. By keeping them in a separate module, we ensure that the Lyng core remains 100% safe by default. You only enable what you explicitly need.
|
||||
@ -12,15 +10,9 @@
|
||||
|
||||
#### Included Modules
|
||||
|
||||
- **[lyng.io.db](lyng.io.db.md):** Portable SQL database access. Provides `Database`, `SqlTransaction`, `ResultSet`, SQLite support through `lyng.io.db.sqlite`, and JVM JDBC support through `lyng.io.db.jdbc`.
|
||||
- **[lyng.io.fs](lyng.io.fs.md):** Async filesystem access. Provides the `Path` class for file/directory operations, streaming, and globbing.
|
||||
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
||||
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
||||
- **[lyng.io.http](lyng.io.http.md):** HTTP/HTTPS client access. Provides `Http`, `HttpRequest`, `HttpResponse`, and `HttpHeaders`.
|
||||
- **[lyng.io.http.server](lyng.io.http.server.md):** Minimal HTTP/1.1 and WebSocket server. Provides `HttpServer`, `Router`, `ServerRequest`, `RequestContext`, and `ServerWebSocket`.
|
||||
- **[lyng.io.ws](lyng.io.ws.md):** WebSocket client access. Provides `Ws`, `WsSession`, and `WsMessage`.
|
||||
- **[lyng.io.net](lyng.io.net.md):** Transport networking. Provides `Net`, `TcpSocket`, `TcpServer`, `UdpSocket`, and `SocketAddress`.
|
||||
- **Shared networking type packages:** `lyng.io.http.types`, `lyng.io.ws.types`, and `lyng.io.net.types` expose reusable value types such as `HttpHeaders`, `WsMessage`, `IpVersion`, `SocketAddress`, and `Datagram` when host code wants type-only imports without installing the corresponding capability object module.
|
||||
|
||||
---
|
||||
|
||||
@ -45,58 +37,31 @@ dependencies {
|
||||
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.io.db.createDbModule
|
||||
import net.sergeych.lyng.io.db.jdbc.createJdbcModule
|
||||
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.io.process.createProcessModule
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyng.io.http.createHttpModule
|
||||
import net.sergeych.lyng.io.net.createNetModule
|
||||
import net.sergeych.lyng.io.ws.createWsModule
|
||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
||||
|
||||
suspend fun runMyScript() {
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val scope = Script.newScope()
|
||||
|
||||
// Install modules with policies
|
||||
createDbModule(scope)
|
||||
createJdbcModule(scope)
|
||||
createSqliteModule(scope)
|
||||
createFs(PermitAllAccessPolicy, scope)
|
||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||
createHttpModule(PermitAllHttpAccessPolicy, scope)
|
||||
createNetModule(PermitAllNetAccessPolicy, scope)
|
||||
createWsModule(PermitAllWsAccessPolicy, scope)
|
||||
|
||||
// Now scripts can import them
|
||||
session.eval("""
|
||||
import lyng.io.db
|
||||
import lyng.io.db.jdbc
|
||||
import lyng.io.db.sqlite
|
||||
scope.eval("""
|
||||
import lyng.io.fs
|
||||
import lyng.io.process
|
||||
import lyng.io.console
|
||||
import lyng.io.http
|
||||
import lyng.io.net
|
||||
import lyng.io.ws
|
||||
|
||||
println("H2 JDBC available: " + (openH2("mem:demo;DB_CLOSE_DELAY=-1") != null))
|
||||
println("SQLite available: " + (openSqlite(":memory:") != null))
|
||||
println("Working dir: " + Path(".").readUtf8())
|
||||
println("OS: " + Platform.details().name)
|
||||
println("TTY: " + Console.isTty())
|
||||
println("HTTP available: " + Http.isSupported())
|
||||
println("TCP available: " + Net.isTcpAvailable())
|
||||
println("WS available: " + Ws.isSupported())
|
||||
""")
|
||||
}
|
||||
```
|
||||
@ -108,39 +73,23 @@ suspend fun runMyScript() {
|
||||
`lyngio` is built with a "Secure by Default" philosophy. Every I/O or process operation is checked against a policy.
|
||||
|
||||
- **Filesystem Security:** Implement `FsAccessPolicy` to restrict access to specific paths or operations (e.g., read-only access to a sandbox directory).
|
||||
- **Database Installation:** Database access is still explicit-capability style. The host must install `lyng.io.db` and at least one provider such as `lyng.io.db.sqlite` or `lyng.io.db.jdbc`; otherwise scripts cannot open databases.
|
||||
- **Process Security:** Implement `ProcessAccessPolicy` to restrict which executables can be run or to disable shell execution entirely.
|
||||
- **Console Security:** Implement `ConsoleAccessPolicy` to control output writes, event reads, and raw mode switching.
|
||||
- **HTTP Security:** Implement `HttpAccessPolicy` to restrict which requests scripts may send.
|
||||
- **Transport Security:** Implement `NetAccessPolicy` to restrict DNS resolution and TCP/UDP socket operations.
|
||||
- **WebSocket Security:** Implement `WsAccessPolicy` to restrict websocket connects and message flow.
|
||||
|
||||
For more details, see the specific module documentation:
|
||||
- [Database Module Details](lyng.io.db.md)
|
||||
- [Filesystem Security Details](lyng.io.fs.md#access-policy-security)
|
||||
- [Process Security Details](lyng.io.process.md#security-policy)
|
||||
- [Console Module Details](lyng.io.console.md)
|
||||
- [HTTP Module Details](lyng.io.http.md)
|
||||
- [HTTP Server Module Details](lyng.io.http.server.md)
|
||||
- [Transport Networking Details](lyng.io.net.md)
|
||||
- [WebSocket Module Details](lyng.io.ws.md)
|
||||
|
||||
---
|
||||
|
||||
#### Platform Support Overview
|
||||
|
||||
| Platform | lyng.io.db/sqlite | lyng.io.db/jdbc | lyng.io.fs | lyng.io.process | lyng.io.console | lyng.io.http | lyng.io.ws | lyng.io.net |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Linux Native** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Apple Native** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||
| **Windows Native** | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Android** | ⚠️ | ❌ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||
| **JS / Node** | ❌ | ❌ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||
| **JS / Browser** | ❌ | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ✅ | ✅ | ❌ |
|
||||
| **Wasm** | ❌ | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ❌ | ❌ | ❌ |
|
||||
|
||||
Legend:
|
||||
- `✅` supported
|
||||
- `⚠️` available but environment-dependent or not fully host-verified yet
|
||||
- `❌` unsupported
|
||||
| Platform | lyng.io.fs | lyng.io.process | lyng.io.console |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| **JVM** | ✅ | ✅ | ✅ (baseline) |
|
||||
| **Native (Linux/macOS)** | ✅ | ✅ | 🚧 |
|
||||
| **Native (Windows)** | ✅ | 🚧 (Planned) | 🚧 |
|
||||
| **Android** | ✅ | ❌ | ❌ |
|
||||
| **NodeJS** | ✅ | ❌ | ❌ |
|
||||
| **Browser / Wasm** | ✅ (In-memory) | ❌ | ❌ |
|
||||
|
||||
76
docs/math.md
76
docs/math.md
@ -60,13 +60,8 @@ but:
|
||||
|
||||
## Round and range
|
||||
|
||||
The following functions return the argument unchanged if it is `Int`.
|
||||
|
||||
For `Decimal`:
|
||||
- `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations
|
||||
- the result stays `Decimal`
|
||||
|
||||
For `Real`, the result is a transformed `Real`.
|
||||
The following functions return its argument if it is `Int`,
|
||||
or transformed `Real` otherwise.
|
||||
|
||||
| name | description |
|
||||
|----------------|--------------------------------------------------------|
|
||||
@ -77,14 +72,6 @@ For `Real`, the result is a transformed `Real`.
|
||||
|
||||
## Lyng math functions
|
||||
|
||||
Decimal note:
|
||||
- all scalar math helpers accept `Decimal`
|
||||
- `abs(x)` stays exact for `Decimal`
|
||||
- `pow(x, y)` is exact for `Decimal` when `y` is an integral exponent
|
||||
- the remaining `Decimal` cases currently use a temporary bridge:
|
||||
`Decimal -> Real -> host math -> Decimal`
|
||||
- this is temporary; native decimal implementations are planned
|
||||
|
||||
| name | meaning |
|
||||
|-----------|------------------------------------------------------|
|
||||
| sin(x) | sine |
|
||||
@ -104,7 +91,7 @@ Decimal note:
|
||||
| log10(x) | $log_{10}(x)$ |
|
||||
| pow(x, y) | ${x^y}$ |
|
||||
| sqrt(x) | $ \sqrt {x}$ |
|
||||
| abs(x) | absolute value of x. Int if x is Int, Decimal if x is Decimal, Real otherwise |
|
||||
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
|
||||
| clamp(x, range) | limit x to be inside range boundaries |
|
||||
|
||||
For example:
|
||||
@ -117,69 +104,12 @@ For example:
|
||||
assert( abs(-1) is Int)
|
||||
assert( abs(-2.21) == 2.21 )
|
||||
|
||||
import lyng.decimal
|
||||
|
||||
// Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily:
|
||||
assert( (abs("-2.5".d) as Decimal).toStringExpanded() == "2.5" )
|
||||
assert( (floor("2.9".d) as Decimal).toStringExpanded() == "2" )
|
||||
assert( sin("0.5".d) is Decimal )
|
||||
|
||||
// clamp() limits value to the range:
|
||||
assert( clamp(15, 0..10) == 10 )
|
||||
assert( clamp(-5, 0..10) == 0 )
|
||||
assert( 5.clamp(0..10) == 5 )
|
||||
>>> void
|
||||
|
||||
## Linear algebra: `lyng.matrix`
|
||||
|
||||
For vectors and dense matrices, import `lyng.matrix`:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
```
|
||||
|
||||
It provides:
|
||||
|
||||
- `Vector`
|
||||
- `Matrix`
|
||||
- `vector(values)`
|
||||
- `matrix(rows)`
|
||||
|
||||
Core operations include:
|
||||
|
||||
- matrix addition and subtraction
|
||||
- matrix-matrix multiplication
|
||||
- matrix-vector multiplication
|
||||
- transpose
|
||||
- determinant
|
||||
- inverse
|
||||
- linear solve
|
||||
- vector dot, norm, normalize, cross, outer product
|
||||
|
||||
Example:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
val b: Matrix = matrix([[7, 8], [9, 10], [11, 12]])
|
||||
val product: Matrix = a * b
|
||||
assertEquals([[58.0, 64.0], [139.0, 154.0]], product.toList())
|
||||
```
|
||||
|
||||
Matrices also support two-axis bracket indexing and slicing:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||
assertEquals(6.0, m[1, 2])
|
||||
val sub: Matrix = m[0..1, 1..2]
|
||||
assertEquals([[2.0, 3.0], [5.0, 6.0]], sub.toList())
|
||||
```
|
||||
|
||||
See [Matrix](Matrix.md) for the full API.
|
||||
|
||||
## Random values
|
||||
|
||||
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
||||
|
||||
@ -32,36 +32,10 @@ Depending on the platform, these coroutines may be executed on different CPU and
|
||||
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, cancel it if it is no longer needed, and retrieve possible result.
|
||||
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.
|
||||
|
||||
When you have an iterable of deferreds, use `joinAll()` to await all of them and collect results in input order:
|
||||
|
||||
val jobs = (1..4).map { n ->
|
||||
launch {
|
||||
delay(1)
|
||||
n * 10
|
||||
}
|
||||
}
|
||||
assertEquals([10, 20, 30, 40], jobs.joinAll())
|
||||
>>> void
|
||||
|
||||
If you no longer need the result, cancel the deferred. Awaiting a cancelled deferred throws `CancellationException`:
|
||||
|
||||
var reached = false
|
||||
val work = launch {
|
||||
delay(100)
|
||||
reached = true
|
||||
"ok"
|
||||
}
|
||||
work.cancel()
|
||||
assertThrows(CancellationException) { work.await() }
|
||||
assert(work.isCancelled)
|
||||
assert(!work.isActive)
|
||||
assert(!reached)
|
||||
>>> void
|
||||
|
||||
## Synchronization: Mutex
|
||||
|
||||
Suppose we have a resource, that could be used concurrently, a counter in our case. If we won't protect it, concurrent usage cause RC, Race Condition, providing wrong result:
|
||||
@ -230,73 +204,6 @@ Flows allow easy transforming of any [Iterable]. See how the standard Lyng libra
|
||||
}
|
||||
}
|
||||
|
||||
## Channel
|
||||
|
||||
A [Channel] is a **hot pipe** between coroutines: values are pushed in by a producer and pulled out by a consumer, with each value consumed exactly once.
|
||||
|
||||
Unlike a `Flow` (which is cold and re-runs its generator on every collection), a `Channel` is stateful — the right tool for classic _producer / consumer_ work.
|
||||
|
||||
val ch = Channel() // rendezvous: sender waits for receiver
|
||||
|
||||
val producer = launch {
|
||||
for (i in 1..5) ch.send(i)
|
||||
ch.close() // signal: no more values
|
||||
}
|
||||
|
||||
var item = ch.receive() // suspends until a value is ready
|
||||
while (item != null) {
|
||||
println(item)
|
||||
item = ch.receive()
|
||||
}
|
||||
// prints 1 2 3 4 5
|
||||
|
||||
`receive()` returns `null` when the channel is both closed and fully drained — that is the idiomatic loop termination condition.
|
||||
|
||||
Channels can also be buffered so the producer can run ahead:
|
||||
|
||||
val ch = Channel(4) // buffer up to 4 items without blocking
|
||||
|
||||
ch.send(10)
|
||||
ch.send(20)
|
||||
ch.send(30)
|
||||
ch.close()
|
||||
|
||||
assertEquals(10, ch.receive())
|
||||
assertEquals(20, ch.receive())
|
||||
assertEquals(30, ch.receive())
|
||||
assertEquals(null, ch.receive()) // drained
|
||||
|
||||
For the full API — including `tryReceive`, `Channel.UNLIMITED`, and the fan-out / ping-pong patterns — see the [Channel] reference page.
|
||||
|
||||
## LaunchPool
|
||||
|
||||
When you need **bounded concurrency** — run at most *N* tasks at the same time without spawning a new coroutine per task — use [LaunchPool]:
|
||||
|
||||
```lyng
|
||||
val pool = LaunchPool(4) // at most 4 tasks run in parallel
|
||||
|
||||
val jobs = (1..20).map { n ->
|
||||
pool.launch { expensiveCompute(n) }
|
||||
}
|
||||
pool.closeAndJoin() // wait for all tasks to complete
|
||||
|
||||
val results = jobs.joinAll()
|
||||
```
|
||||
|
||||
Exceptions thrown inside a submitted lambda are captured in the returned `Deferred` and do not crash the pool, so other tasks continue running normally.
|
||||
|
||||
See [LaunchPool] for the full API including bounded queues and cancellation.
|
||||
|
||||
[LaunchPool]: LaunchPool.md
|
||||
|
||||
| | Flow | Channel |
|
||||
|---|---|---|
|
||||
| **temperature** | cold (lazy) | hot (eager) |
|
||||
| **replay** | every collector gets a fresh run | each item consumed once |
|
||||
| **consumers** | any number, each gets all items | one receiver per item |
|
||||
| **typical use** | transform pipelines, sequences | producer–consumer, fan-out |
|
||||
|
||||
[Channel]: Channel.md
|
||||
|
||||
[Iterable]: Iterable.md
|
||||
|
||||
|
||||
@ -114,12 +114,10 @@ When running end‑to‑end “book” workloads or heavier benches, you can ena
|
||||
Flags are mutable at runtime, e.g.:
|
||||
|
||||
```kotlin
|
||||
runTest {
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val r1 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val r2 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||
}
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val r1 = (Scope().eval(script) as ObjInt).value
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val r2 = (Scope().eval(script) as ObjInt).value
|
||||
```
|
||||
|
||||
Reset flags at the end of a test to avoid impacting other tests.
|
||||
@ -621,3 +619,4 @@ Reproduce
|
||||
Notes
|
||||
- Negative caches are installed only after a real miss throws (cache‑after‑miss), preserving error semantics and invalidation on `layoutVersion` changes.
|
||||
- IndexRef PIC augments the existing direct path and uses move‑to‑front promotion; it is keyed on `(classId, layoutVersion)` like other PICs.
|
||||
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
## Pi Spigot JVM Baseline
|
||||
|
||||
Saved on April 4, 2026 before the `List<Int>` indexed-access follow-up fix.
|
||||
|
||||
Benchmark target:
|
||||
- [examples/pi-bench.py](/home/sergeych/dev/lyng/examples/pi-bench.py)
|
||||
- [examples/pi-bench.lyng](../examples/pi-bench.lyng)
|
||||
|
||||
Execution path:
|
||||
- Python: `python3 examples/pi-bench.py`
|
||||
- Lyng JVM: `./gradlew :lyng:runJvm --args='/home/sergeych/dev/lyng/examples/pi-bench.lyng'`
|
||||
- Constraint: do not use Kotlin/Native `lyng` CLI for perf comparisons
|
||||
|
||||
Baseline measurements:
|
||||
- Python full script: `167 ms`
|
||||
- Lyng JVM full script: `1.287097604 s`
|
||||
- Python warm function average over 5 runs: `126.126 ms`
|
||||
- Lyng JVM warm function average over 5 runs: about `1071.6 ms`
|
||||
|
||||
Baseline ratio:
|
||||
- Full script: about `7.7x` slower on Lyng JVM
|
||||
- Warm function only: about `8.5x` slower on Lyng JVM
|
||||
|
||||
Primary finding at baseline:
|
||||
- The hot `reminders[j]` accesses in `piSpigot` were still lowered through boxed object index ops and boxed arithmetic.
|
||||
- Newly added `GET_INDEX_INT` and `SET_INDEX_INT` only reached `pi`, not `reminders`.
|
||||
- Root cause: initializer element inference handled list literals, but not `List.fill(boxes) { 2 }`, so `reminders` did not become known `List<Int>` at compile time.
|
||||
|
||||
## After Optimizations 1-4
|
||||
|
||||
Follow-up change:
|
||||
- propagate inferred lambda return class into bytecode compilation
|
||||
- infer `List.fill(...)` element type from the fill lambda
|
||||
- lower `reminders[j]` reads and writes to `GET_INDEX_INT` and `SET_INDEX_INT`
|
||||
- add primitive-backed `ObjList` storage for all-int lists
|
||||
- lower `List.fill(Int) { Int }` to `LIST_FILL_INT`
|
||||
- stop boxing the integer index inside `GET_INDEX_INT` / `SET_INDEX_INT`
|
||||
|
||||
Verification:
|
||||
- `piSpigot` disassembly now contains typed ops for `reminders`, for example:
|
||||
- `GET_INDEX_INT s5(reminders), s10(j), ...`
|
||||
- `SET_INDEX_INT s5(reminders), s10(j), ...`
|
||||
|
||||
Post-change measurements using `jlyng`:
|
||||
- Full script: `655.819559 ms`
|
||||
- Warm 5-run total: `1.430945810 s`
|
||||
- Warm average per run: about `286.2 ms`
|
||||
|
||||
Observed improvement vs baseline:
|
||||
- Full script: about `1.96x` faster (`1.287 s -> 0.656 s`)
|
||||
- Warm function: about `3.74x` faster (`1071.6 ms -> 286.2 ms`)
|
||||
|
||||
Residual gap vs Python baseline:
|
||||
- Full script: Lyng JVM is still about `3.9x` slower than Python (`655.8 ms` vs `167 ms`)
|
||||
- Warm function: Lyng JVM is still about `2.3x` slower than Python (`286.2 ms` vs `126.126 ms`)
|
||||
|
||||
Current benchmark-test snapshot (`n=200`, JVM test harness):
|
||||
- `optimized-int-division-rval-off`: `135 ms`
|
||||
- `optimized-int-division-rval-on`: `125 ms`
|
||||
- `piSpigot` bytecode now contains:
|
||||
- `LIST_FILL_INT` for both `pi` and `reminders`
|
||||
- `GET_INDEX_INT` / `SET_INDEX_INT` for the hot indexed loop
|
||||
@ -1,50 +0,0 @@
|
||||
class Tag(name: String) {
|
||||
val name = name
|
||||
var inner = ""
|
||||
|
||||
fun child(tagName: String, block: Tag.()->void) {
|
||||
val child = Tag(tagName)
|
||||
with(child) { block(this) }
|
||||
inner += child.render()
|
||||
}
|
||||
|
||||
fun head(block: Tag.()->void) { child("head", block) }
|
||||
fun body(block: Tag.()->void) { child("body", block) }
|
||||
fun title(block: Tag.()->void) { child("title", block) }
|
||||
fun h1(block: Tag.()->void) { child("h1", block) }
|
||||
|
||||
fun addText(text: String) {
|
||||
inner += text
|
||||
}
|
||||
|
||||
fun render() {
|
||||
"<" + name + ">" + inner + "</" + name + ">"
|
||||
}
|
||||
}
|
||||
|
||||
context(Tag)
|
||||
fun String.unaryPlus() {
|
||||
this@Tag.addText(this)
|
||||
}
|
||||
|
||||
fun html(block: Tag.()->void) {
|
||||
val root = Tag("html")
|
||||
with(root) { block(this) }
|
||||
root.render()
|
||||
}
|
||||
|
||||
val page = html {
|
||||
head {
|
||||
title {
|
||||
+"Demo"
|
||||
}
|
||||
}
|
||||
body {
|
||||
h1 {
|
||||
+"Heading 1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println(page)
|
||||
assertEquals("<html><head><title>Demo</title></head><body><h1>Heading 1</h1></body></html>", page)
|
||||
@ -1,9 +1,6 @@
|
||||
// Sample: Operator Overloading in Lyng
|
||||
|
||||
class Vector<T>(val x: T, val y: T) {
|
||||
// Overload unary +
|
||||
fun unaryPlus() = this
|
||||
|
||||
// Overload +
|
||||
fun plus(other: Vector<U>) = Vector(x + other.x, y + other.y)
|
||||
|
||||
@ -31,11 +28,6 @@ val v2 = Vector(5, 5)
|
||||
println("v1: " + v1)
|
||||
println("v2: " + v2)
|
||||
|
||||
// Test unary +
|
||||
val v0 = +v1
|
||||
println("+v1 = " + v0)
|
||||
assertEquals(Vector(10, 20), v0)
|
||||
|
||||
// Test binary +
|
||||
val v3 = v1 + v2
|
||||
println("v1 + v2 = " + v3)
|
||||
|
||||
@ -6,7 +6,6 @@ This page documents the **current** rules: static name resolution, closure captu
|
||||
|
||||
## Current rules (bytecode compiler)
|
||||
- **All names resolve at compile time**: locals, parameters, captures, members, imports, and module globals must be known when compiling. Missing symbols are compile-time errors.
|
||||
- **Exception: `compile if` can skip dead branches**: inside an untaken `compile if (...)` branch, names are not resolved or type-checked at all. This is the supported way to guard optional classes or packages such as `defined(Udp)` or `defined(lyng.io.net)`.
|
||||
- **No runtime fallbacks**: there is no dynamic name lookup, no fallback opcodes, and no “search parent scopes” at runtime for missing names.
|
||||
- **Object members on unknown types only**: `toString`, `toInspectString`, `let`, `also`, `apply`, `run` are allowed on unknown types; all other members require a statically known receiver type or an explicit cast.
|
||||
- **Closures capture slots**: lambdas and nested functions capture **frame slots** directly. Captures are resolved at compile time and compiled to slot references.
|
||||
|
||||
@ -1,40 +1,6 @@
|
||||
# Lyng serialization
|
||||
|
||||
Lyng has a built-in serialization module, `lyng.serialization`.
|
||||
|
||||
There are now two built-in formats with different goals:
|
||||
|
||||
- `Lynon`: the canonical binary format for Lyng values.
|
||||
- `Json`: the canonical JSON-based round-trip format for Lyng values.
|
||||
|
||||
In addition, `Obj.toJson()` / `toJsonString()` remain available as a plain JSON projection for interoperability with
|
||||
regular JSON tools and Kotlin `kotlinx.serialization`.
|
||||
|
||||
## Canonical formats
|
||||
|
||||
`Lynon` and `Json` are both exposed as format objects with the same surface:
|
||||
|
||||
- `Format.encode(value)`
|
||||
- `Format.decode(encodedValue)`
|
||||
|
||||
For the built-in formats:
|
||||
|
||||
- `Lynon.encode(x)` returns `BitBuffer`
|
||||
- `Lynon.decode(bitBuffer)` returns the original Lyng value
|
||||
- `Json.encode(x)` returns `String`
|
||||
- `Json.decode(jsonString)` returns the original Lyng value
|
||||
|
||||
`Json` also provides a typed canonical mode:
|
||||
|
||||
- `Json.encodeAs(Type, value)` returns `String`
|
||||
- `Json.decodeAs(Type, jsonString)` returns the original Lyng value of the specified type
|
||||
|
||||
This is still canonical JSON, but it is schema-driven instead of fully self-describing.
|
||||
|
||||
## Lynon
|
||||
|
||||
Lynon is LYng Object Notation. It is typed, binary, bit-effective, implements caching, automatic compression,
|
||||
variable-length integers, one-bit booleans, and preserves Lyng runtime structure well.
|
||||
Lyng has builting binary bit-effective serialization format, called Lynon for LYng Object Notation. It is typed, binary, implements caching, automatic compression, variable-length ints, one-bit Booleans an many nice features.
|
||||
|
||||
It is as simple as:
|
||||
|
||||
@ -54,8 +20,7 @@ It is as simple as:
|
||||
assert( text.length > (encodedBits.toBuffer() as Buffer).size )
|
||||
>>> void
|
||||
|
||||
Any class you create is serializable by default; Lynon serializes first constructor fields, then any `var` member
|
||||
fields.
|
||||
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields.
|
||||
|
||||
## Transient Fields
|
||||
|
||||
@ -75,7 +40,7 @@ class MyData(@Transient val tempSecret, val publicData) {
|
||||
|
||||
Transient fields:
|
||||
- Are **omitted** from Lynon binary streams.
|
||||
- Are **omitted** from JSON output (`toJson`) and canonical `Json.encode(...)`.
|
||||
- Are **omitted** from JSON output (via `toJson`).
|
||||
- Are **ignored** during structural equality checks (`==`).
|
||||
- If a transient constructor parameter has a **default value**, it will be restored to that default value during deserialization. Otherwise, it will be `null`.
|
||||
- Class body fields marked as `@Transient` will keep their initial values (or values assigned in `init`) after deserialization.
|
||||
@ -84,131 +49,8 @@ Transient fields:
|
||||
|
||||
- **Singleton Objects**: `object` declarations are serializable by name. Their state (mutable fields) is also serialized and restored, respecting `@Transient`.
|
||||
- **Classes**: Class objects themselves can be serialized. They are serialized by their full qualified name. When converted to JSON, a class object includes its public static fields (excluding those marked `@Transient`).
|
||||
- **Exceptions**: canonical formats preserve exception class, message, extra data, and captured stack trace.
|
||||
|
||||
## Plain JSON projection vs canonical Json format
|
||||
|
||||
There are two JSON-related APIs and they serve different purposes:
|
||||
|
||||
- `Obj.toJson()` / `toJsonString()`
|
||||
- produce ordinary JSON values
|
||||
- best for interop with external JSON systems
|
||||
- best for `Obj.decodeSerializable()` / `decodeSerializableWith()`
|
||||
- may be lossy for Lyng-specific structures
|
||||
|
||||
- `Json.encode()` / `Json.decode()`
|
||||
- produce JSON text too
|
||||
- use Lyng-specific type tags so the payload is self-describing
|
||||
- intended for round-tripping Lyng values
|
||||
- intended to match Lynon semantics where JSON can carry them
|
||||
- still keep ordinary string-key maps in traditional JSON object form
|
||||
- can preserve values that plain JSON cannot represent directly, such as:
|
||||
- maps with non-string keys
|
||||
- sets
|
||||
- buffers and bit buffers
|
||||
- class instances
|
||||
- singleton objects
|
||||
- enums
|
||||
- exceptions
|
||||
- date/time objects
|
||||
- non-finite reals
|
||||
- `void`
|
||||
|
||||
- `Json.encodeAs(Type, value)` / `Json.decodeAs(Type, text)`
|
||||
- also round-trip Lyng values through JSON text
|
||||
- use the declared or requested type as decoding schema
|
||||
- recursively omit type tags when the declared type is already exact enough
|
||||
- keep canonical tags when the runtime value is more specific than the declared type
|
||||
- produce less noisy JSON for closed and otherwise precisely-typed object graphs
|
||||
- still keep ordinary `Map<String, ...>` values in traditional JSON object form
|
||||
|
||||
Why this split exists:
|
||||
|
||||
- plain `toJson()` must remain ordinary JSON so it stays convenient for external JSON systems and Kotlin
|
||||
`kotlinx.serialization`
|
||||
- canonical `Json.encode()` is for Lyng-to-Lyng transport through JSON text without any external schema, so it must
|
||||
remain self-describing and preserve Lyng runtime
|
||||
distinctions whenever possible
|
||||
- `Json.encodeAs()` exists for the cases where a schema is known on both sides and we want canonical round-trip
|
||||
behavior with fewer tags
|
||||
- one API cannot optimize for both goals at once: either you get too many Lyng tags for ordinary JSON interop, or you
|
||||
get lossy round-trips
|
||||
|
||||
Example:
|
||||
|
||||
import lyng.serialization
|
||||
import lyng.time
|
||||
|
||||
enum Color { Red, Green }
|
||||
class Point(x,y) { var z = 42 }
|
||||
|
||||
val p = Point(1,2)
|
||||
p.z = 99
|
||||
val x = List(
|
||||
p,
|
||||
Map([1, "one"], ["two", 2]),
|
||||
Set(1,2,3),
|
||||
"hello".encodeUtf8(),
|
||||
Date(2026,4,15),
|
||||
Color.Green
|
||||
)
|
||||
|
||||
assertEquals(x, Json.decode(Json.encode(x)))
|
||||
>>> void
|
||||
|
||||
Typed canonical example:
|
||||
|
||||
import lyng.serialization
|
||||
|
||||
closed class Point(x: Int, y: Int)
|
||||
closed class Segment(a: Point, b: Point)
|
||||
|
||||
val value = Segment(Point(0,1), Point(2,3))
|
||||
val json = Json.encodeAs(Segment, value)
|
||||
|
||||
assertEquals("{\"a\":{\"x\":0,\"y\":1},\"b\":{\"x\":2,\"y\":3}}", json)
|
||||
assertEquals(value, Json.decodeAs(Segment, json))
|
||||
>>> void
|
||||
|
||||
## Adding more formats from Kotlin modules
|
||||
|
||||
External modules can add new formats on the Kotlin side.
|
||||
|
||||
The common base class is:
|
||||
|
||||
```kotlin
|
||||
abstract class ObjSerializationFormatClass(className: String) : ObjClass(className) {
|
||||
abstract suspend fun encodeValue(scope: Scope, value: Obj): Obj
|
||||
abstract suspend fun decodeValue(scope: Scope, encoded: Obj): Obj
|
||||
}
|
||||
```
|
||||
|
||||
To export a new format from a module:
|
||||
|
||||
```kotlin
|
||||
im.addPackage("test.formats") { module ->
|
||||
module.bindSerializationFormat(
|
||||
object : ObjSerializationFormatClass("Reverse") {
|
||||
override suspend fun encodeValue(scope: Scope, value: Obj): Obj =
|
||||
ObjString(value.toString(scope).value.reversed())
|
||||
|
||||
override suspend fun decodeValue(scope: Scope, encoded: Obj): Obj =
|
||||
ObjString((encoded as ObjString).value.reversed())
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Then from Lyng, after importing the Kotlin module above, usage looks like:
|
||||
|
||||
```lyng
|
||||
import test.formats
|
||||
|
||||
assertEquals("cba", Reverse.encode("abc"))
|
||||
assertEquals("abc", Reverse.decode("cba"))
|
||||
```
|
||||
|
||||
## Notes
|
||||
## Custom Serialization
|
||||
|
||||
Important is to understand that normally `Lynon.decode` wants [BitBuffer], as `Lynon.encode` produces. If you have the regular [Buffer], be sure to convert it:
|
||||
|
||||
@ -217,3 +59,5 @@ Important is to understand that normally `Lynon.decode` wants [BitBuffer], as `L
|
||||
this possibly creates extra zero bits at the end, as bit content could be shorter than byte-grained but for the Lynon format it does not make sense. Note that when you serialize [BitBuffer], exact number of bits is written. To convert bit buffer to bytes:
|
||||
|
||||
Lynon.encode("hello").toBuffer()
|
||||
|
||||
(topic is incomplete and under construction)
|
||||
|
||||
185
docs/time.md
185
docs/time.md
@ -1,135 +1,74 @@
|
||||
# Lyng time functions
|
||||
|
||||
Lyng date and time support requires importing `lyng.time`. The module provides four related types:
|
||||
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
||||
|
||||
- `Instant` for absolute timestamps.
|
||||
- `Date` for calendar dates without time-of-day or timezone.
|
||||
- `DateTime` for calendar-aware points in time in a specific timezone.
|
||||
- `Duration` for absolute elapsed time.
|
||||
- `Instant` class for absolute time stamps with platform-dependent resolution.
|
||||
- `DateTime` class for calendar-aware points in time within a specific time zone.
|
||||
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
|
||||
|
||||
## Time instant: `Instant`
|
||||
|
||||
`Instant` represents some moment of time independently of the calendar. It is similar to SQL `TIMESTAMP`
|
||||
or Kotlin `Instant`.
|
||||
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
|
||||
|
||||
### Constructing and converting
|
||||
|
||||
import lyng.time
|
||||
|
||||
// default constructor returns time now:
|
||||
val t1 = Instant()
|
||||
val t2 = Instant(1704110400)
|
||||
|
||||
// constructing from a number is treated as seconds since unix epoch:
|
||||
val t2 = Instant(1704110400) // 2024-01-01T12:00:00Z
|
||||
|
||||
// from RFC3339 string:
|
||||
val t3 = Instant("2024-01-01T12:00:00.123456Z")
|
||||
|
||||
val t4 = t3.truncateToMinute()
|
||||
assertEquals("2024-01-01T12:00:00Z", t4.toRFC3339())
|
||||
|
||||
|
||||
// truncation:
|
||||
val t4 = t3.truncateToMinute
|
||||
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
|
||||
|
||||
// to localized DateTime (uses system default TZ if not specified):
|
||||
val dt = t3.toDateTime("+02:00")
|
||||
assertEquals(14, dt.hour)
|
||||
|
||||
val d = t3.toDate("Z")
|
||||
assertEquals(Date(2024, 1, 1), d)
|
||||
assertEquals(dt.hour, 14)
|
||||
|
||||
### Instant members
|
||||
|
||||
| member | description |
|
||||
|--------------------------------|------------------------------------------------------|
|
||||
| epochSeconds: Real | offset in seconds since Unix epoch |
|
||||
| epochWholeSeconds: Int | whole seconds since Unix epoch |
|
||||
| nanosecondsOfSecond: Int | nanoseconds within the current second |
|
||||
| isDistantFuture: Bool | true if it is `Instant.distantFuture` |
|
||||
| isDistantPast: Bool | true if it is `Instant.distantPast` |
|
||||
| truncateToMinute(): Instant | truncate to minute precision |
|
||||
| truncateToSecond(): Instant | truncate to second precision |
|
||||
| truncateToMillisecond(): Instant | truncate to millisecond precision |
|
||||
| truncateToMicrosecond(): Instant | truncate to microsecond precision |
|
||||
| toRFC3339(): String | format as RFC3339 string in UTC |
|
||||
| toDateTime(tz?): DateTime | localize to a timezone |
|
||||
| toDate(tz?): Date | convert to a calendar date in a timezone |
|
||||
|
||||
## Calendar date: `Date`
|
||||
|
||||
`Date` represents a pure calendar date. It has no time-of-day and no attached timezone. Use it for values
|
||||
like birthdays, due dates, invoice dates, and SQL `DATE` columns.
|
||||
|
||||
### Constructing
|
||||
|
||||
import lyng.time
|
||||
|
||||
val today = Date()
|
||||
val d1 = Date(2026, 4, 15)
|
||||
val d2 = Date("2024-02-29")
|
||||
val d3 = Date.parseIso("2024-02-29")
|
||||
val d4 = Date(DateTime(2024, 5, 20, 15, 30, 45, "+02:00"))
|
||||
val d5 = Date(Instant("2024-01-01T23:30:00Z"), "+02:00")
|
||||
|
||||
### Date members
|
||||
|
||||
| member | description |
|
||||
|--------------------------------|------------------------------------------------------------|
|
||||
| year: Int | year component |
|
||||
| month: Int | month component (1..12) |
|
||||
| day: Int | day of month (alias `dayOfMonth`) |
|
||||
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||
| dayOfYear: Int | day of year (1..365/366) |
|
||||
| isLeapYear: Bool | whether this date is in a leap year |
|
||||
| lengthOfMonth: Int | number of days in this month |
|
||||
| lengthOfYear: Int | 365 or 366 |
|
||||
| toIsoString(): String | ISO `YYYY-MM-DD` string |
|
||||
| toSortableString(): String | alias to `toIsoString()` |
|
||||
| toDateTime(tz="Z"): DateTime | start-of-day `DateTime` in the specified timezone |
|
||||
| atStartOfDay(tz="Z"): DateTime | alias to `toDateTime()` |
|
||||
| addDays(n): Date | add or subtract calendar days |
|
||||
| addMonths(n): Date | add or subtract months, normalizing end-of-month |
|
||||
| addYears(n): Date | add or subtract years |
|
||||
| daysUntil(other): Int | calendar days until `other` |
|
||||
| daysSince(other): Int | calendar days since `other` |
|
||||
| static today(tz?): Date | today in the specified timezone |
|
||||
| static parseIso(s): Date | parse ISO `YYYY-MM-DD` |
|
||||
|
||||
### Date arithmetic
|
||||
|
||||
`Date` supports only whole-day arithmetic. This is deliberate: calendar dates should not silently accept
|
||||
sub-day durations.
|
||||
|
||||
import lyng.time
|
||||
|
||||
val d1 = Date(2026, 4, 15)
|
||||
val d2 = d1.addDays(10)
|
||||
|
||||
assertEquals(Date(2026, 4, 25), d2)
|
||||
assertEquals(Date(2026, 4, 18), d1 + 3.days)
|
||||
assertEquals(Date(2026, 4, 12), d1 - 3.days)
|
||||
assertEquals(10, d1.daysUntil(d2))
|
||||
assertEquals(10, d2.daysSince(d1))
|
||||
assertEquals(10, d2 - d1)
|
||||
|
||||
### Date conversions
|
||||
|
||||
import lyng.time
|
||||
|
||||
val i = Instant("2024-01-01T23:30:00Z")
|
||||
assertEquals(Date(2024, 1, 1), i.toDate("Z"))
|
||||
assertEquals(Date(2024, 1, 2), i.toDate("+02:00"))
|
||||
|
||||
val dt = DateTime(2024, 5, 20, 15, 30, 45, "+02:00")
|
||||
assertEquals(Date(2024, 5, 20), dt.date)
|
||||
assertEquals(Date(2024, 5, 20), dt.toDate())
|
||||
assertEquals(DateTime(2024, 5, 20, 0, 0, 0, "Z"), Date(2024, 5, 20).toDateTime("Z"))
|
||||
assertEquals(DateTime(2024, 5, 20, 0, 0, 0, "+02:00"), Date(2024, 5, 20).atStartOfDay("+02:00"))
|
||||
| member | description |
|
||||
|--------------------------------|---------------------------------------------------------|
|
||||
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
||||
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
||||
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos |
|
||||
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
||||
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
||||
| truncateToMinute: Instant | create new instance truncated to minute |
|
||||
| truncateToSecond: Instant | create new instance truncated to second |
|
||||
| truncateToMillisecond: Instant | truncate new instance to millisecond |
|
||||
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
||||
| toRFC3339(): String | format as RFC3339 string (UTC) |
|
||||
| toDateTime(tz?): DateTime | localize to a TimeZone (ID string or offset seconds) |
|
||||
|
||||
## Calendar time: `DateTime`
|
||||
|
||||
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components
|
||||
such as year, month, day, and hour.
|
||||
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year,
|
||||
month, and day.
|
||||
|
||||
### Constructing
|
||||
|
||||
import lyng.time
|
||||
|
||||
// Current time in system default timezone
|
||||
val now = DateTime.now()
|
||||
|
||||
// Specific timezone
|
||||
val offsetTime = DateTime.now("+02:00")
|
||||
|
||||
// From Instant
|
||||
val dt = Instant().toDateTime("Z")
|
||||
|
||||
// By components (year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
|
||||
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
|
||||
|
||||
// From RFC3339 string
|
||||
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
||||
|
||||
### DateTime members
|
||||
@ -144,9 +83,7 @@ such as year, month, day, and hour.
|
||||
| second: Int | second component (0..59) |
|
||||
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||
| timeZone: String | timezone ID string |
|
||||
| date: Date | calendar date component |
|
||||
| toInstant(): Instant | convert back to absolute Instant |
|
||||
| toDate(): Date | extract the calendar date in this timezone |
|
||||
| toUTC(): DateTime | shortcut to convert to UTC |
|
||||
| toTimeZone(tz): DateTime | convert to another timezone |
|
||||
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
||||
@ -159,27 +96,28 @@ such as year, month, day, and hour.
|
||||
|
||||
`DateTime` handles calendar arithmetic correctly:
|
||||
|
||||
import lyng.time
|
||||
|
||||
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
||||
val nextYear = leapDay.addYears(1)
|
||||
assertEquals(28, nextYear.day)
|
||||
assertEquals(nextYear.day, 28) // Feb 29, 2024 -> Feb 28, 2025
|
||||
|
||||
# `Duration` class
|
||||
|
||||
`Duration` represents absolute elapsed time between two instants.
|
||||
Represent absolute time distance between two `Instant`.
|
||||
|
||||
import lyng.time
|
||||
|
||||
val t1 = Instant()
|
||||
delay(1.millisecond)
|
||||
val t2 = Instant()
|
||||
|
||||
assert(t2 - t1 >= 1.millisecond)
|
||||
assert(t2 - t1 < 100.millisecond)
|
||||
// yes we can delay to period, and it is not blocking. is suspends!
|
||||
delay(1.millisecond)
|
||||
|
||||
val t2 = Instant()
|
||||
// be suspend, so actual time may vary:
|
||||
assert( t2 - t1 >= 1.millisecond)
|
||||
assert( t2 - t1 < 100.millisecond)
|
||||
>>> void
|
||||
|
||||
Duration values can be created from numbers using extensions on `Int` and `Real`:
|
||||
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
|
||||
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
|
||||
|
||||
- `n.millisecond`, `n.milliseconds`
|
||||
- `n.second`, `n.seconds`
|
||||
@ -187,9 +125,10 @@ Duration values can be created from numbers using extensions on `Int` and `Real`
|
||||
- `n.hour`, `n.hours`
|
||||
- `n.day`, `n.days`
|
||||
|
||||
Larger units like months or years are calendar-dependent and are intentionally not part of `Duration`.
|
||||
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
|
||||
|
||||
Each duration instance can be converted to numbers in these units:
|
||||
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration`
|
||||
instance:
|
||||
|
||||
- `d.microseconds`
|
||||
- `d.milliseconds`
|
||||
@ -198,16 +137,18 @@ Each duration instance can be converted to numbers in these units:
|
||||
- `d.hours`
|
||||
- `d.days`
|
||||
|
||||
Example:
|
||||
for example
|
||||
|
||||
import lyng.time
|
||||
assertEquals( 60, 1.minute.seconds )
|
||||
assertEquals( 10.milliseconds, 0.01.seconds )
|
||||
|
||||
assertEquals(60, 1.minute.seconds)
|
||||
assertEquals(10.milliseconds, 0.01.seconds)
|
||||
>>> void
|
||||
|
||||
# Utility functions
|
||||
|
||||
## `delay(duration: Duration)`
|
||||
## delay(duration: Duration)
|
||||
|
||||
Suspends current coroutine for at least the specified duration.
|
||||
|
||||
|
||||
Suspends the current coroutine for at least the specified duration.
|
||||
|
||||
203
docs/tutorial.md
203
docs/tutorial.md
@ -352,40 +352,6 @@ Sets `this` to the first argument and executes the block. Returns the value retu
|
||||
assertEquals(3, sum)
|
||||
>>> void
|
||||
|
||||
Receiver lambdas can also keep outer receivers in scope. The primary receiver wins for unqualified lookup, and `this@Type`
|
||||
selects an outer receiver explicitly:
|
||||
|
||||
class Html { fun lang() = "en" }
|
||||
class Body { fun lang() = "body" }
|
||||
|
||||
fun html(block: Html.()->String) = with(Html()) { block(this) }
|
||||
fun body(block: Body.()->String) = with(Body()) { block(this) }
|
||||
|
||||
val result = html {
|
||||
body {
|
||||
lang() + ":" + this@Html.lang()
|
||||
}
|
||||
}
|
||||
assertEquals("body:en", result)
|
||||
>>> void
|
||||
|
||||
You can declare the same requirement in a function type:
|
||||
|
||||
val block: context(Html) Body.()->String = {
|
||||
lang() + ":" + this@Html.lang()
|
||||
}
|
||||
|
||||
If the primary receiver does not define a member and multiple outer/context receivers do, Lyng reports an ambiguity instead of picking one silently:
|
||||
|
||||
class A { fun title() = "a" }
|
||||
class B { fun title() = "b" }
|
||||
class C
|
||||
|
||||
val block: context(A, B) C.()->String = {
|
||||
// title() // compile-time ambiguity
|
||||
this@A.title()
|
||||
}
|
||||
|
||||
## run
|
||||
|
||||
Executes a block after it returning the value passed by the block. for example, can be used with elvis operator:
|
||||
@ -409,18 +375,6 @@ It is rather simple, like everywhere else:
|
||||
|
||||
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
||||
|
||||
For linear algebra, import `lyng.matrix`:
|
||||
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[1, 2], [3, 4]])
|
||||
val i: Matrix = Matrix.identity(2)
|
||||
val sum: Matrix = a + i
|
||||
assertEquals([[2.0, 2.0], [3.0, 5.0]], sum.toList())
|
||||
>>> void
|
||||
|
||||
See [Matrix](Matrix.md) for vectors, matrix multiplication, inversion, and slicing such as `m[0..2, 1]`.
|
||||
|
||||
Logical operation could be used the same
|
||||
|
||||
var x = 10
|
||||
@ -845,12 +799,6 @@ Lyng has built-in mutable array class `List` with simple literals:
|
||||
many collection based methods are implemented there.
|
||||
For immutable list values, use `list.toImmutable()` and [ImmutableList].
|
||||
|
||||
To construct a list programmatically, use the static helper `List.fill`:
|
||||
|
||||
val tens = List.fill(5) { index -> index * 10 }
|
||||
assertEquals([0, 10, 20, 30, 40], tens)
|
||||
>>> void
|
||||
|
||||
Lists can contain any type of objects, lists too:
|
||||
|
||||
val list = [1, [2, 3], 4]
|
||||
@ -863,14 +811,6 @@ Lists can contain any type of objects, lists too:
|
||||
|
||||
Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md).
|
||||
|
||||
In general, bracket indexing may contain more than one selector:
|
||||
|
||||
value[i]
|
||||
value[i, j]
|
||||
|
||||
For built-in lists, strings, maps, and buffers, the selector is usually a single value such as an `Int`, `Range`, or `Regex`.
|
||||
For types with custom indexers, multiple selectors are packed into one list-like index object and passed to `getAt` / `putAt`.
|
||||
|
||||
When you want to "flatten" it to single array, you can use splat syntax:
|
||||
|
||||
[1, ...[2,3], 4]
|
||||
@ -1128,37 +1068,6 @@ Or, more neat:
|
||||
>>> just 3
|
||||
>>> void
|
||||
|
||||
## compile if
|
||||
|
||||
`compile if` is a compile-time conditional. Unlike normal `if`, the compiler evaluates its condition while compiling
|
||||
the file and completely skips the untaken branch. This is useful when some class or package may or may not be
|
||||
available:
|
||||
|
||||
compile if (defined(Udp)) {
|
||||
val socket = Udp()
|
||||
println("udp is available")
|
||||
} else {
|
||||
println("udp is not available")
|
||||
}
|
||||
|
||||
`compile if` also supports single-statement branches:
|
||||
|
||||
compile if (defined(lyng.io.net) && !defined(Udp))
|
||||
println("network module exists, but Udp is not visible here")
|
||||
else
|
||||
println("either Udp exists or the module is unavailable")
|
||||
|
||||
Current condition syntax is intentionally limited to compile-time symbol checks:
|
||||
|
||||
- `defined(Name)`
|
||||
- `defined(package.name)`
|
||||
- `!`, `&&`, `||`
|
||||
- parentheses
|
||||
|
||||
Examples:
|
||||
compile if (defined(Udp) && defined(Tcp))
|
||||
println("both transports are available")
|
||||
|
||||
## When
|
||||
|
||||
See also: [Comprehensive guide to `when`](when.md)
|
||||
@ -1430,41 +1339,6 @@ size and index access, like lists:
|
||||
"total letters: "+letters
|
||||
>>> "total letters: 10"
|
||||
|
||||
When you need a counting loop that goes backwards, use an explicit descending
|
||||
range:
|
||||
|
||||
var sum = 0
|
||||
for( i in 5 downTo 1 ) {
|
||||
sum += i
|
||||
}
|
||||
sum
|
||||
>>> 15
|
||||
|
||||
If the lower bound should be excluded, use `downUntil`:
|
||||
|
||||
val xs = []
|
||||
for( i in 5 downUntil 1 ) {
|
||||
xs.add(i)
|
||||
}
|
||||
xs
|
||||
>>> [5,4,3,2]
|
||||
|
||||
This is intentionally explicit: `5..1` is an empty ascending range, not an
|
||||
implicit reverse loop.
|
||||
|
||||
Descending loops also support `step`:
|
||||
|
||||
val xs = []
|
||||
for( i in 10 downTo 1 step 3 ) {
|
||||
xs.add(i)
|
||||
}
|
||||
xs
|
||||
>>> [10,7,4,1]
|
||||
|
||||
For descending ranges, `step` stays positive. The direction comes from
|
||||
`downTo` / `downUntil`, so `10 downTo 1 step 3` is valid, while
|
||||
`10 downTo 1 step -3` is an error.
|
||||
|
||||
For loop support breaks the same as while loops above:
|
||||
|
||||
fun search(haystack, needle) {
|
||||
@ -1594,12 +1468,6 @@ It could be open and closed:
|
||||
assert( 5 !in (1..<5) )
|
||||
>>> void
|
||||
|
||||
Descending ranges are explicit too:
|
||||
|
||||
assertEquals([5,4,3,2,1], (5 downTo 1).toList())
|
||||
assertEquals([5,4,3,2], (5 downUntil 1).toList())
|
||||
>>> void
|
||||
|
||||
Ranges could be inside other ranges:
|
||||
|
||||
assert( (2..3) in (1..10) )
|
||||
@ -1612,19 +1480,11 @@ There are character ranges too:
|
||||
|
||||
and you can use ranges in for-loops:
|
||||
|
||||
for( x in 'a'..<'c' ) println(x)
|
||||
for( x in 'a' ..< 'c' ) println(x)
|
||||
>>> a
|
||||
>>> b
|
||||
>>> void
|
||||
|
||||
Descending character ranges work the same way:
|
||||
|
||||
for( ch in 'e' downTo 'a' step 2 ) println(ch)
|
||||
>>> e
|
||||
>>> c
|
||||
>>> a
|
||||
>>> void
|
||||
|
||||
See [Ranges](Range.md) for detailed documentation on it.
|
||||
|
||||
# Time routines
|
||||
@ -1688,27 +1548,15 @@ The type for the character objects is `Char`.
|
||||
|
||||
### String literal escapes
|
||||
|
||||
Lyng string literals can use either double quotes or backticks:
|
||||
|
||||
val a = "hello"
|
||||
val b = `hello`
|
||||
assert(a == b)
|
||||
|
||||
| escape | ASCII value |
|
||||
|--------|-----------------------|
|
||||
| \n | 0x10, newline |
|
||||
| \r | 0x13, carriage return |
|
||||
| \t | 0x07, tabulation |
|
||||
| \\ | \ slash character |
|
||||
| \" | " double quote |
|
||||
| \uXXXX | unicode code point |
|
||||
|
||||
Delimiter-specific escapes:
|
||||
|
||||
| form | escape | value |
|
||||
|--------|--------|------------------|
|
||||
| `"..."` | \" | " double quote |
|
||||
| `` `...` `` | \` | ` backtick |
|
||||
|
||||
Unicode escape form is exactly 4 hex digits, e.g. `"\u263A"` -> `☺`.
|
||||
|
||||
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
||||
@ -1741,15 +1589,10 @@ Example:
|
||||
|
||||
val name = "Lyng"
|
||||
assertEquals("hello, Lyng!", "hello, $name!")
|
||||
assertEquals("hello, Lyng!", `hello, $name!`)
|
||||
assertEquals("sum=3", "sum=${1+2}")
|
||||
assertEquals("sum=3", `sum=${1+2}`)
|
||||
assertEquals("\$name", "\$name")
|
||||
assertEquals("\$name", "$$name")
|
||||
assertEquals("\$name", `\$name`)
|
||||
assertEquals("\$name", `$$name`)
|
||||
assertEquals("\\Lyng", "\\$name")
|
||||
assertEquals("\\Lyng", `\\$name`)
|
||||
>>> void
|
||||
|
||||
Interpolation and `printf`-style formatting can be combined when needed:
|
||||
@ -1856,14 +1699,6 @@ Open-ended ranges could be used to get start and end too:
|
||||
assertEquals( "pult", "catapult"[ 4.. ])
|
||||
>>> void
|
||||
|
||||
The same bracket syntax is also used by imported numeric modules such as `lyng.matrix`, where indexing can be multi-axis:
|
||||
|
||||
import lyng.matrix
|
||||
|
||||
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6]])
|
||||
assertEquals(6.0, m[1, 2])
|
||||
>>> void
|
||||
|
||||
### String operations
|
||||
|
||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||
@ -1884,14 +1719,6 @@ Part match:
|
||||
assert( "foo" == ($~ as RegexMatch).value )
|
||||
>>> void
|
||||
|
||||
Replacing text:
|
||||
|
||||
assertEquals("bonono", "banana".replace('a', 'o'))
|
||||
assertEquals("a-b-c", "a.b.c".replace(".", "-")) // string patterns are literal
|
||||
assertEquals("v#.#.#", "v1.2.3".replace("\d+".re, "#"))
|
||||
assertEquals("v[1].[2].[3]", "v1.2.3".replace("(\d+)".re) { m -> "[" + m[1] + "]" })
|
||||
>>> void
|
||||
|
||||
Repeating the fragment:
|
||||
|
||||
assertEquals("hellohello", "hello"*2)
|
||||
@ -1927,8 +1754,6 @@ A typical set of String functions includes:
|
||||
| characters | create [List] of characters (1) |
|
||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||
| matches(re) | matches the regular expression (2) |
|
||||
| replace(old, new) | replace all literal or regex matches; regex needs [Regex] |
|
||||
| replaceFirst(old,new)| replace the first literal or regex match |
|
||||
| | |
|
||||
|
||||
(1)
|
||||
@ -2155,30 +1980,6 @@ Example with custom accessors:
|
||||
"abc".firstChar
|
||||
>>> 'a'
|
||||
|
||||
### Extension indexers
|
||||
|
||||
Indexers can also be extended by overriding `getAt` and `putAt` on the receiver:
|
||||
|
||||
```lyng
|
||||
object Storage
|
||||
|
||||
var storageData = {}
|
||||
|
||||
override fun Storage.getAt(key: String): Object? {
|
||||
storageData[key]
|
||||
}
|
||||
|
||||
override fun Storage.putAt(key: String, value: Object) {
|
||||
storageData[key] = value
|
||||
}
|
||||
|
||||
Storage["answer"] = 42
|
||||
val answer: Int? = Storage["answer"]
|
||||
assertEquals(42, answer)
|
||||
```
|
||||
|
||||
This works for classes and named singleton `object` declarations. Bracket syntax is lowered to `getAt` / `putAt`, and multiple selectors are packed into one list-like index object the same way as other custom indexers.
|
||||
|
||||
Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security.
|
||||
|
||||
To get details on OOP in Lyng, see [OOP notes](OOP.md).
|
||||
|
||||
@ -1,134 +1,16 @@
|
||||
# What's New in Lyng
|
||||
|
||||
This document highlights the current Lyng release, **1.5.5**, and the broader additions from the 1.5 cycle.
|
||||
It is intentionally user-facing: new language features, new modules, new tools, and the practical things you can build with them.
|
||||
For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5.md`.
|
||||
|
||||
## Release 1.5.5 Highlights
|
||||
|
||||
- `1.5.5` extends the 1.5 line with practical database APIs, first-class calendar dates, and better coroutine building blocks.
|
||||
- The 1.5 line now brings together richer ranges and loops, interpolation, math modules, immutable and observable collections, richer `lyngio`, and much better CLI/IDE support.
|
||||
- `1.5.5` adds `Channel`, `LaunchPool`, and `joinAll()` so coroutine-heavy scripts can coordinate work more directly.
|
||||
- `1.5.5` adds `Date`, the portable `lyng.io.db` layer, SQLite/JDBC providers, and a compatibility `lyng.legacy_digest` module.
|
||||
- `1.5.5` also continues runtime/compiler hardening with better import dispatch, faster exact lambda calls, and correct `val +=`/`-=` behavior for mutating types versus real reassignment.
|
||||
- The docs, homepage samples, and release metadata now point at the current stable version.
|
||||
|
||||
## User Highlights Across 1.5.x
|
||||
|
||||
- Descending ranges and loops with `downTo` / `downUntil`
|
||||
- String interpolation with `$name` and `${expr}`
|
||||
- Backtick string literals for raw-ish string text
|
||||
- Decimal arithmetic, matrices/vectors, and complex numbers
|
||||
- Calendar `Date` support in `lyng.time`
|
||||
- `Channel`, `LaunchPool`, and `joinAll()` for coroutine workflows
|
||||
- Immutable collections and opt-in `ObservableList`
|
||||
- Rich `lyngio` modules for SQLite/JDBC databases, console, HTTP, WebSocket, TCP, and UDP
|
||||
- Legacy SHA-1 compatibility helpers in `lyng.legacy_digest`
|
||||
- CLI improvements including the built-in formatter `lyng fmt`
|
||||
- Better IDE support and stronger docs around the released feature set
|
||||
This document highlights the latest additions and improvements to the Lyng language and its ecosystem.
|
||||
For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
||||
|
||||
## Language Features
|
||||
|
||||
### Descending Ranges and Loops
|
||||
Lyng ranges are no longer just ascending. You can now write explicit descending ranges with inclusive or exclusive lower bounds.
|
||||
|
||||
```lyng
|
||||
assertEquals([5,4,3,2,1], (5 downTo 1).toList())
|
||||
assertEquals([5,4,3,2], (5 downUntil 1).toList())
|
||||
|
||||
for (i in 10 downTo 1 step 3) {
|
||||
println(i)
|
||||
}
|
||||
```
|
||||
|
||||
This also works for characters:
|
||||
|
||||
```lyng
|
||||
assertEquals(['e','c','a'], ('e' downTo 'a' step 2).toList())
|
||||
```
|
||||
|
||||
See [Range](Range.md).
|
||||
|
||||
### String Interpolation
|
||||
Lyng 1.5.1 added built-in string interpolation:
|
||||
|
||||
- `$name`
|
||||
- `${expr}`
|
||||
|
||||
Literal dollar forms are explicit too:
|
||||
|
||||
- `\$` -> `$`
|
||||
- `$$` -> `$`
|
||||
|
||||
```lyng
|
||||
val name = "Lyng"
|
||||
assertEquals("hello, Lyng!", "hello, $name!")
|
||||
assertEquals("sum=3", "sum=${1+2}")
|
||||
assertEquals("\$name", "\$name")
|
||||
assertEquals("\$name", "$$name")
|
||||
```
|
||||
|
||||
If you need legacy literal-dollar behavior in a file, add:
|
||||
|
||||
```lyng
|
||||
// feature: interpolation: off
|
||||
```
|
||||
|
||||
See [Tutorial](tutorial.md).
|
||||
|
||||
### Matrix and Vector Module (`lyng.matrix`)
|
||||
Lyng now ships a dense linear algebra module with immutable double-precision `Matrix` and `Vector` types.
|
||||
|
||||
It provides:
|
||||
|
||||
- `matrix([[...]])` and `vector([...])`
|
||||
- matrix multiplication
|
||||
- matrix inversion
|
||||
- determinant, trace, rank
|
||||
- solving `A * x = b`
|
||||
- vector operations such as `dot`, `normalize`, `cross`, and `outer`
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val a: Matrix = matrix([[4, 7], [2, 6]])
|
||||
val inv: Matrix = a.inverse()
|
||||
assert(abs(inv.get(0, 0) - 0.6) < 1e-9)
|
||||
```
|
||||
|
||||
Matrices also support Lyng-style slicing:
|
||||
|
||||
```lyng
|
||||
import lyng.matrix
|
||||
|
||||
val m: Matrix = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||
assertEquals(6.0, m[1, 2])
|
||||
val column: Matrix = m[0..2, 2]
|
||||
val tail: Matrix = m[1.., 1..]
|
||||
assertEquals([[3.0], [6.0], [9.0]], column.toList())
|
||||
assertEquals([[5.0, 6.0], [8.0, 9.0]], tail.toList())
|
||||
```
|
||||
|
||||
See [Matrix](Matrix.md).
|
||||
|
||||
### Multiple Selectors in Bracket Indexing
|
||||
Bracket indexing now accepts more than one selector:
|
||||
|
||||
```lyng
|
||||
value[i]
|
||||
value[i, j]
|
||||
value[i, j, k]
|
||||
```
|
||||
|
||||
For custom indexers, multiple selectors are packed into one list-like index object and dispatched through `getAt` / `putAt`.
|
||||
This is the rule used by `lyng.matrix` and by embedding APIs for Kotlin-backed indexers.
|
||||
|
||||
### Decimal Arithmetic Module (`lyng.decimal`)
|
||||
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
||||
|
||||
It provides:
|
||||
|
||||
- `Decimal`
|
||||
- `BigDecimal`
|
||||
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
||||
- mixed arithmetic with `Int` and `Real`
|
||||
- local division precision and rounding control via `withDecimalContext(...)`
|
||||
@ -153,46 +35,6 @@ The distinction between `Real -> Decimal` and exact decimal parsing is explicit
|
||||
|
||||
See [Decimal](Decimal.md).
|
||||
|
||||
### Complex Numbers (`lyng.complex`)
|
||||
Lyng also ships a complex-number module for ordinary arithmetic in the complex plane.
|
||||
|
||||
```lyng
|
||||
import lyng.complex
|
||||
|
||||
assertEquals(Complex(1.0, 2.0), 1 + 2.i)
|
||||
assertEquals(Complex(2.0, 2.0), 2.i + 2)
|
||||
|
||||
val z = 1 + π.i
|
||||
println(z.exp())
|
||||
```
|
||||
|
||||
See [Complex](Complex.md).
|
||||
|
||||
### Legacy Digest Module (`lyng.legacy_digest`)
|
||||
|
||||
For situations where an external protocol or file format requires a SHA-1 value,
|
||||
Lyng now ships a `lyng.legacy_digest` module backed by a pure Kotlin/KMP
|
||||
implementation with no extra dependencies.
|
||||
|
||||
> ⚠️ SHA-1 is cryptographically broken. Use only for legacy-compatibility work.
|
||||
|
||||
```lyng
|
||||
import lyng.legacy_digest
|
||||
|
||||
val hex = LegacyDigest.sha1("abc")
|
||||
// → "a9993e364706816aba3e25717850c26c9cd0d89d"
|
||||
|
||||
// Also accepts raw bytes:
|
||||
import lyng.buffer
|
||||
val buf = Buffer.decodeHex("616263")
|
||||
assertEquals(hex, LegacyDigest.sha1(buf))
|
||||
```
|
||||
|
||||
The name `LegacyDigest` is intentional: it signals that these algorithms belong
|
||||
to a compatibility layer, not to a current security toolkit.
|
||||
|
||||
See [LegacyDigest](LegacyDigest.md).
|
||||
|
||||
### Binary Operator Interop Registry
|
||||
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.
|
||||
|
||||
@ -327,30 +169,13 @@ Singleton objects are declared using the `object` keyword. They provide a conven
|
||||
|
||||
```lyng
|
||||
object Config {
|
||||
val version = "1.5.6-SNAPSHOT"
|
||||
val version = "1.5.0-SNAPSHOT"
|
||||
fun show() = println("Config version: " + version)
|
||||
}
|
||||
|
||||
Config.show()
|
||||
```
|
||||
|
||||
Named singleton objects can also be used as extension receivers:
|
||||
|
||||
```lyng
|
||||
object X {
|
||||
fun base() = "base"
|
||||
}
|
||||
|
||||
fun X.decorate(value): String {
|
||||
this.base() + ":" + value.toString()
|
||||
}
|
||||
|
||||
val X.tag get() = this.base() + ":tag"
|
||||
|
||||
assertEquals("base:42", X.decorate(42))
|
||||
assertEquals("base:tag", X.tag)
|
||||
```
|
||||
|
||||
### Nested Declarations and Lifted Enums
|
||||
You can now declare classes, objects, enums, and type aliases inside another class. These nested declarations live in the class namespace (no outer instance capture) and are accessed with a qualifier.
|
||||
|
||||
@ -467,124 +292,8 @@ x.clamp(0..10) // returns 10
|
||||
|
||||
`clamp()` correctly handles inclusive (`..`) and exclusive (`..<`) ranges. For discrete types like `Int` and `Char`, clamping to an exclusive upper bound returns the previous value.
|
||||
|
||||
### Immutable Collections
|
||||
Lyng 1.5 adds immutable collection types for APIs that should not expose mutable state through aliases:
|
||||
|
||||
- `ImmutableList`
|
||||
- `ImmutableSet`
|
||||
- `ImmutableMap`
|
||||
|
||||
```lyng
|
||||
val a = ImmutableList(1,2,3)
|
||||
val b = a + 4
|
||||
|
||||
assertEquals(ImmutableList(1,2,3), a)
|
||||
assertEquals(ImmutableList(1,2,3,4), b)
|
||||
```
|
||||
|
||||
See [ImmutableList](ImmutableList.md), [ImmutableSet](ImmutableSet.md), and [ImmutableMap](ImmutableMap.md).
|
||||
|
||||
### Observable Mutable Lists
|
||||
For reactive-style code, `lyng.observable` provides `ObservableList` with hooks and change streams.
|
||||
|
||||
```lyng
|
||||
import lyng.observable
|
||||
|
||||
val xs = [1,2].observable()
|
||||
xs.onChange { println("changed") }
|
||||
xs += 3
|
||||
```
|
||||
|
||||
You can validate or reject mutations in `beforeChange`, listen in `onChange`, and consume structured change events from `changes()`.
|
||||
|
||||
See [ObservableList](ObservableList.md).
|
||||
|
||||
### Random API
|
||||
The standard library now includes a built-in random API plus deterministic seeded generators.
|
||||
|
||||
```lyng
|
||||
val rng = Random.seeded(1234)
|
||||
assert(rng.next(1..10) in 1..10)
|
||||
assert(rng.next('a'..<'f') in 'a'..<'f')
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
- `Random.nextInt()`
|
||||
- `Random.nextFloat()`
|
||||
- `Random.next(range)`
|
||||
- `Random.seeded(seed)`
|
||||
|
||||
## Tooling and Infrastructure
|
||||
|
||||
### Rich Console Apps with `lyng.io.console`
|
||||
`lyngio` now includes a real console module for terminal applications:
|
||||
|
||||
- TTY detection
|
||||
- screen clearing and cursor movement
|
||||
- alternate screen buffer
|
||||
- raw input mode
|
||||
- typed key and resize events
|
||||
|
||||
```lyng
|
||||
import lyng.io.console
|
||||
|
||||
Console.enterAltScreen()
|
||||
Console.clear()
|
||||
Console.moveTo(1, 1)
|
||||
Console.write("Hello from Lyng console app")
|
||||
Console.flush()
|
||||
Console.leaveAltScreen()
|
||||
```
|
||||
|
||||
The repository includes a full interactive Tetris sample built on this API.
|
||||
|
||||
See [lyng.io.console](lyng.io.console.md).
|
||||
|
||||
### HTTP, WebSocket, TCP, and UDP in `lyngio`
|
||||
`lyngio` grew from filesystem/process support into a broader application-facing I/O library. In 1.5.x it includes:
|
||||
|
||||
- `lyng.io.http` for HTTP/HTTPS client calls
|
||||
- `lyng.io.ws` for WebSocket clients
|
||||
- `lyng.io.net` for raw TCP/UDP transport
|
||||
|
||||
HTTP example:
|
||||
|
||||
```lyng
|
||||
import lyng.io.http
|
||||
|
||||
val r = Http.get("https://example.com")
|
||||
println(r.status)
|
||||
println(r.text())
|
||||
```
|
||||
|
||||
TCP example:
|
||||
|
||||
```lyng
|
||||
import lyng.io.net
|
||||
|
||||
val socket = Net.tcpConnect("127.0.0.1", 4040)
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
println(socket.readLine())
|
||||
socket.close()
|
||||
```
|
||||
|
||||
WebSocket example:
|
||||
|
||||
```lyng
|
||||
import lyng.io.ws
|
||||
|
||||
val ws = Ws.connect("wss://example.com/socket")
|
||||
ws.sendText("hello")
|
||||
println(ws.receive())
|
||||
ws.close()
|
||||
```
|
||||
|
||||
These modules are capability-gated and host-installed, keeping Lyng safe by default while making networked scripts practical when enabled.
|
||||
|
||||
See [lyngio overview](lyngio.md), [lyng.io.db](lyng.io.db.md), [lyng.io.http](lyng.io.http.md), [lyng.io.ws](lyng.io.ws.md), and [lyng.io.net](lyng.io.net.md).
|
||||
|
||||
### CLI: Formatting Command
|
||||
A new `fmt` subcommand has been added to the Lyng CLI.
|
||||
|
||||
@ -594,15 +303,6 @@ lyng fmt --in-place MyFile.lyng # Format file in-place
|
||||
lyng fmt --check MyFile.lyng # Check if file needs formatting
|
||||
```
|
||||
|
||||
### CLI: Better Terminal Workflows
|
||||
The CLI is no longer just a script launcher. In the 1.5 line it also gained:
|
||||
|
||||
- built-in formatter support
|
||||
- integrated `lyng.io.console` support for terminal programs
|
||||
- downloadable packaged distributions for easier local use
|
||||
|
||||
This makes CLI-first scripting and console applications much more practical than in earlier releases.
|
||||
|
||||
### IDEA Plugin: Autocompletion
|
||||
Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion.
|
||||
|
||||
|
||||
@ -1,325 +0,0 @@
|
||||
#!/usr/bin/env lyng
|
||||
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.io.fs
|
||||
|
||||
val DB_FILE_NAME = "contents.db"
|
||||
val ANSI_ESC = "\u001b["
|
||||
val NEWLINE = "\n"
|
||||
val WINDOWS_SEPARATOR = "\\"
|
||||
val SQLITE_JOURNAL_SUFFIXES = ["-wal", "-shm", "-journal"]
|
||||
|
||||
val USAGE_TEXT = "
|
||||
Lyng content index
|
||||
Scan a directory tree, diff it against a SQLite snapshot, and optionally refresh the snapshot.
|
||||
|
||||
usage:
|
||||
lyng examples/content_index_db.lyng <root> [-u|--update]
|
||||
|
||||
options:
|
||||
-u, --update write the current scan back to $DB_FILE_NAME
|
||||
|
||||
notes:
|
||||
- the database lives inside <root>/$DB_FILE_NAME
|
||||
- on first run the snapshot is created automatically
|
||||
- the script ignores its own SQLite sidecar files
|
||||
"
|
||||
|
||||
val CREATE_FILE_INDEX_SQL = "
|
||||
create table if not exists file_index(
|
||||
path text primary key not null,
|
||||
size integer not null,
|
||||
mtime integer not null
|
||||
)
|
||||
"
|
||||
|
||||
val CREATE_CURRENT_SCAN_SQL = "
|
||||
create temp table current_scan(
|
||||
path text primary key not null,
|
||||
size integer not null,
|
||||
mtime integer not null
|
||||
)
|
||||
"
|
||||
|
||||
val SELECT_ADDED_SQL = "
|
||||
select
|
||||
c.path,
|
||||
c.size,
|
||||
c.mtime
|
||||
from current_scan c
|
||||
left join file_index f on f.path = c.path
|
||||
where f.path is null
|
||||
order by c.path
|
||||
"
|
||||
|
||||
val SELECT_REMOVED_SQL = "
|
||||
select f.path, f.size, f.mtime
|
||||
from file_index f
|
||||
left join current_scan c on c.path = f.path
|
||||
where c.path is null
|
||||
order by f.path
|
||||
"
|
||||
|
||||
val SELECT_CHANGED_SQL = "
|
||||
select c.path, f.size as old_size, c.size as new_size, f.mtime as old_mtime,
|
||||
c.mtime as new_mtime
|
||||
from current_scan c
|
||||
join file_index f on f.path = c.path
|
||||
where c.size != f.size or c.mtime != f.mtime
|
||||
order by c.path
|
||||
"
|
||||
|
||||
val DELETE_MISSING_SQL = "
|
||||
delete from file_index
|
||||
where not exists (
|
||||
select 1
|
||||
from current_scan c
|
||||
where c.path = file_index.path
|
||||
)
|
||||
"
|
||||
|
||||
val UPSERT_SCAN_SQL = "
|
||||
insert or replace into file_index(path, size, mtime)
|
||||
select path, size, mtime
|
||||
from current_scan
|
||||
"
|
||||
|
||||
val INSERT_SCAN_ROW_SQL = "
|
||||
insert into current_scan(path, size, mtime)
|
||||
values(?, ?, ?)
|
||||
"
|
||||
|
||||
val USE_COLOR = true
|
||||
|
||||
class CliOptions(val rootText: String, val updateSnapshot: Bool) {}
|
||||
|
||||
fun out(text: String? = null): Void {
|
||||
if (text == null) {
|
||||
print(NEWLINE)
|
||||
return
|
||||
}
|
||||
print(text + NEWLINE)
|
||||
}
|
||||
|
||||
fun paint(code: String, text: String): String {
|
||||
if (!USE_COLOR) return text
|
||||
ANSI_ESC + code + "m" + text + ANSI_ESC + "0m"
|
||||
}
|
||||
|
||||
fun bold(text: String): String = paint("1", text)
|
||||
fun dim(text: String): String = paint("2", text)
|
||||
fun cyan(text: String): String = paint("36", text)
|
||||
fun green(text: String): String = paint("32", text)
|
||||
fun yellow(text: String): String = paint("33", text)
|
||||
fun red(text: String): String = paint("31", text)
|
||||
|
||||
fun signed(value: Int): String = if (value > 0) "+" + value else value.toString()
|
||||
|
||||
fun plural(count: Int, one: String, many: String): String {
|
||||
if (count == 1) return one
|
||||
many
|
||||
}
|
||||
|
||||
fun childPath(parent: Path, name: String): Path {
|
||||
val base = parent.toString()
|
||||
if (base.endsWith("/") || base.endsWith(WINDOWS_SEPARATOR)) {
|
||||
return Path(base + name)
|
||||
}
|
||||
Path(base + "/" + name)
|
||||
}
|
||||
|
||||
fun relativePath(root: Path, file: Path): String {
|
||||
val parts: List<String> = []
|
||||
for (i in root.segments.size..<file.segments.size) {
|
||||
parts.add(file.segments[i] as String)
|
||||
}
|
||||
parts.joinToString("/")
|
||||
}
|
||||
|
||||
fun isDatabaseArtifact(relative: String): Bool {
|
||||
relative == DB_FILE_NAME || SQLITE_JOURNAL_SUFFIXES.any { relative == DB_FILE_NAME + (it as String) }
|
||||
}
|
||||
|
||||
fun printUsage(message: String? = null): Void {
|
||||
if (message != null && message.trim().isNotEmpty()) {
|
||||
out(red("error: ") + message)
|
||||
out()
|
||||
}
|
||||
|
||||
out(bold(USAGE_TEXT))
|
||||
}
|
||||
|
||||
fun parseArgs(argv: List<String>): CliOptions? {
|
||||
var rootText: String? = null
|
||||
var updateSnapshot = false
|
||||
|
||||
for (arg in argv) {
|
||||
when (arg) {
|
||||
"-u", "--update" -> updateSnapshot = true
|
||||
"-h", "--help" -> {
|
||||
printUsage()
|
||||
return null
|
||||
}
|
||||
else -> {
|
||||
if (arg.startsWith("-")) {
|
||||
printUsage("unknown option: " + arg)
|
||||
return null
|
||||
}
|
||||
if (rootText != null) {
|
||||
printUsage("only one root path is allowed")
|
||||
return null
|
||||
}
|
||||
rootText = arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rootText == null) {
|
||||
printUsage("missing required <root> argument")
|
||||
return null
|
||||
}
|
||||
|
||||
CliOptions(rootText as String, updateSnapshot)
|
||||
}
|
||||
|
||||
fun printBanner(root: Path, dbFile: Path, dbWasCreated: Bool, updateSnapshot: Bool): Void {
|
||||
val mode =
|
||||
if (dbWasCreated) "bootstrap snapshot"
|
||||
else if (updateSnapshot) "scan + refresh snapshot"
|
||||
else "scan only"
|
||||
|
||||
out(cyan("== Lyng content index =="))
|
||||
out(dim("root: " + root))
|
||||
out(dim("db: " + dbFile))
|
||||
out(dim("mode: " + mode))
|
||||
out()
|
||||
}
|
||||
|
||||
fun printSection(title: String, accent: (String)->String, rows: List<SqlRow>, render: (SqlRow)->String): Void {
|
||||
out(accent(title + " (" + rows.size + ")"))
|
||||
if (rows.isEmpty()) {
|
||||
out(dim(" none"))
|
||||
out()
|
||||
return
|
||||
}
|
||||
|
||||
for (row in rows) {
|
||||
out(render(row))
|
||||
}
|
||||
out()
|
||||
}
|
||||
|
||||
fun renderAdded(row: SqlRow): String {
|
||||
val path = row["path"] as String
|
||||
val size = row["size"] as Int
|
||||
val mtime = row["mtime"] as Int
|
||||
" " + green("+") + " " + bold(path) + dim(" %12d B mtime %d"(size, mtime))
|
||||
}
|
||||
|
||||
fun renderRemoved(row: SqlRow): String {
|
||||
val path = row["path"] as String
|
||||
val size = row["size"] as Int
|
||||
val mtime = row["mtime"] as Int
|
||||
" " + red("-") + " " + bold(path) + dim(" %12d B mtime %d"(size, mtime))
|
||||
}
|
||||
|
||||
fun renderChanged(row: SqlRow): String {
|
||||
val path = row["path"] as String
|
||||
val oldSize = row["old_size"] as Int
|
||||
val newSize = row["new_size"] as Int
|
||||
val oldMtime = row["old_mtime"] as Int
|
||||
val newMtime = row["new_mtime"] as Int
|
||||
val sizeDelta = newSize - oldSize
|
||||
val mtimeDelta = newMtime - oldMtime
|
||||
|
||||
" " + yellow("~") + " " + bold(path) +
|
||||
dim(
|
||||
" size %d -> %d (%s B), mtime %d -> %d (%s ms)"(
|
||||
oldSize,
|
||||
newSize,
|
||||
signed(sizeDelta),
|
||||
oldMtime,
|
||||
newMtime,
|
||||
signed(mtimeDelta)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadRows(tx: SqlTransaction, query: String): List<SqlRow> = tx.select(query).toList()
|
||||
|
||||
fun main() {
|
||||
val argv: List<String> = []
|
||||
for (raw in ARGV as List) {
|
||||
argv.add(raw as String)
|
||||
}
|
||||
val options = parseArgs(argv)
|
||||
if (options == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val root = Path(options.rootText)
|
||||
if (!root.exists()) {
|
||||
printUsage("root does not exist: " + root)
|
||||
return
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
printUsage("root is not a directory: " + root)
|
||||
return
|
||||
}
|
||||
|
||||
val dbFile = childPath(root, DB_FILE_NAME)
|
||||
val dbWasCreated = !dbFile.exists()
|
||||
val shouldUpdateSnapshot = dbWasCreated || options.updateSnapshot
|
||||
|
||||
printBanner(root, dbFile, dbWasCreated, shouldUpdateSnapshot)
|
||||
|
||||
val db = openSqlite(dbFile.toString())
|
||||
|
||||
db.transaction { tx ->
|
||||
tx.execute(CREATE_FILE_INDEX_SQL)
|
||||
|
||||
tx.execute("drop table if exists temp.current_scan")
|
||||
tx.execute(CREATE_CURRENT_SCAN_SQL)
|
||||
|
||||
var scannedFiles = 0
|
||||
for (rawEntry in root.glob("**")) {
|
||||
val entry = rawEntry as Path
|
||||
if (!entry.isFile()) continue
|
||||
|
||||
val relative = relativePath(root, entry)
|
||||
if (isDatabaseArtifact(relative)) continue
|
||||
|
||||
val size = entry.size() ?: 0
|
||||
val mtime = entry.modifiedAtMillis() ?: 0
|
||||
tx.execute(INSERT_SCAN_ROW_SQL, relative, size, mtime)
|
||||
scannedFiles++
|
||||
}
|
||||
|
||||
val added = loadRows(tx, SELECT_ADDED_SQL)
|
||||
val removed = loadRows(tx, SELECT_REMOVED_SQL)
|
||||
val changed = loadRows(tx, SELECT_CHANGED_SQL)
|
||||
|
||||
val totalChanges = added.size + removed.size + changed.size
|
||||
|
||||
out(dim("scanned %d %s under %s"(scannedFiles, plural(scannedFiles, "file", "files"), root.toString())))
|
||||
out(dim("detected %d %s"(totalChanges, plural(totalChanges, "change", "changes"))))
|
||||
out()
|
||||
|
||||
printSection("Added", { green(it) }, added) { renderAdded(it) }
|
||||
printSection("Removed", { red(it) }, removed) { renderRemoved(it) }
|
||||
printSection("Changed", { yellow(it) }, changed) { renderChanged(it) }
|
||||
|
||||
if (shouldUpdateSnapshot) {
|
||||
tx.execute(DELETE_MISSING_SQL)
|
||||
tx.execute(UPSERT_SCAN_SQL)
|
||||
|
||||
val action = if (dbWasCreated) "created" else "updated"
|
||||
out(cyan("snapshot " + action + " in " + dbFile.name))
|
||||
} else {
|
||||
out(dim("snapshot unchanged; re-run with -u or --update to persist the scan"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@ -1,23 +0,0 @@
|
||||
#!/env/bin lyng
|
||||
|
||||
import lyng.io.http
|
||||
|
||||
// Step 1: download the main lynglang.com page.
|
||||
val home = Http.get("https://lynglang.com").text()
|
||||
|
||||
// Step 2: find the version-script reference in the page HTML.
|
||||
val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home)
|
||||
require(jsRef != null, "lyng-version.js reference not found on the homepage")
|
||||
|
||||
// Step 3: extract the referenced script path from the first regex capture.
|
||||
val versionJsPath = jsRef[1]
|
||||
|
||||
// Step 4: download the script that exposes `window.LYNG_VERSION`.
|
||||
val versionJs = Http.get("https://lynglang.com/" + versionJsPath).text()
|
||||
|
||||
// Step 5: pull the actual version string from the JavaScript source.
|
||||
val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs)
|
||||
require(versionMatch != null, "LYNG_VERSION assignment not found")
|
||||
|
||||
// Step 6: print the discovered version for the user.
|
||||
println("Lynglang.com version: " + ((versionMatch as RegexMatch)[1]))
|
||||
@ -1,15 +0,0 @@
|
||||
import lyng.time
|
||||
|
||||
val n = 700_000
|
||||
|
||||
fun tm<T>(block: ()->T): T {
|
||||
val t = Instant()
|
||||
block().also {
|
||||
println("tm: ${Instant() - t}")
|
||||
}
|
||||
}
|
||||
|
||||
val x = tm { List.fill(n) { it * 10 + 1 } }
|
||||
val y = tm { List.fill(n, n + 10) { it * 10 + 1 } }
|
||||
tm { x.add(-1) }
|
||||
tm { y.add(-2) }
|
||||
@ -1,76 +0,0 @@
|
||||
fun calculateDepth(
|
||||
T: Real,
|
||||
m: Real,
|
||||
d: Real,
|
||||
rho: Real = 1.2,
|
||||
c: Real = 340.0,
|
||||
g: Real = 9.81,
|
||||
Cd: Real = 0.5,
|
||||
eps: Real = 1e-3,
|
||||
maxIter: Int = 100
|
||||
): Real? {
|
||||
// Площадь миделя
|
||||
val r = d / 2.0
|
||||
val A = π * r * r
|
||||
|
||||
// Коэффициент сопротивления
|
||||
val k = 0.5 * Cd * rho * A
|
||||
|
||||
// Предельная скорость
|
||||
val vTerm = sqrt(m * g / k)
|
||||
|
||||
// Функция времени падения с высоты h
|
||||
fun tFall(h: Real): Real {
|
||||
// Для численной стабильности при больших h используем логарифмическую форму
|
||||
val arg = exp(g * h / (vTerm * vTerm))
|
||||
// arcosh(x) = ln(x + sqrt(x^2 - 1))
|
||||
val arcosh = ln(arg + sqrt(arg * arg - 1.0))
|
||||
return vTerm / g * arcosh
|
||||
}
|
||||
|
||||
// Полное расчётное время
|
||||
fun Tcalc(h: Real): Real = tFall(h) + h / c
|
||||
|
||||
// Находим интервал, содержащий корень
|
||||
// Нижняя граница: глубина не может быть отрицательной
|
||||
var lo = 0.0
|
||||
// Верхняя граница: сначала попробуем оценку по свободному падению (без звука)
|
||||
var hi = 0.5 * g * T * T // максимальная глубина, если бы не было сопротивления и звука
|
||||
// Уточним hi, чтобы Tcalc(hi) было заведомо больше T
|
||||
while (Tcalc(hi) < T && hi < 1e4) {
|
||||
hi *= 2.0
|
||||
}
|
||||
// Проверка, что hi достаточно велико
|
||||
if (Tcalc(hi) < T) return null // слишком большая глубина, не укладываемся в разумное
|
||||
|
||||
// Бисекция
|
||||
var iter = 0
|
||||
var h = (lo + hi) / 2.0
|
||||
while (iter < maxIter && (hi - lo) > eps) {
|
||||
val f = Tcalc(h) - T
|
||||
if (abs(f) < eps) break
|
||||
if (f > 0) {
|
||||
hi = h
|
||||
} else {
|
||||
lo = h
|
||||
}
|
||||
h = (lo + hi) / 2.0
|
||||
iter++
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Пример: T=12 секунд
|
||||
val T = 26.0
|
||||
val m = 1.0 // кг
|
||||
val d = 0.1 // м
|
||||
|
||||
val depth = calculateDepth(T, m, d)
|
||||
if (depth != null) {
|
||||
println("Глубина: %.2f м"(depth))
|
||||
// Для проверки выведем теоретическое время при найденной глубине
|
||||
// (можно добавить функцию для самопроверки)
|
||||
} else {
|
||||
println("Расчёт не сошёлся")
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import lyng.io.db
|
||||
import lyng.io.db.jdbc
|
||||
|
||||
println("H2 JDBC demo: typed open, generic open, generated keys")
|
||||
|
||||
val db = openH2("mem:lyng_h2_demo;DB_CLOSE_DELAY=-1")
|
||||
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table if not exists person(id bigint auto_increment primary key, name varchar(120) not null, active boolean not null)")
|
||||
tx.execute("delete from person")
|
||||
|
||||
val firstInsert = tx.execute(
|
||||
"insert into person(name, active) values(?, ?)",
|
||||
"Ada",
|
||||
true
|
||||
)
|
||||
val firstId = firstInsert.getGeneratedKeys().toList()[0][0]
|
||||
assertEquals(1, firstId)
|
||||
|
||||
tx.execute(
|
||||
"insert into person(name, active) values(?, ?)",
|
||||
"Linus",
|
||||
false
|
||||
)
|
||||
|
||||
val rows = tx.select("select id, name, active from person order by id").toList()
|
||||
assertEquals(2, rows.size)
|
||||
println("#" + rows[0]["id"] + " " + rows[0]["name"] + " active=" + rows[0]["active"])
|
||||
println("#" + rows[1]["id"] + " " + rows[1]["name"] + " active=" + rows[1]["active"])
|
||||
}
|
||||
|
||||
val genericDb = openDatabase(
|
||||
"jdbc:h2:mem:lyng_h2_generic;DB_CLOSE_DELAY=-1",
|
||||
Map()
|
||||
)
|
||||
|
||||
val answer = genericDb.transaction { tx ->
|
||||
tx.select("select 42 as answer").toList()[0]["answer"]
|
||||
}
|
||||
|
||||
assertEquals(42, answer)
|
||||
println("Generic JDBC openDatabase(...) also works: answer=$answer")
|
||||
println("OK")
|
||||
@ -1,19 +0,0 @@
|
||||
import lyng.io.http.server
|
||||
|
||||
closed class CreateUserRequest(name: String, age: Int)
|
||||
closed class CreateUserResponse(id: Int, name: String, age: Int)
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.postPath("/api/users") {
|
||||
val req = jsonBody<CreateUserRequest>()
|
||||
|
||||
if (req.name.isBlank()) {
|
||||
respondJson({ error: "name must not be empty" }, 400)
|
||||
return
|
||||
}
|
||||
|
||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
@ -1,67 +0,0 @@
|
||||
import lyng.time
|
||||
|
||||
val WORK_SIZE = 500
|
||||
val THREADS = 1
|
||||
|
||||
fn piSpigot(iThread: Int, n: Int) {
|
||||
var piIter = 0
|
||||
var pi = List.fill(n) { 0 }
|
||||
val boxes = n * 10 / 3
|
||||
var reminders = List.fill(boxes) { 2 }
|
||||
var heldDigits = 0
|
||||
for (i in 0..<n) {
|
||||
var carriedOver = 0
|
||||
var sum = 0
|
||||
for (j in (boxes - 1) downTo 0) {
|
||||
val denom = j * 2 + 1
|
||||
reminders[j] *= 10
|
||||
sum = reminders[j] + carriedOver
|
||||
val quotient = sum / denom
|
||||
reminders[j] = sum % denom
|
||||
carriedOver = quotient * j
|
||||
}
|
||||
reminders[0] = sum % 10
|
||||
var q = sum / 10
|
||||
if (q == 9) {
|
||||
++heldDigits
|
||||
} else if (q == 10) {
|
||||
q = 0
|
||||
for (k in 1..heldDigits) {
|
||||
var replaced = pi[i - k]
|
||||
if (replaced == 9) {
|
||||
replaced = 0
|
||||
} else {
|
||||
++replaced
|
||||
}
|
||||
pi[i - k] = replaced
|
||||
}
|
||||
heldDigits = 1
|
||||
} else {
|
||||
heldDigits = 1
|
||||
}
|
||||
pi[piIter] = q
|
||||
++piIter
|
||||
}
|
||||
|
||||
var res = ""
|
||||
for (i in (n - 8)..<n) {
|
||||
res += pi[i]
|
||||
}
|
||||
|
||||
println(iThread.toString() + ": " + res)
|
||||
res
|
||||
}
|
||||
|
||||
for( r in 0..100 ) {
|
||||
val t0 = Instant()
|
||||
|
||||
println("piBench (lyng): THREADS = " + THREADS + ", WORK_SIZE = " + WORK_SIZE)
|
||||
for (i in 0..<THREADS) {
|
||||
piSpigot(i, WORK_SIZE)
|
||||
}
|
||||
|
||||
val dt = Instant() - t0
|
||||
|
||||
println("all done, dt = ", dt)
|
||||
delay(800)
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
# Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
def piSpigot(iThread, nx):
|
||||
piIter = 0
|
||||
pi = [None] * nx
|
||||
boxes = nx * 10 // 3
|
||||
reminders = [None]*boxes
|
||||
i = 0
|
||||
while i < boxes:
|
||||
reminders[i] = 2
|
||||
i += 1
|
||||
heldDigits = 0
|
||||
i = 0
|
||||
while i < nx:
|
||||
carriedOver = 0
|
||||
sum = 0
|
||||
j = boxes - 1
|
||||
while j >= 0:
|
||||
reminders[j] *= 10
|
||||
sum = reminders[j] + carriedOver
|
||||
quotient = sum // (j * 2 + 1)
|
||||
reminders[j] = sum % (j * 2 + 1)
|
||||
carriedOver = quotient * j
|
||||
j -= 1
|
||||
reminders[0] = sum % 10
|
||||
q = sum // 10
|
||||
if q == 9:
|
||||
heldDigits += 1
|
||||
elif q == 10:
|
||||
q = 0
|
||||
k = 1
|
||||
while k <= heldDigits:
|
||||
replaced = pi[i - k]
|
||||
if replaced == 9:
|
||||
replaced = 0
|
||||
else:
|
||||
replaced += 1
|
||||
pi[i - k] = replaced
|
||||
k += 1
|
||||
heldDigits = 1
|
||||
else:
|
||||
heldDigits = 1
|
||||
pi[piIter] = q
|
||||
piIter += 1
|
||||
i += 1
|
||||
res = ""
|
||||
for i in range(len(pi)-8, len(pi), 1):
|
||||
res += str(pi[i])
|
||||
print(str(iThread) + ": " + res)
|
||||
|
||||
def createProcesses():
|
||||
THREADS = 1
|
||||
WORK_SIZE = 500
|
||||
print("piBench (python3): THREADS = " + str(THREADS) + ", WORK_SIZE = " + str(WORK_SIZE))
|
||||
pa = []
|
||||
for i in range(THREADS):
|
||||
p = Process(target=piSpigot, args=(i, WORK_SIZE))
|
||||
p.start()
|
||||
pa.append(p)
|
||||
for p in pa:
|
||||
p.join()
|
||||
|
||||
if __name__ == "__main__":
|
||||
t1 = time.time()
|
||||
createProcesses()
|
||||
dt = time.time() - t1
|
||||
print("total time: %i ms" % (dt*1000))
|
||||
@ -1,71 +0,0 @@
|
||||
import lyng.io.db.jdbc
|
||||
|
||||
/*
|
||||
PostgreSQL JDBC demo.
|
||||
|
||||
Usage:
|
||||
lyng examples/postgres_basic.lyng [jdbc-url] [user] [password]
|
||||
|
||||
Typical local URL:
|
||||
jdbc:postgresql://127.0.0.1/postgres
|
||||
*/
|
||||
|
||||
fun cliArgs(): List<String> {
|
||||
val result: List<String> = []
|
||||
for (raw in ARGV as List) {
|
||||
result.add(raw as String)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val argv = cliArgs()
|
||||
val URL = if (argv.size > 0) argv[0] else "jdbc:postgresql://127.0.0.1/postgres"
|
||||
val USER = if (argv.size > 1) argv[1] else ""
|
||||
val PASSWORD = if (argv.size > 2) argv[2] else ""
|
||||
|
||||
println("PostgreSQL JDBC demo: typed open, generated keys, nested transaction")
|
||||
|
||||
val db = openPostgres(URL, USER, PASSWORD)
|
||||
|
||||
db.transaction { tx ->
|
||||
tx.execute("create table if not exists lyng_pg_demo(id bigserial primary key, title text not null, done boolean not null)")
|
||||
tx.execute("delete from lyng_pg_demo")
|
||||
|
||||
val firstInsert = tx.execute(
|
||||
"insert into lyng_pg_demo(title, done) values(?, ?)",
|
||||
"Verify PostgreSQL JDBC support",
|
||||
false
|
||||
)
|
||||
val firstId = firstInsert.getGeneratedKeys().toList()[0][0]
|
||||
println("First generated id=" + firstId)
|
||||
|
||||
tx.execute(
|
||||
"insert into lyng_pg_demo(title, done) values(?, ?)",
|
||||
"Review documentation",
|
||||
true
|
||||
)
|
||||
|
||||
try {
|
||||
tx.transaction { inner ->
|
||||
inner.execute(
|
||||
"insert into lyng_pg_demo(title, done) values(?, ?)",
|
||||
"This row is rolled back",
|
||||
false
|
||||
)
|
||||
throw IllegalStateException("rollback nested")
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
println("Nested transaction rolled back as expected")
|
||||
}
|
||||
|
||||
val rows = tx.select("select id, title, done from lyng_pg_demo order by id").toList()
|
||||
for (row in rows) {
|
||||
println("#" + row["id"] + " " + row["title"] + " done=" + row["done"])
|
||||
}
|
||||
|
||||
val count = tx.select("select count(*) as count from lyng_pg_demo").toList()[0]["count"]
|
||||
assertEquals(2, count)
|
||||
println("Visible rows after nested rollback: " + count)
|
||||
}
|
||||
|
||||
println("OK")
|
||||
@ -1,89 +0,0 @@
|
||||
import lyng.io.db
|
||||
import lyng.io.db.sqlite
|
||||
import lyng.time
|
||||
|
||||
println("SQLite demo: typed open, generic open, result sets, generated keys, nested rollback")
|
||||
|
||||
// The typed helper is the simplest entry point when you know you want SQLite.
|
||||
val db = openSqlite(":memory:")
|
||||
|
||||
db.transaction { tx ->
|
||||
// Keep schema creation and data changes inside one transaction block.
|
||||
tx.execute("create table task(id integer primary key autoincrement, title text not null, done integer not null, due_date date not null)")
|
||||
|
||||
// execute(...) is for side-effect statements. Generated keys are read from
|
||||
// ExecutionResult rather than from a synthetic row-returning INSERT.
|
||||
val firstInsert = tx.execute(
|
||||
"insert into task(title, done, due_date) values(?, ?, ?)",
|
||||
"Write a SQLite example",
|
||||
false,
|
||||
Date(2026, 4, 15)
|
||||
)
|
||||
val firstGeneratedKeys = firstInsert.getGeneratedKeys()
|
||||
val firstId = firstGeneratedKeys.toList()[0][0]
|
||||
assertEquals(1, firstId)
|
||||
|
||||
tx.execute(
|
||||
"insert into task(title, done, due_date) values(?, ?, ?)",
|
||||
"Review the DB API",
|
||||
true,
|
||||
Date(2026, 4, 16)
|
||||
)
|
||||
|
||||
// Nested transactions are real savepoints. If the inner block fails,
|
||||
// only the nested work is rolled back.
|
||||
try {
|
||||
tx.transaction { inner ->
|
||||
inner.execute(
|
||||
"insert into task(title, done, due_date) values(?, ?, ?)",
|
||||
"This row is rolled back",
|
||||
false,
|
||||
Date(2026, 4, 17)
|
||||
)
|
||||
throw IllegalStateException("demonstrate nested rollback")
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
println("Nested transaction rolled back as expected")
|
||||
}
|
||||
|
||||
// select(...) is for row-producing statements. ResultSet exposes metadata,
|
||||
// cheap emptiness checks, iteration, and conversion to a plain list.
|
||||
val tasks = tx.select("select id, title, done, due_date from task order by id")
|
||||
assertEquals(false, tasks.isEmpty())
|
||||
assertEquals(2, tasks.size())
|
||||
|
||||
println("Columns:")
|
||||
for (column in tasks.columns) {
|
||||
println(" " + column.name + " -> " + column.sqlType + " (native " + column.nativeType + ")")
|
||||
}
|
||||
|
||||
val taskRows = tasks.toList()
|
||||
|
||||
println("Rows:")
|
||||
for (row in taskRows) {
|
||||
// Name lookups are case-insensitive and values are already converted.
|
||||
println(" #" + row["ID"] + " " + row["title"] + " done=" + row["done"] + " due=" + row["due_date"])
|
||||
}
|
||||
|
||||
// toList() materializes detached rows that stay usable after transaction close.
|
||||
val snapshot = tx.select("select title, due_date from task order by id").toList()
|
||||
assertEquals("Write a SQLite example", snapshot[0]["title"])
|
||||
assertEquals(Date(2026, 4, 16), snapshot[1]["due_date"])
|
||||
|
||||
val count = tx.select("select count(*) as count from task").toList()[0]["count"]
|
||||
assertEquals(2, count)
|
||||
println("Visible rows after nested rollback: $count")
|
||||
}
|
||||
|
||||
// The generic entry point stays useful for config-driven code.
|
||||
val genericDb = openDatabase(
|
||||
"sqlite::memory:",{ foreignKeys: true, busyTimeoutMillis: 1000 }
|
||||
)
|
||||
|
||||
val answer = genericDb.transaction { tx ->
|
||||
tx.select("select 42 as answer").toList()[0]["answer"]
|
||||
}
|
||||
|
||||
assertEquals(42, answer)
|
||||
println("Generic openDatabase(...) also works: answer=$answer")
|
||||
println("OK")
|
||||
@ -1,56 +0,0 @@
|
||||
import lyng.io.db.sqlite
|
||||
|
||||
println("SQLite serialization demo: write-side projection and decodeAs<T>()")
|
||||
|
||||
class Payload(name: String, count: Int)
|
||||
|
||||
class Item(
|
||||
id: Int,
|
||||
title: String,
|
||||
@DbJson meta: Payload,
|
||||
@DbLynon state: Payload
|
||||
) {
|
||||
var note: String = ""
|
||||
@DbExcept var cache: String = ""
|
||||
}
|
||||
|
||||
val restored = openSqlite(":memory:").transaction { tx ->
|
||||
tx.execute(
|
||||
"create table item(id integer not null, title text not null, meta text not null, state blob not null, note text not null)"
|
||||
)
|
||||
|
||||
val item = Item(1, "first", Payload("json", 10), Payload("bin", 20))
|
||||
item.note = "created"
|
||||
item.cache = "not stored"
|
||||
|
||||
tx.execute("insert into item(@cols(?1)) values(@vals(?1))", item)
|
||||
|
||||
item.title = "second"
|
||||
item.meta = Payload("json2", 11)
|
||||
item.state = Payload("bin2", 21)
|
||||
item.note = "updated"
|
||||
|
||||
tx.execute(
|
||||
"update item set @set(?1 except: \"id\") where id = ?2",
|
||||
item,
|
||||
item.id
|
||||
)
|
||||
|
||||
val restored = tx.select("select * from item where id = ?", 1).decodeAs<Item>().first
|
||||
|
||||
assertEquals("second", restored.title)
|
||||
assertEquals("json2", restored.meta.name)
|
||||
assertEquals(11, restored.meta.count)
|
||||
assertEquals("bin2", restored.state.name)
|
||||
assertEquals(21, restored.state.count)
|
||||
assertEquals("updated", restored.note)
|
||||
restored
|
||||
}
|
||||
|
||||
println("Restored item:")
|
||||
println(" id=" + restored.id)
|
||||
println(" title=" + restored.title)
|
||||
println(" meta=" + restored.meta.name + "/" + restored.meta.count)
|
||||
println(" state=" + restored.state.name + "/" + restored.state.count)
|
||||
println(" note=" + restored.note)
|
||||
println("OK")
|
||||
@ -1,68 +0,0 @@
|
||||
import lyng.buffer
|
||||
import lyng.io.net
|
||||
|
||||
val host = "127.0.0.1"
|
||||
val port = 8092
|
||||
val N = 5
|
||||
val server = Net.tcpListen(port, host)
|
||||
println("start tcp server at $host:$port")
|
||||
|
||||
fun serveClient(client: TcpSocket) = launch {
|
||||
try {
|
||||
while (true) {
|
||||
val data = client.read()
|
||||
if (data == null) break
|
||||
var line = (data as Buffer).decodeUtf8()
|
||||
line = "[" + client.remoteAddress() + "]> " + line
|
||||
println(line)
|
||||
}
|
||||
} catch (e) {
|
||||
println("ERROR [reader]: " + e)
|
||||
}
|
||||
}
|
||||
|
||||
fun serveRequests(server: TcpServer) = launch {
|
||||
val readers = []
|
||||
try {
|
||||
for (i in 0..<5) {
|
||||
val client = server.accept()
|
||||
println("accept new connection: " + client.remoteAddress())
|
||||
readers.add(serveClient(client as TcpSocket))
|
||||
}
|
||||
} catch (e) {
|
||||
println("ERROR [listener]: " + e)
|
||||
} finally {
|
||||
server.close()
|
||||
}
|
||||
for (i in 0..<readers.size) {
|
||||
val reader = readers[i]
|
||||
(reader as Deferred).await()
|
||||
}
|
||||
}
|
||||
|
||||
val srv = serveRequests(server as TcpServer)
|
||||
|
||||
var clients = []
|
||||
for (i in 0..<N) {
|
||||
//delay(500)
|
||||
clients.add(launch {
|
||||
try{
|
||||
val socket = Net.tcpConnect(host, port)
|
||||
socket.writeUtf8("ping1ping2ping3ping4ping5")
|
||||
socket.flush()
|
||||
socket.close()
|
||||
} catch (e) {
|
||||
println("ERROR [client]: " + e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (i in 0..<clients.size) {
|
||||
val c = clients[i]
|
||||
(c as Deferred).await()
|
||||
println("client done")
|
||||
}
|
||||
|
||||
srv.await()
|
||||
delay(10000)
|
||||
println("FIN")
|
||||
@ -1,60 +0,0 @@
|
||||
import lyng.io.net
|
||||
|
||||
val host = "127.0.0.1"
|
||||
val clientCount = 1000
|
||||
val clientWindow = 128
|
||||
val server = Net.tcpListen(0, host, clientWindow, true)
|
||||
val port = server.localAddress().port
|
||||
|
||||
fun payloadFor(index: Int) = "$index:${Random.nextInt()}:${Random.nextInt()}"
|
||||
|
||||
val serverJob = launch {
|
||||
try {
|
||||
while (true) {
|
||||
val client = server.accept()
|
||||
launch {
|
||||
try {
|
||||
client.readLine()?.let { source ->
|
||||
client.writeUtf8("pong: $source\n")
|
||||
client.flush()
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (server.isOpen()) {
|
||||
throw e
|
||||
}
|
||||
} finally {
|
||||
if (server.isOpen()) {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var completed = 0
|
||||
for (batchStart in 0..<clientCount step clientWindow) {
|
||||
val batchEnd = if (batchStart + clientWindow < clientCount) batchStart + clientWindow else clientCount
|
||||
val replies = (batchStart..<batchEnd).map { index ->
|
||||
val payload = payloadFor(index)
|
||||
launch {
|
||||
val socket = Net.tcpConnect(host, port) as TcpSocket
|
||||
try {
|
||||
socket.writeUtf8(payload + "\n")
|
||||
socket.flush()
|
||||
val reply = socket.readLine()
|
||||
assertEquals("pong: $payload", reply)
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
}.joinAll()
|
||||
completed += replies.size
|
||||
}
|
||||
|
||||
assertEquals(clientCount, completed)
|
||||
server.close()
|
||||
serverJob.await()
|
||||
println("OK: $clientCount concurrent tcp clients")
|
||||
@ -49,7 +49,6 @@ val UNICODE_BOTTOM_RIGHT = "┘"
|
||||
val UNICODE_HORIZONTAL = "──"
|
||||
val UNICODE_VERTICAL = "│"
|
||||
val UNICODE_DOT = "· "
|
||||
val PIECES: List<Piece> = []
|
||||
|
||||
type Cell = List<Int>
|
||||
type Rotation = List<Cell>
|
||||
@ -80,30 +79,6 @@ class GameState(
|
||||
var paused = false
|
||||
}
|
||||
class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {}
|
||||
class InputBuffer {
|
||||
private val mutex: Mutex = Mutex()
|
||||
private val items: List<String> = []
|
||||
|
||||
fun push(value: String): Void {
|
||||
mutex.withLock {
|
||||
if (items.size >= MAX_PENDING_INPUTS) {
|
||||
items.removeAt(0)
|
||||
}
|
||||
items.add(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun drain(): List<String> {
|
||||
val out: List<String> = []
|
||||
mutex.withLock {
|
||||
while (items.size > 0) {
|
||||
out.add(items[0])
|
||||
items.removeAt(0)
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAndHome() {
|
||||
Console.clear()
|
||||
@ -493,6 +468,8 @@ fun rot(a: Cell, b: Cell, c: Cell, d: Cell): Rotation {
|
||||
r
|
||||
}
|
||||
|
||||
val PIECES: List<Piece> = []
|
||||
|
||||
val iRots: Rotations = []
|
||||
iRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(3,1)))
|
||||
iRots.add(rot(cell(2,0), cell(2,1), cell(2,2), cell(2,3)))
|
||||
@ -564,8 +541,9 @@ if (!Console.isSupported()) {
|
||||
)
|
||||
var prevFrameLines: List<String> = []
|
||||
|
||||
val gameMutex: Mutex = Mutex()
|
||||
var forceRedraw = false
|
||||
val inputBuffer: InputBuffer = InputBuffer()
|
||||
val pendingInputs: List<String> = []
|
||||
|
||||
val rawModeEnabled = Console.setRawMode(true)
|
||||
if (!rawModeEnabled) {
|
||||
@ -665,7 +643,13 @@ if (!Console.isSupported()) {
|
||||
continue
|
||||
}
|
||||
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
||||
inputBuffer.push(mapped)
|
||||
val mm: Mutex = gameMutex
|
||||
mm.withLock {
|
||||
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
|
||||
pendingInputs.removeAt(0)
|
||||
}
|
||||
pendingInputs.add(mapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (eventErr: Object) {
|
||||
@ -762,11 +746,19 @@ if (!Console.isSupported()) {
|
||||
continue
|
||||
}
|
||||
|
||||
val toApply = inputBuffer.drain()
|
||||
if (toApply.size > 0) {
|
||||
for (k in toApply) {
|
||||
applyKeyInput(state, k)
|
||||
if (!state.running || state.gameOver) break
|
||||
val mm: Mutex = gameMutex
|
||||
mm.withLock {
|
||||
if (pendingInputs.size > 0) {
|
||||
val toApply: List<String> = []
|
||||
while (pendingInputs.size > 0) {
|
||||
val k = pendingInputs[0]
|
||||
pendingInputs.removeAt(0)
|
||||
toApply.add(k)
|
||||
}
|
||||
for (k in toApply) {
|
||||
applyKeyInput(state, k)
|
||||
if (!state.running || state.gameOver) break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!state.running || state.gameOver) {
|
||||
|
||||
@ -27,6 +27,9 @@ kotlin.mpp.enableCInteropCommonization=true
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
# other
|
||||
kotlin.native.cacheKind.linuxX64=none
|
||||
|
||||
# Workaround: Ensure Gradle uses a JDK with `jlink` available for AGP's JDK image transform.
|
||||
# On this environment, the system JDK 21 installation lacks `jlink`, causing
|
||||
# :lynglib:androidJdkImage to fail. Point Gradle to a JDK that includes `jlink`.
|
||||
@ -34,6 +37,6 @@ android.nonTransitiveRClass=true
|
||||
#org.gradle.java.home=/home/sergeych/.jdks/corretto-21.0.9
|
||||
android.experimental.lint.migrateToK2=false
|
||||
android.lint.useK2Uast=false
|
||||
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
||||
kotlin.mpp.applyDefaultHierarchyTemplate=true
|
||||
|
||||
org.gradle.parallel=true
|
||||
org.gradle.parallel=true
|
||||
@ -2,28 +2,19 @@
|
||||
agp = "8.5.2"
|
||||
clikt = "5.0.3"
|
||||
mordant = "3.0.2"
|
||||
kotlin = "2.3.20"
|
||||
kotlin = "2.3.0"
|
||||
android-minSdk = "24"
|
||||
android-compileSdk = "34"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
kotlinx-datetime = "0.6.1"
|
||||
mp_bintools = "0.3.2"
|
||||
ionspin-bignum = "0.3.10"
|
||||
multik = "0.3.0"
|
||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||
okioVersion = "3.10.2"
|
||||
compiler = "3.2.0-alpha11"
|
||||
ktor = "3.3.1"
|
||||
slf4j = "2.0.17"
|
||||
sqlite-jdbc = "3.50.3.0"
|
||||
h2 = "2.4.240"
|
||||
postgresql = "42.7.8"
|
||||
testcontainers = "1.20.6"
|
||||
hikaricp = "6.2.1"
|
||||
|
||||
[libraries]
|
||||
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
|
||||
clikt-core = { module = "com.github.ajalt.clikt:clikt-core", version.ref = "clikt" }
|
||||
clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" }
|
||||
mordant-core = { module = "com.github.ajalt.mordant:mordant-core", version.ref = "mordant" }
|
||||
mordant-jvm-jna = { module = "com.github.ajalt.mordant:mordant-jvm-jna", version.ref = "mordant" }
|
||||
@ -33,27 +24,11 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
||||
multik-default = { module = "org.jetbrains.kotlinx:multik-default", version.ref = "multik" }
|
||||
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||
okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okioVersion" }
|
||||
compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
|
||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
|
||||
ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
|
||||
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" }
|
||||
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" }
|
||||
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" }
|
||||
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
||||
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
|
||||
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
|
||||
hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
|
||||
|
||||
[plugins]
|
||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
[vps]
|
||||
94.130.36.94 ansible_user=sergeych
|
||||
@ -1,100 +0,0 @@
|
||||
---
|
||||
- name: Setup lynglang.com static site on VPS
|
||||
hosts: vps
|
||||
become: yes
|
||||
vars:
|
||||
domain: lynglang.com
|
||||
web_root: /var/www/lynglang
|
||||
deploy_user: sergeych
|
||||
certbot_email: real.sergeych@gmail.com
|
||||
|
||||
tasks:
|
||||
# Debian 10 buster is EOL; security/backports repos moved to archive.debian.org
|
||||
- name: Fix sources.list for Debian buster EOL
|
||||
copy:
|
||||
dest: /etc/apt/sources.list
|
||||
content: |
|
||||
deb http://archive.debian.org/debian/ buster main contrib non-free
|
||||
deb http://archive.debian.org/debian-security/ buster/updates main contrib non-free
|
||||
deb http://archive.debian.org/debian/ buster-backports main contrib non-free
|
||||
|
||||
- name: Remove stale third-party sources (broken for buster EOL)
|
||||
file:
|
||||
path: "/etc/apt/sources.list.d/{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- cassandra.list
|
||||
- icinga.list
|
||||
- postgres.list
|
||||
- salt-stack.list
|
||||
- yarn.list
|
||||
|
||||
- name: Install nginx, certbot, and python3-certbot-nginx
|
||||
apt:
|
||||
name:
|
||||
- nginx
|
||||
- certbot
|
||||
- python3-certbot-nginx
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Create web root directory
|
||||
file:
|
||||
path: "{{ web_root }}/release/dist"
|
||||
state: directory
|
||||
owner: "{{ deploy_user }}"
|
||||
group: www-data
|
||||
mode: "0755"
|
||||
recurse: yes
|
||||
|
||||
- name: Create distributables directory
|
||||
file:
|
||||
path: "{{ web_root }}/release/dist/distributables"
|
||||
state: directory
|
||||
owner: "{{ deploy_user }}"
|
||||
group: www-data
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy nginx site config (HTTP, pre-certbot)
|
||||
template:
|
||||
src: templates/nginx_lynglang.conf.j2
|
||||
dest: /etc/nginx/sites-available/{{ domain }}
|
||||
notify: reload nginx
|
||||
|
||||
- name: Enable nginx site
|
||||
file:
|
||||
src: /etc/nginx/sites-available/{{ domain }}
|
||||
dest: /etc/nginx/sites-enabled/{{ domain }}
|
||||
state: link
|
||||
notify: reload nginx
|
||||
|
||||
- name: Disable default nginx site
|
||||
file:
|
||||
path: /etc/nginx/sites-enabled/default
|
||||
state: absent
|
||||
notify: reload nginx
|
||||
|
||||
- name: Ensure nginx is started
|
||||
service:
|
||||
name: nginx
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: Reload nginx before certbot
|
||||
meta: flush_handlers
|
||||
|
||||
- name: Obtain SSL certificate via certbot (--nginx plugin)
|
||||
command: >
|
||||
certbot --nginx
|
||||
-d {{ domain }} -d www.{{ domain }}
|
||||
--non-interactive --agree-tos
|
||||
--email {{ certbot_email }}
|
||||
--redirect
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem
|
||||
|
||||
handlers:
|
||||
- name: reload nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
@ -1,24 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ domain }} www.{{ domain }};
|
||||
|
||||
root {{ web_root }}/release/dist;
|
||||
index index.html;
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Distributables served directly
|
||||
location /distributables/ {
|
||||
try_files $uri =404;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
# Long-lived cache for hashed assets
|
||||
location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|ico)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
@ -23,8 +23,6 @@ import com.intellij.execution.ui.ConsoleViewContentType
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowAnchor
|
||||
@ -38,10 +36,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import net.sergeych.lyng.idea.LyngIcons
|
||||
import java.io.File
|
||||
|
||||
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
private fun getPsiFile(e: AnActionEvent): PsiFile? {
|
||||
val project = e.project ?: return null
|
||||
@ -51,99 +48,36 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRunnableFile(e: AnActionEvent): PsiFile? {
|
||||
val psiFile = getPsiFile(e) ?: return null
|
||||
val virtualFile = psiFile.virtualFile ?: return null
|
||||
if (!virtualFile.isInLocalFileSystem) return null
|
||||
if (!psiFile.name.endsWith(".lyng")) return null
|
||||
return psiFile
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val psiFile = getRunnableFile(e)
|
||||
val isRunnable = psiFile != null
|
||||
e.presentation.isEnabledAndVisible = isRunnable
|
||||
if (isRunnable) {
|
||||
e.presentation.text = "Run '${psiFile.name}'"
|
||||
e.presentation.description = "Run the current Lyng script using the Lyng CLI"
|
||||
val psiFile = getPsiFile(e)
|
||||
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
||||
e.presentation.isEnabledAndVisible = isLyng
|
||||
if (isLyng) {
|
||||
e.presentation.isEnabled = false
|
||||
e.presentation.text = "Run '${psiFile.name}' (disabled)"
|
||||
e.presentation.description = "Running scripts from the IDE is disabled; use the CLI."
|
||||
} else {
|
||||
e.presentation.text = "Run Lyng Script"
|
||||
e.presentation.description = "Run the current Lyng script"
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val psiFile = getRunnableFile(e) ?: return
|
||||
val virtualFile = psiFile.virtualFile ?: return
|
||||
FileDocumentManager.getInstance().getDocument(virtualFile)?.let { document ->
|
||||
FileDocumentManager.getInstance().saveDocument(document)
|
||||
}
|
||||
val filePath = virtualFile.path
|
||||
val workingDir = virtualFile.parent?.path ?: project.basePath ?: File(filePath).parent
|
||||
val psiFile = getPsiFile(e) ?: return
|
||||
val fileName = psiFile.name
|
||||
|
||||
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
||||
console.clear()
|
||||
|
||||
toolWindow.show {
|
||||
scope.launch {
|
||||
val command = startLyngProcess(filePath, workingDir)
|
||||
if (command == null) {
|
||||
printToConsole(console, "Unable to start Lyng CLI.\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||
printToConsole(console, "Tried commands: lyng, jlyng.\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||
printToConsole(console, "Install `lyng` or `jlyng` and make sure it is available on PATH.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
return@launch
|
||||
}
|
||||
|
||||
printToConsole(
|
||||
console,
|
||||
"Running ${command.commandLine} in ${command.workingDir}\n",
|
||||
ConsoleViewContentType.SYSTEM_OUTPUT
|
||||
)
|
||||
streamProcess(command.process, console)
|
||||
val exitCode = command.process.waitFor()
|
||||
val outputType = if (exitCode == 0) ConsoleViewContentType.SYSTEM_OUTPUT else ConsoleViewContentType.ERROR_OUTPUT
|
||||
printToConsole(console, "\nProcess finished with exit code $exitCode\n", outputType)
|
||||
console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
||||
console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun streamProcess(process: Process, console: ConsoleView) {
|
||||
val stdout = scope.launch {
|
||||
process.inputStream.bufferedReader().useLines { lines ->
|
||||
lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.NORMAL_OUTPUT) }
|
||||
}
|
||||
}
|
||||
val stderr = scope.launch {
|
||||
process.errorStream.bufferedReader().useLines { lines ->
|
||||
lines.forEach { printToConsole(console, "$it\n", ConsoleViewContentType.ERROR_OUTPUT) }
|
||||
}
|
||||
}
|
||||
stdout.join()
|
||||
stderr.join()
|
||||
}
|
||||
|
||||
private fun printToConsole(console: ConsoleView, text: String, type: ConsoleViewContentType) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
console.print(text, type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLyngProcess(filePath: String, workingDir: String?): StartedProcess? {
|
||||
val candidates = listOf("lyng", "jlyng")
|
||||
for (candidate in candidates) {
|
||||
try {
|
||||
val process = ProcessBuilder(candidate, filePath)
|
||||
.directory(workingDir?.let(::File))
|
||||
.start()
|
||||
return StartedProcess(process, "$candidate $filePath", workingDir ?: File(filePath).parent.orEmpty())
|
||||
} catch (_: java.io.IOException) {
|
||||
// Try the next candidate when the command is not available.
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getConsoleAndToolWindow(project: Project): Pair<ConsoleView, ToolWindow> {
|
||||
val toolWindowManager = ToolWindowManager.getInstance(project)
|
||||
var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN)
|
||||
@ -172,10 +106,4 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
contentManager.setSelectedContent(content)
|
||||
return console to actualToolWindow
|
||||
}
|
||||
|
||||
private data class StartedProcess(
|
||||
val process: Process,
|
||||
val commandLine: String,
|
||||
val workingDir: String
|
||||
)
|
||||
}
|
||||
|
||||
@ -310,7 +310,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// Try literal and call-based receiver inference around the dot
|
||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
val className: String? = when {
|
||||
i >= 0 && (text[i] == '"' || text[i] == '`') -> "String"
|
||||
i >= 0 && text[i] == '"' -> "String"
|
||||
i >= 0 && text[i] == ']' -> "List"
|
||||
i >= 0 && text[i] == '}' -> "Dict"
|
||||
i >= 0 && text[i] == ')' -> {
|
||||
|
||||
@ -24,7 +24,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
||||
import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor
|
||||
import net.sergeych.lyng.format.LyngFormatConfig
|
||||
import net.sergeych.lyng.format.LyngFormatter
|
||||
import net.sergeych.lyng.format.LyngStringDelimiterPolicy
|
||||
import net.sergeych.lyng.idea.LyngLanguage
|
||||
|
||||
/**
|
||||
@ -171,7 +170,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||
applySpacing = true,
|
||||
applyWrapping = false,
|
||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||
)
|
||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||
val text = doc.getText(r)
|
||||
@ -191,7 +189,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||
applySpacing = settings.enableSpacing,
|
||||
applyWrapping = true,
|
||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||
)
|
||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||
val text = doc.getText(r)
|
||||
|
||||
@ -101,8 +101,8 @@ class LyngLexer : LexerBase() {
|
||||
return
|
||||
}
|
||||
|
||||
// String "...", `...`, or '...' with simple escape handling
|
||||
if (ch == '"' || ch == '\'' || ch == '`') {
|
||||
// String "..." or '...' with simple escape handling
|
||||
if (ch == '"' || ch == '\'') {
|
||||
val quote = ch
|
||||
i++
|
||||
while (i < endOffset) {
|
||||
|
||||
@ -27,9 +27,9 @@ import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.idea.LyngFileType
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.idea.util.LyngIdeaImportProvider
|
||||
import net.sergeych.lyng.idea.util.TextCtx
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
|
||||
@ -273,7 +273,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
private fun loadMini(file: PsiFile): MiniScript? {
|
||||
LyngAstManager.getMiniAst(file)?.let { return it }
|
||||
return try {
|
||||
val provider = LyngIdeaImportProvider.create()
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = file.text, fileName = file.name, importProvider = provider)
|
||||
|
||||
@ -28,10 +28,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.idea.LyngFileType
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
import net.sergeych.lyng.tools.*
|
||||
|
||||
object LyngAstManager {
|
||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||
@ -145,8 +142,7 @@ object LyngAstManager {
|
||||
|
||||
val text = file.viewProvider.contents.toString()
|
||||
val built = try {
|
||||
DocsBootstrap.ensure()
|
||||
val provider = LyngIdeaImportProvider.create()
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
|
||||
@ -169,7 +165,7 @@ object LyngAstManager {
|
||||
val dMini = getAnalysis(df)?.mini ?: run {
|
||||
val dText = df.viewProvider.contents.toString()
|
||||
try {
|
||||
val provider = LyngIdeaImportProvider.create()
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider)
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.idea.util
|
||||
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.io.http.createHttpModule
|
||||
import net.sergeych.lyng.io.net.createNetModule
|
||||
import net.sergeych.lyng.io.process.createProcessModule
|
||||
import net.sergeych.lyng.io.ws.createWsModule
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.pacman.ImportProvider
|
||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
||||
|
||||
/**
|
||||
* IDE import provider that knows about optional LyngIO modules used by editor analysis.
|
||||
*
|
||||
* The default import manager only exposes core modules; editor features need the pluggable
|
||||
* `lyng.io.*` packages available as well so imported symbols resolve without false errors.
|
||||
*/
|
||||
class LyngIdeaImportProvider private constructor(root: Scope) : ImportProvider(root) {
|
||||
override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope {
|
||||
return try {
|
||||
baseImportManager.createModuleScope(pos, packageName)
|
||||
} catch (_: Throwable) {
|
||||
ModuleScope(this, pos, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val baseImportManager: ImportManager by lazy {
|
||||
Script.defaultImportManager.copy().apply {
|
||||
createFs(PermitAllAccessPolicy, this)
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, this)
|
||||
createHttpModule(PermitAllHttpAccessPolicy, this)
|
||||
createWsModule(PermitAllWsAccessPolicy, this)
|
||||
createNetModule(PermitAllNetAccessPolicy, this)
|
||||
createProcessModule(PermitAllProcessAccessPolicy, this)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(): LyngIdeaImportProvider = LyngIdeaImportProvider(baseImportManager.rootScope)
|
||||
}
|
||||
}
|
||||
@ -179,20 +179,4 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() {
|
||||
assertTrue("Should not report unresolved name for PlainDeclared", messages.none { it.contains("unresolved name: PlainDeclared") })
|
||||
assertTrue("Should not report unresolved member for hello", messages.none { it.contains("unresolved member: hello") })
|
||||
}
|
||||
|
||||
fun test_DiagnosticsResolveOptionalNetModuleSymbols() {
|
||||
val code = """
|
||||
import lyng.io.net
|
||||
|
||||
val server = Net.tcpListen(0, "127.0.0.1")
|
||||
val port = server.localAddress().port
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||
val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
|
||||
assertTrue("Should not report unresolved name for Net, got=$messages", messages.none { it.contains("unresolved name: Net") })
|
||||
assertTrue("Should not report unresolved member for tcpListen, got=$messages", messages.none { it.contains("unresolved member: tcpListen") })
|
||||
assertTrue("Should not report unresolved member for localAddress, got=$messages", messages.none { it.contains("unresolved member: localAddress") })
|
||||
assertTrue("Should not report unresolved member for port, got=$messages", messages.none { it.contains("unresolved member: port") })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.idea.highlight
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LyngLexerBacktickStringTest {
|
||||
|
||||
@Test
|
||||
fun backtickStringGetsStringTokenAndColor() {
|
||||
val lexer = LyngLexer()
|
||||
val source = """val json = `{"name":"lyng","doc":"use \`quotes\`"}`"""
|
||||
lexer.start(source, 0, source.length, 0)
|
||||
|
||||
val tokens = mutableListOf<Pair<String, String>>()
|
||||
while (lexer.tokenType != null) {
|
||||
val tokenText = source.substring(lexer.tokenStart, lexer.tokenEnd)
|
||||
tokens += lexer.tokenType.toString() to tokenText
|
||||
lexer.advance()
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"KEYWORD" to "val",
|
||||
"WHITESPACE" to " ",
|
||||
"IDENTIFIER" to "json",
|
||||
"WHITESPACE" to " ",
|
||||
"PUNCT" to "=",
|
||||
"WHITESPACE" to " ",
|
||||
"STRING" to "`{\"name\":\"lyng\",\"doc\":\"use \\`quotes\\`\"}`"
|
||||
),
|
||||
tokens
|
||||
)
|
||||
|
||||
val highlighter = LyngSyntaxHighlighter()
|
||||
assertArrayEquals(
|
||||
arrayOf(LyngHighlighterColors.STRING),
|
||||
highlighter.getTokenHighlights(LyngTokenTypes.STRING)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,39 +19,9 @@ plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
}
|
||||
|
||||
import org.gradle.api.Project
|
||||
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "unspecified"
|
||||
|
||||
private fun Project.sqliteLinuxLinkerOpts(vararg defaultDirs: String): List<String> {
|
||||
val overrideDir = providers.gradleProperty("sqlite3.lib.dir").orNull
|
||||
?: providers.environmentVariable("SQLITE3_LIB_DIR").orNull
|
||||
val candidateDirs = buildList {
|
||||
if (!overrideDir.isNullOrBlank()) {
|
||||
add(file(overrideDir))
|
||||
}
|
||||
defaultDirs.forEach { add(file(it)) }
|
||||
}.distinctBy { it.absolutePath }
|
||||
|
||||
val discoveredLib = sequenceOf("libsqlite3.so", "libsqlite3.so.0")
|
||||
.mapNotNull { libraryName ->
|
||||
candidateDirs.firstOrNull { it.resolve(libraryName).isFile }?.let { dir ->
|
||||
listOf("-L${dir.absolutePath}", "-l:$libraryName")
|
||||
}
|
||||
}
|
||||
.firstOrNull()
|
||||
?: listOf("-lsqlite3")
|
||||
|
||||
return discoveredLib + listOf(
|
||||
"-ldl",
|
||||
"-lpthread",
|
||||
"-lm",
|
||||
"-Wl,--allow-shlib-undefined"
|
||||
)
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.universablockchain.com/")
|
||||
@ -64,10 +34,8 @@ kotlin {
|
||||
// Suppress Beta warning for expect/actual classes across all targets in this module
|
||||
targets.configureEach {
|
||||
compilations.configureEach {
|
||||
compileTaskProvider.configure {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
compilerOptions.configure {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,22 +50,6 @@ kotlin {
|
||||
linuxX64 {
|
||||
binaries {
|
||||
executable()
|
||||
all {
|
||||
linkerOpts(
|
||||
*project.sqliteLinuxLinkerOpts(
|
||||
"/lib/x86_64-linux-gnu",
|
||||
"/usr/lib/x86_64-linux-gnu",
|
||||
"/lib64",
|
||||
"/usr/lib64",
|
||||
"/lib",
|
||||
"/usr/lib"
|
||||
).toTypedArray()
|
||||
)
|
||||
if (buildType == org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.RELEASE) {
|
||||
debuggable = false
|
||||
optimized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
@ -109,7 +61,7 @@ kotlin {
|
||||
// filesystem access into the execution Scope by default.
|
||||
implementation(project(":lyngio"))
|
||||
implementation(libs.okio)
|
||||
implementation(libs.clikt.core)
|
||||
implementation(libs.clikt)
|
||||
implementation(kotlin("stdlib-common"))
|
||||
// optional support for rendering markdown in help messages
|
||||
// implementation(libs.clikt.markdown)
|
||||
@ -123,41 +75,19 @@ kotlin {
|
||||
implementation(libs.okio.fakefilesystem)
|
||||
}
|
||||
}
|
||||
val linuxTest by creating {
|
||||
dependsOn(commonTest)
|
||||
}
|
||||
val nativeMain by creating {
|
||||
dependsOn(commonMain)
|
||||
}
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.slf4j.nop)
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(kotlin("test-junit"))
|
||||
}
|
||||
}
|
||||
// val nativeMain by getting {
|
||||
// dependencies {
|
||||
// implementation(kotlin("stdlib-common"))
|
||||
// }
|
||||
// }
|
||||
val linuxX64Main by getting {
|
||||
dependsOn(nativeMain)
|
||||
}
|
||||
val linuxX64Test by getting {
|
||||
dependsOn(linuxTest)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<KotlinNativeTest>("linuxX64Test") {
|
||||
dependsOn(tasks.named("linkDebugExecutableLinuxX64"))
|
||||
dependsOn(tasks.named("linkReleaseExecutableLinuxX64"))
|
||||
environment(
|
||||
"LYNG_CLI_NATIVE_BIN",
|
||||
layout.buildDirectory.file("bin/linuxX64/debugExecutable/lyng.kexe").get().asFile.absolutePath
|
||||
)
|
||||
environment(
|
||||
"LYNG_CLI_NATIVE_RELEASE_BIN",
|
||||
layout.buildDirectory.file("bin/linuxX64/releaseExecutable/lyng.kexe").get().asFile.absolutePath
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -17,8 +17,8 @@
|
||||
|
||||
package net.sergeych
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.Context
|
||||
import com.github.ajalt.clikt.core.CoreCliktCommand
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
@ -26,54 +26,28 @@ import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.arguments.optional
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.LyngVersion
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyng.io.db.createDbModule
|
||||
import net.sergeych.lyng.io.db.jdbc.createJdbcModule
|
||||
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.io.html.createHtmlModule
|
||||
import net.sergeych.lyng.io.http.createHttpModule
|
||||
import net.sergeych.lyng.io.http.server.createHttpServerModule
|
||||
import net.sergeych.lyng.io.net.createNetModule
|
||||
import net.sergeych.lyng.io.ws.createWsModule
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
import net.sergeych.lyngio.net.shutdownSystemNetEngine
|
||||
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import okio.*
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
import okio.SYSTEM
|
||||
import okio.buffer
|
||||
import okio.use
|
||||
|
||||
// common code
|
||||
|
||||
expect fun exit(code: Int)
|
||||
|
||||
internal expect class CliPlatformShutdownHooks {
|
||||
fun uninstall()
|
||||
|
||||
companion object {
|
||||
fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks
|
||||
}
|
||||
}
|
||||
|
||||
expect class ShellCommandExecutor {
|
||||
fun executeCommand(command: String): CommandResult
|
||||
|
||||
@ -88,308 +62,19 @@ data class CommandResult(
|
||||
val error: String
|
||||
)
|
||||
|
||||
private const val cliBuiltinsDeclarations = """
|
||||
extern fun atExit(append: Bool=true, handler: ()->Void)
|
||||
"""
|
||||
|
||||
private class CliExitRequested(val code: Int) : RuntimeException("CLI exit requested: $code")
|
||||
|
||||
internal class CliExecutionRuntime(
|
||||
private val session: EvalSession,
|
||||
private val rootScope: Scope
|
||||
) {
|
||||
private val shutdownMutex = Mutex()
|
||||
private var shutdownStarted = false
|
||||
private val exitHandlers = mutableListOf<Obj>()
|
||||
|
||||
fun registerAtExit(handler: Obj, append: Boolean) {
|
||||
if (append) {
|
||||
exitHandlers += handler
|
||||
} else {
|
||||
exitHandlers.add(0, handler)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun shutdown() {
|
||||
shutdownMutex.withLock {
|
||||
if (shutdownStarted) return
|
||||
shutdownStarted = true
|
||||
}
|
||||
val handlers = exitHandlers.toList()
|
||||
val facade = rootScope.asFacade()
|
||||
for (handler in handlers) {
|
||||
runCatching {
|
||||
facade.call(handler)
|
||||
}
|
||||
}
|
||||
session.cancel()
|
||||
shutdownSystemNetEngine()
|
||||
session.join()
|
||||
}
|
||||
|
||||
fun shutdownBlocking() {
|
||||
runBlocking {
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val baseCliImportManagerDefer = globalDefer {
|
||||
val manager = Script.defaultImportManager.copy().apply {
|
||||
installCliModules(this)
|
||||
}
|
||||
manager.newStdScope()
|
||||
manager
|
||||
}
|
||||
|
||||
private fun ImportManager.invalidateCliModuleCaches() {
|
||||
invalidatePackageCache("lyng.io.fs")
|
||||
invalidatePackageCache("lyng.io.console")
|
||||
invalidatePackageCache("lyng.io.db.jdbc")
|
||||
invalidatePackageCache("lyng.io.db.sqlite")
|
||||
invalidatePackageCache("lyng.io.html")
|
||||
invalidatePackageCache("lyng.io.http")
|
||||
invalidatePackageCache("lyng.io.http.server")
|
||||
invalidatePackageCache("lyng.io.ws")
|
||||
invalidatePackageCache("lyng.io.net")
|
||||
}
|
||||
|
||||
val baseScopeDefer = globalDefer {
|
||||
baseCliImportManagerDefer.await().copy().apply {
|
||||
invalidateCliModuleCaches()
|
||||
}.newStdScope().apply {
|
||||
installCliBuiltins()
|
||||
installCliDeclarations()
|
||||
addConst("ARGV", ObjList(mutableListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Scope.installCliDeclarations() {
|
||||
eval(Source("<cli-builtins>", cliBuiltinsDeclarations))
|
||||
}
|
||||
|
||||
private fun Scope.installCliBuiltins(runtime: CliExecutionRuntime? = null) {
|
||||
addFn("exit") {
|
||||
val code = requireOnlyArg<ObjInt>().toInt()
|
||||
if (runtime == null) {
|
||||
exit(code)
|
||||
Script.newScope().apply {
|
||||
addFn("exit") {
|
||||
exit(requireOnlyArg<ObjInt>().toInt())
|
||||
ObjVoid
|
||||
}
|
||||
throw CliExitRequested(code)
|
||||
// Install lyng.io.fs module with full access by default for the CLI tool's Scope.
|
||||
// Scripts still need to `import lyng.io.fs` to use Path API.
|
||||
createFs(PermitAllAccessPolicy, this)
|
||||
// Install console access by default for interactive CLI scripts.
|
||||
// Scripts still need to `import lyng.io.console` to use it.
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, this)
|
||||
}
|
||||
addFn("atExit") {
|
||||
if (runtime == null) {
|
||||
raiseIllegalState("atExit is only available while running a CLI script")
|
||||
}
|
||||
if (args.list.size > 2) {
|
||||
raiseError("Expected at most 2 positional arguments, got ${args.list.size}")
|
||||
}
|
||||
var append = true
|
||||
var appendSet = false
|
||||
var handler: Obj? = null
|
||||
|
||||
when (args.list.size) {
|
||||
1 -> {
|
||||
val only = args.list[0]
|
||||
if (only.isInstanceOf("Callable")) {
|
||||
handler = only
|
||||
} else {
|
||||
append = only.toBool()
|
||||
appendSet = true
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
append = args.list[0].toBool()
|
||||
appendSet = true
|
||||
handler = args.list[1]
|
||||
}
|
||||
}
|
||||
|
||||
for ((name, value) in args.named) {
|
||||
when (name) {
|
||||
"append" -> {
|
||||
if (appendSet) {
|
||||
raiseIllegalArgument("argument 'append' is already set")
|
||||
}
|
||||
append = value.toBool()
|
||||
appendSet = true
|
||||
}
|
||||
"handler" -> {
|
||||
if (handler != null) {
|
||||
raiseIllegalArgument("argument 'handler' is already set")
|
||||
}
|
||||
handler = value
|
||||
}
|
||||
else -> raiseIllegalArgument("unknown argument '$name'")
|
||||
}
|
||||
}
|
||||
|
||||
val handlerValue = handler ?: raiseError("argument 'handler' is required")
|
||||
if (!handlerValue.isInstanceOf("Callable")) {
|
||||
raiseClassCastError("Expected handler to be callable")
|
||||
}
|
||||
runtime.registerAtExit(handlerValue, append)
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
private fun installCliModules(manager: ImportManager) {
|
||||
// Scripts still need to import the modules they use explicitly.
|
||||
createFs(PermitAllAccessPolicy, manager)
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, manager)
|
||||
createDbModule(manager)
|
||||
createJdbcModule(manager)
|
||||
createSqliteModule(manager)
|
||||
createHtmlModule(manager)
|
||||
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
||||
createHttpServerModule(PermitAllNetAccessPolicy, manager)
|
||||
createWsModule(PermitAllWsAccessPolicy, manager)
|
||||
createNetModule(PermitAllNetAccessPolicy, manager)
|
||||
}
|
||||
|
||||
private data class LocalCliModule(
|
||||
val packageName: String,
|
||||
val source: Source
|
||||
)
|
||||
|
||||
private fun readUtf8(path: Path): String =
|
||||
FileSystem.SYSTEM.source(path).use { fileSource ->
|
||||
fileSource.buffer().use { bs ->
|
||||
bs.readUtf8()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stripShebang(text: String): String {
|
||||
if (!text.startsWith("#!")) return text
|
||||
val pos = text.indexOf('\n')
|
||||
return if (pos >= 0) text.substring(pos + 1) else ""
|
||||
}
|
||||
|
||||
private fun extractDeclaredPackageNameOrNull(source: Source): String? {
|
||||
for (line in source.lines) {
|
||||
if (line.isBlank()) continue
|
||||
return if (line.startsWith("package ")) {
|
||||
line.substring(8).trim()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun canonicalPath(path: Path): Path = FileSystem.SYSTEM.canonicalize(path)
|
||||
|
||||
private fun relativeModuleName(rootDir: Path, file: Path): String {
|
||||
val rootText = rootDir.toString().trimEnd('/', '\\')
|
||||
val fileText = file.toString()
|
||||
val prefix = "$rootText/"
|
||||
if (!fileText.startsWith(prefix)) {
|
||||
throw ScriptError(Pos.builtIn, "local import root mismatch: $fileText is not under $rootText")
|
||||
}
|
||||
val relative = fileText.removePrefix(prefix)
|
||||
val modulePath = relative.removeSuffix(".lyng")
|
||||
return modulePath
|
||||
.split('/', '\\')
|
||||
.filter { it.isNotEmpty() }
|
||||
.joinToString(".")
|
||||
}
|
||||
|
||||
private fun scanLyngFiles(rootDir: Path): List<Path> {
|
||||
val system = FileSystem.SYSTEM
|
||||
val pending = ArrayDeque<Path>()
|
||||
val visited = linkedSetOf<String>()
|
||||
val files = mutableListOf<Path>()
|
||||
pending.add(rootDir)
|
||||
while (pending.isNotEmpty()) {
|
||||
val dir = pending.removeLast()
|
||||
val canonicalDir = canonicalPath(dir)
|
||||
if (!visited.add(canonicalDir.toString())) continue
|
||||
val children = try {
|
||||
system.list(canonicalDir)
|
||||
} catch (_: Exception) {
|
||||
continue
|
||||
}
|
||||
for (child in children) {
|
||||
val meta = try {
|
||||
system.metadata(child)
|
||||
} catch (_: Exception) {
|
||||
continue
|
||||
}
|
||||
when {
|
||||
meta.isDirectory -> pending.add(child)
|
||||
child.name.endsWith(".lyng") -> {
|
||||
val canonicalFile = try {
|
||||
canonicalPath(child)
|
||||
} catch (_: Exception) {
|
||||
continue
|
||||
}
|
||||
files += canonicalFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
private fun discoverLocalCliModules(entryFile: Path): List<LocalCliModule> {
|
||||
val rootDir = entryFile.parent ?: ".".toPath()
|
||||
val seenPackages = linkedMapOf<String, Path>()
|
||||
return scanLyngFiles(rootDir)
|
||||
.asSequence()
|
||||
.filter { it != entryFile }
|
||||
.map { file ->
|
||||
val text = stripShebang(readUtf8(file))
|
||||
val source = Source(file.toString(), text)
|
||||
val expectedPackage = relativeModuleName(rootDir, file)
|
||||
val declaredPackage = extractDeclaredPackageNameOrNull(source)
|
||||
if (declaredPackage != null && declaredPackage != expectedPackage) {
|
||||
throw ScriptError(
|
||||
source.startPos,
|
||||
"local module package mismatch: expected '$expectedPackage' for ${file.toString()} but found '$declaredPackage'"
|
||||
)
|
||||
}
|
||||
val packageName = declaredPackage ?: expectedPackage
|
||||
val previous = seenPackages[packageName]
|
||||
if (previous != null) {
|
||||
throw ScriptError(
|
||||
source.startPos,
|
||||
"duplicate local module '$packageName': ${previous.toString()} and ${file.toString()}"
|
||||
)
|
||||
}
|
||||
seenPackages[packageName] = file
|
||||
LocalCliModule(packageName, source)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun registerLocalCliModules(manager: ImportManager, modules: List<LocalCliModule>) {
|
||||
for (module in modules) {
|
||||
manager.addPackage(module.packageName) { scope ->
|
||||
scope.eval(module.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ImportManager.newCliScope(argv: List<String>): Scope =
|
||||
newStdScope().apply {
|
||||
installCliBuiltins()
|
||||
installCliDeclarations()
|
||||
addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList()))
|
||||
}
|
||||
|
||||
internal suspend fun newCliScope(argv: List<String>, entryFileName: String? = null): Scope {
|
||||
val baseManager = baseCliImportManagerDefer.await().copy().apply {
|
||||
invalidateCliModuleCaches()
|
||||
}
|
||||
if (entryFileName == null) {
|
||||
return baseManager.newCliScope(argv)
|
||||
}
|
||||
val entryFile = canonicalPath(entryFileName.toPath())
|
||||
val localModules = discoverLocalCliModules(entryFile)
|
||||
if (localModules.isEmpty()) {
|
||||
return baseManager.newCliScope(argv)
|
||||
}
|
||||
registerLocalCliModules(baseManager, localModules)
|
||||
return baseManager.newCliScope(argv)
|
||||
}
|
||||
|
||||
fun runMain(args: Array<String>) {
|
||||
@ -413,7 +98,7 @@ fun runMain(args: Array<String>) {
|
||||
.main(args)
|
||||
}
|
||||
|
||||
private class Fmt : CoreCliktCommand(name = "fmt") {
|
||||
private class Fmt : CliktCommand(name = "fmt") {
|
||||
private val checkOnly by option("--check", help = "Check only; print files that would change").flag()
|
||||
private val inPlace by option("-i", "--in-place", help = "Write changes back to files").flag()
|
||||
private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag()
|
||||
@ -436,7 +121,6 @@ private class Fmt : CoreCliktCommand(name = "fmt") {
|
||||
val cfg = net.sergeych.lyng.format.LyngFormatConfig(
|
||||
applySpacing = enableSpacing,
|
||||
applyWrapping = enableWrapping,
|
||||
stringDelimiterPolicy = net.sergeych.lyng.format.LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||
)
|
||||
|
||||
var anyChanged = false
|
||||
@ -472,9 +156,8 @@ private class Fmt : CoreCliktCommand(name = "fmt") {
|
||||
}
|
||||
}
|
||||
|
||||
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktCommand() {
|
||||
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
||||
|
||||
override val invokeWithoutSubcommand = true
|
||||
override val printHelpOnEmptyArgs = true
|
||||
|
||||
val version by option("-v", "--version", help = "Print version and exit").flag()
|
||||
@ -503,6 +186,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktComman
|
||||
if (currentContext.invokedSubcommand != null) return
|
||||
|
||||
runBlocking {
|
||||
val baseScope = baseScopeDefer.await()
|
||||
when {
|
||||
version -> {
|
||||
println("Lyng language version ${LyngVersion}")
|
||||
@ -512,13 +196,20 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktComman
|
||||
val objargs = mutableListOf<String>()
|
||||
script?.let { objargs += it }
|
||||
objargs += args
|
||||
baseScope.addConst(
|
||||
"ARGV", ObjList(
|
||||
objargs.map { ObjString(it) }.toMutableList()
|
||||
)
|
||||
)
|
||||
launcher {
|
||||
// there is no script name, it is a first argument instead:
|
||||
processErrors {
|
||||
executeSource(
|
||||
val script = Compiler.compileWithResolution(
|
||||
Source("<eval>", execute!!),
|
||||
newCliScope(objargs)
|
||||
baseScope.currentImportProvider,
|
||||
seedScope = baseScope
|
||||
)
|
||||
script.execute(baseScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -528,7 +219,8 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktComman
|
||||
println("Error: no script specified.\n")
|
||||
echoFormattedHelp()
|
||||
} else {
|
||||
launcher { executeFile(script!!, args) }
|
||||
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
||||
launcher { executeFile(script!!) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -538,52 +230,30 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktComman
|
||||
|
||||
fun executeFileWithArgs(fileName: String, args: List<String>) {
|
||||
runBlocking {
|
||||
executeFile(fileName, args)
|
||||
baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
||||
executeFile(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun executeSource(source: Source, initialScope: Scope? = null) {
|
||||
val session = EvalSession(initialScope ?: baseScopeDefer.await())
|
||||
val rootScope = session.getScope()
|
||||
val runtime = CliExecutionRuntime(session, rootScope)
|
||||
rootScope.installCliBuiltins(runtime)
|
||||
val shutdownHooks = CliPlatformShutdownHooks.install(runtime)
|
||||
var requestedExitCode: Int? = null
|
||||
try {
|
||||
try {
|
||||
evalOnCliDispatcher(session, source)
|
||||
} catch (e: CliExitRequested) {
|
||||
requestedExitCode = e.code
|
||||
} catch (e: ExecutionError) {
|
||||
val cliExit = generateSequence<Throwable>(e) { it.cause }
|
||||
.filterIsInstance<CliExitRequested>()
|
||||
.firstOrNull()
|
||||
if (cliExit != null) {
|
||||
requestedExitCode = cliExit.code
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
suspend fun executeFile(fileName: String) {
|
||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
||||
fileSource.buffer().use { bs ->
|
||||
bs.readUtf8()
|
||||
}
|
||||
} finally {
|
||||
shutdownHooks.uninstall()
|
||||
runtime.shutdown()
|
||||
}
|
||||
requestedExitCode?.let { exit(it) }
|
||||
}
|
||||
|
||||
internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source): Obj =
|
||||
withContext(Dispatchers.Default) {
|
||||
session.eval(source)
|
||||
if( text.startsWith("#!") ) {
|
||||
// skip shebang
|
||||
val pos = text.indexOf('\n')
|
||||
text = text.substring(pos + 1)
|
||||
}
|
||||
|
||||
suspend fun executeFile(fileName: String, args: List<String> = emptyList()) {
|
||||
val canonicalFile = canonicalPath(fileName.toPath())
|
||||
val text = stripShebang(readUtf8(canonicalFile))
|
||||
processErrors {
|
||||
executeSource(
|
||||
Source(canonicalFile.toString(), text),
|
||||
newCliScope(args, canonicalFile.toString())
|
||||
val scope = baseScopeDefer.await()
|
||||
val script = Compiler.compileWithResolution(
|
||||
Source(fileName, text),
|
||||
scope.currentImportProvider,
|
||||
seedScope = scope
|
||||
)
|
||||
script.execute(scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CliTcpServerRegressionTest {
|
||||
|
||||
@Test
|
||||
fun reducedTcpServerExampleRunsWithCopiedCliImportManager() = runBlocking {
|
||||
val cliScope = newCliScope(emptyList())
|
||||
val session = EvalSession(cliScope)
|
||||
|
||||
try {
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<tcp-server-regression>",
|
||||
"""
|
||||
import lyng.buffer
|
||||
import lyng.io.net
|
||||
|
||||
val host = "127.0.0.1"
|
||||
val server = Net.tcpListen(0, host)
|
||||
val port = server.localAddress().port
|
||||
val accepted = launch {
|
||||
val client = server.accept()
|
||||
val line = (client.read(4) as Buffer).decodeUtf8()
|
||||
client.close()
|
||||
server.close()
|
||||
line
|
||||
}
|
||||
|
||||
val socket = Net.tcpConnect(host, port)
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
socket.close()
|
||||
accepted.await()
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals("ping", (result as ObjString).value)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun concurrentTcpExampleRunsInCliScope() = runBlocking {
|
||||
val cliScope = newCliScope(emptyList())
|
||||
val session = EvalSession(cliScope)
|
||||
|
||||
try {
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<tcp-server-concurrency-cli>",
|
||||
"""
|
||||
import lyng.io.net
|
||||
|
||||
val host = "127.0.0.1"
|
||||
val clientCount = 32
|
||||
val server: TcpServer = Net.tcpListen(0, host, 32, true) as TcpServer
|
||||
val port: Int = server.localAddress().port
|
||||
|
||||
fun payloadFor(index: Int): String {
|
||||
"${'$'}index:${'$'}{Random.nextInt()}:${'$'}{Random.nextInt()}"
|
||||
}
|
||||
|
||||
fun handleClient(client: TcpSocket): String {
|
||||
try {
|
||||
val source = client.readLine()
|
||||
if( source == null ) {
|
||||
return "server-eof"
|
||||
}
|
||||
val reply = "pong: ${'$'}source"
|
||||
client.writeUtf8(reply + "\n")
|
||||
client.flush()
|
||||
reply
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
val serverJob: Deferred = launch {
|
||||
var handlers: List<Deferred> = List()
|
||||
try {
|
||||
for( i in 0..<32 ) {
|
||||
val client: TcpSocket = server.accept() as TcpSocket
|
||||
handlers += launch {
|
||||
handleClient(client)
|
||||
}
|
||||
}
|
||||
handlers.joinAll()
|
||||
} finally {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
||||
val clientJobs = (0..<clientCount).map { index ->
|
||||
val payload = payloadFor(index)
|
||||
launch {
|
||||
val socket: TcpSocket = Net.tcpConnect(host, port) as TcpSocket
|
||||
try {
|
||||
socket.writeUtf8(payload + "\n")
|
||||
socket.flush()
|
||||
val reply = socket.readLine()
|
||||
if( reply == null ) {
|
||||
"client-eof:${'$'}payload"
|
||||
} else {
|
||||
assertEquals("pong: ${'$'}payload", reply)
|
||||
reply
|
||||
}
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val replies = clientJobs.joinAll()
|
||||
val serverReplies = serverJob.await() as List<Object>
|
||||
assertEquals(clientCount, replies.size)
|
||||
assertEquals(clientCount, serverReplies.size)
|
||||
assertEquals(replies.toSet, serverReplies.toSet)
|
||||
"OK:${'$'}clientCount:${'$'}{replies.toSet}:${'$'}{serverReplies.toSet}"
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
|
||||
val text = (result as ObjString).value
|
||||
assertTrue(text.startsWith("OK:32:"), text)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mixedModuleAndLocalCapturesWorkInCliScope() = runBlocking {
|
||||
val cliScope = newCliScope(emptyList())
|
||||
val session = EvalSession(cliScope)
|
||||
|
||||
try {
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<cli-capture-regression>",
|
||||
"""
|
||||
val prefix = "pong"
|
||||
val jobs = (0..<32).map { index ->
|
||||
val payload = "${'$'}index:${'$'}{Random.nextInt()}"
|
||||
launch {
|
||||
delay(5)
|
||||
"${'$'}prefix:${'$'}payload"
|
||||
}
|
||||
}
|
||||
jobs.joinAll()
|
||||
""".trimIndent()
|
||||
)
|
||||
) as net.sergeych.lyng.obj.ObjList
|
||||
|
||||
assertEquals(32, result.list.size)
|
||||
assertEquals(32, result.list.map { (it as ObjString).value }.toSet().size)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,34 +24,6 @@ import kotlin.system.exitProcess
|
||||
@PublishedApi
|
||||
internal var jvmExitImpl: (Int) -> Nothing = { code -> exitProcess(code) }
|
||||
|
||||
internal actual class CliPlatformShutdownHooks private constructor(
|
||||
private val shutdownHook: Thread?
|
||||
) {
|
||||
actual fun uninstall() {
|
||||
val hook = shutdownHook ?: return
|
||||
runCatching {
|
||||
Runtime.getRuntime().removeShutdownHook(hook)
|
||||
}
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks {
|
||||
val hook = Thread(
|
||||
{
|
||||
runtime.shutdownBlocking()
|
||||
},
|
||||
"lyng-cli-shutdown"
|
||||
)
|
||||
return runCatching {
|
||||
Runtime.getRuntime().addShutdownHook(hook)
|
||||
CliPlatformShutdownHooks(hook)
|
||||
}.getOrElse {
|
||||
CliPlatformShutdownHooks(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exit(code: Int) {
|
||||
jvmExitImpl(code)
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjList
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class CliDispatcherJvmTest {
|
||||
@Test
|
||||
fun executeSourceRunsOnDefaultDispatcher() = runBlocking {
|
||||
val callerThread = Thread.currentThread()
|
||||
val callerThreadKey = "${System.identityHashCode(callerThread)}:${callerThread.name}"
|
||||
val scope = Script.newScope().apply {
|
||||
addFn("threadKey") { ObjString("${System.identityHashCode(Thread.currentThread())}:${Thread.currentThread().name}") }
|
||||
addFn("threadName") { ObjString(Thread.currentThread().name) }
|
||||
}
|
||||
val session = EvalSession(scope)
|
||||
|
||||
try {
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<test>",
|
||||
"""
|
||||
val task = launch { [threadKey(), threadName()] }
|
||||
val child = task.await()
|
||||
[threadKey(), threadName(), child]
|
||||
""".trimIndent()
|
||||
)
|
||||
) as ObjList
|
||||
|
||||
val topLevelThreadKey = (result.list[0] as ObjString).value
|
||||
val topLevelThreadName = (result.list[1] as ObjString).value
|
||||
val child = result.list[2] as ObjList
|
||||
val childThreadKey = (child.list[0] as ObjString).value
|
||||
val childThreadName = (child.list[1] as ObjString).value
|
||||
|
||||
assertNotEquals(
|
||||
callerThreadKey,
|
||||
topLevelThreadKey,
|
||||
"CLI top-level script body should not run on the runBlocking caller thread: $topLevelThreadName"
|
||||
)
|
||||
assertNotEquals(
|
||||
callerThreadKey,
|
||||
childThreadKey,
|
||||
"CLI launch child should not inherit the runBlocking caller thread: $childThreadName"
|
||||
)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cliEvalInfersDeferredItTypeFromMapLambdaLocal() = runBlocking {
|
||||
val session = EvalSession(Script.newScope())
|
||||
try {
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<cli-repro>",
|
||||
"""
|
||||
var sum = 0
|
||||
var counter = 0
|
||||
|
||||
(1..3).map { n ->
|
||||
val counterState = counter
|
||||
val task = launch { counterState + n }
|
||||
++counter
|
||||
task
|
||||
}.forEach { sum += it.await() }
|
||||
|
||||
sum
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(9, (result as ObjInt).value)
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,290 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CliLocalModuleImportRegressionJvmTest {
|
||||
private val originalOut: PrintStream = System.out
|
||||
private val originalErr: PrintStream = System.err
|
||||
|
||||
private class TestExit(val code: Int) : RuntimeException()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
jvmExitImpl = { code -> throw TestExit(code) }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||
}
|
||||
|
||||
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||
|
||||
private fun runCli(vararg args: String): CliResult {
|
||||
val outBuf = ByteArrayOutputStream()
|
||||
val errBuf = ByteArrayOutputStream()
|
||||
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||
|
||||
var exitCode: Int? = null
|
||||
try {
|
||||
runMain(arrayOf(*args))
|
||||
} catch (e: TestExit) {
|
||||
exitCode = e.code
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||
}
|
||||
|
||||
private fun writeTransitiveImportTree(root: java.nio.file.Path) {
|
||||
val packageDir = Files.createDirectories(root.resolve("package1"))
|
||||
val nestedDir = Files.createDirectories(packageDir.resolve("nested"))
|
||||
|
||||
packageDir.resolve("alpha.lyng").writeText(
|
||||
"""
|
||||
package package1.alpha
|
||||
|
||||
import lyng.stdlib
|
||||
import lyng.io.net
|
||||
|
||||
class Alpha {
|
||||
val headers = Map<String, String>()
|
||||
|
||||
fun makeTask(port: Int, host: String): Deferred = launch {
|
||||
host + ":" + port
|
||||
}
|
||||
|
||||
fun netModule() = Net
|
||||
}
|
||||
|
||||
fun alphaValue() = "alpha"
|
||||
""".trimIndent()
|
||||
)
|
||||
packageDir.resolve("beta.lyng").writeText(
|
||||
"""
|
||||
package package1.beta
|
||||
|
||||
import lyng.stdlib
|
||||
import package1.alpha
|
||||
|
||||
fun betaValue() = alphaValue() + "|beta"
|
||||
""".trimIndent()
|
||||
)
|
||||
nestedDir.resolve("gamma.lyng").writeText(
|
||||
"""
|
||||
package package1.nested.gamma
|
||||
|
||||
import lyng.io.net
|
||||
import package1.alpha
|
||||
import package1.beta
|
||||
|
||||
val String.gammaTag get() = this + "|gamma"
|
||||
|
||||
fun gammaValue() = betaValue().gammaTag
|
||||
fun netModule() = Net
|
||||
""".trimIndent()
|
||||
)
|
||||
packageDir.resolve("entry.lyng").writeText(
|
||||
"""
|
||||
package package1.entry
|
||||
|
||||
import lyng.stdlib
|
||||
import lyng.io.net
|
||||
import package1.alpha
|
||||
import package1.beta
|
||||
import package1.nested.gamma
|
||||
|
||||
fun report() = gammaValue() + "|entry"
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeNestedLaunchImportBugTree(root: java.nio.file.Path) {
|
||||
val packageDir = Files.createDirectories(root.resolve("package1"))
|
||||
|
||||
packageDir.resolve("alpha.lyng").writeText(
|
||||
"""
|
||||
import lyng.io.net
|
||||
import package1.bravo
|
||||
|
||||
class Alpha {
|
||||
val tcpServer: TcpServer
|
||||
val headers = Map<String, String>()
|
||||
|
||||
fn startListen(port, host) {
|
||||
var eager = Bravo()
|
||||
eager.doSomething()
|
||||
tcpServer = Net.tcpListen(port, host)
|
||||
println("tcpServer.isOpen: " + tcpServer.isOpen())
|
||||
launch {
|
||||
try {
|
||||
while (true) {
|
||||
println("wait for accept...")
|
||||
val tcpSocket = tcpServer.accept()
|
||||
println("var bravo = Bravo()")
|
||||
var bravo = Bravo()
|
||||
println("bravo.doSomething()...")
|
||||
bravo.doSomething()
|
||||
println("bravo.doSomething()... OK")
|
||||
tcpSocket.close()
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
println("ERR [Alpha.startListen]: '", e, "'")
|
||||
} finally {
|
||||
println("FIN [Alpha.startListen]")
|
||||
tcpServer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
packageDir.resolve("bravo.lyng").writeText(
|
||||
"""
|
||||
class Bravo {
|
||||
fn doSomething() {
|
||||
println("Bravo.doSomething")
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localModuleUsingLaunchAndNetImportsWithoutStdlibRedefinition() = runBlocking {
|
||||
val root = Files.createTempDirectory("lyng-cli-import-regression")
|
||||
try {
|
||||
val mainFile = root.resolve("main.lyng")
|
||||
writeTransitiveImportTree(root)
|
||||
mainFile.writeText(
|
||||
"""
|
||||
import package1.entry
|
||||
import package1.beta
|
||||
import package1.nested.gamma
|
||||
|
||||
println(report())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
executeFile(mainFile.toString(), emptyList())
|
||||
} finally {
|
||||
root.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localModuleImportsAreNoOpsWhenEvaldRepeatedlyOnSameCliContext() = runBlocking {
|
||||
val root = Files.createTempDirectory("lyng-cli-import-regression-repeat")
|
||||
try {
|
||||
val mainFile = root.resolve("main.lyng")
|
||||
writeTransitiveImportTree(root)
|
||||
mainFile.writeText("println(\"bootstrap\")")
|
||||
|
||||
val session = EvalSession(newCliScope(emptyList(), mainFile.toString()))
|
||||
try {
|
||||
repeat(5) { index ->
|
||||
val result = evalOnCliDispatcher(
|
||||
session,
|
||||
Source(
|
||||
"<repeat-local-import-$index>",
|
||||
"""
|
||||
import package1.entry
|
||||
import package1.nested.gamma
|
||||
import package1.beta
|
||||
import package1.alpha
|
||||
|
||||
report()
|
||||
""".trimIndent()
|
||||
)
|
||||
) as ObjString
|
||||
|
||||
assertEquals(
|
||||
"alpha|beta|gamma|entry",
|
||||
result.value
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
} finally {
|
||||
root.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localModuleImportUsedOnlyInsideMethodLaunchClosureRemainsPrepared() = runBlocking {
|
||||
val root = Files.createTempDirectory("lyng-cli-import-regression-launch")
|
||||
try {
|
||||
val mainFile = root.resolve("main.lyng")
|
||||
val port = java.net.ServerSocket(0).let {
|
||||
val selected = it.localPort
|
||||
it.close()
|
||||
selected
|
||||
}
|
||||
writeNestedLaunchImportBugTree(root)
|
||||
mainFile.writeText(
|
||||
"""
|
||||
import lyng.io.net
|
||||
import package1.alpha
|
||||
|
||||
val alpha = Alpha()
|
||||
alpha.startListen($port, "127.0.0.1")
|
||||
|
||||
delay(50)
|
||||
|
||||
val socket = Net.tcpConnect("127.0.0.1", $port)
|
||||
println("send ping...")
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
socket.close()
|
||||
|
||||
delay(50)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val result = runCli(mainFile.toString())
|
||||
assertTrue(result.err.isBlank(), result.err)
|
||||
assertFalse(result.out.contains("ERR [Alpha.startListen]"), result.out)
|
||||
assertFalse(result.out.contains("module capture 'Bravo'"), result.out)
|
||||
assertTrue(result.out.contains("bravo.doSomething()... OK"), result.out)
|
||||
assertEquals(2, Regex("Bravo\\.doSomething").findAll(result.out).count(), result.out)
|
||||
} finally {
|
||||
root.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import net.sergeych.jvmExitImpl
|
||||
import net.sergeych.runMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class CliAtExitJvmTest {
|
||||
private val originalOut: PrintStream = System.out
|
||||
private val originalErr: PrintStream = System.err
|
||||
|
||||
private class TestExit(val code: Int) : RuntimeException()
|
||||
|
||||
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
jvmExitImpl = { code -> throw TestExit(code) }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||
}
|
||||
|
||||
private fun runCli(vararg args: String): CliResult {
|
||||
val outBuf = ByteArrayOutputStream()
|
||||
val errBuf = ByteArrayOutputStream()
|
||||
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||
|
||||
var exitCode: Int? = null
|
||||
try {
|
||||
runMain(arrayOf(*args))
|
||||
} catch (e: TestExit) {
|
||||
exitCode = e.code
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||
}
|
||||
|
||||
private fun runScript(scriptText: String): CliResult {
|
||||
val tmp: Path = Files.createTempFile("lyng_atexit_", ".lyng")
|
||||
try {
|
||||
Files.writeString(tmp, scriptText)
|
||||
return runCli(tmp.toString())
|
||||
} finally {
|
||||
Files.deleteIfExists(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atExitRunsInRequestedOrderAndIgnoresHandlerExceptions() {
|
||||
val result = runScript(
|
||||
"""
|
||||
atExit {
|
||||
println("tail")
|
||||
}
|
||||
atExit(false) {
|
||||
println("head")
|
||||
throw Exception("ignored")
|
||||
}
|
||||
println("body")
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertNull(result.err.takeIf { it.isNotBlank() })
|
||||
assertNull(result.exitCode)
|
||||
val lines = result.out
|
||||
.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toList()
|
||||
assertEquals(listOf("body", "head", "tail"), lines)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atExitRunsBeforeScriptExitTerminatesProcess() {
|
||||
val result = runScript(
|
||||
"""
|
||||
atExit {
|
||||
println("cleanup")
|
||||
}
|
||||
exit(7)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
assertEquals(7, result.exitCode)
|
||||
assertTrue(result.out.lineSequence().any { it.trim() == "cleanup" })
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -131,18 +131,4 @@ class CliFmtJvmTest {
|
||||
Files.deleteIfExists(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inlineExecuteWithDashXStillWorksAndPassesArgv() {
|
||||
val r = runCli(
|
||||
"-x",
|
||||
"""println("INLINE"); println(ARGV[0]); println(ARGV[1])""",
|
||||
"one",
|
||||
"two"
|
||||
)
|
||||
assertTrue("Expected inline execution output", r.out.contains("INLINE"))
|
||||
assertTrue("Expected ARGV to include first trailing arg", r.out.contains("one"))
|
||||
assertTrue("Expected ARGV to include second trailing arg", r.out.contains("two"))
|
||||
assertTrue("Did not expect CLI exit()", r.exitCode == null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,237 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import net.sergeych.jvmExitImpl
|
||||
import net.sergeych.runMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
|
||||
class CliLocalImportsJvmTest {
|
||||
private val originalOut: PrintStream = System.out
|
||||
private val originalErr: PrintStream = System.err
|
||||
|
||||
private class TestExit(val code: Int) : RuntimeException()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
jvmExitImpl = { code -> throw TestExit(code) }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||
}
|
||||
|
||||
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||
|
||||
private fun runCli(vararg args: String): CliResult {
|
||||
val outBuf = ByteArrayOutputStream()
|
||||
val errBuf = ByteArrayOutputStream()
|
||||
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||
|
||||
var exitCode: Int? = null
|
||||
try {
|
||||
runMain(arrayOf(*args))
|
||||
} catch (e: TestExit) {
|
||||
exitCode = e.code
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||
}
|
||||
|
||||
private fun writeTransitiveImportTree(root: java.nio.file.Path) {
|
||||
val packageDir = Files.createDirectories(root.resolve("package1"))
|
||||
val nestedDir = Files.createDirectories(packageDir.resolve("nested"))
|
||||
|
||||
Files.writeString(
|
||||
packageDir.resolve("alpha.lyng"),
|
||||
"""
|
||||
package package1.alpha
|
||||
|
||||
import lyng.stdlib
|
||||
import lyng.io.net
|
||||
|
||||
class Alpha {
|
||||
val headers = Map<String, String>()
|
||||
|
||||
fun makeTask(port: Int, host: String): Deferred = launch {
|
||||
host + ":" + port
|
||||
}
|
||||
|
||||
fun netModule() = Net
|
||||
}
|
||||
|
||||
fun alphaValue() = "alpha"
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
packageDir.resolve("beta.lyng"),
|
||||
"""
|
||||
package package1.beta
|
||||
|
||||
import lyng.stdlib
|
||||
import package1.alpha
|
||||
|
||||
fun betaValue() = alphaValue() + "|beta"
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
nestedDir.resolve("gamma.lyng"),
|
||||
"""
|
||||
package package1.nested.gamma
|
||||
|
||||
import lyng.io.net
|
||||
import package1.alpha
|
||||
import package1.beta
|
||||
|
||||
val String.gammaTag get() = this + "|gamma"
|
||||
|
||||
fun gammaValue() = betaValue().gammaTag
|
||||
fun netModule() = Net
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
packageDir.resolve("entry.lyng"),
|
||||
"""
|
||||
package package1.entry
|
||||
|
||||
import lyng.stdlib
|
||||
import lyng.io.net
|
||||
import package1.alpha
|
||||
import package1.beta
|
||||
import package1.nested.gamma
|
||||
|
||||
fun report() = gammaValue() + "|entry"
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() {
|
||||
val dir = Files.createTempDirectory("lyng_cli_local_imports_")
|
||||
try {
|
||||
val mathDir = Files.createDirectories(dir.resolve("math"))
|
||||
val utilDir = Files.createDirectories(dir.resolve("util"))
|
||||
val mainFile = dir.resolve("main.lyng")
|
||||
Files.writeString(
|
||||
mathDir.resolve("add.lyng"),
|
||||
"""
|
||||
fun plus(a, b) = a + b
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
utilDir.resolve("answer.lyng"),
|
||||
"""
|
||||
package util.answer
|
||||
|
||||
import math.add
|
||||
|
||||
fun answer() = plus(40, 2)
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
mainFile,
|
||||
"""
|
||||
import util.answer
|
||||
|
||||
println(answer())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val result = runCli(mainFile.toString())
|
||||
assertTrue(result.err, result.err.isBlank())
|
||||
assertTrue(result.out, result.out.contains("42"))
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cliRejectsPackageThatDoesNotMatchRelativePath() {
|
||||
val dir = Files.createTempDirectory("lyng_cli_local_imports_badpkg_")
|
||||
try {
|
||||
val utilDir = Files.createDirectories(dir.resolve("util"))
|
||||
val mainFile = dir.resolve("main.lyng")
|
||||
Files.writeString(
|
||||
utilDir.resolve("answer.lyng"),
|
||||
"""
|
||||
package util.wrong
|
||||
|
||||
fun answer() = 42
|
||||
""".trimIndent()
|
||||
)
|
||||
Files.writeString(
|
||||
mainFile,
|
||||
"""
|
||||
import util.answer
|
||||
|
||||
println(answer())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val result = runCli(mainFile.toString())
|
||||
assertTrue(result.out, result.out.contains("local module package mismatch"))
|
||||
assertTrue(result.out, result.out.contains("expected 'util.answer'"))
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cliHandlesOverlappingDirectoryImportsWithTransitiveStdlibAndNetSymbols() {
|
||||
val dir = Files.createTempDirectory("lyng_cli_local_imports_transitive_")
|
||||
try {
|
||||
val mainFile = dir.resolve("main.lyng")
|
||||
writeTransitiveImportTree(dir)
|
||||
Files.writeString(
|
||||
mainFile,
|
||||
"""
|
||||
import package1.entry
|
||||
import package1.beta
|
||||
import package1.nested.gamma
|
||||
|
||||
println(report())
|
||||
println(gammaValue())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val result = runCli(mainFile.toString())
|
||||
assertTrue(result.err, result.err.isBlank())
|
||||
assertTrue(
|
||||
result.out,
|
||||
result.out.contains("alpha|beta|gamma|entry")
|
||||
)
|
||||
assertTrue(
|
||||
result.out,
|
||||
result.out.contains("alpha|beta|gamma")
|
||||
)
|
||||
} finally {
|
||||
dir.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import net.sergeych.jvmExitImpl
|
||||
import net.sergeych.runMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
class CliNetworkJvmTest {
|
||||
private val originalOut: PrintStream = System.out
|
||||
private val originalErr: PrintStream = System.err
|
||||
|
||||
private class TestExit(val code: Int) : RuntimeException()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
jvmExitImpl = { code -> throw TestExit(code) }
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
jvmExitImpl = { code -> kotlin.system.exitProcess(code) }
|
||||
}
|
||||
|
||||
private data class CliResult(val out: String, val err: String, val exitCode: Int?)
|
||||
|
||||
private fun runCli(vararg args: String): CliResult {
|
||||
val outBuf = ByteArrayOutputStream()
|
||||
val errBuf = ByteArrayOutputStream()
|
||||
System.setOut(PrintStream(outBuf, true, Charsets.UTF_8))
|
||||
System.setErr(PrintStream(errBuf, true, Charsets.UTF_8))
|
||||
|
||||
var exitCode: Int? = null
|
||||
try {
|
||||
runMain(arrayOf(*args))
|
||||
} catch (e: TestExit) {
|
||||
exitCode = e.code
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
}
|
||||
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cliHasAllNetworkingModulesInstalled() {
|
||||
val server = newServer()
|
||||
try {
|
||||
val script = """
|
||||
import lyng.io.http
|
||||
import lyng.io.http.server
|
||||
import lyng.io.ws
|
||||
import lyng.io.net
|
||||
|
||||
assert(Http.isSupported())
|
||||
assert(HttpServer() is HttpServer)
|
||||
println("ws=" + Ws.isSupported())
|
||||
println("net=" + Net.isSupported())
|
||||
|
||||
val home = Http.get("http://127.0.0.1:${server.address.port}/").text()
|
||||
val jsRef = "src=\"([^\"]*lyng-version\\.js)\"".re.find(home)
|
||||
require(jsRef != null, "lyng-version.js reference not found")
|
||||
|
||||
val versionJsPath = (jsRef as RegexMatch)[1]
|
||||
val versionJs = Http.get("http://127.0.0.1:${server.address.port}/" + versionJsPath).text()
|
||||
val versionMatch = "LYNG_VERSION\\s*=\\s*\"([^\"]+)\"".re.find(versionJs)
|
||||
require(versionMatch != null, "LYNG_VERSION assignment not found")
|
||||
|
||||
println("version=" + ((versionMatch as RegexMatch)[1]))
|
||||
""".trimIndent()
|
||||
|
||||
val result = runCli("-x", script)
|
||||
assertNull(result.exitCode)
|
||||
assertTrue(result.err, result.err.isBlank())
|
||||
assertTrue(result.out, result.out.contains("ws="))
|
||||
assertTrue(result.out, result.out.contains("net="))
|
||||
assertTrue(result.out, result.out.contains("version=9.9.9-test"))
|
||||
} finally {
|
||||
server.stop(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newServer(): HttpServer {
|
||||
val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
|
||||
server.createContext("/") { exchange ->
|
||||
when (exchange.requestURI.path) {
|
||||
"/" -> writeResponse(
|
||||
exchange,
|
||||
200,
|
||||
"""
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="lyng-version.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<span id="lyng-version-ribbon"></span>
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
"/lyng-version.js" -> writeResponse(exchange, 200, """window.LYNG_VERSION = "9.9.9-test";""")
|
||||
else -> writeResponse(exchange, 404, "missing")
|
||||
}
|
||||
}
|
||||
server.start()
|
||||
return server
|
||||
}
|
||||
|
||||
private fun writeResponse(exchange: HttpExchange, status: Int, body: String) {
|
||||
val bytes = body.toByteArray()
|
||||
exchange.sendResponseHeaders(status, bytes.size.toLong())
|
||||
exchange.responseBody.use { out ->
|
||||
out.write(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,12 +31,11 @@ class FsIntegrationJvmTest {
|
||||
val dir = createTempDirectory("lyng_cli_fs_test_")
|
||||
try {
|
||||
val file = dir.resolve("hello.txt")
|
||||
val filePath = file.toString().replace("\\", "\\\\")
|
||||
// Drive the operation via Lyng code to validate bindings end-to-end
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.io.fs
|
||||
val p = Path("${filePath}")
|
||||
val p = Path("${'$'}{file}")
|
||||
p.writeUtf8("hello from cli test")
|
||||
assertEquals(true, p.exists())
|
||||
assertEquals("hello from cli test", p.readUtf8())
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
import platform.posix.O_CREAT
|
||||
import platform.posix.O_TRUNC
|
||||
import platform.posix.O_WRONLY
|
||||
import platform.posix.SIGTERM
|
||||
import platform.posix._exit
|
||||
import platform.posix.close
|
||||
import platform.posix.dup2
|
||||
import platform.posix.execvp
|
||||
import platform.posix.fork
|
||||
import platform.posix.getenv
|
||||
import platform.posix.getpid
|
||||
import platform.posix.kill
|
||||
import platform.posix.open
|
||||
import platform.posix.usleep
|
||||
import platform.posix.waitpid
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
class CliAtExitLinuxNativeTest {
|
||||
@Test
|
||||
fun atExitRunsOnSigtermForNativeCli() {
|
||||
val executable = getenv("LYNG_CLI_NATIVE_BIN")?.toKString()
|
||||
?: error("LYNG_CLI_NATIVE_BIN is not set")
|
||||
val fs = FileSystem.SYSTEM
|
||||
val tempDir = "/tmp/lyng_cli_native_${getpid()}_${kotlin.random.Random.nextInt()}".toPath()
|
||||
val scriptPath = tempDir / "sigterm.lyng"
|
||||
val stdoutPath = tempDir / "stdout.txt"
|
||||
val stderrPath = tempDir / "stderr.txt"
|
||||
|
||||
fs.createDirectories(tempDir)
|
||||
try {
|
||||
fs.write(scriptPath) {
|
||||
writeUtf8(
|
||||
"""
|
||||
atExit {
|
||||
println("cleanup-native")
|
||||
}
|
||||
while(true) {
|
||||
yield()
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
val pid = launchCli(executable, scriptPath, stdoutPath, stderrPath)
|
||||
usleep(300_000u)
|
||||
assertEquals(0, kill(pid, SIGTERM), "failed to send SIGTERM")
|
||||
|
||||
val status = waitForPid(pid)
|
||||
val exitCode = if ((status and 0x7f) == 0) (status shr 8) and 0xff else -1
|
||||
val stdout = readUtf8IfExists(fs, stdoutPath)
|
||||
val stderr = readUtf8IfExists(fs, stderrPath)
|
||||
|
||||
assertEquals(143, exitCode, "unexpected native CLI exit status; stderr=$stderr")
|
||||
assertTrue(stdout.contains("cleanup-native"), "stdout did not contain cleanup marker. stdout=$stdout stderr=$stderr")
|
||||
} finally {
|
||||
fs.deleteRecursively(tempDir, mustExist = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readUtf8IfExists(fs: FileSystem, path: Path): String {
|
||||
return if (fs.exists(path)) {
|
||||
fs.read(path) { readUtf8() }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForPid(pid: Int): Int = memScoped {
|
||||
val status = alloc<IntVar>()
|
||||
val waited = waitpid(pid, status.ptr, 0)
|
||||
check(waited == pid) { "waitpid failed for $pid" }
|
||||
status.value
|
||||
}
|
||||
|
||||
private fun launchCli(
|
||||
executable: String,
|
||||
scriptPath: Path,
|
||||
stdoutPath: Path,
|
||||
stderrPath: Path
|
||||
): Int = memScoped {
|
||||
val pid = fork()
|
||||
check(pid >= 0) { "fork failed" }
|
||||
if (pid == 0) {
|
||||
val stdoutFd = open(stdoutPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
val stderrFd = open(stderrPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
if (stdoutFd < 0 || stderrFd < 0) {
|
||||
_exit(2)
|
||||
}
|
||||
dup2(stdoutFd, 1)
|
||||
dup2(stderrFd, 2)
|
||||
close(stdoutFd)
|
||||
close(stderrFd)
|
||||
|
||||
val argv = allocArray<CPointerVar<ByteVar>>(3)
|
||||
argv[0] = executable.cstr.ptr
|
||||
argv[1] = scriptPath.toString().cstr.ptr
|
||||
argv[2] = null
|
||||
execvp(executable, argv)
|
||||
_exit(127)
|
||||
}
|
||||
pid
|
||||
}
|
||||
}
|
||||
@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng_cli
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
import platform.posix.O_CREAT
|
||||
import platform.posix.O_TRUNC
|
||||
import platform.posix.O_WRONLY
|
||||
import platform.posix.SIGKILL
|
||||
import platform.posix.SIGSEGV
|
||||
import platform.posix._exit
|
||||
import platform.posix.close
|
||||
import platform.posix.dup2
|
||||
import platform.posix.execvp
|
||||
import platform.posix.fork
|
||||
import platform.posix.getenv
|
||||
import platform.posix.getpid
|
||||
import platform.posix.kill
|
||||
import platform.posix.open
|
||||
import platform.posix.usleep
|
||||
import platform.posix.waitpid
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
class CliWebSocketNativeRegressionTest {
|
||||
@Test
|
||||
fun releaseCliDoesNotSegfaultOnConcurrentWebSocketClients() {
|
||||
val executable = getenv("LYNG_CLI_NATIVE_RELEASE_BIN")?.toKString()
|
||||
?: error("LYNG_CLI_NATIVE_RELEASE_BIN is not set")
|
||||
val fs = FileSystem.SYSTEM
|
||||
val repoRoot = ascend(executable.toPath(), 6)
|
||||
val scriptPath = repoRoot / "bugs" / "ws-segfault.lyng"
|
||||
check(fs.exists(scriptPath)) { "bug repro script not found at $scriptPath" }
|
||||
|
||||
val tempDir = "/tmp/lyng_ws_native_${getpid()}_${kotlin.random.Random.nextInt()}".toPath()
|
||||
val stdoutPath = tempDir / "stdout.txt"
|
||||
val stderrPath = tempDir / "stderr.txt"
|
||||
|
||||
fs.createDirectories(tempDir)
|
||||
try {
|
||||
val pid = launchCli(executable, scriptPath, stdoutPath, stderrPath)
|
||||
usleep(5_000_000u)
|
||||
|
||||
if (kill(pid, 0) == 0) {
|
||||
kill(pid, SIGKILL)
|
||||
}
|
||||
|
||||
val status = waitForPid(pid)
|
||||
val termSignal = status and 0x7f
|
||||
val stdout = readUtf8IfExists(fs, stdoutPath)
|
||||
val stderr = readUtf8IfExists(fs, stderrPath)
|
||||
val allOutput = "$stdout\n$stderr"
|
||||
|
||||
assertFalse(termSignal == SIGSEGV, "native CLI crashed with SIGSEGV. Output:\n$allOutput")
|
||||
assertTrue(
|
||||
stdout.lineSequence().count { it == "test send to ws://127.0.0.1:9998... OK" } == 2,
|
||||
"expected both websocket clients to finish. Output:\n$allOutput"
|
||||
)
|
||||
assertFalse(allOutput.contains("Segmentation fault"), "process output reported a segmentation fault:\n$allOutput")
|
||||
} finally {
|
||||
fs.deleteRecursively(tempDir, mustExist = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ascend(path: Path, levels: Int): Path {
|
||||
var current = path
|
||||
repeat(levels) {
|
||||
current = current.parent ?: error("cannot ascend $levels levels from $path")
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private fun readUtf8IfExists(fs: FileSystem, path: Path): String {
|
||||
return if (fs.exists(path)) fs.read(path) { readUtf8() } else ""
|
||||
}
|
||||
|
||||
private fun waitForPid(pid: Int): Int = memScoped {
|
||||
val status = alloc<IntVar>()
|
||||
val waited = waitpid(pid, status.ptr, 0)
|
||||
check(waited == pid) { "waitpid failed for $pid" }
|
||||
status.value
|
||||
}
|
||||
|
||||
private fun launchCli(
|
||||
executable: String,
|
||||
scriptPath: Path,
|
||||
stdoutPath: Path,
|
||||
stderrPath: Path,
|
||||
): Int = memScoped {
|
||||
val pid = fork()
|
||||
check(pid >= 0) { "fork failed" }
|
||||
if (pid == 0) {
|
||||
val stdoutFd = open(stdoutPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
val stderrFd = open(stderrPath.toString(), O_WRONLY or O_CREAT or O_TRUNC, 0x1A4)
|
||||
if (stdoutFd < 0 || stderrFd < 0) {
|
||||
_exit(2)
|
||||
}
|
||||
dup2(stdoutFd, 1)
|
||||
dup2(stderrFd, 2)
|
||||
close(stdoutFd)
|
||||
close(stderrFd)
|
||||
|
||||
val argv = allocArray<CPointerVar<ByteVar>>(3)
|
||||
argv[0] = executable.cstr.ptr
|
||||
argv[1] = scriptPath.toString().cstr.ptr
|
||||
argv[2] = null
|
||||
execvp(executable, argv)
|
||||
_exit(127)
|
||||
}
|
||||
pid
|
||||
}
|
||||
}
|
||||
@ -22,40 +22,11 @@
|
||||
package net.sergeych
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
import platform.posix.fgets
|
||||
import platform.posix.pclose
|
||||
import platform.posix.popen
|
||||
import platform.posix.signal
|
||||
import platform.posix.atexit
|
||||
import platform.posix.SIGINT
|
||||
import platform.posix.SIGHUP
|
||||
import platform.posix.SIGTERM
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@ThreadLocal
|
||||
private var activeCliRuntime: CliExecutionRuntime? = null
|
||||
|
||||
@ThreadLocal
|
||||
private var nativeCliHooksInstalled: Boolean = false
|
||||
|
||||
private fun installNativeCliHooksOnce() {
|
||||
if (nativeCliHooksInstalled) return
|
||||
nativeCliHooksInstalled = true
|
||||
atexit(staticCFunction(::nativeCliAtExit))
|
||||
signal(SIGTERM, staticCFunction(::nativeCliSignalHandler))
|
||||
signal(SIGINT, staticCFunction(::nativeCliSignalHandler))
|
||||
signal(SIGHUP, staticCFunction(::nativeCliSignalHandler))
|
||||
}
|
||||
|
||||
private fun nativeCliAtExit() {
|
||||
activeCliRuntime?.shutdownBlocking()
|
||||
}
|
||||
|
||||
private fun nativeCliSignalHandler(signal: Int) {
|
||||
exitProcess(128 + signal)
|
||||
}
|
||||
|
||||
actual class ShellCommandExecutor() {
|
||||
actual fun executeCommand(command: String): CommandResult {
|
||||
val outputBuilder = StringBuilder()
|
||||
@ -91,24 +62,6 @@ actual class ShellCommandExecutor() {
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class CliPlatformShutdownHooks private constructor(
|
||||
private val runtime: CliExecutionRuntime
|
||||
) {
|
||||
actual fun uninstall() {
|
||||
if (activeCliRuntime === runtime) {
|
||||
activeCliRuntime = null
|
||||
}
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks {
|
||||
installNativeCliHooksOnce()
|
||||
activeCliRuntime = runtime
|
||||
return CliPlatformShutdownHooks(runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual fun exit(code: Int) {
|
||||
exitProcess(code)
|
||||
}
|
||||
}
|
||||
@ -31,33 +31,6 @@ plugins {
|
||||
group = "net.sergeych"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
private fun Project.sqliteLinuxLinkerOpts(vararg defaultDirs: String): List<String> {
|
||||
val overrideDir = providers.gradleProperty("sqlite3.lib.dir").orNull
|
||||
?: providers.environmentVariable("SQLITE3_LIB_DIR").orNull
|
||||
val candidateDirs = buildList {
|
||||
if (!overrideDir.isNullOrBlank()) {
|
||||
add(file(overrideDir))
|
||||
}
|
||||
defaultDirs.forEach { add(file(it)) }
|
||||
}.distinctBy { it.absolutePath }
|
||||
|
||||
val discoveredLib = sequenceOf("libsqlite3.so", "libsqlite3.so.0")
|
||||
.mapNotNull { libraryName ->
|
||||
candidateDirs.firstOrNull { it.resolve(libraryName).isFile }?.let { dir ->
|
||||
listOf("-L${dir.absolutePath}", "-l:$libraryName")
|
||||
}
|
||||
}
|
||||
.firstOrNull()
|
||||
?: listOf("-lsqlite3")
|
||||
|
||||
return discoveredLib + listOf(
|
||||
"-ldl",
|
||||
"-lpthread",
|
||||
"-lm",
|
||||
"-Wl,--allow-shlib-undefined"
|
||||
)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvm()
|
||||
@ -71,6 +44,7 @@ kotlin {
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
macosX64()
|
||||
macosArm64()
|
||||
mingwX64()
|
||||
linuxX64()
|
||||
@ -85,46 +59,11 @@ kotlin {
|
||||
// nodejs()
|
||||
// }
|
||||
|
||||
targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).configureEach {
|
||||
compilations.getByName("main").cinterops.create("sqlite3") {
|
||||
defFile(project.file("src/nativeInterop/cinterop/sqlite/sqlite3.def"))
|
||||
packageName("net.sergeych.lyng.io.db.sqlite.cinterop")
|
||||
includeDirs(project.file("src/nativeInterop/cinterop/sqlite"))
|
||||
}
|
||||
binaries.all {
|
||||
when (konanTarget.name) {
|
||||
"linux_x64" -> linkerOpts(
|
||||
*project.sqliteLinuxLinkerOpts(
|
||||
"/lib/x86_64-linux-gnu",
|
||||
"/usr/lib/x86_64-linux-gnu",
|
||||
"/lib64",
|
||||
"/usr/lib64",
|
||||
"/lib",
|
||||
"/usr/lib"
|
||||
).toTypedArray()
|
||||
)
|
||||
"linux_arm64" -> linkerOpts(
|
||||
*project.sqliteLinuxLinkerOpts(
|
||||
"/lib/aarch64-linux-gnu",
|
||||
"/usr/lib/aarch64-linux-gnu",
|
||||
"/lib64",
|
||||
"/usr/lib64",
|
||||
"/lib",
|
||||
"/usr/lib"
|
||||
).toTypedArray()
|
||||
)
|
||||
else -> linkerOpts("-lsqlite3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep expect/actual warning suppressed consistently with other modules
|
||||
targets.configureEach {
|
||||
compilations.configureEach {
|
||||
compileTaskProvider.configure {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
compilerOptions.configure {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,64 +79,13 @@ kotlin {
|
||||
api(libs.okio)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.mordant.core)
|
||||
api(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
}
|
||||
}
|
||||
val nativeMain by creating {
|
||||
dependsOn(commonMain)
|
||||
dependencies {
|
||||
implementation(libs.ktor.network)
|
||||
}
|
||||
}
|
||||
val darwinMain by creating {
|
||||
dependsOn(nativeMain)
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.darwin)
|
||||
}
|
||||
}
|
||||
val iosMain by creating {
|
||||
dependsOn(darwinMain)
|
||||
}
|
||||
val linuxMain by creating {
|
||||
dependsOn(nativeMain)
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.curl)
|
||||
}
|
||||
}
|
||||
val macosMain by creating {
|
||||
dependsOn(darwinMain)
|
||||
}
|
||||
val mingwMain by creating {
|
||||
dependsOn(nativeMain)
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.winhttp)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.testcontainers)
|
||||
implementation(libs.testcontainers.postgresql)
|
||||
}
|
||||
}
|
||||
val linuxTest by creating {
|
||||
dependsOn(commonTest)
|
||||
}
|
||||
val iosX64Main by getting { dependsOn(iosMain) }
|
||||
val iosArm64Main by getting { dependsOn(iosMain) }
|
||||
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
|
||||
val macosArm64Main by getting { dependsOn(macosMain) }
|
||||
val mingwX64Main by getting { dependsOn(mingwMain) }
|
||||
val linuxX64Main by getting { dependsOn(linuxMain) }
|
||||
val linuxArm64Main by getting { dependsOn(linuxMain) }
|
||||
val linuxX64Test by getting { dependsOn(linuxTest) }
|
||||
val linuxArm64Test by getting { dependsOn(linuxTest) }
|
||||
|
||||
// JS: use runtime detection in jsMain to select Node vs Browser implementation
|
||||
val jsMain by getting {
|
||||
@ -205,13 +93,6 @@ kotlin {
|
||||
api(libs.okio)
|
||||
implementation(libs.okio.fakefilesystem)
|
||||
implementation("com.squareup.okio:okio-nodefilesystem:${libs.versions.okioVersion.get()}")
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.network)
|
||||
}
|
||||
}
|
||||
val jvmMain by getting {
|
||||
@ -219,12 +100,6 @@ kotlin {
|
||||
implementation(libs.mordant.jvm.jna)
|
||||
implementation("org.jline:jline-reader:3.29.0")
|
||||
implementation("org.jline:jline-terminal:3.29.0")
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.network)
|
||||
implementation(libs.sqlite.jdbc)
|
||||
implementation(libs.h2)
|
||||
implementation(libs.postgresql)
|
||||
implementation(libs.hikaricp)
|
||||
}
|
||||
}
|
||||
// // For Wasm we use in-memory VFS for now
|
||||
@ -237,10 +112,10 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GenerateLyngioDecls : DefaultTask() {
|
||||
@get:InputDirectory
|
||||
abstract class GenerateLyngioConsoleDecls : DefaultTask() {
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val sourceDir: DirectoryProperty
|
||||
abstract val sourceFile: RegularFileProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
@ -250,9 +125,9 @@ abstract class GenerateLyngioDecls : DefaultTask() {
|
||||
val targetPkg = "net.sergeych.lyngio.stdlib_included"
|
||||
val pkgPath = targetPkg.replace('.', '/')
|
||||
val targetDir = outputDir.get().asFile.resolve(pkgPath)
|
||||
if (targetDir.exists()) targetDir.deleteRecursively()
|
||||
targetDir.mkdirs()
|
||||
|
||||
val text = sourceFile.get().asFile.readText()
|
||||
fun escapeForQuoted(s: String): String = buildString {
|
||||
for (ch in s) when (ch) {
|
||||
'\\' -> append("\\\\")
|
||||
@ -267,39 +142,30 @@ abstract class GenerateLyngioDecls : DefaultTask() {
|
||||
val out = buildString {
|
||||
append("package ").append(targetPkg).append("\n\n")
|
||||
append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
|
||||
sourceDir.get().asFile
|
||||
.listFiles { file -> file.isFile && file.extension == "lyng" }
|
||||
?.sortedBy { it.name }
|
||||
?.forEach { file ->
|
||||
val propertyName = buildString {
|
||||
append(file.nameWithoutExtension)
|
||||
append("Lyng")
|
||||
}
|
||||
append("internal val ").append(propertyName).append(" = \"")
|
||||
append(escapeForQuoted(file.readText()))
|
||||
append("\"\n")
|
||||
}
|
||||
append("internal val consoleLyng = \"")
|
||||
append(escapeForQuoted(text))
|
||||
append("\"\n")
|
||||
}
|
||||
targetDir.resolve("lyngio_types_lyng.generated.kt").writeText(out)
|
||||
targetDir.resolve("console_types_lyng.generated.kt").writeText(out)
|
||||
}
|
||||
}
|
||||
|
||||
val lyngioDeclsDir = layout.projectDirectory.dir("stdlib/lyng/io")
|
||||
val lyngioConsoleDeclsFile = layout.projectDirectory.file("stdlib/lyng/io/console.lyng")
|
||||
val generatedLyngioDeclsDir = layout.buildDirectory.dir("generated/source/lyngioDecls/commonMain/kotlin")
|
||||
|
||||
val generateLyngioDecls by tasks.registering(GenerateLyngioDecls::class) {
|
||||
sourceDir.set(lyngioDeclsDir)
|
||||
val generateLyngioConsoleDecls by tasks.registering(GenerateLyngioConsoleDecls::class) {
|
||||
sourceFile.set(lyngioConsoleDeclsFile)
|
||||
outputDir.set(generatedLyngioDeclsDir)
|
||||
}
|
||||
|
||||
kotlin.sourceSets.named("commonMain") {
|
||||
kotlin.srcDir(generateLyngioDecls)
|
||||
kotlin.srcDir(generateLyngioConsoleDecls)
|
||||
}
|
||||
|
||||
kotlin.targets.configureEach {
|
||||
compilations.configureEach {
|
||||
compileTaskProvider.configure {
|
||||
dependsOn(generateLyngioDecls)
|
||||
dependsOn(generateLyngioConsoleDecls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user