From 224f8b169b524bcbe333953fd9fd44ad3e7eeaa9 Mon Sep 17 00:00:00 2001 From: Alexey Danilevsky Date: Wed, 19 Nov 2025 15:49:55 +0300 Subject: [PATCH] [VSC-38] Improve typer error recovery Problem: The type checker should be more lenient and continue accumulating typing errors, and try to produce the typed AST even with errors. Solution: Use optional parameter `allow_error` in function `with_message_store`. Provide `enable_type_recovery` flag to flexibly switch on and off this functionality. --- src/js/common.ml | 5 +- src/mo_frontend/dune | 2 +- src/mo_frontend/parser.mly | 2 +- src/mo_frontend/test_recovery.ml | 283 +++++++++++++++++++++++++++++++ src/mo_frontend/typing.ml | 10 +- src/mo_frontend/typing.mli | 8 +- src/pipeline/pipeline.ml | 26 ++- src/pipeline/pipeline.mli | 3 + 8 files changed, 325 insertions(+), 14 deletions(-) diff --git a/src/js/common.ml b/src/js/common.ml index fbc2eaf54b4..1986948e942 100644 --- a/src/js/common.ml +++ b/src/js/common.ml @@ -247,13 +247,14 @@ let js_parse_motoko_typed_with_scope_cache_impl enable_recovery paths scope_cach all. Hence, the use of [Obj.magic] is legitimate here. *) String_map_conversion.from_js scope_cache Js.to_string Obj.magic) in - let parse_fn = if Js.Opt.get enable_recovery (fun () -> false) + let recovery_enabled = Js.Opt.get enable_recovery (fun () -> false) in + let parse_fn = if recovery_enabled then Pipeline.parse_file_with_recovery else Pipeline.parse_file in let load_result = Mo_types.Cons.session (fun () -> - Pipeline.load_progs_cached + Pipeline.load_progs_cached ~enable_type_recovery:recovery_enabled parse_fn paths Pipeline.initial_stat_env scope_cache) in match load_result with diff --git a/src/mo_frontend/dune b/src/mo_frontend/dune index f267a03508e..c0a9c000f51 100644 --- a/src/mo_frontend/dune +++ b/src/mo_frontend/dune @@ -28,6 +28,6 @@ (name test_recovery) (inline_tests) (modules test_recovery) - (libraries mo_frontend menhirLib lib lang_utils mo_config mo_def mo_types mo_values wasm_exts ocaml-recovery-parser.menhirRecoveryLib ) + (libraries mo_frontend menhirLib lib lang_utils mo_config mo_def mo_types mo_values wasm_exts ocaml-recovery-parser.menhirRecoveryLib pipeline ) (flags (:standard -w -40)) (preprocess (pps ppx_inline_test ppx_expect))) diff --git a/src/mo_frontend/parser.mly b/src/mo_frontend/parser.mly index 80f90f295af..6bd2be72a01 100644 --- a/src/mo_frontend/parser.mly +++ b/src/mo_frontend/parser.mly @@ -250,7 +250,7 @@ and objblock eo s id ty dec_fields = %token FLOAT %token CHAR %token BOOL -%token ID +%token ID [@recover.expr "__error_recovery_var__"] %token TEXT %token PIPE %token PRIM diff --git a/src/mo_frontend/test_recovery.ml b/src/mo_frontend/test_recovery.ml index 6761586d535..5e2f419a8b8 100644 --- a/src/mo_frontend/test_recovery.ml +++ b/src/mo_frontend/test_recovery.ml @@ -1,6 +1,8 @@ (** Maintenance note: Update of the expected values could be done via [dune runtest --auto-promote]. *) +module Parser = Mo_frontend.Parser +module Lexer = Mo_frontend.Lexer let parse_from_lexbuf lexbuf : Mo_def.Syntax.prog Diag.result = let open Mo_frontend in @@ -27,6 +29,21 @@ let show (r: Mo_def.Syntax.prog Diag.result) : String.t = ^ "\n with errors:\n" ^ show_msgs msgs | Error msgs -> "Errors:\n" ^ show_msgs msgs +let show_with_types (r: Mo_def.Syntax.prog Diag.result) : String.t = + let show_msgs msgs = + String.concat "\n" (List.map Diag.string_of_message msgs) in + match r with + | Ok (prog, msgs) -> + let module Arrange = Mo_def.Arrange.Make( + struct + include Mo_def.Arrange.Default + let include_types = true + end + ) in + "Ok: " ^ Wasm.Sexpr.to_string 80 (Arrange.prog prog) + ^ "\n with errors:\n" ^ show_msgs msgs + | Error msgs -> "Errors:\n" ^ show_msgs msgs + let _parse_test input (expected : string) = let actual = parse_from_string input in @@ -407,3 +424,269 @@ let%expect_test "test5" = module class (e.g. 'module class f(x : Int) : Int = {}') actor class (e.g. 'actor class f(x : Int) : Int = {}') persistent actor class (e.g. 'persistent actor class f(x : Int) : Int = {}') |}] + +let%expect_test "test type recovery 1" = + let s = "func test_func () { + let x = Counter(0); + x. + let a = 1; +} + +class Counter(n: Nat) { + var counter : Nat = n; + func inc() {counter +=1;}; + func get() : Nat {counter}; +}" in + match (parse_from_string s) with + | Ok (prog, _) -> begin + let open Mo_frontend in + let async_cap = Pipeline.async_cap_of_prog prog in + match (Typing.infer_prog ~enable_type_recovery:true Pipeline.initial_stat_env None async_cap prog) with + | Ok (_, msgs) -> + Printf.printf "%s" @@ show_with_types (Ok (prog, msgs)); + [%expect {| + Ok: (Prog + (LetD + (: (VarP (ID test_func)) () -> ()) + (: + (FuncE + () -> () + Local + test_func + (: (TupP) ()) + _ + + (: + (BlockE + (LetD + (: (VarP (ID x)) {}) + (: + (CallE + _ + (: (VarE (ID Counter)) (n : Nat) -> Counter) + (: (LitE (NatLit 0)) Nat) + ) + {} + ) + ) + (ExpD + (: (DotE (: (VarE (ID x)) {}) (ID __error_recovery_var__)) ???) + ) + (LetD (: (VarP (ID a)) Nat) (: (LitE (NatLit 1)) Nat)) + ) + () + ) + ) + () -> () + ) + ) + (ClassD + _ + Local + (ID Counter) + (: + (ParP + (: (AnnotP (: (VarP (ID n)) Nat) (: (PathT (IdH (ID Nat))) Nat)) Nat) + ) + Nat + ) + _ + Object + (ID @anon-object-7.23) + (DecField + (VarD + (ID counter) + (: (AnnotE (: (VarE (ID n)) Nat) (: (PathT (IdH (ID Nat))) Nat)) Nat) + ) + Private + (Flexible) + ) + (DecField + (LetD + (: (VarP (ID inc)) () -> ()) + (: + (FuncE + () -> () + Local + inc + (: (TupP) ()) + _ + + (: + (BlockE + (ExpD + (: + (AssignE + (: (VarE (ID counter)) var Nat) + (: + (BinE + Nat + (: (VarE (ID counter)) Nat) + AddOp + (: (LitE (NatLit 1)) Nat) + ) + Nat + ) + ) + () + ) + ) + ) + () + ) + ) + () -> () + ) + ) + Private + (Flexible) + ) + (DecField + (LetD + (: (VarP (ID get)) () -> Nat) + (: + (FuncE + () -> Nat + Local + get + (: (TupP) ()) + (: (PathT (IdH (ID Nat))) Nat) + + (: (BlockE (ExpD (: (VarE (ID counter)) Nat))) Nat) + ) + () -> Nat + ) + ) + Private + (Flexible) + ) + ) + ) + + with errors: + (unknown location): type error [M0072], field __error_recovery_var__ does not exist in type: + {} + |}] + | Error msgs -> Printf.printf "%s" @@ show (Error msgs) + end + | Error _ as r -> Printf.printf "%s" @@ show r; + [%expect.unreachable] + +let%expect_test "test type recovery 2" = + let s = "module M {}; +let _x = M. +" in + match (parse_from_string s) with + | Ok (prog, _) -> begin + let open Mo_frontend in + let async_cap = Pipeline.async_cap_of_prog prog in + match (Typing.infer_prog ~enable_type_recovery:true Pipeline.initial_stat_env None async_cap prog) with + | Ok (_, msgs) -> + Printf.printf "%s" @@ show_with_types (Ok (prog, msgs)); + [%expect {| + Ok: (Prog + (LetD (: (VarP (ID M)) module {}) (: (ObjBlockE _ Module _) ???)) + (LetD + (: (VarP (ID _x)) ???) + (: (DotE (: (VarE (ID M)) ???) (ID __error_recovery_var__)) ???) + ) + ) + + with errors: + (unknown location): type error [M0072], field __error_recovery_var__ does not exist in type: + module {} + |}] + | Error msgs -> Printf.printf "%s" @@ show (Error msgs) + end + | Error _ as r -> Printf.printf "%s" @@ show r; + [%expect.unreachable] + +let%expect_test "test type recovery 3" = + let s = "let _x = (1 + +" in + match (parse_from_string s) with + | Ok (prog, _) -> begin + let open Mo_frontend in + let async_cap = Pipeline.async_cap_of_prog prog in + match (Typing.infer_prog ~enable_type_recovery:true Pipeline.initial_stat_env None async_cap prog) with + | Ok (_, msgs) -> + Printf.printf "%s" @@ show_with_types (Ok (prog, msgs)); + [%expect {| + Ok: (Prog + (LetD + (: (VarP (ID _x)) Nat) + (: + (BinE + Nat + (: (LitE (NatLit 1)) Nat) + AddOp + (: (LoopE (: (BlockE) ())) None) + ) + Nat + ) + ) + ) + + with errors: + |}] + | Error msgs -> Printf.printf "%s" @@ show (Error msgs) + end + | Error _ as r -> Printf.printf "%s" @@ show r; + [%expect.unreachable] + +let%expect_test "test type recovery 4" = + let s = "f(x +" in + match (parse_from_string s) with + | Ok (prog, _) -> begin + let open Mo_frontend in + let async_cap = Pipeline.async_cap_of_prog prog in + match (Typing.infer_prog ~enable_type_recovery:true Pipeline.initial_stat_env None async_cap prog) with + | Ok (_, msgs) -> + Printf.printf "%s" @@ show_with_types (Ok (prog, msgs)); + [%expect {| + Ok: (Prog (ExpD (: (CallE _ (: (VarE (ID f)) ???) (: (VarE (ID x)) ???)) ???))) + + with errors: + (unknown location): type error [M0057], unbound variable f + |}] + | Error msgs -> Printf.printf "%s" @@ show (Error msgs) + end + | Error _ as r -> Printf.printf "%s" @@ show r; + [%expect.unreachable] + +let%expect_test "test type recovery 5" = + let s = "import A \"a\"; + A.f(x +" in + match (parse_from_string s) with + | Ok (prog, _) -> begin + let open Mo_frontend in + let async_cap = Pipeline.async_cap_of_prog prog in + match (Typing.infer_prog ~enable_type_recovery:true Pipeline.initial_stat_env None async_cap prog) with + | Ok (_, msgs) -> + Printf.printf "%s" @@ show_with_types (Ok (prog, msgs)); + [%expect {| + Ok: (Prog + (LetD (: (VarP (ID A)) ???) (: (ImportE a) ???)) + (ExpD + (: + (CallE + _ + (: (DotE (: (VarE (ID A)) ???) (ID f)) ???) + (: (VarE (ID x)) ???) + ) + ??? + ) + ) + ) + + with errors: + (unknown location): type error [M0020], unresolved import a + |}] + | Error msgs -> Printf.printf "%s" @@ show (Error msgs) + end + | Error _ as r -> Printf.printf "%s" @@ show r; + [%expect.unreachable] + + diff --git a/src/mo_frontend/typing.ml b/src/mo_frontend/typing.ml index bde0be1f27a..2f94c7e4e08 100644 --- a/src/mo_frontend/typing.ml +++ b/src/mo_frontend/typing.ml @@ -4565,12 +4565,16 @@ and infer_dec_valdecs env dec : Scope.t = } (* Programs *) -let infer_prog scope pkg_opt async_cap prog +let infer_prog ?(enable_type_recovery=false) scope pkg_opt async_cap prog : (T.typ * Scope.t) Diag.result = - Diag.with_message_store + let recovery_fn = if enable_type_recovery then + fun f y -> recover_with (Some (T.unit, Scope.empty)) (fun y -> Some (f y)) y; + else recover_opt; + in + Diag.with_message_store ~allow_errors:enable_type_recovery (fun msgs -> - recover_opt + recovery_fn (fun prog -> let env0 = env_of_scope msgs scope in let env = { diff --git a/src/mo_frontend/typing.mli b/src/mo_frontend/typing.mli index 788f4c677aa..60e264ac31f 100644 --- a/src/mo_frontend/typing.mli +++ b/src/mo_frontend/typing.mli @@ -6,7 +6,13 @@ open Scope val initial_scope : scope -val infer_prog : scope -> string option -> Async_cap.async_cap -> Syntax.prog -> (typ * scope) Diag.result +val infer_prog + : ?enable_type_recovery:bool + -> scope + -> string option + -> Async_cap.async_cap + -> Syntax.prog + -> (typ * scope) Diag.result val check_lib : scope -> string option -> Syntax.lib -> scope Diag.result val check_actors : ?check_actors:bool -> scope -> Syntax.prog list -> unit Diag.result diff --git a/src/pipeline/pipeline.ml b/src/pipeline/pipeline.ml index c32aea5d299..73fc72b3b43 100644 --- a/src/pipeline/pipeline.ml +++ b/src/pipeline/pipeline.ml @@ -185,11 +185,16 @@ let async_cap_of_prog prog = else Async_cap.initial_cap() -let infer_prog pkg_opt senv async_cap prog : (Type.typ * Scope.scope) Diag.result = +let infer_prog + ?(enable_type_recovery=false) + pkg_opt + senv + async_cap + prog : (Type.typ * Scope.scope) Diag.result = let filename = prog.Source.note.Syntax.filename in phase "Checking" filename; Cons.session ~scope:filename (fun () -> - let r = Typing.infer_prog pkg_opt senv async_cap prog in + let r = Typing.infer_prog ~enable_type_recovery pkg_opt senv async_cap prog in if !Flags.trace && !Flags.verbose then begin match r with | Ok ((_, scope), _) -> @@ -204,7 +209,10 @@ let infer_prog pkg_opt senv async_cap prog : (Type.typ * Scope.scope) Diag.resul let* () = Definedness.check_prog prog in Diag.return t_sscope) -let check_progs senv progs : (Scope.t list * Scope.t) Diag.result = +let check_progs + ?(enable_type_recovery=false) + senv + progs : (Scope.t list * Scope.t) Diag.result = let rec go senv sscopes = function | [] -> Diag.return (List.rev sscopes, senv) | prog::progs -> @@ -213,7 +221,7 @@ let check_progs senv progs : (Scope.t list * Scope.t) Diag.result = let async_cap = async_cap_of_prog prog in let* _t, sscope = Cons.session ~scope:filename (fun () -> - infer_prog senv None async_cap prog) + infer_prog ~enable_type_recovery senv None async_cap prog) in let senv' = Scope.adjoin senv sscope in let sscopes' = sscope :: sscopes in @@ -494,7 +502,13 @@ let chase_imports parsefn senv0 imports : (Syntax.lib list * Scope.scope) Diag.r let* libs, senv, _cache = chase_imports_cached parsefn senv0 imports cache in Diag.return (libs, senv) -let load_progs_cached ?check_actors parsefn files senv scope_cache : load_result_cached = +let load_progs_cached + ?check_actors + ?(enable_type_recovery=false) + parsefn + files + senv + scope_cache : load_result_cached = let open Diag.Syntax in let* parsed = Diag.traverse (parsefn Source.no_region) files in let* rs = resolve_progs parsed in @@ -506,7 +520,7 @@ let load_progs_cached ?check_actors parsefn files senv scope_cache : load_result let* () = Typing.check_actors ?check_actors senv progs in (* [infer_prog] seems to annotate the AST with types by mutating some of its nodes, therefore, we always run the type checker for programs. *) - let* sscopes, senv = check_progs senv progs in + let* sscopes, senv = check_progs ~enable_type_recovery senv progs in let prog_result = List.map2 (fun (prog, rims) sscope -> diff --git a/src/pipeline/pipeline.mli b/src/pipeline/pipeline.mli index bb89ddcb91d..7906993640d 100644 --- a/src/pipeline/pipeline.mli +++ b/src/pipeline/pipeline.mli @@ -50,8 +50,11 @@ type load_result_cached = * scope_cache ) Diag.result +val async_cap_of_prog : Syntax.prog -> Async_cap.async_cap + val load_progs_cached : ?check_actors:bool + -> ?enable_type_recovery:bool -> parse_fn -> string list -> Scope.t