Skip to content

Fix/qualified tagged template tags - #306

Merged
aspeddro merged 2 commits into
rescript-lang:mainfrom
nkrkv:fix/qualified-tagged-template-tags
Sep 25, 2026
Merged

aspeddro merged 2 commits into
rescript-lang:mainfrom
nkrkv:fix/qualified-tagged-template-tags

Conversation

@nkrkv

@nkrkv nkrkv commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Hi there,

The current implementation errors on a tagged template literals like:

SQL.query`SELECT * FROM something`

which are a perfectly valid ReScript.

This PR fixes this by making literal prefixes what they really are: a function application. I also deleted an unused and actually misleading state flag from the C lexer that I've introduced many years ago. Thanks to Claude. His technical explanation goes below


Why

SQL.query`…` doesn't parse: the tag was a lexical prefix matching only
[a-z_][a-zA-Z0-9_']*.

bsc -dparsetree shows a tagged template is Pexp_apply of a Pexp_ident, so
the tag is a value reference rather than part of the literal — hence the node,
and the function / arguments fields borrowed from call_expression.

The scanner commit

in_backticks was inverted. The opening backtick was fused into the template
token so it never set the flag, while TEMPLATE_CHARS cleared it and BACKTICK
toggled it at the same closing backtick — leaving it false inside templates and
true outside. It feeds in_string, which gates whitespace skipping and
block-comment scanning. Nothing needs it: valid_symbols[TEMPLATE_CHARS] already
tells the scanner when it is in template text.

in_quotes is not symmetric and stays — string content is grammar-lexed, so the
scanner does run between fragments. Removing it fails Strings containing
comment-like content
.

nkrkv and others added 2 commits September 21, 2026 18:27
`in_backticks` never tracked what its name says. The opening backtick was
fused into the template token, so it never set the flag; TEMPLATE_CHARS
clears the flag when it reaches the closing backtick, and the BACKTICK
branch then toggles it at that same backtick. The flag therefore ended up
false inside template literals and true outside them — inverted — and it
feeds `in_string`, which gates whitespace skipping and block-comment
scanning. Every template literal silently flipped the lexer into string
mode for the code that followed it.

Nothing needs the flag. The scanner learns it is lexing template text from
`valid_symbols[TEMPLATE_CHARS]`, which the parser supplies and which is
authoritative; the TEMPLATE_CHARS branch then consumes the whole run
itself, so `/*` inside a template is never offered to the comment scanner.
Quotes are different and keep `in_quotes`: string content is lexed by the
grammar as `unescaped_double_string_fragment`, so the scanner does run
between fragments and does need to be told to stand down. Removing
`in_quotes` as well fails "Strings containing comment-like content".

Both backticks become ordinary grammar tokens. Also drops
`_unescaped_template_string_fragment`, dead since template content moved
to the scanner, and adds a test for a block comment following a template
substitution — the case the inverted flag corrupted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tag was a lexical prefix fused into the template token, matching only
`[a-z_][a-zA-Z0-9_']*`, so a module-qualified tag failed to parse at all:

    let _ = await sql->(SQL.query`SELECT 1`)

ReScript treats the tag as a function. `bsc -dparsetree` gives `Pexp_apply`
of the `Pexp_ident` `SQL.query` over two arrays, so the tag is a value
reference — plain or qualified by a module — rather than part of the
literal. Model it that way, with the `function` and `arguments` fields
`call_expression` already uses. `SQL` and `query` become separate nodes,
which lets highlights scope them as a module and a call.

The compiler settles the edges: whitespace before the literal is allowed,
`obj.field` and `f(1)` are rejected as tags, escaped identifiers are
accepted, and in JSX children no tagged template is formed — `<div>
sql`x` </div>` is two children, which the added conflict reproduces.
Parenthesized tags are left out; accepting an arbitrary parenthesized
expression before every template is not worth the ambiguity.

Injections had to follow: `%raw`/`%graphql`/`%relay` matched content with
`(_ (_))`, which under the extra node captured the tag and the backticks
as injected source. They now name the shapes they accept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fhammerschmidt

Copy link
Copy Markdown
Member

@codex review

@aspeddro aspeddro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!! 🎉

@aspeddro
aspeddro merged commit 90643a6 into rescript-lang:main Sep 25, 2026
4 checks passed
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.

3 participants