Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d824583
Add builtin taggedTemplate type and Stdlib.TaggedTemplate module
JonoPrest Jun 2, 2026
ecedde0
Type-check tagged-template backtick syntax against taggedTemplate
JonoPrest Jun 2, 2026
cf87527
Emit real JS tagged templates for any taggedTemplate value
JonoPrest Jun 2, 2026
53e3cce
Remove @taggedTemplate decorator and its dead FFI path
JonoPrest Jun 2, 2026
65464aa
Support taggedTemplate in editor analysis and gentype
JonoPrest Jun 2, 2026
7065d27
Add tests and docs for first-class taggedTemplate
JonoPrest Jun 2, 2026
1443b54
Expand tagged-template runtime test coverage
JonoPrest Jun 4, 2026
07a467d
Add tagged-template error fixtures (type and syntax)
JonoPrest Jun 4, 2026
2074256
Reword tagged-template test comments to describe the tests
JonoPrest Jun 4, 2026
e98ba67
Test interpolation error distinctly from unclosed backtick
JonoPrest Jun 4, 2026
89c535e
Tagged-template-specific error messages for misuse
JonoPrest Jun 4, 2026
832073e
Execute a bare-package-import tag at runtime
JonoPrest Jun 4, 2026
91ac2c7
Update analysis completion snapshots for Stdlib.TaggedTemplate
JonoPrest Jun 4, 2026
58c7c95
Add gentype coverage for the taggedTemplate type mapping
JonoPrest Jun 4, 2026
1b92724
Emit a real tag function type in gentype instead of unknown
JonoPrest Jun 4, 2026
023928f
Link CHANGELOG entries to PR #8461
JonoPrest Jun 4, 2026
0862564
Migrate vendored rescript-bun tags to taggedTemplate type
JonoPrest Jun 4, 2026
12db98c
Drop unused predef type_tagged_template helper
JonoPrest Jun 4, 2026
111043d
Assert the tagged-template values-array invariant in typecore
JonoPrest Jun 4, 2026
6b85aa7
Drop dead error helper and exclude diagnostic tagged-template arms fr…
JonoPrest Jun 5, 2026
9c03754
Keep tagged-template purity arm in its natural group, not excluded
JonoPrest Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

- Make `Jsx.component` abstract. https://github.com/rescript-lang/rescript/pull/8390
- Drop Node.js version 20.x support, as it is reaching EOL. https://github.com/rescript-lang/rescript/pull/8401
- Remove the `@taggedTemplate` decorator in favor of the new first-class `taggedTemplate<'param, 'output>` builtin type. Using the decorator, or backtick tagged-template syntax on a value that is not a `taggedTemplate`, is now a compile error pointing to the new binding form. https://github.com/rescript-lang/rescript/pull/8461

#### :eyeglasses: Spec Compliance

#### :rocket: New Feature

- Add a first-class `taggedTemplate<'param, 'output>` builtin type and the `TaggedTemplate` stdlib module (`TaggedTemplate.make`). Tagged-template tags are now tracked through the type system, so they emit real JS tagged-template syntax across module boundaries, when passed as first-class values, and when constructed at runtime by a factory (e.g. `postgres`). https://github.com/rescript-lang/rescript/pull/8461

#### :bug: Bug fix

