diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index 44820298f6..39ec623b9b 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -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 diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 739056132b..f4fe4b8b48 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -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 + 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 + 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 @@ -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 diff --git a/compiler/core/lam_compile_const.ml b/compiler/core/lam_compile_const.ml index 58603472dd..0a97eb99ea 100644 --- a/compiler/core/lam_compile_const.ml +++ b/compiler/core/lam_compile_const.ml @@ -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) diff --git a/compiler/core/lam_constant_convert.ml b/compiler/core/lam_constant_convert.ml index 3be30e048c..f33d159a8b 100644 --- a/compiler/core/lam_constant_convert.ml +++ b/compiler/core/lam_constant_convert.ml @@ -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 diff --git a/compiler/frontend/lam_constant.ml b/compiler/frontend/lam_constant.ml index 86ff842322..95c6d64577 100644 --- a/compiler/frontend/lam_constant.ml +++ b/compiler/frontend/lam_constant.ml @@ -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 @@ -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 diff --git a/tests/tests/src/template_literal_consistency_test.res b/tests/tests/src/template_literal_consistency_test.res new file mode 100644 index 0000000000..bb7db19091 --- /dev/null +++ b/tests/tests/src/template_literal_consistency_test.res @@ -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, array) => 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}`}` \ No newline at end of file