70 lines
7.2 KiB
Markdown
70 lines
7.2 KiB
Markdown
# :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
|
|
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
|
- 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.
|
|
- Resolution taken: the VM now refuses to treat synthetic placeholder slots as real module/captured bindings. Reads and capture construction fail with a source-positioned `ScriptError` when the execution scope was not prepared with the script's required module/import bindings.
|
|
|
|
### 4. Medium: subject-less `when { ... }` still crashes through a raw Kotlin `TODO`
|
|
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
|
- 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.
|
|
- Resolution taken: the parser now throws a normal `ScriptError` at the `when` position with an explicit "when without subject is not implemented" message.
|
|
|
|
## 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.
|
|
- `./gradlew :lynglib:jvmTest --tests CompilerVmReviewRegressionTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after fixing findings 3 and 4.
|
|
- 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. Done.
|
|
4. Replace the raw `TODO` in finding 4 so unsupported syntax produces normal diagnostics. Done.
|
|
5. Decide whether finding 5 matters for current module-reload workflows; add a regression before changing behavior.
|