Skip to content
Closed
11 changes: 10 additions & 1 deletion compiler/core/js_dump.ml
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,16 @@ and expression_desc cxt ~(level : int) f x : cxt =
comma_sp f;
expression ~level:1 cxt f el))
| Tagged_template (call_expr, string_args, value_args) ->
let cxt = expression cxt ~level f call_expr in
(* Check if this is an untagged template literal (marked with empty string) *)
let is_untagged = match call_expr.expression_desc with
| Str {txt = ""; _} -> true
| _ -> false
in

let cxt =
if is_untagged then cxt (* Don't print the call expression for untagged literals *)
else expression cxt ~level f call_expr (* Print the function call for tagged literals *)
in
P.string f "`";
let rec aux cxt xs ys =
match (xs, ys) with
Expand Down
59 changes: 58 additions & 1 deletion compiler/core/lam_compile.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,53 @@ let compile output_prefix =
in
Js_output.output_of_block_and_expression lambda_cxt.continuation args_code
exp

(* Check if a Pstringadd chain looks like a template literal *)
let rec is_template_literal_pattern (lam : Lam.t) : bool =
let rec has_template_strings lam =
match lam with
| Lprim {primitive = Pstringadd; args = [left; right]; _} ->
(match right with
| Lconst (Const_string {template = true; _}) -> true
| _ -> has_template_strings left || has_template_strings right)
| Lconst (Const_string {template = true; _}) -> true
| _ -> false
in
has_template_strings lam

(* Extract and compile a template literal from a Pstringadd chain *)
let rec compile_template_literal (lam : Lam.t) (lambda_cxt : Lam_compile_context.t) : Js_output.t =
let rec extract_parts acc_strings acc_exprs current =
match current with
| Lprim {primitive = Pstringadd; args = [left; right]; _} ->
(match right with
| Lconst (Const_string {s; _}) ->
extract_parts (s :: acc_strings) acc_exprs left
| _ ->
extract_parts acc_strings (right :: acc_exprs) left)
| Lconst (Const_string {s; _}) ->
(s :: acc_strings, acc_exprs)
| _ ->
(acc_strings, current :: acc_exprs)
in

let (strings, expressions) = extract_parts [] [] lam in
let string_exprs = List.rev_map (fun s -> E.str s) strings in

(* Compile expressions *)
let compile_expr expr =
match compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} expr with
| {block; value = Some v} -> (v, block)
| {value = None} -> assert false
in
let (value_exprs, expr_blocks) = List.split (List.rev_map compile_expr expressions) in
Comment on lines +1753 to +1778

Choose a reason for hiding this comment

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

[P0] Avoid reversing template literal segments

The newly added compile_template_literal collects the concatenated Const_string and expression nodes from a Pstringadd chain in evaluation order, but then reverses both sequences via List.rev_map before constructing the Tagged_template. For a simple template literal like `Hello ${name}!` this produces the parts ["!", "Hello "] with value_args = [name], yielding JS `!${name}Hello `. Any template literal with interpolations now emits its static and dynamic pieces in the wrong order, so the generated JS strings are incorrect.

Useful? React with 👍 / 👎.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@codex update the PR with this suggestion

Choose a reason for hiding this comment

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

@codex update the PR with this suggestion (path=compiler/core/lam_compile.ml line=1778 side=RIGHT)

For now, I can only help with PRs you've created.

let all_blocks = List.concat expr_blocks in

(* Generate template literal *)
let call_expr = E.str "" in (* Empty string marks untagged template literal *)
let template_expr = E.tagged_template call_expr string_exprs value_exprs in
Comment on lines +1753 to +1783

Choose a reason for hiding this comment

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

[P1] Guard against template literal extraction without interpolations

compile_template_literal always emits a Tagged_template regardless of how many expressions were extracted. When a backtick literal is used purely as a string (e.g. `foo` ++ `bar`), the extractor gathers two string pieces but value_exprs is empty, so the printer’s Tagged_template branch hits the _ -> assert false case in js_dump.ml. This crashes compilation for valid code. The transformation needs to either skip converting such concatenations or ensure string_args and value_args satisfy the expected n+1 shape before building the Tagged_template.

Useful? React with 👍 / 👎.

Js_output.output_of_block_and_expression lambda_cxt.continuation all_blocks template_expr

