Compare commits

...

2 Commits

8 changed files with 338 additions and 1 deletions

View File

@ -72,6 +72,14 @@ For module-level APIs, the default workflow is:
1. declare globals in Lyng using `extern fun` / `extern val` / `extern var`;
2. bind Kotlin implementation via `ModuleScope.globalBinder()`.
This is also the recommended way to expose a Kotlin-backed value that should behave like a true
Lyng global variable/property. If you need `x` to read/write through Kotlin on every access, use
`extern var` / `extern val` plus `bindGlobalVar(...)`.
Do not use `addConst(...)` for this case: `addConst(...)` installs a value, not a Kotlin-backed
property accessor. It is appropriate for fixed values and objects, but not for a global that should
delegate reads/writes back into Kotlin state.
```kotlin
import net.sergeych.lyng.bridge.*
import net.sergeych.lyng.obj.ObjInt
@ -117,6 +125,12 @@ assertEquals("changed", globalProp)
assertEquals("1.0.0", globalVersion)
```
Minimal rule of thumb:
- use `bindGlobalFun(...)` for global functions
- use `bindGlobalVar(...)` for Kotlin-backed global variables/properties
- use `addConst(...)` only for fixed values/objects that do not need getter/setter behavior
For custom argument handling and full runtime access:
```kotlin
@ -462,14 +476,21 @@ im.addPackage("my.tools") { module: ModuleScope ->
module.eval(
"""
extern val version: String
extern var status: String
extern fun triple(x: Int): Int
""".trimIndent()
)
val binder = module.globalBinder()
var status = "ready"
binder.bindGlobalVar(
name = "version",
get = { "1.0" }
)
binder.bindGlobalVar(
name = "status",
get = { status },
set = { status = it }
)
binder.bindGlobalFun1<Int>("triple") { x ->
ObjInt.of((x * 3).toLong())
}
@ -479,6 +500,7 @@ im.addPackage("my.tools") { module: ModuleScope ->
scope.eval("""
import my.tools.*
val v = triple(14)
status = "busy"
""")
val v = scope.eval("v").toKotlin(scope) // -> 42
```

View File

@ -46,6 +46,28 @@ class Script(
// private val catchReturn: Boolean = false,
) : Statement() {
fun statements(): List<Statement> = statements
/**
* Explicitly apply this script's import/module bindings to [scope] without executing the script.
* This is intended for embedding scenarios where the host owns scope lifecycle and wants
* script-specific imports to be a separate, opt-in preparation step.
*/
suspend fun importInto(scope: Scope, seedScope: Scope = scope): Scope {
prepareScriptScope(scope, seedScope)
return scope
}
/**
* Create a fresh raw module scope and apply this script's import/module bindings to it.
* If [seedScope] is provided, its import provider is reused and seed-bound imports resolve from it.
*/
suspend fun instantiateModule(seedScope: Scope? = null, pos: Pos = this.pos): ModuleScope {
val provider = seedScope?.currentImportProvider ?: defaultImportManager
val module = provider.newModuleAt(pos)
prepareScriptScope(module, seedScope ?: module)
return module
}
override suspend fun execute(scope: Scope): Obj {
scope.pos = pos
val execScope = resolveModuleScope(scope) ?: scope
@ -75,6 +97,19 @@ class Script(
return ObjVoid
}
private suspend fun prepareScriptScope(scope: Scope, seedScope: Scope) {
if (importBindings.isNotEmpty() || importedModules.isNotEmpty()) {
seedImportBindings(scope, seedScope)
}
if (moduleSlotPlan.isNotEmpty()) {
scope.applySlotPlan(moduleSlotPlan)
for (name in moduleSlotPlan.keys) {
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
scope.updateSlotFor(name, record)
}
}
}
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
if (importBindings.isEmpty() && importedModules.isEmpty()) return
seedImportBindings(scope, seedScope)

View File

@ -36,7 +36,7 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
} else {
record.value
}
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, base + i)) {
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, i)) {
continue
}
frame.setObjUnchecked(base + i, value)

