Skip to content

Commit a4eb201

Browse files
committed
Add exnDecElab {gen|app} expert MLB annotation
Allow the elaboration/implementation of exception declarations to be either generative (default) or applicative. The default generative behavior is according to the Definition of Standard ML, where each dynamic evaluation of an `exception C of ty` introduces a fresh exception variant with name `C` but distinct from any previous evaluations of this `exception` declaration. The implementation of a generative exception declaration is: * introduce a `C of unit ref * ty` variant for the `exn` datatype * replace the `exception C of ty` declaration with `val nonce : unit ref = ref ()` * replace any `C arg` constructor applications with `C (nonce, arg)` constructor applications * replace any `case e of C x => exp | _ => next ()` pattern matches with `case e of C (n, x) => if nonce = n then exp else next () | _ => next ()` Note that the freshness of the `ref () : unit ref` allocation is what ensures that each dynamic evaluation of `exception C of ty` is distinct from any previous evaluations of this `exception` declaration. The (new) applicative behavior simply changes the implementation to use a `unit` nonce rather than a `unit ref` nonce. This avoids the allocation of a fresh `unit ref` at the `exception C of ty` declaration. Because MLton implements exceptions after monomorphisation, this means that an applicative exception declaration essentially introduces a distinct variant for each monomorphic type at which the `exception` declaration is evaluated, allowing distinct evaluations to share the same variant when they share the same monomorphic type. Because MLton implements monomorphisation after SML type checking and elaboration, the sharing of variants is with respect to the *elaborated* types (and ignores any type distinctions that may have been present in the source code due to opaque signature constraints). The utility of applicative exception declarations is to slightly optimize the implementation of a universal type using exceptions (see http://mlton.org/UniversalType). In the special case that one can be sure that the use of the universal type will never `inject` at one type and then try to `project` at another type that would be considered distinct in the source code (due to opaque signature constraints) but has the same *elaborated* type, then implementing the universal type with an applicative exception declaration can remove the overhead of allocating the `unit ref`. Normally, universal types are used sparingly in idiomatic Standard ML code and rarely occur on a hot/fast/critical path. An exception to this is in the implementation of parallelism, such as MaPLe's `pcall`. Consider `pcallFork : (unit -> 'a) * (unit -> 'b) -> 'a * 'b`. Simplifying somewhat, if the second thunk is stolen, then a `'b option ref` must be allocated to communicate the result of the stolen work to the main computation. A simple implementation would be: fun pcallFork (f, g) = let val gres = ref NONE fun seq fres = (fres, g ()) fun par fres = (fres, get gres) fun spwn () = (put (gres, g ()) ; exit ()) in pcall (f, seq, par, spwn) end where `put` and `get` treat an `'a option ref` as an Id-style I-structure. The disadvantage of this implementation is that it incurs a `ref NONE : 'b option ref` allocation for *every* `pcallFork`, although most evaluations of `pcallFork` will not have the second thunk stolen. To avoid this overhead, we'd like to move the `ref NONE` allocation to the slow path, occurring only when the second thunk is stolen. It is for this reason that the lower-level `pcall` operation allows the slow-path stealing code to pass (a pointer to) some data back to the `par` and `spwn` continuations; in particular, we can have the stealing code allocate the `ref NONE`: fun pcallFork (f, g) = let fun seq fres = (fres, g ()) fun par (fres, gres) = (fres, get gres) fun spwn gres = (put (gres, g ()) ; exit ()) in pcall (f, seq, par, spwn) end However, the stealing code is *generic* and partially implemented in SML (and, therefore, must integrate with the SML type system). In particular, it only has access to an opaque `Thread.t` representing the interrupted thread that has a `pcall` to steal and has no obvious means of obtaining the type that the to-be-stolen thunk will return in order to properly allocate a `ref NONE : 'b option ref`. One expedient approach is to use a universal type. Now, the stealing code can allocate a `ref NONE : Universal.t option ref` and the `pcallFork` can `inject` to / `project` from the universal type: fun pcallFork (f, g) = let val (inject, project) = Universal.embed () fun seq fres = (fres, g ()) fun par (fres, gres) = (fres, valOf (project (get gres))) fun spwn gres = (put (gres, inject (g ())) ; exit ()) in pcall (f, seq, par, spwn) end Unfortunately, when `structure Universal` is implemented with generative exceptions, this reintroduces a `val nonce : unit ref = ref ()` allocation for *every* `pcallFork`. (Arguably, a `unit ref` is "cheaper" than a `'b ref`, since it is expected that a meaningful `pcallFork` will have a second thunk that returns a non-`unit` result. A `unit ref` can be allocated with only a header (and no object data), while a `'b ref` (with `'b` not `unit`) will be allocated with a header and at least 8-bytes of object data (typically, an object pointer).) However, when `structure Universal` is implemented with applicative exceptions, then there is only a `val nonce : unit = ()` (which will be optimized away). Note that a distinct `gres` is created for each stolen `pcall` and is properly passed exactly to the `par` and `spwn` continuations of the stolen `pcall`, so there is no possibility of conflating the `Universal.t` values from one `pcall` with another and the monomorphic behavior of applicative exceptions is acceptable.
1 parent 9cb00da commit a4eb201

19 files changed

+184
-57
lines changed

mlton/atoms/atoms.fun

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ structure Atoms =
8080
structure RealSize = RealSize
8181
structure WordSize = WordSize)
8282

