Make tagged templates a first-class type#8461
Open
JonoPrest wants to merge 21 commits into
Open
Conversation
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.
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`.
Contributor
Author
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #8461 +/- ##
==========================================
+ Coverage 60.91% 60.95% +0.03%
==========================================
Files 374 374
Lines 54095 54112 +17
==========================================
+ Hits 32953 32984 +31
+ Misses 21142 21128 -14
🚀 New features to boost your workflow:
|
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.
rescript
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #8415.
Replaces the
@taggedTemplatedecorator with a first-classtaggedTemplate<'param, 'output>type (plus a smallTaggedTemplatestdlibmodule). Because tag-ness now lives in the type instead of the FFI annotation,
the compiler emits a real JS tagged-template literal at every call site -
across modules, through first-class values, and for tags built at runtime by a
factory (the
postgres-style case).TaggedTemplate.makelifts a plain(array<string>, array<'param>) => 'outputfunction into the type so ReScript-authored tags work with backtick syntax too.
Breaking: the
@taggedTemplatedecorator is removed. Using this or backticksyntax on a non-
taggedTemplatevalue is now a compile error pointing at thenew binding form, with tag-specific messages for the common misuse cases.