Skip to content

Commit e1c1591

Browse files
fix(effects): pin v1 effect-row registry + reject unknown names (#196)
Root cause of #59: lower_effect_expr accepted ANY string as an effect (silent ESingleton name), so every contributor invented their own and migrated /{IO} code did not unify with stdlib /io. PR-1 (additive + aliases, per owner decision): - effect.ml: canonical v1 registry — IO/Async/Partial/Throws/Mut, reserved Random/Time/Net, legacy aliases io->IO/state->Mut/exn->Throws. - typecheck: declared_effects on context (populated by register_effect_decl); lower_effect_expr canonicalizes via the registry and rejects names that are neither v1/reserved/alias nor a user-declared — new UnknownEffect type_error with an actionable message, raised via Effect_validation_error and caught at the check_program boundary. - stdlib/effects.affine untouched (legacy names still compile via aliases — verified). Gate: dune build clean; dune test --force 253/253, zero regression (incl. AOT effects smoke). Verified: /IO /Async accepted, /io canonicalizes, /Zonk rejected. Follow-up (tracked, not in scope): braced /{IO} return-position row syntax from the migration-stance doc is a separate grammar item. Refs #59 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5605688 commit e1c1591

3 files changed

Lines changed: 80 additions & 7 deletions

File tree

lib/effect.ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,31 @@ let string_of_eff (e : eff) : string =
120120
end
121121
in
122122
aux (normalize_eff e)
123+
124+
(** {1 Effect-row v1 canonical registry (issue #59)}
125+
126+
Pins the v1 effect-name list so migration can begin without every
127+
contributor inventing their own names. This is effect *tracking*
128+
only — handler design remains out of scope (see
129+
[docs/guides/effects-migration-stance.adoc]). *)
130+
131+
(** Canonical v1 effect names. [Throws] is written [Throws[E]] at use
132+
sites; the type parameter is carried syntactically but not yet
133+
threaded (tracking-only v1). *)
134+
let v1_effects = [ "IO"; "Async"; "Partial"; "Throws"; "Mut" ]
135+
136+
(** Reserved for v1.x — recognised so the names are not repurposed, not
137+
yet wired into the stdlib. *)
138+
let reserved_effects = [ "Random"; "Time"; "Net" ]
139+
140+
(** Legacy lowercase stdlib effects → canonical v1. Kept as aliases so
141+
[stdlib/effects.affine] and existing code compile unchanged
142+
(additive migration; a rename sweep is a later, separate change). *)
143+
let legacy_aliases = [ ("io", "IO"); ("state", "Mut"); ("exn", "Throws") ]
144+
145+
(** Canonical registry name for a source effect name, or [None] if it is
146+
neither a v1/reserved name nor a legacy alias. Callers additionally
147+
accept user-declared effects (`effect <name>;`). *)
148+
let canonical_effect_name (s : string) : string option =
149+
if List.mem s v1_effects || List.mem s reserved_effects then Some s
150+
else List.assoc_opt s legacy_aliases

lib/effect.mli

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,18 @@ val normalize_eff : eff -> eff
2626

2727
(** Pretty print effect *)
2828
val string_of_eff : eff -> string
29+
30+
(** {1 Effect-row v1 canonical registry (issue #59)} *)
31+
32+
(** Canonical v1 effect names. *)
33+
val v1_effects : string list
34+
35+
(** Reserved-for-v1.x effect names. *)
36+
val reserved_effects : string list
37+
38+
(** Legacy lowercase stdlib effect → canonical v1 name. *)
39+
val legacy_aliases : (string * string) list
40+
41+
(** Canonical registry name for a source effect name, or [None] if
42+
unknown to the registry (caller also accepts declared effects). *)
43+
val canonical_effect_name : string -> string option

lib/typecheck.ml

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ type type_error =
103103
| BranchTypeMismatch of { then_ty : ty; else_ty : ty }
104104
| QuantityError of Quantity.quantity_error * Span.t
105105
(** QTT quantity violation detected after type checking. *)
106+
| UnknownEffect of string
107+
(** Effect name not in the v1 registry, not a legacy alias, and
108+
not a user-declared effect (issue #59). *)
106109

107110
(** Format a type error for human consumption. *)
108111
let show_type_error = function
@@ -132,9 +135,22 @@ let show_type_error = function
132135
(ty_to_string then_ty) (ty_to_string else_ty)
133136
| QuantityError (qerr, _span) ->
134137
Printf.sprintf "Quantity error: %s" (Quantity.format_quantity_error qerr)
138+
| UnknownEffect name ->
139+
Printf.sprintf
140+
"Unknown effect '%s'. Use a v1 effect (%s), a reserved name (%s), \
141+
or declare it with `effect %s;`."
142+
name
143+
(String.concat ", " Effect.v1_effects)
144+
(String.concat ", " Effect.reserved_effects)
145+
name
135146

136147
let format_type_error = show_type_error
137148

149+
(** Raised by [lower_effect_expr] for an unknown effect name; caught at
150+
the [check_program] boundary and converted to [UnknownEffect]
151+
(lowering is not in the result monad). *)
152+
exception Effect_validation_error of string
153+
138154
(** {1 Context} *)
139155

140156
(** Type checking context.
@@ -157,6 +173,9 @@ type context = {
157173
(** The current effect context — unified with declared effects *)
158174
trait_registry : Trait.trait_registry;
159175
(** Trait registry — stores trait definitions and impls for dispatch *)
176+
declared_effects : (string, unit) Hashtbl.t;
177+
(** User-declared effect names (`effect <name>;`). Consulted by
178+
[lower_effect_expr] alongside the v1 registry (issue #59). *)
160179
}
161180

162181
type 'a result = ('a, type_error) Result.t
@@ -186,6 +205,7 @@ let create_context (symbols : Symbol.t) : context =
186205
level = 0;
187206
current_eff = fresh_effvar 0;
188207
trait_registry = Trait.create_registry ();
208+
declared_effects = Hashtbl.create 16;
189209
}
190210

191211
(** Enter a deeper let-level. *)
@@ -418,14 +438,21 @@ let rec lower_type_expr (ctx : context) (te : type_expr) : ty =
418438
fresh_tyvar ctx.level
419439

420440
and lower_effect_expr (ctx : context) (ee : effect_expr) : eff =
441+
(* Resolve a source effect name to a canonical singleton (issue #59):
442+
Pure → pure; v1/reserved/legacy-alias → canonical registry name;
443+
user-declared (`effect <name>;`) → kept as written; anything else
444+
is rejected so contributors cannot silently invent effect names. *)
445+
let resolve (name : string) : eff =
446+
if name = "Pure" then EPure
447+
else match Effect.canonical_effect_name name with
448+
| Some canonical -> ESingleton canonical
449+
| None ->
450+
if Hashtbl.mem ctx.declared_effects name then ESingleton name
451+
else raise (Effect_validation_error name)
452+
in
421453
match ee with
422-
| EffVar { name; _ } ->
423-
begin match name with
424-
| "Pure" -> EPure
425-
| _ -> ESingleton name
426-
end
427-
| EffCon ({ name; _ }, _args) ->
428-
ESingleton name
454+
| EffVar { name; _ } -> resolve name
455+
| EffCon ({ name; _ }, _args) -> resolve name
429456
| EffUnion (e1, e2) ->
430457
EUnion [lower_effect_expr ctx e1; lower_effect_expr ctx e2]
431458

@@ -1565,6 +1592,7 @@ let register_type_decl (ctx : context) (td : type_decl) : unit result =
15651592

15661593
(** Register an effect declaration. *)
15671594
let register_effect_decl (ctx : context) (ed : effect_decl) : unit result =
1595+
Hashtbl.replace ctx.declared_effects ed.ed_name.name ();
15681596
List.iter (fun (op : effect_op_decl) ->
15691597
let param_tys = List.map (fun (p : param) ->
15701598
lower_type_expr ctx p.p_ty
@@ -1671,6 +1699,7 @@ let check_decl (ctx : context) (decl : top_level) : (unit, type_error) Result.t
16711699
let check_program ?(import_types : (string, scheme) Hashtbl.t option)
16721700
(symbols : Symbol.t) (prog : Ast.program)
16731701
: (context, type_error) Result.t =
1702+
try
16741703
let ctx = create_context symbols in
16751704
register_builtins ctx;
16761705
Option.iter (fun tbl ->
@@ -1717,3 +1746,4 @@ let check_program ?(import_types : (string, scheme) Hashtbl.t option)
17171746
Error (QuantityError (qerr, span))
17181747
end
17191748
| Error e -> Error e
1749+
with Effect_validation_error name -> Error (UnknownEffect name)

0 commit comments

Comments
 (0)