83+
structure ExnDecElab = ExnDecElab ()
84+
8385
structure Prod = Prod ()
8486
structure Handler = Handler (structure Label = Label)
8587
structure Return = Return (structure Label = Label

mlton/atoms/atoms.sig

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ signature ATOMS' =
2424
structure CharSize: CHAR_SIZE
2525
structure Con: CON
2626
structure Const: CONST
27+
structure ExnDecElab: EXN_DEC_ELAB
2728
structure Ffi: FFI
2829
structure Field: FIELD
2930
structure Func: FUNC
@@ -95,6 +96,7 @@ signature ATOMS =
9596
sharing Cases = Atoms.Cases
9697
sharing Con = Atoms.Con
9798
sharing Const = Atoms.Const
99+
sharing ExnDecElab = Atoms.ExnDecElab
98100
sharing Ffi = Atoms.Ffi
99101
sharing Field = Atoms.Field
100102
sharing Func = Atoms.Func

mlton/atoms/exn-dec-elab.fun

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
(* Copyright (C) 2024 Matthew Fluet.
2+
*
3+
* MLton is released under a HPND-style license.
4+
* See the file MLton-LICENSE for details.
5+
*)
6+
7+
functor ExnDecElab (S: EXN_DEC_ELAB_STRUCTS): EXN_DEC_ELAB =
8+
struct
9+
10+
open S
11+
12+
open Control.Elaborate.ExnDecElab
13+
14+
val all = [App, Gen]
15+
16+
val layout = Layout.str o toString
17+
18+
val parse =
19+
let
20+
open Parse
21+
infix 3 *>
22+
in
23+
any (List.map (all, fn ede => kw (toString ede) *> pure ede))
24+
end
25+
26+
end

mlton/atoms/exn-dec-elab.sig

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
(* Copyright (C) 2024 Matthew Fluet.
2+
*
3+
* MLton is released under a HPND-style license.
4+
* See the file MLton-LICENSE for details.
5+
*)
6+
7+
signature EXN_DEC_ELAB_STRUCTS =
8+
sig
9+
end
10+
11+
signature EXN_DEC_ELAB =
12+
sig
13+
include EXN_DEC_ELAB_STRUCTS
14+
15+
datatype t = App | Gen
16+
17+
val layout: t -> Layout.t
18+
val parse: t Parse.t
19+
end

mlton/atoms/sources.cm

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ signature CHAR_SIZE
1515
signature CONST
1616
signature C_FUNCTION
1717
signature C_TYPE
18+
signature EXN_DEC_ELAB
1819
signature HASH_TYPE
1920
signature ID
2021
signature INT_SIZE
@@ -108,6 +109,8 @@ cases.sig
108109
cases.fun
109110
prim.sig
110111
prim.fun
112+
exn-dec-elab.sig
113+
exn-dec-elab.fun
111114
prod.sig
112115
prod.fun
113116
handler.sig

mlton/atoms/sources.mlb

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ local
7070
cases.fun
7171
prim.sig
7272
prim.fun
73+
exn-dec-elab.sig
74+
exn-dec-elab.fun
7375
prod.sig
7476
prod.fun
7577
handler.sig
@@ -97,6 +99,7 @@ in
9799
signature CONST
98100
signature C_FUNCTION
99101
signature C_TYPE
102+
signature EXN_DEC_ELAB
100103
signature HASH_TYPE
101104
signature ID
102105
signature INT_SIZE

mlton/control/control-flags.sig

+7
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ signature CONTROL_FLAGS =
134134
Default
135135
| Ignore
136136
end
137+
structure ExnDecElab :
138+
sig
139+
datatype t = App | Gen
140+
val toString: t -> string
141+
end
142+
137143
structure ResolveScope :
138144
sig
139145
datatype t =
@@ -154,6 +160,7 @@ signature CONTROL_FLAGS =
154160
val allowRedefineSpecialIds: (bool,bool) t
155161
val allowSpecifySpecialIds: (bool,bool) t
156162
val deadCode: (bool,bool) t
163+
val exnDecElab: (ExnDecElab.t,ExnDecElab.t) t
157164
val forceUsed: (unit,bool) t
158165
val ffiStr: (string,string option) t
159166
val nonexhaustiveBind: (DiagEIW.t,DiagEIW.t) t

mlton/control/control-flags.sml

+27
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,20 @@ structure Elaborate =
356356
| Ignore => "ignore"
357357
end
358358

359+
structure ExnDecElab =
360+
struct
361+
datatype t = App | Gen
362+
363+
val fromString: string -> t option =
364+
fn "app" => SOME App
365+
| "gen" => SOME Gen
366+
| _ => NONE
367+
368+
val toString: t -> string =
369+
fn App => "app"
370+
| Gen => "gen"
371+
end
372+
359373
structure ResolveScope =
360374
struct
361375
datatype t =
@@ -658,6 +672,19 @@ structure Elaborate =
658672
val (deadCode, ac) =
659673
makeBool ({name = "deadCode",
660674
default = false, expert = true}, ac)
675+
val (exnDecElab, ac) =
676+
make ({choices = SOME [ExnDecElab.App, ExnDecElab.Gen],
677+
default = ExnDecElab.Gen,
678+
expert = true,
679+
toString = ExnDecElab.toString,
680+
name = "exnDecElab",
681+
newCur = fn (_,ede) => ede,
682+
newDef = fn (_,ede) => ede,
683+
parseArgs = fn args' =>
684+
case args' of
685+
[arg'] => ExnDecElab.fromString arg'
686+
| _ => NONE},
687+
ac)
661688
val (forceUsed, ac) =
662689
make ({choices = NONE,
663690
default = false,

mlton/core-ml/core-ml.fun

+7-3
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ datatype dec =
166166
tycon: Tycon.t,
167167
tyvars: Tyvar.t vector} vector
168168
| Exception of {arg: Type.t option,
169-
con: Con.t}
169+
con: Con.t,
170+
elab: ExnDecElab.t}
170171
| Fun of {decs: {lambda: lambda,
171172
var: Var.t} vector,
172173
tyvars: unit -> Tyvar.t vector}
@@ -248,8 +249,11 @@ in
248249
align
249250
(separateLeft (Vector.toListMap (cons, layoutConArg),
250251
"| "))]))]
251-
| Exception ca =>
252-
seq [str "exception ", layoutConArg ca]
252+
| Exception {con, arg, elab} =>
253+
seq [str "exception ",
254+
ExnDecElab.layout elab,
255+
str " ",
256+
layoutConArg {con = con, arg = arg}]
253257
| Fun {decs, tyvars, ...} => layoutFuns (tyvars, decs)
254258
| Val {rvbs, tyvars, vbs, ...} =>
255259
align [layoutFuns (tyvars, rvbs),

mlton/core-ml/core-ml.sig

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ signature CORE_ML =
163163
tycon: Tycon.t,
164164
tyvars: Tyvar.t vector} vector
165165
| Exception of {arg: Type.t option,
166-
con: Con.t}
166+
con: Con.t,
167+
elab: ExnDecElab.t}
167168
| Fun of {decs: {lambda: Lambda.t,
168169
var: Var.t} vector,
169170
tyvars: unit -> Tyvar.t vector}

