Skip to content

Commit a1e8b6b

Browse files
committed
typing: defer failed-application classification (perf)
Operator selection reported every rejected candidate through EcUnify.classify_application, which peels the argument list one type at a time, head-normalising via `ty_hnorm (ty_subst (Tuni.subst (UniEnv.assubst ue)) _)` at each step. UniEnv.assubst is subst_of_uf, materialising the whole unifier by folding over UF.domain (every univar in the unienv), so each candidate cost O(arity * |unienv|) instead of the O(arity) incremental union-find of the previous single unification. Overload resolution rejects candidates constantly on the *success* path (any application of an overloaded name tries and drops the non-matching instances), so this classification ran pervasively even though the diagnostics it produces are only ever consumed when an application fails outright and an error is raised. On a large formosa-keccak module this turned a ~8s require into ~54s. Defer the classification: - EcUnify: select_outcome's KO now carries a `... Lazy.t`. select_op decides OK/KO with the cheap single `unify top texpected` and drops KO without ever forcing it; select_op_failures forces the thunks and is meant for the error path only. classify_application is unchanged. - EcTyping.gen_select_op: the OK selection uses select_op; the failure list becomes a lazy closure (program-variable failures, which cost one unification, stay eager). tyerror_noop forces it only when building the UnappliedOp / UnknownVarOrOp error. Diagnostics are byte-identical (candidate order preserved). Keccak require back to 8.4s; tests/op-application-errors.ec passes; unit (84/84) and stdlib (128/128) green.
1 parent e07ffa0 commit a1e8b6b

3 files changed

Lines changed: 82 additions & 46 deletions

File tree

src/ecTyping.ml

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,9 @@ let gen_select_op
374374
(ue : EcUnify.unienv)
375375
(psig : EcTypes.dom * EcTypes.ty option)
376376

377-
: OpSelect.gopsel list * OpSelect.opfailure list
377+
: OpSelect.gopsel list * OpSelect.opfailure list Lazy.t
378378
=
379379

380-
let opfailures = ref [] in
381-
382380
let fpv me (pv, ty, ue) : OpSelect.gopsel =
383381
(`Pv (me, pv), ty, ue, (pv :> opmatch))
384382

@@ -417,30 +415,24 @@ let gen_select_op
417415
|> Option.to_list
418416
else [] in
419417

418+
(* Cheap: only program-variable failures, which cost a single unification.
419+
Operator failures are classified lazily below, off the fast typing path. *)
420+
let pvfailures = ref [] in
421+
420422
let ops () : OpSelect.gopsel list =
421-
let outcomes =
422-
EcUnify.select_op_outcomes ~filter:ue_filter tvi env name ue psig in
423-
let ops =
424-
List.filter_map
425-
(function EcUnify.OK r -> Some r | EcUnify.KO _ -> None) outcomes in
426-
opfailures := !opfailures @
427-
List.filter_map
428-
(function
429-
| EcUnify.KO (p, inst, ty, f) -> Some (`Op (p, inst, ty), f)
430-
| EcUnify.OK _ -> None)
431-
outcomes;
423+
let ops = EcUnify.select_op ~filter:ue_filter tvi env name ue psig in
432424
let ops = opsc |> ofold (fun opsc -> List.mbfilter (by_scope opsc)) ops in
433425
let ops = match List.mbfilter by_current ops with [] -> ops | ops -> ops in
434426
let ops = match List.mbfilter by_tc ops with [] -> ops | ops -> ops in
435427
(List.map fop ops)
436428

437429
and pvs () : OpSelect.gopsel list =
438-
let me, (pvs, pvfailures) =
430+
let me, (pvs, pvf) =
439431
match EcEnv.Memory.get_active_ss env, actonly with
440432
| None, true -> (None, ([], []))
441433
| me , _ -> ( me, select_pv env me name ue tvi psig)
442434
in
443-
opfailures := !opfailures @ pvfailures;
435+
pvfailures := pvf;
444436
List.map (fpv me) pvs
445437
in
446438

