7.2 KiB
7.2 KiB
:lynglib Compiler and VM Review (2026-03-26)
Scope
- Reviewed
:lynglibcompiler 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:jvmTeston 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
FrameSlotRefagainstbase + i, butFrameSlotRefstores the underlyingBytecodeFramelocal slot index, not the VM absolute slot id. - The same slot is then written using
setObjUnchecked(base + i, value)atSeedLocals.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
FrameSlotRefchains. Reads then recurse throughBytecodeFrame.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), andScript.seedModuleLocals()also compares with the local index, notscopeSlotCount + i(Script.kt:109). - Suggested fix: compare with
i, notbase + i, and add a regression test aroundexecuteBytecodeWithSeedseeding a scope that already contains aFrameSlotRefto 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 shouldSeedModuleis only true whenexecScopeis aModuleScopeorthisObj === 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 intomoduleTarget. - 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 viaScript.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 requestedslotIdis missing but the capture has aname, it callstarget.applySlotPlan(mapOf(name to slotId))and immediately returnstarget.getSlotRecord(slotId). - That record is a newly created placeholder from
Scope.applySlotPlan()(Scope.kt:460-471) and defaults toObjUnset. - 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
ScriptErrorwhen 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
whenis not implemented, so the language limitation itself is documented. The problem is the failure mode: the compiler throws a raw KotlinNotImplementedErrorinstead of a normalScriptErroror 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
ScriptErrorat thewhenposition 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 oldBytecodeFramewith a larger one, it retargetsFrameSlotRefs only inobjectsandlocalBindings. - 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:jvmTestpassed during this review../gradlew :lynglib:jvmTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTestpassed after the fixes/API additions../gradlew :lynglib:jvmTest --tests CompilerVmReviewRegressionTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTestpassed after fixing findings 3 and 4.- Finding 1 is covered directly; finding 2 is covered by explicit preparation API tests.
Suggested Fix Order
- Fix finding 1 first: it is a concrete slot-index bug with likely recursive failure modes. Done.
- Keep
Script.execute(scope)semantics stable and use explicit preparation APIs where script-owned import/module setup is needed. Done. - Tighten finding 3 next: fail fast on capture mismatches. Done.
- Replace the raw
TODOin finding 4 so unsupported syntax produces normal diagnostics. Done. - Decide whether finding 5 matters for current module-reload workflows; add a regression before changing behavior.