From f8199dc5a29dbd886471ead7e7e18dd83906a871 Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Mon, 26 Oct 2026 14:33:01 +0100 Subject: [PATCH] Fix template literal substitution (Iann29) --- quickjs-atom.h | 1 - quickjs.c | 17 +++++++++++------ tests/test_language.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/quickjs-atom.h b/quickjs-atom.h index 82f6981..ea461ae 100644 --- a/quickjs-atom.h +++ b/quickjs-atom.h @@ -129,7 +129,6 @@ DEF(input, "input") DEF(defineProperties, "defineProperties") DEF(apply, "apply") DEF(join, "join") -DEF(concat, "concat") DEF(split, "split") DEF(construct, "construct") DEF(getPrototypeOf, "getPrototypeOf") diff --git a/quickjs.c b/quickjs.c index b2a6949..c564fdb 100644 --- a/quickjs.c +++ b/quickjs.c @@ -25029,8 +25029,8 @@ static __exception int js_parse_template(JSParseState *s, int call, int *argc) if (depth == 0) { if (s->token.u.str.sep == '`') goto done1; - emit_op(s, OP_get_field2); - emit_atom(s, JS_ATOM_concat); + } else { + emit_op(s, OP_add); } depth++; } else { @@ -25043,11 +25043,19 @@ static __exception int js_parse_template(JSParseState *s, int call, int *argc) return -1; if (js_parse_expr(s)) return -1; + /* XXX: should instead add OP_to_string and OP_concat so that + all the strings can be concatenated at the end (faster) */ + if (!call) { + /* ToString right away, before the next substitution is evaluated + (ECMA-262 13.2.8.6). OP_to_propkey keeps a symbol, which + OP_add then rejects, as ToString does. */ + emit_op(s, OP_to_propkey); + emit_op(s, OP_add); + } depth++; if (s->token.val != '}') { return js_parse_error(s, "expected '}' after template expression"); } - /* XXX: should convert to string at this stage? */ free_token(s, &s->token); /* Resume TOK_TEMPLATE parsing (s->token.line_num and * s->token.ptr are OK) */ @@ -25063,9 +25071,6 @@ static __exception int js_parse_template(JSParseState *s, int call, int *argc) seal_template_obj(ctx, raw_array); seal_template_obj(ctx, template_object); *argc = depth + 1; - } else { - emit_op(s, OP_call_method); - emit_u16(s, depth - 1); } done1: return next_token(s); diff --git a/tests/test_language.js b/tests/test_language.js index babece3..aa96c22 100644 --- a/tests/test_language.js +++ b/tests/test_language.js @@ -375,6 +375,38 @@ function test_template_skip() assert(b, "BaraBarbaz"); } +function test_template_tostring_order() +{ + /* each substitution is converted with ToString right after it is + evaluated (ECMA-262 13.2.8.6), and String.prototype.concat is not + involved */ + var log = []; + var o = { toString() { log.push("o"); return "O"; } }; + assert(`${o}${(log.push("next"), "n")}`, "On"); + assert(log.join(), "o,next"); + + var c = { n: 0, toString() { return "" + this.n; } }; + assert(`${c} ${c.n++}`, "0 0"); + + var d = new Date(2024, 0, 15, 12); + assert(`${d}|${d.setFullYear(2000)}`.indexOf("2024"), 11); + + var p = { [Symbol.toPrimitive](hint) { return hint; } }; + assert(`${p}`, "string"); + var v = { valueOf() { return 1; }, toString() { return "s"; } }; + assert(`${v}`, "s"); + + var concat = String.prototype.concat; + String.prototype.concat = function() { return "concat"; }; + try { + assert(`a${1}b${2}`, "a1b2"); + } finally { + String.prototype.concat = concat; + } + + assert_throws(TypeError, () => `${Symbol()}`); +} + function test_object_literal() { var x = 0, get = 1, set = 2, async = 3; @@ -1004,6 +1036,7 @@ test_arguments(); test_class(); test_template(); test_template_skip(); +test_template_tostring_order(); test_object_literal(); test_regexp_skip(); test_labels(); -- 2.47.3