Skip to content

First-class taggedTemplate type (#8415)#3

Draft
JonoPrest wants to merge 22 commits into
masterfrom
jono/tagged-template-type
Draft

First-class taggedTemplate type (#8415)#3
JonoPrest wants to merge 22 commits into
masterfrom
jono/tagged-template-type

Conversation

@JonoPrest

@JonoPrest JonoPrest commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Draft for reviewing the diff. Implements rescript-lang#8415.

Makes "tagged-template tag" a first-class type so the compiler tracks
it through module boundaries, let aliases, function parameters/returns,
and runtime-constructed values — emitting a real JS tagged-template
literal at every backtick call site.

Design

Backtick syntax now has exactly one meaning: v`...${x}...` is valid
only when v : taggedTemplate<'param, 'output>, and always emits a
real JS tagged template.

  • Builtin type (predef.ml): global taggedTemplate<'param, 'output>
    (like promise/dict) + thin Stdlib.TaggedTemplate (type t alias +
    make, which lifts a plain ReScript tag function into the type).
  • Typing (typecore.ml): backtick application unifies the tag with
    taggedTemplate<'param, 'output> and types the desugared
    (array<string>, array<'param>) args → 'output. Clean inference for
    unannotated tags; a clear migration error otherwise.
  • Codegen: new Ptagged_template lambda primitive emits the real
    backtick literal for any value of the type (external, let-binding,
    function param, factory result, cross-module).
  • Editor/gentype: completion resolves an applied tag to its 'output;
    gentype maps the (variadic JS) type opaquely.

Breaking (RC13)

The @taggedTemplate external decorator is removed — using it, or using
backtick syntax on the old (array<string>, array<'a>) => 'o shape, is a
compile error pointing to the new binding form. The dead FFI flag/branch
is removed.

Fixes all four limitations in the issue

factory-returned tags (postgres), wrapper leakage, cross-module
degradation, first-class/param use, and ReScript-authored wrappers via
TaggedTemplate.make.

Tests

  • Rewritten runtime suite (factory, function-param, make, module-scoped)
    — all 8 pass; every call site emits real backticks.
  • super_errors fixtures for both new errors + ERROR_VARIANTS catalog entry.
  • Updated analysis completion snapshot. CHANGELOG entries.

🤖 Generated with Claude Code

JonoPrest added 13 commits June 4, 2026 11:04
Introduce a predefined global `taggedTemplate<'param, 'output>` type
(arity 2, mirroring `promise`/`dict` in predef.ml) so that
tagged-templateness can live on the type of a value rather than on an
external binding site.

Add the thin `Stdlib.TaggedTemplate` module aliasing it as
`t<'param, 'output>` and exposing `make`, which lifts a plain ReScript
tag function `(array<string>, array<'param>) => 'output` into the type by
adapting the variadic JS tag-call convention.

Part of the first-class tagged-template work (rescript-lang#8415).
When an application carries the parser's `res.taggedTemplate` attribute,
unify the tag's type with a fresh `taggedTemplate<'param, 'output>` and
type the desugared `(array<string>, array<'param>)` arguments against it,
yielding `'output`. This gives clean inference for unannotated tags
(`tag => tag\`...${x}...\``).

If the tag's type is not a `taggedTemplate` (e.g. an old
`(array<string>, array<'a>) => 'o` tag function), raise a dedicated
migration error steering the user to the new binding form or
`TaggedTemplate.make`.

Part of rescript-lang#8415.
Add a `Ptagged_template` primitive (`[tag; strings; values]`) carried
from the ml lambda layer through to the JS-IR layer. translcore detects
a `res.taggedTemplate` application and emits the primitive, translating
the tag as a plain value — so the real backtick literal is emitted at
every call site regardless of how the tag was obtained (external,
let-binding, function parameter, factory result, cross-module).

lam_compile_primitive turns the primitive into the existing
`Js_exp_make.tagged_template` node, reusing the established JS dump path.

Part of rescript-lang#8415.
Tagged-templateness now lives on the type, so the `@taggedTemplate`
external decorator is obsolete. Using it is now a compile error that
steers users to bind the external with the `taggedTemplate<...>` type
instead.

With the decorator gone, the `tagged_template` flag on the `Js_call` FFI
spec is always false, so remove the field and the now-unreachable
tagged-template branch in lam_compile_external_call.

Part of rescript-lang#8415.
Teach the completion engine that applying a value of type
`taggedTemplate<'param, 'output>` (via backtick syntax) yields its
`'output` type, restoring dot/pipe completions on a tagged-template
result. Update the analysis fixture to the new binding form.

Map the `taggedTemplate` builtin to an opaque type in gentype (it is a
variadic JS function), so genType'd values don't emit a dangling TS
reference.

Part of rescript-lang#8415.
Rewrite the tagged-template runtime test around the new model: an
external bound with the `taggedTemplate` type, a runtime-constructed tag
via a factory, a tag passed as a function argument, and a ReScript tag
lifted with `TaggedTemplate.make` — asserting real tagged-template
syntax is emitted at every call site.

Add super_errors fixtures for the two new errors (removed
`@taggedTemplate` decorator, and backtick syntax on a non-tag value),
catalog the new `Tagged_template_non_tag` variant in ERROR_VARIANTS.md,
and add CHANGELOG entries.

Part of rescript-lang#8415.
Add runtime assertions that pin down the real behaviour of the feature:

- A `rawTag` binding proves the compiler emits a genuine tagged template:
  the tag receives a frozen `TemplateStringsArray` with `.raw`, not a
  plain/variadic function call.
- A cross-module case: a `taggedTemplate` value defined in
  `Tagged_template_binding` and consumed here with backtick syntax, so the
  consumer sees only the type yet still emits a real tagged template.
- A compile-only `tagged_template_global_import` fixture pins the
  generated JS for a bare-package import (`@module("postgres")`) returning
  a `taggedTemplate`, vs. the existing relative `./file.js` bindings.

Part of rescript-lang#8415.
Cover the ways a tagged template can be used incorrectly:

- super_errors: calling a `taggedTemplate` value as a regular function
  (`sql(strings, args)`) — it is not a function type; and interpolating a
  value of the wrong `'param` type.
- syntax_tests: unclosed tagged-template backticks (both an unterminated
  literal and an unterminated `${...}` interpolation), which recover with
  "Did you forget to close this template expression with a backtick?".

Part of rescript-lang#8415.
Drop the references to the issue's "Problem N" labels; each comment now
just explains what the test exercises.
The previous `taggedTemplateUnclosedInterpolation` fixture left both the
`${...}` and the backtick unterminated, so it just re-exercised the
"forgot the backtick" path. Replace it with an empty interpolation
(`${}`) inside a properly closed template, which exercises the distinct
"this expression block is empty" interpolation error.
Two error paths reported confusing, leaky messages for tagged templates:

- Calling a tag as a function (`sql(strings, args)`) gave the generic
  "this can't be called, it's not a function". It now explains the value
  is a tagged-template tag and to use backtick syntax instead.

- Interpolating a value of the wrong `'param` type reported an "array
  item" type error (exposing the desugared values array, which the user
  never wrote). It now uses a dedicated `TaggedTemplateValue` clash
  context: "This interpolated value has type: … But this tag expects
  interpolations of type: …", typing each `${...}` directly instead of
  routing through the array-literal typer.
Add a runtime test for a tag bound to a bare (non-relative) import
specifier, complementing the relative-path bindings. A Node `imports`
subpath entry (`#tagged-template-pg`) resolves the bare specifier to a
committed mock without installing a package. The mock throws unless it
receives a real `TemplateStringsArray`, so the test passing proves the
call site emitted real tagged-template syntax against the bare import.
Adding the `TaggedTemplate` stdlib module makes it appear in module
completion lists, so the affected analysis snapshots gain the new entry.
@JonoPrest JonoPrest force-pushed the jono/tagged-template-type branch from 1880baa to 37bf615 Compare June 4, 2026 11:11
JonoPrest added 2 commits June 4, 2026 11:36
Exercise the `taggedTemplate` / `TaggedTemplate.t` -> opaque (`unknown`)
gentype translation, which was previously untested. Both the global
builtin and the stdlib alias are emitted as `(x:unknown) => unknown`.
A `taggedTemplate<'param, 'output>` is a variadic tag function, so map it
to the precise TypeScript signature

  (strings: TemplateStringsArray, ...values: 'param[]) => 'output

rather than the opaque `unknown`. This is also the signature TypeScript
requires for a value to be usable with backtick tagged-template syntax.
gentype has no rest-argument field, so the spread is encoded in the
parameter name (emitted verbatim before the type); the generated output
type-checks under `tsc`.
@JonoPrest JonoPrest force-pushed the jono/tagged-template-type branch from 97d80b6 to 1b92724 Compare June 4, 2026 13:10
JonoPrest and others added 7 commits June 4, 2026 17:05
The testrepo's rescript-bun dependency still used the removed
@taggedTemplate decorator on its sh/shExpr shell tags, which now
fails to compile. Extend the existing yarn patch to bind them with
the first-class taggedTemplate<'param, 'output> type instead.
typecore builds the tagged-template type directly via
Predef.path_tagged_template, so the type_tagged_template constructor
helper was never called. Remove it (and its .mli signature) rather
than leave dead, uncoverable code.
The parser always desugars the interpolated values of a tagged
template into an array literal, so the non-array branch was dead
code. Replace its silent generic-array fallback with assert false,
matching the sibling sargs match in the same block and documenting
the invariant instead of masking a potential desugaring bug.
…om coverage

- Remove the unused context_to_string function (zero callers repo-wide);
  its TaggedTemplateValue arm was the only flagged line there.
- Mark the debug-only lambda printers and the optimizer fast-path arms
  (no_side_effects, eq_primitive_approx) with [@coverage off] + a comment.
  These are reachable only from -drawlambda/-dlambda dumps or optimizer
  term-equality/purity checks that the test suite never exercises for
  tagged templates. The shared OR-pattern groups are kept intact so their
  existing coverage is unaffected.
The no_side_effects tagged-template branch was wrongly marked
[@coverage off] with a comment implying it was unreachable. It is in
fact reachable by an ordinary top-level tag application with pure
arguments. Put Ptagged_template back alongside the other effectful
primitives (Pjs_call, etc.); its bisect row is cold for the same
reason theirs is (no_side_effects short-circuits on impure args), so
it needs no special handling.
Co-authored-by: Paul Tsnobiladzé <paul.tsnobiladze@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant