Compare commits
111 Commits
bigdecimal
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 30e56946a0 | |||
| 49fc700233 | |||
| 07cb5a519c | |||
| 8f66cd7680 | |||
| bb2119b1d1 | |||
| 64273ac60a | |||
| ee392daa13 | |||
| 0affa92674 | |||
| a2e3f80ab6 | |||
| f8d2533b48 | |||
| bb9af2258b | |||
| b3be908242 | |||
| 14dc73db3e | |||
| f9bbdd56bf | |||
| 04e80c384e | |||
| 6d340824e4 | |||
| 5f6f6b9ae4 | |||
| 55ba6113e7 | |||
| b42ceec686 | |||
| fada848907 | |||
| e925195495 | |||
| 3b6bdda0a4 | |||
| ab39110834 | |||
| 47eb2d7d61 | |||
| eefa4c8f1a | |||
| 6171b0d95f | |||
| a1ea09440d | |||
| 88ce04102a | |||
| 0e73d80707 | |||
| 121d860043 | |||
| 717758149b | |||
| 73ec8f9be6 | |||
| 805c54a408 | |||
| 9b90fe370b | |||
| 840cd32574 | |||
| fbb1683ba3 | |||
| c9eb3df93d | |||
| 402b8bb1b3 | |||
| aa1b74620e | |||
| ef95ed4405 | |||
| b6c6ef021a | |||
| c6cfd52b01 | |||
| 12fb4fe0ba | |||
| 368ce2ce8c | |||
| d0d79d2f07 | |||
| 69013392d3 | |||
| f145a90845 | |||
| 2f145a0ea7 | |||
| 15617f6998 | |||
| 583067780f | |||
| 8386337c42 | |||
| 83099148bd | |||
| 1fca2b112f | |||
| 35628c8453 | |||
| 671583638b | |||
| 214f1aec9e | |||
| 657c0c5d18 | |||
| 90718c3c17 | |||
| d8454a11fc | |||
| 161f3f74e2 | |||
| 1cead7822a | |||
| 064b927b1a | |||
| a7ab0d3905 | |||
| f1003f5b95 | |||
| d8c53c500e | |||
| b3efe019d9 | |||
| 2414da59a7 | |||
| fd6d05d568 | |||
| caad7d8ab9 | |||
| 88b0bb2147 | |||
| 38646c6257 | |||
| f61ac35580 | |||
| 9bee0aed5b | |||
| cd7e001f41 | |||
| 62520f6203 | |||
| d0aaa2c256 | |||
| 5346d15a9f | |||
| 7578128689 | |||
| 71243b6bde | |||
| 7b65ff9d0e | |||
| d409a4bb8b | |||
| f168e9f6ed | |||
| a051280e0c | |||
| fc01016a74 | |||
| 446c8d9a6e | |||
| c140567e0c | |||
| 311cf6ee44 | |||
| 3e338f3d53 | |||
| c097464750 | |||
| 6e9333844e | |||
| a976702caf | |||
| 850efedb72 | |||
| f845213332 | |||
| cdd48ec871 | |||
| 286ec30422 | |||
| 8a560f5417 | |||
| 966b6a31ae | |||
| c3c0a3292b | |||
| aa9565b40b | |||
| 05d7432b37 | |||
| c7c333b71a | |||
| e55d9c835a | |||
| 83d8c8b71f | |||
| a72991d1b7 | |||
| cd007050a8 | |||
| 418b1ae2b6 | |||
| b0fb65a036 | |||
| 86e8b2e2bc | |||
| a98fe51983 | |||
| f8d36beae1 | |||
| 5ab074ba9e |
4
.gitignore
vendored
4
.gitignore
vendored
@ -27,3 +27,7 @@ debug.log
|
|||||||
/compile_jvm_output.txt
|
/compile_jvm_output.txt
|
||||||
/compile_metadata_output.txt
|
/compile_metadata_output.txt
|
||||||
test_output*.txt
|
test_output*.txt
|
||||||
|
/site/src/version-template/lyng-version.js
|
||||||
|
/bugcontents.db
|
||||||
|
/bugs/
|
||||||
|
contents.db
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
- 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).
|
- 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.
|
- 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.
|
- 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.
|
- Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead.
|
||||||
|
|
||||||
|
|||||||
42
CHANGELOG.md
42
CHANGELOG.md
@ -7,6 +7,48 @@ History note:
|
|||||||
- Entries below are synchronized and curated for `1.5.x`.
|
- 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.
|
- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Database access
|
||||||
|
- Added the portable `lyng.io.db` SQL contract and the first concrete provider, `lyng.io.db.sqlite`.
|
||||||
|
- Added SQLite support on JVM and Linux Native with:
|
||||||
|
- generic `openDatabase("sqlite:...")` dispatch
|
||||||
|
- typed `openSqlite(...)` helper
|
||||||
|
- real nested transactions via savepoints
|
||||||
|
- generated keys through `ExecutionResult.getGeneratedKeys()`
|
||||||
|
- strict schema-driven value conversion for `Bool`, `Decimal`, `Date`, `DateTime`, and `Instant`
|
||||||
|
- documented option handling for `readOnly`, `createIfMissing`, `foreignKeys`, and `busyTimeoutMillis`
|
||||||
|
- Added public docs for database usage and SQLite provider behavior.
|
||||||
|
|
||||||
|
### Time
|
||||||
|
- Added `Date` to `lyng.time` and the core library as a first-class calendar-date type.
|
||||||
|
- Added `Instant.toDate(...)`, `DateTime.date`, `DateTime.toDate()`, `Date.toDateTime(...)`, and related date arithmetic.
|
||||||
|
- Added docs, stdlib reference updates, serialization support, and comprehensive tests for `Date`.
|
||||||
|
|
||||||
|
### Release notes
|
||||||
|
- Full `:lyngio:jvmTest` and `:lyngio:linuxX64Test` pass on the release tree after SQLite hardening.
|
||||||
|
|
||||||
|
## 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)
|
## 1.5.1 (2026-03-25)
|
||||||
|
|
||||||
### Language
|
### Language
|
||||||
|
|||||||
80
README.md
80
README.md
@ -48,9 +48,12 @@ assertEquals(A.E.One, A.One)
|
|||||||
|
|
||||||
- [Language home](https://lynglang.com)
|
- [Language home](https://lynglang.com)
|
||||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||||
|
- [Latest release notes (1.5.4)](docs/whats_new.md)
|
||||||
- [What's New in 1.5](docs/whats_new_1_5.md)
|
- [What's New in 1.5](docs/whats_new_1_5.md)
|
||||||
- [Testing and Assertions](docs/Testing.md)
|
- [Testing and Assertions](docs/Testing.md)
|
||||||
- [Filesystem and Processes (lyngio)](docs/lyngio.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)
|
- [Return Statement](docs/return_statement.md)
|
||||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||||
- [Samples directory](docs/samples)
|
- [Samples directory](docs/samples)
|
||||||
@ -63,8 +66,7 @@ assertEquals(A.E.One, A.One)
|
|||||||
### Add dependency to your project
|
### Add dependency to your project
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// update to current please:
|
val lyngVersion = "1.5.4"
|
||||||
val lyngVersion = "1.5.0-SNAPSHOT"
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
// ...
|
// ...
|
||||||
@ -93,42 +95,49 @@ import net.sergeych.lyng.*
|
|||||||
// we need a coroutine to start, as Lyng
|
// we need a coroutine to start, as Lyng
|
||||||
// is a coroutine based language, async topdown
|
// is a coroutine based language, async topdown
|
||||||
runBlocking {
|
runBlocking {
|
||||||
assert(5 == eval(""" 3*3 - 4 """).toInt())
|
val session = EvalSession()
|
||||||
eval(""" println("Hello, Lyng!") """)
|
assert(5 == session.eval(""" 3*3 - 4 """).toInt())
|
||||||
|
session.eval(""" println("Hello, Lyng!") """)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exchanging information
|
### Exchanging information
|
||||||
|
|
||||||
Script is executed over some `Scope`. Create instance,
|
The preferred host runtime is `EvalSession`. It owns the script scope and any coroutines
|
||||||
add your specific vars and functions to it, and call:
|
started with `launch { ... }`. Create a session, grab its scope when you need low-level
|
||||||
|
binding APIs, then execute scripts through the session:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
|
||||||
// simple function
|
runBlocking {
|
||||||
val scope = Script.newScope().apply {
|
val session = EvalSession()
|
||||||
addFn("sumOf") {
|
val scope = session.getScope().apply {
|
||||||
var sum = 0.0
|
// simple function
|
||||||
for (a in args) sum += a.toDouble()
|
addFn("sumOf") {
|
||||||
ObjReal(sum)
|
var sum = 0.0
|
||||||
}
|
for (a in args) sum += a.toDouble()
|
||||||
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
ObjReal(sum)
|
||||||
|
}
|
||||||
|
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
||||||
|
|
||||||
// callback back to kotlin to some suspend fn, for example::
|
// callback back to kotlin to some suspend fn, for example::
|
||||||
// suspend fun doSomeWork(text: String): Int
|
// suspend fun doSomeWork(text: String): Int
|
||||||
addFn("doSomeWork") {
|
addFn("doSomeWork") {
|
||||||
// this _is_ a suspend lambda, we can call suspend function,
|
// this _is_ a suspend lambda, we can call suspend function,
|
||||||
// and it won't consume the thread.
|
// and it won't consume the thread.
|
||||||
// note that in kotlin handler, `args` is a list of `Obj` arguments
|
// note that in kotlin handler, `args` is a list of `Obj` arguments
|
||||||
// and return value from this lambda should be Obj too:
|
// and return value from this lambda should be Obj too:
|
||||||
doSomeWork(args[0]).toObj()
|
doSomeWork(args[0]).toObj()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute through the session:
|
||||||
|
session.eval("sumOf(1,2,3)") // <- 6
|
||||||
}
|
}
|
||||||
// adding constant:
|
|
||||||
scope.eval("sumOf(1,2,3)") // <- 6
|
|
||||||
```
|
```
|
||||||
Note that the scope stores all changes in it so you can make calls on a single scope to preserve state between calls.
|
Note that the 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.
|
||||||
|
|
||||||
## IntelliJ IDEA plugin: Lightweight autocompletion (experimental)
|
## IntelliJ IDEA plugin: Lightweight autocompletion (experimental)
|
||||||
|
|
||||||
@ -177,8 +186,7 @@ Designed to add scripting to kotlin multiplatform application in easy and effici
|
|||||||
|
|
||||||
# Language Roadmap
|
# Language Roadmap
|
||||||
|
|
||||||
We are now at **v1.5.0-SNAPSHOT** (stable development cycle): basic optimization performed, battery included: standard library is 90% here, initial
|
The current stable release is **v1.5.4**: the 1.5 cycle is feature-complete, compiler/runtime stabilization work is in, and the language, tooling, and site are aligned around the current release.
|
||||||
support in HTML, popular editors, and IDEA; tools to syntax highlight and format code are ready. It was released closed to schedule.
|
|
||||||
|
|
||||||
Ready features:
|
Ready features:
|
||||||
|
|
||||||
@ -215,23 +223,21 @@ Ready features:
|
|||||||
- [x] assign-if-null operator `?=`
|
- [x] assign-if-null operator `?=`
|
||||||
- [x] user-defined exception classes
|
- [x] user-defined exception classes
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## plan: towards v2.0 Next Generation
|
## plan: towards v2.0 Next Generation
|
||||||
|
|
||||||
- [x] site with integrated interpreter to give a try
|
- [x] site with integrated interpreter to give a try
|
||||||
- [x] kotlin part public API good docs, integration focused
|
- [x] kotlin part public API good docs, integration focused
|
||||||
- [ ] type specifications
|
- [x] type specifications
|
||||||
- [x] Textmate Bundle
|
- [x] Textmate Bundle
|
||||||
- [x] IDEA plugin
|
- [x] IDEA plugin
|
||||||
- [ ] source docs and maybe lyng.md to a standard
|
- [x] source docs and maybe lyng.md to a standard
|
||||||
- [ ] metadata first class access from lyng
|
|
||||||
- [x] aggressive optimizations
|
- [x] aggressive optimizations
|
||||||
- [ ] compile to JVM bytecode optimization
|
|
||||||
|
|
||||||
## After 1.5 "Ideal scripting"
|
## After 1.5 "Ideal scripting"
|
||||||
|
|
||||||
Estimated summer 2026
|
* __we are here now ;)__
|
||||||
|
|
||||||
- propose your feature!
|
- propose your feature!
|
||||||
|
|
||||||
@ -239,8 +245,12 @@ Estimated summer 2026
|
|||||||
|
|
||||||
@-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.
|
@-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.
|
||||||
|
|
||||||
__Sergey Chernov__ @sergeych: Initial idea and architecture, language concept, design, implementation.
|
<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.
|
||||||
|
|
||||||
|
|
||||||
__Yulia Nezhinskaya__ @AlterEgoJuliaN: System analysis, math and features design.
|
|
||||||
|
|
||||||
[parallelism]: docs/parallelism.md
|
[parallelism]: docs/parallelism.md
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
set -e
|
set -e
|
||||||
echo "publishing all artifacts"
|
echo "publishing all artifacts"
|
||||||
echo
|
echo
|
||||||
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel
|
./gradlew publishToMavenLocal site:jsBrowserDistribution publish buildInstallablePlugin :lyng:linkReleaseExecutableLinuxX64 :lyng:installJvmDist --parallel --no-configuration-cache
|
||||||
|
|
||||||
#echo
|
#echo
|
||||||
#echo "Creating plugin"
|
#echo "Creating plugin"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
# Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -19,13 +19,15 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
root=./lyng/build/install/lyng-jvm/
|
archive=./lyng/build/distributions/lyng-jvm.zip
|
||||||
|
install_root="$HOME/bin/jlyng-jvm"
|
||||||
|
launcher="$install_root/lyng-jvm/bin/lyng"
|
||||||
|
|
||||||
./gradlew :lyng:installJvmDist
|
./gradlew :lyng:jvmDistZip
|
||||||
#strip $file
|
mkdir -p ./distributables
|
||||||
#upx $file
|
cp "$archive" ./distributables/lyng-jvm.zip
|
||||||
rm -rf ~/bin/jlyng-jvm || true
|
rm -rf "$install_root" || true
|
||||||
rm ~/bin/jlyng 2>/dev/null || true
|
rm "$HOME/bin/jlyng" 2>/dev/null || true
|
||||||
mkdir -p ~/bin/jlyng-jvm
|
mkdir -p "$install_root"
|
||||||
cp -R $root ~/bin/jlyng-jvm
|
unzip -q ./distributables/lyng-jvm.zip -d "$install_root"
|
||||||
ln -s ~/bin/jlyng-jvm/lyng-jvm/bin/lyng ~/bin/jlyng
|
ln -s "$launcher" "$HOME/bin/jlyng"
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
# 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,6 +4,13 @@ It's an interface if the [Collection] that provides indexing access, like `array
|
|||||||
Array therefore implements [Iterable] too. Well known implementations of `Array` are
|
Array therefore implements [Iterable] too. Well known implementations of `Array` are
|
||||||
[List] and [ImmutableList].
|
[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:
|
Array adds the following methods:
|
||||||
|
|
||||||
## Binary search
|
## Binary search
|
||||||
|
|||||||
@ -120,17 +120,18 @@ which is used in `toString`) and hex encoding:
|
|||||||
|
|
||||||
## Members
|
## Members
|
||||||
|
|
||||||
| name | meaning | type |
|
| name | meaning | type |
|
||||||
|----------------------------|-----------------------------------------|---------------|
|
|----------------------------|------------------------------------------------|---------------|
|
||||||
| `size` | size | Int |
|
| `size` | size | Int |
|
||||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||||
| `+` | buffer concatenation | Any |
|
| `+` | buffer concatenation | Any |
|
||||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||||
| `hex` | encode to hex strign | String |
|
| `hex` | encode to hex strign | String |
|
||||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
| `base64` | encode to base64 (url flavor) (2) | String |
|
||||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
| `base64std` | encode to base64 (default vocabulary, filling) | String |
|
||||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
||||||
|
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: optimized implementation that override `Iterable` one
|
: optimized implementation that override `Iterable` one
|
||||||
|
|||||||
82
docs/Complex.md
Normal file
82
docs/Complex.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 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
|
import lyng.decimal
|
||||||
```
|
```
|
||||||
|
|
||||||
## What `BigDecimal` Is For
|
## What `Decimal` Is For
|
||||||
|
|
||||||
Use `BigDecimal` when values are fundamentally decimal:
|
Use `Decimal` when values are fundamentally decimal:
|
||||||
|
|
||||||
- money
|
- money
|
||||||
- human-entered quantities
|
- human-entered quantities
|
||||||
@ -38,8 +38,8 @@ assertEquals("2.2", c.toStringExpanded())
|
|||||||
|
|
||||||
The three forms mean different things:
|
The three forms mean different things:
|
||||||
|
|
||||||
- `1.d`: convert `Int -> BigDecimal`
|
- `1.d`: convert `Int -> Decimal`
|
||||||
- `2.2.d`: convert `Real -> BigDecimal`
|
- `2.2.d`: convert `Real -> Decimal`
|
||||||
- `"2.2".d`: parse exact decimal text
|
- `"2.2".d`: parse exact decimal text
|
||||||
|
|
||||||
That distinction is intentional.
|
That distinction is intentional.
|
||||||
@ -67,16 +67,36 @@ The explicit factory methods are:
|
|||||||
```lyng
|
```lyng
|
||||||
import lyng.decimal
|
import lyng.decimal
|
||||||
|
|
||||||
BigDecimal.fromInt(10)
|
Decimal.fromInt(10)
|
||||||
BigDecimal.fromReal(2.5)
|
Decimal.fromReal(2.5)
|
||||||
BigDecimal.fromString("12.34")
|
Decimal.fromString("12.34")
|
||||||
```
|
```
|
||||||
|
|
||||||
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
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
|
## Core Operations
|
||||||
|
|
||||||
`BigDecimal` supports:
|
`Decimal` supports:
|
||||||
|
|
||||||
- `+`
|
- `+`
|
||||||
- `-`
|
- `-`
|
||||||
@ -115,7 +135,7 @@ assert(2 == 2.d)
|
|||||||
assert(3 > 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 + BigDecimal` and `Real + BigDecimal`.
|
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`.
|
||||||
|
|
||||||
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
||||||
|
|
||||||
@ -146,6 +166,17 @@ assertEquals(2.9, "2.9".d.toReal())
|
|||||||
|
|
||||||
Use `toReal()` only when you are willing to return to binary floating-point semantics.
|
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 Context
|
||||||
|
|
||||||
Division is the operation where precision and rounding matter most.
|
Division is the operation where precision and rounding matter most.
|
||||||
@ -223,6 +254,50 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (
|
|||||||
|
|
||||||
## Recommended Usage Rules
|
## 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:
|
If you care about exact decimal source text:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
|
|||||||
66
docs/LegacyDigest.md
Normal file
66
docs/LegacyDigest.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# 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.
|
||||||
18
docs/List.md
18
docs/List.md
@ -30,6 +30,13 @@ There is a shortcut for the last:
|
|||||||
|
|
||||||
__Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too.
|
__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
|
## Concatenation
|
||||||
|
|
||||||
You can concatenate lists or iterable objects:
|
You can concatenate lists or iterable objects:
|
||||||
@ -38,6 +45,16 @@ You can concatenate lists or iterable objects:
|
|||||||
assert( [4,5] + (1..3) == [4, 5, 1, 2, 3])
|
assert( [4,5] + (1..3) == [4, 5, 1, 2, 3])
|
||||||
>>> void
|
>>> 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
|
## Appending
|
||||||
|
|
||||||
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
|
To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will
|
||||||
@ -157,6 +174,7 @@ List could be sorted in place, just like [Collection] provide sorted copies, in
|
|||||||
| `[index]` | get or set element at index | Int |
|
| `[index]` | get or set element at index | Int |
|
||||||
| `[Range]` | get slice of the array (copy) | Range |
|
| `[Range]` | get slice of the array (copy) | Range |
|
||||||
| `+=` | append element(s) (2) | List or Obj |
|
| `+=` | append element(s) (2) | List or Obj |
|
||||||
|
| `List.fill(size, block)` | build a new list from indices `0..<size` | Int, Callable |
|
||||||
| `sort()` | in-place sort, natural order | void |
|
| `sort()` | in-place sort, natural order | void |
|
||||||
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
| `sortBy(predicate)` | in-place sort bu `predicate` call result (3) | void |
|
||||||
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
| `sortWith(comparator)` | in-place sort using `comarator` function (4) | void |
|
||||||
|
|||||||
192
docs/Matrix.md
Normal file
192
docs/Matrix.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# 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.
|
||||||
151
docs/OOP.md
151
docs/OOP.md
@ -1001,9 +1001,9 @@ Static fields can be accessed from static methods via the class qualifier:
|
|||||||
assertEquals("bar", Test.getData() )
|
assertEquals("bar", Test.getData() )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
# Extending classes
|
# Extension members
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## Extension methods
|
## Extension methods
|
||||||
|
|
||||||
@ -1020,6 +1020,133 @@ For example, we want to create an extension method that would test if a value ca
|
|||||||
assert( ! "5.2".isInteger() )
|
assert( ! "5.2".isInteger() )
|
||||||
>>> void
|
>>> 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
|
## 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.
|
Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.
|
||||||
@ -1183,8 +1310,24 @@ collection's sugar won't work with it:
|
|||||||
assertEquals("buzz", x[0])
|
assertEquals("buzz", x[0])
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
If you want dynamic to function like an array, create a [feature
|
Multiple selectors are packed into one list index object:
|
||||||
request](https://gitea.sergeych.net/SergeychWorks/lyng/issues).
|
|
||||||
|
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.
|
||||||
|
|
||||||
# Theory
|
# Theory
|
||||||
|
|
||||||
|
|||||||
@ -160,15 +160,15 @@ import lyng.decimal
|
|||||||
3 > 2.d
|
3 > 2.d
|
||||||
```
|
```
|
||||||
|
|
||||||
work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`.
|
work naturally even though `Int` and `Real` themselves were not edited to know `Decimal`.
|
||||||
|
|
||||||
The shape is:
|
The shape is:
|
||||||
|
|
||||||
- `leftClass = Int` or `Real`
|
- `leftClass = Int` or `Real`
|
||||||
- `rightClass = BigDecimal`
|
- `rightClass = Decimal`
|
||||||
- `commonClass = BigDecimal`
|
- `commonClass = Decimal`
|
||||||
- convert built-ins into `BigDecimal`
|
- convert built-ins into `Decimal`
|
||||||
- leave `BigDecimal` values unchanged
|
- leave `Decimal` values unchanged
|
||||||
|
|
||||||
## Step-By-Step Pattern For Your Own Type
|
## Step-By-Step Pattern For Your Own Type
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,23 @@ Exclusive end ranges are adopted from kotlin either:
|
|||||||
assert(4 in r)
|
assert(4 in r)
|
||||||
>>> void
|
>>> 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
|
In any case, we can test an object to belong to using `in` and `!in` and
|
||||||
access limits:
|
access limits:
|
||||||
|
|
||||||
@ -73,6 +90,23 @@ but
|
|||||||
>>> 2
|
>>> 2
|
||||||
>>> void
|
>>> 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
|
### Stepped ranges
|
||||||
|
|
||||||
Use `step` to change the iteration increment. The range bounds still define membership,
|
Use `step` to change the iteration increment. The range bounds still define membership,
|
||||||
@ -80,9 +114,18 @@ 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,5] == (1..5 step 2).toList() )
|
||||||
assert( [1,3] == (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() )
|
assert( ['a','c','e'] == ('a'..'e' step 2).toList() )
|
||||||
>>> void
|
>>> 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:
|
Real ranges require an explicit step:
|
||||||
|
|
||||||
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
|
assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() )
|
||||||
@ -97,7 +140,7 @@ Open-ended ranges require an explicit step to iterate:
|
|||||||
|
|
||||||
You can use Char as both ends of the closed range:
|
You can use Char as both ends of the closed range:
|
||||||
|
|
||||||
val r = 'a' .. 'c'
|
val r = 'a'..'c'
|
||||||
assert( 'b' in r)
|
assert( 'b' in r)
|
||||||
assert( 'e' !in r)
|
assert( 'e' !in r)
|
||||||
for( ch in r )
|
for( ch in r )
|
||||||
@ -119,6 +162,7 @@ Exclusive end char ranges are supported too:
|
|||||||
|-----------------|------------------------------|---------------|
|
|-----------------|------------------------------|---------------|
|
||||||
| contains(other) | used in `in` | Range, or Any |
|
| contains(other) | used in `in` | Range, or Any |
|
||||||
| isEndInclusive | true for '..' | Bool |
|
| isEndInclusive | true for '..' | Bool |
|
||||||
|
| isDescending | true for `downTo`/`downUntil`| Bool |
|
||||||
| isOpen | at any end | Bool |
|
| isOpen | at any end | Bool |
|
||||||
| isIntRange | both start and end are Int | Bool |
|
| isIntRange | both start and end are Int | Bool |
|
||||||
| step | explicit iteration step | Any? |
|
| step | explicit iteration step | Any? |
|
||||||
|
|||||||
@ -19,6 +19,8 @@ you can use it's class to ensure type:
|
|||||||
|-----------------|-------------------------------------------------------------|------|
|
|-----------------|-------------------------------------------------------------|------|
|
||||||
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
||||||
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | 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 |
|
| `.clamp(range)` | clamp value within range boundaries | Real |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
|
|||||||
@ -64,6 +64,18 @@ Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!
|
|||||||
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
||||||
>>> void
|
>>> 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
|
# Regex class reference
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# Lyng Language Reference for AI Agents (Current Compiler State)
|
# Lyng Language Reference for AI Agents (Current Compiler State)
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
Purpose: dense, implementation-first reference for generating valid Lyng code.
|
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`.
|
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`.
|
||||||
@ -13,15 +15,17 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
|
|
||||||
## 2. Lexical Syntax
|
## 2. Lexical Syntax
|
||||||
- Comments: `// line`, `/* block */`.
|
- Comments: `// line`, `/* block */`.
|
||||||
- Strings: `"..."` (supports escapes). Multiline string content is normalized by indentation logic.
|
- Strings: `"..."` or `` `...` `` (supports escapes). Multiline string content is normalized by indentation logic.
|
||||||
- Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits).
|
- 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 `` `...` ``.
|
||||||
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
|
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
|
||||||
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`).
|
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`).
|
||||||
- String interpolation is supported:
|
- String interpolation is supported:
|
||||||
- identifier form: `"$name"`
|
- identifier form: `"$name"` or `` `$name` ``
|
||||||
- expression form: `"${expr}"`
|
- expression form: `"${expr}"` or `` `${expr}` ``
|
||||||
- escaped dollar: `"\$"` and `"$$"` both produce literal `$`.
|
- escaped dollar: `"\$"`, `"$$"`, `` `\$` ``, and `` `$$` `` all produce literal `$`.
|
||||||
- `\\$x` means backslash + interpolated `x`.
|
- `\\$x` means backslash + interpolated `x` in either delimiter form.
|
||||||
- Per-file opt-out is supported via leading comment directive:
|
- Per-file opt-out is supported via leading comment directive:
|
||||||
- `// feature: interpolation: off`
|
- `// feature: interpolation: off`
|
||||||
- with this directive, `$...` stays literal text.
|
- with this directive, `$...` stays literal text.
|
||||||
@ -48,8 +52,10 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- Range literals:
|
- Range literals:
|
||||||
- inclusive: `a..b`
|
- inclusive: `a..b`
|
||||||
- exclusive end: `a..<b`
|
- exclusive end: `a..<b`
|
||||||
|
- descending inclusive: `a downTo b`
|
||||||
|
- descending exclusive end: `a downUntil b`
|
||||||
- open-ended forms are supported (`a..`, `..b`, `..`).
|
- open-ended forms are supported (`a..`, `..b`, `..`).
|
||||||
- optional step: `a..b step 2`
|
- optional step: `a..b step 2`, `a downTo b step 2`
|
||||||
- Lambda literal:
|
- Lambda literal:
|
||||||
- with params: `{ x, y -> x + y }`
|
- with params: `{ x, y -> x + y }`
|
||||||
- implicit `it`: `{ it + 1 }`
|
- implicit `it`: `{ it + 1 }`
|
||||||
@ -82,11 +88,17 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
||||||
- Null-safe family:
|
- Null-safe family:
|
||||||
- member access: `?.`
|
- member access: `?.`
|
||||||
- safe index: `?[i]`
|
- safe index: `?[i]`, `?[i, j]`
|
||||||
- safe invoke: `?(...)`
|
- safe invoke: `?(...)`
|
||||||
- safe block invoke: `?{ ... }`
|
- safe block invoke: `?{ ... }`
|
||||||
- elvis: `?:` and `??`.
|
- elvis: `?:` and `??`.
|
||||||
- Increment/decrement: prefix and postfix `++`, `--`.
|
- 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
|
## 5. Declarations
|
||||||
- Variables:
|
- Variables:
|
||||||
@ -107,6 +119,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- shorthand: `fun f(x) = expr`.
|
- shorthand: `fun f(x) = expr`.
|
||||||
- generics: `fun f<T>(x: T): T`.
|
- generics: `fun f<T>(x: T): T`.
|
||||||
- extension functions: `fun Type.name(...) { ... }`.
|
- extension functions: `fun Type.name(...) { ... }`.
|
||||||
|
- 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`.
|
- delegated callable: `fun f(...) by delegate`.
|
||||||
- Type aliases:
|
- Type aliases:
|
||||||
- `type Name = TypeExpr`
|
- `type Name = TypeExpr`
|
||||||
@ -122,6 +136,9 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
|
|
||||||
## 6. Control Flow
|
## 6. Control Flow
|
||||||
- `if` is expression-like.
|
- `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.
|
- `when(value) { ... }` supported.
|
||||||
- branch conditions support equality, `in`, `!in`, `is`, `!is`, and `nullable` predicate.
|
- branch conditions support equality, `in`, `!in`, `is`, `!is`, and `nullable` predicate.
|
||||||
- `when { ... }` (subject-less) is currently not implemented.
|
- `when { ... }` (subject-less) is currently not implemented.
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# AI notes: avoid Kotlin/Wasm invalid IR with suspend lambdas
|
# AI notes: avoid Kotlin/Wasm invalid IR with suspend lambdas
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
## Do
|
## Do
|
||||||
- Prefer explicit `object : Statement()` with `override suspend fun execute(...)` when building compiler statements.
|
- 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.
|
- Keep `Statement` objects non-lambda, especially in compiler hot paths like parsing/var declarations.
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# Lyng Stdlib Reference for AI Agents (Compact)
|
# Lyng Stdlib Reference for AI Agents (Compact)
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
Purpose: fast overview of what is available by default and what must be imported.
|
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`.
|
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`.
|
||||||
@ -14,7 +16,13 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
- Assertions/tests: `assert`, `assertEquals`/`assertEqual`, `assertNotEquals`, `assertThrows`.
|
- Assertions/tests: `assert`, `assertEquals`/`assertEqual`, `assertNotEquals`, `assertThrows`.
|
||||||
- Preconditions: `require`, `check`.
|
- Preconditions: `require`, `check`.
|
||||||
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
|
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
|
||||||
|
- `Deferred.cancel()` cancels an active task.
|
||||||
|
- `Deferred.await()` throws `CancellationException` if that task was cancelled.
|
||||||
- Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`.
|
- 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
|
## 3. Core Global Constants/Types
|
||||||
- Values: `Unset`, `π`.
|
- Values: `Unset`, `π`.
|
||||||
@ -22,13 +30,14 @@ 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`.
|
- Collections/types: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `Range`, `RingBuffer`.
|
||||||
- Random: singleton `Random` and class `SeededRandom`.
|
- Random: singleton `Random` and class `SeededRandom`.
|
||||||
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
|
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
|
||||||
|
- Async exception: `CancellationException`.
|
||||||
- Delegation types: `Delegate`, `DelegateContext`.
|
- Delegation types: `Delegate`, `DelegateContext`.
|
||||||
- Regex types: `Regex`, `RegexMatch`.
|
- Regex types: `Regex`, `RegexMatch`.
|
||||||
- Also present: `Math.PI` namespace constant.
|
- Also present: `Math.PI` namespace constant.
|
||||||
|
|
||||||
## 4. `lyng.stdlib` Module Surface (from `root.lyng`)
|
## 4. `lyng.stdlib` Module Surface (from `root.lyng`)
|
||||||
### 4.1 Extern class declarations
|
### 4.1 Extern class declarations
|
||||||
- Exceptions/delegation base: `Exception`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`.
|
- Exceptions/delegation base: `Exception`, `CancellationException`, `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>`.
|
- 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>`.
|
- Host iterator bridge: `KotlinIterator<T>`.
|
||||||
- Random APIs: `extern object Random`, `extern class SeededRandom`.
|
- Random APIs: `extern object Random`, `extern class SeededRandom`.
|
||||||
@ -37,7 +46,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
|
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
|
||||||
- Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`.
|
- Search/predicates: `findFirst`, `findFirstOrNull`, `any`, `all`, `count`, `first`, `last`.
|
||||||
- Mapping/aggregation: `map`, `flatMap`, `flatten`, `sum`, `sumOf`, `minOf`, `maxOf`.
|
- Mapping/aggregation: `map`, `flatMap`, `flatten`, `sum`, `sumOf`, `minOf`, `maxOf`.
|
||||||
- Ordering: `sorted`, `sortedBy`, `shuffled`, `List.sort`, `List.sortBy`.
|
- 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`.
|
||||||
- String helper: `joinToString`, `String.re`.
|
- String helper: `joinToString`, `String.re`.
|
||||||
|
|
||||||
### 4.3 Delegation helpers
|
### 4.3 Delegation helpers
|
||||||
@ -56,20 +66,33 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
## 5. Additional Built-in Modules (import explicitly)
|
## 5. Additional Built-in Modules (import explicitly)
|
||||||
- `import lyng.observable`
|
- `import lyng.observable`
|
||||||
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
- `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`
|
- `import lyng.buffer`
|
||||||
- `Buffer`, `MutableBuffer`.
|
- `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`
|
- `import lyng.serialization`
|
||||||
- `Lynon` serialization utilities.
|
- `Lynon` serialization utilities.
|
||||||
- `import lyng.time`
|
- `import lyng.time`
|
||||||
- `Instant`, `DateTime`, `Duration`, and module `delay`.
|
- `Instant`, `Date`, `DateTime`, `Duration`, and module `delay`.
|
||||||
|
|
||||||
## 6. Optional (lyngio) Modules
|
## 6. Optional (lyngio) Modules
|
||||||
Requires installing `lyngio` into the import manager from host code.
|
Requires installing `lyngio` into the import manager from host code.
|
||||||
- `import lyng.io.fs` (filesystem `Path` API)
|
- `import lyng.io.fs` (filesystem `Path` API)
|
||||||
- `import lyng.io.process` (process execution API)
|
- `import lyng.io.process` (process execution API)
|
||||||
- `import lyng.io.console` (console capabilities, geometry, ANSI/output, events)
|
- `import lyng.io.console` (console capabilities, geometry, ANSI/output, events)
|
||||||
|
- `import lyng.io.http` (HTTP/HTTPS client 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)
|
||||||
|
|
||||||
## 7. AI Generation Tips
|
## 7. AI Generation Tips
|
||||||
- Assume `lyng.stdlib` APIs exist in regular script contexts.
|
- Assume `lyng.stdlib` APIs exist in regular script contexts.
|
||||||
- For platform-sensitive code (`fs`, `process`, `console`), gate assumptions and mention required module install.
|
- For platform-sensitive code (`fs`, `process`, `console`, `http`, `ws`, `net`), gate assumptions and mention required module install.
|
||||||
- Prefer extension-method style (`items.filter { ... }`) and standard scope helpers (`let`/`also`/`apply`/`run`).
|
- Prefer extension-method style (`items.filter { ... }`) and standard scope helpers (`let`/`also`/`apply`/`run`).
|
||||||
|
|||||||
12
docs/downloads.md
Normal file
12
docs/downloads.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 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,5 +1,7 @@
|
|||||||
# Embedding Lyng in your Kotlin project
|
# Embedding Lyng in your Kotlin project
|
||||||
|
|
||||||
|
[//]: # (topMenu)
|
||||||
|
|
||||||
Lyng is a tiny, embeddable, Kotlin‑first scripting language. This page shows, step by step, how to:
|
Lyng is a tiny, embeddable, Kotlin‑first scripting language. This page shows, step by step, how to:
|
||||||
|
|
||||||
- add Lyng to your build
|
- add Lyng to your build
|
||||||
@ -36,21 +38,60 @@ dependencies {
|
|||||||
|
|
||||||
If you use Kotlin Multiplatform, add the dependency in the `commonMain` source set (and platform‑specific sets if you need platform APIs).
|
If you use Kotlin Multiplatform, add the dependency in the `commonMain` source set (and platform‑specific sets if you need platform APIs).
|
||||||
|
|
||||||
### 2) Create a runtime (Scope) and execute scripts
|
### 2) Preferred runtime: `EvalSession`
|
||||||
|
|
||||||
The easiest way to get a ready‑to‑use scope with standard packages is via `Script.newScope()`.
|
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.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
fun main() = kotlinx.coroutines.runBlocking {
|
fun main() = kotlinx.coroutines.runBlocking {
|
||||||
val scope = Script.newScope() // suspends on first init
|
val scope = Script.newScope() // suspends on first init
|
||||||
|
|
||||||
// Evaluate a one‑liner
|
|
||||||
val result = scope.eval("1 + 2 * 3")
|
val result = scope.eval("1 + 2 * 3")
|
||||||
println("Lyng result: $result") // ObjReal/ObjInt etc.
|
println("Lyng result: $result")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pre‑compile a script and execute it multiple times:
|
You can also pre‑compile a script and execute it multiple times on the same scope:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val script = Compiler.compile("""
|
val script = Compiler.compile("""
|
||||||
@ -63,7 +104,8 @@ val run1 = script.execute(scope)
|
|||||||
val run2 = script.execute(scope)
|
val run2 = script.execute(scope)
|
||||||
```
|
```
|
||||||
|
|
||||||
`Scope.eval("...")` is a shortcut that compiles and executes on the given scope.
|
`Scope.eval("...")` is the low-level shortcut that compiles and executes on the given scope.
|
||||||
|
For most embedding use cases, prefer `session.eval("...")`.
|
||||||
|
|
||||||
### 3) Preferred: bind extern globals from Kotlin
|
### 3) Preferred: bind extern globals from Kotlin
|
||||||
|
|
||||||
@ -85,6 +127,8 @@ import net.sergeych.lyng.bridge.*
|
|||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val im = Script.defaultImportManager.copy()
|
val im = Script.defaultImportManager.copy()
|
||||||
im.addPackage("my.api") { module ->
|
im.addPackage("my.api") { module ->
|
||||||
module.eval("""
|
module.eval("""
|
||||||
@ -149,6 +193,9 @@ binder.bindGlobalFunRaw("echoRaw") { _, args ->
|
|||||||
Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3.
|
Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// A function returning value
|
// A function returning value
|
||||||
scope.addFn<ObjInt>("inc") {
|
scope.addFn<ObjInt>("inc") {
|
||||||
val x = args.firstAndOnly() as ObjInt
|
val x = args.firstAndOnly() as ObjInt
|
||||||
@ -167,7 +214,7 @@ scope.addVoidFn("log") {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Call them from Lyng
|
// Call them from Lyng
|
||||||
scope.eval("val y = inc(41); log('Answer:', y)")
|
session.eval("val y = inc(41); log('Answer:', y)")
|
||||||
```
|
```
|
||||||
|
|
||||||
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
||||||
@ -183,11 +230,79 @@ 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.
|
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
|
### 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.
|
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
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val myClass = ObjClass("MyClass")
|
val myClass = ObjClass("MyClass")
|
||||||
|
|
||||||
// Add a read-only field (constant)
|
// Add a read-only field (constant)
|
||||||
@ -215,6 +330,8 @@ 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`.
|
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
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val myClass = ObjClass("MyClass")
|
val myClass = ObjClass("MyClass")
|
||||||
var internalValue: Long = 10
|
var internalValue: Long = 10
|
||||||
|
|
||||||
@ -381,8 +498,9 @@ For Kotlin code that needs dynamic access to Lyng variables, functions, or membe
|
|||||||
It provides explicit, cached handles and predictable lookup rules.
|
It provides explicit, cached handles and predictable lookup rules.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
scope.eval("""
|
val scope = session.getScope()
|
||||||
|
session.eval("""
|
||||||
val x = 40
|
val x = 40
|
||||||
fun add(a, b) = a + b
|
fun add(a, b) = a + b
|
||||||
class Box { var value = 1 }
|
class Box { var value = 1 }
|
||||||
@ -397,7 +515,7 @@ val x = resolver.resolveVal("x").get(scope)
|
|||||||
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
|
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
|
||||||
|
|
||||||
// Member access
|
// Member access
|
||||||
val box = scope.eval("Box()")
|
val box = session.eval("Box()")
|
||||||
val valueHandle = resolver.resolveMemberVar(box, "value")
|
val valueHandle = resolver.resolveMemberVar(box, "value")
|
||||||
valueHandle.set(scope, ObjInt(10))
|
valueHandle.set(scope, ObjInt(10))
|
||||||
val value = valueHandle.get(scope)
|
val value = valueHandle.get(scope)
|
||||||
@ -408,12 +526,14 @@ val value = valueHandle.get(scope)
|
|||||||
The simplest approach: evaluate an expression that yields the value and convert it.
|
The simplest approach: evaluate an expression that yields the value and convert it.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val kotlinAnswer = scope.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
val kotlinAnswer = session.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
||||||
|
|
||||||
// After scripts manipulate your vars:
|
// After scripts manipulate your vars:
|
||||||
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
||||||
scope.eval("name = name + ' rocks!'")
|
session.eval("name = name + ' rocks!'")
|
||||||
val kotlinName = scope.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
val kotlinName = session.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.
|
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.
|
||||||
@ -426,16 +546,20 @@ There are two convenient patterns.
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Suppose Lyng defines: fun add(a, b) = a + b
|
// Suppose Lyng defines: fun add(a, b) = a + b
|
||||||
scope.eval("fun add(a, b) = a + b")
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
session.eval("fun add(a, b) = a + b")
|
||||||
|
|
||||||
val sum = scope.eval("add(20, 22)").toKotlin(scope) // -> 42
|
val sum = session.eval("add(20, 22)").toKotlin(scope) // -> 42
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Call a Lyng function by name via a prepared call scope:
|
2) Call a Lyng function by name via a prepared call scope:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Ensure the function exists in the scope
|
// Ensure the function exists in the scope
|
||||||
scope.eval("fun add(a, b) = a + b")
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
session.eval("fun add(a, b) = a + b")
|
||||||
|
|
||||||
// Look up the function object
|
// Look up the function object
|
||||||
val addFn = scope.get("add")!!.value as Statement
|
val addFn = scope.get("add")!!.value as Statement
|
||||||
@ -466,7 +590,8 @@ Register a Kotlin‑built package:
|
|||||||
import net.sergeych.lyng.bridge.*
|
import net.sergeych.lyng.bridge.*
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// Access the import manager behind this scope
|
// Access the import manager behind this scope
|
||||||
val im: ImportManager = scope.importManager
|
val im: ImportManager = scope.importManager
|
||||||
@ -497,12 +622,12 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use it from Lyng
|
// Use it from Lyng
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
import my.tools.*
|
import my.tools.*
|
||||||
val v = triple(14)
|
val v = triple(14)
|
||||||
status = "busy"
|
status = "busy"
|
||||||
""")
|
""")
|
||||||
val v = scope.eval("v").toKotlin(scope) // -> 42
|
val v = session.eval("v").toKotlin(scope) // -> 42
|
||||||
```
|
```
|
||||||
|
|
||||||
Register a package from Lyng source text:
|
Register a package from Lyng source text:
|
||||||
@ -516,24 +641,27 @@ val pkgText = """
|
|||||||
|
|
||||||
scope.importManager.addTextPackages(pkgText)
|
scope.importManager.addTextPackages(pkgText)
|
||||||
|
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
import math.extra.*
|
import math.extra.*
|
||||||
val s = sqr(12)
|
val s = sqr(12)
|
||||||
""")
|
""")
|
||||||
val s = scope.eval("s").toKotlin(scope) // -> 144
|
val s = session.eval("s").toKotlin(scope) // -> 144
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
||||||
|
|
||||||
### 10) Executing from files, security, and isolation
|
### 10) Executing from files, security, and isolation
|
||||||
|
|
||||||
- To run code from a file, read it and pass to `scope.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
- To run code from a file, read it and pass to `session.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.
|
- `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, create fresh modules/scopes via `Scope.new()` or `Script.newScope()` when you need a clean environment per request.
|
- For isolation, prefer a fresh `EvalSession()` per request. Use `Scope.new()` / `Script.newScope()` when you specifically need low-level raw scopes or modules.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Fresh module based on the default manager, without the standard prelude
|
// Preferred per-request runtime:
|
||||||
val isolated = net.sergeych.lyng.Scope.new()
|
val isolatedSession = EvalSession()
|
||||||
|
|
||||||
|
// Low-level fresh module based on the default manager, without the standard prelude:
|
||||||
|
val isolatedScope = net.sergeych.lyng.Scope.new()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11) Tips and troubleshooting
|
### 11) Tips and troubleshooting
|
||||||
@ -568,8 +696,11 @@ 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.
|
You can serialize Lyng exception objects using `Lynon` to transmit them across boundaries and then rethrow them.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scope.eval("throw MyUserException(404, \"Not Found\")")
|
session.eval("throw MyUserException(404, \"Not Found\")")
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
// 1. Serialize the Lyng exception object
|
// 1. Serialize the Lyng exception object
|
||||||
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
||||||
|
|||||||
@ -122,7 +122,8 @@ data class TestJson2(
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deserializeMapWithJsonTest() = runTest {
|
fun deserializeMapWithJsonTest() = runTest {
|
||||||
val x = eval("""
|
val session = EvalSession()
|
||||||
|
val x = session.eval("""
|
||||||
import lyng.serialization
|
import lyng.serialization
|
||||||
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
||||||
""".trimIndent()).decodeSerializable<TestJson2>()
|
""".trimIndent()).decodeSerializable<TestJson2>()
|
||||||
@ -143,7 +144,8 @@ data class TestJson3(
|
|||||||
)
|
)
|
||||||
@Test
|
@Test
|
||||||
fun deserializeAnyMapWithJsonTest() = runTest {
|
fun deserializeAnyMapWithJsonTest() = runTest {
|
||||||
val x = eval("""
|
val session = EvalSession()
|
||||||
|
val x = session.eval("""
|
||||||
import lyng.serialization
|
import lyng.serialization
|
||||||
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
||||||
""".trimIndent()).decodeSerializable<TestJson3>()
|
""".trimIndent()).decodeSerializable<TestJson3>()
|
||||||
@ -175,4 +177,3 @@ on [Instant](time.md), see `Instant.truncateTo...` functions.
|
|||||||
|
|
||||||
(3)
|
(3)
|
||||||
: Map keys must be strings, map values may be any objects serializable to Json.
|
: Map keys must be strings, map values may be any objects serializable to Json.
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,13 @@
|
|||||||
#### Install in host
|
#### Install in host
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
import net.sergeych.lyng.io.console.createConsoleModule
|
||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||||
|
|
||||||
suspend fun initScope() {
|
suspend fun initScope() {
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
401
docs/lyng.io.db.md
Normal file
401
docs/lyng.io.db.md
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
### 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
|
##### `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.
|
||||||
|
|
||||||
|
##### `SqlRow`
|
||||||
|
|
||||||
|
- `row[index]` — zero-based positional access.
|
||||||
|
- `row["columnName"]` — case-insensitive lookup by output column label.
|
||||||
|
|
||||||
|
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
|
||||||
|
|
||||||
|
##### `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`.
|
||||||
|
|
||||||
|
Portable result metadata categories:
|
||||||
|
|
||||||
|
- `Binary`
|
||||||
|
- `String`
|
||||||
|
- `Int`
|
||||||
|
- `Double`
|
||||||
|
- `Decimal`
|
||||||
|
- `Bool`
|
||||||
|
- `Date`
|
||||||
|
- `DateTime`
|
||||||
|
- `Instant`
|
||||||
|
|
||||||
|
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 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,23 +39,27 @@ This brings in:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Install the module into a Lyng Scope
|
#### Install the module into a Lyng session
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
Kotlin (host) bootstrap example:
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||||
|
|
||||||
val scope: Scope = Scope.new()
|
suspend fun bootstrapFs() {
|
||||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
val session = EvalSession()
|
||||||
// installed == true on first registration in this ImportManager, false on repeats
|
val scope: Scope = session.getScope()
|
||||||
|
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||||
|
// installed == true on first registration in this ImportManager, false on repeats
|
||||||
|
|
||||||
// In scripts (or via scope.eval), import the module to use its symbols:
|
// In scripts (or via session.eval), import the module to use its symbols:
|
||||||
scope.eval("import lyng.io.fs")
|
session.eval("import lyng.io.fs")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can install with a custom policy too (see Access policy below).
|
You can install with a custom policy too (see Access policy below).
|
||||||
@ -185,7 +189,7 @@ val denyWrites = object : FsAccessPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createFs(denyWrites, scope)
|
createFs(denyWrites, scope)
|
||||||
scope.eval("import lyng.io.fs")
|
session.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)`).
|
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)`).
|
||||||
|
|||||||
179
docs/lyng.io.http.md
Normal file
179
docs/lyng.io.http.md
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
### 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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
|
||||||
175
docs/lyng.io.net.md
Normal file
175
docs/lyng.io.net.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
### 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.
|
||||||
|
>
|
||||||
|
> **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,24 +20,26 @@ For external projects, ensure you have the appropriate Maven repository configur
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Install the module into a Lyng Scope
|
#### Install the module into a Lyng session
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
Kotlin (host) bootstrap example:
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.io.process.createProcessModule
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
|
||||||
// ... inside a suspend function or runBlocking
|
suspend fun bootstrapProcess() {
|
||||||
val scope: Scope = Script.newScope()
|
val session = EvalSession()
|
||||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
val scope: Scope = session.getScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
// In scripts (or via scope.eval), import the module:
|
// In scripts (or via session.eval), import the module:
|
||||||
scope.eval("import lyng.io.process")
|
session.eval("import lyng.io.process")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
148
docs/lyng.io.ws.md
Normal file
148
docs/lyng.io.ws.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
### 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
Simple text message exchange:
|
||||||
|
|
||||||
|
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 message exchange:
|
||||||
|
|
||||||
|
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 websocket (`wss`) exchange:
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### API reference
|
||||||
|
|
||||||
|
##### `Ws` (static methods)
|
||||||
|
|
||||||
|
- `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`, e.g. `"Authorization" => "Bearer x"`
|
||||||
|
- 2-item lists, e.g. `["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`
|
||||||
|
|
||||||
|
`receive()` returns `null` after a clean close.
|
||||||
|
|
||||||
|
##### `WsMessage`
|
||||||
|
|
||||||
|
- `isText: Bool`
|
||||||
|
- `text: String?`
|
||||||
|
- `data: Buffer?`
|
||||||
|
|
||||||
|
Text messages populate `text`; binary messages populate `data`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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,13 +1,15 @@
|
|||||||
### Lyng CLI (`lyng`)
|
# Lyng CLI (`lyng`)
|
||||||
|
|
||||||
The Lyng CLI is the reference command-line tool for the Lyng language. It lets you:
|
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)
|
- Run Lyng scripts from files or inline strings (shebangs accepted)
|
||||||
- Use standard argument passing (`ARGV`) to your scripts.
|
- 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.
|
- Format Lyng source files via the built-in `fmt` subcommand.
|
||||||
|
- Register synchronous process-exit handlers with `atExit(...)`.
|
||||||
|
|
||||||
|
|
||||||
#### Building on Linux
|
## Building on Linux
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- JDK 17+ (for Gradle and the JVM distribution)
|
- JDK 17+ (for Gradle and the JVM distribution)
|
||||||
@ -19,7 +21,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.
|
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:
|
1) Build the native binary:
|
||||||
|
|
||||||
@ -38,26 +40,27 @@ What this does:
|
|||||||
- Produces `distributables/lyng-linuxX64.zip` containing the `lyng` executable.
|
- 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 and links it to `~/bin/jlyng`.
|
This creates a JVM distribution with a launcher script, packages it as a downloadable zip, and links it to `~/bin/jlyng`.
|
||||||
|
|
||||||
```
|
```
|
||||||
bin/local_jrelease
|
bin/local_jrelease
|
||||||
```
|
```
|
||||||
|
|
||||||
What this does:
|
What this does:
|
||||||
- Runs `./gradlew :lyng:installJvmDist` to build the JVM app distribution to `lyng/build/install/lyng-jvm`.
|
- Runs `./gradlew :lyng:jvmDistZip` to build the JVM app distribution archive at `lyng/build/distributions/lyng-jvm.zip`.
|
||||||
- Copies the distribution under `~/bin/jlyng-jvm`.
|
- Copies the archive to `distributables/lyng-jvm.zip`.
|
||||||
|
- Unpacks that distribution under `~/bin/jlyng-jvm`.
|
||||||
- Creates a symlink `~/bin/jlyng` pointing to the launcher script.
|
- 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).
|
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`:
|
- Run a script by file name and pass arguments to `ARGV`:
|
||||||
|
|
||||||
@ -72,6 +75,7 @@ lyng -- -my-script.lyng arg1 arg2
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Execute inline code with `-x/--execute` and pass positional args to `ARGV`:
|
- 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
|
lyng -x "println(\"Hello\")" more args
|
||||||
@ -84,7 +88,101 @@ lyng --version
|
|||||||
lyng --help
|
lyng --help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use in shell scripts
|
### 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
|
||||||
|
|
||||||
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
|
Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts directly executable on Unix-like systems. For example:
|
||||||
|
|
||||||
@ -92,7 +190,7 @@ Standard unix shebangs (`#!`) are supported, so you can make Lyng scripts direct
|
|||||||
println("Hello, world!")
|
println("Hello, world!")
|
||||||
|
|
||||||
|
|
||||||
##### Formatting source: `fmt` subcommand
|
### Formatting source: `fmt` subcommand
|
||||||
|
|
||||||
Format Lyng files with the built-in formatter.
|
Format Lyng files with the built-in formatter.
|
||||||
|
|
||||||
@ -134,7 +232,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.
|
- 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`.
|
- When executing scripts, all positional arguments after the script name are available in Lyng as `ARGV`.
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
`lyngio` is a separate library that extends the Lyng core (`lynglib`) with powerful, multiplatform, and secure I/O capabilities.
|
`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?
|
#### 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.
|
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.
|
||||||
@ -10,9 +12,13 @@
|
|||||||
|
|
||||||
#### Included Modules
|
#### 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.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.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.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.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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -37,31 +43,58 @@ dependencies {
|
|||||||
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Script
|
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.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyng.io.process.createProcessModule
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
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.fs.security.PermitAllAccessPolicy
|
||||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
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() {
|
suspend fun runMyScript() {
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// Install modules with policies
|
// Install modules with policies
|
||||||
|
createDbModule(scope)
|
||||||
|
createJdbcModule(scope)
|
||||||
|
createSqliteModule(scope)
|
||||||
createFs(PermitAllAccessPolicy, scope)
|
createFs(PermitAllAccessPolicy, scope)
|
||||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||||
|
createHttpModule(PermitAllHttpAccessPolicy, scope)
|
||||||
|
createNetModule(PermitAllNetAccessPolicy, scope)
|
||||||
|
createWsModule(PermitAllWsAccessPolicy, scope)
|
||||||
|
|
||||||
// Now scripts can import them
|
// Now scripts can import them
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.jdbc
|
||||||
|
import lyng.io.db.sqlite
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
import lyng.io.process
|
import lyng.io.process
|
||||||
import lyng.io.console
|
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("Working dir: " + Path(".").readUtf8())
|
||||||
println("OS: " + Platform.details().name)
|
println("OS: " + Platform.details().name)
|
||||||
println("TTY: " + Console.isTty())
|
println("TTY: " + Console.isTty())
|
||||||
|
println("HTTP available: " + Http.isSupported())
|
||||||
|
println("TCP available: " + Net.isTcpAvailable())
|
||||||
|
println("WS available: " + Ws.isSupported())
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -73,23 +106,38 @@ suspend fun runMyScript() {
|
|||||||
`lyngio` is built with a "Secure by Default" philosophy. Every I/O or process operation is checked against a policy.
|
`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).
|
- **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.
|
- **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.
|
- **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:
|
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)
|
- [Filesystem Security Details](lyng.io.fs.md#access-policy-security)
|
||||||
- [Process Security Details](lyng.io.process.md#security-policy)
|
- [Process Security Details](lyng.io.process.md#security-policy)
|
||||||
- [Console Module Details](lyng.io.console.md)
|
- [Console Module Details](lyng.io.console.md)
|
||||||
|
- [HTTP Module Details](lyng.io.http.md)
|
||||||
|
- [Transport Networking Details](lyng.io.net.md)
|
||||||
|
- [WebSocket Module Details](lyng.io.ws.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Platform Support Overview
|
#### Platform Support Overview
|
||||||
|
|
||||||
| Platform | lyng.io.fs | lyng.io.process | lyng.io.console |
|
| 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** | ✅ | ✅ | ✅ (baseline) |
|
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| **Native (Linux/macOS)** | ✅ | ✅ | 🚧 |
|
| **Linux Native** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| **Native (Windows)** | ✅ | 🚧 (Planned) | 🚧 |
|
| **Apple Native** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||||
| **Android** | ✅ | ❌ | ❌ |
|
| **Windows Native** | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
||||||
| **NodeJS** | ✅ | ❌ | ❌ |
|
| **Android** | ⚠️ | ❌ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||||
| **Browser / Wasm** | ✅ (In-memory) | ❌ | ❌ |
|
| **JS / Node** | ❌ | ❌ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||||
|
| **JS / Browser** | ❌ | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ✅ | ✅ | ❌ |
|
||||||
|
| **Wasm** | ❌ | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
- `✅` supported
|
||||||
|
- `⚠️` available but environment-dependent or not fully host-verified yet
|
||||||
|
- `❌` unsupported
|
||||||
|
|||||||
76
docs/math.md
76
docs/math.md
@ -60,8 +60,13 @@ but:
|
|||||||
|
|
||||||
## Round and range
|
## Round and range
|
||||||
|
|
||||||
The following functions return its argument if it is `Int`,
|
The following functions return the argument unchanged if it is `Int`.
|
||||||
or transformed `Real` otherwise.
|
|
||||||
|
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`.
|
||||||
|
|
||||||
| name | description |
|
| name | description |
|
||||||
|----------------|--------------------------------------------------------|
|
|----------------|--------------------------------------------------------|
|
||||||
@ -72,6 +77,14 @@ or transformed `Real` otherwise.
|
|||||||
|
|
||||||
## Lyng math functions
|
## 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 |
|
| name | meaning |
|
||||||
|-----------|------------------------------------------------------|
|
|-----------|------------------------------------------------------|
|
||||||
| sin(x) | sine |
|
| sin(x) | sine |
|
||||||
@ -91,7 +104,7 @@ or transformed `Real` otherwise.
|
|||||||
| log10(x) | $log_{10}(x)$ |
|
| log10(x) | $log_{10}(x)$ |
|
||||||
| pow(x, y) | ${x^y}$ |
|
| pow(x, y) | ${x^y}$ |
|
||||||
| sqrt(x) | $ \sqrt {x}$ |
|
| sqrt(x) | $ \sqrt {x}$ |
|
||||||
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
|
| abs(x) | absolute value of x. Int if x is Int, Decimal if x is Decimal, Real otherwise |
|
||||||
| clamp(x, range) | limit x to be inside range boundaries |
|
| clamp(x, range) | limit x to be inside range boundaries |
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
@ -104,12 +117,69 @@ For example:
|
|||||||
assert( abs(-1) is Int)
|
assert( abs(-1) is Int)
|
||||||
assert( abs(-2.21) == 2.21 )
|
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:
|
// clamp() limits value to the range:
|
||||||
assert( clamp(15, 0..10) == 10 )
|
assert( clamp(15, 0..10) == 10 )
|
||||||
assert( clamp(-5, 0..10) == 0 )
|
assert( clamp(-5, 0..10) == 0 )
|
||||||
assert( 5.clamp(0..10) == 5 )
|
assert( 5.clamp(0..10) == 5 )
|
||||||
>>> void
|
>>> 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
|
## Random values
|
||||||
|
|
||||||
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
||||||
|
|||||||
@ -32,10 +32,25 @@ Depending on the platform, these coroutines may be executed on different CPU and
|
|||||||
assert(xIsCalled)
|
assert(xIsCalled)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
This example shows how to launch a coroutine with `launch` which returns [Deferred] instance, the latter have ways to await for the coroutine completion and retrieve possible result.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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
|
## 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:
|
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:
|
||||||
|
|||||||
@ -114,10 +114,12 @@ When running end‑to‑end “book” workloads or heavier benches, you can ena
|
|||||||
Flags are mutable at runtime, e.g.:
|
Flags are mutable at runtime, e.g.:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
PerfFlags.ARG_BUILDER = false
|
runTest {
|
||||||
val r1 = (Scope().eval(script) as ObjInt).value
|
PerfFlags.ARG_BUILDER = false
|
||||||
PerfFlags.ARG_BUILDER = true
|
val r1 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||||
val r2 = (Scope().eval(script) as ObjInt).value
|
PerfFlags.ARG_BUILDER = true
|
||||||
|
val r2 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Reset flags at the end of a test to avoid impacting other tests.
|
Reset flags at the end of a test to avoid impacting other tests.
|
||||||
@ -619,4 +621,3 @@ Reproduce
|
|||||||
Notes
|
Notes
|
||||||
- Negative caches are installed only after a real miss throws (cache‑after‑miss), preserving error semantics and invalidation on `layoutVersion` changes.
|
- 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.
|
- IndexRef PIC augments the existing direct path and uses move‑to‑front promotion; it is keyed on `(classId, layoutVersion)` like other PICs.
|
||||||
|
|
||||||
|
|||||||
62
docs/pi_spigot_perf_baseline.md
Normal file
62
docs/pi_spigot_perf_baseline.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
## 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](/home/sergeych/dev/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
|
||||||
@ -6,6 +6,7 @@ This page documents the **current** rules: static name resolution, closure captu
|
|||||||
|
|
||||||
## Current rules (bytecode compiler)
|
## 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.
|
- **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.
|
- **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.
|
- **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.
|
- **Closures capture slots**: lambdas and nested functions capture **frame slots** directly. Captures are resolved at compile time and compiled to slot references.
|
||||||
|
|||||||
181
docs/time.md
181
docs/time.md
@ -1,74 +1,135 @@
|
|||||||
# Lyng time functions
|
# Lyng time functions
|
||||||
|
|
||||||
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
Lyng date and time support requires importing `lyng.time`. The module provides four related types:
|
||||||
|
|
||||||
- `Instant` class for absolute time stamps with platform-dependent resolution.
|
- `Instant` for absolute timestamps.
|
||||||
- `DateTime` class for calendar-aware points in time within a specific time zone.
|
- `Date` for calendar dates without time-of-day or timezone.
|
||||||
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
|
- `DateTime` for calendar-aware points in time in a specific timezone.
|
||||||
|
- `Duration` for absolute elapsed time.
|
||||||
|
|
||||||
## Time instant: `Instant`
|
## Time instant: `Instant`
|
||||||
|
|
||||||
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
|
`Instant` represents some moment of time independently of the calendar. It is similar to SQL `TIMESTAMP`
|
||||||
|
or Kotlin `Instant`.
|
||||||
|
|
||||||
### Constructing and converting
|
### Constructing and converting
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// default constructor returns time now:
|
|
||||||
val t1 = Instant()
|
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 t3 = Instant("2024-01-01T12:00:00.123456Z")
|
||||||
|
|
||||||
// truncation:
|
val t4 = t3.truncateToMinute()
|
||||||
val t4 = t3.truncateToMinute
|
assertEquals("2024-01-01T12:00:00Z", t4.toRFC3339())
|
||||||
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
|
|
||||||
|
|
||||||
// to localized DateTime (uses system default TZ if not specified):
|
|
||||||
val dt = t3.toDateTime("+02:00")
|
val dt = t3.toDateTime("+02:00")
|
||||||
assertEquals(dt.hour, 14)
|
assertEquals(14, dt.hour)
|
||||||
|
|
||||||
|
val d = t3.toDate("Z")
|
||||||
|
assertEquals(Date(2024, 1, 1), d)
|
||||||
|
|
||||||
### Instant members
|
### Instant members
|
||||||
|
|
||||||
| member | description |
|
| member | description |
|
||||||
|--------------------------------|---------------------------------------------------------|
|
|--------------------------------|------------------------------------------------------|
|
||||||
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
| epochSeconds: Real | offset in seconds since Unix epoch |
|
||||||
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
| epochWholeSeconds: Int | whole seconds since Unix epoch |
|
||||||
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos |
|
| nanosecondsOfSecond: Int | nanoseconds within the current second |
|
||||||
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
| isDistantFuture: Bool | true if it is `Instant.distantFuture` |
|
||||||
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
| isDistantPast: Bool | true if it is `Instant.distantPast` |
|
||||||
| truncateToMinute: Instant | create new instance truncated to minute |
|
| truncateToMinute(): Instant | truncate to minute precision |
|
||||||
| truncateToSecond: Instant | create new instance truncated to second |
|
| truncateToSecond(): Instant | truncate to second precision |
|
||||||
| truncateToMillisecond: Instant | truncate new instance to millisecond |
|
| truncateToMillisecond(): Instant | truncate to millisecond precision |
|
||||||
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
| truncateToMicrosecond(): Instant | truncate to microsecond precision |
|
||||||
| toRFC3339(): String | format as RFC3339 string (UTC) |
|
| toRFC3339(): String | format as RFC3339 string in UTC |
|
||||||
| toDateTime(tz?): DateTime | localize to a TimeZone (ID string or offset seconds) |
|
| toDateTime(tz?): DateTime | localize to a timezone |
|
||||||
|
| toDate(tz?): Date | convert to a calendar date in a timezone |
|
||||||
|
|
||||||
## Calendar time: `DateTime`
|
## Calendar date: `Date`
|
||||||
|
|
||||||
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year,
|
`Date` represents a pure calendar date. It has no time-of-day and no attached timezone. Use it for values
|
||||||
month, and day.
|
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"))
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
### Constructing
|
### Constructing
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// Current time in system default timezone
|
|
||||||
val now = DateTime.now()
|
val now = DateTime.now()
|
||||||
|
|
||||||
// Specific timezone
|
|
||||||
val offsetTime = DateTime.now("+02:00")
|
val offsetTime = DateTime.now("+02:00")
|
||||||
|
|
||||||
// From Instant
|
|
||||||
val dt = Instant().toDateTime("Z")
|
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")
|
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
|
||||||
|
|
||||||
// From RFC3339 string
|
|
||||||
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
||||||
|
|
||||||
### DateTime members
|
### DateTime members
|
||||||
@ -83,7 +144,9 @@ month, and day.
|
|||||||
| second: Int | second component (0..59) |
|
| second: Int | second component (0..59) |
|
||||||
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||||
| timeZone: String | timezone ID string |
|
| timeZone: String | timezone ID string |
|
||||||
|
| date: Date | calendar date component |
|
||||||
| toInstant(): Instant | convert back to absolute Instant |
|
| toInstant(): Instant | convert back to absolute Instant |
|
||||||
|
| toDate(): Date | extract the calendar date in this timezone |
|
||||||
| toUTC(): DateTime | shortcut to convert to UTC |
|
| toUTC(): DateTime | shortcut to convert to UTC |
|
||||||
| toTimeZone(tz): DateTime | convert to another timezone |
|
| toTimeZone(tz): DateTime | convert to another timezone |
|
||||||
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
||||||
@ -96,28 +159,27 @@ month, and day.
|
|||||||
|
|
||||||
`DateTime` handles calendar arithmetic correctly:
|
`DateTime` handles calendar arithmetic correctly:
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
||||||
val nextYear = leapDay.addYears(1)
|
val nextYear = leapDay.addYears(1)
|
||||||
assertEquals(nextYear.day, 28) // Feb 29, 2024 -> Feb 28, 2025
|
assertEquals(28, nextYear.day)
|
||||||
|
|
||||||
# `Duration` class
|
# `Duration` class
|
||||||
|
|
||||||
Represent absolute time distance between two `Instant`.
|
`Duration` represents absolute elapsed time between two instants.
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
val t1 = Instant()
|
val t1 = Instant()
|
||||||
|
|
||||||
// yes we can delay to period, and it is not blocking. is suspends!
|
|
||||||
delay(1.millisecond)
|
delay(1.millisecond)
|
||||||
|
|
||||||
val t2 = Instant()
|
val t2 = Instant()
|
||||||
// be suspend, so actual time may vary:
|
|
||||||
assert( t2 - t1 >= 1.millisecond)
|
assert(t2 - t1 >= 1.millisecond)
|
||||||
assert( t2 - t1 < 100.millisecond)
|
assert(t2 - t1 < 100.millisecond)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
|
Duration values can be created from numbers using extensions on `Int` and `Real`:
|
||||||
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
|
|
||||||
|
|
||||||
- `n.millisecond`, `n.milliseconds`
|
- `n.millisecond`, `n.milliseconds`
|
||||||
- `n.second`, `n.seconds`
|
- `n.second`, `n.seconds`
|
||||||
@ -125,10 +187,9 @@ Duration can be converted from numbers, like `5.minutes` and so on. Extensions a
|
|||||||
- `n.hour`, `n.hours`
|
- `n.hour`, `n.hours`
|
||||||
- `n.day`, `n.days`
|
- `n.day`, `n.days`
|
||||||
|
|
||||||
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
|
Larger units like months or years are calendar-dependent and are intentionally not part of `Duration`.
|
||||||
|
|
||||||
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration`
|
Each duration instance can be converted to numbers in these units:
|
||||||
instance:
|
|
||||||
|
|
||||||
- `d.microseconds`
|
- `d.microseconds`
|
||||||
- `d.milliseconds`
|
- `d.milliseconds`
|
||||||
@ -137,18 +198,16 @@ instance:
|
|||||||
- `d.hours`
|
- `d.hours`
|
||||||
- `d.days`
|
- `d.days`
|
||||||
|
|
||||||
for example
|
Example:
|
||||||
|
|
||||||
import lyng.time
|
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
|
>>> void
|
||||||
|
|
||||||
# Utility functions
|
# 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.
|
||||||
|
|||||||
169
docs/tutorial.md
169
docs/tutorial.md
@ -375,6 +375,18 @@ It is rather simple, like everywhere else:
|
|||||||
|
|
||||||
See [math](math.md) for more on it. Notice using Greek as identifier, all languages are allowed.
|
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
|
Logical operation could be used the same
|
||||||
|
|
||||||
var x = 10
|
var x = 10
|
||||||
@ -799,6 +811,12 @@ Lyng has built-in mutable array class `List` with simple literals:
|
|||||||
many collection based methods are implemented there.
|
many collection based methods are implemented there.
|
||||||
For immutable list values, use `list.toImmutable()` and [ImmutableList].
|
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:
|
Lists can contain any type of objects, lists too:
|
||||||
|
|
||||||
val list = [1, [2, 3], 4]
|
val list = [1, [2, 3], 4]
|
||||||
@ -811,6 +829,14 @@ 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).
|
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:
|
When you want to "flatten" it to single array, you can use splat syntax:
|
||||||
|
|
||||||
[1, ...[2,3], 4]
|
[1, ...[2,3], 4]
|
||||||
@ -1068,6 +1094,37 @@ Or, more neat:
|
|||||||
>>> just 3
|
>>> just 3
|
||||||
>>> void
|
>>> 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
|
## When
|
||||||
|
|
||||||
See also: [Comprehensive guide to `when`](when.md)
|
See also: [Comprehensive guide to `when`](when.md)
|
||||||
@ -1339,6 +1396,41 @@ size and index access, like lists:
|
|||||||
"total letters: "+letters
|
"total letters: "+letters
|
||||||
>>> "total letters: 10"
|
>>> "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:
|
For loop support breaks the same as while loops above:
|
||||||
|
|
||||||
fun search(haystack, needle) {
|
fun search(haystack, needle) {
|
||||||
@ -1468,6 +1560,12 @@ It could be open and closed:
|
|||||||
assert( 5 !in (1..<5) )
|
assert( 5 !in (1..<5) )
|
||||||
>>> void
|
>>> 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:
|
Ranges could be inside other ranges:
|
||||||
|
|
||||||
assert( (2..3) in (1..10) )
|
assert( (2..3) in (1..10) )
|
||||||
@ -1480,11 +1578,19 @@ There are character ranges too:
|
|||||||
|
|
||||||
and you can use ranges in for-loops:
|
and you can use ranges in for-loops:
|
||||||
|
|
||||||
for( x in 'a' ..< 'c' ) println(x)
|
for( x in 'a'..<'c' ) println(x)
|
||||||
>>> a
|
>>> a
|
||||||
>>> b
|
>>> b
|
||||||
>>> void
|
>>> 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.
|
See [Ranges](Range.md) for detailed documentation on it.
|
||||||
|
|
||||||
# Time routines
|
# Time routines
|
||||||
@ -1548,15 +1654,27 @@ The type for the character objects is `Char`.
|
|||||||
|
|
||||||
### String literal escapes
|
### 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 |
|
| escape | ASCII value |
|
||||||
|--------|-----------------------|
|
|--------|-----------------------|
|
||||||
| \n | 0x10, newline |
|
| \n | 0x10, newline |
|
||||||
| \r | 0x13, carriage return |
|
| \r | 0x13, carriage return |
|
||||||
| \t | 0x07, tabulation |
|
| \t | 0x07, tabulation |
|
||||||
| \\ | \ slash character |
|
| \\ | \ slash character |
|
||||||
| \" | " double quote |
|
|
||||||
| \uXXXX | unicode code point |
|
| \uXXXX | unicode code point |
|
||||||
|
|
||||||
|
Delimiter-specific escapes:
|
||||||
|
|
||||||
|
| form | escape | value |
|
||||||
|
|--------|--------|------------------|
|
||||||
|
| `"..."` | \" | " double quote |
|
||||||
|
| `` `...` `` | \` | ` backtick |
|
||||||
|
|
||||||
Unicode escape form is exactly 4 hex digits, e.g. `"\u263A"` -> `☺`.
|
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.:
|
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
||||||
@ -1589,10 +1707,15 @@ Example:
|
|||||||
|
|
||||||
val name = "Lyng"
|
val name = "Lyng"
|
||||||
assertEquals("hello, Lyng!", "hello, $name!")
|
assertEquals("hello, Lyng!", "hello, $name!")
|
||||||
|
assertEquals("hello, Lyng!", `hello, $name!`)
|
||||||
assertEquals("sum=3", "sum=${1+2}")
|
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("\$name", `\$name`)
|
||||||
|
assertEquals("\$name", `$$name`)
|
||||||
assertEquals("\\Lyng", "\\$name")
|
assertEquals("\\Lyng", "\\$name")
|
||||||
|
assertEquals("\\Lyng", `\\$name`)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Interpolation and `printf`-style formatting can be combined when needed:
|
Interpolation and `printf`-style formatting can be combined when needed:
|
||||||
@ -1699,6 +1822,14 @@ Open-ended ranges could be used to get start and end too:
|
|||||||
assertEquals( "pult", "catapult"[ 4.. ])
|
assertEquals( "pult", "catapult"[ 4.. ])
|
||||||
>>> void
|
>>> 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
|
### String operations
|
||||||
|
|
||||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There is also
|
||||||
@ -1719,6 +1850,14 @@ Part match:
|
|||||||
assert( "foo" == ($~ as RegexMatch).value )
|
assert( "foo" == ($~ as RegexMatch).value )
|
||||||
>>> void
|
>>> 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:
|
Repeating the fragment:
|
||||||
|
|
||||||
assertEquals("hellohello", "hello"*2)
|
assertEquals("hellohello", "hello"*2)
|
||||||
@ -1754,6 +1893,8 @@ A typical set of String functions includes:
|
|||||||
| characters | create [List] of characters (1) |
|
| characters | create [List] of characters (1) |
|
||||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||||
| matches(re) | matches the regular expression (2) |
|
| 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)
|
(1)
|
||||||
@ -1980,6 +2121,30 @@ Example with custom accessors:
|
|||||||
"abc".firstChar
|
"abc".firstChar
|
||||||
>>> 'a'
|
>>> '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.
|
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).
|
To get details on OOP in Lyng, see [OOP notes](OOP.md).
|
||||||
|
|||||||
@ -1,16 +1,131 @@
|
|||||||
# What's New in Lyng
|
# What's New in Lyng
|
||||||
|
|
||||||
This document highlights the latest additions and improvements to the Lyng language and its ecosystem.
|
This document highlights the current Lyng release, **1.5.4**, and the broader additions from the 1.5 cycle.
|
||||||
For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
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.4 Highlights
|
||||||
|
|
||||||
|
- `1.5.4` is the stabilization release for the 1.5 feature set.
|
||||||
|
- 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.4` specifically fixes user-visible issues around decimal arithmetic, mixed numeric flows, list behavior, and observable list hooks.
|
||||||
|
- `1.5.4` also fixes extension-member registration for named singleton `object` declarations, so `fun X.foo()` and `val X.bar` now work as expected.
|
||||||
|
- `1.5.4` also lets named singleton `object` declarations use scoped indexer extensions with bracket syntax, so patterns like `Storage["name"]` can be implemented with `override fun Storage.getAt(...)` / `putAt(...)`.
|
||||||
|
- 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}`
|
||||||
|
- Decimal arithmetic, matrices/vectors, and complex numbers
|
||||||
|
- Calendar `Date` support in `lyng.time`
|
||||||
|
- Immutable collections and opt-in `ObservableList`
|
||||||
|
- Rich `lyngio` modules for SQLite databases, console, HTTP, WebSocket, TCP, and UDP
|
||||||
|
- CLI improvements including the built-in formatter `lyng fmt`
|
||||||
|
- Better IDE support and stronger docs around the released feature set
|
||||||
|
|
||||||
## Language Features
|
## 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`)
|
### 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.
|
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
||||||
|
|
||||||
It provides:
|
It provides:
|
||||||
|
|
||||||
- `BigDecimal`
|
- `Decimal`
|
||||||
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
||||||
- mixed arithmetic with `Int` and `Real`
|
- mixed arithmetic with `Int` and `Real`
|
||||||
- local division precision and rounding control via `withDecimalContext(...)`
|
- local division precision and rounding control via `withDecimalContext(...)`
|
||||||
@ -35,6 +150,46 @@ The distinction between `Real -> Decimal` and exact decimal parsing is explicit
|
|||||||
|
|
||||||
See [Decimal](Decimal.md).
|
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
|
### Binary Operator Interop Registry
|
||||||
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.
|
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.
|
||||||
|
|
||||||
@ -169,13 +324,30 @@ Singleton objects are declared using the `object` keyword. They provide a conven
|
|||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
object Config {
|
object Config {
|
||||||
val version = "1.5.0-SNAPSHOT"
|
val version = "1.5.4"
|
||||||
fun show() = println("Config version: " + version)
|
fun show() = println("Config version: " + version)
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.show()
|
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
|
### 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.
|
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.
|
||||||
|
|
||||||
@ -292,8 +464,124 @@ 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.
|
`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
|
## 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
|
### CLI: Formatting Command
|
||||||
A new `fmt` subcommand has been added to the Lyng CLI.
|
A new `fmt` subcommand has been added to the Lyng CLI.
|
||||||
|
|
||||||
@ -303,6 +591,15 @@ lyng fmt --in-place MyFile.lyng # Format file in-place
|
|||||||
lyng fmt --check MyFile.lyng # Check if file needs formatting
|
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
|
### IDEA Plugin: Autocompletion
|
||||||
Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion.
|
Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion.
|
||||||
|
|
||||||
|
|||||||
325
examples/content_index_db.lyng
Normal file
325
examples/content_index_db.lyng
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
#!/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()
|
||||||
23
examples/extract_lynglang_version.lyng
Normal file
23
examples/extract_lynglang_version.lyng
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/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]))
|
||||||
76
examples/free_fall.lyng
Normal file
76
examples/free_fall.lyng
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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("Расчёт не сошёлся")
|
||||||
|
}
|
||||||
43
examples/h2_basic.lyng
Normal file
43
examples/h2_basic.lyng
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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")
|
||||||
67
examples/pi-bench.lyng
Normal file
67
examples/pi-bench.lyng
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
83
examples/pi-bench.py
Normal file
83
examples/pi-bench.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# 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))
|
||||||
71
examples/postgres_basic.lyng
Normal file
71
examples/postgres_basic.lyng
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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")
|
||||||
89
examples/sqlite_basic.lyng
Normal file
89
examples/sqlite_basic.lyng
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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")
|
||||||
68
examples/tcp-server.lyng
Normal file
68
examples/tcp-server.lyng
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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")
|
||||||
60
examples/tcpserver.lyng
Normal file
60
examples/tcpserver.lyng
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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,6 +49,7 @@ val UNICODE_BOTTOM_RIGHT = "┘"
|
|||||||
val UNICODE_HORIZONTAL = "──"
|
val UNICODE_HORIZONTAL = "──"
|
||||||
val UNICODE_VERTICAL = "│"
|
val UNICODE_VERTICAL = "│"
|
||||||
val UNICODE_DOT = "· "
|
val UNICODE_DOT = "· "
|
||||||
|
val PIECES: List<Piece> = []
|
||||||
|
|
||||||
type Cell = List<Int>
|
type Cell = List<Int>
|
||||||
type Rotation = List<Cell>
|
type Rotation = List<Cell>
|
||||||
@ -79,6 +80,30 @@ class GameState(
|
|||||||
var paused = false
|
var paused = false
|
||||||
}
|
}
|
||||||
class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {}
|
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() {
|
fun clearAndHome() {
|
||||||
Console.clear()
|
Console.clear()
|
||||||
@ -468,8 +493,6 @@ fun rot(a: Cell, b: Cell, c: Cell, d: Cell): Rotation {
|
|||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
val PIECES: List<Piece> = []
|
|
||||||
|
|
||||||
val iRots: Rotations = []
|
val iRots: Rotations = []
|
||||||
iRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(3,1)))
|
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)))
|
iRots.add(rot(cell(2,0), cell(2,1), cell(2,2), cell(2,3)))
|
||||||
@ -541,9 +564,8 @@ if (!Console.isSupported()) {
|
|||||||
)
|
)
|
||||||
var prevFrameLines: List<String> = []
|
var prevFrameLines: List<String> = []
|
||||||
|
|
||||||
val gameMutex: Mutex = Mutex()
|
|
||||||
var forceRedraw = false
|
var forceRedraw = false
|
||||||
val pendingInputs: List<String> = []
|
val inputBuffer: InputBuffer = InputBuffer()
|
||||||
|
|
||||||
val rawModeEnabled = Console.setRawMode(true)
|
val rawModeEnabled = Console.setRawMode(true)
|
||||||
if (!rawModeEnabled) {
|
if (!rawModeEnabled) {
|
||||||
@ -643,13 +665,7 @@ if (!Console.isSupported()) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
||||||
val mm: Mutex = gameMutex
|
inputBuffer.push(mapped)
|
||||||
mm.withLock {
|
|
||||||
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
|
|
||||||
pendingInputs.removeAt(0)
|
|
||||||
}
|
|
||||||
pendingInputs.add(mapped)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (eventErr: Object) {
|
} catch (eventErr: Object) {
|
||||||
@ -746,19 +762,11 @@ if (!Console.isSupported()) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val mm: Mutex = gameMutex
|
val toApply = inputBuffer.drain()
|
||||||
mm.withLock {
|
if (toApply.size > 0) {
|
||||||
if (pendingInputs.size > 0) {
|
for (k in toApply) {
|
||||||
val toApply: List<String> = []
|
applyKeyInput(state, k)
|
||||||
while (pendingInputs.size > 0) {
|
if (!state.running || state.gameOver) break
|
||||||
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) {
|
if (!state.running || state.gameOver) {
|
||||||
|
|||||||
@ -27,9 +27,6 @@ kotlin.mpp.enableCInteropCommonization=true
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.nonTransitiveRClass=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.
|
# 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
|
# On this environment, the system JDK 21 installation lacks `jlink`, causing
|
||||||
# :lynglib:androidJdkImage to fail. Point Gradle to a JDK that includes `jlink`.
|
# :lynglib:androidJdkImage to fail. Point Gradle to a JDK that includes `jlink`.
|
||||||
@ -37,6 +34,6 @@ kotlin.native.cacheKind.linuxX64=none
|
|||||||
#org.gradle.java.home=/home/sergeych/.jdks/corretto-21.0.9
|
#org.gradle.java.home=/home/sergeych/.jdks/corretto-21.0.9
|
||||||
android.experimental.lint.migrateToK2=false
|
android.experimental.lint.migrateToK2=false
|
||||||
android.lint.useK2Uast=false
|
android.lint.useK2Uast=false
|
||||||
kotlin.mpp.applyDefaultHierarchyTemplate=true
|
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
||||||
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
@ -2,19 +2,28 @@
|
|||||||
agp = "8.5.2"
|
agp = "8.5.2"
|
||||||
clikt = "5.0.3"
|
clikt = "5.0.3"
|
||||||
mordant = "3.0.2"
|
mordant = "3.0.2"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.20"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
kotlinx-datetime = "0.6.1"
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
ionspin-bignum = "0.3.10"
|
ionspin-bignum = "0.3.10"
|
||||||
|
multik = "0.3.0"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
compiler = "3.2.0-alpha11"
|
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]
|
[libraries]
|
||||||
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
|
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" }
|
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-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" }
|
mordant-jvm-jna = { module = "com.github.ajalt.mordant:mordant-jvm-jna", version.ref = "mordant" }
|
||||||
@ -24,11 +33,27 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
|
|||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||||
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
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" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", 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" }
|
okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okioVersion" }
|
||||||
compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" }
|
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]
|
[plugins]
|
||||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import com.intellij.execution.ui.ConsoleViewContentType
|
|||||||
import com.intellij.openapi.actionSystem.AnAction
|
import com.intellij.openapi.actionSystem.AnAction
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
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.project.Project
|
||||||
import com.intellij.openapi.wm.ToolWindow
|
import com.intellij.openapi.wm.ToolWindow
|
||||||
import com.intellij.openapi.wm.ToolWindowAnchor
|
import com.intellij.openapi.wm.ToolWindowAnchor
|
||||||
@ -36,9 +38,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.sergeych.lyng.idea.LyngIcons
|
import net.sergeych.lyng.idea.LyngIcons
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
private fun getPsiFile(e: AnActionEvent): PsiFile? {
|
private fun getPsiFile(e: AnActionEvent): PsiFile? {
|
||||||
val project = e.project ?: return null
|
val project = e.project ?: return null
|
||||||
@ -48,36 +51,99 @@ 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) {
|
override fun update(e: AnActionEvent) {
|
||||||
val psiFile = getPsiFile(e)
|
val psiFile = getRunnableFile(e)
|
||||||
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
val isRunnable = psiFile != null
|
||||||
e.presentation.isEnabledAndVisible = isLyng
|
e.presentation.isEnabledAndVisible = isRunnable
|
||||||
if (isLyng) {
|
if (isRunnable) {
|
||||||
e.presentation.isEnabled = false
|
e.presentation.text = "Run '${psiFile.name}'"
|
||||||
e.presentation.text = "Run '${psiFile.name}' (disabled)"
|
e.presentation.description = "Run the current Lyng script using the Lyng CLI"
|
||||||
e.presentation.description = "Running scripts from the IDE is disabled; use the CLI."
|
|
||||||
} else {
|
} else {
|
||||||
e.presentation.text = "Run Lyng Script"
|
e.presentation.text = "Run Lyng Script"
|
||||||
|
e.presentation.description = "Run the current Lyng script"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
val project = e.project ?: return
|
val project = e.project ?: return
|
||||||
val psiFile = getPsiFile(e) ?: return
|
val psiFile = getRunnableFile(e) ?: return
|
||||||
val fileName = psiFile.name
|
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 (console, toolWindow) = getConsoleAndToolWindow(project)
|
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
||||||
console.clear()
|
console.clear()
|
||||||
|
|
||||||
toolWindow.show {
|
toolWindow.show {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
val command = startLyngProcess(filePath, workingDir)
|
||||||
console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
if (command == null) {
|
||||||
console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
private fun getConsoleAndToolWindow(project: Project): Pair<ConsoleView, ToolWindow> {
|
||||||
val toolWindowManager = ToolWindowManager.getInstance(project)
|
val toolWindowManager = ToolWindowManager.getInstance(project)
|
||||||
var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN)
|
var toolWindow = toolWindowManager.getToolWindow(ToolWindowId.RUN)
|
||||||
@ -106,4 +172,10 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
contentManager.setSelectedContent(content)
|
contentManager.setSelectedContent(content)
|
||||||
return console to actualToolWindow
|
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
|
// Try literal and call-based receiver inference around the dot
|
||||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||||
val className: String? = when {
|
val className: String? = when {
|
||||||
i >= 0 && text[i] == '"' -> "String"
|
i >= 0 && (text[i] == '"' || text[i] == '`') -> "String"
|
||||||
i >= 0 && text[i] == ']' -> "List"
|
i >= 0 && text[i] == ']' -> "List"
|
||||||
i >= 0 && text[i] == '}' -> "Dict"
|
i >= 0 && text[i] == '}' -> "Dict"
|
||||||
i >= 0 && text[i] == ')' -> {
|
i >= 0 && text[i] == ')' -> {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
|||||||
import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor
|
import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor
|
||||||
import net.sergeych.lyng.format.LyngFormatConfig
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
import net.sergeych.lyng.format.LyngFormatter
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
|
import net.sergeych.lyng.format.LyngStringDelimiterPolicy
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,6 +171,7 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
applySpacing = true,
|
applySpacing = true,
|
||||||
applyWrapping = false,
|
applyWrapping = false,
|
||||||
|
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||||
)
|
)
|
||||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text = doc.getText(r)
|
val text = doc.getText(r)
|
||||||
@ -189,6 +191,7 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
applySpacing = settings.enableSpacing,
|
applySpacing = settings.enableSpacing,
|
||||||
applyWrapping = true,
|
applyWrapping = true,
|
||||||
|
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||||
)
|
)
|
||||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text = doc.getText(r)
|
val text = doc.getText(r)
|
||||||
|
|||||||
@ -101,8 +101,8 @@ class LyngLexer : LexerBase() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// String "..." or '...' with simple escape handling
|
// String "...", `...`, or '...' with simple escape handling
|
||||||
if (ch == '"' || ch == '\'') {
|
if (ch == '"' || ch == '\'' || ch == '`') {
|
||||||
val quote = ch
|
val quote = ch
|
||||||
i++
|
i++
|
||||||
while (i < endOffset) {
|
while (i < endOffset) {
|
||||||
|
|||||||
@ -27,9 +27,9 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.idea.LyngFileType
|
import net.sergeych.lyng.idea.LyngFileType
|
||||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
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.idea.util.TextCtx
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
|
||||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||||
|
|
||||||
@ -273,7 +273,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
private fun loadMini(file: PsiFile): MiniScript? {
|
private fun loadMini(file: PsiFile): MiniScript? {
|
||||||
LyngAstManager.getMiniAst(file)?.let { return it }
|
LyngAstManager.getMiniAst(file)?.let { return it }
|
||||||
return try {
|
return try {
|
||||||
val provider = IdeLenientImportProvider.create()
|
val provider = LyngIdeaImportProvider.create()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
LyngLanguageTools.analyze(
|
LyngLanguageTools.analyze(
|
||||||
LyngAnalysisRequest(text = file.text, fileName = file.name, importProvider = provider)
|
LyngAnalysisRequest(text = file.text, fileName = file.name, importProvider = provider)
|
||||||
|
|||||||
@ -28,7 +28,10 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import net.sergeych.lyng.binding.BindingSnapshot
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
import net.sergeych.lyng.idea.LyngFileType
|
import net.sergeych.lyng.idea.LyngFileType
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.tools.*
|
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||||
|
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||||
|
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||||
|
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||||
|
|
||||||
object LyngAstManager {
|
object LyngAstManager {
|
||||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||||
@ -142,7 +145,8 @@ object LyngAstManager {
|
|||||||
|
|
||||||
val text = file.viewProvider.contents.toString()
|
val text = file.viewProvider.contents.toString()
|
||||||
val built = try {
|
val built = try {
|
||||||
val provider = IdeLenientImportProvider.create()
|
DocsBootstrap.ensure()
|
||||||
|
val provider = LyngIdeaImportProvider.create()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
LyngLanguageTools.analyze(
|
LyngLanguageTools.analyze(
|
||||||
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
|
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
|
||||||
@ -165,7 +169,7 @@ object LyngAstManager {
|
|||||||
val dMini = getAnalysis(df)?.mini ?: run {
|
val dMini = getAnalysis(df)?.mini ?: run {
|
||||||
val dText = df.viewProvider.contents.toString()
|
val dText = df.viewProvider.contents.toString()
|
||||||
try {
|
try {
|
||||||
val provider = IdeLenientImportProvider.create()
|
val provider = LyngIdeaImportProvider.create()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
LyngLanguageTools.analyze(
|
LyngLanguageTools.analyze(
|
||||||
LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider)
|
LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider)
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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,4 +179,20 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() {
|
|||||||
assertTrue("Should not report unresolved name for PlainDeclared", messages.none { it.contains("unresolved name: PlainDeclared") })
|
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") })
|
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") })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -19,9 +19,39 @@ plugins {
|
|||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "unspecified"
|
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 {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://maven.universablockchain.com/")
|
maven("https://maven.universablockchain.com/")
|
||||||
@ -34,8 +64,10 @@ kotlin {
|
|||||||
// Suppress Beta warning for expect/actual classes across all targets in this module
|
// Suppress Beta warning for expect/actual classes across all targets in this module
|
||||||
targets.configureEach {
|
targets.configureEach {
|
||||||
compilations.configureEach {
|
compilations.configureEach {
|
||||||
compilerOptions.configure {
|
compileTaskProvider.configure {
|
||||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,6 +82,22 @@ kotlin {
|
|||||||
linuxX64 {
|
linuxX64 {
|
||||||
binaries {
|
binaries {
|
||||||
executable()
|
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 {
|
sourceSets {
|
||||||
@ -61,7 +109,7 @@ kotlin {
|
|||||||
// filesystem access into the execution Scope by default.
|
// filesystem access into the execution Scope by default.
|
||||||
implementation(project(":lyngio"))
|
implementation(project(":lyngio"))
|
||||||
implementation(libs.okio)
|
implementation(libs.okio)
|
||||||
implementation(libs.clikt)
|
implementation(libs.clikt.core)
|
||||||
implementation(kotlin("stdlib-common"))
|
implementation(kotlin("stdlib-common"))
|
||||||
// optional support for rendering markdown in help messages
|
// optional support for rendering markdown in help messages
|
||||||
// implementation(libs.clikt.markdown)
|
// implementation(libs.clikt.markdown)
|
||||||
@ -75,19 +123,41 @@ kotlin {
|
|||||||
implementation(libs.okio.fakefilesystem)
|
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 {
|
val jvmTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation(kotlin("test-junit"))
|
implementation(kotlin("test-junit"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// val nativeMain by getting {
|
|
||||||
// dependencies {
|
|
||||||
// implementation(kotlin("stdlib-common"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
val linuxX64Main by getting {
|
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,7 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych
|
package net.sergeych
|
||||||
|
|
||||||
import com.github.ajalt.clikt.core.CliktCommand
|
import com.github.ajalt.clikt.core.CoreCliktCommand
|
||||||
import com.github.ajalt.clikt.core.Context
|
import com.github.ajalt.clikt.core.Context
|
||||||
import com.github.ajalt.clikt.core.main
|
import com.github.ajalt.clikt.core.main
|
||||||
import com.github.ajalt.clikt.core.subcommands
|
import com.github.ajalt.clikt.core.subcommands
|
||||||
@ -26,28 +26,51 @@ import com.github.ajalt.clikt.parameters.arguments.multiple
|
|||||||
import com.github.ajalt.clikt.parameters.arguments.optional
|
import com.github.ajalt.clikt.parameters.arguments.optional
|
||||||
import com.github.ajalt.clikt.parameters.options.flag
|
import com.github.ajalt.clikt.parameters.options.flag
|
||||||
import com.github.ajalt.clikt.parameters.options.option
|
import com.github.ajalt.clikt.parameters.options.option
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.Compiler
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.LyngVersion
|
import net.sergeych.lyng.LyngVersion
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
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.fs.createFs
|
||||||
|
import net.sergeych.lyng.io.http.createHttpModule
|
||||||
|
import net.sergeych.lyng.io.net.createNetModule
|
||||||
|
import net.sergeych.lyng.io.ws.createWsModule
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyngio.net.shutdownSystemNetEngine
|
||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
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.ws.security.PermitAllWsAccessPolicy
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
import okio.FileSystem
|
import okio.*
|
||||||
import okio.Path.Companion.toPath
|
import okio.Path.Companion.toPath
|
||||||
import okio.SYSTEM
|
|
||||||
import okio.buffer
|
|
||||||
import okio.use
|
|
||||||
|
|
||||||
// common code
|
// common code
|
||||||
|
|
||||||
expect fun exit(code: Int)
|
expect fun exit(code: Int)
|
||||||
|
|
||||||
|
internal expect class CliPlatformShutdownHooks {
|
||||||
|
fun uninstall()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun install(runtime: CliExecutionRuntime): CliPlatformShutdownHooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect class ShellCommandExecutor {
|
expect class ShellCommandExecutor {
|
||||||
fun executeCommand(command: String): CommandResult
|
fun executeCommand(command: String): CommandResult
|
||||||
|
|
||||||
@ -62,19 +85,304 @@ data class CommandResult(
|
|||||||
val error: String
|
val error: String
|
||||||
)
|
)
|
||||||
|
|
||||||
val baseScopeDefer = globalDefer {
|
private const val cliBuiltinsDeclarations = """
|
||||||
Script.newScope().apply {
|
extern fun atExit(append: Bool=true, handler: ()->Void)
|
||||||
addFn("exit") {
|
"""
|
||||||
exit(requireOnlyArg<ObjInt>().toInt())
|
|
||||||
ObjVoid
|
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)
|
||||||
}
|
}
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.http")
|
||||||
|
invalidatePackageCache("lyng.io.ws")
|
||||||
|
invalidatePackageCache("lyng.io.net")
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseScopeDefer = globalDefer {
|
||||||
|
baseCliImportManagerDefer.await().copy().apply {
|
||||||
|
invalidateCliModuleCaches()
|
||||||
|
}.newStdScope().apply {
|
||||||
|
installCliDeclarations()
|
||||||
|
installCliBuiltins()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
throw CliExitRequested(code)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
createHttpModule(PermitAllHttpAccessPolicy, 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 {
|
||||||
|
installCliDeclarations()
|
||||||
|
installCliBuiltins()
|
||||||
|
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>) {
|
fun runMain(args: Array<String>) {
|
||||||
@ -98,7 +406,7 @@ fun runMain(args: Array<String>) {
|
|||||||
.main(args)
|
.main(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Fmt : CliktCommand(name = "fmt") {
|
private class Fmt : CoreCliktCommand(name = "fmt") {
|
||||||
private val checkOnly by option("--check", help = "Check only; print files that would change").flag()
|
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 inPlace by option("-i", "--in-place", help = "Write changes back to files").flag()
|
||||||
private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag()
|
private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag()
|
||||||
@ -121,6 +429,7 @@ private class Fmt : CliktCommand(name = "fmt") {
|
|||||||
val cfg = net.sergeych.lyng.format.LyngFormatConfig(
|
val cfg = net.sergeych.lyng.format.LyngFormatConfig(
|
||||||
applySpacing = enableSpacing,
|
applySpacing = enableSpacing,
|
||||||
applyWrapping = enableWrapping,
|
applyWrapping = enableWrapping,
|
||||||
|
stringDelimiterPolicy = net.sergeych.lyng.format.LyngStringDelimiterPolicy.PreferFewerEscapes,
|
||||||
)
|
)
|
||||||
|
|
||||||
var anyChanged = false
|
var anyChanged = false
|
||||||
@ -156,8 +465,9 @@ private class Fmt : CliktCommand(name = "fmt") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() {
|
private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktCommand() {
|
||||||
|
|
||||||
|
override val invokeWithoutSubcommand = true
|
||||||
override val printHelpOnEmptyArgs = true
|
override val printHelpOnEmptyArgs = true
|
||||||
|
|
||||||
val version by option("-v", "--version", help = "Print version and exit").flag()
|
val version by option("-v", "--version", help = "Print version and exit").flag()
|
||||||
@ -186,7 +496,6 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
if (currentContext.invokedSubcommand != null) return
|
if (currentContext.invokedSubcommand != null) return
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val baseScope = baseScopeDefer.await()
|
|
||||||
when {
|
when {
|
||||||
version -> {
|
version -> {
|
||||||
println("Lyng language version ${LyngVersion}")
|
println("Lyng language version ${LyngVersion}")
|
||||||
@ -196,20 +505,13 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
val objargs = mutableListOf<String>()
|
val objargs = mutableListOf<String>()
|
||||||
script?.let { objargs += it }
|
script?.let { objargs += it }
|
||||||
objargs += args
|
objargs += args
|
||||||
baseScope.addConst(
|
|
||||||
"ARGV", ObjList(
|
|
||||||
objargs.map { ObjString(it) }.toMutableList()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
launcher {
|
launcher {
|
||||||
// there is no script name, it is a first argument instead:
|
// there is no script name, it is a first argument instead:
|
||||||
processErrors {
|
processErrors {
|
||||||
val script = Compiler.compileWithResolution(
|
executeSource(
|
||||||
Source("<eval>", execute!!),
|
Source("<eval>", execute!!),
|
||||||
baseScope.currentImportProvider,
|
newCliScope(objargs)
|
||||||
seedScope = baseScope
|
|
||||||
)
|
)
|
||||||
script.execute(baseScope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,8 +521,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
println("Error: no script specified.\n")
|
println("Error: no script specified.\n")
|
||||||
echoFormattedHelp()
|
echoFormattedHelp()
|
||||||
} else {
|
} else {
|
||||||
baseScope.addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
launcher { executeFile(script!!, args) }
|
||||||
launcher { executeFile(script!!) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,30 +531,43 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand()
|
|||||||
|
|
||||||
fun executeFileWithArgs(fileName: String, args: List<String>) {
|
fun executeFileWithArgs(fileName: String, args: List<String>) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
baseScopeDefer.await().addConst("ARGV", ObjList(args.map { ObjString(it) }.toMutableList()))
|
executeFile(fileName, args)
|
||||||
executeFile(fileName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun executeFile(fileName: String) {
|
suspend fun executeSource(source: Source, initialScope: Scope? = null) {
|
||||||
var text = FileSystem.SYSTEM.source(fileName.toPath()).use { fileSource ->
|
val session = EvalSession(initialScope ?: baseScopeDefer.await())
|
||||||
fileSource.buffer().use { bs ->
|
val rootScope = session.getScope()
|
||||||
bs.readUtf8()
|
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
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
shutdownHooks.uninstall()
|
||||||
|
runtime.shutdown()
|
||||||
}
|
}
|
||||||
if( text.startsWith("#!") ) {
|
requestedExitCode?.let { exit(it) }
|
||||||
// skip shebang
|
}
|
||||||
val pos = text.indexOf('\n')
|
|
||||||
text = text.substring(pos + 1)
|
internal suspend fun evalOnCliDispatcher(session: EvalSession, source: Source): Obj =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
session.eval(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun executeFile(fileName: String, args: List<String> = emptyList()) {
|
||||||
|
val canonicalFile = canonicalPath(fileName.toPath())
|
||||||
|
val text = stripShebang(readUtf8(canonicalFile))
|
||||||
processErrors {
|
processErrors {
|
||||||
val scope = baseScopeDefer.await()
|
executeSource(
|
||||||
val script = Compiler.compileWithResolution(
|
Source(canonicalFile.toString(), text),
|
||||||
Source(fileName, text),
|
newCliScope(args, canonicalFile.toString())
|
||||||
scope.currentImportProvider,
|
|
||||||
seedScope = scope
|
|
||||||
)
|
)
|
||||||
script.execute(scope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,172 @@
|
|||||||
|
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,6 +24,34 @@ import kotlin.system.exitProcess
|
|||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal var jvmExitImpl: (Int) -> Nothing = { code -> exitProcess(code) }
|
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) {
|
actual fun exit(code: Int) {
|
||||||
jvmExitImpl(code)
|
jvmExitImpl(code)
|
||||||
}
|
}
|
||||||
104
lyng/src/jvmTest/kotlin/net/sergeych/CliDispatcherJvmTest.kt
Normal file
104
lyng/src/jvmTest/kotlin/net/sergeych/CliDispatcherJvmTest.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,290 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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 2025 Sergey S. Chernov real.sergeych@gmail.com
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -131,4 +131,18 @@ class CliFmtJvmTest {
|
|||||||
Files.deleteIfExists(tmp)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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.ws
|
||||||
|
import lyng.io.net
|
||||||
|
|
||||||
|
assert(Http.isSupported())
|
||||||
|
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,11 +31,12 @@ class FsIntegrationJvmTest {
|
|||||||
val dir = createTempDirectory("lyng_cli_fs_test_")
|
val dir = createTempDirectory("lyng_cli_fs_test_")
|
||||||
try {
|
try {
|
||||||
val file = dir.resolve("hello.txt")
|
val file = dir.resolve("hello.txt")
|
||||||
|
val filePath = file.toString().replace("\\", "\\\\")
|
||||||
// Drive the operation via Lyng code to validate bindings end-to-end
|
// Drive the operation via Lyng code to validate bindings end-to-end
|
||||||
scope.eval(
|
scope.eval(
|
||||||
"""
|
"""
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
val p = Path("${'$'}{file}")
|
val p = Path("${filePath}")
|
||||||
p.writeUtf8("hello from cli test")
|
p.writeUtf8("hello from cli test")
|
||||||
assertEquals(true, p.exists())
|
assertEquals(true, p.exists())
|
||||||
assertEquals("hello from cli test", p.readUtf8())
|
assertEquals("hello from cli test", p.readUtf8())
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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,11 +22,40 @@
|
|||||||
package net.sergeych
|
package net.sergeych
|
||||||
|
|
||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
|
import kotlin.native.concurrent.ThreadLocal
|
||||||
import platform.posix.fgets
|
import platform.posix.fgets
|
||||||
import platform.posix.pclose
|
import platform.posix.pclose
|
||||||
import platform.posix.popen
|
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
|
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 class ShellCommandExecutor() {
|
||||||
actual fun executeCommand(command: String): CommandResult {
|
actual fun executeCommand(command: String): CommandResult {
|
||||||
val outputBuilder = StringBuilder()
|
val outputBuilder = StringBuilder()
|
||||||
@ -62,6 +91,24 @@ 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) {
|
actual fun exit(code: Int) {
|
||||||
exitProcess(code)
|
exitProcess(code)
|
||||||
}
|
}
|
||||||
@ -31,6 +31,33 @@ plugins {
|
|||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.0.1-SNAPSHOT"
|
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 {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
jvm()
|
jvm()
|
||||||
@ -44,7 +71,6 @@ kotlin {
|
|||||||
iosX64()
|
iosX64()
|
||||||
iosArm64()
|
iosArm64()
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
macosX64()
|
|
||||||
macosArm64()
|
macosArm64()
|
||||||
mingwX64()
|
mingwX64()
|
||||||
linuxX64()
|
linuxX64()
|
||||||
@ -59,11 +85,46 @@ kotlin {
|
|||||||
// nodejs()
|
// 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
|
// Keep expect/actual warning suppressed consistently with other modules
|
||||||
targets.configureEach {
|
targets.configureEach {
|
||||||
compilations.configureEach {
|
compilations.configureEach {
|
||||||
compilerOptions.configure {
|
compileTaskProvider.configure {
|
||||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,13 +140,64 @@ kotlin {
|
|||||||
api(libs.okio)
|
api(libs.okio)
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mordant.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 {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.kotlin.test)
|
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
|
// JS: use runtime detection in jsMain to select Node vs Browser implementation
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
@ -93,6 +205,13 @@ kotlin {
|
|||||||
api(libs.okio)
|
api(libs.okio)
|
||||||
implementation(libs.okio.fakefilesystem)
|
implementation(libs.okio.fakefilesystem)
|
||||||
implementation("com.squareup.okio:okio-nodefilesystem:${libs.versions.okioVersion.get()}")
|
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 {
|
val jvmMain by getting {
|
||||||
@ -100,6 +219,12 @@ kotlin {
|
|||||||
implementation(libs.mordant.jvm.jna)
|
implementation(libs.mordant.jvm.jna)
|
||||||
implementation("org.jline:jline-reader:3.29.0")
|
implementation("org.jline:jline-reader:3.29.0")
|
||||||
implementation("org.jline:jline-terminal: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
|
// // For Wasm we use in-memory VFS for now
|
||||||
@ -112,10 +237,10 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenerateLyngioConsoleDecls : DefaultTask() {
|
abstract class GenerateLyngioDecls : DefaultTask() {
|
||||||
@get:InputFile
|
@get:InputDirectory
|
||||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
abstract val sourceFile: RegularFileProperty
|
abstract val sourceDir: DirectoryProperty
|
||||||
|
|
||||||
@get:OutputDirectory
|
@get:OutputDirectory
|
||||||
abstract val outputDir: DirectoryProperty
|
abstract val outputDir: DirectoryProperty
|
||||||
@ -125,9 +250,9 @@ abstract class GenerateLyngioConsoleDecls : DefaultTask() {
|
|||||||
val targetPkg = "net.sergeych.lyngio.stdlib_included"
|
val targetPkg = "net.sergeych.lyngio.stdlib_included"
|
||||||
val pkgPath = targetPkg.replace('.', '/')
|
val pkgPath = targetPkg.replace('.', '/')
|
||||||
val targetDir = outputDir.get().asFile.resolve(pkgPath)
|
val targetDir = outputDir.get().asFile.resolve(pkgPath)
|
||||||
|
if (targetDir.exists()) targetDir.deleteRecursively()
|
||||||
targetDir.mkdirs()
|
targetDir.mkdirs()
|
||||||
|
|
||||||
val text = sourceFile.get().asFile.readText()
|
|
||||||
fun escapeForQuoted(s: String): String = buildString {
|
fun escapeForQuoted(s: String): String = buildString {
|
||||||
for (ch in s) when (ch) {
|
for (ch in s) when (ch) {
|
||||||
'\\' -> append("\\\\")
|
'\\' -> append("\\\\")
|
||||||
@ -142,30 +267,39 @@ abstract class GenerateLyngioConsoleDecls : DefaultTask() {
|
|||||||
val out = buildString {
|
val out = buildString {
|
||||||
append("package ").append(targetPkg).append("\n\n")
|
append("package ").append(targetPkg).append("\n\n")
|
||||||
append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
|
append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
|
||||||
append("internal val consoleLyng = \"")
|
sourceDir.get().asFile
|
||||||
append(escapeForQuoted(text))
|
.listFiles { file -> file.isFile && file.extension == "lyng" }
|
||||||
append("\"\n")
|
?.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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
targetDir.resolve("console_types_lyng.generated.kt").writeText(out)
|
targetDir.resolve("lyngio_types_lyng.generated.kt").writeText(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val lyngioConsoleDeclsFile = layout.projectDirectory.file("stdlib/lyng/io/console.lyng")
|
val lyngioDeclsDir = layout.projectDirectory.dir("stdlib/lyng/io")
|
||||||
val generatedLyngioDeclsDir = layout.buildDirectory.dir("generated/source/lyngioDecls/commonMain/kotlin")
|
val generatedLyngioDeclsDir = layout.buildDirectory.dir("generated/source/lyngioDecls/commonMain/kotlin")
|
||||||
|
|
||||||
val generateLyngioConsoleDecls by tasks.registering(GenerateLyngioConsoleDecls::class) {
|
val generateLyngioDecls by tasks.registering(GenerateLyngioDecls::class) {
|
||||||
sourceFile.set(lyngioConsoleDeclsFile)
|
sourceDir.set(lyngioDeclsDir)
|
||||||
outputDir.set(generatedLyngioDeclsDir)
|
outputDir.set(generatedLyngioDeclsDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.sourceSets.named("commonMain") {
|
kotlin.sourceSets.named("commonMain") {
|
||||||
kotlin.srcDir(generateLyngioConsoleDecls)
|
kotlin.srcDir(generateLyngioDecls)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.targets.configureEach {
|
kotlin.targets.configureEach {
|
||||||
compilations.configureEach {
|
compilations.configureEach {
|
||||||
compileTaskProvider.configure {
|
compileTaskProvider.configure {
|
||||||
dependsOn(generateLyngioConsoleDecls)
|
dependsOn(generateLyngioDecls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package net.sergeych.lyng.io.db.jdbc
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.io.db.SqlCoreModule
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseBackend
|
||||||
|
import net.sergeych.lyng.obj.ObjException
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
|
||||||
|
internal actual suspend fun openJdbcBackend(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
core: SqlCoreModule,
|
||||||
|
options: JdbcOpenOptions,
|
||||||
|
): SqlDatabaseBackend {
|
||||||
|
scope.raiseError(
|
||||||
|
ObjException(
|
||||||
|
core.databaseException,
|
||||||
|
scope.requireScope(),
|
||||||
|
ObjString("lyng.io.db.jdbc is available only on the JVM target")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.db.sqlite
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.io.db.SqlCoreModule
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseBackend
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyng.obj.ObjException
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
|
||||||
|
internal actual suspend fun openSqliteBackend(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
core: SqlCoreModule,
|
||||||
|
options: SqliteOpenOptions,
|
||||||
|
): SqlDatabaseBackend {
|
||||||
|
scope.raiseError(
|
||||||
|
ObjException(
|
||||||
|
core.databaseException,
|
||||||
|
scope.requireScope(),
|
||||||
|
ObjString("SQLite provider is not implemented on this platform yet")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package net.sergeych.lyngio.http
|
||||||
|
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
|
||||||
|
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
|
||||||
@ -0,0 +1,208 @@
|
|||||||
|
package net.sergeych.lyngio.net
|
||||||
|
|
||||||
|
import io.ktor.network.selector.ActorSelectorManager
|
||||||
|
import io.ktor.network.selector.SelectorManager
|
||||||
|
import io.ktor.network.sockets.BoundDatagramSocket
|
||||||
|
import io.ktor.network.sockets.InetSocketAddress
|
||||||
|
import io.ktor.network.sockets.ServerSocket
|
||||||
|
import io.ktor.network.sockets.Socket
|
||||||
|
import io.ktor.network.sockets.aSocket
|
||||||
|
import io.ktor.network.sockets.isClosed
|
||||||
|
import io.ktor.network.sockets.openReadChannel
|
||||||
|
import io.ktor.network.sockets.openWriteChannel
|
||||||
|
import io.ktor.network.sockets.toJavaAddress
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.ByteWriteChannel
|
||||||
|
import io.ktor.utils.io.readAvailable
|
||||||
|
import io.ktor.utils.io.readUTF8Line
|
||||||
|
import io.ktor.utils.io.writeFully
|
||||||
|
import io.ktor.utils.io.writeStringUtf8
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.io.Buffer
|
||||||
|
import kotlinx.io.readByteArray
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
actual fun getSystemNetEngine(): LyngNetEngine = AndroidKtorNetEngine
|
||||||
|
|
||||||
|
actual fun shutdownSystemNetEngine() {}
|
||||||
|
|
||||||
|
private object AndroidKtorNetEngine : LyngNetEngine {
|
||||||
|
private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) }
|
||||||
|
|
||||||
|
override val isSupported: Boolean = true
|
||||||
|
override val isTcpAvailable: Boolean = true
|
||||||
|
override val isTcpServerAvailable: Boolean = true
|
||||||
|
override val isUdpAvailable: Boolean = true
|
||||||
|
|
||||||
|
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> = withContext(Dispatchers.IO) {
|
||||||
|
InetAddress.getAllByName(host).map { address ->
|
||||||
|
address.toLyngSocketAddress(port = port, resolved = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun tcpConnect(
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
timeoutMillis: Long?,
|
||||||
|
noDelay: Boolean,
|
||||||
|
): LyngTcpSocket {
|
||||||
|
val connectBlock: suspend () -> Socket = {
|
||||||
|
aSocket(selectorManager).tcp().connect(host, port) {
|
||||||
|
this.noDelay = noDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val socket = if (timeoutMillis != null) withTimeout(timeoutMillis) { connectBlock() } else connectBlock()
|
||||||
|
return AndroidLyngTcpSocket(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun tcpListen(
|
||||||
|
host: String?,
|
||||||
|
port: Int,
|
||||||
|
backlog: Int,
|
||||||
|
reuseAddress: Boolean,
|
||||||
|
): LyngTcpServer {
|
||||||
|
val bindHost = host ?: "0.0.0.0"
|
||||||
|
val server = aSocket(selectorManager).tcp().bind(bindHost, port) {
|
||||||
|
backlogSize = backlog
|
||||||
|
this.reuseAddress = reuseAddress
|
||||||
|
}
|
||||||
|
return AndroidLyngTcpServer(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket {
|
||||||
|
val bindHost = host ?: "0.0.0.0"
|
||||||
|
val socket = aSocket(selectorManager).udp().bind(bindHost, port) {
|
||||||
|
this.reuseAddress = reuseAddress
|
||||||
|
}
|
||||||
|
return AndroidLyngUdpSocket(socket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AndroidLyngTcpSocket(
|
||||||
|
private val socket: Socket,
|
||||||
|
) : LyngTcpSocket {
|
||||||
|
private val input: ByteReadChannel by lazy { socket.openReadChannel() }
|
||||||
|
private val output: ByteWriteChannel by lazy { socket.openWriteChannel(autoFlush = true) }
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean = !socket.isClosed
|
||||||
|
|
||||||
|
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
|
||||||
|
|
||||||
|
override fun remoteAddress(): LyngSocketAddress = socket.remoteAddress.toLyngSocketAddress(resolved = true)
|
||||||
|
|
||||||
|
override suspend fun read(maxBytes: Int): ByteArray? {
|
||||||
|
if (!input.awaitContent(1)) return null
|
||||||
|
val buffer = ByteArray(maxBytes)
|
||||||
|
val count = input.readAvailable(buffer, 0, maxBytes)
|
||||||
|
return when {
|
||||||
|
count <= 0 -> null
|
||||||
|
count == maxBytes -> buffer
|
||||||
|
else -> buffer.copyOf(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun readLine(): String? = input.readUTF8Line()
|
||||||
|
|
||||||
|
override suspend fun write(data: ByteArray) {
|
||||||
|
output.writeFully(data, 0, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun writeUtf8(text: String) {
|
||||||
|
output.writeStringUtf8(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun flush() {
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AndroidLyngTcpServer(
|
||||||
|
private val server: ServerSocket,
|
||||||
|
) : LyngTcpServer {
|
||||||
|
override fun isOpen(): Boolean = !server.isClosed
|
||||||
|
|
||||||
|
override fun localAddress(): LyngSocketAddress = server.localAddress.toLyngSocketAddress(resolved = true)
|
||||||
|
|
||||||
|
override suspend fun accept(): LyngTcpSocket = AndroidLyngTcpSocket(server.accept())
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
server.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AndroidLyngUdpSocket(
|
||||||
|
private val socket: BoundDatagramSocket,
|
||||||
|
) : LyngUdpSocket {
|
||||||
|
override fun isOpen(): Boolean = !socket.isClosed
|
||||||
|
|
||||||
|
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
|
||||||
|
|
||||||
|
override suspend fun receive(maxBytes: Int): LyngDatagram? {
|
||||||
|
val datagram = try {
|
||||||
|
socket.receive()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (!isOpen()) return null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
val bytes = datagram.packet.readByteArray().let {
|
||||||
|
if (it.size <= maxBytes) it else it.copyOf(maxBytes)
|
||||||
|
}
|
||||||
|
return LyngDatagram(bytes, datagram.address.toLyngSocketAddress(resolved = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun send(data: ByteArray, host: String, port: Int) {
|
||||||
|
val packet = Buffer()
|
||||||
|
packet.write(data)
|
||||||
|
socket.send(io.ktor.network.sockets.Datagram(packet, InetSocketAddress(host, port)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun io.ktor.network.sockets.SocketAddress.toLyngSocketAddress(
|
||||||
|
port: Int? = null,
|
||||||
|
resolved: Boolean,
|
||||||
|
): LyngSocketAddress {
|
||||||
|
val javaAddress = this.toJavaAddress()
|
||||||
|
val inetSocket = javaAddress as? java.net.InetSocketAddress
|
||||||
|
if (inetSocket != null) {
|
||||||
|
val inetAddress = inetSocket.address
|
||||||
|
val host = inetAddress?.hostAddress ?: inetSocket.hostString
|
||||||
|
val actualPort = port ?: inetSocket.port
|
||||||
|
val version = when (inetAddress) {
|
||||||
|
is Inet6Address -> LyngIpVersion.IPV6
|
||||||
|
is Inet4Address -> LyngIpVersion.IPV4
|
||||||
|
else -> if (host.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4
|
||||||
|
}
|
||||||
|
return LyngSocketAddress(host = host, port = actualPort, ipVersion = version, resolved = resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rendered = toString()
|
||||||
|
return LyngSocketAddress(
|
||||||
|
host = rendered,
|
||||||
|
port = port ?: 0,
|
||||||
|
ipVersion = if (rendered.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
|
||||||
|
resolved = resolved,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun InetAddress.toLyngSocketAddress(port: Int, resolved: Boolean): LyngSocketAddress =
|
||||||
|
LyngSocketAddress(
|
||||||
|
host = hostAddress ?: hostName ?: "0.0.0.0",
|
||||||
|
port = port,
|
||||||
|
ipVersion = when (this) {
|
||||||
|
is Inet6Address -> LyngIpVersion.IPV6
|
||||||
|
else -> LyngIpVersion.IPV4
|
||||||
|
},
|
||||||
|
resolved = resolved,
|
||||||
|
)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package net.sergeych.lyngio.ws
|
||||||
|
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
|
||||||
|
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
|
||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.io.console
|
package net.sergeych.lyng.io.console
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
@ -58,18 +59,31 @@ fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Bo
|
|||||||
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
|
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
|
||||||
|
|
||||||
manager.addPackage(CONSOLE_MODULE_NAME) { module ->
|
manager.addPackage(CONSOLE_MODULE_NAME) { module ->
|
||||||
buildConsoleModule(module, policy)
|
buildConsoleModule(module, policy, getSystemConsole())
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
|
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
|
||||||
|
|
||||||
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
|
internal fun createConsoleModule(
|
||||||
|
policy: ConsoleAccessPolicy,
|
||||||
|
manager: ImportManager,
|
||||||
|
console: LyngConsole,
|
||||||
|
): Boolean {
|
||||||
|
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
|
||||||
|
|
||||||
|
manager.addPackage(CONSOLE_MODULE_NAME) { module ->
|
||||||
|
buildConsoleModule(module, policy, console)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy, baseConsole: LyngConsole) {
|
||||||
// Load Lyng declarations for console enums/types first (module-local source of truth).
|
// Load Lyng declarations for console enums/types first (module-local source of truth).
|
||||||
module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng))
|
module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng))
|
||||||
ConsoleEnums.initialize(module)
|
ConsoleEnums.initialize(module)
|
||||||
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
|
val console: LyngConsole = LyngConsoleSecured(baseConsole, policy)
|
||||||
|
|
||||||
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
|
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
|
||||||
|
|
||||||
@ -179,7 +193,7 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
|
|
||||||
addClassFn("events") {
|
addClassFn("events") {
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.events().toConsoleEventStream()
|
ObjConsoleEventStream { console.events() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,12 +224,8 @@ private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ConsoleEventSource.toConsoleEventStream(): ObjConsoleEventStream {
|
|
||||||
return ObjConsoleEventStream(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ObjConsoleEventStream(
|
private class ObjConsoleEventStream(
|
||||||
private val source: ConsoleEventSource,
|
private val sourceFactory: () -> ConsoleEventSource,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
override val objClass: net.sergeych.lyng.obj.ObjClass
|
override val objClass: net.sergeych.lyng.obj.ObjClass
|
||||||
get() = type
|
get() = type
|
||||||
@ -224,35 +234,61 @@ private class ObjConsoleEventStream(
|
|||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
|
||||||
addFn("iterator") {
|
addFn("iterator") {
|
||||||
val stream = thisAs<ObjConsoleEventStream>()
|
val stream = thisAs<ObjConsoleEventStream>()
|
||||||
ObjConsoleEventIterator(stream.source)
|
ObjConsoleEventIterator(stream.sourceFactory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ObjConsoleEventIterator(
|
private class ObjConsoleEventIterator(
|
||||||
private val source: ConsoleEventSource,
|
private val sourceFactory: () -> ConsoleEventSource,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
private var cached: Obj? = null
|
private var cached: Obj? = null
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
private var source: ConsoleEventSource? = null
|
||||||
|
|
||||||
override val objClass: net.sergeych.lyng.obj.ObjClass
|
override val objClass: net.sergeych.lyng.obj.ObjClass
|
||||||
get() = type
|
get() = type
|
||||||
|
|
||||||
|
private fun ensureSource(): ConsoleEventSource {
|
||||||
|
val current = source
|
||||||
|
if (current != null) return current
|
||||||
|
return sourceFactory().also { source = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun recycleSource(reason: String, error: Throwable? = null) {
|
||||||
|
if (error != null) {
|
||||||
|
consoleFlowDebug(reason, error)
|
||||||
|
} else {
|
||||||
|
consoleFlowDebug(reason)
|
||||||
|
}
|
||||||
|
val current = source
|
||||||
|
source = null
|
||||||
|
runCatching { current?.close() }
|
||||||
|
.onFailure { consoleFlowDebug("console-bridge: failed to close recycled source", it) }
|
||||||
|
if (!closed) delay(25)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun ensureCached(): Boolean {
|
private suspend fun ensureCached(): Boolean {
|
||||||
if (closed) return false
|
if (closed) return false
|
||||||
if (cached != null) return true
|
if (cached != null) return true
|
||||||
while (!closed && cached == null) {
|
while (!closed && cached == null) {
|
||||||
val event = try {
|
val currentSource = try {
|
||||||
source.nextEvent()
|
ensureSource()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Consumer loops must survive source/read failures: report and keep polling.
|
recycleSource("console-bridge: source creation failed; retrying", e)
|
||||||
consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e)
|
continue
|
||||||
|
}
|
||||||
|
val event = try {
|
||||||
|
currentSource.nextEvent()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Consumer loops must survive source/read failures: rebuild the source and keep polling.
|
||||||
|
recycleSource("console-bridge: nextEvent failed; recycling source", e)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (event == null) {
|
if (event == null) {
|
||||||
closeSource()
|
recycleSource("console-bridge: source ended; recreating")
|
||||||
return false
|
continue
|
||||||
}
|
}
|
||||||
cached = try {
|
cached = try {
|
||||||
event.toObjEvent()
|
event.toObjEvent()
|
||||||
@ -268,10 +304,10 @@ private class ObjConsoleEventIterator(
|
|||||||
private suspend fun closeSource() {
|
private suspend fun closeSource() {
|
||||||
if (closed) return
|
if (closed) return
|
||||||
closed = true
|
closed = true
|
||||||
// Do not close the underlying console source from VM iterator cancellation.
|
val current = source
|
||||||
// CmdFrame.cancelIterators() may call cancelIteration() while user code is still
|
source = null
|
||||||
// expected to keep processing input (e.g. recover from app-level exceptions).
|
runCatching { current?.close() }
|
||||||
// The source lifecycle is managed by the console runtime.
|
.onFailure { consoleFlowDebug("console-bridge: failed to close iterator source", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun hasNext(): Boolean = ensureCached()
|
suspend fun hasNext(): Boolean = ensureCached()
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.db
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjException
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyngio.stdlib_included.dbLyng
|
||||||
|
|
||||||
|
private const val DB_MODULE_NAME = "lyng.io.db"
|
||||||
|
|
||||||
|
fun createDbModule(scope: Scope): Boolean = createDbModule(scope.importManager)
|
||||||
|
|
||||||
|
fun createDb(scope: Scope): Boolean = createDbModule(scope)
|
||||||
|
|
||||||
|
fun createDbModule(manager: ImportManager): Boolean {
|
||||||
|
if (manager.packageNames.contains(DB_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(DB_MODULE_NAME) { module ->
|
||||||
|
buildDbModule(module)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDb(manager: ImportManager): Boolean = createDbModule(manager)
|
||||||
|
|
||||||
|
private suspend fun buildDbModule(module: ModuleScope) {
|
||||||
|
module.eval(Source(DB_MODULE_NAME, dbLyng))
|
||||||
|
val exceptions = installDbExceptionClasses(module)
|
||||||
|
val registry = DbProviderRegistry()
|
||||||
|
|
||||||
|
module.addFn("registerDatabaseProvider") {
|
||||||
|
val scheme = requiredArg<ObjString>(0).value
|
||||||
|
val opener = args.list.getOrNull(1)
|
||||||
|
?: raiseError("Expected exactly 2 arguments, got ${args.list.size}")
|
||||||
|
registry.register(this, scheme, opener)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
|
||||||
|
module.addFn("openDatabase") {
|
||||||
|
val connectionUrl = requiredArg<ObjString>(0).value
|
||||||
|
val extraParams = args.list.getOrNull(1)
|
||||||
|
?: raiseError("Expected exactly 2 arguments, got ${args.list.size}")
|
||||||
|
if (!extraParams.isInstanceOf("Map")) {
|
||||||
|
raiseIllegalArgument("extraParams must be Map")
|
||||||
|
}
|
||||||
|
val scheme = parseConnectionScheme(connectionUrl)
|
||||||
|
?: raiseIllegalArgument("Malformed database connection URL: $connectionUrl")
|
||||||
|
val opener = registry.providers[scheme]
|
||||||
|
?: raiseDatabaseException(exceptions.database, "No database provider registered for scheme '$scheme'")
|
||||||
|
call(opener, Arguments(listOf(ObjString(connectionUrl), extraParams)), newThisObj = ObjNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DbExceptionClasses(
|
||||||
|
val database: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlExecution: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlConstraint: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlUsage: ObjException.Companion.ExceptionClass,
|
||||||
|
val rollback: ObjException.Companion.ExceptionClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun installDbExceptionClasses(module: ModuleScope): DbExceptionClasses {
|
||||||
|
val database = ObjException.Companion.ExceptionClass("DatabaseException", ObjException.Root)
|
||||||
|
val sqlExecution = ObjException.Companion.ExceptionClass("SqlExecutionException", database)
|
||||||
|
val sqlConstraint = ObjException.Companion.ExceptionClass("SqlConstraintException", sqlExecution)
|
||||||
|
val sqlUsage = ObjException.Companion.ExceptionClass("SqlUsageException", database)
|
||||||
|
val rollback = ObjException.Companion.ExceptionClass("RollbackException", ObjException.Root)
|
||||||
|
|
||||||
|
module.addConst("DatabaseException", database)
|
||||||
|
module.addConst("SqlExecutionException", sqlExecution)
|
||||||
|
module.addConst("SqlConstraintException", sqlConstraint)
|
||||||
|
module.addConst("SqlUsageException", sqlUsage)
|
||||||
|
module.addConst("RollbackException", rollback)
|
||||||
|
|
||||||
|
return DbExceptionClasses(
|
||||||
|
database = database,
|
||||||
|
sqlExecution = sqlExecution,
|
||||||
|
sqlConstraint = sqlConstraint,
|
||||||
|
sqlUsage = sqlUsage,
|
||||||
|
rollback = rollback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DbProviderRegistry {
|
||||||
|
val providers: MutableMap<String, Obj> = linkedMapOf()
|
||||||
|
|
||||||
|
fun register(scope: ScopeFacade, rawScheme: String, opener: Obj) {
|
||||||
|
val scheme = normalizeScheme(rawScheme)
|
||||||
|
?: scope.raiseIllegalArgument("Database provider scheme must not be empty")
|
||||||
|
if (!opener.isInstanceOf("Callable")) {
|
||||||
|
scope.raiseIllegalArgument("Database provider opener must be callable")
|
||||||
|
}
|
||||||
|
if (providers.containsKey(scheme)) {
|
||||||
|
scope.raiseIllegalState("Database provider already registered for scheme '$scheme'")
|
||||||
|
}
|
||||||
|
providers[scheme] = opener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeScheme(rawScheme: String): String? {
|
||||||
|
val trimmed = rawScheme.trim()
|
||||||
|
if (trimmed.isEmpty()) return null
|
||||||
|
if (':' in trimmed) return null
|
||||||
|
return trimmed.lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseConnectionScheme(connectionUrl: String): String? {
|
||||||
|
val colonIndex = connectionUrl.indexOf(':')
|
||||||
|
if (colonIndex <= 0) return null
|
||||||
|
return normalizeScheme(connectionUrl.substring(0, colonIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScopeFacade.raiseDatabaseException(
|
||||||
|
exceptionClass: ObjException.Companion.ExceptionClass,
|
||||||
|
message: String,
|
||||||
|
): Nothing = raiseError(ObjException(exceptionClass, requireScope(), ObjString(message)))
|
||||||
@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.db
|
||||||
|
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjEnumClass
|
||||||
|
import net.sergeych.lyng.obj.ObjEnumEntry
|
||||||
|
import net.sergeych.lyng.obj.ObjException
|
||||||
|
import net.sergeych.lyng.obj.ObjImmutableList
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.thisAs
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import kotlin.collections.List
|
||||||
|
import kotlin.collections.Map
|
||||||
|
import kotlin.collections.MutableList
|
||||||
|
import kotlin.collections.associateWith
|
||||||
|
import kotlin.collections.drop
|
||||||
|
import kotlin.collections.first
|
||||||
|
import kotlin.collections.forEachIndexed
|
||||||
|
import kotlin.collections.getOrNull
|
||||||
|
import kotlin.collections.getOrPut
|
||||||
|
import kotlin.collections.indices
|
||||||
|
import kotlin.collections.linkedMapOf
|
||||||
|
import kotlin.collections.listOf
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.collections.mutableListOf
|
||||||
|
import kotlin.text.lowercase
|
||||||
|
|
||||||
|
internal data class SqlColumnMeta(
|
||||||
|
val name: String,
|
||||||
|
val sqlType: ObjEnumEntry,
|
||||||
|
val nullable: Boolean,
|
||||||
|
val nativeType: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class SqlResultSetData(
|
||||||
|
val columns: List<SqlColumnMeta>,
|
||||||
|
val rows: List<List<Obj>>,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class SqlExecutionResultData(
|
||||||
|
val affectedRowsCount: Int,
|
||||||
|
val generatedKeys: SqlResultSetData,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal interface SqlDatabaseBackend {
|
||||||
|
suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T
|
||||||
|
fun close() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface SqlTransactionBackend {
|
||||||
|
suspend fun select(scope: ScopeFacade, clause: String, params: List<Obj>): SqlResultSetData
|
||||||
|
suspend fun execute(scope: ScopeFacade, clause: String, params: List<Obj>): SqlExecutionResultData
|
||||||
|
suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqlTransactionBackend) -> T): T
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlCoreModule private constructor(
|
||||||
|
val module: ModuleScope,
|
||||||
|
val databaseClass: ObjClass,
|
||||||
|
val transactionClass: ObjClass,
|
||||||
|
val resultSetClass: ObjClass,
|
||||||
|
val rowClass: ObjClass,
|
||||||
|
val columnClass: ObjClass,
|
||||||
|
val executionResultClass: ObjClass,
|
||||||
|
val databaseException: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlExecutionException: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlConstraintException: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlUsageException: ObjException.Companion.ExceptionClass,
|
||||||
|
val rollbackException: ObjException.Companion.ExceptionClass,
|
||||||
|
val sqlTypes: SqlTypeEntries,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun resolve(module: ModuleScope): SqlCoreModule = SqlCoreModule(
|
||||||
|
module = module,
|
||||||
|
databaseClass = module.requireClass("Database"),
|
||||||
|
transactionClass = module.requireClass("SqlTransaction"),
|
||||||
|
resultSetClass = module.requireClass("ResultSet"),
|
||||||
|
rowClass = module.requireClass("SqlRow"),
|
||||||
|
columnClass = module.requireClass("SqlColumn"),
|
||||||
|
executionResultClass = module.requireClass("ExecutionResult"),
|
||||||
|
databaseException = module.requireClass("DatabaseException") as ObjException.Companion.ExceptionClass,
|
||||||
|
sqlExecutionException = module.requireClass("SqlExecutionException") as ObjException.Companion.ExceptionClass,
|
||||||
|
sqlConstraintException = module.requireClass("SqlConstraintException") as ObjException.Companion.ExceptionClass,
|
||||||
|
sqlUsageException = module.requireClass("SqlUsageException") as ObjException.Companion.ExceptionClass,
|
||||||
|
rollbackException = module.requireClass("RollbackException") as ObjException.Companion.ExceptionClass,
|
||||||
|
sqlTypes = SqlTypeEntries.resolve(module),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlTypeEntries private constructor(
|
||||||
|
private val entries: Map<String, ObjEnumEntry>,
|
||||||
|
) {
|
||||||
|
fun require(name: String): ObjEnumEntry = entries[name]
|
||||||
|
?: error("lyng.io.db.SqlType entry is missing: $name")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun resolve(module: ModuleScope): SqlTypeEntries {
|
||||||
|
val enumClass = resolveEnum(module, "SqlType")
|
||||||
|
return SqlTypeEntries(
|
||||||
|
listOf(
|
||||||
|
"Binary", "String", "Int", "Double", "Decimal",
|
||||||
|
"Bool", "Instant", "Date", "DateTime"
|
||||||
|
).associateWith { name ->
|
||||||
|
enumClass.byName[ObjString(name)] as? ObjEnumEntry
|
||||||
|
?: error("lyng.io.db.SqlType.$name is missing")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveEnum(module: ModuleScope, enumName: String): ObjEnumClass {
|
||||||
|
val local = module.get(enumName)?.value as? ObjEnumClass
|
||||||
|
if (local != null) return local
|
||||||
|
val root = module.importProvider.rootScope.get(enumName)?.value as? ObjEnumClass
|
||||||
|
return root ?: error("lyng.io.db declaration enum is missing: $enumName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlRuntimeTypes private constructor(
|
||||||
|
val core: SqlCoreModule,
|
||||||
|
val databaseClass: ObjClass,
|
||||||
|
val transactionClass: ObjClass,
|
||||||
|
val resultSetClass: ObjClass,
|
||||||
|
val rowClass: ObjClass,
|
||||||
|
val columnClass: ObjClass,
|
||||||
|
val executionResultClass: ObjClass,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(prefix: String, core: SqlCoreModule): SqlRuntimeTypes {
|
||||||
|
val databaseClass = object : ObjClass("${prefix}Database", core.databaseClass) {}
|
||||||
|
val transactionClass = object : ObjClass("${prefix}Transaction", core.transactionClass) {}
|
||||||
|
val resultSetClass = object : ObjClass("${prefix}ResultSet", core.resultSetClass) {}
|
||||||
|
val rowClass = object : ObjClass("${prefix}Row", core.rowClass) {}
|
||||||
|
val columnClass = object : ObjClass("${prefix}Column", core.columnClass) {}
|
||||||
|
val executionResultClass = object : ObjClass("${prefix}ExecutionResult", core.executionResultClass) {}
|
||||||
|
val runtime = SqlRuntimeTypes(
|
||||||
|
core = core,
|
||||||
|
databaseClass = databaseClass,
|
||||||
|
transactionClass = transactionClass,
|
||||||
|
resultSetClass = resultSetClass,
|
||||||
|
rowClass = rowClass,
|
||||||
|
columnClass = columnClass,
|
||||||
|
executionResultClass = executionResultClass,
|
||||||
|
)
|
||||||
|
runtime.bind()
|
||||||
|
return runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind() {
|
||||||
|
databaseClass.addFn("close") {
|
||||||
|
thisAs<SqlDatabaseObj>().backend.close()
|
||||||
|
ObjNull
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseClass.addFn("transaction") {
|
||||||
|
val self = thisAs<SqlDatabaseObj>()
|
||||||
|
val block = args.list.getOrNull(0) ?: raiseError("Expected exactly 1 argument, got ${args.list.size}")
|
||||||
|
if (!block.isInstanceOf("Callable")) {
|
||||||
|
raiseClassCastError("transaction block must be callable")
|
||||||
|
}
|
||||||
|
self.backend.transaction(this) { backend ->
|
||||||
|
val lifetime = SqlTransactionLifetime(this@SqlRuntimeTypes.core)
|
||||||
|
try {
|
||||||
|
call(block, Arguments(SqlTransactionObj(this@SqlRuntimeTypes, backend, lifetime)), ObjNull)
|
||||||
|
} finally {
|
||||||
|
lifetime.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionClass.addFn("select") {
|
||||||
|
val self = thisAs<SqlTransactionObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
val clause = (args.list.getOrNull(0) as? ObjString)?.value
|
||||||
|
?: raiseClassCastError("query must be String")
|
||||||
|
val params = args.list.drop(1)
|
||||||
|
SqlResultSetObj(self.types, self.lifetime, self.backend.select(this, clause, params))
|
||||||
|
}
|
||||||
|
transactionClass.addFn("execute") {
|
||||||
|
val self = thisAs<SqlTransactionObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
val clause = (args.list.getOrNull(0) as? ObjString)?.value
|
||||||
|
?: raiseClassCastError("query must be String")
|
||||||
|
val params = args.list.drop(1)
|
||||||
|
SqlExecutionResultObj(self.types, self.lifetime, self.backend.execute(this, clause, params))
|
||||||
|
}
|
||||||
|
transactionClass.addFn("transaction") {
|
||||||
|
val self = thisAs<SqlTransactionObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
val block = args.list.getOrNull(0) ?: raiseError("Expected exactly 1 argument, got ${args.list.size}")
|
||||||
|
if (!block.isInstanceOf("Callable")) {
|
||||||
|
raiseClassCastError("transaction block must be callable")
|
||||||
|
}
|
||||||
|
self.backend.transaction(this) { backend ->
|
||||||
|
val lifetime = SqlTransactionLifetime(this@SqlRuntimeTypes.core)
|
||||||
|
try {
|
||||||
|
call(block, Arguments(SqlTransactionObj(self.types, backend, lifetime)), ObjNull)
|
||||||
|
} finally {
|
||||||
|
lifetime.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSetClass.addProperty("columns", getter = {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjImmutableList(self.columns)
|
||||||
|
})
|
||||||
|
resultSetClass.addFn("size") {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjInt.of(self.rows.size.toLong())
|
||||||
|
}
|
||||||
|
resultSetClass.addFn("isEmpty") {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjBool(self.rows.isEmpty())
|
||||||
|
}
|
||||||
|
resultSetClass.addFn("iterator") {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjImmutableList(self.rows).invokeInstanceMethod(requireScope(), "iterator")
|
||||||
|
}
|
||||||
|
resultSetClass.addFn("toList") {
|
||||||
|
val self = thisAs<SqlResultSetObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjImmutableList(self.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowClass.addProperty("size", getter = {
|
||||||
|
val self = thisAs<SqlRowObj>()
|
||||||
|
ObjInt.of(self.values.size.toLong())
|
||||||
|
})
|
||||||
|
rowClass.addProperty("values", getter = {
|
||||||
|
val self = thisAs<SqlRowObj>()
|
||||||
|
ObjImmutableList(self.values)
|
||||||
|
})
|
||||||
|
|
||||||
|
columnClass.addProperty("name", getter = { ObjString(thisAs<SqlColumnObj>().meta.name) })
|
||||||
|
columnClass.addProperty("sqlType", getter = { thisAs<SqlColumnObj>().meta.sqlType })
|
||||||
|
columnClass.addProperty("nullable", getter = { ObjBool(thisAs<SqlColumnObj>().meta.nullable) })
|
||||||
|
columnClass.addProperty("nativeType", getter = { ObjString(thisAs<SqlColumnObj>().meta.nativeType) })
|
||||||
|
|
||||||
|
executionResultClass.addProperty("affectedRowsCount", getter = {
|
||||||
|
val self = thisAs<SqlExecutionResultObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
ObjInt.of(self.result.affectedRowsCount.toLong())
|
||||||
|
})
|
||||||
|
executionResultClass.addFn("getGeneratedKeys") {
|
||||||
|
val self = thisAs<SqlExecutionResultObj>()
|
||||||
|
self.lifetime.ensureActive(this)
|
||||||
|
SqlResultSetObj(self.types, self.lifetime, self.result.generatedKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlTransactionLifetime(
|
||||||
|
private val core: SqlCoreModule,
|
||||||
|
) {
|
||||||
|
private var active = true
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
active = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureActive(scope: ScopeFacade) {
|
||||||
|
if (!active) {
|
||||||
|
scope.raiseError(
|
||||||
|
ObjException(core.sqlUsageException, scope.requireScope(), ObjString("SQL result can be used only while its transaction is active"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlDatabaseObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val backend: SqlDatabaseBackend,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.databaseClass
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlTransactionObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val backend: SqlTransactionBackend,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.transactionClass
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlResultSetObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
|
data: SqlResultSetData,
|
||||||
|
) : Obj() {
|
||||||
|
val columns: List<Obj> = data.columns.map { SqlColumnObj(types, it) }
|
||||||
|
val rows: List<Obj> = buildRows(types, data)
|
||||||
|
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.resultSetClass
|
||||||
|
|
||||||
|
private fun buildRows(
|
||||||
|
types: SqlRuntimeTypes,
|
||||||
|
data: SqlResultSetData,
|
||||||
|
): List<Obj> {
|
||||||
|
val indexByName = linkedMapOf<String, MutableList<Int>>()
|
||||||
|
data.columns.forEachIndexed { index, column ->
|
||||||
|
indexByName.getOrPut(column.name.lowercase()) { mutableListOf() }.add(index)
|
||||||
|
}
|
||||||
|
return data.rows.map { rowValues ->
|
||||||
|
SqlRowObj(types, rowValues, indexByName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlRowObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val values: List<Obj>,
|
||||||
|
private val indexByName: Map<String, List<Int>>,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.rowClass
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
|
return when (index) {
|
||||||
|
is ObjInt -> {
|
||||||
|
val idx = index.value.toInt()
|
||||||
|
if (idx !in values.indices) {
|
||||||
|
scope.raiseIndexOutOfBounds("SQL row index $idx is out of bounds")
|
||||||
|
}
|
||||||
|
values[idx]
|
||||||
|
}
|
||||||
|
is ObjString -> {
|
||||||
|
val matches = indexByName[index.value.lowercase()]
|
||||||
|
?: scope.raiseError(
|
||||||
|
ObjException(
|
||||||
|
types.core.sqlUsageException,
|
||||||
|
scope,
|
||||||
|
ObjString("No such SQL result column: ${index.value}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (matches.size != 1) {
|
||||||
|
scope.raiseError(
|
||||||
|
ObjException(
|
||||||
|
types.core.sqlUsageException,
|
||||||
|
scope,
|
||||||
|
ObjString("Ambiguous SQL result column: ${index.value}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
values[matches.first()]
|
||||||
|
}
|
||||||
|
else -> scope.raiseClassCastError("SQL row index must be Int or String")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlColumnObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val meta: SqlColumnMeta,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.columnClass
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SqlExecutionResultObj(
|
||||||
|
val types: SqlRuntimeTypes,
|
||||||
|
val lifetime: SqlTransactionLifetime,
|
||||||
|
val result: SqlExecutionResultData,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = types.executionResultClass
|
||||||
|
}
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.db.jdbc
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.io.db.SqlCoreModule
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseBackend
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseObj
|
||||||
|
import net.sergeych.lyng.io.db.SqlRuntimeTypes
|
||||||
|
import net.sergeych.lyng.io.db.createDbModule
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjImmutableMap
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjMap
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyngio.stdlib_included.db_jdbcLyng
|
||||||
|
|
||||||
|
private const val JDBC_MODULE_NAME = "lyng.io.db.jdbc"
|
||||||
|
private const val DB_MODULE_NAME = "lyng.io.db"
|
||||||
|
private const val JDBC_SCHEME = "jdbc"
|
||||||
|
private const val H2_DRIVER = "org.h2.Driver"
|
||||||
|
private const val POSTGRES_DRIVER = "org.postgresql.Driver"
|
||||||
|
|
||||||
|
fun createJdbcModule(scope: Scope): Boolean = createJdbcModule(scope.importManager)
|
||||||
|
|
||||||
|
fun createJdbc(scope: Scope): Boolean = createJdbcModule(scope)
|
||||||
|
|
||||||
|
fun createJdbcModule(manager: ImportManager): Boolean {
|
||||||
|
createDbModule(manager)
|
||||||
|
if (manager.packageNames.contains(JDBC_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(JDBC_MODULE_NAME) { module ->
|
||||||
|
buildJdbcModule(module)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createJdbc(manager: ImportManager): Boolean = createJdbcModule(manager)
|
||||||
|
|
||||||
|
private suspend fun buildJdbcModule(module: ModuleScope) {
|
||||||
|
module.eval(Source(JDBC_MODULE_NAME, db_jdbcLyng))
|
||||||
|
val dbModule = module.importProvider.createModuleScope(Pos.builtIn, DB_MODULE_NAME)
|
||||||
|
val core = SqlCoreModule.resolve(dbModule)
|
||||||
|
val runtimeTypes = SqlRuntimeTypes.create("Jdbc", core)
|
||||||
|
|
||||||
|
module.addFn("openJdbc") {
|
||||||
|
val options = parseOpenJdbcArgs(this)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openJdbcBackend(this, core, options))
|
||||||
|
}
|
||||||
|
module.addFn("openH2") {
|
||||||
|
val options = parseOpenShortcutArgs(this, H2_DRIVER, ::normalizeH2Url)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openJdbcBackend(this, core, options))
|
||||||
|
}
|
||||||
|
module.addFn("openPostgres") {
|
||||||
|
val options = parseOpenShortcutArgs(this, POSTGRES_DRIVER, ::normalizePostgresUrl)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openJdbcBackend(this, core, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProvider(dbModule, runtimeTypes, core, JDBC_SCHEME, null)
|
||||||
|
registerProvider(dbModule, runtimeTypes, core, "h2", H2_DRIVER)
|
||||||
|
registerProvider(dbModule, runtimeTypes, core, "postgres", POSTGRES_DRIVER)
|
||||||
|
registerProvider(dbModule, runtimeTypes, core, "postgresql", POSTGRES_DRIVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun registerProvider(
|
||||||
|
dbModule: ModuleScope,
|
||||||
|
runtimeTypes: SqlRuntimeTypes,
|
||||||
|
core: SqlCoreModule,
|
||||||
|
scheme: String,
|
||||||
|
implicitDriverClass: String?,
|
||||||
|
) {
|
||||||
|
dbModule.callFn(
|
||||||
|
"registerDatabaseProvider",
|
||||||
|
ObjString(scheme),
|
||||||
|
net.sergeych.lyng.obj.ObjExternCallable.fromBridge {
|
||||||
|
val connectionUrl = requiredArg<ObjString>(0).value
|
||||||
|
val extraParams = args.list.getOrNull(1)
|
||||||
|
?: raiseError("Expected exactly 2 arguments, got ${args.list.size}")
|
||||||
|
val options = parseJdbcConnectionUrl(this, scheme, implicitDriverClass, connectionUrl, extraParams)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openJdbcBackend(this, core, options))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseOpenJdbcArgs(scope: ScopeFacade): JdbcOpenOptions {
|
||||||
|
val rawUrl = readArg(scope, "connectionUrl", 0) ?: scope.raiseError("argument 'connectionUrl' is required")
|
||||||
|
val connectionUrl = (rawUrl as? ObjString)?.value ?: scope.raiseClassCastError("connectionUrl must be String")
|
||||||
|
val user = readNullableStringArg(scope, "user", 1)
|
||||||
|
val password = readNullableStringArg(scope, "password", 2)
|
||||||
|
val driverClass = readNullableStringArg(scope, "driverClass", 3)
|
||||||
|
val properties = readPropertiesArg(scope, "properties", 4)
|
||||||
|
return JdbcOpenOptions(
|
||||||
|
connectionUrl = normalizeJdbcUrl(connectionUrl, scope),
|
||||||
|
user = user,
|
||||||
|
password = password,
|
||||||
|
driverClass = driverClass,
|
||||||
|
properties = properties,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseOpenShortcutArgs(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
defaultDriverClass: String,
|
||||||
|
normalize: (String, ScopeFacade) -> String,
|
||||||
|
): JdbcOpenOptions {
|
||||||
|
val rawUrl = readArg(scope, "connectionUrl", 0) ?: scope.raiseError("argument 'connectionUrl' is required")
|
||||||
|
val connectionUrl = (rawUrl as? ObjString)?.value ?: scope.raiseClassCastError("connectionUrl must be String")
|
||||||
|
val user = readNullableStringArg(scope, "user", 1)
|
||||||
|
val password = readNullableStringArg(scope, "password", 2)
|
||||||
|
val properties = readPropertiesArg(scope, "properties", 3)
|
||||||
|
return JdbcOpenOptions(
|
||||||
|
connectionUrl = normalize(connectionUrl, scope),
|
||||||
|
user = user,
|
||||||
|
password = password,
|
||||||
|
driverClass = defaultDriverClass,
|
||||||
|
properties = properties,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseJdbcConnectionUrl(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
scheme: String,
|
||||||
|
implicitDriverClass: String?,
|
||||||
|
connectionUrl: String,
|
||||||
|
extraParams: Obj,
|
||||||
|
): JdbcOpenOptions {
|
||||||
|
val driverClass = mapNullableString(extraParams, scope, "driverClass") ?: implicitDriverClass
|
||||||
|
val user = mapNullableString(extraParams, scope, "user")
|
||||||
|
val password = mapNullableString(extraParams, scope, "password")
|
||||||
|
val properties = mapProperties(extraParams, scope, "properties")
|
||||||
|
val normalizedUrl = when (scheme) {
|
||||||
|
JDBC_SCHEME -> normalizeJdbcUrl(connectionUrl, scope)
|
||||||
|
"h2" -> normalizeH2Url(connectionUrl, scope)
|
||||||
|
"postgres", "postgresql" -> normalizePostgresUrl(connectionUrl, scope)
|
||||||
|
else -> scope.raiseIllegalArgument("Unsupported JDBC provider scheme: $scheme")
|
||||||
|
}
|
||||||
|
return JdbcOpenOptions(
|
||||||
|
connectionUrl = normalizedUrl,
|
||||||
|
user = user,
|
||||||
|
password = password,
|
||||||
|
driverClass = driverClass,
|
||||||
|
properties = properties,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeJdbcUrl(rawUrl: String, scope: ScopeFacade): String {
|
||||||
|
val trimmed = rawUrl.trim()
|
||||||
|
if (!trimmed.startsWith("jdbc:", ignoreCase = true)) {
|
||||||
|
scope.raiseIllegalArgument("JDBC connection URL must start with jdbc:")
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeH2Url(rawUrl: String, scope: ScopeFacade): String {
|
||||||
|
val trimmed = rawUrl.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
scope.raiseIllegalArgument("H2 connection URL must not be empty")
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
trimmed.startsWith("jdbc:h2:", ignoreCase = true) -> trimmed
|
||||||
|
trimmed.startsWith("h2:", ignoreCase = true) -> "jdbc:${trimmed}"
|
||||||
|
else -> "jdbc:h2:$trimmed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizePostgresUrl(rawUrl: String, scope: ScopeFacade): String {
|
||||||
|
val trimmed = rawUrl.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
scope.raiseIllegalArgument("PostgreSQL connection URL must not be empty")
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
trimmed.startsWith("jdbc:postgresql:", ignoreCase = true) -> trimmed
|
||||||
|
trimmed.startsWith("postgresql:", ignoreCase = true) -> "jdbc:$trimmed"
|
||||||
|
trimmed.startsWith("postgres:", ignoreCase = true) -> "jdbc:postgresql:${trimmed.substringAfter(':')}"
|
||||||
|
else -> "jdbc:postgresql:$trimmed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readArg(scope: ScopeFacade, name: String, position: Int): Obj? {
|
||||||
|
val named = scope.args.named[name]
|
||||||
|
val positional = scope.args.list.getOrNull(position)
|
||||||
|
if (named != null && positional != null) {
|
||||||
|
scope.raiseIllegalArgument("argument '$name' is already set")
|
||||||
|
}
|
||||||
|
return named ?: positional
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readNullableStringArg(scope: ScopeFacade, name: String, position: Int): String? {
|
||||||
|
val value = readArg(scope, name, position) ?: return null
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjString -> value.value
|
||||||
|
else -> scope.raiseClassCastError("$name must be String?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readPropertiesArg(scope: ScopeFacade, name: String, position: Int): Map<String, String> {
|
||||||
|
val value = readArg(scope, name, position) ?: return emptyMap()
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> emptyMap()
|
||||||
|
else -> objToStringMap(value, scope, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapNullableString(map: Obj, scope: ScopeFacade, key: String): String? {
|
||||||
|
val value = map.getAt(scope.requireScope(), ObjString(key))
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjString -> value.value
|
||||||
|
else -> scope.raiseClassCastError("extraParams.$key must be String?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapProperties(map: Obj, scope: ScopeFacade, key: String): Map<String, String> {
|
||||||
|
val value = map.getAt(scope.requireScope(), ObjString(key))
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> emptyMap()
|
||||||
|
else -> objToStringMap(value, scope, "extraParams.$key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun objToStringMap(value: Obj, scope: ScopeFacade, label: String): Map<String, String> {
|
||||||
|
val rawEntries = when (value) {
|
||||||
|
is ObjMap -> value.map
|
||||||
|
is ObjImmutableMap -> value.map
|
||||||
|
else -> scope.raiseClassCastError("$label must be Map<String, Object?>")
|
||||||
|
}
|
||||||
|
val properties = linkedMapOf<String, String>()
|
||||||
|
for ((rawKey, rawValue) in rawEntries) {
|
||||||
|
val key = (rawKey as? ObjString)?.value ?: scope.raiseClassCastError("$label keys must be String")
|
||||||
|
if (rawValue == ObjNull) continue
|
||||||
|
properties[key] = scope.toStringOf(rawValue).value
|
||||||
|
}
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ModuleScope.callFn(name: String, vararg args: Obj): Obj {
|
||||||
|
val callee = get(name)?.value ?: error("Missing $name in module")
|
||||||
|
return callee.invoke(this, ObjNull, *args)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class JdbcOpenOptions(
|
||||||
|
val connectionUrl: String,
|
||||||
|
val user: String?,
|
||||||
|
val password: String?,
|
||||||
|
val driverClass: String?,
|
||||||
|
val properties: Map<String, String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal expect suspend fun openJdbcBackend(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
core: SqlCoreModule,
|
||||||
|
options: JdbcOpenOptions,
|
||||||
|
): SqlDatabaseBackend
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.db.sqlite
|
||||||
|
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
|
import net.sergeych.lyng.io.db.SqlCoreModule
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseBackend
|
||||||
|
import net.sergeych.lyng.io.db.SqlDatabaseObj
|
||||||
|
import net.sergeych.lyng.io.db.SqlRuntimeTypes
|
||||||
|
import net.sergeych.lyng.io.db.SqlTransactionBackend
|
||||||
|
import net.sergeych.lyng.io.db.createDbModule
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyngio.stdlib_included.db_sqliteLyng
|
||||||
|
|
||||||
|
private const val SQLITE_MODULE_NAME = "lyng.io.db.sqlite"
|
||||||
|
private const val DB_MODULE_NAME = "lyng.io.db"
|
||||||
|
|
||||||
|
fun createSqliteModule(scope: Scope): Boolean = createSqliteModule(scope.importManager)
|
||||||
|
|
||||||
|
fun createSqlite(scope: Scope): Boolean = createSqliteModule(scope)
|
||||||
|
|
||||||
|
fun createSqliteModule(manager: ImportManager): Boolean {
|
||||||
|
createDbModule(manager)
|
||||||
|
if (manager.packageNames.contains(SQLITE_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(SQLITE_MODULE_NAME) { module ->
|
||||||
|
buildSqliteModule(module)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSqlite(manager: ImportManager): Boolean = createSqliteModule(manager)
|
||||||
|
|
||||||
|
private suspend fun buildSqliteModule(module: ModuleScope) {
|
||||||
|
module.eval(Source(SQLITE_MODULE_NAME, db_sqliteLyng))
|
||||||
|
val dbModule = module.importProvider.createModuleScope(Pos.builtIn, DB_MODULE_NAME)
|
||||||
|
val core = SqlCoreModule.resolve(dbModule)
|
||||||
|
val runtimeTypes = SqlRuntimeTypes.create("Sqlite", core)
|
||||||
|
|
||||||
|
module.addFn("openSqlite") {
|
||||||
|
val options = parseOpenSqliteArgs(this)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openSqliteBackend(this, core, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
dbModule.callFn(
|
||||||
|
"registerDatabaseProvider",
|
||||||
|
ObjString("sqlite"),
|
||||||
|
net.sergeych.lyng.obj.ObjExternCallable.fromBridge {
|
||||||
|
val connectionUrl = requiredArg<ObjString>(0).value
|
||||||
|
val extraParams = args.list.getOrNull(1)
|
||||||
|
?: raiseError("Expected exactly 2 arguments, got ${args.list.size}")
|
||||||
|
val options = parseSqliteConnectionUrl(this, connectionUrl, extraParams)
|
||||||
|
SqlDatabaseObj(runtimeTypes, openSqliteBackend(this, core, options))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseOpenSqliteArgs(scope: ScopeFacade): SqliteOpenOptions {
|
||||||
|
val pathValue = readArg(scope, "path", 0) ?: scope.raiseError("argument 'path' is required")
|
||||||
|
val path = (pathValue as? ObjString)?.value ?: scope.raiseClassCastError("path must be String")
|
||||||
|
val readOnly = readBoolArg(scope, "readOnly", 1, false)
|
||||||
|
val createIfMissing = readBoolArg(scope, "createIfMissing", 2, true)
|
||||||
|
val foreignKeys = readBoolArg(scope, "foreignKeys", 3, true)
|
||||||
|
val busyTimeoutMillis = readIntArg(scope, "busyTimeoutMillis", 4, 5000)
|
||||||
|
return SqliteOpenOptions(
|
||||||
|
path = normalizeSqlitePath(path, scope),
|
||||||
|
readOnly = readOnly,
|
||||||
|
createIfMissing = createIfMissing,
|
||||||
|
foreignKeys = foreignKeys,
|
||||||
|
busyTimeoutMillis = busyTimeoutMillis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseSqliteConnectionUrl(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
connectionUrl: String,
|
||||||
|
extraParams: Obj,
|
||||||
|
): SqliteOpenOptions {
|
||||||
|
val prefix = "sqlite:"
|
||||||
|
if (!connectionUrl.startsWith(prefix, ignoreCase = true)) {
|
||||||
|
scope.raiseIllegalArgument("Malformed SQLite connection URL: $connectionUrl")
|
||||||
|
}
|
||||||
|
val rawPath = connectionUrl.substring(prefix.length)
|
||||||
|
val path = normalizeSqlitePath(rawPath, scope)
|
||||||
|
val readOnly = mapBool(extraParams, scope, "readOnly") ?: false
|
||||||
|
val createIfMissing = mapBool(extraParams, scope, "createIfMissing") ?: true
|
||||||
|
val foreignKeys = mapBool(extraParams, scope, "foreignKeys") ?: true
|
||||||
|
val busyTimeoutMillis = mapInt(extraParams, scope, "busyTimeoutMillis") ?: 5000
|
||||||
|
return SqliteOpenOptions(
|
||||||
|
path = path,
|
||||||
|
readOnly = readOnly,
|
||||||
|
createIfMissing = createIfMissing,
|
||||||
|
foreignKeys = foreignKeys,
|
||||||
|
busyTimeoutMillis = busyTimeoutMillis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeSqlitePath(rawPath: String, scope: ScopeFacade): String {
|
||||||
|
val path = rawPath.trim()
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
scope.raiseIllegalArgument("SQLite path must not be empty")
|
||||||
|
}
|
||||||
|
if (path.startsWith("//")) {
|
||||||
|
scope.raiseIllegalArgument("Unsupported SQLite URL form: sqlite:$path")
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readArg(scope: ScopeFacade, name: String, position: Int): Obj? {
|
||||||
|
val named = scope.args.named[name]
|
||||||
|
val positional = scope.args.list.getOrNull(position)
|
||||||
|
if (named != null && positional != null) {
|
||||||
|
scope.raiseIllegalArgument("argument '$name' is already set")
|
||||||
|
}
|
||||||
|
return named ?: positional
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readBoolArg(scope: ScopeFacade, name: String, position: Int, default: Boolean): Boolean {
|
||||||
|
val value = readArg(scope, name, position) ?: return default
|
||||||
|
return (value as? ObjBool)?.value ?: scope.raiseClassCastError("$name must be Bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readIntArg(scope: ScopeFacade, name: String, position: Int, default: Int): Int {
|
||||||
|
val value = readArg(scope, name, position) ?: return default
|
||||||
|
return when (value) {
|
||||||
|
is ObjInt -> value.value.toInt()
|
||||||
|
else -> scope.raiseClassCastError("$name must be Int")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapBool(map: Obj, scope: ScopeFacade, key: String): Boolean? {
|
||||||
|
val value = map.getAt(scope.requireScope(), ObjString(key))
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjBool -> value.value
|
||||||
|
else -> scope.raiseClassCastError("extraParams.$key must be Bool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapInt(map: Obj, scope: ScopeFacade, key: String): Int? {
|
||||||
|
val value = map.getAt(scope.requireScope(), ObjString(key))
|
||||||
|
return when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjInt -> value.value.toInt()
|
||||||
|
else -> scope.raiseClassCastError("extraParams.$key must be Int")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ModuleScope.callFn(name: String, vararg args: Obj): Obj {
|
||||||
|
val callee = get(name)?.value ?: error("Missing $name in module")
|
||||||
|
return callee.invoke(this, ObjNull, *args)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class SqliteOpenOptions(
|
||||||
|
val path: String,
|
||||||
|
val readOnly: Boolean,
|
||||||
|
val createIfMissing: Boolean,
|
||||||
|
val foreignKeys: Boolean,
|
||||||
|
val busyTimeoutMillis: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal expect suspend fun openSqliteBackend(
|
||||||
|
scope: ScopeFacade,
|
||||||
|
core: SqlCoreModule,
|
||||||
|
options: SqliteOpenOptions,
|
||||||
|
): SqlDatabaseBackend
|
||||||
@ -0,0 +1,390 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.http
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjBuffer
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjImmutableMap
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjList
|
||||||
|
import net.sergeych.lyng.obj.ObjMap
|
||||||
|
import net.sergeych.lyng.obj.ObjMapEntry
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.obj.thisAs
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.raiseIllegalOperation
|
||||||
|
import net.sergeych.lyng.requireNoArgs
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyngio.http.LyngHttpRequest
|
||||||
|
import net.sergeych.lyngio.http.LyngHttpResponse
|
||||||
|
import net.sergeych.lyngio.http.getSystemHttpEngine
|
||||||
|
import net.sergeych.lyngio.http.security.HttpAccessDeniedException
|
||||||
|
import net.sergeych.lyngio.http.security.HttpAccessOp
|
||||||
|
import net.sergeych.lyngio.http.security.HttpAccessPolicy
|
||||||
|
import net.sergeych.lyngio.stdlib_included.httpLyng
|
||||||
|
|
||||||
|
private const val HTTP_MODULE_NAME = "lyng.io.http"
|
||||||
|
|
||||||
|
fun createHttpModule(policy: HttpAccessPolicy, scope: Scope): Boolean =
|
||||||
|
createHttpModule(policy, scope.importManager)
|
||||||
|
|
||||||
|
fun createHttp(policy: HttpAccessPolicy, scope: Scope): Boolean = createHttpModule(policy, scope)
|
||||||
|
|
||||||
|
fun createHttpModule(policy: HttpAccessPolicy, manager: ImportManager): Boolean {
|
||||||
|
if (manager.packageNames.contains(HTTP_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(HTTP_MODULE_NAME) { module ->
|
||||||
|
buildHttpModule(module, policy)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHttp(policy: HttpAccessPolicy, manager: ImportManager): Boolean = createHttpModule(policy, manager)
|
||||||
|
|
||||||
|
private suspend fun buildHttpModule(module: ModuleScope, policy: HttpAccessPolicy) {
|
||||||
|
module.eval(Source(HTTP_MODULE_NAME, httpLyng))
|
||||||
|
val engine = getSystemHttpEngine()
|
||||||
|
|
||||||
|
val headersType = ObjHttpHeaders.type
|
||||||
|
val requestType = ObjHttpRequest.type
|
||||||
|
val responseType = ObjHttpResponse.type
|
||||||
|
|
||||||
|
val httpType = object : ObjClass("Http") {}
|
||||||
|
httpType.addClassFn("isSupported") {
|
||||||
|
ObjBool(engine.isSupported)
|
||||||
|
}
|
||||||
|
httpType.addClassFn("request") {
|
||||||
|
httpGuard {
|
||||||
|
val req = requiredArg<ObjHttpRequest>(0)
|
||||||
|
val built = req.toRequest(this)
|
||||||
|
policy.require(HttpAccessOp.Request(built.method, built.url))
|
||||||
|
ObjHttpResponse.from(engine.request(built))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpType.addClassFn("get") {
|
||||||
|
httpGuard {
|
||||||
|
val url = requiredArg<ObjString>(0).value
|
||||||
|
val headers = parseHeaderEntries(args.list.drop(1))
|
||||||
|
policy.require(HttpAccessOp.Request("GET", url))
|
||||||
|
ObjHttpResponse.from(engine.request(LyngHttpRequest(method = "GET", url = url, headers = headers)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpType.addClassFn("post") {
|
||||||
|
httpGuard {
|
||||||
|
val url = requiredArg<ObjString>(0).value
|
||||||
|
val bodyText = requiredArg<ObjString>(1).value
|
||||||
|
val contentType = args.list.getOrNull(2)?.let { objOrNullToString(this, it) }
|
||||||
|
val headers = parseHeaderEntries(args.list.drop(3)).toMutableMap()
|
||||||
|
if (contentType != null && "Content-Type" !in headers) headers["Content-Type"] = contentType
|
||||||
|
policy.require(HttpAccessOp.Request("POST", url))
|
||||||
|
ObjHttpResponse.from(
|
||||||
|
engine.request(
|
||||||
|
LyngHttpRequest(method = "POST", url = url, headers = headers, bodyText = bodyText)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpType.addClassFn("postBytes") {
|
||||||
|
httpGuard {
|
||||||
|
val url = requiredArg<ObjString>(0).value
|
||||||
|
val body = requiredArg<ObjBuffer>(1).byteArray.toByteArray()
|
||||||
|
val contentType = args.list.getOrNull(2)?.let { objOrNullToString(this, it) }
|
||||||
|
val headers = parseHeaderEntries(args.list.drop(3)).toMutableMap()
|
||||||
|
if (contentType != null && "Content-Type" !in headers) headers["Content-Type"] = contentType
|
||||||
|
policy.require(HttpAccessOp.Request("POST", url))
|
||||||
|
ObjHttpResponse.from(
|
||||||
|
engine.request(
|
||||||
|
LyngHttpRequest(method = "POST", url = url, headers = headers, bodyBytes = body)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.addConst("Http", httpType)
|
||||||
|
module.addConst("HttpHeaders", headersType)
|
||||||
|
module.addConst("HttpRequest", requestType)
|
||||||
|
module.addConst("HttpResponse", responseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun ScopeFacade.httpGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
|
return try {
|
||||||
|
block()
|
||||||
|
} catch (e: HttpAccessDeniedException) {
|
||||||
|
raiseIllegalOperation(e.reasonDetail ?: "http access denied")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
raiseIllegalOperation(e.message ?: "http error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjHttpHeaders(
|
||||||
|
singleValueHeaders: Map<String, String> = emptyMap(),
|
||||||
|
private val allHeaders: Map<String, List<String>> = emptyMap(),
|
||||||
|
) : Obj() {
|
||||||
|
private val entries: LinkedHashMap<Obj, Obj> =
|
||||||
|
LinkedHashMap(singleValueHeaders.entries.associate { ObjString(it.key) to ObjString(it.value) })
|
||||||
|
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type
|
||||||
|
|
||||||
|
override suspend fun getAt(scope: Scope, index: Obj): Obj = findEntry(index)?.value ?: ObjNull
|
||||||
|
|
||||||
|
override suspend fun contains(scope: Scope, other: Obj): Boolean = findEntry(other) != null
|
||||||
|
|
||||||
|
override suspend fun defaultToString(scope: Scope): ObjString {
|
||||||
|
val rendered = buildString {
|
||||||
|
append("HttpHeaders(")
|
||||||
|
var first = true
|
||||||
|
for ((k, v) in entries) {
|
||||||
|
if (!first) append(", ")
|
||||||
|
append(k.toString(scope).value)
|
||||||
|
append(" => ")
|
||||||
|
append(v.toString(scope).value)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
return ObjString(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("HttpHeaders", ObjMap.type) {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj = ObjHttpHeaders()
|
||||||
|
}.apply {
|
||||||
|
addFn("get") {
|
||||||
|
val self = thisAs<ObjHttpHeaders>()
|
||||||
|
val name = requiredArg<ObjString>(0).value
|
||||||
|
self.firstValue(name)?.let(::ObjString) ?: ObjNull
|
||||||
|
}
|
||||||
|
addFn("getAll") {
|
||||||
|
val self = thisAs<ObjHttpHeaders>()
|
||||||
|
val name = requiredArg<ObjString>(0).value
|
||||||
|
ObjList(self.valuesOf(name).map(::ObjString).toMutableList())
|
||||||
|
}
|
||||||
|
addFn("names") {
|
||||||
|
val self = thisAs<ObjHttpHeaders>()
|
||||||
|
ObjList(self.allHeaders.keys.map(::ObjString).toMutableList())
|
||||||
|
}
|
||||||
|
addFn("getOrNull") {
|
||||||
|
val self = thisAs<ObjHttpHeaders>()
|
||||||
|
val name = requiredArg<ObjString>(0).value
|
||||||
|
self.firstValue(name)?.let(::ObjString) ?: ObjNull
|
||||||
|
}
|
||||||
|
addProperty("size", getter = { ObjInt(thisAs<ObjHttpHeaders>().entries.size.toLong()) })
|
||||||
|
addProperty("keys", getter = { ObjList(thisAs<ObjHttpHeaders>().entries.keys.toMutableList()) })
|
||||||
|
addProperty("values", getter = { ObjList(thisAs<ObjHttpHeaders>().entries.values.toMutableList()) })
|
||||||
|
addFn("iterator") {
|
||||||
|
ObjList(
|
||||||
|
thisAs<ObjHttpHeaders>().entries.map { (k, v) -> ObjMapEntry(k, v) }.toMutableList()
|
||||||
|
).invokeInstanceMethod(requireScope(), "iterator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun valuesOf(name: String): List<String> = allHeaders[lookupKey(name)] ?: emptyList()
|
||||||
|
|
||||||
|
private fun firstValue(name: String): String? = valuesOf(name).firstOrNull()
|
||||||
|
|
||||||
|
private fun lookupKey(name: String): String =
|
||||||
|
allHeaders.keys.firstOrNull { it.equals(name, ignoreCase = true) } ?: name
|
||||||
|
|
||||||
|
private fun findEntry(index: Obj): Map.Entry<Obj, Obj>? {
|
||||||
|
if (index is ObjString) {
|
||||||
|
return entries.entries.firstOrNull { (k, _) ->
|
||||||
|
(k as? ObjString)?.value?.equals(index.value, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries.entries.firstOrNull { it.key == index }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjHttpRequest(
|
||||||
|
var method: String = "GET",
|
||||||
|
var url: String = "",
|
||||||
|
val headers: MutableMap<String, String> = linkedMapOf(),
|
||||||
|
var bodyText: String? = null,
|
||||||
|
var bodyBytes: ByteArray? = null,
|
||||||
|
var timeoutMillis: Long? = null,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type
|
||||||
|
|
||||||
|
suspend fun toRequest(scope: ScopeFacade): LyngHttpRequest {
|
||||||
|
if (bodyText != null && bodyBytes != null) {
|
||||||
|
scope.raiseIllegalArgument("Only one of bodyText or bodyBytes may be set")
|
||||||
|
}
|
||||||
|
return LyngHttpRequest(
|
||||||
|
method = method,
|
||||||
|
url = url,
|
||||||
|
headers = LinkedHashMap(headers),
|
||||||
|
bodyText = bodyText,
|
||||||
|
bodyBytes = bodyBytes,
|
||||||
|
timeoutMillis = timeoutMillis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("HttpRequest") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
if (scope.args.list.isNotEmpty()) scope.raiseError("HttpRequest() does not accept arguments")
|
||||||
|
return ObjHttpRequest()
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addProperty("method",
|
||||||
|
getter = { ObjString(thisAs<ObjHttpRequest>().method) },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().method = objOrNullToString(this, value)
|
||||||
|
?: raiseIllegalArgument("method cannot be null")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addProperty("url",
|
||||||
|
getter = { ObjString(thisAs<ObjHttpRequest>().url) },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().url = objOrNullToString(this, value)
|
||||||
|
?: raiseIllegalArgument("url cannot be null")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addProperty("headers",
|
||||||
|
getter = { thisAs<ObjHttpRequest>().headers.toObjMap() },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().headers.clear()
|
||||||
|
thisAs<ObjHttpRequest>().headers.putAll(mapObjToStrings(this, value))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addProperty("bodyText",
|
||||||
|
getter = { thisAs<ObjHttpRequest>().bodyText?.let(::ObjString) ?: ObjNull },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().bodyText = objOrNullToString(this, value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addProperty("bodyBytes",
|
||||||
|
getter = { thisAs<ObjHttpRequest>().bodyBytes?.let { ObjBuffer(it.toUByteArray()) } ?: ObjNull },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().bodyBytes = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjBuffer -> value.byteArray.toByteArray()
|
||||||
|
else -> raiseClassCastError("bodyBytes must be Buffer or null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addProperty("timeoutMillis",
|
||||||
|
getter = { thisAs<ObjHttpRequest>().timeoutMillis?.let { ObjInt(it) } ?: ObjNull },
|
||||||
|
setter = { value ->
|
||||||
|
thisAs<ObjHttpRequest>().timeoutMillis = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjInt -> value.value
|
||||||
|
else -> raiseClassCastError("timeoutMillis must be Int or null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjHttpResponse(
|
||||||
|
val status: Long,
|
||||||
|
val statusText: String,
|
||||||
|
val headers: ObjHttpHeaders,
|
||||||
|
private val bodyBytes: ByteArray,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("HttpResponse") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("HttpResponse cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addProperty("status", getter = { ObjInt(thisAs<ObjHttpResponse>().status) })
|
||||||
|
addProperty("statusText", getter = { ObjString(thisAs<ObjHttpResponse>().statusText) })
|
||||||
|
addProperty("headers", getter = { thisAs<ObjHttpResponse>().headers })
|
||||||
|
addFn("text") {
|
||||||
|
ObjString(thisAs<ObjHttpResponse>().bodyBytes.decodeToString())
|
||||||
|
}
|
||||||
|
addFn("bytes") {
|
||||||
|
ObjBuffer(thisAs<ObjHttpResponse>().bodyBytes.toUByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(response: LyngHttpResponse): ObjHttpResponse {
|
||||||
|
val single = linkedMapOf<String, String>()
|
||||||
|
response.headers.forEach { (name, values) ->
|
||||||
|
if (values.isNotEmpty() && !single.containsKey(name)) {
|
||||||
|
single[name] = values.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ObjHttpResponse(
|
||||||
|
status = response.status.toLong(),
|
||||||
|
statusText = response.statusText,
|
||||||
|
headers = ObjHttpHeaders(singleValueHeaders = single, allHeaders = response.headers),
|
||||||
|
bodyBytes = response.bodyBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.parseHeaderEntries(values: List<Obj>): Map<String, String> {
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
values.forEach { value ->
|
||||||
|
when (value) {
|
||||||
|
is ObjMapEntry -> {
|
||||||
|
out[toStringOf(value.key).value] = toStringOf(value.value).value
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (!value.isInstanceOf(net.sergeych.lyng.obj.ObjArray)) {
|
||||||
|
raiseIllegalArgument("headers entries must be MapEntry or [key, value]")
|
||||||
|
}
|
||||||
|
val size = (value.invokeInstanceMethod(requireScope(), "size") as ObjInt).value.toInt()
|
||||||
|
if (size != 2) {
|
||||||
|
raiseIllegalArgument("header entry array must contain exactly 2 items")
|
||||||
|
}
|
||||||
|
out[toStringOf(value.getAt(requireScope(), ObjInt.Zero)).value] =
|
||||||
|
toStringOf(value.getAt(requireScope(), ObjInt.One)).value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapObjToStrings(scope: ScopeFacade, value: Obj): MutableMap<String, String> {
|
||||||
|
val entries = when (value) {
|
||||||
|
is ObjMap -> value.map
|
||||||
|
is ObjImmutableMap -> value.map
|
||||||
|
ObjNull -> return linkedMapOf()
|
||||||
|
else -> scope.raiseClassCastError("headers must be Map<String, String>")
|
||||||
|
}
|
||||||
|
return entries.entries.associateTo(linkedMapOf()) { (k, v) ->
|
||||||
|
scope.toStringOf(k).value to scope.toStringOf(v).value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun objOrNullToString(scope: ScopeFacade, value: Obj): String? = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
else -> scope.toStringOf(value).value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String>.toObjMap(): ObjMap =
|
||||||
|
ObjMap(entries.associate { ObjString(it.key) to ObjString(it.value) }.toMutableMap())
|
||||||
@ -0,0 +1,376 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.net
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjBuffer
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjList
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.obj.thisAs
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.raiseIllegalOperation
|
||||||
|
import net.sergeych.lyng.requireNoArgs
|
||||||
|
import net.sergeych.lyngio.net.LyngDatagram
|
||||||
|
import net.sergeych.lyngio.net.LyngIpVersion
|
||||||
|
import net.sergeych.lyngio.net.LyngNetEngine
|
||||||
|
import net.sergeych.lyngio.net.LyngSocketAddress
|
||||||
|
import net.sergeych.lyngio.net.LyngTcpServer
|
||||||
|
import net.sergeych.lyngio.net.LyngTcpSocket
|
||||||
|
import net.sergeych.lyngio.net.LyngUdpSocket
|
||||||
|
import net.sergeych.lyngio.net.getSystemNetEngine
|
||||||
|
import net.sergeych.lyngio.net.security.NetAccessDeniedException
|
||||||
|
import net.sergeych.lyngio.net.security.NetAccessOp
|
||||||
|
import net.sergeych.lyngio.net.security.NetAccessPolicy
|
||||||
|
import net.sergeych.lyngio.stdlib_included.netLyng
|
||||||
|
|
||||||
|
private const val NET_MODULE_NAME = "lyng.io.net"
|
||||||
|
|
||||||
|
fun createNetModule(policy: NetAccessPolicy, scope: Scope): Boolean =
|
||||||
|
createNetModule(policy, scope.importManager)
|
||||||
|
|
||||||
|
fun createNet(policy: NetAccessPolicy, scope: Scope): Boolean = createNetModule(policy, scope)
|
||||||
|
|
||||||
|
fun createNetModule(policy: NetAccessPolicy, manager: ImportManager): Boolean {
|
||||||
|
if (manager.packageNames.contains(NET_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(NET_MODULE_NAME) { module ->
|
||||||
|
buildNetModule(module, policy)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNet(policy: NetAccessPolicy, manager: ImportManager): Boolean = createNetModule(policy, manager)
|
||||||
|
|
||||||
|
private suspend fun buildNetModule(module: ModuleScope, policy: NetAccessPolicy) {
|
||||||
|
module.eval(Source(NET_MODULE_NAME, netLyng))
|
||||||
|
val engine = getSystemNetEngine()
|
||||||
|
val enumValues = NetEnumValues.load(module)
|
||||||
|
|
||||||
|
val netType = object : ObjClass("Net") {}
|
||||||
|
netType.addClassFn("isSupported") { ObjBool(engine.isSupported) }
|
||||||
|
netType.addClassFn("isTcpAvailable") { ObjBool(engine.isTcpAvailable) }
|
||||||
|
netType.addClassFn("isTcpServerAvailable") { ObjBool(engine.isTcpServerAvailable) }
|
||||||
|
netType.addClassFn("isUdpAvailable") { ObjBool(engine.isUdpAvailable) }
|
||||||
|
netType.addClassFn("resolve") {
|
||||||
|
netGuard {
|
||||||
|
val host = requiredArg<ObjString>(0).value
|
||||||
|
val port = requirePort(requiredArg<ObjInt>(1).value)
|
||||||
|
policy.require(NetAccessOp.Resolve(host, port))
|
||||||
|
ObjList(engine.resolve(host, port).map { ObjSocketAddress(it, enumValues) }.toMutableList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
netType.addClassFn("tcpConnect") {
|
||||||
|
netGuard {
|
||||||
|
val host = requiredArg<ObjString>(0).value
|
||||||
|
val port = requirePort(requiredArg<ObjInt>(1).value)
|
||||||
|
val timeoutMillis = args.list.getOrNull(2)?.let { objOrNullToLong(this, it, "timeoutMillis") }
|
||||||
|
val noDelay = args.list.getOrNull(3)?.let { objToBool(this, it, "noDelay") } ?: true
|
||||||
|
policy.require(NetAccessOp.TcpConnect(host, port))
|
||||||
|
ObjTcpSocket(engine.tcpConnect(host, port, timeoutMillis, noDelay), enumValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
netType.addClassFn("tcpListen") {
|
||||||
|
netGuard {
|
||||||
|
val port = requirePort(requiredArg<ObjInt>(0).value)
|
||||||
|
val host = args.list.getOrNull(1)?.let { objOrNullToString(this, it, "host") }
|
||||||
|
val backlog = args.list.getOrNull(2)?.let { objToInt(this, it, "backlog") } ?: 128
|
||||||
|
requirePositive(backlog, "backlog")
|
||||||
|
val reuseAddress = args.list.getOrNull(3)?.let { objToBool(this, it, "reuseAddress") } ?: true
|
||||||
|
policy.require(NetAccessOp.TcpListen(host, port, backlog))
|
||||||
|
ObjTcpServer(engine.tcpListen(host, port, backlog, reuseAddress), enumValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
netType.addClassFn("udpBind") {
|
||||||
|
netGuard {
|
||||||
|
val port = args.list.getOrNull(0)?.let { objToInt(this, it, "port") } ?: 0
|
||||||
|
requirePort(port)
|
||||||
|
val host = args.list.getOrNull(1)?.let { objOrNullToString(this, it, "host") }
|
||||||
|
val reuseAddress = args.list.getOrNull(2)?.let { objToBool(this, it, "reuseAddress") } ?: true
|
||||||
|
policy.require(NetAccessOp.UdpBind(host, port))
|
||||||
|
ObjUdpSocket(engine.udpBind(host, port, reuseAddress), enumValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.addConst("Net", netType)
|
||||||
|
module.addConst("SocketAddress", ObjSocketAddress.type(enumValues))
|
||||||
|
module.addConst("Datagram", ObjDatagram.type(enumValues))
|
||||||
|
module.addConst("TcpSocket", ObjTcpSocket.type(enumValues))
|
||||||
|
module.addConst("TcpServer", ObjTcpServer.type(enumValues))
|
||||||
|
module.addConst("UdpSocket", ObjUdpSocket.type(enumValues))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun ScopeFacade.netGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
|
return try {
|
||||||
|
block()
|
||||||
|
} catch (e: NetAccessDeniedException) {
|
||||||
|
raiseIllegalOperation(e.reasonDetail ?: "network access denied")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
raiseIllegalOperation(e.message ?: "network error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NetEnumValues(
|
||||||
|
val ipv4: Obj,
|
||||||
|
val ipv6: Obj,
|
||||||
|
) {
|
||||||
|
fun of(version: LyngIpVersion): Obj = when (version) {
|
||||||
|
LyngIpVersion.IPV4 -> ipv4
|
||||||
|
LyngIpVersion.IPV6 -> ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend fun load(module: ModuleScope): NetEnumValues {
|
||||||
|
val ipVersionClass = module["IpVersion"]?.value as? ObjClass
|
||||||
|
?: error("lyng.io.net.IpVersion is missing after declaration load")
|
||||||
|
return NetEnumValues(
|
||||||
|
ipv4 = ipVersionClass.readField(module, "IPV4").value,
|
||||||
|
ipv6 = ipVersionClass.readField(module, "IPV6").value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjSocketAddress(
|
||||||
|
private val address: LyngSocketAddress,
|
||||||
|
private val enumValues: NetEnumValues,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type(enumValues)
|
||||||
|
|
||||||
|
override suspend fun defaultToString(scope: Scope): ObjString = ObjString(renderAddress(address))
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val types = mutableMapOf<NetEnumValues, ObjClass>()
|
||||||
|
|
||||||
|
fun type(enumValues: NetEnumValues): ObjClass =
|
||||||
|
types.getOrPut(enumValues) {
|
||||||
|
object : ObjClass("SocketAddress") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("SocketAddress cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addProperty("host", getter = { ObjString(thisAs<ObjSocketAddress>().address.host) })
|
||||||
|
addProperty("port", getter = { ObjInt(thisAs<ObjSocketAddress>().address.port.toLong()) })
|
||||||
|
addProperty("ipVersion", getter = { enumValues.of(thisAs<ObjSocketAddress>().address.ipVersion) })
|
||||||
|
addProperty("resolved", getter = { ObjBool(thisAs<ObjSocketAddress>().address.resolved) })
|
||||||
|
addFn("toString") { ObjString(renderAddress(thisAs<ObjSocketAddress>().address)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjDatagram(
|
||||||
|
private val datagram: LyngDatagram,
|
||||||
|
private val enumValues: NetEnumValues,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type(enumValues)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val types = mutableMapOf<NetEnumValues, ObjClass>()
|
||||||
|
|
||||||
|
fun type(enumValues: NetEnumValues): ObjClass =
|
||||||
|
types.getOrPut(enumValues) {
|
||||||
|
object : ObjClass("Datagram") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("Datagram cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addProperty("data", getter = {
|
||||||
|
ObjBuffer(thisAs<ObjDatagram>().datagram.data.toUByteArray())
|
||||||
|
})
|
||||||
|
addProperty("address", getter = {
|
||||||
|
ObjSocketAddress(thisAs<ObjDatagram>().datagram.address, enumValues)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjTcpSocket(
|
||||||
|
private val socket: LyngTcpSocket,
|
||||||
|
private val enumValues: NetEnumValues,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type(enumValues)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val types = mutableMapOf<NetEnumValues, ObjClass>()
|
||||||
|
|
||||||
|
fun type(enumValues: NetEnumValues): ObjClass =
|
||||||
|
types.getOrPut(enumValues) {
|
||||||
|
object : ObjClass("TcpSocket") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("TcpSocket cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("isOpen") { ObjBool(thisAs<ObjTcpSocket>().socket.isOpen()) }
|
||||||
|
addFn("localAddress") { ObjSocketAddress(thisAs<ObjTcpSocket>().socket.localAddress(), enumValues) }
|
||||||
|
addFn("remoteAddress") { ObjSocketAddress(thisAs<ObjTcpSocket>().socket.remoteAddress(), enumValues) }
|
||||||
|
addFn("read") {
|
||||||
|
val maxBytes = args.list.getOrNull(0)?.let { objToInt(this, it, "maxBytes") } ?: 65536
|
||||||
|
requirePositive(maxBytes, "maxBytes")
|
||||||
|
thisAs<ObjTcpSocket>().socket.read(maxBytes)?.let { ObjBuffer(it.toUByteArray()) } ?: ObjNull
|
||||||
|
}
|
||||||
|
addFn("readLine") {
|
||||||
|
thisAs<ObjTcpSocket>().socket.readLine()?.let(::ObjString) ?: ObjNull
|
||||||
|
}
|
||||||
|
addFn("write") {
|
||||||
|
val data = requiredArg<ObjBuffer>(0).byteArray.toByteArray()
|
||||||
|
thisAs<ObjTcpSocket>().socket.write(data)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("writeUtf8") {
|
||||||
|
val text = requiredArg<ObjString>(0).value
|
||||||
|
thisAs<ObjTcpSocket>().socket.writeUtf8(text)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("flush") {
|
||||||
|
requireNoArgs()
|
||||||
|
thisAs<ObjTcpSocket>().socket.flush()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("close") {
|
||||||
|
requireNoArgs()
|
||||||
|
thisAs<ObjTcpSocket>().socket.close()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjTcpServer(
|
||||||
|
private val server: LyngTcpServer,
|
||||||
|
private val enumValues: NetEnumValues,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type(enumValues)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val types = mutableMapOf<NetEnumValues, ObjClass>()
|
||||||
|
|
||||||
|
fun type(enumValues: NetEnumValues): ObjClass =
|
||||||
|
types.getOrPut(enumValues) {
|
||||||
|
object : ObjClass("TcpServer") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("TcpServer cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("isOpen") { ObjBool(thisAs<ObjTcpServer>().server.isOpen()) }
|
||||||
|
addFn("localAddress") { ObjSocketAddress(thisAs<ObjTcpServer>().server.localAddress(), enumValues) }
|
||||||
|
addFn("accept") {
|
||||||
|
ObjTcpSocket(thisAs<ObjTcpServer>().server.accept(), enumValues)
|
||||||
|
}
|
||||||
|
addFn("close") {
|
||||||
|
requireNoArgs()
|
||||||
|
thisAs<ObjTcpServer>().server.close()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjUdpSocket(
|
||||||
|
private val socket: LyngUdpSocket,
|
||||||
|
private val enumValues: NetEnumValues,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type(enumValues)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val types = mutableMapOf<NetEnumValues, ObjClass>()
|
||||||
|
|
||||||
|
fun type(enumValues: NetEnumValues): ObjClass =
|
||||||
|
types.getOrPut(enumValues) {
|
||||||
|
object : ObjClass("UdpSocket") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("UdpSocket cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("isOpen") { ObjBool(thisAs<ObjUdpSocket>().socket.isOpen()) }
|
||||||
|
addFn("localAddress") { ObjSocketAddress(thisAs<ObjUdpSocket>().socket.localAddress(), enumValues) }
|
||||||
|
addFn("receive") {
|
||||||
|
val maxBytes = args.list.getOrNull(0)?.let { objToInt(this, it, "maxBytes") } ?: 65536
|
||||||
|
requirePositive(maxBytes, "maxBytes")
|
||||||
|
thisAs<ObjUdpSocket>().socket.receive(maxBytes)?.let { ObjDatagram(it, enumValues) } ?: ObjNull
|
||||||
|
}
|
||||||
|
addFn("send") {
|
||||||
|
val data = requiredArg<ObjBuffer>(0).byteArray.toByteArray()
|
||||||
|
val host = requiredArg<ObjString>(1).value
|
||||||
|
val port = requirePort(requiredArg<ObjInt>(2).value)
|
||||||
|
thisAs<ObjUdpSocket>().socket.send(data, host, port)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("close") {
|
||||||
|
requireNoArgs()
|
||||||
|
thisAs<ObjUdpSocket>().socket.close()
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAddress(address: LyngSocketAddress): String =
|
||||||
|
if (address.ipVersion == LyngIpVersion.IPV6) "[${address.host}]:${address.port}" else "${address.host}:${address.port}"
|
||||||
|
|
||||||
|
private fun ScopeFacade.requirePort(value: Long): Int {
|
||||||
|
if (value !in 0..65535) raiseIllegalArgument("port must be in 0..65535")
|
||||||
|
return value.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScopeFacade.requirePort(value: Int): Int {
|
||||||
|
if (value !in 0..65535) raiseIllegalArgument("port must be in 0..65535")
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScopeFacade.requirePositive(value: Int, name: String) {
|
||||||
|
if (value <= 0) raiseIllegalArgument("$name must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun objOrNullToString(scope: ScopeFacade, value: Obj, name: String): String? = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
else -> scope.toStringOf(value).value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun objToInt(scope: ScopeFacade, value: Obj, name: String): Int = when (value) {
|
||||||
|
is ObjInt -> value.value.toInt()
|
||||||
|
else -> scope.raiseClassCastError("$name must be Int")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun objToBool(scope: ScopeFacade, value: Obj, name: String): Boolean = when (value) {
|
||||||
|
is ObjBool -> value.value
|
||||||
|
else -> scope.raiseClassCastError("$name must be Bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun objOrNullToLong(scope: ScopeFacade, value: Obj, name: String): Long? = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
is ObjInt -> value.value
|
||||||
|
else -> scope.raiseClassCastError("$name must be Int or null")
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.ws
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjBuffer
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjMapEntry
|
||||||
|
import net.sergeych.lyng.obj.ObjNull
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
import net.sergeych.lyng.obj.requiredArg
|
||||||
|
import net.sergeych.lyng.obj.thisAs
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.raiseIllegalOperation
|
||||||
|
import net.sergeych.lyng.requireNoArgs
|
||||||
|
import net.sergeych.lyng.requireScope
|
||||||
|
import net.sergeych.lyngio.stdlib_included.wsLyng
|
||||||
|
import net.sergeych.lyngio.ws.LyngWsEngine
|
||||||
|
import net.sergeych.lyngio.ws.LyngWsMessage
|
||||||
|
import net.sergeych.lyngio.ws.LyngWsSession
|
||||||
|
import net.sergeych.lyngio.ws.getSystemWsEngine
|
||||||
|
import net.sergeych.lyngio.ws.security.WsAccessDeniedException
|
||||||
|
import net.sergeych.lyngio.ws.security.WsAccessOp
|
||||||
|
import net.sergeych.lyngio.ws.security.WsAccessPolicy
|
||||||
|
|
||||||
|
private const val WS_MODULE_NAME = "lyng.io.ws"
|
||||||
|
|
||||||
|
fun createWsModule(policy: WsAccessPolicy, scope: Scope): Boolean =
|
||||||
|
createWsModule(policy, scope.importManager)
|
||||||
|
|
||||||
|
fun createWs(policy: WsAccessPolicy, scope: Scope): Boolean = createWsModule(policy, scope)
|
||||||
|
|
||||||
|
fun createWsModule(policy: WsAccessPolicy, manager: ImportManager): Boolean {
|
||||||
|
if (manager.packageNames.contains(WS_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(WS_MODULE_NAME) { module ->
|
||||||
|
buildWsModule(module, policy)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWs(policy: WsAccessPolicy, manager: ImportManager): Boolean = createWsModule(policy, manager)
|
||||||
|
|
||||||
|
private suspend fun buildWsModule(module: ModuleScope, policy: WsAccessPolicy) {
|
||||||
|
module.eval(Source(WS_MODULE_NAME, wsLyng))
|
||||||
|
val engine = getSystemWsEngine()
|
||||||
|
|
||||||
|
val wsType = object : ObjClass("Ws") {}
|
||||||
|
wsType.addClassFn("isSupported") { ObjBool(engine.isSupported) }
|
||||||
|
wsType.addClassFn("connect") {
|
||||||
|
wsGuard {
|
||||||
|
val url = requiredArg<ObjString>(0).value
|
||||||
|
val headers = parseHeaderEntries(args.list.drop(1))
|
||||||
|
policy.require(WsAccessOp.Connect(url))
|
||||||
|
ObjWsSession(url, engine.connect(url, headers), policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.addConst("Ws", wsType)
|
||||||
|
module.addConst("WsMessage", ObjWsMessage.type)
|
||||||
|
module.addConst("WsSession", ObjWsSession.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun ScopeFacade.wsGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
|
return try {
|
||||||
|
block()
|
||||||
|
} catch (e: WsAccessDeniedException) {
|
||||||
|
raiseIllegalOperation(e.reasonDetail ?: "websocket access denied")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
raiseIllegalOperation(e.message ?: "websocket error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjWsMessage(
|
||||||
|
private val message: LyngWsMessage,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("WsMessage") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("WsMessage cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addProperty("isText", getter = { ObjBool(thisAs<ObjWsMessage>().message.isText) })
|
||||||
|
addProperty("text", getter = {
|
||||||
|
thisAs<ObjWsMessage>().message.text?.let(::ObjString) ?: ObjNull
|
||||||
|
})
|
||||||
|
addProperty("data", getter = {
|
||||||
|
thisAs<ObjWsMessage>().message.data?.let { ObjBuffer(it.toUByteArray()) } ?: ObjNull
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ObjWsSession(
|
||||||
|
private val targetUrl: String,
|
||||||
|
private val session: LyngWsSession,
|
||||||
|
private val policy: WsAccessPolicy,
|
||||||
|
) : Obj() {
|
||||||
|
override val objClass: ObjClass
|
||||||
|
get() = type
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("WsSession") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
scope.raiseError("WsSession cannot be created directly")
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addFn("isOpen") {
|
||||||
|
ObjBool(thisAs<ObjWsSession>().session.isOpen())
|
||||||
|
}
|
||||||
|
addFn("url") {
|
||||||
|
ObjString(thisAs<ObjWsSession>().targetUrl)
|
||||||
|
}
|
||||||
|
addFn("sendText") {
|
||||||
|
val self = thisAs<ObjWsSession>()
|
||||||
|
val text = requiredArg<ObjString>(0).value
|
||||||
|
self.policy.require(WsAccessOp.Send(self.targetUrl, text.encodeToByteArray().size, isText = true))
|
||||||
|
self.session.sendText(text)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("sendBytes") {
|
||||||
|
val self = thisAs<ObjWsSession>()
|
||||||
|
val data = requiredArg<ObjBuffer>(0).byteArray.toByteArray()
|
||||||
|
self.policy.require(WsAccessOp.Send(self.targetUrl, data.size, isText = false))
|
||||||
|
self.session.sendBytes(data)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
addFn("receive") {
|
||||||
|
val self = thisAs<ObjWsSession>()
|
||||||
|
self.policy.require(WsAccessOp.Receive(self.targetUrl))
|
||||||
|
self.session.receive()?.let(::ObjWsMessage) ?: ObjNull
|
||||||
|
}
|
||||||
|
addFn("close") {
|
||||||
|
val self = thisAs<ObjWsSession>()
|
||||||
|
val code = args.list.getOrNull(0)?.let { objToInt(this, it, "code") } ?: 1000
|
||||||
|
val reason = args.list.getOrNull(1)?.let { objOrNullToString(this, it, "reason") } ?: ""
|
||||||
|
self.session.close(code, reason)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.parseHeaderEntries(values: List<Obj>): Map<String, String> {
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
values.forEach { value ->
|
||||||
|
when (value) {
|
||||||
|
is ObjMapEntry -> {
|
||||||
|
out[toStringOf(value.key).value] = toStringOf(value.value).value
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (!value.isInstanceOf(net.sergeych.lyng.obj.ObjArray)) {
|
||||||
|
raiseIllegalArgument("headers entries must be MapEntry or [key, value]")
|
||||||
|
}
|
||||||
|
val size = (value.invokeInstanceMethod(requireScope(), "size") as ObjInt).value.toInt()
|
||||||
|
if (size != 2) {
|
||||||
|
raiseIllegalArgument("header entry array must contain exactly 2 items")
|
||||||
|
}
|
||||||
|
out[toStringOf(value.getAt(requireScope(), ObjInt.Zero)).value] =
|
||||||
|
toStringOf(value.getAt(requireScope(), ObjInt.One)).value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun objOrNullToString(scope: ScopeFacade, value: Obj, name: String): String? = when (value) {
|
||||||
|
ObjNull -> null
|
||||||
|
else -> scope.toStringOf(value).value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun objToInt(scope: ScopeFacade, value: Obj, name: String): Int = when (value) {
|
||||||
|
is ObjInt -> value.value.toInt()
|
||||||
|
else -> scope.raiseClassCastError("$name must be Int")
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package net.sergeych.lyngio.http
|
||||||
|
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
|
import io.ktor.client.engine.HttpClientEngineFactory
|
||||||
|
import io.ktor.client.plugins.timeout
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.request
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.http.headers
|
||||||
|
import io.ktor.http.takeFrom
|
||||||
|
|
||||||
|
internal fun createKtorHttpEngine(
|
||||||
|
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||||
|
): LyngHttpEngine = KtorLyngHttpEngine(engineFactory)
|
||||||
|
|
||||||
|
private class KtorLyngHttpEngine(
|
||||||
|
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||||
|
) : LyngHttpEngine {
|
||||||
|
private val clientResult = runCatching {
|
||||||
|
HttpClient(engineFactory) {
|
||||||
|
expectSuccess = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isSupported: Boolean
|
||||||
|
get() = clientResult.isSuccess
|
||||||
|
|
||||||
|
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||||
|
val httpClient = clientResult.getOrElse {
|
||||||
|
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = httpClient.request {
|
||||||
|
applyRequest(request)
|
||||||
|
}
|
||||||
|
return LyngHttpResponse(
|
||||||
|
status = response.status.value,
|
||||||
|
statusText = response.status.description,
|
||||||
|
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||||
|
bodyBytes = response.body<ByteArray>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||||
|
method = HttpMethod.parse(request.method.uppercase())
|
||||||
|
url.takeFrom(request.url)
|
||||||
|
headers {
|
||||||
|
request.headers.forEach { (name, value) -> append(name, value) }
|
||||||
|
}
|
||||||
|
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||||
|
when {
|
||||||
|
request.bodyBytes != null && request.bodyText != null ->
|
||||||
|
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||||
|
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||||
|
request.bodyText != null -> setBody(request.bodyText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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.lyngio.http
|
||||||
|
|
||||||
|
data class LyngHttpRequest(
|
||||||
|
val method: String,
|
||||||
|
val url: String,
|
||||||
|
val headers: Map<String, String> = emptyMap(),
|
||||||
|
val bodyText: String? = null,
|
||||||
|
val bodyBytes: ByteArray? = null,
|
||||||
|
val timeoutMillis: Long? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LyngHttpResponse(
|
||||||
|
val status: Int,
|
||||||
|
val statusText: String,
|
||||||
|
val headers: Map<String, List<String>>,
|
||||||
|
val bodyBytes: ByteArray,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface LyngHttpEngine {
|
||||||
|
val isSupported: Boolean
|
||||||
|
suspend fun request(request: LyngHttpRequest): LyngHttpResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object UnsupportedHttpEngine : LyngHttpEngine {
|
||||||
|
override val isSupported: Boolean = false
|
||||||
|
|
||||||
|
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||||
|
throw UnsupportedOperationException("HTTP client is not supported on this runtime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect fun getSystemHttpEngine(): LyngHttpEngine
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.lyngio.http.security
|
||||||
|
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessContext
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||||
|
import net.sergeych.lyngio.fs.security.Decision
|
||||||
|
|
||||||
|
sealed interface HttpAccessOp {
|
||||||
|
data class Request(val method: String, val url: String) : HttpAccessOp
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpAccessDeniedException(
|
||||||
|
val op: HttpAccessOp,
|
||||||
|
val reasonDetail: String? = null,
|
||||||
|
) : IllegalStateException("HTTP access denied for $op" + (reasonDetail?.let { ": $it" } ?: ""))
|
||||||
|
|
||||||
|
interface HttpAccessPolicy {
|
||||||
|
suspend fun check(op: HttpAccessOp, ctx: AccessContext = AccessContext()): AccessDecision
|
||||||
|
|
||||||
|
suspend fun require(op: HttpAccessOp, ctx: AccessContext = AccessContext()) {
|
||||||
|
val res = check(op, ctx)
|
||||||
|
if (!res.isAllowed()) throw HttpAccessDeniedException(op, res.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PermitAllHttpAccessPolicy : HttpAccessPolicy {
|
||||||
|
override suspend fun check(op: HttpAccessOp, ctx: AccessContext): AccessDecision =
|
||||||
|
AccessDecision(Decision.Allow)
|
||||||
|
}
|
||||||
101
lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt
Normal file
101
lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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.lyngio.net
|
||||||
|
|
||||||
|
enum class LyngIpVersion {
|
||||||
|
IPV4,
|
||||||
|
IPV6,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LyngSocketAddress(
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val ipVersion: LyngIpVersion,
|
||||||
|
val resolved: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LyngDatagram(
|
||||||
|
val data: ByteArray,
|
||||||
|
val address: LyngSocketAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface LyngTcpSocket {
|
||||||
|
fun isOpen(): Boolean
|
||||||
|
fun localAddress(): LyngSocketAddress
|
||||||
|
fun remoteAddress(): LyngSocketAddress
|
||||||
|
suspend fun read(maxBytes: Int): ByteArray?
|
||||||
|
suspend fun readLine(): String?
|
||||||
|
suspend fun write(data: ByteArray)
|
||||||
|
suspend fun writeUtf8(text: String)
|
||||||
|
suspend fun flush()
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LyngTcpServer {
|
||||||
|
fun isOpen(): Boolean
|
||||||
|
fun localAddress(): LyngSocketAddress
|
||||||
|
suspend fun accept(): LyngTcpSocket
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LyngUdpSocket {
|
||||||
|
fun isOpen(): Boolean
|
||||||
|
fun localAddress(): LyngSocketAddress
|
||||||
|
suspend fun receive(maxBytes: Int): LyngDatagram?
|
||||||
|
suspend fun send(data: ByteArray, host: String, port: Int)
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LyngNetEngine {
|
||||||
|
val isSupported: Boolean
|
||||||
|
val isTcpAvailable: Boolean
|
||||||
|
val isTcpServerAvailable: Boolean
|
||||||
|
val isUdpAvailable: Boolean
|
||||||
|
|
||||||
|
suspend fun resolve(host: String, port: Int): List<LyngSocketAddress>
|
||||||
|
suspend fun tcpConnect(host: String, port: Int, timeoutMillis: Long?, noDelay: Boolean): LyngTcpSocket
|
||||||
|
suspend fun tcpListen(host: String?, port: Int, backlog: Int, reuseAddress: Boolean): LyngTcpServer
|
||||||
|
suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object UnsupportedLyngNetEngine : LyngNetEngine {
|
||||||
|
override val isSupported: Boolean = false
|
||||||
|
override val isTcpAvailable: Boolean = false
|
||||||
|
override val isTcpServerAvailable: Boolean = false
|
||||||
|
override val isUdpAvailable: Boolean = false
|
||||||
|
|
||||||
|
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> {
|
||||||
|
throw UnsupportedOperationException("Raw networking is not supported on this runtime")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun tcpConnect(host: String, port: Int, timeoutMillis: Long?, noDelay: Boolean): LyngTcpSocket {
|
||||||
|
throw UnsupportedOperationException("TCP client sockets are not supported on this runtime")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun tcpListen(host: String?, port: Int, backlog: Int, reuseAddress: Boolean): LyngTcpServer {
|
||||||
|
throw UnsupportedOperationException("TCP server sockets are not supported on this runtime")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket {
|
||||||
|
throw UnsupportedOperationException("UDP sockets are not supported on this runtime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect fun getSystemNetEngine(): LyngNetEngine
|
||||||
|
|
||||||
|
expect fun shutdownSystemNetEngine()
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.lyngio.net.security
|
||||||
|
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessContext
|
||||||
|
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||||
|
import net.sergeych.lyngio.fs.security.Decision
|
||||||
|
|
||||||
|
sealed interface NetAccessOp {
|
||||||
|
data class Resolve(val host: String, val port: Int) : NetAccessOp
|
||||||
|
data class TcpConnect(val host: String, val port: Int) : NetAccessOp
|
||||||
|
data class TcpListen(val host: String?, val port: Int, val backlog: Int) : NetAccessOp
|
||||||
|
data class UdpBind(val host: String?, val port: Int) : NetAccessOp
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetAccessDeniedException(
|
||||||
|
val op: NetAccessOp,
|
||||||
|
val reasonDetail: String? = null,
|
||||||
|
) : IllegalStateException("Network access denied for $op" + (reasonDetail?.let { ": $it" } ?: ""))
|
||||||
|
|
||||||
|
interface NetAccessPolicy {
|
||||||
|
suspend fun check(op: NetAccessOp, ctx: AccessContext = AccessContext()): AccessDecision
|
||||||
|
|
||||||
|
suspend fun require(op: NetAccessOp, ctx: AccessContext = AccessContext()) {
|
||||||
|
val res = check(op, ctx)
|
||||||
|
if (!res.isAllowed()) throw NetAccessDeniedException(op, res.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PermitAllNetAccessPolicy : NetAccessPolicy {
|
||||||
|
override suspend fun check(op: NetAccessOp, ctx: AccessContext): AccessDecision =
|
||||||
|
AccessDecision(Decision.Allow)
|
||||||
|
}
|
||||||
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