mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 21:51:36 +00:00
JavaScript: check identifier characters against every Unicode 16 code point
- A new test walks every code point from U+0000 to U+10FFFF, surrogates left out, and compares the start and part checks with Unicode Standard Annex #31's general-category definition of ID_Start and ID_Continue, written out in the test. The test also checks the set sizes against the totals in Unicode 16.0's DerivedCoreProperties (141,269 and 144,541). - The Other_ID_Continue test covers every code point of the property, U+136A to U+1370 and the zero width non-joiner and joiner included. - The zero width non-joiner and joiner are part of the Other_ID_Continue list in chars.rs instead of a separate case.
This commit is contained in:
@@ -35,14 +35,13 @@ pub fn is_id_start(c: u32) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// A character an identifier may hold after its first: a Unicode `ID_Continue` character, `$`,
|
||||
/// `_`, or the zero width non-joiner and joiner. `ID_Continue` adds to `ID_Start` the decimal
|
||||
/// digits (Nd), combining marks (Mn, Mc), connector punctuation (Pc) and `Other_ID_Continue`,
|
||||
/// less `Pattern_Syntax` and `Pattern_White_Space`.
|
||||
/// A character an identifier may hold after its first: a Unicode `ID_Continue` character or `$`.
|
||||
/// `ID_Continue` adds to `ID_Start` the decimal digits (Nd), combining marks (Mn, Mc), connector
|
||||
/// punctuation (Pc, which holds `_`) and `Other_ID_Continue` (which holds the zero width
|
||||
/// non-joiner and joiner), less `Pattern_Syntax` and `Pattern_White_Space`.
|
||||
pub fn is_id_part(c: u32) -> bool {
|
||||
match c {
|
||||
0x30..=0x39 => true,
|
||||
0x200C | 0x200D => true,
|
||||
_ if is_id_start(c) => true,
|
||||
0..=0x7F => false,
|
||||
_ if is_other_id_continue(c) => true,
|
||||
@@ -59,10 +58,10 @@ fn is_other_id_start(c: u32) -> bool {
|
||||
matches!(c, 0x1885 | 0x1886 | 0x2118 | 0x212E | 0x309B | 0x309C)
|
||||
}
|
||||
|
||||
/// `Other_ID_Continue` (Unicode PropList) outside ASCII, less the zero width non-joiner and joiner,
|
||||
/// which [`is_id_part`] takes on their own.
|
||||
/// `Other_ID_Continue` (Unicode PropList 16.0): characters kept in `ID_Continue` though their
|
||||
/// category is not a letter, mark, digit or connector.
|
||||
fn is_other_id_continue(c: u32) -> bool {
|
||||
matches!(c, 0xB7 | 0x387 | 0x1369..=0x1371 | 0x19DA | 0x30FB | 0xFF65)
|
||||
matches!(c, 0xB7 | 0x387 | 0x1369..=0x1371 | 0x19DA | 0x200C | 0x200D | 0x30FB | 0xFF65)
|
||||
}
|
||||
|
||||
/// Non-ASCII `Pattern_Syntax` ranges (Unicode PropList). Unicode does not change this set, and no
|
||||
|
||||
+54
-10
@@ -413,7 +413,7 @@ fn identifiers_take_the_other_id_start_and_continue_characters() {
|
||||
}
|
||||
}
|
||||
// Other_ID_Continue: may follow the first character, but cannot start a name.
|
||||
for c in [0xB7, 0x387, 0x1369, 0x1371, 0x19DA, 0x30FB, 0xFF65] {
|
||||
for c in [0xB7, 0x387].into_iter().chain(0x1369..=0x1371).chain([0x19DA, 0x200C, 0x200D, 0x30FB, 0xFF65]) {
|
||||
let ch = char::from_u32(c).unwrap_or_default();
|
||||
for src in [format!("var a{ch} = 1;"), format!("var a\\u{c:04X} = 1;"), format!("var a\\u{{{c:X}}} = 1;")] {
|
||||
ok(&src);
|
||||
@@ -445,19 +445,29 @@ fn identifiers_refuse_pattern_syntax_surrogates_and_bad_escapes() {
|
||||
assert!(err("1\u{10000};").message.contains("immediately after numeric literal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pattern_syntax_or_white_space_character_is_an_identifier_character() {
|
||||
use crate::chars::{is_id_part, is_id_start};
|
||||
// Pattern_Syntax and Pattern_White_Space as Unicode's PropList lists them; Unicode does not
|
||||
// change either set. `$` is Pattern_Syntax but an identifier character in scripts.
|
||||
/// The code point ranges of `Pattern_Syntax` and `Pattern_White_Space` as Unicode's PropList lists
|
||||
/// them; Unicode does not change either set.
|
||||
fn pattern_syntax_and_white_space() -> Vec<std::ops::RangeInclusive<u32>> {
|
||||
let pattern = "0009..000D 0020 0021..002F 003A..0040 005B..005E 0060 007B..007E 0085 00A1..00A7 00A9 00AB..00AC 00AE \
|
||||
00B0..00B1 00B6 00BB 00BF 00D7 00F7 200E..200F 2010..2027 2028..2029 2030..203E 2041..2053 2055..205E \
|
||||
2190..245F 2500..2775 2794..2BFF 2E00..2E7F 3001..3003 3008..3020 3030 FD3E..FD3F FE45..FE46";
|
||||
pattern
|
||||
.split_whitespace()
|
||||
.map(|range| {
|
||||
let (lo, hi) = range.split_once("..").unwrap_or((range, range));
|
||||
let (Ok(lo), Ok(hi)) = (u32::from_str_radix(lo, 16), u32::from_str_radix(hi, 16)) else { panic!("{range}") };
|
||||
lo..=hi
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pattern_syntax_or_white_space_character_is_an_identifier_character() {
|
||||
use crate::chars::{is_id_part, is_id_start};
|
||||
// `$` is Pattern_Syntax but an identifier character in scripts.
|
||||
let mut checked = 0;
|
||||
for range in pattern.split_whitespace() {
|
||||
let (lo, hi) = range.split_once("..").unwrap_or((range, range));
|
||||
let (Ok(lo), Ok(hi)) = (u32::from_str_radix(lo, 16), u32::from_str_radix(hi, 16)) else { panic!("{range}") };
|
||||
for c in (lo..=hi).filter(|&c| c != 0x24) {
|
||||
for range in pattern_syntax_and_white_space() {
|
||||
for c in range.filter(|&c| c != 0x24) {
|
||||
assert!(!is_id_start(c) && !is_id_part(c), "U+{c:04X}");
|
||||
checked += 1;
|
||||
}
|
||||
@@ -469,6 +479,40 @@ fn no_pattern_syntax_or_white_space_character_is_an_identifier_character() {
|
||||
assert!(!is_id_start(0x110000) && !is_id_part(0x110000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifier_characters_are_unicode_16_id_start_and_id_continue() {
|
||||
use crate::chars::{is_id_part, is_id_start};
|
||||
use unicode_general_category::{GeneralCategory as G, get_general_category};
|
||||
// Unicode Standard Annex #31: ID_Start is the categories Lu, Ll, Lt, Lm, Lo and Nl plus
|
||||
// Other_ID_Start; ID_Continue adds Mn, Mc, Nd, Pc and Other_ID_Continue; both leave out
|
||||
// Pattern_Syntax and Pattern_White_Space. Other_ID_Start and Other_ID_Continue as PropList 16.0
|
||||
// lists them.
|
||||
let other_start = [0x1885..=0x1886, 0x2118..=0x2118, 0x212E..=0x212E, 0x309B..=0x309C];
|
||||
let other_continue = [0xB7..=0xB7, 0x387..=0x387, 0x1369..=0x1371, 0x19DA..=0x19DA, 0x200C..=0x200D, 0x30FB..=0x30FB, 0xFF65..=0xFF65];
|
||||
let mut excluded = vec![false; 0x11_0000];
|
||||
for range in pattern_syntax_and_white_space() {
|
||||
for c in range {
|
||||
excluded[c as usize] = true;
|
||||
}
|
||||
}
|
||||
let (mut id_starts, mut id_continues) = (0, 0);
|
||||
for c in (0..=0x10_FFFF_u32).filter(|c| !(0xD800..=0xDFFF).contains(c)) {
|
||||
let Some(ch) = char::from_u32(c) else { panic!("U+{c:04X}") };
|
||||
let g = get_general_category(ch);
|
||||
let letter = matches!(g, G::UppercaseLetter | G::LowercaseLetter | G::TitlecaseLetter | G::ModifierLetter | G::OtherLetter | G::LetterNumber);
|
||||
let id_start = (letter || other_start.iter().any(|r| r.contains(&c))) && !excluded[c as usize];
|
||||
let continuing = matches!(g, G::NonspacingMark | G::SpacingMark | G::DecimalNumber | G::ConnectorPunctuation);
|
||||
let id_continue = (id_start || continuing || other_continue.iter().any(|r| r.contains(&c))) && !excluded[c as usize];
|
||||
id_starts += usize::from(id_start);
|
||||
id_continues += usize::from(id_continue);
|
||||
// Scripts add `$` and `_` at the start and `$` later (ECMA-262, Names and Keywords).
|
||||
assert_eq!(is_id_start(c), id_start || c == 0x24 || c == 0x5F, "start U+{c:04X}");
|
||||
assert_eq!(is_id_part(c), id_continue || c == 0x24, "part U+{c:04X}");
|
||||
}
|
||||
// The totals of ID_Start and ID_Continue in DerivedCoreProperties 16.0.
|
||||
assert_eq!((id_starts, id_continues), (141_269, 144_541));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_and_line_terminators() {
|
||||
let p = ok("a /* one\ntwo */ b");
|
||||
|
||||
@@ -727,7 +727,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785); `run --commands FILE` (or `-` for standard input) reads the steps from a JSON array or JSON Lines, past the Windows command-line limit (#3147, `apps/vectorcraft-cli/tests/it/cli.rs` `run_reads_*`, `run_refuses_malformed_commands_naming_the_step`).
|
||||
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
|
||||
- `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). This entry covers parsing; the next entry covers running scripts. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`.
|
||||
- `vectorcraft-js` identifiers, written out or as `\uXXXX` and `\u{...}` escapes, take the characters Unicode's `ID_Start` and `ID_Continue` properties allow (https://www.unicode.org/reports/tr31/), plus `$`, `_`, and after the first character the zero width non-joiner and joiner, as later ECMAScript editions define them (https://tc39.es/ecma262/#sec-names-and-keywords). `Other_ID_Start` characters such as U+2118 and `Other_ID_Continue` characters such as U+00B7 are accepted, and no `Pattern_Syntax` or `Pattern_White_Space` character is. Letter categories come from Unicode 16.0, so characters assigned in later Unicode versions are not yet identifier characters. Tests: `crates/js/src/tests.rs` (`identifiers_take_the_other_id_start_and_continue_characters`, `identifiers_refuse_pattern_syntax_surrogates_and_bad_escapes`, `no_pattern_syntax_or_white_space_character_is_an_identifier_character`).
|
||||
- `vectorcraft-js` identifiers, written out or as `\uXXXX` and `\u{...}` escapes, take the characters Unicode's `ID_Start` and `ID_Continue` properties allow (https://www.unicode.org/reports/tr31/), plus `$` and, at the start, `_`, as later ECMAScript editions define them (https://tc39.es/ecma262/#sec-names-and-keywords). `Other_ID_Start` characters such as U+2118 and `Other_ID_Continue` characters such as U+00B7 and the zero width non-joiner and joiner are accepted, and no `Pattern_Syntax` or `Pattern_White_Space` character is. Letter categories come from Unicode 16.0, so characters assigned in later Unicode versions are not yet identifier characters. Tests: `crates/js/src/tests.rs` (`identifier_characters_are_unicode_16_id_start_and_id_continue`, which checks every code point, `identifiers_take_the_other_id_start_and_continue_characters`, `identifiers_refuse_pattern_syntax_surrogates_and_bad_escapes`, `no_pattern_syntax_or_white_space_character_is_an_identifier_character`).
|
||||
- `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap, with the built-ins the next two entries list. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only the built-ins present: 4,280 of 4,286 runs pass (4 of the others expect the completion values of later editions, and 2 expect a configurable function `length`). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- `vectorcraft-js` provides the ECMAScript 5.1 built-ins `Object` with its property-descriptor functions, `create`, `keys`, `seal` and `freeze` (section 15.2), `Function.prototype.bind` (15.3.4.5), `Array` with every ES5 method (15.4), the error types (15.11), `Math` (15.8), `JSON` with revivers, replacers and indentation (15.12), and the global functions with Annex B's `escape` and `unescape` (15.1, B.2). The array methods work on array-like objects. A method visits only the indices that exist when its range is longer than 4,096 indices and more than 8 times the properties on the object and its prototypes, so `new Array(4294967295).join("")` ends at once; a shorter range is visited index by index, holes included, and each index takes fuel. `join` writes a cycle back to an array as an empty string, `JSON.stringify` throws a `TypeError` on a cycle, and deep `JSON` nesting uses no native stack. Every loop takes fuel and every result is checked against the heap limit. `cargo xtask test262` runs the suite's ES5 built-in tests that need only these built-ins: 6,798 of 6,866 runs pass (the 34 failing tests expect rules of later editions). It also fetches the `json2.js` polyfill at a pinned commit and checks that its `stringify` and `parse` agree with the built-in `JSON`. Tests: `crates/js/src/builtins_tests.rs`, `crates/js/tests/test262.rs`, `crates/js/tests/json2.rs`.
|
||||
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,379 of 1,411 pass for `String`, 298 of 298 for `Number`, 954 of 988 for `RegExp` and 8 of 8 for `Date`. The others expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
|
||||
Reference in New Issue
Block a user