You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Inlining binds an argument before a read of an immutable record field #9223
With #9177, inlining keeps the arguments of a call in call order: when the function body uses an argument with side effects after a later argument, that argument is bound to a variable unless the later one is safe to read after it. A record field read never counts as safe, even when the field is immutable:
typep= {x: int}
typem= {mutabley: int}
@valexternaleff: unit=>int="eff"letminus= (a, b) =>b-aletf= (p: p) =>minus(eff(), p.x)
letg= (m: m) =>minus(eff(), m.y)
g needs the binding, since eff() may assign m.y. f does not: p.x cannot change, and before #9177f compiled to return p.x - eff() | 0;.
Lambda does not record whether a field is mutable. Translcore builds a field read as Pfield (pos, Fld_record {name}) (compiler/ml/translcore.ml), although the label's lbl_mut is known there, so Lam_analysis.delay_safe treats every Fld_record read as a read of state that may change. Recording the mutability in Fld_record and Fld_record_inline would let delay_safe accept reads of immutable fields and keep the previous output for them.
Implementing this would bring back a miscompilation, so I'm closing it.
A field declared immutable can still change. Record coercion lets a record with a mutable field be seen through a type where the field is immutable (#9086 rejects only the other direction):
eff() runs before p.x is read, so this prints -8, as master does:
functionf(p){leta=eff();returnp.x-a|0;}
12.3.1 compiles f to return p.x - eff() | 0; and prints -9. Treating the read of an immutable field as safe to move gives that output again.
The declaration of p does not tell whether another type, in any module, sees the field as mutable, so I see no sound way to do this for a record that comes from elsewhere. A JavaScript object bound as a record can change as well.
Pattern matching already makes this assumption, and gets the same kind of wrong result. With r and view from above:
g compiles to return next()(r.x); on master and 12.3.1 and returns 2; it should return 1. The field of a destructured record is bound as an alias when the field is immutable, and an alias may be read where it is used. let x = view.x instead of the pattern gives 1 with #9296.
Activity
mununki commentedon Nov 13, 2026
Implementing this would bring back a miscompilation, so I'm closing it.
A field declared immutable can still change. Record coercion lets a record with a mutable field be seen through a type where the field is immutable (#9086 rejects only the other direction):
eff()runs beforep.xis read, so this prints-8, as master does:12.3.1 compiles
ftoreturn p.x - eff() | 0;and prints-9. Treating the read of an immutable field as safe to move gives that output again.The declaration of
pdoes not tell whether another type, in any module, sees the field as mutable, so I see no sound way to do this for a record that comes from elsewhere. A JavaScript object bound as a record can change as well.Pattern matching already makes this assumption, and gets the same kind of wrong result. With
randviewfrom above:gcompiles toreturn next()(r.x);on master and 12.3.1 and returns2; it should return1. The field of a destructured record is bound as an alias when the field is immutable, and an alias may be read where it is used.let x = view.xinstead of the pattern gives1with #9296.The pattern case needs an issue of its own.