mlton/defunctorize/defunctorize.fun

+3-2
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,10 @@ fun defunctorize (CoreML.Program.T {decs}) =
744744
in
745745
case d of
746746
Datatype _ => e
747-
| Exception {arg, con} =>
747+
| Exception {arg, con, elab} =>
748748
prefix (Xdec.Exception {arg = Option.map (arg, loopTy),
749-
con = con})
749+
con = con,
750+
elab = elab})
750751
| Fun {decs, tyvars} =>
751752
prefix (Xdec.Fun {decs = processLambdas decs,
752753
tyvars = tyvars ()})

mlton/elaborate/elaborate-core.fun

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ open S
1515
local
1616
open Control.Elaborate
1717
in
18+
val exnDecElab = fn () => current exnDecElab
1819
val nonexhaustiveBind = fn () => current nonexhaustiveBind
1920
val nonexhaustiveExnBind = fn () => current nonexhaustiveExnBind
2021
val nonexhaustiveExnMatch = fn () => current nonexhaustiveExnMatch
@@ -137,6 +138,7 @@ in
137138
structure Convention = CFunction.Convention
138139
structure Cdec = Dec
139140
structure Cexp = Exp
141+
structure ExnDecElab = ExnDecElab
140142
structure Ffi = Ffi
141143
structure IntSize = IntSize
142144
structure Lambda = Lambda
@@ -2336,10 +2338,17 @@ fun elaborateDec (d, {env = E, nest}) =
23362338
end
23372339
val scheme = Scheme.fromType ty
23382340
val _ = Env.extendExn (E, exn, exn', scheme)
2341+
val elab =
2342+
case exnDecElab () of
2343+
Control.Elaborate.ExnDecElab.App =>
2344+
ExnDecElab.App
2345+
| Control.Elaborate.ExnDecElab.Gen =>
2346+
ExnDecElab.Gen
23392347
in
23402348
Decs.add (decs,
23412349
Cdec.Exception {arg = arg,
2342-
con = exn'})
2350+
con = exn',
2351+
elab = elab})
23432352
end
23442353
in
23452354
decs

mlton/main/compile.fun

+2-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ local
225225
List.concat [[Datatype primitiveDatatypes],
226226
List.map
227227
(primitiveExcons, fn c =>
228-
Exception {con = c, arg = NONE})]
228+
Exception {con = c, arg = NONE,
229+
elab = CoreML.ExnDecElab.Gen})]
229230
end
230231

231232
in

0 commit comments

Comments
 (0)