From 7a16d973be1d9abdd32dfe42c0191365512fe6a5 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 12:28:48 +0100 Subject: [PATCH 01/10] implement autofilled source loc magic constants --- compiler/bsc/rescript_compiler_main.ml | 4 ++ compiler/ml/clflags.ml | 2 + compiler/ml/clflags.mli | 1 + compiler/ml/lambda.ml | 38 +++++++++++- compiler/ml/lambda.mli | 16 ++++- compiler/ml/predef.ml | 13 ++++ compiler/ml/predef.mli | 2 + compiler/ml/printlambda.ml | 2 + compiler/ml/translcore.ml | 35 +++++++++-- compiler/ml/translcore.mli | 2 + compiler/ml/translmod.ml | 60 ++++++++++--------- compiler/ml/typecore.ml | 48 ++++++++++++--- packages/@rescript/runtime/Pervasives.res | 2 + packages/@rescript/runtime/Stdlib.res | 1 + .../@rescript/runtime/Stdlib_SourceLoc.res | 44 ++++++++++++++ packages/@rescript/runtime/lib/es6/Stdlib.mjs | 5 +- .../runtime/lib/es6/Stdlib_SourceLoc.mjs | 57 ++++++++++++++++++ packages/@rescript/runtime/lib/js/Stdlib.cjs | 5 +- .../runtime/lib/js/Stdlib_SourceLoc.cjs | 55 +++++++++++++++++ tests/tests/src/source_loc_test.mjs | 30 ++++++++++ tests/tests/src/source_loc_test.res | 28 +++++++++ tests/tests/src/test_per.res | 2 + 22 files changed, 404 insertions(+), 48 deletions(-) create mode 100644 packages/@rescript/runtime/Stdlib_SourceLoc.res create mode 100644 packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs create mode 100644 packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs create mode 100644 tests/tests/src/source_loc_test.mjs create mode 100644 tests/tests/src/source_loc_test.res diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 8d4113cebdf..b030546352f 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -336,6 +336,10 @@ let command_line_flags : (string * Bsc_args.spec * string) array = ( "-bs-noassertfalse", set Clflags.no_assert_false, "*internal* no code for assert false" ); + ( "-implicit-source-loc", + set Clflags.implicit_source_loc, + "*internal* Enable implicit source-loc autofill for optional source-loc \ + args" ); ( "-noassert", set Clflags.noassert, "*internal* Do not compile assertion checks" ); diff --git a/compiler/ml/clflags.ml b/compiler/ml/clflags.ml index f0cd88115e7..12c05239246 100644 --- a/compiler/ml/clflags.ml +++ b/compiler/ml/clflags.ml @@ -46,6 +46,8 @@ and ignore_parse_errors = ref false (* -ignore-parse-errors *) let dont_write_files = ref false (* set to true under ocamldoc *) +let implicit_source_loc = ref false + let reset_dump_state () = dump_source := false; dump_parsetree := false; diff --git a/compiler/ml/clflags.mli b/compiler/ml/clflags.mli index 0cb5f1ea3e5..528352affb7 100644 --- a/compiler/ml/clflags.mli +++ b/compiler/ml/clflags.mli @@ -22,6 +22,7 @@ val dump_typedtree : bool ref val dump_rawlambda : bool ref val dump_lambda : bool ref val dont_write_files : bool ref +val implicit_source_loc : bool ref val keep_locs : bool ref val only_parse : bool ref val ignore_parse_errors : bool ref diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index db810d4f912..c4c667ac1a2 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -13,7 +13,14 @@ (* *) (**************************************************************************) -type loc_kind = Loc_FILE | Loc_LINE | Loc_MODULE | Loc_LOC | Loc_POS +type loc_kind = + | Loc_FILE + | Loc_LINE + | Loc_MODULE + | Loc_SOURCE_LOC_VALUE_PATH + | Loc_SOURCE_LOC_POS + | Loc_LOC + | Loc_POS type tag_info = | Blk_constructor of { @@ -699,13 +706,22 @@ let raise_kind = function | Raise_regular -> "raise" | Raise_reraise -> "reraise" -let lam_of_loc kind loc = +let lam_of_loc ?(root_path : Path.t option) ?(current_value_path = []) kind loc = let loc_start = loc.Location.loc_start in + let loc_end = loc.loc_end in let file, lnum, cnum = Location.get_pos_info loc_start in + let _, end_lnum, end_cnum = Location.get_pos_info loc_end in let file = Filename.basename file in let enum = loc.Location.loc_end.Lexing.pos_cnum - loc_start.Lexing.pos_cnum + cnum in + let module_path = + match root_path with + | Some path -> Path.name path + | None -> + let name = Env.get_unit_name () in + if name = "" then Filename.remove_extension file else name + in match kind with | Loc_POS -> Lconst @@ -718,6 +734,24 @@ let lam_of_loc kind loc = Const_base (Const_int enum); ] )) | Loc_FILE -> Lconst (Const_immstring file) + | Loc_SOURCE_LOC_POS -> + Lconst + (Const_immstring + ([ + file; + string_of_int lnum; + string_of_int cnum; + string_of_int end_lnum; + string_of_int end_cnum; + ] + |> String.concat ";")) + | Loc_SOURCE_LOC_VALUE_PATH -> + let value_path = + match current_value_path with + | [] -> module_path + | _ -> String.concat "." (module_path :: current_value_path) + in + Lconst (Const_immstring value_path) | Loc_MODULE -> let filename = Filename.basename file in let name = Env.get_unit_name () in diff --git a/compiler/ml/lambda.mli b/compiler/ml/lambda.mli index d8eaf57be6f..81ccb39a854 100644 --- a/compiler/ml/lambda.mli +++ b/compiler/ml/lambda.mli @@ -17,7 +17,14 @@ open Asttypes -type loc_kind = Loc_FILE | Loc_LINE | Loc_MODULE | Loc_LOC | Loc_POS +type loc_kind = + | Loc_FILE + | Loc_LINE + | Loc_MODULE + | Loc_SOURCE_LOC_VALUE_PATH + | Loc_SOURCE_LOC_POS + | Loc_LOC + | Loc_POS type tag_info = | Blk_constructor of { @@ -419,4 +426,9 @@ val is_guarded : lambda -> bool val patch_guarded : lambda -> lambda -> lambda val raise_kind : raise_kind -> string -val lam_of_loc : loc_kind -> Location.t -> lambda +val lam_of_loc : + ?root_path:Path.t -> + ?current_value_path:string list -> + loc_kind -> + Location.t -> + lambda diff --git a/compiler/ml/predef.ml b/compiler/ml/predef.ml index 5913791eff9..30782fc187a 100644 --- a/compiler/ml/predef.ml +++ b/compiler/ml/predef.ml @@ -51,6 +51,10 @@ and ident_result = ident_create "result" and ident_dict = ident_create "dict" +and ident_source_loc_pos = ident_create "sourceLocPos" + +and ident_source_loc_value_path = ident_create "sourceLocValuePath" + and ident_bigint = ident_create "bigint" and ident_string = ident_create "string" @@ -67,6 +71,9 @@ let type_is_builtin_path_but_option (p : Path.t) : test = match p with | Pident {stamp} when stamp = ident_option.stamp -> For_sure_no | Pident {stamp} when stamp = ident_unit.stamp -> For_sure_no + | Pident {stamp} when stamp = ident_source_loc_pos.stamp -> For_sure_yes + | Pident {stamp} when stamp = ident_source_loc_value_path.stamp -> + For_sure_yes | Pident {stamp} when stamp >= ident_int.stamp && stamp <= ident_promise.stamp -> For_sure_yes @@ -94,6 +101,10 @@ and path_result = Pident ident_result and path_dict = Pident ident_dict +and path_source_loc_pos = Pident ident_source_loc_pos + +and path_source_loc_value_path = Pident ident_source_loc_value_path + and path_bigint = Pident ident_bigint and path_string = Pident ident_string @@ -371,6 +382,8 @@ let common_initial_env add_type add_extension empty_env = |> add_type ident_array decl_array |> add_type ident_list decl_list |> add_type ident_dict decl_dict + |> add_type ident_source_loc_pos decl_abstr + |> add_type ident_source_loc_value_path decl_abstr |> add_type ident_unknown decl_unknown |> add_exception ident_undefined_recursive_module [newgenty (Ttuple [type_string; type_int; type_int])] diff --git a/compiler/ml/predef.mli b/compiler/ml/predef.mli index 8e3330fe43c..6aefe24561a 100644 --- a/compiler/ml/predef.mli +++ b/compiler/ml/predef.mli @@ -45,6 +45,8 @@ val path_list : Path.t val path_option : Path.t val path_result : Path.t val path_dict : Path.t +val path_source_loc_pos : Path.t +val path_source_loc_value_path : Path.t val path_bigint : Path.t val path_extension_constructor : Path.t diff --git a/compiler/ml/printlambda.ml b/compiler/ml/printlambda.ml index 0282f6e113c..020f6eeee8a 100644 --- a/compiler/ml/printlambda.ml +++ b/compiler/ml/printlambda.ml @@ -55,6 +55,8 @@ let string_of_loc_kind = function | Loc_FILE -> "loc_FILE" | Loc_LINE -> "loc_LINE" | Loc_MODULE -> "loc_MODULE" + | Loc_SOURCE_LOC_VALUE_PATH -> "loc_SOURCE_LOC_VALUE_PATH" + | Loc_SOURCE_LOC_POS -> "loc_SOURCE_LOC_POS" | Loc_POS -> "loc_POS" | Loc_LOC -> "loc_LOC" diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 078cbf133a0..b50b700d9c5 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -34,6 +34,21 @@ let transl_module = (fun _cc _rootpath _modl -> assert false : module_coercion -> Path.t option -> module_expr -> lambda) +let current_root_path = ref None +let current_value_path = ref [] + +let with_current_value_path pat f = + let path_segment = + match pat.pat_desc with + | Tpat_var (id, _) -> Some (Ident.name id) + | Tpat_alias (_, id, _) -> Some (Ident.name id) + | _ -> None + in + match path_segment with + | None -> f () + | Some name -> + Ext_ref.protect current_value_path (!current_value_path @ [name]) f + (* Compile an exception/extension definition *) let transl_extension_constructor env path ext = @@ -244,6 +259,8 @@ let primitives_table = ("%loc_LINE", Ploc Loc_LINE); ("%loc_POS", Ploc Loc_POS); ("%loc_MODULE", Ploc Loc_MODULE); + ("%loc_SOURCE_LOC_VALUE_PATH", Ploc Loc_SOURCE_LOC_VALUE_PATH); + ("%loc_SOURCE_LOC_POS", Ploc Loc_SOURCE_LOC_POS); (* BEGIN Triples for ref data type *) ("%makeref", Pmakeblock Lambda.ref_tag_info); ("%refset", Psetfield (0, Lambda.ref_field_set_info)); @@ -449,7 +466,10 @@ let transl_primitive loc p env ty = in match prim with | Ploc kind -> ( - let lam = lam_of_loc kind loc in + let lam = + lam_of_loc ?root_path:!current_root_path + ~current_value_path:!current_value_path kind loc + in match p.prim_arity with | 0 -> lam | 1 -> @@ -743,9 +763,14 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = | _ -> k in wrap (Lprim (Praise k, [targ], e.exp_loc)) - | Ploc kind, [] -> lam_of_loc kind e.exp_loc + | Ploc kind, [] -> + lam_of_loc ?root_path:!current_root_path + ~current_value_path:!current_value_path kind e.exp_loc | Ploc kind, [arg1] -> - let lam = lam_of_loc kind arg1.exp_loc in + let lam = + lam_of_loc ?root_path:!current_root_path + ~current_value_path:!current_value_path kind arg1.exp_loc + in Lprim (Pmakeblock Blk_tuple, lam :: argl, e.exp_loc) | Ploc _, _ -> assert false | _, _ -> ( @@ -1076,7 +1101,7 @@ and transl_let rec_flag pat_expr_list body = let rec transl = function | [] -> body | {vb_pat = pat; vb_expr = expr; vb_attributes = attr; vb_loc} :: rem -> - let lam = transl_exp expr in + let lam = with_current_value_path pat (fun () -> transl_exp expr) in let lam = Translattribute.add_inline_attribute lam vb_loc attr in Matching.for_let pat.pat_loc lam pat (transl rem) in @@ -1092,7 +1117,7 @@ and transl_let rec_flag pat_expr_list body = Only variables are allowed as left-hand side of `let rec' *) in - let lam = transl_exp expr in + let lam = with_current_value_path pat (fun () -> transl_exp expr) in let lam = Translattribute.add_inline_attribute lam vb_loc vb_attributes in (id, lam) in diff --git a/compiler/ml/translcore.mli b/compiler/ml/translcore.mli index 1847a4883c4..b84678bd81c 100644 --- a/compiler/ml/translcore.mli +++ b/compiler/ml/translcore.mli @@ -34,6 +34,8 @@ val transl_primitive : val transl_extension_constructor : Env.t -> Path.t option -> Typedtree.extension_constructor -> Lambda.lambda +val current_root_path : Path.t option ref + (* Forward declaration -- to be filled in by Translmod.transl_module *) val transl_module : (Typedtree.module_coercion -> diff --git a/compiler/ml/translmod.ml b/compiler/ml/translmod.ml index 87471ac26bf..55179e93b88 100644 --- a/compiler/ml/translmod.ml +++ b/compiler/ml/translmod.ml @@ -254,35 +254,37 @@ let rec compile_functor mexp coercion root_path loc = (* Compile a module expression *) and transl_module cc rootpath mexp = - List.iter (Translattribute.check_attribute_on_module mexp) mexp.mod_attributes; - let loc = mexp.mod_loc in - match mexp.mod_type with - | Mty_alias (Mta_absent, _) -> - apply_coercion loc Alias cc Lambda.lambda_module_alias - | _ -> ( - match mexp.mod_desc with - | Tmod_ident (path, _) -> - apply_coercion loc Strict cc - (Lambda.transl_module_path ~loc mexp.mod_env path) - | Tmod_structure str -> fst (transl_struct loc [] cc rootpath str) - | Tmod_functor _ -> compile_functor mexp cc rootpath loc - | Tmod_apply (funct, arg, ccarg) -> - let inlined_attribute, funct = - Translattribute.get_and_remove_inlined_attribute_on_module funct - in - apply_coercion loc Strict cc - (Lapply - { - ap_loc = loc; - ap_func = transl_module Tcoerce_none None funct; - ap_args = [transl_module ccarg None arg]; - ap_inlined = inlined_attribute; - ap_transformed_jsx = false; - }) - | Tmod_constraint (arg, _, _, ccarg) -> - transl_module (compose_coercions cc ccarg) rootpath arg - | Tmod_unpack (arg, _) -> - apply_coercion loc Strict cc (Translcore.transl_exp arg)) + Ext_ref.protect Translcore.current_root_path rootpath (fun () -> + List.iter (Translattribute.check_attribute_on_module mexp) + mexp.mod_attributes; + let loc = mexp.mod_loc in + match mexp.mod_type with + | Mty_alias (Mta_absent, _) -> + apply_coercion loc Alias cc Lambda.lambda_module_alias + | _ -> ( + match mexp.mod_desc with + | Tmod_ident (path, _) -> + apply_coercion loc Strict cc + (Lambda.transl_module_path ~loc mexp.mod_env path) + | Tmod_structure str -> fst (transl_struct loc [] cc rootpath str) + | Tmod_functor _ -> compile_functor mexp cc rootpath loc + | Tmod_apply (funct, arg, ccarg) -> + let inlined_attribute, funct = + Translattribute.get_and_remove_inlined_attribute_on_module funct + in + apply_coercion loc Strict cc + (Lapply + { + ap_loc = loc; + ap_func = transl_module Tcoerce_none None funct; + ap_args = [transl_module ccarg None arg]; + ap_inlined = inlined_attribute; + ap_transformed_jsx = false; + }) + | Tmod_constraint (arg, _, _, ccarg) -> + transl_module (compose_coercions cc ccarg) rootpath arg + | Tmod_unpack (arg, _) -> + apply_coercion loc Strict cc (Translcore.transl_exp arg))) and transl_struct loc fields cc rootpath str = transl_structure loc fields cc rootpath str.str_final_env str.str_items diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index ee304dff7d7..96dddaabc00 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2239,6 +2239,21 @@ let extract_function_name funct = | Texp_ident (path, _, _) -> Some (Longident.parse (Path.name path)) | _ -> None +type implicit_source_loc_kind = Source_loc_pos | Source_loc_value_path + +let optional_source_loc_kind env ty = + match (expand_head env ty).desc with + | Tconstr (option_path, [inner], _) + when Path.same option_path Predef.path_option -> ( + match (expand_head env inner).desc with + | Tconstr (path, [], _) when Path.same path Predef.path_source_loc_pos -> + Some Source_loc_pos + | Tconstr (path, [], _) + when Path.same path Predef.path_source_loc_value_path -> + Some Source_loc_value_path + | _ -> None) + | _ -> None + type lazy_args = (Asttypes.arg_label * (unit -> Typedtree.expression) option) list @@ -2247,6 +2262,24 @@ let rec type_exp ?deprecated_context ~context ?recarg env sexp = (* We now delegate everything to type_expect *) type_expect ?deprecated_context ~context ?recarg env sexp (newvar ()) +and implicit_source_loc_arg ~apply_loc env ty kind = + let loc = {apply_loc with Location.loc_ghost = true} in + let expr = + Ast_helper.Exp.ident ~loc + (Location.mknoloc + (Longident.Lident + (match kind with + | Source_loc_pos -> "__SOURCE_LOC_POS__" + | Source_loc_value_path -> "__SOURCE_LOC_VALUE_PATH__"))) + in + option_some (type_expect ~context:None env expr (extract_option_type env ty)) + +and missing_optional_arg ~apply_loc env ty = + match optional_source_loc_kind env ty with + | Some kind when !Clflags.implicit_source_loc -> + fun () -> implicit_source_loc_arg ~apply_loc env ty kind + | _ -> fun () -> option_none (instance env ty) Location.none + (* Typing of an expression with an expected type. This provide better error messages, and allows controlled propagation of return type information. @@ -2449,7 +2482,9 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected) let args, ty_res, fully_applied = match translate_unified_ops env funct sargs with | Some (targs, result_type) -> (targs, result_type, true) - | None -> type_application ~context total_app env funct sargs + | None -> + type_application ~context ~apply_loc:sexp.pexp_loc total_app env funct + sargs in end_def (); unify_var env (newvar ()) funct.exp_type; @@ -3531,7 +3566,7 @@ and translate_unified_ops (env : Env.t) (funct : Typedtree.expression) | _ -> None) | _ -> None -and type_application ~context total_app env funct (sargs : sargs) : +and type_application ~context ~apply_loc total_app env funct (sargs : sargs) : targs * Types.type_expr * bool = let result_type omitted ty_fun = List.fold_left @@ -3631,9 +3666,7 @@ and type_application ~context total_app env funct (sargs : sargs) : match (expand_head env ty_fun).desc with | Tarrow ({lbl; typ = t1}, t2, _, _) when is_optional lbl -> ignored := (lbl, t1, ty_fun.level) :: !ignored; - let arg = - (lbl, Some (fun () -> option_none (instance env t1) Location.none)) - in + let arg = (lbl, Some (missing_optional_arg ~apply_loc env t1)) in type_unknown_args max_arity ~args:(arg :: args) ~top_arity:None omitted t2 [] | _ -> collect_args () @@ -3708,9 +3741,8 @@ and type_application ~context total_app env funct (sargs : sargs) : | None -> if optional && (total_app || label_assoc Nolabel sargs) then ( ignored := (l, ty, lv) :: !ignored; - ( sargs, - omitted, - Some (fun () -> option_none (instance env ty) Location.none) )) + let arg = Some (missing_optional_arg ~apply_loc env ty) in + (sargs, omitted, arg)) else (sargs, (l, ty, lv) :: omitted, None) | Some (l', sarg0, sargs) -> if (not optional) && is_optional l' then diff --git a/packages/@rescript/runtime/Pervasives.res b/packages/@rescript/runtime/Pervasives.res index dcd2a29780a..a96856db370 100644 --- a/packages/@rescript/runtime/Pervasives.res +++ b/packages/@rescript/runtime/Pervasives.res @@ -50,6 +50,8 @@ external __LOC__: string = "%loc_LOC" external __FILE__: string = "%loc_FILE" external __LINE__: int = "%loc_LINE" external __MODULE__: string = "%loc_MODULE" +external __SOURCE_LOC_VALUE_PATH__: sourceLocValuePath = "%loc_SOURCE_LOC_VALUE_PATH" +external __SOURCE_LOC_POS__: sourceLocPos = "%loc_SOURCE_LOC_POS" external __POS__: (string, int, int, int) = "%loc_POS" external __LOC_OF__: 'a => (string, 'a) = "%loc_LOC" diff --git a/packages/@rescript/runtime/Stdlib.res b/packages/@rescript/runtime/Stdlib.res index 52f2771c699..35b7bf168c2 100644 --- a/packages/@rescript/runtime/Stdlib.res +++ b/packages/@rescript/runtime/Stdlib.res @@ -27,6 +27,7 @@ module Pair = Stdlib_Pair module Promise = Stdlib_Promise module RegExp = Stdlib_RegExp module Result = Stdlib_Result +module SourceLoc = Stdlib_SourceLoc module String = Stdlib_String module Symbol = Stdlib_Symbol module Type = Stdlib_Type diff --git a/packages/@rescript/runtime/Stdlib_SourceLoc.res b/packages/@rescript/runtime/Stdlib_SourceLoc.res new file mode 100644 index 00000000000..75d30583ab4 --- /dev/null +++ b/packages/@rescript/runtime/Stdlib_SourceLoc.res @@ -0,0 +1,44 @@ +module Pos = { + type t = sourceLocPos + + type decoded = { + file: string, + startLine: int, + startCol: int, + endLine: int, + endCol: int, + } + + external toString: t => string = "%identity" + + let decode = (value: t): option => { + switch value->toString->Stdlib_String.split(";") { + | [file, startLine, startCol, endLine, endCol] => + switch ( + startLine->Stdlib_Int.fromString, + startCol->Stdlib_Int.fromString, + endLine->Stdlib_Int.fromString, + endCol->Stdlib_Int.fromString, + ) { + | (Some(startLine), Some(startCol), Some(endLine), Some(endCol)) => + Some({file, startLine, startCol, endLine, endCol}) + | _ => None + } + | _ => None + } + } +} + +module ValuePath = { + type t = sourceLocValuePath + + external toString: t => string = "%identity" + + let segments = (value: t) => value->toString->Stdlib_String.split(".") + + let name = (value: t) => { + let segments = value->segments + let length = segments->Stdlib_Array.length + length === 0 ? "" : segments->Stdlib_Array.getUnsafe(length - 1) + } +} diff --git a/packages/@rescript/runtime/lib/es6/Stdlib.mjs b/packages/@rescript/runtime/lib/es6/Stdlib.mjs index 28ccab5bbc2..de9742d0eee 100644 --- a/packages/@rescript/runtime/lib/es6/Stdlib.mjs +++ b/packages/@rescript/runtime/lib/es6/Stdlib.mjs @@ -12,7 +12,7 @@ function assertEqual(a, b) { RE_EXN_ID: "Assert_failure", _1: [ "Stdlib.res", - 153, + 154, 4 ], Error: new Error() @@ -77,6 +77,8 @@ let $$RegExp; let Result; +let SourceLoc; + let $$String; let $$Symbol; @@ -153,6 +155,7 @@ export { $$Promise, $$RegExp, Result, + SourceLoc, $$String, $$Symbol, Type, diff --git a/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs b/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs new file mode 100644 index 00000000000..ac49ec0fa1d --- /dev/null +++ b/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs @@ -0,0 +1,57 @@ + + +import * as Stdlib_Int from "./Stdlib_Int.mjs"; + +function decode(value) { + let match = value.split(";"); + if (match.length !== 5) { + return; + } + let file = match[0]; + let startLine = match[1]; + let startCol = match[2]; + let endLine = match[3]; + let endCol = match[4]; + let match$1 = Stdlib_Int.fromString(startLine); + let match$2 = Stdlib_Int.fromString(startCol); + let match$3 = Stdlib_Int.fromString(endLine); + let match$4 = Stdlib_Int.fromString(endCol); + if (match$1 !== undefined && match$2 !== undefined && match$3 !== undefined && match$4 !== undefined) { + return { + file: file, + startLine: match$1, + startCol: match$2, + endLine: match$3, + endCol: match$4 + }; + } +} + +let Pos = { + decode: decode +}; + +function segments(value) { + return value.split("."); +} + +function name(value) { + let segments = value.split("."); + let length = segments.length; + if (length === 0) { + return ""; + } else { + return segments[length - 1 | 0]; + } +} + +let ValuePath = { + segments: segments, + name: name +}; + +export { + Pos, + ValuePath, +} +/* No side effect */ diff --git a/packages/@rescript/runtime/lib/js/Stdlib.cjs b/packages/@rescript/runtime/lib/js/Stdlib.cjs index a36bc7bc7fc..b74e3c09421 100644 --- a/packages/@rescript/runtime/lib/js/Stdlib.cjs +++ b/packages/@rescript/runtime/lib/js/Stdlib.cjs @@ -12,7 +12,7 @@ function assertEqual(a, b) { RE_EXN_ID: "Assert_failure", _1: [ "Stdlib.res", - 153, + 154, 4 ], Error: new Error() @@ -77,6 +77,8 @@ let $$RegExp; let Result; +let SourceLoc; + let $$String; let $$Symbol; @@ -152,6 +154,7 @@ exports.Pair = Pair; exports.$$Promise = $$Promise; exports.$$RegExp = $$RegExp; exports.Result = Result; +exports.SourceLoc = SourceLoc; exports.$$String = $$String; exports.$$Symbol = $$Symbol; exports.Type = Type; diff --git a/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs b/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs new file mode 100644 index 00000000000..6f8871a11e6 --- /dev/null +++ b/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs @@ -0,0 +1,55 @@ +'use strict'; + +let Stdlib_Int = require("./Stdlib_Int.cjs"); + +function decode(value) { + let match = value.split(";"); + if (match.length !== 5) { + return; + } + let file = match[0]; + let startLine = match[1]; + let startCol = match[2]; + let endLine = match[3]; + let endCol = match[4]; + let match$1 = Stdlib_Int.fromString(startLine); + let match$2 = Stdlib_Int.fromString(startCol); + let match$3 = Stdlib_Int.fromString(endLine); + let match$4 = Stdlib_Int.fromString(endCol); + if (match$1 !== undefined && match$2 !== undefined && match$3 !== undefined && match$4 !== undefined) { + return { + file: file, + startLine: match$1, + startCol: match$2, + endLine: match$3, + endCol: match$4 + }; + } +} + +let Pos = { + decode: decode +}; + +function segments(value) { + return value.split("."); +} + +function name(value) { + let segments = value.split("."); + let length = segments.length; + if (length === 0) { + return ""; + } else { + return segments[length - 1 | 0]; + } +} + +let ValuePath = { + segments: segments, + name: name +}; + +exports.Pos = Pos; +exports.ValuePath = ValuePath; +/* No side effect */ diff --git a/tests/tests/src/source_loc_test.mjs b/tests/tests/src/source_loc_test.mjs new file mode 100644 index 00000000000..5239b46239e --- /dev/null +++ b/tests/tests/src/source_loc_test.mjs @@ -0,0 +1,30 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Stdlib_SourceLoc from "@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs"; + +Mocha.describe("SourceLoc", () => { + Mocha.test("Pos.decode parses sourceLocPos", () => { + let decoded = Stdlib_SourceLoc.Pos.decode("demo.res;1;2;3;4"); + Test_utils.eq("File \"source_loc_test.res\", line 10, characters 7-14", decoded, { + file: "demo.res", + startLine: 1, + startCol: 2, + endLine: 3, + endCol: 4 + }); + }); + Mocha.test("Pos.decode rejects malformed sourceLocPos", () => Test_utils.eq("File \"source_loc_test.res\", line 20, characters 7-14", Stdlib_SourceLoc.Pos.decode("bad"), undefined)); + Mocha.test("ValuePath helpers expose segments and name", () => { + let valuePath = "Demo.Nested.run"; + Test_utils.eq("File \"source_loc_test.res\", line 25, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), [ + "Demo", + "Nested", + "run" + ]); + Test_utils.eq("File \"source_loc_test.res\", line 26, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), "run"); + }); +}); + +/* Not a pure module */ diff --git a/tests/tests/src/source_loc_test.res b/tests/tests/src/source_loc_test.res new file mode 100644 index 00000000000..9d2448eed87 --- /dev/null +++ b/tests/tests/src/source_loc_test.res @@ -0,0 +1,28 @@ +open Mocha +open Test_utils + +external unsafeSourceLocPos: string => sourceLocPos = "%identity" +external unsafeSourceLocValuePath: string => sourceLocValuePath = "%identity" + +describe("SourceLoc", () => { + test("Pos.decode parses sourceLocPos", () => { + let decoded = SourceLoc.Pos.decode(unsafeSourceLocPos("demo.res;1;2;3;4")) + eq(__LOC__, decoded, Some({ + file: "demo.res", + startLine: 1, + startCol: 2, + endLine: 3, + endCol: 4, + })) + }) + + test("Pos.decode rejects malformed sourceLocPos", () => { + eq(__LOC__, SourceLoc.Pos.decode(unsafeSourceLocPos("bad")), None) + }) + + test("ValuePath helpers expose segments and name", () => { + let valuePath = unsafeSourceLocValuePath("Demo.Nested.run") + eq(__LOC__, SourceLoc.ValuePath.segments(valuePath), ["Demo", "Nested", "run"]) + eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "run") + }) +}) diff --git a/tests/tests/src/test_per.res b/tests/tests/src/test_per.res index b53ba0515de..ade84b0b60e 100644 --- a/tests/tests/src/test_per.res +++ b/tests/tests/src/test_per.res @@ -18,6 +18,8 @@ external __LOC__: string = "%loc_LOC" external __MODULE__: string = "%loc_FILE" external __LINE__: int = "%loc_LINE" external __MODULE__: string = "%loc_MODULE" +external __SOURCE_LOC_VALUE_PATH__: sourceLocValuePath = "%loc_SOURCE_LOC_VALUE_PATH" +external __SOURCE_LOC_POS__: sourceLocPos = "%loc_SOURCE_LOC_POS" external __POS__: (string, int, int, int) = "%loc_POS" external __LOC_OF__: 'a => (string, 'a) = "%loc_LOC" From a52c77fc387fe6f19411f3574fbfc3a2657494fa Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 12:46:49 +0100 Subject: [PATCH 02/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3126ba3d180..22b86dc7166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Reanalyze: add glob pattern support for suppress/unsuppress configurations (e.g., `"src/generated/**"`). https://github.com/rescript-lang/rescript/pull/8277 - Add optional `~locales` and `~options` parameters to `String.localeCompare`. https://github.com/rescript-lang/rescript/pull/8287 +- Add new `sourceLocPos` and `sourceLocValuePath` magic constants, and allow them to be autoinjected from the call site into function arguments via `-implicit-source-loc`. https://github.com/rescript-lang/rescript/pull/8303 #### :bug: Bug fix From 07b130fd4af9cda478b1dba747d34cf436848b23 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 12:53:23 +0100 Subject: [PATCH 03/10] format --- compiler/ml/lambda.ml | 3 ++- compiler/ml/translmod.ml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index c4c667ac1a2..bc9b410b9e5 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -706,7 +706,8 @@ let raise_kind = function | Raise_regular -> "raise" | Raise_reraise -> "reraise" -let lam_of_loc ?(root_path : Path.t option) ?(current_value_path = []) kind loc = +let lam_of_loc ?(root_path : Path.t option) ?(current_value_path = []) kind loc + = let loc_start = loc.Location.loc_start in let loc_end = loc.loc_end in let file, lnum, cnum = Location.get_pos_info loc_start in diff --git a/compiler/ml/translmod.ml b/compiler/ml/translmod.ml index 55179e93b88..08c721f67c1 100644 --- a/compiler/ml/translmod.ml +++ b/compiler/ml/translmod.ml @@ -255,7 +255,8 @@ let rec compile_functor mexp coercion root_path loc = (* Compile a module expression *) and transl_module cc rootpath mexp = Ext_ref.protect Translcore.current_root_path rootpath (fun () -> - List.iter (Translattribute.check_attribute_on_module mexp) + List.iter + (Translattribute.check_attribute_on_module mexp) mexp.mod_attributes; let loc = mexp.mod_loc in match mexp.mod_type with From 629d9b8247de91ee4047ba6e7929899b16f9afcc Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 13:01:49 +0100 Subject: [PATCH 04/10] format --- tests/tests/src/source_loc_test.res | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/tests/src/source_loc_test.res b/tests/tests/src/source_loc_test.res index 9d2448eed87..dfe7b002a83 100644 --- a/tests/tests/src/source_loc_test.res +++ b/tests/tests/src/source_loc_test.res @@ -7,13 +7,17 @@ external unsafeSourceLocValuePath: string => sourceLocValuePath = "%identity" describe("SourceLoc", () => { test("Pos.decode parses sourceLocPos", () => { let decoded = SourceLoc.Pos.decode(unsafeSourceLocPos("demo.res;1;2;3;4")) - eq(__LOC__, decoded, Some({ - file: "demo.res", - startLine: 1, - startCol: 2, - endLine: 3, - endCol: 4, - })) + eq( + __LOC__, + decoded, + Some({ + file: "demo.res", + startLine: 1, + startCol: 2, + endLine: 3, + endCol: 4, + }), + ) }) test("Pos.decode rejects malformed sourceLocPos", () => { From 78049df303398dae57239640d727bb7a393ef89c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 13:36:05 +0100 Subject: [PATCH 05/10] regenerate tests --- tests/tests/src/source_loc_test.mjs | 111 +++++++++++++++++++++++++++- tests/tests/src/source_loc_test.res | 75 +++++++++++++++++++ 2 files changed, 182 insertions(+), 4 deletions(-) diff --git a/tests/tests/src/source_loc_test.mjs b/tests/tests/src/source_loc_test.mjs index 5239b46239e..08f0be6fb7a 100644 --- a/tests/tests/src/source_loc_test.mjs +++ b/tests/tests/src/source_loc_test.mjs @@ -4,10 +4,87 @@ import * as Mocha from "mocha"; import * as Test_utils from "./test_utils.mjs"; import * as Stdlib_SourceLoc from "@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs"; +function capture(pos, valuePath) { + return [ + pos, + valuePath + ]; +} + +let topLevelBinding_0 = "source_loc_test.res;16;22;16;31"; + +let topLevelBinding_1 = "Source_loc_test.topLevelBinding"; + +let topLevelBinding = [ + topLevelBinding_0, + topLevelBinding_1 +]; + +let topLevelExpressionCapture = { + contents: undefined +}; + +function storeTopLevelExpression(pos, valuePath) { + topLevelExpressionCapture.contents = [ + pos, + valuePath + ]; +} + +storeTopLevelExpression("source_loc_test.res;27;0;27;25", "Source_loc_test"); + +let nestedBinding_0 = "source_loc_test.res;30;22;30;31"; + +let nestedBinding_1 = "Source_loc_test.Nested.nestedBinding"; + +let nestedBinding = [ + nestedBinding_0, + nestedBinding_1 +]; + +let topLevelExpressionCapture$1 = { + contents: undefined +}; + +function storeTopLevelExpression$1(pos, valuePath) { + topLevelExpressionCapture$1.contents = [ + pos, + valuePath + ]; +} + +storeTopLevelExpression$1("source_loc_test.res;41;2;41;27", "Source_loc_test.Nested"); + +let Nested = { + nestedBinding: nestedBinding, + topLevelExpressionCapture: topLevelExpressionCapture$1, + storeTopLevelExpression: storeTopLevelExpression$1 +}; + +function expectCapture(loc, param, expectedValuePath) { + let valuePath = param[1]; + let pos = param[0]; + if (pos !== undefined) { + let match = Stdlib_SourceLoc.Pos.decode(pos); + if (match !== undefined) { + Test_utils.eq(loc, match.file, "source_loc_test.res"); + } else { + Test_utils.ok(loc, false); + } + } else { + Test_utils.ok(loc, false); + } + if (valuePath !== undefined) { + return Test_utils.eq(loc, valuePath, expectedValuePath); + } else { + return Test_utils.ok(loc, false); + } +} + Mocha.describe("SourceLoc", () => { Mocha.test("Pos.decode parses sourceLocPos", () => { let decoded = Stdlib_SourceLoc.Pos.decode("demo.res;1;2;3;4"); - Test_utils.eq("File \"source_loc_test.res\", line 10, characters 7-14", decoded, { + Test_utils.eq("File \"source_loc_test.res\", line 64, characters 6-13", decoded, { file: "demo.res", startLine: 1, startCol: 2, @@ -15,16 +92,42 @@ Mocha.describe("SourceLoc", () => { endCol: 4 }); }); - Mocha.test("Pos.decode rejects malformed sourceLocPos", () => Test_utils.eq("File \"source_loc_test.res\", line 20, characters 7-14", Stdlib_SourceLoc.Pos.decode("bad"), undefined)); + Mocha.test("Pos.decode rejects malformed sourceLocPos", () => Test_utils.eq("File \"source_loc_test.res\", line 77, characters 7-14", Stdlib_SourceLoc.Pos.decode("bad"), undefined)); Mocha.test("ValuePath helpers expose segments and name", () => { let valuePath = "Demo.Nested.run"; - Test_utils.eq("File \"source_loc_test.res\", line 25, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), [ + Test_utils.eq("File \"source_loc_test.res\", line 82, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), [ "Demo", "Nested", "run" ]); - Test_utils.eq("File \"source_loc_test.res\", line 26, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), "run"); + Test_utils.eq("File \"source_loc_test.res\", line 83, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), "run"); + }); + Mocha.test("implicit source loc autofill works for a top-level let binding", () => expectCapture("File \"source_loc_test.res\", line 87, characters 18-25", topLevelBinding, "Source_loc_test.topLevelBinding")); + Mocha.test("implicit source loc autofill works for a top-level expression", () => { + let capture = topLevelExpressionCapture.contents; + if (capture !== undefined) { + return expectCapture("File \"source_loc_test.res\", line 92, characters 37-44", capture, "Source_loc_test"); + } else { + return Test_utils.ok("File \"source_loc_test.res\", line 93, characters 17-24", false); + } + }); + Mocha.test("implicit source loc autofill works for a nested module let binding", () => expectCapture("File \"source_loc_test.res\", line 98, characters 18-25", nestedBinding, "Source_loc_test.Nested.nestedBinding")); + Mocha.test("implicit source loc autofill works for a nested module expression", () => { + let capture = topLevelExpressionCapture$1.contents; + if (capture !== undefined) { + return expectCapture("File \"source_loc_test.res\", line 103, characters 37-44", capture, "Source_loc_test.Nested"); + } else { + return Test_utils.ok("File \"source_loc_test.res\", line 104, characters 17-24", false); + } }); }); +export { + capture, + topLevelBinding, + topLevelExpressionCapture, + storeTopLevelExpression, + Nested, + expectCapture, +} /* Not a pure module */ diff --git a/tests/tests/src/source_loc_test.res b/tests/tests/src/source_loc_test.res index dfe7b002a83..a8a92bb9048 100644 --- a/tests/tests/src/source_loc_test.res +++ b/tests/tests/src/source_loc_test.res @@ -1,9 +1,62 @@ +@@config({flags: ["-implicit-source-loc"]}) + open Mocha open Test_utils external unsafeSourceLocPos: string => sourceLocPos = "%identity" external unsafeSourceLocValuePath: string => sourceLocValuePath = "%identity" +type sourceLocCapture = (option, option) + +let capture = (~pos: option=?, ~valuePath: option=?) => ( + pos, + valuePath, +) + +let topLevelBinding = capture() + +let topLevelExpressionCapture = ref((None: option)) + +let storeTopLevelExpression = ( + ~pos: option=?, + ~valuePath: option=?, +) => { + topLevelExpressionCapture.contents = Some((pos, valuePath)) +} + +storeTopLevelExpression() + +module Nested = { + let nestedBinding = capture() + + let topLevelExpressionCapture = ref((None: option)) + + let storeTopLevelExpression = ( + ~pos: option=?, + ~valuePath: option=?, + ) => { + topLevelExpressionCapture.contents = Some((pos, valuePath)) + } + + storeTopLevelExpression() +} + +let expectCapture = (loc, (pos, valuePath): sourceLocCapture, expectedValuePath) => { + switch pos { + | Some(pos) => + switch SourceLoc.Pos.decode(pos) { + | Some({file}) => eq(loc, file, "source_loc_test.res") + | None => ok(loc, false) + } + | None => ok(loc, false) + } + + switch valuePath { + | Some(valuePath) => eq(loc, SourceLoc.ValuePath.toString(valuePath), expectedValuePath) + | None => ok(loc, false) + } +} + describe("SourceLoc", () => { test("Pos.decode parses sourceLocPos", () => { let decoded = SourceLoc.Pos.decode(unsafeSourceLocPos("demo.res;1;2;3;4")) @@ -29,4 +82,26 @@ describe("SourceLoc", () => { eq(__LOC__, SourceLoc.ValuePath.segments(valuePath), ["Demo", "Nested", "run"]) eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "run") }) + + test("implicit source loc autofill works for a top-level let binding", () => { + expectCapture(__LOC__, topLevelBinding, "Source_loc_test.topLevelBinding") + }) + + test("implicit source loc autofill works for a top-level expression", () => { + switch topLevelExpressionCapture.contents { + | Some(capture) => expectCapture(__LOC__, capture, "Source_loc_test") + | None => ok(__LOC__, false) + } + }) + + test("implicit source loc autofill works for a nested module let binding", () => { + expectCapture(__LOC__, Nested.nestedBinding, "Source_loc_test.Nested.nestedBinding") + }) + + test("implicit source loc autofill works for a nested module expression", () => { + switch Nested.topLevelExpressionCapture.contents { + | Some(capture) => expectCapture(__LOC__, capture, "Source_loc_test.Nested") + | None => ok(__LOC__, false) + } + }) }) From c6e15b88e119c5bc8c501204562eaef117235fda Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 13:51:32 +0100 Subject: [PATCH 06/10] update analysis test output --- .../tests/src/expected/CompletionFunctionArguments.res.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/analysis_tests/tests/src/expected/CompletionFunctionArguments.res.txt b/tests/analysis_tests/tests/src/expected/CompletionFunctionArguments.res.txt index 48c89535fbe..4afceb1ced9 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionFunctionArguments.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionFunctionArguments.res.txt @@ -232,8 +232,15 @@ Path someFnTakingVariant "tags": [], "detail": "someVariant", "documentation": {"kind": "markdown", "value": "```rescript\ntype someVariant = One | Two | Three(int, string)\n```"}, + "sortText": "A Some(_)", "insertText": "Some($0)", "insertTextFormat": 2 + }, { + "label": "SourceLoc", + "kind": 9, + "tags": [], + "detail": "module SourceLoc", + "documentation": null }] Complete src/CompletionFunctionArguments.res 60:44 From 90d070b98dbb3e01adaacdb9b5383531453bc4b9 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 17 Mar 2026 14:53:37 +0100 Subject: [PATCH 07/10] artifact list --- packages/artifacts.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/artifacts.json b/packages/artifacts.json index a024c5ebc4e..7cc6780b75e 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -182,6 +182,7 @@ "lib/es6/Stdlib_RegExp.mjs", "lib/es6/Stdlib_Result.mjs", "lib/es6/Stdlib_Set.mjs", + "lib/es6/Stdlib_SourceLoc.mjs", "lib/es6/Stdlib_String.mjs", "lib/es6/Stdlib_Symbol.mjs", "lib/es6/Stdlib_Type.mjs", @@ -356,6 +357,7 @@ "lib/js/Stdlib_RegExp.cjs", "lib/js/Stdlib_Result.cjs", "lib/js/Stdlib_Set.cjs", + "lib/js/Stdlib_SourceLoc.cjs", "lib/js/Stdlib_String.cjs", "lib/js/Stdlib_Symbol.cjs", "lib/js/Stdlib_Type.cjs", @@ -1192,6 +1194,10 @@ "lib/ocaml/Stdlib_Set.cmti", "lib/ocaml/Stdlib_Set.res", "lib/ocaml/Stdlib_Set.resi", + "lib/ocaml/Stdlib_SourceLoc.cmi", + "lib/ocaml/Stdlib_SourceLoc.cmj", + "lib/ocaml/Stdlib_SourceLoc.cmt", + "lib/ocaml/Stdlib_SourceLoc.res", "lib/ocaml/Stdlib_String.cmi", "lib/ocaml/Stdlib_String.cmj", "lib/ocaml/Stdlib_String.cmt", From 8f0ca7a3382dfa250894d33a23a30451354fc5e0 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 18 Mar 2026 14:29:21 +0100 Subject: [PATCH 08/10] bring back %autofill --- compiler/ml/typecore.ml | 78 ++++++++++++++++-- .../@rescript/runtime/Stdlib_SourceLoc.res | 5 +- .../runtime/lib/es6/Stdlib_SourceLoc.mjs | 12 ++- .../runtime/lib/js/Stdlib_SourceLoc.cjs | 12 ++- ...g_autofill_missing_annotation.res.expected | 8 ++ ...e_loc_arg_autofill_wrong_type.res.expected | 8 ++ ...urce_loc_arg_missing_autofill.res.expected | 8 ++ ..._loc_arg_non_autofill_default.res.expected | 8 ++ ...ce_loc_arg_autofill_missing_annotation.res | 1 + .../source_loc_arg_autofill_wrong_type.res | 1 + .../source_loc_arg_missing_autofill.res | 1 + .../source_loc_arg_non_autofill_default.res | 1 + .../tests/src/source_loc_no_autofill_test.mjs | 29 +++++++ .../tests/src/source_loc_no_autofill_test.res | 16 ++++ tests/tests/src/source_loc_test.mjs | 80 +++++++++---------- tests/tests/src/source_loc_test.res | 40 ++++++---- 16 files changed, 231 insertions(+), 77 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected create mode 100644 tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected create mode 100644 tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected create mode 100644 tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res create mode 100644 tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res create mode 100644 tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res create mode 100644 tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res create mode 100644 tests/tests/src/source_loc_no_autofill_test.mjs create mode 100644 tests/tests/src/source_loc_no_autofill_test.res diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 96dddaabc00..1c00b22a3b4 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2241,23 +2241,70 @@ let extract_function_name funct = type implicit_source_loc_kind = Source_loc_pos | Source_loc_value_path +let source_loc_kind env ty = + match (expand_head env ty).desc with + | Tconstr (path, [], _) when Path.same path Predef.path_source_loc_pos -> + Some Source_loc_pos + | Tconstr (path, [], _) when Path.same path Predef.path_source_loc_value_path + -> + Some Source_loc_value_path + | _ -> None + let optional_source_loc_kind env ty = match (expand_head env ty).desc with | Tconstr (option_path, [inner], _) - when Path.same option_path Predef.path_option -> ( - match (expand_head env inner).desc with - | Tconstr (path, [], _) when Path.same path Predef.path_source_loc_pos -> - Some Source_loc_pos - | Tconstr (path, [], _) - when Path.same path Predef.path_source_loc_value_path -> - Some Source_loc_value_path - | _ -> None) + when Path.same option_path Predef.path_option -> + source_loc_kind env inner | _ -> None type lazy_args = (Asttypes.arg_label * (unit -> Typedtree.expression) option) list type targs = (Asttypes.arg_label * Typedtree.expression option) list + +let source_loc_default_arg ~loc _kind = + Ast_helper.Exp.apply ~loc + (Ast_helper.Exp.ident ~loc + (Location.mknoloc (Longident.Ldot (Longident.Lident "Obj", "magic")))) + [(Nolabel, Ast_helper.Exp.constant ~loc (Pconst_string ("", None)))] + +let source_loc_kind_of_pattern_annotation env spat = + match spat.ppat_desc with + | Ppat_constraint (_, sty) -> + let cty = Typetexp.transl_simple_type env false sty in + source_loc_kind env cty.ctyp_type + | _ -> None + +let autofill_default_loc expr = + match expr.pexp_desc with + | Pexp_extension ({txt = "autofill"; loc}, PStr []) -> Some loc + | Pexp_extension ({txt = "autofill"; loc}, _) -> + raise + (Error_forward + (Location.errorf ~loc + "`%%autofill` does not take a payload. Use `%%autofill`.")) + | _ -> None + +let invalid_source_loc_optional_arg ~loc = + raise + (Error_forward + (Location.errorf ~loc + "Optional `sourceLocPos` and `sourceLocValuePath` args must use \ + `=%%autofill`. Remove the optional/default syntax to make this a \ + regular required arg.")) + +let invalid_autofill_arg ~loc = + raise + (Error_forward + (Location.errorf ~loc + "`%%autofill` can only be used on args explicitly annotated as \ + `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: \ + sourceLocPos=%%autofill`.")) + +(* We don't preserve `%autofill` in the function type. + Instead we reserve optional source-loc args for autofill and enforce that + `%autofill` is the only supported way to author them. A deeper integration + would represent autofill explicitly in the AST/type layer. *) let rec type_exp ?deprecated_context ~context ?recarg env sexp = (* We now delegate everything to type_expect *) type_expect ?deprecated_context ~context ?recarg env sexp (newvar ()) @@ -2422,6 +2469,16 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected) async; } -> assert (is_optional l); + let default = + match + ( source_loc_kind_of_pattern_annotation env spat, + autofill_default_loc default ) + with + | Some kind, Some _ -> source_loc_default_arg ~loc:default.pexp_loc kind + | Some _, None -> invalid_source_loc_optional_arg ~loc:default.pexp_loc + | None, Some loc -> invalid_autofill_arg ~loc + | None, None -> default + in (* default allowed only with optional argument *) let open Ast_helper in let default_loc = default.pexp_loc in @@ -2463,6 +2520,11 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected) [Exp.case pat body] | Pexp_fun {arg_label = l; default = None; lhs = spat; rhs = sbody; arity; async} -> + let () = + match (is_optional l, source_loc_kind_of_pattern_annotation env spat) with + | true, Some _ -> invalid_source_loc_optional_arg ~loc:spat.ppat_loc + | _ -> () + in type_function ?in_function ~arity ~async loc sexp.pexp_attributes env ty_expected l [Ast_helper.Exp.case spat sbody] diff --git a/packages/@rescript/runtime/Stdlib_SourceLoc.res b/packages/@rescript/runtime/Stdlib_SourceLoc.res index 75d30583ab4..eb20048925a 100644 --- a/packages/@rescript/runtime/Stdlib_SourceLoc.res +++ b/packages/@rescript/runtime/Stdlib_SourceLoc.res @@ -34,7 +34,10 @@ module ValuePath = { external toString: t => string = "%identity" - let segments = (value: t) => value->toString->Stdlib_String.split(".") + let segments = (value: t) => { + let value = value->toString + value === "" ? [] : value->Stdlib_String.split(".") + } let name = (value: t) => { let segments = value->segments diff --git a/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs b/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs index ac49ec0fa1d..7aeebb546b8 100644 --- a/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs +++ b/packages/@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs @@ -32,16 +32,20 @@ let Pos = { }; function segments(value) { - return value.split("."); + if (value === "") { + return []; + } else { + return value.split("."); + } } function name(value) { - let segments = value.split("."); - let length = segments.length; + let segments$1 = segments(value); + let length = segments$1.length; if (length === 0) { return ""; } else { - return segments[length - 1 | 0]; + return segments$1[length - 1 | 0]; } } diff --git a/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs b/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs index 6f8871a11e6..8a45fbc2152 100644 --- a/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs +++ b/packages/@rescript/runtime/lib/js/Stdlib_SourceLoc.cjs @@ -32,16 +32,20 @@ let Pos = { }; function segments(value) { - return value.split("."); + if (value === "") { + return []; + } else { + return value.split("."); + } } function name(value) { - let segments = value.split("."); - let length = segments.length; + let segments$1 = segments(value); + let length = segments$1.length; if (length === 0) { return ""; } else { - return segments[length - 1 | 0]; + return segments$1[length - 1 | 0]; } } diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected new file mode 100644 index 00000000000..def7e50f3d8 --- /dev/null +++ b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/source_loc_arg_autofill_missing_annotation.res:1:17-25 + + 1 │ let log = (~pos=%autofill, ()) => pos + 2 │ + + `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected new file mode 100644 index 00000000000..1cca2026573 --- /dev/null +++ b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/source_loc_arg_autofill_wrong_type.res:1:25-33 + + 1 │ let log = (~pos: string=%autofill, ()) => pos + 2 │ + + `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected new file mode 100644 index 00000000000..2ebde53f5e9 --- /dev/null +++ b/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/source_loc_arg_missing_autofill.res:1:12-29 + + 1 │ let log = (~pos: sourceLocPos=?, ()) => pos + 2 │ + + Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected new file mode 100644 index 00000000000..baf60f9d754 --- /dev/null +++ b/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/source_loc_arg_non_autofill_default.res:1:31-48 + + 1 │ let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__, ()) => pos + 2 │ + + Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res new file mode 100644 index 00000000000..3b882c1ebda --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res @@ -0,0 +1 @@ +let log = (~pos=%autofill, ()) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res new file mode 100644 index 00000000000..bc7ebe6fc78 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res @@ -0,0 +1 @@ +let log = (~pos: string=%autofill, ()) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res new file mode 100644 index 00000000000..211c6ed26be --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res @@ -0,0 +1 @@ +let log = (~pos: sourceLocPos=?, ()) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res new file mode 100644 index 00000000000..2539ace61d4 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res @@ -0,0 +1 @@ +let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__, ()) => pos diff --git a/tests/tests/src/source_loc_no_autofill_test.mjs b/tests/tests/src/source_loc_no_autofill_test.mjs new file mode 100644 index 00000000000..7f94879dc1e --- /dev/null +++ b/tests/tests/src/source_loc_no_autofill_test.mjs @@ -0,0 +1,29 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Stdlib_SourceLoc from "@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs"; + +function capture(posOpt, valuePathOpt) { + let pos = posOpt !== undefined ? posOpt : ""; + let valuePath = valuePathOpt !== undefined ? valuePathOpt : ""; + return [ + pos, + valuePath + ]; +} + +Mocha.describe("SourceLoc without -implicit-source-loc", () => { + Mocha.test("missing args fall back to empty source loc values", () => { + let match = capture(undefined, undefined); + let valuePath = match[1]; + Test_utils.eq("File \"source_loc_no_autofill_test.res\", line 12, characters 7-14", Stdlib_SourceLoc.Pos.decode(match[0]), undefined); + Test_utils.eq("File \"source_loc_no_autofill_test.res\", line 13, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), []); + Test_utils.eq("File \"source_loc_no_autofill_test.res\", line 14, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), ""); + }); +}); + +export { + capture, +} +/* Not a pure module */ diff --git a/tests/tests/src/source_loc_no_autofill_test.res b/tests/tests/src/source_loc_no_autofill_test.res new file mode 100644 index 00000000000..b01271d32be --- /dev/null +++ b/tests/tests/src/source_loc_no_autofill_test.res @@ -0,0 +1,16 @@ +open Mocha +open Test_utils + +let capture = (~pos: sourceLocPos=%autofill, ~valuePath: sourceLocValuePath=%autofill) => ( + pos, + valuePath, +) + +describe("SourceLoc without -implicit-source-loc", () => { + test("missing args fall back to empty source loc values", () => { + let (pos, valuePath) = capture() + eq(__LOC__, SourceLoc.Pos.decode(pos), None) + eq(__LOC__, SourceLoc.ValuePath.segments(valuePath), []) + eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "") + }) +}) diff --git a/tests/tests/src/source_loc_test.mjs b/tests/tests/src/source_loc_test.mjs index 08f0be6fb7a..32cd6fca4cf 100644 --- a/tests/tests/src/source_loc_test.mjs +++ b/tests/tests/src/source_loc_test.mjs @@ -4,27 +4,24 @@ import * as Mocha from "mocha"; import * as Test_utils from "./test_utils.mjs"; import * as Stdlib_SourceLoc from "@rescript/runtime/lib/es6/Stdlib_SourceLoc.mjs"; -function capture(pos, valuePath) { +function capture(posOpt, valuePathOpt) { + let pos = posOpt !== undefined ? posOpt : ""; + let valuePath = valuePathOpt !== undefined ? valuePathOpt : ""; return [ pos, valuePath ]; } -let topLevelBinding_0 = "source_loc_test.res;16;22;16;31"; - -let topLevelBinding_1 = "Source_loc_test.topLevelBinding"; - -let topLevelBinding = [ - topLevelBinding_0, - topLevelBinding_1 -]; +let topLevelBinding = capture("source_loc_test.res;16;22;16;31", "Source_loc_test.topLevelBinding"); let topLevelExpressionCapture = { contents: undefined }; -function storeTopLevelExpression(pos, valuePath) { +function storeTopLevelExpression(posOpt, valuePathOpt) { + let pos = posOpt !== undefined ? posOpt : ""; + let valuePath = valuePathOpt !== undefined ? valuePathOpt : ""; topLevelExpressionCapture.contents = [ pos, valuePath @@ -33,20 +30,15 @@ function storeTopLevelExpression(pos, valuePath) { storeTopLevelExpression("source_loc_test.res;27;0;27;25", "Source_loc_test"); -let nestedBinding_0 = "source_loc_test.res;30;22;30;31"; - -let nestedBinding_1 = "Source_loc_test.Nested.nestedBinding"; - -let nestedBinding = [ - nestedBinding_0, - nestedBinding_1 -]; +let nestedBinding = capture("source_loc_test.res;30;22;30;31", "Source_loc_test.Nested.nestedBinding"); let topLevelExpressionCapture$1 = { contents: undefined }; -function storeTopLevelExpression$1(pos, valuePath) { +function storeTopLevelExpression$1(posOpt, valuePathOpt) { + let pos = posOpt !== undefined ? posOpt : ""; + let valuePath = valuePathOpt !== undefined ? valuePathOpt : ""; topLevelExpressionCapture$1.contents = [ pos, valuePath @@ -62,29 +54,19 @@ let Nested = { }; function expectCapture(loc, param, expectedValuePath) { - let valuePath = param[1]; - let pos = param[0]; - if (pos !== undefined) { - let match = Stdlib_SourceLoc.Pos.decode(pos); - if (match !== undefined) { - Test_utils.eq(loc, match.file, "source_loc_test.res"); - } else { - Test_utils.ok(loc, false); - } + let match = Stdlib_SourceLoc.Pos.decode(param[0]); + if (match !== undefined) { + Test_utils.eq(loc, match.file, "source_loc_test.res"); } else { Test_utils.ok(loc, false); } - if (valuePath !== undefined) { - return Test_utils.eq(loc, valuePath, expectedValuePath); - } else { - return Test_utils.ok(loc, false); - } + Test_utils.eq(loc, param[1], expectedValuePath); } Mocha.describe("SourceLoc", () => { Mocha.test("Pos.decode parses sourceLocPos", () => { let decoded = Stdlib_SourceLoc.Pos.decode("demo.res;1;2;3;4"); - Test_utils.eq("File \"source_loc_test.res\", line 64, characters 6-13", decoded, { + Test_utils.eq("File \"source_loc_test.res\", line 57, characters 6-13", decoded, { file: "demo.res", startLine: 1, startCol: 2, @@ -92,34 +74,44 @@ Mocha.describe("SourceLoc", () => { endCol: 4 }); }); - Mocha.test("Pos.decode rejects malformed sourceLocPos", () => Test_utils.eq("File \"source_loc_test.res\", line 77, characters 7-14", Stdlib_SourceLoc.Pos.decode("bad"), undefined)); + Mocha.test("Pos.decode rejects malformed sourceLocPos", () => Test_utils.eq("File \"source_loc_test.res\", line 70, characters 7-14", Stdlib_SourceLoc.Pos.decode("bad"), undefined)); Mocha.test("ValuePath helpers expose segments and name", () => { let valuePath = "Demo.Nested.run"; - Test_utils.eq("File \"source_loc_test.res\", line 82, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), [ + Test_utils.eq("File \"source_loc_test.res\", line 75, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), [ "Demo", "Nested", "run" ]); - Test_utils.eq("File \"source_loc_test.res\", line 83, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), "run"); + Test_utils.eq("File \"source_loc_test.res\", line 76, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), "run"); }); - Mocha.test("implicit source loc autofill works for a top-level let binding", () => expectCapture("File \"source_loc_test.res\", line 87, characters 18-25", topLevelBinding, "Source_loc_test.topLevelBinding")); + Mocha.test("ValuePath helpers treat empty strings as missing", () => { + let valuePath = ""; + Test_utils.eq("File \"source_loc_test.res\", line 81, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), []); + Test_utils.eq("File \"source_loc_test.res\", line 82, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), ""); + }); + Mocha.test("implicit source loc autofill works for a top-level let binding", () => expectCapture("File \"source_loc_test.res\", line 86, characters 18-25", topLevelBinding, "Source_loc_test.topLevelBinding")); Mocha.test("implicit source loc autofill works for a top-level expression", () => { let capture = topLevelExpressionCapture.contents; if (capture !== undefined) { - return expectCapture("File \"source_loc_test.res\", line 92, characters 37-44", capture, "Source_loc_test"); + return expectCapture("File \"source_loc_test.res\", line 91, characters 37-44", capture, "Source_loc_test"); } else { - return Test_utils.ok("File \"source_loc_test.res\", line 93, characters 17-24", false); + return Test_utils.ok("File \"source_loc_test.res\", line 92, characters 17-24", false); } }); - Mocha.test("implicit source loc autofill works for a nested module let binding", () => expectCapture("File \"source_loc_test.res\", line 98, characters 18-25", nestedBinding, "Source_loc_test.Nested.nestedBinding")); + Mocha.test("implicit source loc autofill works for a nested module let binding", () => expectCapture("File \"source_loc_test.res\", line 97, characters 18-25", nestedBinding, "Source_loc_test.Nested.nestedBinding")); Mocha.test("implicit source loc autofill works for a nested module expression", () => { let capture = topLevelExpressionCapture$1.contents; if (capture !== undefined) { - return expectCapture("File \"source_loc_test.res\", line 103, characters 37-44", capture, "Source_loc_test.Nested"); + return expectCapture("File \"source_loc_test.res\", line 102, characters 37-44", capture, "Source_loc_test.Nested"); } else { - return Test_utils.ok("File \"source_loc_test.res\", line 104, characters 17-24", false); + return Test_utils.ok("File \"source_loc_test.res\", line 103, characters 17-24", false); } }); + Mocha.test("explicit source loc args override autofill", () => { + let match = capture("", ""); + Test_utils.eq("File \"source_loc_test.res\", line 112, characters 7-14", Stdlib_SourceLoc.Pos.decode(match[0]), undefined); + Test_utils.eq("File \"source_loc_test.res\", line 113, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(match[1]), []); + }); }); export { @@ -130,4 +122,4 @@ export { Nested, expectCapture, } -/* Not a pure module */ +/* topLevelBinding Not a pure module */ diff --git a/tests/tests/src/source_loc_test.res b/tests/tests/src/source_loc_test.res index a8a92bb9048..9bc0c84f701 100644 --- a/tests/tests/src/source_loc_test.res +++ b/tests/tests/src/source_loc_test.res @@ -6,9 +6,9 @@ open Test_utils external unsafeSourceLocPos: string => sourceLocPos = "%identity" external unsafeSourceLocValuePath: string => sourceLocValuePath = "%identity" -type sourceLocCapture = (option, option) +type sourceLocCapture = (sourceLocPos, sourceLocValuePath) -let capture = (~pos: option=?, ~valuePath: option=?) => ( +let capture = (~pos: sourceLocPos=%autofill, ~valuePath: sourceLocValuePath=%autofill) => ( pos, valuePath, ) @@ -18,8 +18,8 @@ let topLevelBinding = capture() let topLevelExpressionCapture = ref((None: option)) let storeTopLevelExpression = ( - ~pos: option=?, - ~valuePath: option=?, + ~pos: sourceLocPos=%autofill, + ~valuePath: sourceLocValuePath=%autofill, ) => { topLevelExpressionCapture.contents = Some((pos, valuePath)) } @@ -32,8 +32,8 @@ module Nested = { let topLevelExpressionCapture = ref((None: option)) let storeTopLevelExpression = ( - ~pos: option=?, - ~valuePath: option=?, + ~pos: sourceLocPos=%autofill, + ~valuePath: sourceLocValuePath=%autofill, ) => { topLevelExpressionCapture.contents = Some((pos, valuePath)) } @@ -42,19 +42,12 @@ module Nested = { } let expectCapture = (loc, (pos, valuePath): sourceLocCapture, expectedValuePath) => { - switch pos { - | Some(pos) => - switch SourceLoc.Pos.decode(pos) { - | Some({file}) => eq(loc, file, "source_loc_test.res") - | None => ok(loc, false) - } + switch SourceLoc.Pos.decode(pos) { + | Some({file}) => eq(loc, file, "source_loc_test.res") | None => ok(loc, false) } - switch valuePath { - | Some(valuePath) => eq(loc, SourceLoc.ValuePath.toString(valuePath), expectedValuePath) - | None => ok(loc, false) - } + eq(loc, SourceLoc.ValuePath.toString(valuePath), expectedValuePath) } describe("SourceLoc", () => { @@ -83,6 +76,12 @@ describe("SourceLoc", () => { eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "run") }) + test("ValuePath helpers treat empty strings as missing", () => { + let valuePath = unsafeSourceLocValuePath("") + eq(__LOC__, SourceLoc.ValuePath.segments(valuePath), []) + eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "") + }) + test("implicit source loc autofill works for a top-level let binding", () => { expectCapture(__LOC__, topLevelBinding, "Source_loc_test.topLevelBinding") }) @@ -104,4 +103,13 @@ describe("SourceLoc", () => { | None => ok(__LOC__, false) } }) + + test("explicit source loc args override autofill", () => { + let (pos, valuePath) = capture( + ~pos=unsafeSourceLocPos(""), + ~valuePath=unsafeSourceLocValuePath(""), + ) + eq(__LOC__, SourceLoc.Pos.decode(pos), None) + eq(__LOC__, SourceLoc.ValuePath.segments(valuePath), []) + }) }) From f62dc9826e9240579ff720c12fb91d30cf452006 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 19 Mar 2026 10:11:35 +0100 Subject: [PATCH 09/10] rename arg, and clean up --- CHANGELOG.md | 2 +- compiler/bsc/rescript_compiler_main.ml | 7 +++---- compiler/ml/clflags.ml | 2 +- compiler/ml/clflags.mli | 2 +- compiler/ml/typecore.ml | 8 ++++---- packages/artifacts.json | 2 ++ ...ce_loc_arg_autofill_missing_annotation.res.expected | 4 ++-- .../source_loc_arg_autofill_wrong_type.res.expected | 4 ++-- .../source_loc_arg_missing_autofill.res.expected | 4 ++-- .../source_loc_arg_non_autofill_default.res.expected | 4 ++-- .../source_loc_arg_autofill_missing_annotation.res | 2 +- .../fixtures/source_loc_arg_autofill_wrong_type.res | 2 +- .../fixtures/source_loc_arg_missing_autofill.res | 2 +- .../fixtures/source_loc_arg_non_autofill_default.res | 2 +- tests/tests/src/source_loc_no_autofill_test.mjs | 2 +- tests/tests/src/source_loc_no_autofill_test.res | 2 +- tests/tests/src/source_loc_test.mjs | 8 ++++---- tests/tests/src/source_loc_test.res | 10 +++++----- 18 files changed, 35 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b86dc7166..44c3b61f1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Reanalyze: add glob pattern support for suppress/unsuppress configurations (e.g., `"src/generated/**"`). https://github.com/rescript-lang/rescript/pull/8277 - Add optional `~locales` and `~options` parameters to `String.localeCompare`. https://github.com/rescript-lang/rescript/pull/8287 -- Add new `sourceLocPos` and `sourceLocValuePath` magic constants, and allow them to be autoinjected from the call site into function arguments via `-implicit-source-loc`. https://github.com/rescript-lang/rescript/pull/8303 +- Add new `sourceLocPos` and `sourceLocValuePath` magic constants, and allow them to be autoinjected from the call site into function arguments via `-allow-autofill-source-loc`. https://github.com/rescript-lang/rescript/pull/8303 #### :bug: Bug fix diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index b030546352f..540cd299293 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -336,10 +336,9 @@ let command_line_flags : (string * Bsc_args.spec * string) array = ( "-bs-noassertfalse", set Clflags.no_assert_false, "*internal* no code for assert false" ); - ( "-implicit-source-loc", - set Clflags.implicit_source_loc, - "*internal* Enable implicit source-loc autofill for optional source-loc \ - args" ); + ( "-allow-autofill-source-loc", + set Clflags.allow_autofill_source_loc, + "*internal* Allow source-loc autofill for optional source-loc args" ); ( "-noassert", set Clflags.noassert, "*internal* Do not compile assertion checks" ); diff --git a/compiler/ml/clflags.ml b/compiler/ml/clflags.ml index 12c05239246..385920e9a4b 100644 --- a/compiler/ml/clflags.ml +++ b/compiler/ml/clflags.ml @@ -46,7 +46,7 @@ and ignore_parse_errors = ref false (* -ignore-parse-errors *) let dont_write_files = ref false (* set to true under ocamldoc *) -let implicit_source_loc = ref false +let allow_autofill_source_loc = ref false let reset_dump_state () = dump_source := false; diff --git a/compiler/ml/clflags.mli b/compiler/ml/clflags.mli index 528352affb7..df374dbe824 100644 --- a/compiler/ml/clflags.mli +++ b/compiler/ml/clflags.mli @@ -22,7 +22,7 @@ val dump_typedtree : bool ref val dump_rawlambda : bool ref val dump_lambda : bool ref val dont_write_files : bool ref -val implicit_source_loc : bool ref +val allow_autofill_source_loc : bool ref val keep_locs : bool ref val only_parse : bool ref val ignore_parse_errors : bool ref diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 1c00b22a3b4..4c4a41f813a 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2239,7 +2239,7 @@ let extract_function_name funct = | Texp_ident (path, _, _) -> Some (Longident.parse (Path.name path)) | _ -> None -type implicit_source_loc_kind = Source_loc_pos | Source_loc_value_path +type autofill_source_loc_kind = Source_loc_pos | Source_loc_value_path let source_loc_kind env ty = match (expand_head env ty).desc with @@ -2309,7 +2309,7 @@ let rec type_exp ?deprecated_context ~context ?recarg env sexp = (* We now delegate everything to type_expect *) type_expect ?deprecated_context ~context ?recarg env sexp (newvar ()) -and implicit_source_loc_arg ~apply_loc env ty kind = +and autofill_source_loc_arg ~apply_loc env ty kind = let loc = {apply_loc with Location.loc_ghost = true} in let expr = Ast_helper.Exp.ident ~loc @@ -2323,8 +2323,8 @@ and implicit_source_loc_arg ~apply_loc env ty kind = and missing_optional_arg ~apply_loc env ty = match optional_source_loc_kind env ty with - | Some kind when !Clflags.implicit_source_loc -> - fun () -> implicit_source_loc_arg ~apply_loc env ty kind + | Some kind when !Clflags.allow_autofill_source_loc -> + fun () -> autofill_source_loc_arg ~apply_loc env ty kind | _ -> fun () -> option_none (instance env ty) Location.none (* Typing of an expression with an expected type. diff --git a/packages/artifacts.json b/packages/artifacts.json index 7cc6780b75e..19aa6eec0ef 100644 --- a/packages/artifacts.json +++ b/packages/artifacts.json @@ -7,6 +7,7 @@ "LICENSE", "LICENSE.MIT", "README.md", + "cli/bsc.exe", "cli/bsc.js", "cli/common/args.js", "cli/common/bins.js", @@ -14,6 +15,7 @@ "cli/common/runtime.js", "cli/rescript-tools.js", "cli/rescript.js", + "cli/rescript_compiler_main.exe", "docs/docson/build-schema.json", "package.json" ], diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected index def7e50f3d8..b096ab95a39 100644 --- a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected +++ b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_missing_annotation.res.expected @@ -2,7 +2,7 @@ We've found a bug for you! /.../fixtures/source_loc_arg_autofill_missing_annotation.res:1:17-25 - 1 │ let log = (~pos=%autofill, ()) => pos + 1 │ let log = (~pos=%autofill) => pos 2 │ - `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. \ No newline at end of file + `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected index 1cca2026573..2ee41a67361 100644 --- a/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected +++ b/tests/build_tests/super_errors/expected/source_loc_arg_autofill_wrong_type.res.expected @@ -2,7 +2,7 @@ We've found a bug for you! /.../fixtures/source_loc_arg_autofill_wrong_type.res:1:25-33 - 1 │ let log = (~pos: string=%autofill, ()) => pos + 1 │ let log = (~pos: string=%autofill) => pos 2 │ - `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. \ No newline at end of file + `%autofill` can only be used on args explicitly annotated as `sourceLocPos` or `sourceLocValuePath`. Example: `~pos: sourceLocPos=%autofill`. diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected index 2ebde53f5e9..1840cb2b6d9 100644 --- a/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected +++ b/tests/build_tests/super_errors/expected/source_loc_arg_missing_autofill.res.expected @@ -2,7 +2,7 @@ We've found a bug for you! /.../fixtures/source_loc_arg_missing_autofill.res:1:12-29 - 1 │ let log = (~pos: sourceLocPos=?, ()) => pos + 1 │ let log = (~pos: sourceLocPos=?) => pos 2 │ - Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. \ No newline at end of file + Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. diff --git a/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected b/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected index baf60f9d754..2d4b0e9e11f 100644 --- a/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected +++ b/tests/build_tests/super_errors/expected/source_loc_arg_non_autofill_default.res.expected @@ -2,7 +2,7 @@ We've found a bug for you! /.../fixtures/source_loc_arg_non_autofill_default.res:1:31-48 - 1 │ let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__, ()) => pos + 1 │ let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__) => pos 2 │ - Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. \ No newline at end of file + Optional `sourceLocPos` and `sourceLocValuePath` args must use `=%autofill`. Remove the optional/default syntax to make this a regular required arg. diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res index 3b882c1ebda..28a76190500 100644 --- a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_missing_annotation.res @@ -1 +1 @@ -let log = (~pos=%autofill, ()) => pos +let log = (~pos=%autofill) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res index bc7ebe6fc78..6385a4ce9a6 100644 --- a/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_autofill_wrong_type.res @@ -1 +1 @@ -let log = (~pos: string=%autofill, ()) => pos +let log = (~pos: string=%autofill) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res index 211c6ed26be..750ae425813 100644 --- a/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_missing_autofill.res @@ -1 +1 @@ -let log = (~pos: sourceLocPos=?, ()) => pos +let log = (~pos: sourceLocPos=?) => pos diff --git a/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res b/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res index 2539ace61d4..7078893e72e 100644 --- a/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res +++ b/tests/build_tests/super_errors/fixtures/source_loc_arg_non_autofill_default.res @@ -1 +1 @@ -let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__, ()) => pos +let log = (~pos: sourceLocPos=__SOURCE_LOC_POS__) => pos diff --git a/tests/tests/src/source_loc_no_autofill_test.mjs b/tests/tests/src/source_loc_no_autofill_test.mjs index 7f94879dc1e..5e7a03dde26 100644 --- a/tests/tests/src/source_loc_no_autofill_test.mjs +++ b/tests/tests/src/source_loc_no_autofill_test.mjs @@ -13,7 +13,7 @@ function capture(posOpt, valuePathOpt) { ]; } -Mocha.describe("SourceLoc without -implicit-source-loc", () => { +Mocha.describe("SourceLoc without -allow-autofill-source-loc", () => { Mocha.test("missing args fall back to empty source loc values", () => { let match = capture(undefined, undefined); let valuePath = match[1]; diff --git a/tests/tests/src/source_loc_no_autofill_test.res b/tests/tests/src/source_loc_no_autofill_test.res index b01271d32be..dc93c245df0 100644 --- a/tests/tests/src/source_loc_no_autofill_test.res +++ b/tests/tests/src/source_loc_no_autofill_test.res @@ -6,7 +6,7 @@ let capture = (~pos: sourceLocPos=%autofill, ~valuePath: sourceLocValuePath=%aut valuePath, ) -describe("SourceLoc without -implicit-source-loc", () => { +describe("SourceLoc without -allow-autofill-source-loc", () => { test("missing args fall back to empty source loc values", () => { let (pos, valuePath) = capture() eq(__LOC__, SourceLoc.Pos.decode(pos), None) diff --git a/tests/tests/src/source_loc_test.mjs b/tests/tests/src/source_loc_test.mjs index 32cd6fca4cf..629726ea51a 100644 --- a/tests/tests/src/source_loc_test.mjs +++ b/tests/tests/src/source_loc_test.mjs @@ -89,8 +89,8 @@ Mocha.describe("SourceLoc", () => { Test_utils.eq("File \"source_loc_test.res\", line 81, characters 7-14", Stdlib_SourceLoc.ValuePath.segments(valuePath), []); Test_utils.eq("File \"source_loc_test.res\", line 82, characters 7-14", Stdlib_SourceLoc.ValuePath.name(valuePath), ""); }); - Mocha.test("implicit source loc autofill works for a top-level let binding", () => expectCapture("File \"source_loc_test.res\", line 86, characters 18-25", topLevelBinding, "Source_loc_test.topLevelBinding")); - Mocha.test("implicit source loc autofill works for a top-level expression", () => { + Mocha.test("source loc autofill works for a top-level let binding", () => expectCapture("File \"source_loc_test.res\", line 86, characters 18-25", topLevelBinding, "Source_loc_test.topLevelBinding")); + Mocha.test("source loc autofill works for a top-level expression", () => { let capture = topLevelExpressionCapture.contents; if (capture !== undefined) { return expectCapture("File \"source_loc_test.res\", line 91, characters 37-44", capture, "Source_loc_test"); @@ -98,8 +98,8 @@ Mocha.describe("SourceLoc", () => { return Test_utils.ok("File \"source_loc_test.res\", line 92, characters 17-24", false); } }); - Mocha.test("implicit source loc autofill works for a nested module let binding", () => expectCapture("File \"source_loc_test.res\", line 97, characters 18-25", nestedBinding, "Source_loc_test.Nested.nestedBinding")); - Mocha.test("implicit source loc autofill works for a nested module expression", () => { + Mocha.test("source loc autofill works for a nested module let binding", () => expectCapture("File \"source_loc_test.res\", line 97, characters 18-25", nestedBinding, "Source_loc_test.Nested.nestedBinding")); + Mocha.test("source loc autofill works for a nested module expression", () => { let capture = topLevelExpressionCapture$1.contents; if (capture !== undefined) { return expectCapture("File \"source_loc_test.res\", line 102, characters 37-44", capture, "Source_loc_test.Nested"); diff --git a/tests/tests/src/source_loc_test.res b/tests/tests/src/source_loc_test.res index 9bc0c84f701..9de6587a43a 100644 --- a/tests/tests/src/source_loc_test.res +++ b/tests/tests/src/source_loc_test.res @@ -1,4 +1,4 @@ -@@config({flags: ["-implicit-source-loc"]}) +@@config({flags: ["-allow-autofill-source-loc"]}) open Mocha open Test_utils @@ -82,22 +82,22 @@ describe("SourceLoc", () => { eq(__LOC__, SourceLoc.ValuePath.name(valuePath), "") }) - test("implicit source loc autofill works for a top-level let binding", () => { + test("source loc autofill works for a top-level let binding", () => { expectCapture(__LOC__, topLevelBinding, "Source_loc_test.topLevelBinding") }) - test("implicit source loc autofill works for a top-level expression", () => { + test("source loc autofill works for a top-level expression", () => { switch topLevelExpressionCapture.contents { | Some(capture) => expectCapture(__LOC__, capture, "Source_loc_test") | None => ok(__LOC__, false) } }) - test("implicit source loc autofill works for a nested module let binding", () => { + test("source loc autofill works for a nested module let binding", () => { expectCapture(__LOC__, Nested.nestedBinding, "Source_loc_test.Nested.nestedBinding") }) - test("implicit source loc autofill works for a nested module expression", () => { + test("source loc autofill works for a nested module expression", () => { switch Nested.topLevelExpressionCapture.contents { | Some(capture) => expectCapture(__LOC__, capture, "Source_loc_test.Nested") | None => ok(__LOC__, false) From c0390d4a77cac4362b64d489cf1696975ac6c125 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 19 Mar 2026 10:21:15 +0100 Subject: [PATCH 10/10] add a few comments --- compiler/ml/typecore.ml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 4c4a41f813a..2d4316819f2 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2262,6 +2262,8 @@ type lazy_args = type targs = (Asttypes.arg_label * Typedtree.expression option) list +(* This Obj.magic models `%autofill` as an empty string (which represents "missing"). + The real source-loc payload is synthesized later at each call site. *) let source_loc_default_arg ~loc _kind = Ast_helper.Exp.apply ~loc (Ast_helper.Exp.ident ~loc @@ -2309,6 +2311,8 @@ let rec type_exp ?deprecated_context ~context ?recarg env sexp = (* We now delegate everything to type_expect *) type_expect ?deprecated_context ~context ?recarg env sexp (newvar ()) +(* For the `%autofill` case (or rather, the `~whatever: sourceLocPos=%autofill` case), + inject the special loc primitive at the application site. *) and autofill_source_loc_arg ~apply_loc env ty kind = let loc = {apply_loc with Location.loc_ghost = true} in let expr = @@ -2469,6 +2473,7 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected) async; } -> assert (is_optional l); + (* Handle `%autofill` injection if needed. *) let default = match ( source_loc_kind_of_pattern_annotation env spat,