View File

@ -0,0 +1,87 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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 kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.pacman.ImportManager
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class ScriptImportPreparationTest {
@Test
fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest {
val manager = ImportManager()
manager.addTextPackages(
"""
package foo
val answer = 42
""".trimIndent()
)
val script = Compiler.compile(
Source(
"<prepare-scope>",
"""
import foo
answer
""".trimIndent()
),
manager
)
val scope = manager.newModule()
assertNull(scope["answer"])
script.importInto(scope)
val record = assertNotNull(scope["answer"])
assertEquals(42, scope.resolve(record, "answer").toInt())
}
@Test
fun scriptInstantiateModuleUsesSeedScopeImportProvider() = runTest {
val manager = ImportManager()
manager.addTextPackages(
"""
package foo
val answer = 42
""".trimIndent()
)
val script = Compiler.compile(
Source(
"<instantiate-module>",
"""
import foo
answer
""".trimIndent()
),
manager
)
val seedScope = manager.newModule()
val module = script.instantiateModule(seedScope)
val record = assertNotNull(module["answer"])
assertEquals(42, module.resolve(record, "answer").toInt())
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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 kotlinx.coroutines.test.runTest
import net.sergeych.lyng.FrameSlotRef
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.bytecode.BytecodeConst
import net.sergeych.lyng.bytecode.CmdFrame
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope
import kotlin.test.Test
import kotlin.test.assertNull
class SeedLocalsRegressionTest {
@Test
fun seedFrameLocalsSkipsSelfReferentialFrameSlotRef() = runTest {
val fn = CmdFunction(
name = "seed-self-ref",
localCount = 1,
addrCount = 0,
returnLabels = emptySet(),
scopeSlotCount = 1,
scopeSlotIndices = intArrayOf(0),
scopeSlotNames = arrayOf(null),
scopeSlotIsModule = booleanArrayOf(false),
localSlotNames = arrayOf("x"),
localSlotMutables = booleanArrayOf(true),
localSlotDelegated = booleanArrayOf(false),
localSlotCaptures = booleanArrayOf(false),
constants = listOf(BytecodeConst.IntVal(0)),
cmds = emptyArray(),
posByIp = emptyArray()
)
val scope = Scope(null, pos = Pos.builtIn)
val record = scope.addConst("x", net.sergeych.lyng.obj.ObjInt.of(1))
val frame = CmdFrame(CmdVm(), fn, scope, emptyList())
record.value = FrameSlotRef(frame.frame, 0)
scope.updateSlotFor("x", record)
seedFrameLocalsFromScope(frame, scope)
assertNull(frame.frame.getRawObj(0))
}
}

View File

@ -0,0 +1,66 @@
# :lynglib Compiler and VM Review (2026-03-26)
## Scope
- Reviewed `:lynglib` compiler and bytecode VM paths, focusing on compile/execution correctness.
- Read core files around `Compiler`, `Script`, `BytecodeCompiler`, `CmdRuntime`, `Scope`, `ModuleScope`, and capture/slot plumbing.
- Ran `./gradlew :lynglib:jvmTest` on 2026-03-26: PASS.
## Findings
### 1. High: local seeding uses the wrong slot index when checking for self-referential `FrameSlotRef`
- Status: fixed in worktree on 2026-03-26; covered by `SeedLocalsRegressionTest`.
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt:39`
- The self-reference guard compares `FrameSlotRef` against `base + i`, but `FrameSlotRef` stores the underlying `BytecodeFrame` local slot index, not the VM absolute slot id.
- The same slot is then written using `setObjUnchecked(base + i, value)` at `SeedLocals.kt:42`, which means a self-reference is not filtered out before being reinserted.
- Impact: this can seed a local with a reference to itself, creating recursive `FrameSlotRef` chains. Reads then recurse through `BytecodeFrame.getObj()` / `FrameSlotRef.read()` instead of resolving to a value, which is a realistic path to stack overflow or non-terminating execution in bytecode-only helper execution (`executeBytecodeWithSeed`).
- Why this looks real: the equivalent check in `CmdFrame.applyCaptureRecords()` uses the local index directly (`CmdRuntime.kt:3867`), and `Script.seedModuleLocals()` also compares with the local index, not `scopeSlotCount + i` (`Script.kt:109`).
- Suggested fix: compare with `i`, not `base + i`, and add a regression test around `executeBytecodeWithSeed` seeding a scope that already contains a `FrameSlotRef` to the same local.
### 2. Design note: script-specific import/module preparation should stay explicit, not hidden in `execute(scope)`
- Status: resolved by API addition, without changing `Script.execute(scope)` semantics.
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt:51-57`
- `shouldSeedModule` is only true when `execScope` is a `ModuleScope` or `thisObj === ObjVoid`.
- If a compiled script is executed against a non-module scope with a real receiver object, `seedModuleSlots()` is skipped entirely, so script imports and explicit module bindings are never installed into `moduleTarget`.
- Original concern: top-level bytecode that expects imported names or module globals can see different behavior depending on whether the host prepared the target scope explicitly.
- Resolution taken: do **not** change `Script.execute(scope)`, because in this codebase it is expected to run on exactly the provided scope and many embedding flows already rely on explicit scope setup via `Script.newScope()`, `Scope.eval(...)`, `addFn(...)`, `addConst(...)`, and direct import-manager configuration.
- New explicit APIs added instead:
- `Script.importInto(scope, seedScope = scope)`
- `Script.instantiateModule(seedScope = null, pos = script.pos)`
- This keeps existing execution behavior stable while giving hosts an opt-in way to apply a script's import/module bindings when they actually want that preparation step.
- Coverage added: `ScriptImportPreparationTest`.
### 3. Medium: missing module captures are silently converted into fresh `Unset` slots
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt:3950-3970`
- In `CmdFrame.buildCaptureRecords()`, the module-capture path first tries several lookups. If the requested `slotId` is missing but the capture has a `name`, it calls `target.applySlotPlan(mapOf(name to slotId))` and immediately returns `target.getSlotRecord(slotId)`.
- That record is a newly created placeholder from `Scope.applySlotPlan()` (`Scope.kt:460-471`) and defaults to `ObjUnset`.
- Impact: a compiler/runtime disagreement in capture resolution is masked as a normal capture of `Unset`, so the failure moves far away from closure creation and becomes data corruption or an unrelated later exception. This will be difficult to debug when it happens.
- Suggested fix: if the named module capture cannot be resolved to an existing record, fail immediately with a Lyng error instead of manufacturing a placeholder slot. Add a regression test around missing imported/module captures.
### 4. Medium: subject-less `when { ... }` still crashes through a raw Kotlin `TODO`
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:6447-6449`
- The unsupported branch uses `TODO("when without object is not yet implemented")`.
- Current docs explicitly say subject-less `when` is not implemented, so the language limitation itself is documented. The problem is the failure mode: the compiler throws a raw Kotlin `NotImplementedError` instead of a normal `ScriptError` or a feature diagnostic.
- Impact: IDE/embedding callers get an implementation exception rather than a source-positioned language error, which is especially bad across non-JVM targets and for editor tooling.
- Suggested fix: replace the `TODO(...)` with a `ScriptError` at the current source position, or gate it earlier in parsing with a normal diagnostic.
## Risks Worth Checking Next
### 5. Medium risk: module frame growth only retargets records stored in `objects`/`localBindings`
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt:39-67`
- When `ensureModuleFrame()` replaces the old `BytecodeFrame` with a larger one, it retargets `FrameSlotRef`s only in `objects` and `localBindings`.
- Existing closures/capture records that already hold `FrameSlotRef(oldFrame, slot)` are not retargeted here.
- Impact: if a module scope survives across recompilation/re-execution with a larger local frame, previously exported closures can keep reading stale values from the old frame instance.
- Confidence is lower because this depends on module reuse across differing compiled shapes, but the reference-retargeting logic is clearly incomplete for that scenario.
- Suggested check: add a regression covering module re-execution with increased local count and an exported closure captured before the resize.
## Test Status
- `./gradlew :lynglib:jvmTest` passed during this review.
- `./gradlew :lynglib:jvmTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after the fixes/API additions.
- Finding 1 is covered directly; finding 2 is covered by explicit preparation API tests.
## Suggested Fix Order
1. Fix finding 1 first: it is a concrete slot-index bug with likely recursive failure modes. Done.
2. Keep `Script.execute(scope)` semantics stable and use explicit preparation APIs where script-owned import/module setup is needed. Done.
3. Tighten finding 3 next: fail fast on capture mismatches.
4. Replace the raw `TODO` in finding 4 so unsupported syntax produces normal diagnostics.
5. Decide whether finding 5 matters for current module-reload workflows; add a regression before changing behavior.

View File

@ -0,0 +1,46 @@
WASM использует стековую модель памяти. Lyng стеки не использует. В чем разница?
Стек это очень древняя структура, была придумана в 1955 (магазинная память Бауэра и Самельсона), и приняла современную форму в 1960, благодаря Барбаре Лисокв, вполседствии лауреата премии Тьюринга, которого, в свою очередь, за выдающийся вклад в развитие IT кастрировали, формально, за гомосексуализм, но в британии, где на трех джентльменов приходилось тогда, и приходится и теперь, четыре гомосексуала, все понимают, что это был просто предлог.
Стек был прекрасен в 60е годы, когда IBM планировала продавать по компьютеру в год в лучшем случае, а за идеи о микропроцессорах или ядрах можно было устроиться только пациентом психушки. Стек это заранее жестко выделенная область памяти, которая обычно вообще не используется, и по ней в одну сторону растет используемая область, которая хранит локальные переменные, адреса возврата и иногда состояние процессора.
Она крайне плохо подходит для многозадачности. Представь себе что у тебя есть wasm процесс, со своим стеком, и ему надо переключиться, остановив текущий поток исполнения (например, он ждет ответа от сети). Тогда тебе придется создать _новый стек_ — пока еще пустой, переключиться на него и исполнять там другую задачу. Сколько потоков - столько и стеков. И все они по большей части не используются — там просто запас памяти "на вырост", если исполнение потребует. А оценить заранее адекватно почти невозможно, дают с запасом.
Далее, васм использует фиксированную память выделенную на процесс. Ее тоже приходится брать "с запасом", так как если по ходу не хватит, процесс вылетит. И она тоже в изрядной части пустая. Ну и назасладочку, васм был задуман однозадачным, и все попытки туда хотя бы треды засунуть, получаются очень кривыми.
Линг использует фреймы и сопрограммы. Линг вообще треды не использует. Основная команда исполнения программы на линге - сопрограмма (coroutine), ей не нужен стейк, она им не пользуется. Вместо этого каждый вызов в линге, грубо говоря, это вызов сопрограммы. Для него формируется фрейм (не на стеке, а в динамической памяти!), которого гарантировано достаточно для исполнения собственно кода. Он заменяет собой "стековый фрейм", но его размер известен заранее, и его можно распределять и освобождать проще, как обычную динамическую память.
Далее, линг использует ту же динамическую память, что и его родительская платформа. Если он работает на Java, то использует память со сборкой мусора JVM, на Javascript - память машины JS, в нативных машинах - специальнй менеджер памяти со сборкой мусора, от Kotlin Native (довольно хороший и очень быстро развивающийся). В результате, запустить одновременно десять, или сто тысяч программ на лигне, которые будут исполняться конкурентно, вполне реально. Просто попытка запустить 10к васм-машин на v8 или другом движке скорее всего убьет систему, да и 10 тысяч тредов редко какой сервер приложению даст. Огромный перерасход ресурсов и тормоза.
Далее. Васм СТЕКОВАЯ машина со стековыми командами, он без стека вообще не может. Посмотрим как он считает примитивное выражение:
```wasm
(module
;; Экспортируем функцию, чтобы её можно было вызвать из JS
(func (export "calc") (param $a i32) (param $b i32) (param $c i32) (param $d i32) (result i32)
;; --- вычисление (a + b) / c * d ---
local.get $a ;; помещаем a на стек
local.get $b ;; помещаем b на стек
i32.add ;; складываем: стек содержит (a+b)
local.get $c ;; помещаем c
i32.div_s ;; знаковое деление: стек содержит ((a+b) / c)
local.get $d ;; помещаем d
i32.mul ;; умножение: стек содержит (((a+b)/c) * d)
;; результат остаётся на стеке — это возвращаемое значение функции
)
)
```
На ВМ Линга нет стека, он трехадресный универсальный ассемблер, если можно так сказать:
```
add_int s1, s2 -> s5 // a + b -> s5
div_int s5, s3 -> s5 // s5 -> s5 / c
mul_int s5, s4 -> s5 // s5 -> s5 * d
return s5
```
Этот код крайне эффективно реализуется на любом реальном процессоре, и хорошо оптимизируется. Исполняется он без всякого стека.

21
proposals/why_not_wasm.md Normal file
View File

@ -0,0 +1,21 @@
Почему не WASM?
- wasm очень громоздкий и неудобный для разработки, он проектировался очень давно и с другими целями, так что сейчас его безуспешно пытаются "перезаточить" в современные области, но он поддается с трудом. Компилировать в wasm была бы задача по сложности превосходящая весь проект, а результат был бы уныл: мы и так имеем все достоинсва wasm, полезные для Lyng, но не имеем многих его недостатков.
Виртуалка lyng легкая, и доступна на всех платформах уже сейчас, а вот прикрутить скажем wasm машину к котлину на сервере, чтобы она работала как сопрограммы первого класса вместе с котлинскими, задача сейчас неразрешимая.
То есть, преимущества wasm что он доступен на многих платформах, для нас неважно - мы и так на тех же платформах имеем полную поддержку.
- Lyng VM изначально заточена на исполнение именно сопрограмм со сборкой мусора. Эти два ключевых механизма линга практически отсутствуют в wasm, их мучительно прикручивают как расширения, и они довольно слабо поддерживаются.
- LyngVM заточен на исполнение программ с гибридной ООП-ФП моделью, она хорошо поддерживает множественное наследование, делегацию, работу со сложными списками аргументов (ФП фичи, подстановки, деструктурирование), на васме это очень непросто написать, будет медленно и громоздко.
- одна из целей Lyng была получить быстрое безопасное и очень мощное скриптовое решение для вклучения в проекты на Java/Kotlin multiplatform. На wasm это в принципе невозможно - его включение это кошмар, он громоздкий. К тому же требует компиляции, а Lyng работает с JIT (исполняет прямо исходник, компилируя его на лету, как JS), и может использоваться как скрипт (wasm не может)
- типы данных которые мы используем в Lyng в WASM отсутстсвтуют, их бы пришлось добавлять
В результате если бы мы компилировали Lyng->WASM мы бы получили огромный и медленный код. Для которого потребовалась бы огромная wasm VM. Практического смысла в этом нет.
Если же ты рассматриваешь идею в единой платформе использовать разные языки, а wasm как промежуточный язых совместимости, то это собственно противоречит идее использовать общую кодовую базу и интерфейсы. Разные языки имеют несовпадающие, несовместимые интерфейсы вызовов, так что приходится писать руками биндинги, которые не добавляют скорости и добавляют ошибки, к тому же, разные языки предоставляют часто вообще несовместимые модели программирования (например прототипы в JS, множественное наследование С++ или Линга и кошмар на улице вязов от руста, в владением ссылками).
Платформ на васме полно, и там и ловить нечего, и неинтересно. Я же предлагаю вылезти в другой класс. Навеяно реальным опоытом переносов контрактов из старого золота в мольтпей, которое получается на удивление хорошо