-
Notifications
You must be signed in to change notification settings - Fork 471
Implement consistent JavaScript template literal compilation for ReScript template literals #7832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
40f319f
ed6cd17
41455b0
48dd317
754b276
b2f4195
25a0a93
fbfa22c
8dcab20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+1753
to
+1783
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Guard against template literal extraction without interpolations
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 | ||
|
@@ -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 | ||
|
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}`}` |
There was a problem hiding this comment.
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 concatenatedConst_string
and expression nodes from aPstringadd
chain in evaluation order, but then reverses both sequences viaList.rev_map
before constructing theTagged_template
. For a simple template literal like`Hello ${name}!`
this produces the parts["!", "Hello "]
withvalue_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 👍 / 👎.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I can only help with PRs you've created.