From 52e94d4f8a585d95312ccd4f7792f6ee88b1b9ce Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 20:12:42 -0800 Subject: [PATCH] JavaScript: compiling a pattern charges the room its lists reserve - The compiler charges each working list's added capacity before it grows (capacity doubles), the code that joins alternatives, and the flattened code with its stack, instead of an estimate from instruction and fragment counts. - The scan for capturing groups and the syntax check before compiling each take one unit of fuel per unit of the pattern. - Bytes a dropped HeldBytes released leave the heap's total at the next limit check, not at the next collection. - The progress line lists the deletable RegExp.prototype flag properties among the test262 failures. --- crates/js/src/interp.rs | 20 +++- crates/js/src/regexp_builtins.rs | 3 +- crates/js/src/regexp_vm.rs | 177 +++++++++++++++++++------------ crates/js/src/text_tests.rs | 55 +++++++++- docs/target-app-parity.md | 2 +- 5 files changed, 178 insertions(+), 79 deletions(-) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index fb4e607b5..895630fd7 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -170,15 +170,18 @@ pub(crate) struct Interp { pub(crate) held: Rc, } -/// The bytes all live [`HeldBytes`] hold. +/// The bytes all live [`HeldBytes`] hold, and the bytes dropped holders released that the heap's +/// total still counts. #[derive(Default)] pub(crate) struct HeldCount { now: std::cell::Cell, + released: std::cell::Cell, } /// Bytes that native code holds in Rust memory while it works (a matcher's state and stack, a -/// replacement being built, a pattern being compiled), counted against the heap limit. A -/// collection adds them to the live total until the holder is dropped. +/// replacement being built, a pattern being compiled), counted against the heap limit. They are +/// in the heap's total, and in the live total of each collection, until the holder is dropped; +/// the heap's total gives them back before the next limit check. pub(crate) struct HeldBytes { count: Rc, bytes: usize, @@ -209,6 +212,7 @@ impl HeldBytes { impl Drop for HeldBytes { fn drop(&mut self) { self.count.now.set(self.count.now.get().saturating_sub(self.bytes)); + self.count.released.set(self.count.released.get().saturating_add(self.bytes)); } } @@ -439,6 +443,7 @@ impl Interp { fn tick(&mut self) -> JsResult<()> { self.use_fuel()?; + self.settle_held(); if self.heap.bytes > self.gc_threshold { self.collect_garbage(); if self.heap.bytes > self.limits.heap_bytes { @@ -452,6 +457,7 @@ impl Interp { /// Ends the run with the heap limit when `bytes` more would not fit in it after a /// collection. Everything the caller still needs must be reachable from the roots. pub(crate) fn reserve_heap(&mut self, bytes: usize) -> JsResult<()> { + self.settle_held(); if self.heap.bytes.saturating_add(bytes) <= self.limits.heap_bytes { return Ok(()); } @@ -463,6 +469,12 @@ impl Interp { Ok(()) } + /// Takes the bytes that dropped [`HeldBytes`] released off the heap's total. + fn settle_held(&mut self) { + let released = self.held.released.replace(0); + self.heap.bytes = self.heap.bytes.saturating_sub(released); + } + /// Collects garbage now and sets when the next collection runs. #[cfg(test)] pub(crate) fn collect_for_room(&mut self) { @@ -497,6 +509,8 @@ impl Interp { roots.objects.extend(self.intrinsics.all()); roots.envs.push(self.global_env); self.heap.collect(&roots); + // The collection counts the live total again, so bytes already released are not in it. + self.held.released.set(0); self.heap.bytes = self.heap.bytes.saturating_add(self.held.now.get()); } diff --git a/crates/js/src/regexp_builtins.rs b/crates/js/src/regexp_builtins.rs index 2ecadafd8..8733ded43 100644 --- a/crates/js/src/regexp_builtins.rs +++ b/crates/js/src/regexp_builtins.rs @@ -35,7 +35,7 @@ pub(crate) fn regexp_of(interp: &Interp, v: &Value) -> Option<(ObjId, Rc) } /// Checks and compiles a pattern, throwing a `SyntaxError` for a pattern or flags that are not -/// valid. What compiling holds is held against the heap limit as it grows. The compiled form's +/// valid. The check takes one unit of fuel for each unit of the pattern. What compiling holds is held against the heap limit as it grows. The compiled form's /// bytes are then reserved together with `extra` more and added to the heap, so a caller that /// stores the result in an object it allocates next reserves that object here. fn compile_regex(interp: &mut Interp, pattern: &[u16], flags: &[u16], extra: usize) -> JsResult> { @@ -44,6 +44,7 @@ fn compile_regex(interp: &mut Interp, pattern: &[u16], flags: &[u16], extra: usi Err(m) => return Err(interp.throw(ErrorType::Syntax, &m)), }; let depth = interp.options.max_depth; + use_fuel_n(interp, pattern.len() as u64)?; if let Err(m) = crate::regexp::check(pattern, depth) { return Err(interp.throw(ErrorType::Syntax, &format!("invalid regular expression: {m}"))); } diff --git a/crates/js/src/regexp_vm.rs b/crates/js/src/regexp_vm.rs index 26b0b37ef..c0c557ea7 100644 --- a/crates/js/src/regexp_vm.rs +++ b/crates/js/src/regexp_vm.rs @@ -328,10 +328,12 @@ impl Frags { } /// The instructions of `top` with every fragment in place, written with a loop, with room for - /// one more. + /// one more. Its stack holds at most one entry for each fragment and two more, so it never + /// grows past [`flatten_bytes`]. fn flatten(&self, top: &[Item]) -> Vec { let mut out = Vec::with_capacity(self.len_of(top).saturating_add(1)); - let mut stack: Vec<(&[Item], usize)> = vec![(top, 0)]; + let mut stack: Vec<(&[Item], usize)> = Vec::with_capacity(self.items.len().saturating_add(2)); + stack.push((top, 0)); while let Some((items, i)) = stack.pop() { let Some(item) = items.get(i) else { continue }; stack.push((items, i + 1)); @@ -365,12 +367,55 @@ impl Ctx { } } -/// Adds `inst` to `ctx`, counting it in `emitted`. -fn emit(emitted: &mut usize, ctx: &mut Ctx, inst: Inst) { - *emitted += 1; +/// Adds `inst` to `ctx`. +fn emit(ctx: &mut Ctx, inst: Inst) { ctx.cur.push(Item::Inst(inst)); } +/// The bytes of one item of code being compiled. +pub(crate) const ITEM_BYTES: usize = size_of::(); + +/// The bytes of one instruction of compiled code. +pub(crate) const INST_BYTES: usize = size_of::(); + +/// Charges `bytes` that compiling holds. +fn hold(bytes: usize, charge: &mut dyn FnMut(Cost) -> JsResult<()>) -> Result<(), CompileFail> { + if bytes == 0 { + return Ok(()); + } + charge(Cost::Heap(bytes)).map_err(CompileFail::Abrupt) +} + +/// Makes room in `list` for `extra` more entries. When it must grow, its capacity at least +/// doubles, and the bytes of the added capacity are charged before it is reserved. +fn room(list: &mut Vec, extra: usize, charge: &mut dyn FnMut(Cost) -> JsResult<()>) -> Result<(), CompileFail> { + let need = list.len().saturating_add(extra); + let cap = list.capacity(); + if need <= cap { + return Ok(()); + } + let new_cap = need.max(cap.saturating_mul(2)); + hold(size_of::().saturating_mul(new_cap - cap), charge)?; + list.reserve_exact(new_cap - list.len()); + Ok(()) +} + +/// The bytes `join` holds for `alts` alternatives besides the fragments it adds: the code that +/// joins them and their lengths. +fn join_bytes(alts: usize) -> usize { + if alts <= 1 { + return 0; + } + alts.saturating_mul(2 * ITEM_BYTES + size_of::()) +} + +/// The bytes flattening holds: `instructions` and one more, and its stack for `fragments`. +fn flatten_bytes(instructions: usize, fragments: usize) -> usize { + INST_BYTES + .saturating_mul(instructions.saturating_add(1)) + .saturating_add(size_of::<(&[Item], usize)>().saturating_mul(fragments.saturating_add(2))) +} + fn offset(n: usize) -> Result { i32::try_from(n).map_err(|_| CompileFail::Syntax("regular expression too large".into())) } @@ -593,81 +638,62 @@ fn class(pattern: &[u16], mut i: usize, work: &mut u64, charge: &mut dyn FnMut(C } } -/// At least the bytes a class escape's set (`\d`, `\S`, ...) holds. -const ESCAPE_SET_BYTES: usize = size_of::() + 16 * size_of::<(u16, u16)>(); +/// At least the bytes of a class escape's ranges (`\d`, `\S`, ...). +const ESCAPE_RANGES_BYTES: usize = 16 * size_of::<(u16, u16)>(); /// The bytes a character class's ranges hold. fn set_bytes(set: &CharSet) -> usize { size_of::<(u16, u16)>().saturating_mul(set.ranges.capacity()) } -/// The bytes compiling holds: each instruction as an item and in the flattened code, each -/// fragment with its item, and the classes. -pub(crate) fn compile_bytes(instructions: usize, fragments: usize, sets_size: usize) -> usize { - instructions - .saturating_mul(size_of::() + size_of::()) - .saturating_add(fragments.saturating_mul(size_of::() + 32)) - .saturating_add(sets_size) -} - -/// Compiles `pattern`. Groups may nest `max_depth` deep. `charge` takes fuel for the work done, and -/// heap bytes for what compiling holds before it grows: the instructions, fragments and classes -/// made so far (with the two instructions each `|` adds), the groups still open and the ranges of -/// the class being read. An error from `charge` stops compiling. +/// Compiles `pattern`. Groups may nest `max_depth` deep. `charge` takes fuel for the work done, +/// counting each unit of the scan for capturing groups and of the compile itself, and heap bytes +/// for what compiling holds before it is made: the capacity each list reserves as it grows, the +/// ranges of each class, and the code that joins alternatives and the flattened code. An error +/// from `charge` stops compiling. pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: &mut dyn FnMut(Cost) -> JsResult<()>) -> Result { + let mut work: u64 = 0; + add_work(&mut work, pattern.len() as u64, charge)?; let total_groups = count_groups(pattern); + hold(size_of::(), charge)?; let mut sets: Vec = Vec::new(); - let mut sets_size = 0usize; let mut stack: Vec = vec![Ctx::new(Kind::Top, (0, 0))]; let mut frags = Frags::default(); let mut groups: u32 = 0; let mut loops: u32 = 0; let mut i = 0; - let mut work: u64 = 0; - // The instructions made so far, counting the `Split` and `Jump` each `|` adds when it is joined. - let mut emitted = 0usize; - // The fragments the joins of the groups still open will add, one for each `|` read in them. - let mut joined = 0usize; - // The heap bytes charged so far. - let mut held = 0usize; let canon = |u: u16| if flags.ignore_case { canonicalize(u) } else { u }; while let Some(&u) = pattern.get(i) { add_work(&mut work, 1, charge)?; - // One step adds at most three instructions, two fragments, one open group and one class - // escape's set (a class charges its own ranges), so their bytes are charged before the - // step makes them. - let now = compile_bytes(emitted + 3, frags.items.len() + joined + 2, sets_size.saturating_add(ESCAPE_SET_BYTES)) - .saturating_add(size_of::().saturating_mul(stack.len() + 1)); - if now > held { - charge(Cost::Heap(now - held)).map_err(CompileFail::Abrupt)?; - held = now; - } + // One step adds at most one open group, one class, one alternative and four items to the + // current alternative, so those lists have room for them before the step. + room(&mut stack, 1, charge)?; + room(&mut sets, 1, charge)?; let ctx = stack.last_mut().ok_or("unmatched ')'")?; + room(&mut ctx.cur, 4, charge)?; + room(&mut ctx.alts, 1, charge)?; let start = ctx.cur.len(); let before = groups; match u { 0x5E | 0x24 => { - emit(&mut emitted, ctx, if u == 0x5E { Inst::LineStart } else { Inst::LineEnd }); + emit(ctx, if u == 0x5E { Inst::LineStart } else { Inst::LineEnd }); ctx.atom = None; i += 1; } 0x7C => { - emitted += 2; - joined += 1; let cur = std::mem::take(&mut ctx.cur); ctx.alts.push(cur); ctx.atom = None; i += 1; } 0x2E => { - emit(&mut emitted, ctx, Inst::Any); + emit(ctx, Inst::Any); ctx.atom = Some((start, before)); i += 1; } 0x5B => { let (set, next) = class(pattern, i, &mut work, charge)?; - emit(&mut emitted, ctx, Inst::Set(u32::try_from(sets.len()).map_err(|_| "too many classes")?)); - sets_size = sets_size.saturating_add(size_of::() + set_bytes(&set)); + emit(ctx, Inst::Set(u32::try_from(sets.len()).map_err(|_| "too many classes")?)); sets.push(set); ctx.atom = Some((start, before)); i = next; @@ -695,27 +721,32 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & } let Some(mut done) = stack.pop() else { return Err("unmatched ')'".into()) }; let cur = std::mem::take(&mut done.cur); - // The fragments counted for this group's `|`s become real ones in the join. - joined = joined.saturating_sub(done.alts.len()); done.alts.push(cur); + // Joining adds a fragment for each alternative when there are several, and the + // group's body is one more. + let alts = done.alts.len(); + hold(join_bytes(alts), charge)?; + room(&mut frags.items, alts + 1, charge)?; + room(&mut frags.lens, alts + 1, charge)?; let body = join(std::mem::take(&mut done.alts), &mut frags)?; work += body.len() as u64; let body_len = frags.len_of(&body); let body = frags.add(body); let (start, before) = done.opened_at; let parent = stack.last_mut().ok_or("unmatched ')'")?; + room(&mut parent.cur, 3, charge)?; match done.kind { Kind::Capture(n) => { - emit(&mut emitted, parent, Inst::GroupStart(n)); + emit(parent, Inst::GroupStart(n)); parent.cur.push(body); - emit(&mut emitted, parent, Inst::GroupEnd(n)); + emit(parent, Inst::GroupEnd(n)); } Kind::Group | Kind::Top => parent.cur.push(body), Kind::Look(negative) => { let end = offset(body_len + 2)?; - emit(&mut emitted, parent, Inst::Look { negative, end }); + emit(parent, Inst::Look { negative, end }); parent.cur.push(body); - emit(&mut emitted, parent, Inst::LookEnd); + emit(parent, Inst::LookEnd); } } parent.atom = Some((start, before)); @@ -730,7 +761,7 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & Some(b) => b, None => { // Annex B: a `{` that starts no quantifier is itself. - emit(&mut emitted, ctx, Inst::Char(canon(u))); + emit(ctx, Inst::Char(canon(u))); ctx.atom = Some((start, before)); i += 1; continue; @@ -746,23 +777,26 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & if min == 1 && max == 1 { continue; } + hold(ITEM_BYTES.saturating_mul(ctx.cur.len().saturating_sub(atom_start)), charge)?; + room(&mut frags.items, 1, charge)?; + room(&mut frags.lens, 1, charge)?; let body: Vec = ctx.cur.drain(atom_start..).collect(); work += body.len() as u64; let reg = loops; loops = loops.checked_add(1).ok_or("too many quantifiers")?; let n = frags.len_of(&body); let body = frags.add(body); - emit(&mut emitted, ctx, Inst::RepInit(reg)); - emit(&mut emitted, ctx, Inst::Rep { reg, min, max, greedy, caps: (atom_groups + 1, groups + 1), exit: offset(n + 2)? }); + emit(ctx, Inst::RepInit(reg)); + emit(ctx, Inst::Rep { reg, min, max, greedy, caps: (atom_groups + 1, groups + 1), exit: offset(n + 2)? }); ctx.cur.push(body); - emit(&mut emitted, ctx, Inst::RepEnd { reg, head: -offset(n + 1)? }); + emit(ctx, Inst::RepEnd { reg, head: -offset(n + 1)? }); } 0x5C => { let next = *pattern.get(i + 1).ok_or("\\ at end of pattern")?; let mut atom = true; match next { 0x62 | 0x42 => { - emit(&mut emitted, ctx, Inst::WordBoundary(next == 0x62)); + emit(ctx, Inst::WordBoundary(next == 0x62)); atom = false; i += 2; } @@ -774,40 +808,41 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & j += 1; } if n <= total_groups as u64 { - emit(&mut emitted, ctx, Inst::BackRef(u32::try_from(n).map_err(|_| "too many groups")?)); + emit(ctx, Inst::BackRef(u32::try_from(n).map_err(|_| "too many groups")?)); i = j; } else if next <= 0x37 { let (v, end) = octal(pattern, i + 1); - emit(&mut emitted, ctx, Inst::Char(canon(v))); + emit(ctx, Inst::Char(canon(v))); i = end; } else { - emit(&mut emitted, ctx, Inst::Char(canon(next))); + emit(ctx, Inst::Char(canon(next))); i += 2; } } 0x30 => { if pattern.get(i + 2).is_some_and(|&d| (0x30..=0x37).contains(&d)) { let (v, end) = octal(pattern, i + 1); - emit(&mut emitted, ctx, Inst::Char(canon(v))); + emit(ctx, Inst::Char(canon(v))); i = end; } else { - emit(&mut emitted, ctx, Inst::Char(0)); + emit(ctx, Inst::Char(0)); i += 2; } } _ => { + if matches!(next, 0x64 | 0x44 | 0x73 | 0x53 | 0x77 | 0x57) { + hold(ESCAPE_RANGES_BYTES, charge)?; + } if let Some(r) = escape_ranges(next) { - emit(&mut emitted, ctx, Inst::Set(u32::try_from(sets.len()).map_err(|_| "too many classes")?)); - let set = CharSet { ranges: r, negated: false }; - sets_size = sets_size.saturating_add(size_of::() + set_bytes(&set)); - sets.push(set); + emit(ctx, Inst::Set(u32::try_from(sets.len()).map_err(|_| "too many classes")?)); + sets.push(CharSet { ranges: r, negated: false }); i += 2; } else { let (atom_unit, end) = match class_atom(pattern, i)? { (ClassAtom::Unit(v), end) => (v, end), (ClassAtom::Ranges(_), end) => (next, end), }; - emit(&mut emitted, ctx, Inst::Char(canon(atom_unit))); + emit(ctx, Inst::Char(canon(atom_unit))); i = end; } } @@ -815,7 +850,7 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & ctx.atom = if atom { Some((start, before)) } else { None }; } _ => { - emit(&mut emitted, ctx, Inst::Char(canon(u))); + emit(ctx, Inst::Char(canon(u))); ctx.atom = Some((start, before)); i += 1; } @@ -827,12 +862,14 @@ pub(crate) fn compile(pattern: &[u16], flags: Flags, max_depth: usize, charge: & let Some(mut top) = stack.pop() else { return Err("unterminated group".into()) }; let cur = std::mem::take(&mut top.cur); top.alts.push(cur); - // Joining adds a fragment for each alternative, and flattening writes every instruction. - let now = compile_bytes(emitted + 1, frags.items.len() + joined + 1, sets_size); - if now > held { - charge(Cost::Heap(now - held)).map_err(CompileFail::Abrupt)?; - } + // Joining adds a fragment for each alternative when there are several, and flattening writes + // every instruction. + let alts = top.alts.len(); + hold(join_bytes(alts), charge)?; + room(&mut frags.items, alts, charge)?; + room(&mut frags.lens, alts, charge)?; let top = join(top.alts, &mut frags)?; + hold(flatten_bytes(frags.len_of(&top), frags.items.len()), charge)?; let mut insts = frags.flatten(&top); insts.push(Inst::Match); sets.shrink_to_fit(); diff --git a/crates/js/src/text_tests.rs b/crates/js/src/text_tests.rs index 649924725..809adb5f8 100644 --- a/crates/js/src/text_tests.rs +++ b/crates/js/src/text_tests.rs @@ -2,6 +2,7 @@ use proptest::prelude::*; +use crate::regexp_vm::{INST_BYTES, ITEM_BYTES}; use crate::{JsValue, LimitKind, Limits, Options, Realm, RunError, RunErrorKind}; fn run_in(realm: &mut Realm, src: &str) -> JsValue { @@ -302,18 +303,64 @@ fn compiling_a_pattern_counts_against_the_heap() { assert_eq!(run(src), JsValue::Number(131_072.0)); // The compiler stops while it reads the pattern, before the code it builds passes the room. let pattern = "[\\s\\S]a*".repeat(1000); - assert!(stopped_by(&compile_within(&pattern, crate::regexp_vm::compile_bytes(400, 0, 0), u64::MAX).0, LimitKind::Heap)); + assert!(stopped_by(&compile_within(&pattern, 400 * (ITEM_BYTES + INST_BYTES), u64::MAX).0, LimitKind::Heap)); assert!(compile_within(&pattern, usize::MAX, u64::MAX).0.is_ok()); // A literal pattern takes what its instructions hold, not the most a unit can take. - assert!(compile_within(&"a".repeat(10_000), crate::regexp_vm::compile_bytes(20_000, 0, 0), u64::MAX).0.is_ok()); + assert!(compile_within(&"a".repeat(10_000), 20_000 * (ITEM_BYTES + INST_BYTES), u64::MAX).0.is_ok()); // A class's ranges count while they are read, even when they collapse into one range. assert!(stopped_by(&compile_within(&format!("[{}]", "a".repeat(10_000)), 1000, u64::MAX).0, LimitKind::Heap)); // So do the ranges of class escapes, and each atom and range read takes fuel. let escapes = format!("[{}]", "\\w".repeat(10_000)); assert!(stopped_by(&compile_within(&escapes, 64 << 10, u64::MAX).0, LimitKind::Heap)); assert!(stopped_by(&compile_within(&escapes, usize::MAX, 20_000).0, LimitKind::Fuel)); - // The joining and flattening after the last unit count too. - assert!(stopped_by(&compile_within("ab", crate::regexp_vm::compile_bytes(1, 0, 0), u64::MAX).0, LimitKind::Heap)); + // The lists count with the room they reserve as they double: 4097 literal units hold room for + // 8192 items, and the flattened code after the last unit adds 4098 instructions. + let literal = "a".repeat(4097); + let (compiled, bytes, _) = compile_within(&literal, usize::MAX, u64::MAX); + assert!(compiled.is_ok()); + assert!(bytes >= 8192 * ITEM_BYTES + 4098 * INST_BYTES, "{bytes}"); + assert!(stopped_by(&compile_within(&literal, 8192 * ITEM_BYTES + 4000 * INST_BYTES, u64::MAX).0, LimitKind::Heap)); +} + +#[test] +fn compiling_a_pattern_takes_fuel_for_each_scan_of_it() { + // The scan for capturing groups takes a unit of fuel for each unit of the pattern, and so does + // the compile itself. + let literal = "a".repeat(100_000); + let (compiled, _, fuel) = compile_within(&literal, usize::MAX, u64::MAX); + assert!(compiled.is_ok()); + assert!(fuel >= 200_000, "{fuel}"); + assert!(stopped_by(&compile_within(&literal, usize::MAX, 150_000).0, LimitKind::Fuel)); + // The syntax check before compiling takes a unit for each unit too. + let mut realm = Realm::new(); + let interp = realm.interp(); + let (source, flags) = (crate::value::Str::from(literal.as_str()), crate::value::Str::from("")); + interp.fuel = 1 << 40; + assert!(crate::regexp_builtins::new_regexp(interp, &source, &flags).is_ok()); + let used = (1u64 << 40) - interp.fuel; + assert!(used >= 300_000, "{used}"); + interp.fuel = 250_000; + assert!(matches!(crate::regexp_builtins::new_regexp(interp, &source, &flags), Err(crate::interp::Abrupt::Limit(LimitKind::Fuel)))); +} + +#[test] +fn held_bytes_leave_the_heap_total_when_their_holder_is_dropped() { + let mut realm = Realm::new(); + let interp = realm.interp(); + interp.collect_for_room(); + let live = interp.heap.bytes; + interp.limits.heap_bytes = live + 10_000; + let mut held = crate::interp::HeldBytes::new(interp); + assert!(held.add(interp, 6000).is_ok()); + assert_eq!(interp.heap.bytes, live + 6000); + assert!(matches!(crate::interp::HeldBytes::new(interp).add(interp, 6000), Err(crate::interp::Abrupt::Limit(LimitKind::Heap)))); + drop(held); + // The next check gives the bytes back without a collection. + assert!(interp.reserve_heap(0).is_ok()); + assert_eq!(interp.heap.bytes, live); + let mut again = crate::interp::HeldBytes::new(interp); + assert!(again.add(interp, 6000).is_ok()); + assert_eq!(interp.heap.bytes, live + 6000); } /// Compiles `pattern` with no flags through a charge that allows `heap` bytes and `fuel` units, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0782b7bbf..80afed899 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -655,7 +655,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - 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`'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. Built-ins are limited to `Object`, `Function` with `call` and `apply`, `eval` and the error types. 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 these built-ins: 3,074 of 3,078 runs pass (the 4 others expect the completion values of later editions). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.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,113 of 1,169 pass for `String`, 262 of 262 for `Number`, 878 of 908 for `RegExp` and 6 of 6 for `Date`. The others need `Array` and `Boolean` methods, or 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). The ES5 language tests that can now run give 3,742 of 3,746 passing runs. Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.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,113 of 1,169 pass for `String`, 262 of 262 for `Number`, 878 of 908 for `RegExp` and 6 of 6 for `Date`. The others need `Array` and `Boolean` methods, or 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). The ES5 language tests that can now run give 3,742 of 3,746 passing runs. Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`. - **UI:** Illustrator 2026 layout restyled to measured values: - Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs. - Hint bar, contextual task bar, 19 dock panels with ≡ menus.