- Fix directive `@warning("-102")` not working. https://github.com/rescript-lang/rescript/pull/8322
Expand Down
14 changes: 13 additions & 1 deletion analysis/src/completion_back_end.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1123,7 +1123,19 @@ and get_completions_for_context_path ~debug ~full ~opens ~raw_opens ~pos ~env
let args = process_apply args labels in
let ret_type = reconstruct_function_type args t_ret in
[Completion.create "dummy" ~env ~kind:(Completion.Value ret_type)]
| _ -> [])
| _ -> (
(* A tagged template tag has type taggedTemplate<'param, 'output>;
applying it (via backtick syntax) yields the 'output type. *)
let rec unwrap (t : Types.type_expr) =
match t.desc with
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> unwrap t1
| _ -> t
in
match (unwrap typ).desc with
| Tconstr (path, [_param; output_type], _)
when Path.name path = "taggedTemplate" ->
[Completion.create "dummy" ~env ~kind:(Completion.Value output_type)]
| _ -> []))
| _ -> [])
| CPField
{context_path = CPId {path; completion_context = Module}; field_name} ->
Expand Down
6 changes: 4 additions & 2 deletions compiler/core/lam_analysis.ml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ let rec no_side_effects (lam : Lam.t) : bool =
{code_info = Exp (Js_function _ | Js_literal _) | Stmt Js_stmt_comment}
->
true
| Pjs_apply | Pjs_runtime_apply | Pjs_call _ | Pinit_mod | Pupdate_mod
| Pjs_unsafe_downgrade _ | Pdebugger | Pjs_fn_method
(* A tagged template invokes its tag at runtime, so it always has side
effects. *)
| Ptagged_template | Pjs_apply | Pjs_runtime_apply | Pjs_call _ | Pinit_mod
| Pupdate_mod | Pjs_unsafe_downgrade _ | Pdebugger | Pjs_fn_method
(* Await promise *)
| Pawait
(* TODO *)
Expand Down
27 changes: 1 addition & 26 deletions compiler/core/lam_compile_external_call.ml
Original file line number Diff line number Diff line change
Expand Up @@ -271,32 +271,7 @@ let translate_ffi ?(transformed_jsx = false) (cxt : Lam_compile_context.t)
arg_types (ffi : External_ffi_types.external_spec)
(args : J.expression list) ~dynamic_import =
match ffi with
| Js_call
{external_module_name; name; splice : _; scopes; tagged_template = true}
-> (
let fn =
translate_scoped_module_val external_module_name name scopes
~dynamic_import
in
match args with
| [
{expression_desc = Array (strings, _); _};
{expression_desc = Array (values, _); _};
] ->
E.tagged_template fn strings values
| _ ->
let args, eff, dynamic = assemble_args_has_splice arg_types args in
let args = if dynamic then E.variadic_args args else args in
add_eff eff
(E.call ~info:(Js_call_info.na_full_call transformed_jsx) fn args))
| Js_call
{
external_module_name = module_name;
name = fn;
splice;
scopes;
tagged_template = false;
} ->
| Js_call {external_module_name = module_name; name = fn; splice; scopes} ->
let fn =
translate_scoped_module_val module_name fn scopes ~dynamic_import
in
Expand Down
10 changes: 10 additions & 0 deletions compiler/core/lam_compile_primitive.ml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ let translate output_prefix loc (cxt : Lam_compile_context.t)
match args with
| fn :: rest -> E.call ~info:call_info fn rest
| _ -> assert false)
| Ptagged_template -> (
(* [tag; strings_array; values_array] -> tag`...` *)
match args with
| [
fn;
{expression_desc = Array (strings, _); _};
{expression_desc = Array (values, _); _};
] ->
E.tagged_template fn strings values
| _ -> assert false)
| Pnull_to_opt -> (
match args with
| [e] -> (
Expand Down
1 change: 1 addition & 0 deletions compiler/core/lam_convert.ml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ let lam_prim ~primitive:(p : Lambda.primitive) ~args loc : Lam.t =
| Pfield (id, info) -> prim ~primitive:(Pfield (id, info)) ~args loc
| Psetfield (id, info) -> prim ~primitive:(Psetfield (id, info)) ~args loc
| Pduprecord -> prim ~primitive:Pduprecord ~args loc
| Ptagged_template -> prim ~primitive:Ptagged_template ~args loc
| Praise _ -> prim ~primitive:Praise ~args loc
| Pobjcomp x -> prim ~primitive:(Pobjcomp x) ~args loc
| Pobjorder -> prim ~primitive:Pobjorder ~args loc
Expand Down
5 changes: 5 additions & 0 deletions compiler/core/lam_primitive.ml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type t =
| Psetfield of int * Lam_compat.set_field_dbg_info
(* could have field info at least for record *)
| Pduprecord
(* Tagged template literal: [tag; strings_array; values_array] *)
| Ptagged_template
(* External call *)
| Pjs_call of {
prim_name: string;
Expand Down Expand Up @@ -230,6 +232,9 @@ let eq_primitive_approx (lhs : t) (rhs : t) =
| Parrayrefs | Parraysets | Pjs_fn_make_unit | Pjs_fn_method | Phash
| Phash_mixstring | Phash_mixint | Phash_finalmix ->
rhs = lhs
(* Reachable only via the optimizer's term-equality comparison, which the
test suite doesn't exercise for tagged templates. *)
| Ptagged_template -> ( ((rhs = lhs) [@coverage off]))
| Pcreate_extension a -> (
match rhs with
| Pcreate_extension b -> a = (b : string)
Expand Down
1 change: 1 addition & 0 deletions compiler/core/lam_primitive.mli
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type t =
| Pfield of int * Lambda.field_dbg_info
| Psetfield of int * Lambda.set_field_dbg_info
| Pduprecord
| Ptagged_template
| Pjs_call of {
(* Location.t * [loc] is passed down *)
prim_name: string;
Expand Down
2 changes: 2 additions & 0 deletions compiler/core/lam_print.ml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ let primitive ppf (prim : Lam_primitive.t) =
| Pupdate_mod -> fprintf ppf "update_mod!"
| Pjs_apply -> fprintf ppf "#apply"
| Pjs_runtime_apply -> fprintf ppf "#runtime_apply"
(* Debug-only dump, exercised solely under -drawlambda/-dlambda. *)
| Ptagged_template -> fprintf ppf "#tagged_template" [@coverage off]
| Pjs_unsafe_downgrade {name; setter} ->
if setter then fprintf ppf "##%s#=" name else fprintf ppf "##%s" name
| Pfn_arity -> fprintf ppf "fn.length"
Expand Down
32 changes: 11 additions & 21 deletions compiler/frontend/ast_external_process.ml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ type external_desc = {
get_name: bundle_source option;
mk_obj: bool;
return_wrapper: External_ffi_types.return_wrapper;
tagged_template: bool;
}

let init_st =
Expand All @@ -185,7 +184,6 @@ let init_st =
get_name = None;
mk_obj = false;
return_wrapper = Return_unset;
tagged_template = false;
}

let return_wrapper loc (txt : string) : External_ffi_types.return_wrapper =
Expand Down Expand Up @@ -353,7 +351,13 @@ let parse_external_attributes (no_arguments : bool) (prim_name_check : string)
between unset/set
*)
| scopes -> {st with scopes})
| "taggedTemplate" -> {st with splice = true; tagged_template = true}
| "taggedTemplate" ->
Location.raise_errorf ~loc
"The @@taggedTemplate decorator has been removed. Bind the \
external with the builtin taggedTemplate<'param, 'output> type \
instead, e.g. `@@module(\"x\") external sql: taggedTemplate<'a, \
string> = \"sql\"`. The tag can then be used with backtick \
syntax across module boundaries and as a first-class value."
| "variadic" -> {st with splice = true}
| "send" ->
{st with val_send = Some (name_from_payload_or_prim ~loc payload)}
Expand Down Expand Up @@ -423,7 +427,6 @@ let process_obj (loc : Location.t) (st : external_desc) (prim_name : string)
get_name = None;
get_index = false;
return_wrapper = Return_unset;
tagged_template = _;
set_index = false;
mk_obj = _;
scopes =
Expand Down Expand Up @@ -614,7 +617,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
get_name = None;
return_wrapper = _;
mk_obj = _;
tagged_template = _;
} ->
if arg_type_specs_length = 3 then
Js_set_index {js_set_index_scopes = scopes}
Expand All @@ -639,7 +641,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
set_index = false;
mk_obj = _;
return_wrapper = _;
tagged_template = _;
} ->
if arg_type_specs_length = 2 then
Js_get_index {js_get_index_scopes = scopes}
Expand All @@ -666,7 +667,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
set_index = false;
return_wrapper = _;
mk_obj = _;
tagged_template = _;
} -> (
match (arg_types_ty, new_name, val_name) with
| [], None, _ -> Js_module_as_var external_module_name
Expand Down Expand Up @@ -708,20 +708,17 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
mk_obj = _;
(* mk_obj is always false *)
return_wrapper = _;
tagged_template;
} ->
let name = prim_name_or_pval_prim.name in
if arg_type_specs_length = 0 then
(*
{[
external ff : int -> int [@bs] = "" [@@module "xx"]
]}
FIXME: splice is not supported here
FIXME: splice is not supported here
*)
Js_var {name; external_module_name = None; scopes}
else
Js_call
{splice; name; external_module_name = None; scopes; tagged_template}
else Js_call {splice; name; external_module_name = None; scopes}
| {
call_name = Some {name; source = _};
splice;
Expand All @@ -737,7 +734,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
get_name = None;
mk_obj = _;
return_wrapper = _;
tagged_template;
} ->
if arg_type_specs_length = 0 then
(*
Expand All @@ -747,7 +743,7 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
*)
Js_var {name; external_module_name; scopes}
(*FIXME: splice is not supported here *)
else Js_call {splice; name; external_module_name; scopes; tagged_template}
else Js_call {splice; name; external_module_name; scopes}
| {call_name = Some _} ->
Bs_syntaxerr.err loc
(Conflict_ffi_attribute "Attribute found that conflicts with @val")
Expand All @@ -766,7 +762,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
return_wrapper = _;
splice = false;
scopes;
tagged_template = _;
} ->
(*
if no_arguments -->
Expand All @@ -793,7 +788,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
get_name = None;
mk_obj = _;
return_wrapper = _;
tagged_template;
} ->
let name = prim_name_or_pval_prim.name in
if arg_type_specs_length = 0 then
Expand All @@ -803,7 +797,7 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
]}
*)
Js_var {name; external_module_name; scopes}
else Js_call {splice; name; external_module_name; scopes; tagged_template}
else Js_call {splice; name; external_module_name; scopes}
| {
val_send = Some {name; source = _};
splice;
Expand All @@ -819,7 +813,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
external_module_name = None;
mk_obj = _;
return_wrapper = _;
tagged_template = _;
} -> (
(* PR #2162 - since when we assemble arguments the first argument in
[@@send] is ignored
Expand Down Expand Up @@ -851,7 +844,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
scopes;
mk_obj = _;
return_wrapper = _;
tagged_template = _;
} ->
Js_new {name; external_module_name; splice; scopes}
| {new_name = Some _} ->
Expand All @@ -872,7 +864,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
mk_obj = _;
return_wrapper = _;
scopes;
tagged_template = _;
} ->
if arg_type_specs_length = 2 then
Js_set {js_set_scopes = scopes; js_set_name = name}
Expand All @@ -896,7 +887,6 @@ let external_desc_of_non_obj (loc : Location.t) (st : external_desc)
mk_obj = _;
return_wrapper = _;
scopes;
tagged_template = _;
} ->
if arg_type_specs_length = 1 then
(* Check if the first argument is unit, which is invalid for @get *)
Expand Down
5 changes: 1 addition & 4 deletions compiler/frontend/external_ffi_types.ml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ type external_spec =
external_module_name: external_module_name option;
splice: bool;
scopes: string list;
tagged_template: bool;
}
| Js_send of {name: string; splice: bool; js_send_scopes: string list}
(* we know it is a js send, but what will happen if you pass an ocaml objct *)
Expand Down Expand Up @@ -194,9 +193,7 @@ let check_ffi ?loc ffi : bool =
upgrade (is_package_relative_path external_module_name.bundle);
check_external_module_name external_module_name
| Js_new {external_module_name; name; splice = _; scopes = _}
| Js_call
{external_module_name; name; splice = _; scopes = _; tagged_template = _}
->
| Js_call {external_module_name; name; splice = _; scopes = _} ->
Ext_option.iter external_module_name (fun external_module_name ->
upgrade (is_package_relative_path external_module_name.bundle));
Ext_option.iter external_module_name (fun name ->
Expand Down
1 change: 0 additions & 1 deletion compiler/frontend/external_ffi_types.mli
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ type external_spec =
external_module_name: external_module_name option;
splice: bool;
scopes: string list;
tagged_template: bool;
}
| Js_send of {name: string; splice: bool; js_send_scopes: string list}
(* we know it is a js send, but what will happen if you pass an ocaml objct *)
Expand Down
24 changes: 24 additions & 0 deletions compiler/gentype/translate_type_expr_from_types.ml
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,30 @@ let translate_constr ~config ~params_translation ~(path : Path.t) ~type_env =
[param_translation] ) ->
{param_translation with type_ = Dict param_translation.type_}
| ["Stdlib"; "JSON"; "t"], [] -> {dependencies = []; type_ = unknown}
| ( (["taggedTemplate"] | ["Stdlib"; "TaggedTemplate"; "t"]),
[param_translation; output_translation] ) ->
(* A tagged-template tag is a variadic function usable with backtick syntax:
(strings: TemplateStringsArray, ...values: 'param[]) => 'output.
gentype has no rest-argument field, so the spread is encoded in the
parameter name, which is emitted verbatim before the type. *)
{
dependencies =
param_translation.dependencies @ output_translation.dependencies;
type_ =
Function
{
arg_types =
[
{a_name = "strings"; a_type = ident "TemplateStringsArray"};
{
a_name = "...values";
a_type = Array (param_translation.type_, Mutable);
};
];
ret_type = output_translation.type_;
type_vars = [];
};
}
| _ -> default_case ()

type process_variant = {
Expand Down
Loading
Loading