fix(beam): support module-level mutable variables via process dictionary#4676
Open
dbrattli wants to merge 3 commits into
Open
fix(beam): support module-level mutable variables via process dictionary#4676dbrattli wants to merge 3 commits into
dbrattli wants to merge 3 commits into
Conversation
Module-level `let mutable a = 10` was broken: the declaration compiled to a constant-returning function `a() -> 10`, but assignments (`a <- 20`) created an unrelated local variable, and reads still called `a()` (returning the stale initial value). Fix: route module-level mutables through the process dictionary, the same mechanism already used for local mutable `let` bindings: - `MemberDeclaration` for a mutable value (no args, IsMutable) now emits a `main/0` that initialises via `put(name_atom, value)`. The existing multiple-main/0 merge logic combines it with the ActionDeclaration body, so the initialisation always runs first. - `IdentExpr` for a mutable ident not in `ctx.MutableVars`/`LocalVars` now emits `get(name_atom)` instead of calling the (now-absent) function. - `Set(IdentExpr, ValueSet, ...)` for a mutable ident not in `ctx.MutableVars` now emits `put(name_atom, value)` instead of a local match. With this change `let mutable a = 10; a <- 20; printfn "A=%A" a` correctly prints `A=20`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in tests The previous module-level mutable fix routed mutables through the process dictionary, initialised in main/0. Two problems surfaced: 1. Immutable module values that read a module-level mutable (e.g. `let c = topA`) were compiled as lazy 0-arity functions that re-read the live process-dictionary value, so they observed later reassignments instead of the value at binding time. F# evaluates them once at module init, before any later `do`/reassignment, so they must be snapshotted. They now emit a main/0 init (`put(name, value)`, in declaration order) plus an accessor that reads the snapshot. Only values reading a *free* mutable are affected; self-contained bodies (local mutables) stay lazy to avoid a spurious dependency on main/0. 2. The Erlang test runner called test_/0 functions directly and never ran main/0, so module-level initialisation (mutable inits and `do` actions) never executed and reads returned `undefined`. The runner now invokes a module's main/0 before its tests, mirroring .NET module initialisation. Enables the previously-skipped "Setting a top-level value doesn't alter values at same level" test and adds dedicated read/write, multiple-assignment and cross-function module-level mutable tests. Full beam suite: 2457 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…follow-ups Record how module-level mutables are routed through the process dictionary, the snapshotting of immutable values that read a module-level mutable, and the test runner now invoking main/0 before tests. Add a Future Improvements section covering the per-process / main/0-dependent limitation and the persistent_term and ETS follow-up options. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Module-level
let mutable a = 10was broken: the declaration compiled to a constant-returning functiona() -> 10, but assignments (a <- 20) created an unrelated local variable, and reads still calleda()(returning the stale initial value).Route module-level mutables through the process dictionary
The same mechanism already used for local mutable
letbindings:MemberDeclarationfor a mutable value (no args,IsMutable) now emits amain/0that initialises viaput(name_atom, value). The existing multiple-main/0merge logic combines it with theActionDeclarationbody, so the initialisation always runs first.IdentExprfor a mutable ident not inctx.MutableVars/LocalVarsnow emitsget(name_atom)instead of calling the (now-absent) function.Set(IdentExpr, ValueSet, ...)for a mutable ident not inctx.MutableVarsnow emitsput(name_atom, value)instead of a local match.With this change
let mutable a = 10; a <- 20; printfn "A=%A" acorrectly printsA=20.Snapshot immutable values that read a module-level mutable
An immutable module value that reads a module-level mutable (e.g.
let c = topA) was compiled as a lazy 0-arity function that re-read the live process-dictionary value, so it observed later reassignments instead of the value at binding time. F# evaluates such bindings once at module init — before any laterdo/reassignment — so they must be snapshotted. They now emit amain/0init (put(name, value), in declaration order) plus an accessor that reads the snapshot. Only values reading a free mutable are affected; self-contained bodies (local mutables) stay lazy to avoid a spurious dependency onmain/0.Run module initialisation before tests
The Erlang test runner called
test_*/0functions directly and never ranmain/0, so module-level initialisation (mutable inits anddoactions) never executed and reads returnedundefined. The runner now invokes a module'smain/0before its tests, mirroring .NET module initialisation (which runs before any module code).Tests
Enables the previously-skipped
Setting a top-level value doesn't alter values at same leveltest and adds dedicated read/write, multiple-assignment and cross-function module-level mutable tests. Full beam suite: 2457 passed, 0 failed.This was previously pushed directly to
main(488f4cd) and has been moved to a PR.🤖 Generated with Claude Code