Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Lang/Unif.mli
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,11 @@ module Type : sig
of unification variables accordingly. On error, it returns escaping type
variable. *)
val shrink_scope : scope:Scope.t -> typ -> (typ, tvar) result

(** Compute size of a type. Unification variables have size one, so
this function is not stable under instantiation of unification variables.
*)
val size : typ -> int
end

(* ========================================================================= *)
Expand Down
17 changes: 17 additions & 0 deletions src/Lang/UnifPriv/Type.ml
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,20 @@ let scheme_is_monomorphic sch =
match scheme_to_type sch with
| Some _ -> true
| None -> false

(* ========================================================================= *)

let rec size tp =
match view tp with
| TEffect | TUVar _ | TVar _ -> 1
| TArrow(sch, tp, _) -> scheme_size sch + size tp
| THandler(_, tp, itp, otp) -> size tp + size itp + size otp
| TLabel tp -> 1 + size tp
| TApp(tp1, tp2) -> size tp1 + size tp2
| TAlias(_, tp) -> size tp

and scheme_size sch =
List.fold_left
(fun acc (_, sch) -> acc + scheme_size sch)
(size sch.sch_body)
sch.sch_named
2 changes: 1 addition & 1 deletion src/TypeInference/Constr.ml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ open Common
type t =
| ResolveMethod : {
hole : T.poly_fun option BRef.t;
vset : Var.Set.t;
vset : int Var.Map.t;
pos : Position.t;
env : 'st Env.t;
method_env : 'st Env.t;
Expand Down
3 changes: 2 additions & 1 deletion src/TypeInference/Constr.mli
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ type t =
hole : T.poly_fun option BRef.t;
(** Hole to be filled with method implementation *)

vset : Var.Set.t;
vset : int Var.Map.t;
(** Set of variables that cannot be used during method resolution.
This set is just passed as [~vset] to [ParamResolve.resolve_method].
Do not use it directly.
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation describes vset as a 'Set of variables that cannot be used' but it's now a map from variables to their sizes. The comment should clarify that the map values represent size thresholds - variables can only be reused if the new usage has a strictly smaller size than the recorded value.

Suggested change
(** Set of variables that cannot be used during method resolution.
This set is just passed as [~vset] to [ParamResolve.resolve_method].
Do not use it directly.
(** Map from variables to their size thresholds during method resolution.
A variable can only be reused if the new usage has a strictly smaller size than the recorded value.
This map is just passed as [~vset] to [ParamResolve.resolve_method]. Do not use it directly.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, that is correct. However, the actual representation of this "variable set" is an implementation detail of the ParamResolve module, but since the method constraints should be defined earlier, the actual type representation leaks to the public interface. I added the comment "Do not use it directly" to stress that this is an implementation detail, and the actual representation doesn't matter. I don't see any point in making the documentation more precise here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like the comments to at least acknowledge in any way that vset is a map. If I were to stumble upon this fragment while working on something else, I would probably feel very lost: a variable named vset, that comments repeatedly insist is a Set, but has type int Var.Map.t.
You are right that the comment stresses to "not use it directly", and links to the very place where the type and role of vset is explained, but I still think the code as it is now is needlessly arcane.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change vset name to something that doesn't suggest a representation, like used_vars. Or, we can introduce another module, that provides some basic functionality related to these "sets". The type would remain abstract, and the code should be cleaner, but at the cost of introducing a new module. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be perfectly happy with renaming it to used_vars. I don't think it's worth introducing a new module for such a small part of what ultimately is a stopgap measure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The longer I look at this code, the more convinced I am that we should add a separate module for this "stopgap measure". I see at least two reasons to do that:

  1. data abstraction — I feel really bad that some internal data representation leaks, and pollutes types in other modules;
  2. code maintainability — this is simple data structure with a well-defined interface. However, I can imagine the situation when this module might grow in the future. For instance we can add command-line parameters to tune looping detection, like max depth proposed on the last meeting. Or when we will add type-classes we will face additional challenges that could be addressed there. If the implementation will stay in the ParamResolve module we could end up with spaghetti code, that is hard to maintain.

*)

pos : Position.t;
Expand Down
28 changes: 17 additions & 11 deletions src/TypeInference/ParamResolve.ml
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,18 @@ let guess_types ~pos env targs =
let scope = Env.scope env in
List.fold_left_map guess_type (T.Subst.empty ~scope) targs

(** Check for cyclic dependencies in the type parameters, and extend a set of
restricted variables. *)
let restrict_var ~vset ~pos ~pp x name =
if Var.Set.mem x vset then
(** Check for cyclic dependencies in named parameters, and extend a set of
restricted variables. A parameter might be used several times, but each
next usage on the same path must have a smaller size. For methods, the
size is determined from the [self] type, while for other kinds of
parameters, the size is 0. *)
let restrict_var ~vset ~pos ~pp x ?(size=0) name =
match Var.Map.find_opt x vset with
| None -> Var.Map.add x size vset
| Some old_size when size < old_size ->
Var.Map.add x size vset
| Some _ ->
Error.fatal (Error.looping_named_param ~pos ~pp name)
else
Var.Set.add x vset

(* ------------------------------------------------------------------------- *)

Expand Down Expand Up @@ -253,7 +258,8 @@ and resolve_method ~vset ~pos env ?(method_env=env) mname (sch : T.scheme) =
let name = Name.NMethod(owner, mname) in
begin match ModulePath.try_lookup_method ~pos method_env owner mname with
| Some(x, x_sch) ->
let vset = restrict_var ~vset ~pos ~pp x name in
let size = T.Type.size self_tp in
let vset = restrict_var ~vset ~pos ~pp x ~size name in
let e = { T.pos; T.pp = Env.pp_tree env; T.data = T.EVar x } in
coerce_scheme ~vset ~pos ~name env e x_sch sch

Expand All @@ -270,7 +276,7 @@ and resolve_method ~vset ~pos env ?(method_env=env) mname (sch : T.scheme) =

(* ========================================================================= *)
let instantiate ~pos env rctx poly_expr (sch : T.scheme) =
let vset = Var.Set.empty in
let vset = Var.Map.empty in
let (sub, tps) = guess_types ~pos env sch.sch_targs in
let (named, cs) = resolve_params ~sub ~vset ~pos env rctx sch.sch_named in
let e =
Expand All @@ -281,12 +287,12 @@ let instantiate ~pos env rctx poly_expr (sch : T.scheme) =
(e, T.Type.subst sub sch.sch_body, cs)

let coerce_scheme ~pos ~name env poly_expr sch_in sch_out =
let vset = Var.Set.empty in
let vset = Var.Map.empty in
coerce_scheme ~vset ~pos ~name env poly_expr sch_in sch_out

let resolve_implicit ~pos env iname sch =
let vset = Var.Set.empty in
let vset = Var.Map.empty in
resolve_implicit ~vset ~pos env no_reinst iname sch

let resolve_method ?(vset=Var.Set.empty) ~pos env ?method_env mname sch =
let resolve_method ?(vset=Var.Map.empty) ~pos env ?method_env mname sch =
resolve_method ~vset ~pos env ?method_env mname sch
2 changes: 1 addition & 1 deletion src/TypeInference/ParamResolve.mli
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ val resolve_implicit :
visible in the interface because it is used by the constraint solver.
Do not use it directly. *)
val resolve_method :
?vset:Var.Set.t ->
?vset:int Var.Map.t ->
pos:Position.t -> 'st Env.t -> ?method_env:'st Env.t ->
S.method_name -> T.scheme -> T.poly_fun * Constr.t list
20 changes: 20 additions & 0 deletions test/ok/ok0139_sizedMethodResolve.fram
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
data B X = B of X
data P X Y = P of X, Y

data rec Shape =
| SU
| SB of Shape
| SP of Shape, Shape

method shape () = SU
method shape {X, method shape : X -> Shape} (B (x : X)) =
SB x.shape
method shape
{ X, Y
, method shape : X -> Shape
, method shape : Y -> Shape
} (P (x : X) (y : Y)) =
SP x.shape y.shape

let _ =
(P (P (P (B (B ())) (B (P () ()))) (B (P () (B (P () ()))))) ()).shape