and compile_lambda (lambda_cxt : Lam_compile_context.t) (cur_lam : Lam.t) :
Js_output.t =
match cur_lam with
Expand Down Expand Up @@ -1795,7 +1842,17 @@ let compile output_prefix =
*)
Js_output.output_of_block_and_expression lambda_cxt.continuation []
(E.ml_module_as_var ~dynamic_import i)
| Lprim prim_info -> compile_prim prim_info lambda_cxt
| Lprim prim_info -> (
(* Special handling for potential template literals *)
match prim_info with
| {primitive = Pstringadd; args = [left; right]; _} ->
(* Check if this looks like a template literal pattern *)
if is_template_literal_pattern (Lprim prim_info) then
compile_template_literal (Lprim prim_info) lambda_cxt
else
compile_prim prim_info lambda_cxt
| _ ->
compile_prim prim_info lambda_cxt)
| Lsequence (l1, l2) ->
let output_l1 =
compile_lambda {lambda_cxt with continuation = EffectCall Not_tail} l1
Expand Down
10 changes: 8 additions & 2 deletions compiler/core/lam_compile_const.ml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,14 @@ and translate (x : Lam_constant.t) : J.expression =
| Const_char i -> Js_of_lam_string.const_char i
| Const_bigint (sign, i) -> E.bigint sign i
| Const_float f -> E.float f (* TODO: preserve float *)
| Const_string {s; unicode = false} -> E.str s
| Const_string {s; unicode = true} -> E.str ~delim:DStarJ s
| Const_string {s; unicode = false; template = false} -> E.str s
| Const_string {s; unicode = true; template = false} -> E.str ~delim:DStarJ s
| Const_string {s; unicode = false; template = true} ->
(* Generate template literal syntax for backquoted strings *)
E.tagged_template (E.str "") [E.str s] []
| Const_string {s; unicode = true; template = true} ->
(* Generate template literal syntax for unicode backquoted strings *)
E.tagged_template (E.str "") [E.str ~delim:DStarJ s] []
| Const_pointer name -> E.str name
| Const_block (tag, tag_info, xs) ->
Js_of_lam_block.make_block NA tag_info (E.small_int tag)
Expand Down
7 changes: 6 additions & 1 deletion compiler/core/lam_constant_convert.ml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ let rec convert_constant (const : Lambda.structured_constant) : Lam_constant.t =
| Some opt -> Ast_utf8_string_interp.is_unicode_string opt
| _ -> false
in
Const_string {s; unicode}
let template =
match opt with
| Some "" -> true (* Template literal marker *)
| _ -> false
in
Const_string {s; unicode; template}
| Const_base (Const_float i) -> Const_float i
| Const_base (Const_int32 i) -> Const_int {i; comment = None}
| Const_base (Const_int64 _) -> assert false
Expand Down
6 changes: 3 additions & 3 deletions compiler/frontend/lam_constant.ml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type t =
| Const_js_false
| Const_int of {i: int32; comment: pointer_info}
| Const_char of int
| Const_string of {s: string; unicode: bool}
| Const_string of {s: string; unicode: bool; template: bool}
| Const_float of string
| Const_bigint of bool * string
| Const_pointer of string
Expand All @@ -73,9 +73,9 @@ let rec eq_approx (x : t) (y : t) =
match y with
| Const_char iy -> ix = iy
| _ -> false)
| Const_string {s = sx; unicode = ux} -> (
| Const_string {s = sx; unicode = ux; template = tx} -> (
match y with
| Const_string {s = sy; unicode = uy} -> sx = sy && ux = uy
| Const_string {s = sy; unicode = uy; template = ty} -> sx = sy && ux = uy && tx = ty
| _ -> false)
| Const_float ix -> (
match y with
Expand Down
81 changes: 81 additions & 0 deletions tests/tests/src/template_literal_consistency_test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* Test case for template literal compilation consistency
* This should demonstrate that both external and ReScript tagged template calls
* generate the same JavaScript template literal syntax
*/

// External tagged template (already works correctly)
@module("./lib.js") @taggedTemplate
external sqlExternal: (array<string>, array<string>) => string = "sql"

// ReScript function that should now also generate template literal syntax
let sqlReScript = (strings, values) => {
// Simple implementation for testing
let result = ref("")
let valCount = Belt.Array.length(values)
for i in 0 to valCount - 1 {
result := result.contents ++ strings[i] ++ Belt.Int.toString(values[i])
}
result.contents ++ strings[valCount]
}

// Regular function with two array args - should NOT be treated as template literal
let regularFunction = (arr1, arr2) => {
"regular function result"
}

// Test data
let table = "users"
let id = 42

// Both calls should now generate identical JavaScript template literal syntax:
// sqlExternal`SELECT * FROM ${table} WHERE id = ${id}`
// sqlReScript`SELECT * FROM ${table} WHERE id = ${id}`
let externalResult = sqlExternal`SELECT * FROM ${table} WHERE id = ${id}`
let rescriptResult = sqlReScript`SELECT * FROM ${table} WHERE id = ${id}`

// Simple cases
let simple1 = sqlExternal`hello ${123} world`
let simple2 = sqlReScript`hello ${123} world`

// Edge cases: empty interpolations
let empty1 = sqlExternal`no interpolations`
let empty2 = sqlReScript`no interpolations`

// Regular function call (should remain as function call, not template literal)
let regularCall = regularFunction(["not", "template"], ["literal", "call"])

// Test various data types
let numberTest1 = sqlExternal`number: ${42}`
let numberTest2 = sqlReScript`number: ${42}`

let stringTest1 = sqlExternal`string: ${"test"}`
let stringTest2 = sqlReScript`string: ${"test"}`

// NEW: Test regular template literals (the main issue)
// These should generate template literal syntax instead of string concatenation

let name = "World"
let count = 42

// Basic template literal with one interpolation
let basicTemplate = `Hello ${name}!`

// Template literal with multiple interpolations
let multiTemplate = `Hello ${name}, you have ${count} messages`

// Template literal with number interpolation
let numberTemplate = `Count: ${count}`

// Template literal with mixed types
let mixedTemplate = `User: ${name} (${count} years old)`

// Template literals with empty strings
let emptyStart = `${name} is here`
let emptyEnd = `Welcome ${name}`
let emptyMiddle = `${name}${count}`

// Template literal with just interpolation (edge case)
let justInterpolation = `${name}`

// Nested template expressions
let nested = `Outer: ${`Inner: ${name}`}`
Loading