@@ -183,6 +183,31 @@ let resolveImportModuleName (com: IBeamCompiler) (importPath: string) =
183183 else
184184 Some name
185185
186+ /// Detect whether an expression reads a * free* mutable ident — a module-level mutable
187+ /// not bound locally within the expression. Such reads must be snapshotted at module-init
188+ /// time (eager) rather than recomputed lazily on each access, because the module-level
189+ /// mutable may be reassigned (via a ` do ` action or later binding) after this value is bound.
190+ /// Locally-bound mutables are excluded: their initializers are self-contained, so lazy
191+ /// recomputation yields the same value and avoids creating a spurious dependency on main/0.
192+ let rec readsFreeMutable ( bound : Set < string >) ( expr : Expr ) : bool =
193+ match expr with
194+ | IdentExpr ident -> ident.IsMutable && not ( bound.Contains ident.Name)
195+ | Let( ident, value, body) -> readsFreeMutable bound value || readsFreeMutable ( Set.add ident.Name bound) body
196+ | LetRec( bindings, body) ->
197+ let bound = bindings |> List.fold ( fun s ( i , _ ) -> Set.add i.Name s) bound
198+
199+ ( bindings |> List.exists ( fun ( _ , v ) -> readsFreeMutable bound v))
200+ || readsFreeMutable bound body
201+ | Lambda( arg, body, _) -> readsFreeMutable ( Set.add arg.Name bound) body
202+ | Delegate( args, body, _, _) ->
203+ let bound = args |> List.fold ( fun s a -> Set.add a.Name s) bound
204+ readsFreeMutable bound body
205+ | ForLoop( ident, start, limit, body, _, _) ->
206+ readsFreeMutable bound start
207+ || readsFreeMutable bound limit
208+ || readsFreeMutable ( Set.add ident.Name bound) body
209+ | _ -> getSubExpressions expr |> List.exists ( readsFreeMutable bound)
210+
186211let rec transformExpr ( com : IBeamCompiler ) ( ctx : Context ) ( expr : Expr ) : Beam.ErlExpr =
187212 match expr with
188213 | Unresolved(_, _, r) ->
@@ -215,6 +240,9 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
215240 | None ->
216241 if ctx.LocalVars.Contains( ident.Name) || ctx.RecursiveBindings.Contains( ident.Name) then
217242 Beam.ErlExpr.Variable( capitalizeFirst ident.Name |> sanitizeErlangVar)
243+ elif ident.IsMutable then
244+ // Module-level mutable: read current value from process dictionary
245+ Beam.ErlExpr.Call( None, " get" , [ atomLit ( sanitizeErlangName ident.Name) ])
218246 else
219247 // Module-level function reference: call as 0-arity function
220248 Beam.ErlExpr.Call( None, sanitizeErlangName ident.Name, [])
@@ -521,6 +549,9 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
521549 // Array ref (non-byte): put the new value into the process dict ref
522550 let erlExpr = transformExpr com ctx expr
523551 Beam.ErlExpr.Call( None, " put" , [ erlExpr; transformExpr com ctx value ])
552+ | IdentExpr ident when ident.IsMutable ->
553+ // Module-level mutable: update via process dictionary using name atom
554+ Beam.ErlExpr.Call( None, " put" , [ atomLit ( sanitizeErlangName ident.Name); transformExpr com ctx value ])
524555 | IdentExpr ident -> Beam.ErlExpr.Match( Beam.PVar( capitalizeFirst ident.Name), transformExpr com ctx value)
525556 | _ ->
526557 com.WarnOnlyOnce( " Set with non-identifier target is not supported for Beam target" )
@@ -3475,21 +3506,104 @@ and transformDeclaration (com: IBeamCompiler) (ctx: Context) (decl: Declaration)
34753506 | Beam.ErlExpr.Block exprs -> exprs
34763507 | expr -> [ expr ]
34773508
3478- let funcDef : Beam.ErlFunctionDef =
3479- {
3480- Name = Beam.Atom name
3481- Arity = arity
3482- Clauses =
3483- [
3484- {
3485- Patterns = args
3486- Guard = []
3487- Body = body
3488- }
3489- ]
3490- }
3509+ // The value initializer of a module-level mutable/snapshot is spliced into the
3510+ // shared main/0 clause. A multi-statement body binds local Erlang variables (from
3511+ // F# `let`s); since Erlang `begin...end` does not introduce a new scope, two such
3512+ // initializers reusing the same variable name would clash in main/0. Wrap a
3513+ // multi-statement body in an immediately-invoked `fun` so its locals stay isolated.
3514+ // A single-expression body needs no wrapper.
3515+ let initValue =
3516+ match body with
3517+ | [ single ] -> single
3518+ | _ ->
3519+ Beam.ErlExpr.Apply(
3520+ Beam.ErlExpr.Fun
3521+ [
3522+ {
3523+ Patterns = []
3524+ Guard = []
3525+ Body = body
3526+ }
3527+ ],
3528+ []
3529+ )
34913530
3492- [ Beam.ErlForm.Function funcDef ]
3531+ // Module-level mutable values (no args, IsMutable) are stored in the process
3532+ // dictionary so they can be updated. Emit a main/0 that initializes the value
3533+ // instead of a constant-returning function — the main/0 merges with the
3534+ // ActionDeclaration main/0 so the initialization runs before use.
3535+ if info.IsValue && info.IsMutable && arity = 0 then
3536+ let initStmt = Beam.ErlExpr.Call( None, " put" , [ atomLit name; initValue ])
3537+
3538+ let funcDef : Beam.ErlFunctionDef =
3539+ {
3540+ Name = Beam.Atom " main"
3541+ Arity = 0
3542+ Clauses =
3543+ [
3544+ {
3545+ Patterns = []
3546+ Guard = []
3547+ Body = [ initStmt ]
3548+ }
3549+ ]
3550+ }
3551+
3552+ [ Beam.ErlForm.Function funcDef ]
3553+ elif info.IsValue && arity = 0 && readsFreeMutable Set.empty memDecl.Body then
3554+ // Immutable module-level value whose initializer reads a module-level mutable.
3555+ // F# evaluates it once at module-init, before any later reassignment of that
3556+ // mutable, so it must be snapshotted. Emit a main/0 fragment that stores the
3557+ // value in the process dictionary (in declaration order, so it captures the
3558+ // mutable's value at this point) plus an accessor that reads the snapshot.
3559+ let initStmt = Beam.ErlExpr.Call( None, " put" , [ atomLit name; initValue ])
3560+
3561+ let initDef : Beam.ErlFunctionDef =
3562+ {
3563+ Name = Beam.Atom " main"
3564+ Arity = 0
3565+ Clauses =
3566+ [
3567+ {
3568+ Patterns = []
3569+ Guard = []
3570+ Body = [ initStmt ]
3571+ }
3572+ ]
3573+ }
3574+
3575+ let accessorDef : Beam.ErlFunctionDef =
3576+ {
3577+ Name = Beam.Atom name
3578+ Arity = 0
3579+ Clauses =
3580+ [
3581+ {
3582+ Patterns = []
3583+ Guard = []
3584+ Body = [ Beam.ErlExpr.Call( None, " get" , [ atomLit name ]) ]
3585+ }
3586+ ]
3587+ }
3588+
3589+ [ Beam.ErlForm.Function initDef; Beam.ErlForm.Function accessorDef ]
3590+ else
3591+
3592+ let funcDef : Beam.ErlFunctionDef =
3593+ {
3594+ Name = Beam.Atom name
3595+ Arity = arity
3596+ Clauses =
3597+ [
3598+ {
3599+ Patterns = args
3600+ Guard = []
3601+ Body = body
3602+ }
3603+ ]
3604+ }
3605+
3606+ [ Beam.ErlForm.Function funcDef ]
34933607
34943608 | ActionDeclaration actionDecl ->
34953609 let bodyExpr = transformExpr com ctx actionDecl.Body
0 commit comments