From bda68577b6033eece08845490bde8cbbfa545222 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 12 Oct 2025 16:40:58 +0800 Subject: [PATCH 1/5] Add ES2020 string export/import name support support for ES2020 arbitrary module namespace identifier names, which allows using string literals as export/import names. Examples: ```js export { foo as "string-name" } import { "string-name" as foo } export * as "string-name" from "./mod.js" ``` --- quickjs.c | 38 +++++++++++++++++++++++++-------- tests/fixture_string_exports.js | 12 +++++++++++ tests/test_string_exports.js | 25 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 tests/fixture_string_exports.js create mode 100644 tests/test_string_exports.js diff --git a/quickjs.c b/quickjs.c index 793a13e9f..9fa431161 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29334,11 +29334,17 @@ static __exception int js_parse_export(JSParseState *s) if (token_is_pseudo_keyword(s, JS_ATOM_as)) { if (next_token(s)) goto fail; - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); + if (token_is_ident(s->token.val)) { + export_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + export_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (export_name == JS_ATOM_NULL) { + return -1; + } + } else { + js_parse_error(s, "identifier or string expected"); goto fail; } - export_name = JS_DupAtom(ctx, s->token.u.ident.atom); if (next_token(s)) { fail: JS_FreeAtom(ctx, local_name); @@ -29382,11 +29388,19 @@ static __exception int js_parse_export(JSParseState *s) /* export ns from */ if (next_token(s)) return -1; - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); + + if (token_is_ident(s->token.val)) { + export_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + export_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (export_name == JS_ATOM_NULL) { + return -1; + } + } else { + js_parse_error(s, "identifier or string expected"); return -1; } - export_name = JS_DupAtom(ctx, s->token.u.ident.atom); + if (next_token(s)) goto fail1; module_name = js_parse_from_clause(s); @@ -29560,11 +29574,17 @@ static __exception int js_parse_import(JSParseState *s) return -1; while (s->token.val != '}') { - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); + if (token_is_ident(s->token.val)) { + import_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + import_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (import_name == JS_ATOM_NULL) { + return -1; + } + } else { + js_parse_error(s, "identifier or string expected expected"); return -1; } - import_name = JS_DupAtom(ctx, s->token.u.ident.atom); local_name = JS_ATOM_NULL; if (next_token(s)) goto fail; diff --git a/tests/fixture_string_exports.js b/tests/fixture_string_exports.js new file mode 100644 index 000000000..f6439064a --- /dev/null +++ b/tests/fixture_string_exports.js @@ -0,0 +1,12 @@ +// ES2020 string export names test fixture +export const regularExport = "regular"; +const value1 = "value-1"; +const value2 = "value-2"; + +// String export names (ES2020) +export { value1 as "string-export-1" }; +export { value2 as "string-export-2" }; + +// Mixed: regular and string exports +const mixed = "mixed-value"; +export { mixed as normalName, mixed as "string-name" }; diff --git a/tests/test_string_exports.js b/tests/test_string_exports.js new file mode 100644 index 000000000..b03b19fc6 --- /dev/null +++ b/tests/test_string_exports.js @@ -0,0 +1,25 @@ +// Test ES2020 string export/import names +import { assert } from "./assert.js"; +import * as mod from "./fixture_string_exports.js"; + +// Test string import names +import { "string-export-1" as str1 } from "./fixture_string_exports.js"; +import { "string-export-2" as str2 } from "./fixture_string_exports.js"; +import { "string-name" as strMixed } from "./fixture_string_exports.js"; + +// Test regular imports still work +import { regularExport, normalName } from "./fixture_string_exports.js"; + +// Verify values +assert(str1 === "value-1"); +assert(str2 === "value-2"); +assert(strMixed === "mixed-value"); +assert(regularExport === "regular"); +assert(normalName === "mixed-value"); + +// Verify module namespace has string-named exports +assert(mod["string-export-1"] === "value-1"); +assert(mod["string-export-2"] === "value-2"); +assert(mod["string-name"] === "mixed-value"); +assert(mod.regularExport === "regular"); +assert(mod.normalName === "mixed-value"); From 65adb6bc94a99400df2ad02cf60ce3cf077ac43f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 14 Oct 2025 06:14:25 +0800 Subject: [PATCH 2/5] Enable test262 arbitrary-module-namespace-names feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added another export case, export name as string: ```js export { "☿" } from ... ``` --- quickjs.c | 12 +++++++++--- test262.conf | 2 +- test262_errors.txt | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/quickjs.c b/quickjs.c index 9fa431161..1c7c61633 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29323,11 +29323,17 @@ static __exception int js_parse_export(JSParseState *s) case '{': first_export = m->export_entries_count; while (s->token.val != '}') { - if (!token_is_ident(s->token.val)) { - js_parse_error(s, "identifier expected"); + if (token_is_ident(s->token.val)) { + local_name = JS_DupAtom(ctx, s->token.u.ident.atom); + } else if (s->token.val == TOK_STRING) { + local_name = JS_ValueToAtom(ctx, s->token.u.str.str); + if (local_name == JS_ATOM_NULL) { + return -1; + } + } else { + js_parse_error(s, "identifier or string expected"); return -1; } - local_name = JS_DupAtom(ctx, s->token.u.ident.atom); export_name = JS_ATOM_NULL; if (next_token(s)) goto fail; diff --git a/test262.conf b/test262.conf index 6f756f1bb..130355a9e 100644 --- a/test262.conf +++ b/test262.conf @@ -48,7 +48,7 @@ __proto__ __setter__ AggregateError align-detached-buffer-semantics-with-web-reality -arbitrary-module-namespace-names=skip +arbitrary-module-namespace-names array-find-from-last array-grouping Array.fromAsync diff --git a/test262_errors.txt b/test262_errors.txt index 69224ccc6..5801f4cd5 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -79,6 +79,9 @@ test262/test/language/expressions/in/private-field-invalid-assignment-target.js: test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: strict mode: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: strict mode: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true +test262/test/language/module-code/export-expname-from-as-unpaired-surrogate.js:19: unexpected error type: Test262: This statement should not be evaluated. +test262/test/language/module-code/export-expname-string-binding.js:19: unexpected error type: Test262: This statement should not be evaluated. +test262/test/language/module-code/export-expname-unpaired-surrogate.js:19: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: SyntaxError: invalid property name test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: strict mode: SyntaxError: invalid property name From 93ca3a15fe0f2df362ff40e2f886654224c37e46 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 15 Oct 2025 13:18:42 +0800 Subject: [PATCH 3/5] reject export names with unmatched surrogate pairs --- quickjs.c | 22 ++++++++++++++++++++++ test262_errors.txt | 2 -- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/quickjs.c b/quickjs.c index 1c7c61633..1ddfe80c5 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29290,6 +29290,23 @@ static __exception JSAtom js_parse_from_clause(JSParseState *s) return module_name; } +static bool has_unmatched_surrogate(const uint16_t *s, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) { + if (is_lo_surrogate(s[i])) + return true; + if (!is_hi_surrogate(s[i])) + continue; + if (++i == n) + return true; + if (!is_lo_surrogate(s[i])) + return true; + } + return false; +} + static __exception int js_parse_export(JSParseState *s) { JSContext *ctx = s->ctx; @@ -29343,6 +29360,11 @@ static __exception int js_parse_export(JSParseState *s) if (token_is_ident(s->token.val)) { export_name = JS_DupAtom(ctx, s->token.u.ident.atom); } else if (s->token.val == TOK_STRING) { + JSString *p = JS_VALUE_GET_STRING(s->token.u.str.str); + if (p->is_wide_char && has_unmatched_surrogate(str16(p), p->len)) { + js_parse_error(s, "illegal export name"); + return -1; + } export_name = JS_ValueToAtom(ctx, s->token.u.str.str); if (export_name == JS_ATOM_NULL) { return -1; diff --git a/test262_errors.txt b/test262_errors.txt index 5801f4cd5..d667e3a46 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -79,9 +79,7 @@ test262/test/language/expressions/in/private-field-invalid-assignment-target.js: test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: strict mode: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: strict mode: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true -test262/test/language/module-code/export-expname-from-as-unpaired-surrogate.js:19: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/module-code/export-expname-string-binding.js:19: unexpected error type: Test262: This statement should not be evaluated. -test262/test/language/module-code/export-expname-unpaired-surrogate.js:19: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: SyntaxError: invalid property name test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: strict mode: SyntaxError: invalid property name From 2a0eca156faedbe785576aae0da6bd4d047b93ba Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 15 Oct 2025 16:46:42 +0800 Subject: [PATCH 4/5] don't allow string bindings without from clause --- quickjs.c | 5 +++++ test262_errors.txt | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 1ddfe80c5..286be5777 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29339,6 +29339,7 @@ static __exception int js_parse_export(JSParseState *s) switch(tok) { case '{': first_export = m->export_entries_count; + bool has_string_binding = false; while (s->token.val != '}') { if (token_is_ident(s->token.val)) { local_name = JS_DupAtom(ctx, s->token.u.ident.atom); @@ -29347,6 +29348,7 @@ static __exception int js_parse_export(JSParseState *s) if (local_name == JS_ATOM_NULL) { return -1; } + has_string_binding = true; } else { js_parse_error(s, "identifier or string expected"); return -1; @@ -29409,6 +29411,9 @@ static __exception int js_parse_export(JSParseState *s) me->export_type = JS_EXPORT_TYPE_INDIRECT; me->u.req_module_idx = idx; } + } else if (has_string_binding) { + // Without 'from' clause, string literals cannot be used as local binding names + return js_parse_error(s, "string export name only allowed with 'from' clause"); } break; case '*': diff --git a/test262_errors.txt b/test262_errors.txt index d667e3a46..69224ccc6 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -79,7 +79,6 @@ test262/test/language/expressions/in/private-field-invalid-assignment-target.js: test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: strict mode: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true test262/test/language/expressions/object/computed-property-name-topropertykey-before-value-evaluation.js:31: strict mode: Test262Error: Expected SameValue(«"bad"», «"ok"») to be true -test262/test/language/module-code/export-expname-string-binding.js:19: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: SyntaxError: invalid property name test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: strict mode: SyntaxError: invalid property name From 24a2edf38fa5c0895eaa40c4649c1083a1b039f3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 16 Oct 2025 09:06:53 +0800 Subject: [PATCH 5/5] fix styling and apply other suggestions --- quickjs.c | 17 +++++------------ tests.conf | 1 + tests/test_string_exports.js | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/quickjs.c b/quickjs.c index 286be5777..133cc1189 100644 --- a/quickjs.c +++ b/quickjs.c @@ -29345,13 +29345,11 @@ static __exception int js_parse_export(JSParseState *s) local_name = JS_DupAtom(ctx, s->token.u.ident.atom); } else if (s->token.val == TOK_STRING) { local_name = JS_ValueToAtom(ctx, s->token.u.str.str); - if (local_name == JS_ATOM_NULL) { + if (local_name == JS_ATOM_NULL) return -1; - } has_string_binding = true; } else { - js_parse_error(s, "identifier or string expected"); - return -1; + return js_parse_error(s, "identifier or string expected"); } export_name = JS_ATOM_NULL; if (next_token(s)) @@ -29421,7 +29419,6 @@ static __exception int js_parse_export(JSParseState *s) /* export ns from */ if (next_token(s)) return -1; - if (token_is_ident(s->token.val)) { export_name = JS_DupAtom(ctx, s->token.u.ident.atom); } else if (s->token.val == TOK_STRING) { @@ -29430,10 +29427,8 @@ static __exception int js_parse_export(JSParseState *s) return -1; } } else { - js_parse_error(s, "identifier or string expected"); - return -1; + return js_parse_error(s, "identifier or string expected"); } - if (next_token(s)) goto fail1; module_name = js_parse_from_clause(s); @@ -29611,12 +29606,10 @@ static __exception int js_parse_import(JSParseState *s) import_name = JS_DupAtom(ctx, s->token.u.ident.atom); } else if (s->token.val == TOK_STRING) { import_name = JS_ValueToAtom(ctx, s->token.u.str.str); - if (import_name == JS_ATOM_NULL) { + if (import_name == JS_ATOM_NULL) return -1; - } } else { - js_parse_error(s, "identifier or string expected expected"); - return -1; + return js_parse_error(s, "identifier or string expected expected"); } local_name = JS_ATOM_NULL; if (next_token(s)) diff --git a/tests.conf b/tests.conf index b006e1666..7a2afc6f6 100644 --- a/tests.conf +++ b/tests.conf @@ -8,3 +8,4 @@ tests/empty.js tests/fixture_cyclic_import.js tests/microbench.js tests/test_worker_module.js +tests/fixture_string_exports.js diff --git a/tests/test_string_exports.js b/tests/test_string_exports.js index b03b19fc6..3198716e7 100644 --- a/tests/test_string_exports.js +++ b/tests/test_string_exports.js @@ -11,15 +11,15 @@ import { "string-name" as strMixed } from "./fixture_string_exports.js"; import { regularExport, normalName } from "./fixture_string_exports.js"; // Verify values -assert(str1 === "value-1"); -assert(str2 === "value-2"); -assert(strMixed === "mixed-value"); -assert(regularExport === "regular"); -assert(normalName === "mixed-value"); +assert(str1, "value-1"); +assert(str2, "value-2"); +assert(strMixed, "mixed-value"); +assert(regularExport, "regular"); +assert(normalName, "mixed-value"); // Verify module namespace has string-named exports -assert(mod["string-export-1"] === "value-1"); -assert(mod["string-export-2"] === "value-2"); -assert(mod["string-name"] === "mixed-value"); -assert(mod.regularExport === "regular"); -assert(mod.normalName === "mixed-value"); +assert(mod["string-export-1"], "value-1"); +assert(mod["string-export-2"], "value-2"); +assert(mod["string-name"], "mixed-value"); +assert(mod.regularExport, "regular"); +assert(mod.normalName, "mixed-value");