@@ -460,7 +452,18 @@ let gen_select_op
460452
else
461453
select [locals; pvs; ops]
462454
in
463-
(selected, !opfailures)
455+
456+
(* Reasons the named candidates fail to apply. Only inspected when nothing was
457+
selected (i.e. an error is being reported), so the expensive per-argument
458+
operator classification stays off the fast typing path. *)
459+
let opfailures = lazy (
460+
!pvfailures
461+
@ List.map
462+
(fun (p, inst, ty, f) -> (`Op (p, inst, ty), f))
463+
(EcUnify.select_op_failures ~filter:ue_filter tvi env name ue psig)
464+
) in
465+
466+
(selected, opfailures)
464467

465468
(* -------------------------------------------------------------------- *)
466469
let select_exp_op env mode opsc name ue tvi psig =
@@ -475,10 +478,11 @@ let select_form_op env mode ~forcepv opsc name ue tvi psig =
475478
(* -------------------------------------------------------------------- *)
476479
(* [UnappliedOp] when candidates of that name exist but fail, else
477480
[UnknownVarOrOp]. *)
478-
let tyerror_noop env loc name esig retty (opfailures : OpSelect.opfailure list) =
479-
match opfailures with
481+
let tyerror_noop env loc name esig retty
482+
(opfailures : OpSelect.opfailure list Lazy.t) =
483+
match Lazy.force opfailures with
480484
| [] -> tyerror loc env (UnknownVarOrOp (name, esig))
481-
| _ -> tyerror loc env (UnappliedOp (name, esig, retty, opfailures))
485+
| opfailures -> tyerror loc env (UnappliedOp (name, esig, retty, opfailures))
482486

483487
(* -------------------------------------------------------------------- *)
484488
let select_proj env opsc name ue tvi recty =

src/ecUnify.ml

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,10 @@ type op_instance = (EcIdent.t * ty) list
342342

343343
type select_outcome =
344344
| OK of select_result
345-
| KO of EcPath.path * op_instance * ty * op_failure
346-
(* operator path, partial instantiation, declared operator type, reason *)
345+
| KO of (EcPath.path * op_instance * ty * op_failure) Lazy.t
346+
(* operator path, partial instantiation, declared operator type, reason.
347+
Deferred: classifying the failure is expensive and only needed when an
348+
error is reported, so [select_op] can drop it without ever forcing it. *)
347349

348350
(* -------------------------------------------------------------------- *)
349351
(* [None] if [top] applies to [psig] (and [retty]), updating [ue]; otherwise
@@ -434,17 +436,34 @@ let select_op_outcomes
434436
filter oppath op && filter_on_tvi op
435437
in
436438

437-
let select (path, op) =
439+
let mk_ok (path, op) tip tvs top subue =
440+
let bd =
441+
match op.D.op_kind with
442+
| OB_nott nt ->
443+
let substnt () =
444+
let xs = List.map (snd_map (ty_subst tip)) nt.D.ont_args in
445+
let es = e_subst tip in
446+
let bd = es nt.D.ont_body in
447+
(xs, bd)
448+
in Some (Lazy.from_fun substnt)
449+
450+
| _ -> None
451+
452+
in OK ((path, tvs), top, subue, bd)
453+
in
454+
455+
(* Precise (but expensive) classification of why [op] fails to apply. Called
456+
lazily, only when an error message is actually built, so its per-argument
457+
head-normalisation never runs on the fast typing path. *)
458+
let classify (path, op) =
438459
let subue = UniEnv.copy ue in
439460

440461
let (tip, tvs) = UniEnv.openty_r subue op.D.op_tparams tvi in
441462
let top = ty_subst tip op.D.op_ty in
442463

443464
let resolve ty = ty_subst (Tuni.subst (UniEnv.assubst subue)) ty in
444465

445-
let failure = classify_application env subue top psig retty in
446-
447-
match failure with
466+
match classify_application env subue top psig retty with
448467
| Some f ->
449468
let instance =
450469
List.combine op.D.op_tparams tvs
@@ -453,21 +472,24 @@ let select_op_outcomes
453472
| Tunivar _ -> None
454473
| _ -> Some (tp, resolve tv))
455474
in
456-
KO (path, instance, op.D.op_ty, f)
457-
| None ->
458-
let bd =
459-
match op.D.op_kind with
460-
| OB_nott nt ->
461-
let substnt () =
462-
let xs = List.map (snd_map (ty_subst tip)) nt.D.ont_args in
463-
let es = e_subst tip in
464-
let bd = es nt.D.ont_body in
465-
(xs, bd)
466-
in Some (Lazy.from_fun substnt)
475+
(path, instance, op.D.op_ty, f)
467476

468-
| _ -> None
477+
(* Unreachable: [select] only builds a [KO] once the fast unification has
478+
already rejected the candidate, and the two solve the same problem. *)
479+
| None -> assert false
480+
in
469481

470-
in OK ((path, tvs), top, subue, bd)
482+
let select (path, op) =
483+
let subue = UniEnv.copy ue in
484+
485+
let (tip, tvs) = UniEnv.openty_r subue op.D.op_tparams tvi in
486+
let top = ty_subst tip op.D.op_ty in
487+
488+
let texpected = tfun_expected subue ?retty psig in
489+
490+
match unify env subue top texpected with
491+
| () -> mk_ok (path, op) tip tvs top subue
492+
| exception UnificationFailure _ -> KO (lazy (classify (path, op)))
471493

472494
in
473495
List.map select (EcEnv.Op.all ~check:filter ~name env)
@@ -482,3 +504,17 @@ let select_op
482504
List.filter_map
483505
(function OK r -> Some r | KO _ -> None)
484506
(select_op_outcomes ?hidden ?filter tvi env name ue sig_)
507+
508+
(* -------------------------------------------------------------------- *)
509+
(* The candidates of that name that fail to apply, with the reason why. This
510+
runs the (expensive) per-argument classification and is meant to be called
511+
only when building an error message — never on the fast typing path. *)
512+
let select_op_failures
513+
?hidden ?filter (tvi : tvar_inst option)
514+
(env : EcEnv.env) (name : qsymbol) (ue : unienv)
515+
(sig_ : ty list * ty option)
516+
: (EcPath.path * op_instance * ty * op_failure) list
517+
=
518+
List.filter_map
519+
(function KO lz -> Some (Lazy.force lz) | OK _ -> None)
520+
(select_op_outcomes ?hidden ?filter tvi env name ue sig_)

src/ecUnify.mli

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ type op_failure =
5050
(* Constrained type parameters of an operator (those bound while applying it). *)
5151
type op_instance = (EcIdent.t * ty) list
5252

53-
type select_outcome =
54-
| OK of select_result
55-
| KO of EcPath.path * op_instance * ty * op_failure
56-
(* operator path, partial instantiation, declared operator type, reason *)
57-
5853
(* [None] if [top] applies to [psig] (and [retty]), updating [ue]; otherwise
5954
[Some] of the first argument/result/arity failure. *)
6055
val classify_application :
@@ -70,12 +65,13 @@ val select_op :
7065
-> dom * ty option
7166
-> select_result list
7267

73-
val select_op_outcomes :
68+
(* Expensive: classifies every failing candidate. Call only on the error path. *)
69+
val select_op_failures :
7470
?hidden:bool
7571
-> ?filter:(path -> operator -> bool)
7672
-> tvi
7773
-> EcEnv.env
7874
-> qsymbol
7975
-> unienv
8076
-> dom * ty option
81-
-> select_outcome list
77+
-> (path * op_instance * ty * op_failure) list

0 commit comments

Comments
 (0)