Status: implemented
sergey-shandar approved the untagged-template design in #2822, as DESIGN.md §12 requires.
An untagged template is a backtick-delimited primary expression, with cooked text chunks separated by ${expression} substitutions:
const name = "world";
export default `Hello, ${name}!`;
// "Hello, world!"
Each substitution uses the ordinary full expression grammar, including object literals, function bodies and nested templates. A template can be an operand, call argument or property-access base, as in `abc`.length. Empty templates and adjacent substitutions are admitted too.
Text chunks accept JSON's escapes (\", \\, \/, \b, \f, \n, \r, \t, \uXXXX, \u{…}), hexadecimal escapes (\xHH), vertical-tab escapes (\v), escaped backticks and the escaped substitution opener \${. Quotes stand raw. Only an unescaped ${ opens a substitution. Literal LF, CR and CRLF are admitted; raw CR and CRLF cook to LF, while escaped \r or \u000D stays CR. Raw U+2028 and U+2029 keep their values. Additional JavaScript template escapes, other raw C0 controls and line continuations remain deferred here and are refused. The quoted-string counterparts are tracked separately in js-string-literals, which excludes templates. Malformed or unterminated templates and tagged templates such as tag`a${b}c` are compilation errors.
A hexadecimal escape consumes exactly two hexadecimal digits, in either case. \xHH denotes the same single UTF-16 code unit as \u00HH, including \x80 through \xff; it is not a UTF-8 byte sequence. A following digit is ordinary text: \x414 denotes A4. Missing or non-hexadecimal digits are errors, including \x8?. This uses the existing shared lexer and cooked-value decoder, without a new AST or FAST node.
The language designer sergey-shandar approved the hexadecimal and vertical-tab increments in the working session. Hexadecimal and vertical-tab escapes are also admitted in single-quoted strings; double-quoted strings keep their existing escape subset. \v denotes U+000B, exactly as \u000b does, in every template chunk. Identity escapes, raw C0 controls other than CR/LF, and line continuations remain refused here.
The NUL shorthand \0 is intentionally prohibited, even when followed by an otherwise valid character. Use the fixed-width \u0000 or \x00 spelling instead. The codebase rule also applies to proofs, helpers and host adapters.
| Refused template spelling | Admitted spelling with the same value |
|---|---|
`\0${"key"}\0` | `\u0000${"key"}\u0000` |
For a refused line continuation, join the lines without the continuation sequence to preserve the value. Before marking a module compiler-compatible with the .f.js extension, normalize any deferred spellings it uses. For example, serializer/names's _name uses `\0${key}\0`; its supported equivalent is `\u0000${key}\u0000`. Template support alone does not admit every existing template spelling.
Every substitution is converted with String, and its result is concatenated with the cooked chunks using left-associated binary +. For example, `hello ${a}!` has the same meaning and EDAG as "hello " + String(a) + "!":
['+', ['+', 'hello ', ['String', A]], '!']
Here A is the lowered expression for a. For chunks s0 through sn and substitutions e1 through en, lowering starts with r = s0. In source order, each substitution adds r = ['+', r, ['String', lower(ei)]], then r = ['+', r, si]. Every chunk, including an empty one, is retained, and conversion is never folded away. Thus `${1}${2}` expands to "" + String(1) + "" + String(2) + "", producing "12".
The result of a successful template evaluation is always a string. Evaluation or conversion failure propagates under failure is one outcome: the graph construction order does not impose evaluation-count or source-order barriers on failing computations. Function conversion follows the existing function-source representation exception.
Explicit string conversion matters for objects: String tries toString before valueOf, whereas raw + coercion tries them in the opposite order.
const a = { valueOf: () => 7, toString: () => "a" };
export default ["hello " + a + "!", `hello ${a}!`];
// ["hello 7!", "hello a!"]
Some further examples:
``; // ""
`abc`; // "abc"
`${1n}:${undefined}`; // "1:undefined"
`sum: ${1 + 2}`; // "sum: 3"
`${{ x: 1 }.x}`; // "1"
`${`${1}`}!`; // "1!"
`literal \${a}`; // "literal ${a}"
`\``; // "`"
`\u0000${"key"}\u0000`; // "\u0000key\u0000"
`line
break`; // "line\nbreak"
A template without substitutions lowers directly to its cooked string leaf, so `abc` and "abc" have the same EDAG. Every template is FJS source syntax, including this case; cooking does not classify it as JSON or DataJS. FunctionalScript output writes a string or the explicit String/+ expansion instead of reconstructing template spelling. EDAG and Rust outputs use those existing operations, and JSON and DataJS outputs evaluate them.
The compiler demo offers the programs templateLiterals in module.f.js, each with its real output.
The demo on this page runs sample.f.js, this page's example as a function of one number — a template literal with two substitutions — with every output fjs compile makes of it: the expression graph (EDAG), the Rust and the WebAssembly that Rust builds to. Its Run button calls the default export with 1 and 2, as JavaScript and as WebAssembly, and shows what each call returned or threw.
export default (...a) => `Hello, ${a[0]} world${a[0] > 1 ? "s" : ""}!`;