diff --git a/crates/js/src/ast.rs b/crates/js/src/ast.rs index f0dd6b44b..5a4363108 100644 --- a/crates/js/src/ast.rs +++ b/crates/js/src/ast.rs @@ -175,6 +175,63 @@ pub enum Expr { /// A parenthesized expression, kept so a rule about the bare form (a label, a directive) can /// tell them apart. Paren(ExprId), + /// An XML literal (`d`), or with `list` an XMLList literal (`<>…`). + XmlLiteral { + parts: Vec, + list: bool, + }, + /// An E4X name (`@a`, `*`, `ns::a`), the key of an [`Expr::Index`] or the operand of + /// [`Expr::XmlScopeName`] and [`Expr::XmlDescendants`]. + XmlName { + attribute: bool, + qualifier: Option, + name: XmlNamePart, + }, + /// An E4X name standing alone (`@a` in a filter): looked up on the nearest XML value in scope. + XmlScopeName(ExprId), + /// `object..name`. + XmlDescendants { + object: ExprId, + name: ExprId, + }, + /// `object.(predicate)`. + XmlFilter { + object: ExprId, + predicate: ExprId, + }, +} + +/// A piece of an XML literal, in source order. +#[derive(Clone, Debug, PartialEq)] +pub enum XmlPart { + /// Markup kept as written. + Text(Vec), + /// `{expression}` in element content: XML values keep their markup, other values are escaped. + Content(ExprId), + /// `{expression}` as an attribute's value. + Attribute(ExprId), + /// `{expression}` as an element or attribute name, its string checked as an XML name. + Name(ExprId), + /// `{expression}` as a run of attributes, its string checked as `name="value"` attributes. + Attributes(ExprId), +} + +/// What stands before `::` in a qualified name. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum XmlQualifier { + /// `*::name`: any namespace. + Any, + Expr(ExprId), +} + +/// The name in an E4X name. +#[derive(Clone, Debug, PartialEq)] +pub enum XmlNamePart { + Ident(String), + /// `*`. + Star, + /// `[expression]`. + Computed(ExprId), } #[derive(Clone, Debug, PartialEq)] @@ -218,19 +275,55 @@ pub enum Stmt { Expr(ExprId), Block(Vec), Empty, - If { test: ExprId, consequent: StmtId, alternate: Option }, - DoWhile { body: StmtId, test: ExprId }, - While { test: ExprId, body: StmtId }, - For { init: Option, test: Option, update: Option, body: StmtId }, - ForIn { target: ForInTarget, object: ExprId, body: StmtId }, + If { + test: ExprId, + consequent: StmtId, + alternate: Option, + }, + DoWhile { + body: StmtId, + test: ExprId, + }, + While { + test: ExprId, + body: StmtId, + }, + For { + init: Option, + test: Option, + update: Option, + body: StmtId, + }, + /// `for (… in …)`, or with `each` the E4X `for each (… in …)`, which assigns the values. + ForIn { + target: ForInTarget, + object: ExprId, + body: StmtId, + each: bool, + }, + /// `default xml namespace = expression;`. + DefaultXmlNamespace(ExprId), Continue(Option), Break(Option), Return(Option), - With { object: ExprId, body: StmtId }, - Switch { discriminant: ExprId, cases: Vec }, - Labeled { label: Name, body: StmtId }, + With { + object: ExprId, + body: StmtId, + }, + Switch { + discriminant: ExprId, + cases: Vec, + }, + Labeled { + label: Name, + body: StmtId, + }, Throw(ExprId), - Try { block: Vec, handler: Option, finalizer: Option> }, + Try { + block: Vec, + handler: Option, + finalizer: Option>, + }, Debugger, } diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index dfb2cf9f7..f7c21a8d8 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -132,7 +132,11 @@ fn try_install(interp: &mut Interp) -> JsResult<()> { crate::string_builtins::install(interp)?; crate::number_builtins::install(interp)?; crate::regexp_builtins::install(interp)?; - crate::date_builtins::install(interp) + crate::date_builtins::install(interp)?; + if interp.options.e4x { + crate::xml_builtins::install(interp)?; + } + Ok(()) } // ---- helpers for the built-ins ---- @@ -219,7 +223,7 @@ const UNITS_PER_FUEL: usize = 16; /// against the longest string the heap limit allows, and has its bytes reserved before it is /// added. The buffer's bytes count toward the heap until `finish` or until the native call that /// made the builder returns. Everything the caller still needs must be reachable from the roots. -struct Builder { +pub(crate) struct Builder { units: Vec, /// Code units already reserved and charged to the heap. charged: usize, @@ -228,7 +232,7 @@ struct Builder { } impl Builder { - fn new() -> Self { + pub(crate) fn new() -> Self { Builder { units: Vec::new(), charged: 0, unpaid: 0 } } @@ -260,7 +264,7 @@ impl Builder { Ok(()) } - fn push_units(&mut self, interp: &mut Interp, units: &[u16]) -> JsResult<()> { + pub(crate) fn push_units(&mut self, interp: &mut Interp, units: &[u16]) -> JsResult<()> { self.make_room(interp, units.len())?; self.units.extend_from_slice(units); Ok(()) @@ -268,7 +272,7 @@ impl Builder { /// Appends `units` `count` times. Nothing is written for an empty `units`, however large /// `count` is. - fn push_repeated(&mut self, interp: &mut Interp, units: &[u16], count: u64) -> JsResult<()> { + pub(crate) fn push_repeated(&mut self, interp: &mut Interp, units: &[u16], count: u64) -> JsResult<()> { if units.is_empty() || count == 0 { return Ok(()); } @@ -280,14 +284,14 @@ impl Builder { Ok(()) } - fn push_str(&mut self, interp: &mut Interp, s: &str) -> JsResult<()> { + pub(crate) fn push_str(&mut self, interp: &mut Interp, s: &str) -> JsResult<()> { let units: Vec = s.encode_utf16().collect(); self.push_units(interp, &units) } /// The string built, whose bytes were charged as it grew. From here on the heap counts them /// as the string's. - fn finish(self, interp: &mut Interp) -> Value { + pub(crate) fn finish(self, interp: &mut Interp) -> Value { interp.held_bytes = interp.held_bytes.saturating_sub(2 * self.charged); Value::String(Str::from_units(self.units)) } diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs index 9681d0168..3092530d4 100644 --- a/crates/js/src/bytecode.rs +++ b/crates/js/src/bytecode.rs @@ -145,6 +145,35 @@ pub(crate) enum Op { /// Pushes the completion value, which a `finally` block in script and eval code keeps and /// sets back with `SetCompletion` when it ends normally. GetCompletion, + /// `[qualifier] name` → an E4X name object. + XmlMakeName { + attribute: bool, + qualified: bool, + }, + /// `object name` → the descendants that match. + XmlDescendants, + /// `value` → the string an embedded expression puts in a literal (0 content, 1 attribute + /// value, 2 name, 3 run of attributes). + XmlEscape(u8), + /// The `parts` strings → the XML or XMLList they spell. + XmlLiteral { + parts: u32, + list: bool, + }, + /// `name` → the value of the name on the nearest XML value in scope. + XmlScopeGet, + /// `object` → the filter's state. + XmlFilterStart, + /// `state` → `state item`, or jumps with `state` when the items are used up. + XmlFilterNext(u32), + /// `state value` → `state`, the current item kept when the value is truthy. + XmlFilterTest, + /// `state` → the XMLList of the items kept. + XmlFilterEnd, + /// `iterator name` → `iterator value`: the value under the name a `for each` got. + ForEachValue, + /// `value` → nothing: sets the default XML namespace of the running code. + SetDefaultNamespace, } /// What code is: a function body, a script, or eval code. @@ -274,7 +303,13 @@ fn effect(op: Op, finally_tables: &[Vec]) -> Result { let table = finally_tables.get(t as usize).ok_or_else(|| format!("finally table {t} out of range"))?; Effect { pops: 2, next: Some(0), jumps: table.iter().map(|&target| (target, 1)).collect() } } - Op::ForInNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] }, + Op::ForInNext(t) | Op::XmlFilterNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] }, + Op::XmlMakeName { qualified, .. } => e(1 + u32::from(qualified), 1), + Op::XmlDescendants | Op::XmlFilterTest => e(2, 1), + Op::XmlEscape(_) | Op::XmlScopeGet | Op::XmlFilterStart | Op::XmlFilterEnd => e(1, 1), + Op::XmlLiteral { parts, .. } => e(parts, 1), + Op::ForEachValue => e(2, 2), + Op::SetDefaultNamespace => e(1, 0), }) } diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 34e64814b..071e99a60 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -18,7 +18,7 @@ use std::rc::Rc; use crate::ast::{ AssignOp, Ast, BinaryOp, Expr, ExprId, ForInTarget, ForInit, FunctionId, FunctionKind, LogicalOp, PropertyValue, Stmt, StmtId, UnaryOp, UpdateOp, - VarDecl, + VarDecl, XmlNamePart, XmlPart, XmlQualifier, }; use crate::bytecode::{Code, CodeKind, Handler, Op, Source, fallthrough_effect, verify}; use crate::error::{SyntaxError, TOO_DEEP}; @@ -276,7 +276,14 @@ impl<'a> FnCompiler<'a> { } work.extend(block.iter().rev()); } - Stmt::Expr(_) | Stmt::Empty | Stmt::Continue(_) | Stmt::Break(_) | Stmt::Return(_) | Stmt::Throw(_) | Stmt::Debugger => {} + Stmt::Expr(_) + | Stmt::Empty + | Stmt::Continue(_) + | Stmt::Break(_) + | Stmt::Return(_) + | Stmt::Throw(_) + | Stmt::Debugger + | Stmt::DefaultXmlNamespace(_) => {} } } Ok(out) @@ -348,6 +355,11 @@ impl<'a> FnCompiler<'a> { Ok(()) } Stmt::Try { block, handler, finalizer } => self.try_stmt(block, handler.as_ref(), finalizer.as_deref()), + Stmt::DefaultXmlNamespace(e) => { + self.expr(*e)?; + self.emit(Op::SetDefaultNamespace); + Ok(()) + } } } @@ -467,14 +479,14 @@ impl<'a> FnCompiler<'a> { } self.jump(Op::Jump, head); } - Some(Stmt::ForIn { target, object, body }) => return self.for_in(target, *object, *body, labels), + Some(Stmt::ForIn { target, object, body, each }) => return self.for_in(target, *object, *body, labels, *each), _ => return internal("not a loop"), } self.bind(brk); Ok(()) } - fn for_in(&mut self, target: &ForInTarget, object: ExprId, body: StmtId, labels: Vec) -> CResult<()> { + fn for_in(&mut self, target: &ForInTarget, object: ExprId, body: StmtId, labels: Vec, each: bool) -> CResult<()> { if let ForInTarget::Var(d) = target { self.var_decls(std::slice::from_ref(d))?; } @@ -485,6 +497,9 @@ impl<'a> FnCompiler<'a> { let brk = self.new_label(); self.bind(head); self.jump(Op::ForInNext, brk); + if each { + self.emit(Op::ForEachValue); + } match target { ForInTarget::Var(d) => { let n = self.var_name(&d.name.name); @@ -827,10 +842,83 @@ impl<'a> FnCompiler<'a> { } } Expr::Paren(inner) => self.expr(*inner)?, + Expr::XmlLiteral { parts, list } => self.xml_literal(parts, *list)?, + Expr::XmlName { attribute, qualifier, name } => self.xml_name(*attribute, *qualifier, name)?, + Expr::XmlScopeName(name) => { + self.expr(*name)?; + self.at(self.ast.expr_span(id).start); + self.emit(Op::XmlScopeGet); + } + Expr::XmlDescendants { .. } | Expr::XmlFilter { .. } => self.chain(id)?, } Ok(()) } + fn xml_literal(&mut self, parts: &[XmlPart], list: bool) -> CResult<()> { + for part in parts { + match part { + XmlPart::Text(units) => { + let i = self.string(units); + self.emit(Op::String(i)); + } + XmlPart::Content(e) | XmlPart::Attribute(e) | XmlPart::Name(e) | XmlPart::Attributes(e) => { + self.expr(*e)?; + self.emit(Op::XmlEscape(match part { + XmlPart::Content(_) => 0, + XmlPart::Attribute(_) => 1, + XmlPart::Name(_) => 2, + _ => 3, + })); + } + } + } + self.emit(Op::XmlLiteral { parts: len_u32(parts.len()), list }); + Ok(()) + } + + fn xml_name(&mut self, attribute: bool, qualifier: Option, name: &XmlNamePart) -> CResult<()> { + match qualifier { + Some(XmlQualifier::Any) => self.emit(Op::Null), + Some(XmlQualifier::Expr(e)) => self.expr(e)?, + None => {} + } + match name { + XmlNamePart::Ident(n) => { + let i = self.string(&n.encode_utf16().collect::>()); + self.emit(Op::String(i)); + } + XmlNamePart::Star => { + let i = self.string(&[u16::from(b'*')]); + self.emit(Op::String(i)); + } + XmlNamePart::Computed(e) => self.expr(*e)?, + } + self.emit(Op::XmlMakeName { attribute, qualified: qualifier.is_some() }); + Ok(()) + } + + /// `object.(predicate)` with the object already on the stack: each item is tested with itself + /// as the innermost scope, in list order. + fn xml_filter(&mut self, predicate: ExprId) -> CResult<()> { + self.emit(Op::XmlFilterStart); + let head = self.new_label(); + let done = self.new_label(); + self.bind(head); + let depth = self.depth; + self.jump(Op::XmlFilterNext, done); + self.emit(Op::PushWith); + self.env_depth += 1; + self.expr(predicate)?; + self.emit(Op::PopEnv); + self.env_depth -= 1; + self.emit(Op::XmlFilterTest); + self.jump(Op::Jump, head); + self.bind(done); + self.depth = depth; + self.emit(Op::XmlFilterEnd); + Ok(()) + } + fn args(&mut self, args: &[ExprId]) -> CResult<()> { for a in args { self.expr(*a)?; @@ -869,7 +957,9 @@ impl<'a> FnCompiler<'a> { let mut cur = id; loop { match self.ast.expr(cur) { - Some(Expr::Member { object, .. } | Expr::Index { object, .. }) => { + Some( + Expr::Member { object, .. } | Expr::Index { object, .. } | Expr::XmlDescendants { object, .. } | Expr::XmlFilter { object, .. }, + ) => { suffixes.push(cur); cur = self.ast.unparenthesized(*object); } @@ -910,6 +1000,15 @@ impl<'a> FnCompiler<'a> { self.at(span.start); self.emit(if next_is_call { Op::GetIndexThis } else { Op::GetIndex }); } + Some(Expr::XmlDescendants { name, .. }) => { + self.expr(*name)?; + self.at(span.start); + self.emit(Op::XmlDescendants); + } + Some(Expr::XmlFilter { predicate, .. }) => { + self.at(span.start); + self.xml_filter(*predicate)?; + } Some(Expr::Call { args, .. }) => { let innermost = j + 1 == count; let has_this = if innermost { @@ -1163,9 +1262,15 @@ impl<'a> FnCompiler<'a> { for &(i, label) in &self.patches { let Some(target) = resolve(&self.labels, label) else { return internal("jump to an unplaced label") }; match self.ops.get_mut(i) { - Some(Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) | Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) | Op::ForInNext(t)) => { - *t = target - } + Some( + Op::Jump(t) + | Op::JumpIfFalse(t) + | Op::JumpIfTrue(t) + | Op::JumpIfFalseKeep(t) + | Op::JumpIfTrueKeep(t) + | Op::ForInNext(t) + | Op::XmlFilterNext(t), + ) => *t = target, _ => return internal("patching an instruction that is not a jump"), } } diff --git a/crates/js/src/heap.rs b/crates/js/src/heap.rs index 0dbe94988..dea53c557 100644 --- a/crates/js/src/heap.rs +++ b/crates/js/src/heap.rs @@ -228,12 +228,84 @@ pub(crate) enum Class { Math, /// The `JSON` object (section 15.12). Json, + /// An XML value (ECMA-357 section 9.1). + Xml(Box), + /// An XMLList value (ECMA-357 section 9.2). + XmlList(Box), + Namespace(Box), + /// A QName; `attribute` marks an attribute name (ECMA-357 sections 13.3 and 9.3), and + /// `reference` a name made by E4X syntax, which only an XML value can be accessed with. + QName { + name: Box, + attribute: bool, + reference: bool, + }, + /// A `.( )` filter in progress. + XmlFilter(Box), +} + +/// What an XML value is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum XmlKind { + Element, + Attribute, + Text, + Comment, + Pi, +} + +/// A qualified name. `uri` is `None` for a name that matches any namespace. +#[derive(Clone, Debug)] +pub(crate) struct XmlName { + pub(crate) uri: Option, + pub(crate) local: Key, + pub(crate) prefix: Option, +} + +/// A namespace. `prefix` is `None` when it is undefined. +#[derive(Clone, Debug)] +pub(crate) struct XmlNs { + pub(crate) prefix: Option, + pub(crate) uri: Key, +} + +/// An XML value: a node of a tree, linked by object index. +#[derive(Clone)] +pub(crate) struct XmlNode { + pub(crate) kind: XmlKind, + pub(crate) name: Option, + pub(crate) parent: Option, + pub(crate) value: Str, + pub(crate) attrs: Vec, + pub(crate) children: Vec, + pub(crate) ns: Vec, +} + +/// An XMLList value: its items, and the object and name a write to an empty list targets. +#[derive(Clone)] +pub(crate) struct XmlListData { + pub(crate) items: Vec, + pub(crate) target: Option, + pub(crate) target_name: Option<(XmlName, bool)>, +} + +/// The state of a `.( )` filter: the items to test, the next one's position and those kept. +#[derive(Clone)] +pub(crate) struct XmlFilterState { + pub(crate) input: Vec, + pub(crate) pos: usize, + pub(crate) kept: Vec, } impl Class { /// The `[[Class]]` name `Object.prototype.toString` reports. pub(crate) fn name(&self) -> &'static str { match self { + Class::Xml(_) => "XML", + Class::XmlList(_) => "XMLList", + Class::Namespace(_) => "Namespace", + Class::QName { .. } => "QName", + Class::XmlFilter(_) => "Object", Class::Math => "Math", Class::Json => "JSON", Class::Ordinary | Class::ForIn(_) => "Object", @@ -311,6 +383,14 @@ pub(crate) const ENV_BYTES: usize = 64; pub(crate) const ROPE_BYTES: usize = 48; /// The estimated bytes of each argument a bound function holds. pub(crate) const BOUND_ARG_BYTES: usize = 24; +/// The estimated bytes of an XML node without its names and text. +pub(crate) const XML_NODE_BYTES: usize = 160; +/// The estimated bytes of an XML list without its items. +pub(crate) const XML_LIST_BYTES: usize = 96; +/// The estimated bytes of a reference from an XML node or list to another. +pub(crate) const XML_REF_BYTES: usize = 8; +/// The estimated bytes of a namespace an XML node holds. +pub(crate) const XML_NS_BYTES: usize = 48; /// The estimated bytes of a piece of code without its instructions, tables and strings. const CODE_BYTES: usize = 384; @@ -572,11 +652,45 @@ fn trace_object(o: &Object, work: &mut Vec, strings: &mut StringSizes) -> u work.extend(state.object.iter().map(|o| o.0)); strings.bytes += state.keys.iter().map(|k| string_bytes(k.len())).sum::(); } + Class::Xml(node) => { + work.extend(node.parent.iter().chain(&node.attrs).chain(&node.children).map(|o| o.0)); + strings.add(&node.value); + if let Some(name) = &node.name { + add_name(name, strings); + } + for ns in &node.ns { + add_ns(ns, strings); + } + extra = XML_NODE_BYTES + XML_REF_BYTES * (node.attrs.len() + node.children.len()) + XML_NS_BYTES * node.ns.len(); + } + Class::XmlList(list) => { + work.extend(list.target.iter().chain(&list.items).map(|o| o.0)); + if let Some((name, _)) = &list.target_name { + add_name(name, strings); + } + extra = XML_LIST_BYTES + XML_REF_BYTES * list.items.len(); + } + Class::Namespace(ns) => add_ns(ns, strings), + Class::QName { name, .. } => add_name(name, strings), + Class::XmlFilter(state) => { + work.extend(state.input.iter().chain(&state.kept).map(|o| o.0)); + extra = XML_LIST_BYTES + XML_REF_BYTES * (state.input.len() + state.kept.len()); + } _ => {} } OBJECT_BYTES + PROP_BYTES * o.props.len() + extra } +fn add_name(name: &XmlName, strings: &mut StringSizes) { + strings.add_key(&name.local); + name.uri.iter().chain(&name.prefix).for_each(|k| strings.add_key(k)); +} + +fn add_ns(ns: &XmlNs, strings: &mut StringSizes) { + strings.add_key(&ns.uri); + ns.prefix.iter().for_each(|k| strings.add_key(k)); +} + fn trace_env(e: &Env, work: &mut Vec, strings: &mut StringSizes) -> usize { match e { Env::Decl { bindings, outer } => { diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index be5760e8f..ed3099352 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -72,6 +72,12 @@ pub(crate) struct Intrinsics { pub(crate) error_protos: [ObjId; 7], pub(crate) eval: ObjId, pub(crate) throw_type_error: ObjId, + /// The `XML` constructor, which holds the XML settings. + pub(crate) xml_ctor: ObjId, + pub(crate) xml_proto: ObjId, + pub(crate) xml_list_proto: ObjId, + pub(crate) namespace_proto: ObjId, + pub(crate) qname_proto: ObjId, } impl Intrinsics { @@ -89,6 +95,11 @@ impl Intrinsics { error_protos: [o; 7], eval: o, throw_type_error: o, + xml_ctor: o, + xml_proto: o, + xml_list_proto: o, + namespace_proto: o, + qname_proto: o, } } @@ -117,6 +128,11 @@ impl Intrinsics { self.date_proto, self.eval, self.throw_type_error, + self.xml_ctor, + self.xml_proto, + self.xml_list_proto, + self.namespace_proto, + self.qname_proto, ]; v.extend(self.error_protos); v @@ -128,8 +144,8 @@ pub(crate) struct Frame { pub(crate) pc: usize, /// Where this frame's operands start on the operand stack. base: usize, - env: EnvId, - var_env: EnvId, + pub(crate) env: EnvId, + pub(crate) var_env: EnvId, /// `with` and catch environments entered and not yet left. env_depth: u32, this: Value, @@ -186,6 +202,8 @@ pub(crate) struct Interp { pub(crate) date_host: crate::date_builtins::DateHost, /// The bytes native code holds outside the heap's cells through [`HeldBytes`]. pub(crate) held: Rc, + /// The global code's default XML namespace URI (ECMA-357 section 12.1). + pub(crate) xml_default_uri: Key, /// Set by the host to end the current slice of a resumable run at the next safe point. pub(crate) pause: Rc>, /// The outermost loop of a resumable run is running ([`crate::Realm::advance`]). @@ -407,6 +425,7 @@ impl Interp { install_error: None, date_host: crate::date_builtins::DateHost::default(), held: Rc::default(), + xml_default_uri: Rc::from(Vec::new()), pause: Rc::default(), in_slice: false, can_wait: Rc::default(), @@ -463,11 +482,11 @@ impl Interp { Ok(()) } - fn frame(&self) -> JsResult<&Frame> { + pub(crate) fn frame(&self) -> JsResult<&Frame> { self.frames.last().ok_or_else(|| Abrupt::Internal("no frame".into())) } - fn frame_mut(&mut self) -> JsResult<&mut Frame> { + pub(crate) fn frame_mut(&mut self) -> JsResult<&mut Frame> { self.frames.last_mut().ok_or_else(|| Abrupt::Internal("no frame".into())) } @@ -475,7 +494,7 @@ impl Interp { self.frames.last().map_or(0, |f| f.base) } - fn pop(&mut self) -> JsResult { + pub(crate) fn pop(&mut self) -> JsResult { if self.stack.len() <= self.base() { return internal("operand stack underflow"); } @@ -483,7 +502,7 @@ impl Interp { } /// The value `n` places below the top (0 is the top). - fn peek(&self, n: usize) -> JsResult { + pub(crate) fn peek(&self, n: usize) -> JsResult { let len = self.stack.len(); match len.checked_sub(n + 1).filter(|&i| i >= self.base()) { Some(i) => self.stack.get(i).cloned().ok_or_else(|| Abrupt::Internal("operand stack underflow".into())), @@ -503,7 +522,7 @@ impl Interp { } /// Replaces the top `n` values with `v`. - fn replace(&mut self, n: usize, v: Value) -> JsResult<()> { + pub(crate) fn replace(&mut self, n: usize, v: Value) -> JsResult<()> { self.drop_n(n)?; self.push(v) } @@ -744,7 +763,7 @@ impl Interp { Ok(None) } - fn jump_to(&mut self, target: u32) -> JsResult<()> { + pub(crate) fn jump_to(&mut self, target: u32) -> JsResult<()> { self.frame_mut()?.pc = target as usize; Ok(()) } @@ -851,6 +870,17 @@ impl Interp { let v = self.frame()?.completion.clone(); self.push(v) } + Op::XmlMakeName { attribute, qualified } => self.op_xml_make_name(attribute, qualified), + Op::XmlDescendants => self.op_xml_descendants(), + Op::XmlEscape(kind) => self.op_xml_escape(kind), + Op::XmlLiteral { parts, list } => self.op_xml_literal(parts, list), + Op::XmlScopeGet => self.op_xml_scope_get(), + Op::XmlFilterStart => self.op_xml_filter_start(), + Op::XmlFilterNext(t) => self.op_xml_filter_next(t), + Op::XmlFilterTest => self.op_xml_filter_test(), + Op::XmlFilterEnd => self.op_xml_filter_end(), + Op::ForEachValue => self.op_for_each_value(), + Op::SetDefaultNamespace => self.op_set_default_namespace(), Op::Return | Op::ReturnCompletion => internal("return reached the dispatcher"), } } @@ -927,7 +957,7 @@ impl Interp { // ---- variables ---- - fn resolve(&self, name: &[u16]) -> JsResult { + fn resolve(&mut self, name: &[u16]) -> JsResult { let mut cur = Some(self.frame()?.env); while let Some(e) = cur { match self.heap.env(e) { @@ -937,11 +967,11 @@ impl Interp { } cur = *outer; } - Some(Env::Obj { object, with_this, outer }) => { - if self.has_property(*object, name)? { - return Ok(Resolved::Obj { object: *object, with_this: *with_this }); + Some(&Env::Obj { object, with_this, outer }) => { + if self.scope_has(object, name)? { + return Ok(Resolved::Obj { object, with_this }); } - cur = *outer; + cur = outer; } None => return internal("environment out of range"), } @@ -971,7 +1001,7 @@ impl Interp { match r { Resolved::Decl(e) => Ok((self.binding_value(e, name)?, Value::Undefined)), Resolved::Obj { object, with_this } => { - let v = self.get_from(object, name, &Value::Object(object))?; + let v = self.get_value(&Value::Object(object), name)?; Ok((v, if with_this { Value::Object(object) } else { Value::Undefined })) } Resolved::Unresolved => Err(self.not_defined(name)), @@ -998,6 +1028,9 @@ impl Interp { _ => Ok(()), } } + Resolved::Obj { object, .. } if self.options.e4x && self.is_xml(&Value::Object(object)) => { + self.put_value(&Value::Object(object), name.clone(), v) + } Resolved::Obj { object, .. } => { let throw = self.strict_writes(); self.put(object, name.clone(), v, throw) @@ -1012,6 +1045,13 @@ impl Interp { fn op_get_var(&mut self, n: u32, with_this: bool) -> JsResult<()> { let name = self.name(n)?; + if with_this + && self.options.e4x + && let Some((f, this)) = self.xml_scope_method(&name)? + { + self.push(f)?; + return self.push(this); + } let (v, this) = self.get_var(&name)?; self.push(v)?; if with_this { self.push(this) } else { Ok(()) } @@ -1188,6 +1228,9 @@ impl Interp { /// `base[k]` for any value (GetValue, section 8.7.1). pub(crate) fn get_value(&mut self, base: &Value, k: &[u16]) -> JsResult { + if self.options.e4x && self.is_xml(base) { + return self.xml_get(base, &Value::String(Str::from_units(k.to_vec()))); + } if let Value::String(s) = base { if is(k, "length") { return Ok(Value::Number(s.len() as f64)); @@ -1534,6 +1577,9 @@ impl Interp { /// PutValue on a property reference (section 8.7.2), whose base may be a primitive. fn put_value(&mut self, base: &Value, k: Key, v: Value) -> JsResult<()> { + if self.options.e4x && self.is_xml(base) { + return self.xml_put(base, &Value::String(Str::from_rc(k)), &v); + } let strict = self.strict(); match base { Value::Object(o) => { @@ -1562,6 +1608,11 @@ impl Interp { fn op_get_member(&mut self, n: u32, with_this: bool) -> JsResult<()> { let name = self.name(n)?; let base = self.peek(0)?; + if with_this && self.options.e4x && self.is_xml(&base) { + let (f, this) = self.xml_method(&base, &name)?; + self.replace(1, f)?; + return self.push(this); + } let v = self.get_value(&base, &name)?; self.replace(1, v)?; if with_this { self.push(base) } else { Ok(()) } @@ -1590,6 +1641,9 @@ impl Interp { fn op_to_key(&mut self) -> JsResult<()> { let k = self.peek(0)?; let base = self.peek(1)?; + if self.is_xml_access(&base, &k) { + return Ok(()); + } self.check_coercible(&base, &[])?; let name = self.to_key(&k)?; self.replace(1, Value::String(Str::from_rc(name))) @@ -1598,6 +1652,16 @@ impl Interp { fn op_get_index(&mut self, with_this: bool) -> JsResult<()> { let k = self.peek(0)?; let base = self.peek(1)?; + if self.is_xml_access(&base, &k) { + if with_this { + let name = self.xml_method_key(&k)?; + let (f, this) = self.xml_method(&base, &name)?; + self.replace(2, f)?; + return self.push(this); + } + let v = self.xml_get(&base, &k)?; + return self.replace(2, v); + } self.check_coercible(&base, &[])?; let name = self.to_key(&k)?; let v = self.get_value(&base, &name)?; @@ -1609,6 +1673,10 @@ impl Interp { let v = self.peek(0)?; let k = self.peek(1)?; let base = self.peek(2)?; + if self.is_xml_access(&base, &k) { + self.xml_put(&base, &k, &v)?; + return self.replace(3, v); + } let name = self.to_key(&k)?; self.put_value(&base, name, v.clone())?; self.replace(3, v) @@ -1617,6 +1685,10 @@ impl Interp { fn op_delete_member(&mut self, n: u32) -> JsResult<()> { let name = self.name(n)?; let base = self.peek(0)?; + if self.options.e4x && self.is_xml(&base) { + let r = self.xml_delete(&base, &Value::String(Str::from_rc(name)))?; + return self.replace(1, Value::Bool(r)); + } let o = self.to_object(&base)?; let strict = self.strict(); let r = self.delete(o, &name, strict)?; @@ -1626,6 +1698,10 @@ impl Interp { fn op_delete_index(&mut self) -> JsResult<()> { let k = self.peek(0)?; let base = self.peek(1)?; + if self.is_xml_access(&base, &k) { + let r = self.xml_delete(&base, &k)?; + return self.replace(2, Value::Bool(r)); + } self.check_coercible(&base, &[])?; let name = self.to_key(&k)?; let o = self.to_object(&base)?; @@ -1652,6 +1728,13 @@ impl Interp { fn op_inc_index(&mut self, prefix: bool, dec: bool) -> JsResult<()> { let k = self.peek(0)?; let base = self.peek(1)?; + if self.is_xml_access(&base, &k) { + let v = self.xml_get(&base, &k)?; + let old = self.to_number(&v)?; + let new = if dec { old - 1.0 } else { old + 1.0 }; + self.xml_put(&base, &k, &Value::Number(new))?; + return self.replace(2, Value::Number(if prefix { new } else { old })); + } self.check_coercible(&base, &[])?; let name = self.to_key(&k)?; let r = self.inc_property(&base, name, prefix, dec)?; @@ -1817,6 +1900,9 @@ impl Interp { /// For a String object, the heap budget for all its index names is checked before any is /// built. Each index name and each property visited takes a unit of fuel. fn enumerable_keys(&mut self, id: ObjId) -> JsResult> { + if self.options.e4x && self.is_xml(&Value::Object(id)) { + return self.xml_enumerate(id); + } let mut seen = std::collections::HashSet::new(); let mut out = Vec::new(); let mut cur = Some(id); @@ -1883,7 +1969,7 @@ impl Interp { state.pos += 1; let object = state.object; if let Some(o) = object - && self.has_property(o, &k)? + && self.scope_has(o, &k)? { return self.push(Value::String(Str::from_rc(k))); } @@ -2392,6 +2478,7 @@ impl Interp { Value::Bool(_) => "boolean", Value::Number(_) => "number", Value::String(_) => "string", + Value::Object(_) if self.options.e4x && self.is_xml(v) => "xml", Value::Object(_) if self.is_callable(v) => "function", Value::Object(_) => "object", } @@ -2553,6 +2640,9 @@ impl Interp { BinaryOp::StrictNotEq => Ok(Value::Bool(!strict_eq(a, b))), BinaryOp::Instanceof => self.instance_of(a, b).map(Value::Bool), BinaryOp::In => { + if self.options.e4x && self.is_xml(b) { + return self.xml_has(b, a).map(Value::Bool); + } let Value::Object(o) = b else { return Err(self.type_error("the right side of 'in' is not an object")) }; let k = self.to_key(a)?; self.has_property(*o, &k).map(Value::Bool) @@ -2565,6 +2655,9 @@ impl Interp { if let (Value::Number(x), Value::Number(y)) = (a, b) { return Ok(Value::Number(x + y)); } + if self.options.e4x && self.is_xml(a) && self.is_xml(b) { + return self.xml_concat(a, b); + } let pa = self.to_primitive(a, Hint::Default)?; let pb = self.to_primitive(b, Hint::Default)?; if matches!(pa, Value::String(_)) || matches!(pb, Value::String(_)) { @@ -2597,7 +2690,12 @@ impl Interp { } /// The abstract equality comparison (section 11.9.3). - fn loose_eq(&mut self, a: &Value, b: &Value) -> JsResult { + pub(crate) fn loose_eq(&mut self, a: &Value, b: &Value) -> JsResult { + if self.options.e4x + && let Some(r) = self.xml_loose_eq(a, b)? + { + return Ok(r); + } let (mut x, mut y) = (a.clone(), b.clone()); loop { match (&x, &y) { diff --git a/crates/js/src/lexer.rs b/crates/js/src/lexer.rs index 9a6ce8766..a2a7153dc 100644 --- a/crates/js/src/lexer.rs +++ b/crates/js/src/lexer.rs @@ -47,13 +47,30 @@ pub struct Lexer<'s> { preprocessor_lines: bool, /// The deepest nesting of groups a regular expression literal may have. max_depth: usize, + /// Read the E4X punctuators `..`, `::` and `@`. + e4x: bool, } impl<'s> Lexer<'s> { /// A lexer of `src`. `preprocessor_lines` turns on the reading of preprocessor lines; /// `max_depth` limits how deeply a regular expression literal's groups may nest. pub fn new(src: &'s [u16], preprocessor_lines: bool, max_depth: usize) -> Self { - Self { src, pos: 0, line: 1, line_start: 0, blank_line: true, preprocessor_lines, max_depth } + Self { src, pos: 0, line: 1, line_start: 0, blank_line: true, preprocessor_lines, max_depth, e4x: false } + } + + /// The same lexer, reading the E4X punctuators when `e4x` is true. + pub fn with_e4x(mut self, e4x: bool) -> Self { + self.e4x = e4x; + self + } + + /// Moves to source offset `pos`, at the given line and column (both from 1), after the parser + /// has read an XML literal up to there. + pub(crate) fn seek(&mut self, pos: usize, line: u32, column: u32) { + self.pos = pos; + self.line = line; + self.line_start = pos.saturating_sub(column.saturating_sub(1) as usize); + self.blank_line = false; } /// The code unit at `pos`, as a code point value. @@ -578,6 +595,9 @@ impl<'s> Lexer<'s> { fn punct(&mut self) -> Option { let rest = &self.src[self.pos..]; for (spelling, punct) in Punct::ALL { + if !self.e4x && matches!(punct, Punct::DotDot | Punct::ColonColon | Punct::At) { + continue; + } let bytes = spelling.as_bytes(); if rest.len() >= bytes.len() && rest.iter().zip(bytes).all(|(&u, &b)| u == u16::from(b)) { self.pos += bytes.len(); diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index bfb5d5c0c..a8478aa65 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -37,6 +37,13 @@ //! by [`Limits`]: instructions run (fuel), heap bytes, frames, nested host calls, and an //! [`Interrupt`] flag another thread can set. A run that reaches a limit ends with //! [`RunErrorKind::Limit`], which a script's `try` cannot catch. +//! +//! With [`Options::e4x`], the parser also reads E4X (ECMA-357, second edition, December 2005): +//! XML and XMLList literals, the `.@`, `..`, `::` and `.( )` operators, `for each … in` and +//! `default xml namespace`; the realm then provides `XML`, `XMLList`, `Namespace`, `QName`, +//! `isXMLName` and `setDefaultXMLNamespace`. Without the option, the language and the built-ins are +//! those of ECMAScript 5.1 above. XML trees live in the heap, and parsing, copying, comparing and +//! serializing them use no native recursion and take fuel for each node. #![forbid(unsafe_code)] pub mod ast; @@ -60,10 +67,12 @@ mod regexp_vm; mod string_builtins; mod token; mod value; +mod xml; +mod xml_builtins; pub use ast::{ AssignOp, Ast, BinaryOp, CatchClause, Expr, ExprId, ForInTarget, ForInit, Function, FunctionId, FunctionKind, LogicalOp, Name, Program, Property, - PropertyKey, PropertyValue, Stmt, StmtId, SwitchCase, UnaryOp, UpdateOp, VarDecl, + PropertyKey, PropertyValue, Stmt, StmtId, SwitchCase, UnaryOp, UpdateOp, VarDecl, XmlNamePart, XmlPart, XmlQualifier, }; pub use builtins::extensions::Localization; pub use error::{SyntaxError, TOO_DEEP, TOO_LONG}; @@ -85,11 +94,13 @@ pub struct Options { pub max_depth: usize, /// The most UTF-16 code units a source may hold. pub max_source_units: usize, + /// Read and run E4X (ECMA-357). Off, XML syntax is an error and a realm has no XML built-ins. + pub e4x: bool, } impl Default for Options { fn default() -> Self { - Self { strict: false, preprocessor_lines: false, max_depth: DEFAULT_MAX_DEPTH, max_source_units: 64 << 20 } + Self { strict: false, preprocessor_lines: false, max_depth: DEFAULT_MAX_DEPTH, max_source_units: 64 << 20, e4x: false } } } @@ -197,3 +208,5 @@ mod interp_tests; mod tests; #[cfg(test)] mod text_tests; +#[cfg(test)] +mod xml_tests; diff --git a/crates/js/src/parser.rs b/crates/js/src/parser.rs index d56b75617..3c2ab3ed3 100644 --- a/crates/js/src/parser.rs +++ b/crates/js/src/parser.rs @@ -7,6 +7,8 @@ //! stack, and prefix operators, assignment chains, `else if` chains and labels with loops, so long //! runs of them do not nest at all. +mod xml; + use std::collections::{HashMap, HashSet}; use crate::LineTable; @@ -69,11 +71,13 @@ pub(crate) struct Parser<'s> { /// The next statement stands in a statement list, where a label's body may be a function /// declaration. Read and cleared as each statement starts. in_list: bool, + /// Read E4X syntax. + e4x: bool, } impl<'s> Parser<'s> { pub(crate) fn new(src: &'s [u16], options: &crate::Options, keep_tokens: bool) -> Result { - let lex = Lexer::new(src, options.preprocessor_lines, options.max_depth); + let lex = Lexer::new(src, options.preprocessor_lines, options.max_depth).with_e4x(options.e4x); let placeholder = Token { kind: TokenKind::Eof, span: Span::default(), line: 1, column: 1, newline_before: false, escaped: false, legacy_octal: false }; let mut parser = Self { @@ -90,6 +94,7 @@ impl<'s> Parser<'s> { tokens: keep_tokens.then(Vec::new), function_allowed: false, in_list: false, + e4x: options.e4x, }; parser.tok = parser.next_significant()?; Ok(parser) @@ -391,6 +396,7 @@ impl<'s> Parser<'s> { self.semicolon()?; Ok(self.ast.push_stmt(Stmt::Debugger, self.span_from(start))) } + Keyword::Default if self.at_default_xml_namespace() => self.parse_default_xml_namespace(start), _ => { let expr = self.parse_expression(false)?; self.semicolon()?; @@ -488,7 +494,14 @@ impl<'s> Parser<'s> { fn parse_for(&mut self, start: Span) -> Result { self.bump()?; + let each = self.e4x && self.tok.ident() == Some("each"); + if each { + self.bump()?; + } self.expect(Punct::LParen)?; + if each { + return self.parse_for_each(start); + } let init = if self.at(Punct::Semicolon) { None } else if self.at_keyword(Keyword::Var) { @@ -501,14 +514,14 @@ impl<'s> Parser<'s> { if self.ctx.strict && decl.init.is_some() { return Err(self.error_at_token(&self.tok, "a for-in variable cannot have an initializer in strict mode code")); } - return self.parse_for_in(ForInTarget::Var(decl), start); + return self.parse_for_in(ForInTarget::Var(decl), start, false); } Some(ForInit::Var(decls)) } else { let expr = self.parse_expression(true)?; if self.at_keyword(Keyword::In) { self.check_target(expr, "invalid left-hand side in for-in loop")?; - return self.parse_for_in(ForInTarget::Expr(expr), start); + return self.parse_for_in(ForInTarget::Expr(expr), start, false); } Some(ForInit::Expr(expr)) }; @@ -522,12 +535,27 @@ impl<'s> Parser<'s> { } /// The rest of `for (target in object) body`, from `in`. - fn parse_for_in(&mut self, target: ForInTarget, start: Span) -> Result { + fn parse_for_in(&mut self, target: ForInTarget, start: Span, each: bool) -> Result { self.expect_keyword(Keyword::In)?; let object = self.parse_expression(false)?; self.expect(Punct::RParen)?; let body = self.parse_loop_body()?; - Ok(self.ast.push_stmt(Stmt::ForIn { target, object, body }, self.span_from(start))) + Ok(self.ast.push_stmt(Stmt::ForIn { target, object, body, each }, self.span_from(start))) + } + + /// `for each (target in object) body`, from the target (ECMA-357 section 12.3). + fn parse_for_each(&mut self, start: Span) -> Result { + if self.at_keyword(Keyword::Var) { + self.bump()?; + let mut decls = self.parse_var_decls(true)?; + let (Some(decl), true) = (decls.pop(), decls.is_empty()) else { + return Err(self.error_at_token(&self.tok, "for each takes one variable")); + }; + return self.parse_for_in(ForInTarget::Var(decl), start, true); + } + let expr = self.parse_expression(true)?; + self.check_target(expr, "invalid left-hand side in for each loop")?; + self.parse_for_in(ForInTarget::Expr(expr), start, true) } /// `break` or `continue`, with an optional label on the same line, which must name a label @@ -608,7 +636,7 @@ impl<'s> Parser<'s> { let test = if self.at_keyword(Keyword::Case) { self.bump()?; Some(self.parse_expression(false)?) - } else if self.at_keyword(Keyword::Default) { + } else if self.at_keyword(Keyword::Default) && !self.at_default_xml_namespace() { if has_default { return Err(self.error_at_token(&self.tok, "more than one default clause in switch statement")); } @@ -620,7 +648,10 @@ impl<'s> Parser<'s> { }; self.expect(Punct::Colon)?; let mut body = Vec::new(); - while !(self.at_keyword(Keyword::Case) || self.at_keyword(Keyword::Default) || self.at(Punct::RBrace)) { + while !(self.at_keyword(Keyword::Case) + || (self.at_keyword(Keyword::Default) && !self.at_default_xml_namespace()) + || self.at(Punct::RBrace)) + { if self.tok.kind == TokenKind::Eof { return Err(self.unexpected()); } @@ -953,8 +984,13 @@ impl<'s> Parser<'s> { let mut expr = self.parse_primary()?; loop { match &self.tok.kind { + TokenKind::Punct(Punct::DotDot) => expr = self.parse_xml_descendants(expr)?, TokenKind::Punct(Punct::Dot) => { self.bump()?; + if self.e4x && self.at_e4x_member() { + expr = self.parse_xml_member(expr)?; + continue; + } let Some(name) = self.tok.identifier_name().map(str::to_string) else { return Err(self.error_at_token(&self.tok, format!("expected a property name but found {}", describe(&self.tok)))); }; @@ -1050,10 +1086,16 @@ impl<'s> Parser<'s> { self.expect(Punct::RParen)?; return Ok(self.ast.push_expr(Expr::Paren(inner), self.span_from(start))); } + TokenKind::Punct(Punct::Lt) if self.e4x => return self.parse_xml_literal(), + TokenKind::Punct(Punct::At | Punct::Star) if self.e4x => return self.parse_xml_scope_name(), _ => return Err(self.unexpected()), }; let token = self.bump()?; - Ok(self.ast.push_expr(expr, token.span)) + let id = self.ast.push_expr(expr, token.span); + if self.e4x && self.at(Punct::ColonColon) && matches!(token.kind, TokenKind::Ident(_)) { + return self.parse_xml_qualified_primary(id); + } + Ok(id) } fn parse_function_expression(&mut self) -> Result { @@ -1232,5 +1274,6 @@ fn describe(token: &Token) -> String { TokenKind::String(_) => "string".into(), TokenKind::RegExp { .. } => "regular expression".into(), TokenKind::Directive(_) => "preprocessor line".into(), + TokenKind::XmlLiteral => "XML literal".into(), } } diff --git a/crates/js/src/parser/xml.rs b/crates/js/src/parser/xml.rs new file mode 100644 index 000000000..e91691a58 --- /dev/null +++ b/crates/js/src/parser/xml.rs @@ -0,0 +1,439 @@ +//! E4X syntax (ECMA-357 sections 11.1 and 12.1): literals, names and the operators on them. +//! +//! A literal is read by scanning the source units with an explicit stack of open tags, so tag +//! nesting uses no native recursion and is held to [`crate::Options::max_depth`]. Only an embedded +//! `{expression}` re-enters the expression parser, and that recursion is counted as any other. + +use super::{Parser, Result, describe}; +use crate::ast::{Expr, ExprId, Stmt, StmtId, XmlNamePart, XmlPart, XmlQualifier}; +use crate::error::TOO_DEEP; +use crate::token::{Keyword, Punct, Span, TokenKind}; +use crate::xml::{is_name_start, is_xml_space, scan_name, scan_reference}; + +const LT: u16 = b'<' as u16; +const GT: u16 = b'>' as u16; +const SLASH: u16 = b'/' as u16; +const AMP: u16 = b'&' as u16; +const LBRACE: u16 = b'{' as u16; +const EQ: u16 = b'=' as u16; +const DQUOTE: u16 = b'"' as u16; +const SQUOTE: u16 = b'\'' as u16; + +/// A literal being read: the markup since the last embedded expression, and the finished parts. +#[derive(Default)] +struct Scan { + text: Vec, + parts: Vec, +} + +impl Scan { + fn flush(&mut self) { + if !self.text.is_empty() { + self.parts.push(XmlPart::Text(std::mem::take(&mut self.text))); + } + } +} + +impl Parser<'_> { + pub(super) fn at_default_xml_namespace(&self) -> bool { + self.e4x && self.peek().is_some_and(|t| t.ident() == Some("xml")) + } + + /// `default xml namespace = expression;`, from `default`. + pub(super) fn parse_default_xml_namespace(&mut self, start: Span) -> Result { + self.expect_keyword(Keyword::Default)?; + self.bump()?; + if self.tok.ident() != Some("namespace") { + return Err(self.error_at_token(&self.tok, format!("expected 'namespace' but found {}", describe(&self.tok)))); + } + self.bump()?; + self.expect(Punct::Eq)?; + let value = self.parse_expression(false)?; + self.semicolon()?; + Ok(self.ast.push_stmt(Stmt::DefaultXmlNamespace(value), self.span_from(start))) + } + + /// After a `.`: the next token starts an E4X name or a filter rather than a property name. + pub(super) fn at_e4x_member(&self) -> bool { + match &self.tok.kind { + TokenKind::Punct(Punct::At | Punct::Star | Punct::LParen) => true, + _ => self.tok.identifier_name().is_some() && self.peek().is_some_and(|t| t.is_punct(Punct::ColonColon)), + } + } + + /// `object.@name`, `object.*`, `object.ns::name` or `object.(predicate)`, from the token after + /// the `.`. + pub(super) fn parse_xml_member(&mut self, object: ExprId) -> Result { + let start = self.ast.expr_span(object); + if self.at(Punct::LParen) { + self.bump()?; + let predicate = self.parse_expression(false)?; + self.expect(Punct::RParen)?; + return Ok(self.ast.push_expr(Expr::XmlFilter { object, predicate }, self.span_from(start))); + } + let index = self.parse_xml_name()?; + Ok(self.ast.push_expr(Expr::Index { object, index }, self.span_from(start))) + } + + /// `object..name`, from the `..`. + pub(super) fn parse_xml_descendants(&mut self, object: ExprId) -> Result { + let start = self.ast.expr_span(object); + self.bump()?; + let name = self.parse_xml_name()?; + Ok(self.ast.push_expr(Expr::XmlDescendants { object, name }, self.span_from(start))) + } + + /// `@name`, `*` or `@*` standing alone as a primary expression. + pub(super) fn parse_xml_scope_name(&mut self) -> Result { + let start = self.tok.span; + let name = self.parse_xml_name()?; + Ok(self.ast.push_expr(Expr::XmlScopeName(name), self.span_from(start))) + } + + /// `qualifier::name` as a primary expression, from the `::`; `qualifier` is already read. + pub(super) fn parse_xml_qualified_primary(&mut self, qualifier: ExprId) -> Result { + let start = self.ast.expr_span(qualifier); + self.bump()?; + let name = self.parse_xml_name_part()?; + let qualifier = Some(XmlQualifier::Expr(qualifier)); + let id = self.ast.push_expr(Expr::XmlName { attribute: false, qualifier, name }, self.span_from(start)); + Ok(self.ast.push_expr(Expr::XmlScopeName(id), self.span_from(start))) + } + + /// An E4X name: `@`, then `*`, a name or `[expression]`, with an optional `qualifier::`. + fn parse_xml_name(&mut self) -> Result { + let start = self.tok.span; + let attribute = self.eat(Punct::At)?; + let first_is_star = self.at(Punct::Star); + let qualifier_ident = self.tok.identifier_name().map(str::to_string).filter(|_| matches!(self.tok.kind, TokenKind::Ident(_))); + let first = self.parse_xml_name_part()?; + let (qualifier, name) = if self.at(Punct::ColonColon) { + let qualifier = if first_is_star { + XmlQualifier::Any + } else if let Some(name) = qualifier_ident { + let span = self.span_from(start); + XmlQualifier::Expr(self.ast.push_expr(Expr::Ident(name), span)) + } else { + return Err(self.error_at_token(&self.tok, "expected a namespace name before '::'")); + }; + self.bump()?; + (Some(qualifier), self.parse_xml_name_part()?) + } else { + (None, first) + }; + Ok(self.ast.push_expr(Expr::XmlName { attribute, qualifier, name }, self.span_from(start))) + } + + fn parse_xml_name_part(&mut self) -> Result { + if self.eat(Punct::Star)? { + return Ok(XmlNamePart::Star); + } + if self.eat(Punct::LBracket)? { + let e = self.parse_expression(false)?; + self.expect(Punct::RBracket)?; + return Ok(XmlNamePart::Computed(e)); + } + let Some(name) = self.tok.identifier_name().map(str::to_string) else { + return Err(self.error_at_token(&self.tok, format!("expected an XML name but found {}", describe(&self.tok)))); + }; + self.bump()?; + Ok(XmlNamePart::Ident(name)) + } + + /// An XML or XMLList literal, from its `<` (the current token). + pub(super) fn parse_xml_literal(&mut self) -> Result { + let start = self.tok.span; + let token_slot = self.tokens.as_ref().map(Vec::len); + if let Some(tokens) = &mut self.tokens { + tokens.push(self.tok.clone()); + } + let mut pos = start.start as usize; + let list = self.unit(pos + 1) == Some(GT); + if list { + pos += 2; + } + let mut scan = Scan::default(); + // The name of each open element; `None` when it is computed. + let mut open: Vec>> = Vec::new(); + loop { + if !list && open.is_empty() && !scan.text.is_empty() | !scan.parts.is_empty() { + break; + } + let Some(c) = self.unit(pos) else { return Err(self.xml_error(pos, "unterminated XML literal")) }; + if c == LT { + if self.unit(pos + 1) == Some(SLASH) { + if open.is_empty() { + if list && self.unit(pos + 2) == Some(GT) { + pos += 3; + break; + } + return Err(self.xml_error(pos, "a closing tag has no matching open tag")); + } + pos = self.xml_close_tag(pos, &mut open, &mut scan)?; + } else if self.starts_with(pos, ""); + } + XmlKind::Pi => { + let target = self.xml_node(x)?.name.as_ref().map(|n| n.local.clone()).unwrap_or_else(|| Rc::from(Vec::new())); + out.push_str(self, ""); + } + XmlKind::Element => {} + } + let ns_mark = scope.len(); + let node = self.xml_node(x)?; + let declared = node.ns.clone(); + let attrs = node.attrs.clone(); + let name = node.name.clone().ok_or_else(|| Abrupt::Internal("element without a name".into()))?; + self.xml_take_fuel(declared.len().saturating_mul(scope.len() + 1))?; + let mut unnamed = Vec::new(); + for ns in declared { + if ns.prefix.is_none() { + unnamed.push(ns); + } else if !scope.iter().any(|a| a.uri == ns.uri && a.prefix == ns.prefix) { + self.xml_charge(XML_NS_BYTES)?; + scope.push(ns); + } + } + for ns in unnamed { + if !scope.iter().any(|a| a.uri == ns.uri) { + let prefix = fresh_prefix(scope); + self.xml_charge(XML_NS_BYTES)?; + scope.push(XmlNs { prefix: Some(prefix), uri: ns.uri }); + } + } + let prefix = self.out_prefix(&name, scope, false)?; + let mut attr_prefixes = Vec::new(); + for &a in &attrs { + self.use_fuel()?; + let an = self.xml_node(a)?.name.clone().ok_or_else(|| Abrupt::Internal("attribute without a name".into()))?; + attr_prefixes.push(self.out_prefix(&an, scope, true)?); + } + out.push_units(self, &[LT])?; + self.write_name(out, &prefix, &name.local)?; + for (&a, attr_prefix) in attrs.iter().zip(&attr_prefixes) { + let an = self.xml_node(a)?; + let local = an.name.as_ref().map(|q| q.local.clone()).unwrap_or_else(|| Rc::from(Vec::new())); + let value = an.value.flat(); + out.push_units(self, &[SPACE])?; + self.write_name(out, attr_prefix, &local)?; + out.push_units(self, &[EQ, DQUOTE])?; + self.write_escaped(out, &value, true)?; + out.push_units(self, &[DQUOTE])?; + } + for at in ns_mark..scope.len() { + let Some(ns) = scope.get(at).cloned() else { break }; + let ns_prefix = ns.prefix.unwrap_or_else(|| Rc::from(Vec::new())); + out.push_str(self, " xmlns")?; + if !ns_prefix.is_empty() { + out.push_units(self, &[COLON])?; + out.push_units(self, &ns_prefix)?; + } + out.push_units(self, &[EQ, DQUOTE])?; + self.write_escaped(out, &ns.uri, true)?; + out.push_units(self, &[DQUOTE])?; + } + let count = self.children_len(x)?; + if count == 0 { + scope.truncate(ns_mark); + return out.push_str(self, "/>"); + } + out.push_units(self, &[GT])?; + let first_is_text = match self.child_at(x, 0)? { + Some(c) => self.xml_kind(c)? == XmlKind::Text, + None => false, + }; + let pretty_children = pretty && (count > 1 || !first_is_text); + let child_indent = if pretty_children { indent.saturating_add(u64::from(settings.indent)) } else { 0 }; + stack.push(OutFrame { node: x, next: 0, indent, child_indent, pretty_children, ns_mark, prefix, local: name.local }); + Ok(()) + } + + fn write_node(&mut self, out: &mut Builder, root: ObjId, indent: u64, settings: &Settings, pretty: bool) -> JsResult<()> { + let mut scope: Vec = Vec::new(); + let mut stack: Vec = Vec::new(); + self.write_enter(out, root, indent, settings, pretty, &mut scope, &mut stack)?; + while let Some(frame) = stack.last_mut() { + let (node, next, child_indent, pretty_children) = (frame.node, frame.next, frame.child_indent, frame.pretty_children); + frame.next += 1; + match self.child_at(node, next)? { + Some(child) => { + if pretty_children { + out.push_units(self, &[LF])?; + } + self.write_enter(out, child, child_indent, settings, pretty && pretty_children, &mut scope, &mut stack)?; + } + None => { + let Some(frame) = stack.pop() else { break }; + if frame.pretty_children { + out.push_units(self, &[LF])?; + out.push_repeated(self, &[SPACE], frame.indent)?; + } + out.push_units(self, &[LT, SLASH])?; + self.write_name(out, &frame.prefix, &frame.local)?; + out.push_units(self, &[GT])?; + scope.truncate(frame.ns_mark); + } + } + } + Ok(()) + } + + /// ToXMLString (ECMA-357 section 10.2). `pretty` overrides the setting when it is given. + pub(crate) fn xml_string_of(&mut self, v: &Value, pretty: Option) -> JsResult { + let settings = self.xml_settings()?; + let pretty = pretty.unwrap_or(settings.pretty); + let mut out = Builder::new(); + match self.classify(v) { + Xv::Node(n) => self.write_node(&mut out, n, 0, &settings, pretty)?, + Xv::List(l) => { + for i in 0..self.list_len(l)? { + self.use_fuel()?; + let Some(item) = self.list_at(l, i)? else { break }; + if i > 0 && pretty { + out.push_units(self, &[LF])?; + } + self.write_node(&mut out, item, 0, &settings, pretty)?; + } + } + Xv::Other => return Err(self.not_xml()), + } + let Value::String(s) = out.finish(self) else { return Err(Abrupt::Internal("not a string".into())) }; + Ok(s) + } + + // ---- the parser ---- + + fn syntax(&mut self, message: &str) -> Abrupt { + self.throw(ErrorType::Syntax, message) + } + + fn decode_into(&mut self, src: &[u16], from: usize, to: usize, out: &mut Vec, attribute: bool) -> JsResult<()> { + self.xml_text_fuel(to.saturating_sub(from))?; + let mut pos = from; + while pos < to { + let Some(&c) = src.get(pos) else { break }; + if c == AMP { + let Some((after, ch)) = scan_reference(src, pos).filter(|(after, _)| *after <= to) else { + return Err(self.syntax("invalid XML character or entity reference")); + }; + push_char(out, ch); + pos = after; + continue; + } + if pos + 1 < to && starts_pair(c, src.get(pos + 1).copied()) { + out.extend_from_slice(src.get(pos..pos + 2).unwrap_or_default()); + pos += 2; + continue; + } + if !is_xml_char(u32::from(c)) { + return Err(self.syntax("invalid character in XML text")); + } + pos += 1; + match (c, attribute) { + (CR, _) => { + if src.get(pos) == Some(&LF) { + pos += 1; + } + out.push(if attribute { SPACE } else { LF }); + } + (TAB | LF, true) => out.push(SPACE), + _ => out.push(c), + } + } + Ok(()) + } + + fn find_from(src: &[u16], from: usize, needle: &[u16]) -> Option { + src.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|i| from + i) + } + + fn read_name(&mut self, src: &[u16], pos: usize) -> JsResult { + scan_name(src, pos).ok_or_else(|| self.syntax("invalid XML name")) + } + + fn check_units(&mut self, src: &[u16], from: usize, to: usize) -> JsResult<()> { + if valid_xml_units(src.get(from..to).unwrap_or_default()) { + return Ok(()); + } + Err(self.syntax("invalid character in XML text")) + } + + /// Splits `prefix:local`. + fn split_name<'a>(&mut self, raw: &'a [u16]) -> JsResult<(Option<&'a [u16]>, &'a [u16])> { + let Some(colon) = raw.iter().position(|&c| c == COLON) else { return Ok((None, raw)) }; + let (prefix, rest) = raw.split_at(colon); + let local = rest.get(1..).unwrap_or_default(); + if !is_ncname(prefix) || !is_ncname(local) { + return Err(self.syntax("invalid XML qualified name")); + } + Ok((Some(prefix), local)) + } + + fn resolve_prefix(&mut self, prefix: &[u16], own: &[XmlNs], open: &[ObjId]) -> JsResult> { + if prefix == key("xml").as_ref() { + return Ok(Some(key(XML_URI))); + } + self.xml_take_fuel(own.len() + open.len() + 1)?; + if let Some(ns) = prefix_in_scope(own, prefix) { + return Ok(Some(ns.uri.clone())); + } + for &e in open.iter().rev() { + let node = self.xml_node(e)?; + let len = node.ns.len(); + if let Some(ns) = prefix_in_scope(&node.ns, prefix) { + return Ok(Some(ns.uri.clone())); + } + self.xml_take_fuel(len / UNITS_PER_FUEL)?; + } + Ok(None) + } + + fn place_parsed(&mut self, node: ObjId, open: &[ObjId], roots: &mut Vec) -> JsResult<()> { + if let Some(&top) = open.last() { + self.xml_charge_link(node)?; + self.xml_node_mut(top)?.children.push(node); + self.xml_node_mut(node)?.parent = Some(top); + } else { + self.xml_keep(node); + self.xml_charge(XML_REF_BYTES)?; + roots.push(node); + } + Ok(()) + } + + fn flush_text(&mut self, text: &mut Vec, ignore_whitespace: bool, open: &[ObjId], roots: &mut Vec) -> JsResult<()> { + if text.is_empty() { + return Ok(()); + } + if ignore_whitespace && text.iter().all(|&c| is_xml_space(c)) { + text.clear(); + return Ok(()); + } + let t = self.new_text_node(Str::from_units(std::mem::take(text)), None)?; + self.place_parsed(t, open, roots) + } + + fn parse_open_tag( + &mut self, + src: &[u16], + pos: usize, + open: &mut Vec, + names: &mut Vec<(usize, usize)>, + roots: &mut Vec, + ) -> JsResult { + if open.len() >= self.options.max_depth { + return Err(self.syntax(TOO_DEEP)); + } + let name_start = pos + 1; + let name_end = self.read_name(src, name_start)?; + let raw_name = src.get(name_start..name_end).unwrap_or_default(); + let (elem_prefix, elem_local) = self.split_name(raw_name)?; + let mut at = name_end; + let mut raw_attrs: Vec<(usize, usize, Vec)> = Vec::new(); + let closes = loop { + let before = at; + while src.get(at).copied().is_some_and(is_xml_space) { + at += 1; + } + match src.get(at).copied() { + None => return Err(self.syntax("unterminated XML tag")), + Some(SLASH) if src.get(at + 1) == Some(>) => { + at += 2; + break true; + } + Some(GT) => { + at += 1; + break false; + } + Some(_) if at == before => return Err(self.syntax("expected white space before an attribute")), + Some(_) => { + self.use_fuel()?; + let end = self.read_name(src, at)?; + let mut p = end; + while src.get(p).copied().is_some_and(is_xml_space) { + p += 1; + } + if src.get(p) != Some(&EQ) { + return Err(self.syntax("expected '=' after an attribute name")); + } + p += 1; + while src.get(p).copied().is_some_and(is_xml_space) { + p += 1; + } + let quote = match src.get(p).copied() { + Some(q @ (DQUOTE | SQUOTE)) => q, + _ => return Err(self.syntax("expected a quoted attribute value")), + }; + let value_start = p + 1; + let Some(value_end) = Self::find_from(src, value_start, &[quote]) else { + return Err(self.syntax("unterminated attribute value")); + }; + if src.get(value_start..value_end).is_some_and(|v| v.contains(<)) { + return Err(self.syntax("'<' cannot appear in an attribute value")); + } + let mut value = Vec::new(); + self.decode_into(src, value_start, value_end, &mut value, true)?; + raw_attrs.push((at, end, value)); + at = value_end + 1; + } + } + }; + self.xml_charge(XML_NS_BYTES.saturating_mul(raw_attrs.len()))?; + let mut ns: Vec = Vec::new(); + let mut seen_decl: HashSet> = HashSet::new(); + for (start, end, value) in &raw_attrs { + let raw = src.get(*start..*end).unwrap_or_default(); + if raw == key("xmlns").as_ref() { + if !ns_binding_allowed(&[], value) { + return Err(self.syntax(RESERVED_NS_BINDING)); + } + if !seen_decl.insert(Vec::new()) { + return Err(self.syntax("a namespace is declared twice")); + } + ns.push(XmlNs { prefix: Some(Rc::from(Vec::new())), uri: Rc::from(value.as_slice()) }); + } else if let (Some(p), local) = self.split_name(raw)? + && p == key("xmlns").as_ref() + { + if value.is_empty() { + return Err(self.syntax("a namespace prefix cannot be declared with an empty URI")); + } + if !ns_binding_allowed(local, value) { + return Err(self.syntax(RESERVED_NS_BINDING)); + } + if !seen_decl.insert(local.to_vec()) { + return Err(self.syntax("a namespace is declared twice")); + } + ns.push(XmlNs { prefix: Some(Rc::from(local)), uri: Rc::from(value.as_slice()) }); + } + } + let uri = match elem_prefix { + Some(p) => match self.resolve_prefix(p, &ns, open)? { + Some(u) => u, + None => return Err(self.syntax("an XML name uses an undeclared namespace prefix")), + }, + None => match self.resolve_prefix(&[], &ns, open)? { + Some(u) => u, + None => self.xml_default_uri()?, + }, + }; + let name = XmlName { uri: Some(uri), local: Rc::from(elem_local), prefix: Some(Rc::from(elem_prefix.unwrap_or_default())) }; + let element = self.new_xml_node(XmlNode { + kind: XmlKind::Element, + name: Some(name), + parent: None, + value: Str::from(""), + attrs: Vec::new(), + children: Vec::new(), + ns, + })?; + self.place_parsed(element, open, roots)?; + let mut unique: HashSet<(Key, Vec)> = HashSet::new(); + for (start, end, value) in raw_attrs { + self.use_fuel()?; + let raw = src.get(start..end).unwrap_or_default(); + let (prefix, local) = self.split_name(raw)?; + if raw == key("xmlns").as_ref() || prefix.is_some_and(|p| p == key("xmlns").as_ref()) { + continue; + } + let (auri, stored_prefix): (Key, Key) = match prefix { + Some(p) => { + let own = self.xml_node(element)?.ns.clone(); + match self.resolve_prefix(p, &own, open)? { + Some(u) => (u, Rc::from(p)), + None => return Err(self.syntax("an XML name uses an undeclared namespace prefix")), + } + } + None => (Rc::from(Vec::new()), Rc::from(Vec::new())), + }; + if !unique.insert((auri.clone(), local.to_vec())) { + return Err(self.type_error("an XML element has two attributes with the same name")); + } + let a = self.new_xml_node(XmlNode { + kind: XmlKind::Attribute, + name: Some(XmlName { uri: Some(auri), local: Rc::from(local), prefix: Some(stored_prefix) }), + parent: Some(element), + value: Str::from_units(value), + attrs: Vec::new(), + children: Vec::new(), + ns: Vec::new(), + })?; + self.xml_charge_link(a)?; + self.xml_node_mut(element)?.attrs.push(a); + } + if !closes { + open.push(element); + names.push((name_start, name_end)); + } + Ok(at) + } + + fn parse_close_tag(&mut self, src: &[u16], pos: usize, open: &mut Vec, names: &mut Vec<(usize, usize)>) -> JsResult { + let name_start = pos + 2; + let name_end = self.read_name(src, name_start)?; + let mut at = name_end; + while src.get(at).copied().is_some_and(is_xml_space) { + at += 1; + } + if src.get(at) != Some(>) { + return Err(self.syntax("expected '>' at the end of a closing tag")); + } + let Some((open_start, open_end)) = names.pop() else { + return Err(self.syntax("a closing tag has no matching open tag")); + }; + if src.get(open_start..open_end) != src.get(name_start..name_end) { + return Err(self.syntax("the closing tag does not match the open tag")); + } + open.pop(); + Ok(at + 1) + } + + /// Parses XML text into nodes, kept reachable through the temporary roots until the caller's + /// scope ends. + pub(crate) fn xml_parse(&mut self, source: &[u16]) -> JsResult> { + let mut held = HeldBytes::new(self); + held.add(self, string_bytes(source.len()).saturating_mul(2))?; + let settings = self.xml_settings()?; + let mut src = source; + let mut offset_start = 0; + while src.get(offset_start).copied().is_some_and(is_xml_space) { + offset_start += 1; + } + if src.get(offset_start..).is_some_and(|r| r.starts_with(&parser_units("")) else { + return Err(self.syntax("unterminated XML declaration")); + }; + src = src.get(end + 2..).unwrap_or_default(); + } + let mut pos = 0; + let mut roots = Vec::new(); + let mut open: Vec = Vec::new(); + let mut names: Vec<(usize, usize)> = Vec::new(); + let mut text: Vec = Vec::new(); + while let Some(&c) = src.get(pos) { + self.use_fuel()?; + if c != LT { + let end = Self::find_from(src, pos, &[LT]).unwrap_or(src.len()); + let section_end = parser_units("]]>"); + if src.get(pos..end).is_some_and(|run| run.windows(3).any(|w| w == section_end)) { + return Err(self.syntax("']]>' cannot appear in XML text")); + } + self.decode_into(src, pos, end, &mut text, false)?; + pos = end; + continue; + } + let rest = src.get(pos..).unwrap_or_default(); + if rest.starts_with(&parser_units("")) else { + return Err(self.syntax("unterminated CDATA section")); + }; + self.xml_text_fuel(end - pos)?; + self.check_units(src, pos + 9, end)?; + text.extend_from_slice(src.get(pos + 9..end).unwrap_or_default()); + pos = end + 3; + continue; + } + self.flush_text(&mut text, settings.ignore_whitespace, &open, &mut roots)?; + if rest.starts_with(&parser_units("&y]]>');"; + truth(&format!("{src} t.children().length() === 5")); + truth(&format!("{src} t.children()[0].nodeKind() === 'text' && t.children()[0].toString() === 'x'")); + truth(&format!("{src} t.children()[1].nodeKind() === 'comment' && t.comments().length() === 1")); + truth(&format!("{src} t.children()[2].nodeKind() === 'processing-instruction' && t.children()[2].name().localName === 'pi'")); + truth(&format!("{src} t.children()[3].nodeKind() === 'element'")); + truth(&format!("{src} t.children()[4].toString() === '&y'")); + truth(&format!("{src} t.@p == '1' && t.@q == '<' && t.attributes().length() === 2 && t.@p.nodeKind() === 'attribute'")); +} + +#[test] +fn serialization_matches_the_fixed_markup() { + assert_eq!(string("(1).toXMLString()"), "\n 1\n \n"); + assert_eq!(string("XML.prettyPrinting = false; (1).toXMLString()"), "1"); + assert_eq!(string("XML.prettyIndent = 4; ().toXMLString()"), "\n \n"); + assert_eq!(string("(<&>).toXMLString()"), "<&>"); + assert_eq!(string("().toXMLString()"), "\n \n"); + assert_eq!(string("(<>).toXMLString()"), "\n"); +} + +#[test] +fn embedded_expressions_are_escaped_by_position() { + assert_eq!(string("var v = '<&>'; ({v}).toXMLString()"), "<&>"); + assert_eq!(string("var v = 'a\"<'; ().toXMLString()"), ""); + assert_eq!(string("var n = 'tag'; (<{n} x=\"1\">t).toXMLString()"), "t"); + assert_eq!(string("var c = 1; ({c}).toXMLString()"), "\n 1\n"); + assert_eq!(string("var a = 'p=\"1\"'; ().toXMLString()"), ""); + truth("var i = 0; var x = {i++}{i++}; x == '01' && i === 2"); + truth("var o = {t: 1}; ({o.t ? 'y' : 'n'}) == 'y'"); + truth("({'}'}) == '}'"); + truth("({1 < 2}) == 'true'"); +} + +#[test] +fn a_computed_tag_name_cannot_add_markup() { + for src in [ + r#"(<{'a x="1"'}/>)"#, + "(<{'a>)", + "(t)", + "(<{''}/>)", + "(<{'1a'}/>)", + "()", + r#"()"#, + ] { + assert_eq!(thrown_name(src), "SyntaxError", "{src}"); + } +} + +#[test] +fn a_computed_name_may_be_any_xml_name() { + assert_eq!(string("(<{'a.b-c'}/>).toXMLString()"), ""); + assert_eq!(string("(<{'tag'}>t).toXMLString()"), "t"); + assert_eq!(string("var n = 'n:b'; (<{n}/>).toXMLString()"), "\n \n"); +} + +#[test] +fn a_computed_attribute_name_matches_the_standard_example() { + let vars = "var tagname = 'name', attributename = 'id', attributevalue = 5, content = 'Fred';"; + let literal = "<{tagname} {attributename}={attributevalue}>{content}"; + assert_eq!(string(&format!("{vars} ({literal}).toXMLString()")), "Fred"); + assert_eq!(string(r#"var a = 'id'; ().toXMLString()"#), ""); +} + +#[test] +fn a_computed_attribute_run_cannot_add_markup() { + for src in [ + r#"()"#, + r#"(<>)"#, + "()", + "()", + r#"()"#, + r#"()"#, + ] { + assert_eq!(thrown_name(src), "SyntaxError", "{src}"); + } +} + +#[test] +fn a_computed_attribute_run_may_hold_any_attributes() { + truth( + r#"var a = " a=\"1\"\n b = '>' "; var x = ; x.@a == '1' && x.@b == '>' && x.attributes().length() === 2 && x.children().length() === 0"#, + ); + assert_eq!(string("().toXMLString()"), ""); + assert_eq!(string("().toXMLString()"), ""); +} + +// ---- selectors and updates ---- + +#[test] +fn selectors_return_the_documented_lists() { + let s = |rest: &str| truth(&format!("{BOOKS} {rest}")); + s("xml.book.length() === 4 && xml.book[0].author == 'Farmer'"); + s("xml.book[0].@category == 'COOKING' && xml.book.@category.length() === 4"); + s("xml.book[1].@category.toString() === 'CHILDREN'"); + s("xml.*.length() === 4 && xml.book[0].*.length() === 3"); + s("xml.book[0].@*.length() === 1"); + s("xml.nothing.length() === 0 && xml.@nothing.length() === 0"); + s("xml..title.length() === 4 && xml..title == xml.descendants('title')"); + s("xml..@lang.length() === 4 && xml..@category.length() === 4"); + s("xml.book.(@category == 'CHILDREN').length() === 2"); + s("xml.book.(year > 1899).title.length() === 2"); + s("xml.book.(author == 'Baum').title == 'Oz'"); + s("xml.*::book.length() === 4"); + s("var n = null; xml.n::book.length() === 4"); + s("xml['book'].length() === 4 && xml.book[0]['author'] == 'Farmer' && xml.book[0]['@category'] == 'COOKING'"); + s("var k = 'category'; xml.book[0].@[k] == 'COOKING'"); +} + +#[test] +fn the_top_sample_has_three_children_two_elements_and_seven_descendants() { + truth(&format!("{TOP} x.children().length() === 3")); + truth(&format!("{TOP} x.elements().length() === 2")); + truth(&format!("{TOP} x.descendants().length() === 7")); +} + +#[test] +fn a_filter_tests_each_item_once_in_order() { + truth( + "var seen = []; var l = <>123; var r = l.(seen.push(toString()), true); seen.join() === '1,2,3' && r.length() === 3", + ); + truth("().(false).length() === 0"); + assert_eq!(thrown_name("({}).(1)"), "TypeError"); + assert_eq!(thrown_name("var o = {}; o.@a"), "TypeError"); + assert_eq!(thrown_name("var o = {}; o..a"), "TypeError"); +} + +#[test] +fn assignment_and_delete_update_the_tree() { + let s = |rest: &str| truth(&format!("{BOOKS} {rest}")); + s( + "xml.book[2].author = 'Dodgeson'; xml.book[2].author.length() === 1 && xml.book[2].author == 'Dodgeson' && xml.book[2].*[1].localName() === 'author'", + ); + s("xml.book[2].author[0] = 'First'; xml.book[2].author.length() === 3 && xml.book[2].author[0] == 'First'"); + s("xml.book[1].year = 1901; xml.book[1].year == '1901'"); + s("xml.book[1].rating = '*****'; xml.book[1].*[3].localName() === 'rating' && xml.book[1].rating == '*****'"); + s("xml.book[1].@category = 'FANTASY'; xml.book[1].@category == 'FANTASY'"); + s("xml.book[1].@added = 'y'; xml.book[1].@added == 'y' && xml.book[1].attributes().length() === 2"); + s("delete xml.book[2].author; xml.book[2].author.length() === 0"); + s("delete xml.book[2].author[1]; xml.book[2].author.length() === 2 && xml.book[2].author[1] == 'C'"); + s("delete xml.book[2].@category; xml.book[2].@category.length() === 0"); + s("xml.book[0] = ; xml.book.length() === 4 && xml.book[0].@id == 'new' && xml.book[0].parent() === xml"); + assert_eq!(thrown_name("var x = ; x[0] = 1"), "TypeError"); + assert_eq!(thrown_name("var x = ; delete x[0]"), "TypeError"); + truth("var x = ; x.b.c = 'd'; x.b.c == 'd'"); + truth("var x = ; x.b += 'v'; x.b == 'v'"); + truth("var x = 1; x.n++; x.n == '2'"); + truth("var x = ; x.@t++; x.@t == '2'"); +} + +#[test] +fn operators_follow_the_standard() { + truth("var l = + ; typeof l === 'xml' && l.length() === 2"); + truth("var l = <> + ; l.length() === 2"); + truth("typeof === 'xml' && typeof <> === 'xml' && typeof new Namespace() === 'object'"); + truth("1 == 1 && !(1 == 2)"); + truth(" == "); + truth("1 == '1' && '1' == 1 && 1 == 1"); + truth("!(01 == 1) && true == true && null == null && undefined == undefined && !(x == null)"); + truth("<> == <> && !(<> == <>)"); + truth("new XMLList() == undefined"); + truth("new Namespace('p', 'u') == new Namespace('q', 'u') && new QName('u', 'a') == new QName('u', 'a')"); + truth("var s = ''; for (var i in <>) s += i; s === '01'"); + truth("var s = ''; for each (var v in <>12) s += v; s === '12'"); + truth("var s = ''; for each (var v in [3, 4]) s += v; s === '34'"); + truth("var s = ''; for each (var v in 7) s += v; s === '7'"); + truth("var x = 5; x.length == '5' && x.length() === 1"); + truth("var x = abc; x.charAt(1) === 'b'"); + truth("var l = <>abc; l.toUpperCase() === 'ABC'"); + assert_eq!(thrown_name("().noSuchMethod()"), "TypeError"); + assert_eq!(thrown_name("(<>).nodeKind()"), "TypeError"); + truth("with (1) { b == '1' }"); +} + +#[test] +fn the_default_namespace_is_scoped_to_the_function() { + truth( + "var ns = new Namespace('u'); default xml namespace = ns; var x = ; x.name().uri === 'u' && x.b.length() === 1 && x.b.name().uri === 'u'", + ); + truth("default xml namespace = 'u'; ().@p.name().uri === ''"); + truth( + "function f() { default xml namespace = 'u'; return ().name().uri; } \ + f() === 'u' && ().name().uri === ''", + ); + truth("function f() { default xml namespace = 'u'; return function () { return ().name().uri; }; } f()() === 'u'"); + truth("setDefaultXMLNamespace(new Namespace('p', 'v')); ().name().uri === 'v' && new QName('b').uri === 'v'"); + truth("default xml namespace = 'u'; new QName('a').uri === 'u' && new QName('*').uri === null"); + truth("var x = ; x.b.length() === 0"); + truth("var x = ; var n = new Namespace('u'); x.n::b.length() === 1"); + truth( + "var soap = new Namespace('http://schemas.xmlsoap.org/soap/envelope/'); default xml namespace = soap; \ + var m = ; \ + m.@soap::encodingStyle == 'e' && m.Body.length() === 1", + ); +} + +// ---- built-ins ---- + +#[test] +fn the_constructors_keep_identity_or_copy_as_specified() { + truth("XML().nodeKind() === 'text' && XML(null) == '' && XML(undefined) == ''"); + truth("var x = ; XML(x) === x && new XML(x) !== x && new XML(x) == x"); + truth("var l = <>; XML(l) === l[0]"); + assert_eq!(thrown_name("XML(<>)"), "TypeError"); + assert_eq!(thrown_name("XML('')"), "SyntaxError"); + assert_eq!(thrown_name("XML('')"), "SyntaxError"); + truth("XMLList().length() === 0 && XMLList('').length() === 2"); + truth("var l = <>; XMLList(l) === l && new XMLList(l) !== l && new XMLList(l)[0] === l[0]"); + truth("var n = new Namespace('p', 'u'); Namespace(n) === n && new Namespace(n) !== n && new Namespace(n).prefix === 'p'"); + truth("var n = new Namespace(); n.prefix === '' && n.uri === '' && n.toString() === ''"); + truth("var n = new Namespace('u'); n.prefix === undefined && n.uri === 'u'"); + truth("new Namespace('1bad', 'u').prefix === undefined"); + assert_eq!(thrown_name("new Namespace('p', '')"), "TypeError"); + truth("var q = new QName('u', 'a'); QName(q) === q && new QName(q) !== q && q.uri === 'u' && q.localName === 'a'"); + truth("var q = new QName(new Namespace('p', 'u'), 'a'); q.uri === 'u' && q.toString() === 'u::a'"); + truth("new QName('*').toString() === '*::*' && new QName(null, 'a').uri === null && new QName().localName === ''"); + truth("isXMLName('abc') && isXMLName('a-b') && !isXMLName('a.b') && !isXMLName('$a') && !isXMLName('1a') && !isXMLName('')"); + truth("().localName() === 'a.b'"); +} + +#[test] +fn the_settings_are_per_realm_and_restore() { + truth( + "var d = XML.defaultSettings(); !d.ignoreComments && !d.ignoreProcessingInstructions && d.ignoreWhitespace && d.prettyPrinting && d.prettyIndent === 2", + ); + truth("var s = XML.settings(); XML.prettyIndent = 7; XML.settings().prettyIndent === 7 && (XML.setSettings(s), XML.prettyIndent === 2)"); + truth("XML.prettyIndent = 7; XML.setSettings(); XML.prettyIndent === 2"); + truth("XML.setSettings({ignoreWhitespace: false, prettyIndent: 'bad'}); !XML.ignoreWhitespace && XML.prettyIndent === 2"); + truth("(new XML('')).children().length() === 2"); + truth("XML.ignoreComments = true; XML.ignoreProcessingInstructions = true; (new XML('')).children().length() === 0"); + truth("XML.ignoreWhitespace = false; (new XML(' ')).children().length() === 3"); + truth("(new XML(' ')).children().length() === 1"); + truth("(new XML(' t ')).toString() === ' t '"); + let mut first = realm(); + first.run("XML.prettyIndent = 9").unwrap(); + assert_eq!(realm().run("XML.prettyIndent"), Ok(JsValue::Number(2.0))); +} + +#[test] +fn the_xml_methods_behave_as_specified() { + truth("var x = ; x.appendChild() === x && x.*[1].localName() === 'c' && x.c.parent() === x"); + truth("var x = ; x.appendChild('t'); x.text() == 't'"); + truth("var x = ; x.prependChild(); x.*[0].localName() === 'c'"); + truth("var x = ; x.insertChildAfter(x.b[0], ); x.*[1].localName() === 'c'"); + truth("var x = ; x.insertChildBefore(x.d[0], ); x.*[1].localName() === 'c'"); + truth("var x = ; x.insertChildAfter(null, ); x.*[0].localName() === 'c'"); + truth("var x = ; x.insertChildBefore(null, ); x.*[1].localName() === 'c'"); + truth("var x = ; x.insertChildAfter(, ) === undefined && x.*.length() === 1"); + truth("var x = ; x.c[0].childIndex() === 1 && x.childIndex() === -1"); + truth("var x = ; x.attribute('p') == '1' && x.attributes().length() === 1"); + truth("var x = t; x.child('b').length() === 1 && x.child(1) == 't' && x.child(9).length() === 0"); + truth("var x = ; x.copy() !== x && x.copy() == x && x.copy().parent() === null && x.b.copy().parent() === undefined"); + truth("var c = ().b[0].copy(); c.parent() === null"); + truth("var x = ; x.contains()"); + truth("var x = ; x.hasOwnProperty('b') && !x.hasOwnProperty('c') && x.hasComplexContent() && !x.hasSimpleContent()"); + truth("var x = t; !x.hasComplexContent() && x.hasSimpleContent() && x.length() === 1"); + truth( + "var x = ; x.b[0].inScopeNamespaces().length === 1 && x.namespaceDeclarations()[0].prefix === 'p' && x.b[0].namespaceDeclarations().length === 0", + ); + truth("var x = ; x.namespace('p').uri === 'u' && x.namespace('q') === undefined && x.namespace().uri === ''"); + truth("var x = ; x.addNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'"); + truth("var x = ; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p') === undefined"); + truth("var x = ; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'"); + truth("var x = ; x.name().localName === 'a' && x.localName() === 'a'"); + truth("(t).*[0].name() === null && (t).*[0].localName() === null"); + truth("var x = 1; x.b[0].appendChild('2'); x.b[0].*.length() === 2 && (x.normalize(), x.b[0].*.length() === 1) && x.b == '12'"); + truth("var x = ; x.b[0].parent() === x && x.parent() === null"); + truth("var x = ; x.processingInstructions().length() === 2 && x.processingInstructions('r').length() === 1"); + truth("var x = ; x.propertyIsEnumerable(0) && !x.propertyIsEnumerable(1)"); + truth("var x = ; x.replace('b', ); x.*.length() === 2 && x.*[0].localName() === 'z' && x.*[1].localName() === 'c'"); + truth("var x = ; x.replace(0, 't'); x.* == 't'"); + truth("var x = ; x.replace('nothing', ) === x && x.b.length() === 1"); + truth("var x = ; x.setChildren( + ) === x && x.*.length() === 2 && x.c.length() === 1"); + truth("var x = ; x.setLocalName('b'); x.localName() === 'b'"); + truth("var x = ; x.setName('b'); x.localName() === 'b'"); + truth("var x = ; x.setNamespace(new Namespace('p', 'u')); x.name().uri === 'u' && x.toXMLString() === ''"); + assert_eq!(thrown_name("().setLocalName('1 x')"), "TypeError"); + assert_eq!(thrown_name("().setName('a b')"), "TypeError"); + assert_eq!(string("(1).toString()"), "1"); + assert_eq!(string("().toString()"), "\n \n"); + truth("var x = ; x.valueOf() === x"); + truth("().xpath === undefined || ().xpath.length() === 0"); + truth("var x = ; x.b.attributes !== undefined"); +} + +#[test] +fn the_xml_list_methods_work_item_by_item() { + let l = "var l = <>s;"; + truth(&format!("{l} l.length() === 2 && l.attribute('p').length() === 2 && l.attributes().length() === 2")); + truth(&format!("{l} l.child('x').length() === 1 && l.children().length() === 4 && l.child(0).length() === 2")); + truth(&format!("{l} l.comments().length() === 1 && l.elements().length() === 2 && l.text() == 's'")); + truth(&format!("{l} l.descendants().length() === 4")); + truth(&format!("{l} l.contains(s) && !l.contains()")); + truth(&format!("{l} l.copy()[0] !== l[0] && l.copy() == l")); + truth(&format!("{l} l.hasOwnProperty(0) && !l.hasOwnProperty(5) && l.propertyIsEnumerable(1) && !l.propertyIsEnumerable(2)")); + truth(&format!("{l} l.hasComplexContent() && !l.hasSimpleContent()")); + truth(&format!("{l} l.parent() === undefined && l.valueOf() === l")); + truth("var x = ; x.*.parent() === x"); + truth("new XMLList().hasSimpleContent() && !new XMLList().hasComplexContent() && new XMLList().parent() === undefined"); + truth("var l = <>12; l.normalize() === l && l.toString() === '1\\n2'"); + truth("var r = 1; var l = r.a; l[0] = 2; r.a == '2' && l[0] == '2'"); + truth("var r = 1; r.a[1] = 'n'; r.a.length() === 2 && r.a[1] == 'n' && r.*[1].parent() === r"); + truth("var r = 1; var list = r.b; list.c = 'v'; r.b.c == 'v'"); + truth("var r = ; r.a.@t[0] = '2'; r.a.@t == '2'"); + truth("var r = 1; r.a[0] = <>

; r.*.length() === 3 && r.*[0].localName() === 'p'"); +} + +#[test] +fn inserting_a_node_moves_it_out_of_its_old_parent() { + truth("var b = ; var a = ; var c = b.c[0]; a.appendChild(c); b.c.length() === 0 && a.c.length() === 1 && c.parent() === a"); + truth("var a = ; a.appendChild(a.b[0]); a.*.length() === 2 && a.*[0].localName() === 'c'"); + truth("var b = ; var a = ; var c = b.c[0]; a.appendChild(c); c.appendChild(b); a..*.length() === 2 && b.parent() === c"); +} + +#[test] +fn namespaces_survive_serialization() { + assert_eq!(string("var x = ; x.toXMLString()"), "\n \n"); + assert_eq!(string("var x = ; x.toXMLString()"), ""); + truth("var x = ; var y = new XML(x.toXMLString()); x == y && y.*[0].name().uri === 'u'"); + truth( + "var q = new QName(new Namespace('u'), 'a'); var x = new XML(''); x.appendChild(); x.*[0].setName(q); (new XML(x.toXMLString())).*[0].name().uri === 'u'", + ); + truth( + "var x = ; x.@[new QName('u', 'b')] = '1'; var y = new XML(x.toXMLString()); y.attributes()[0].name().uri === 'u' && y.attributes()[0] == '1'", + ); +} + +const XML_NS: &str = "http://www.w3.org/XML/1998/namespace"; +const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/"; + +#[test] +fn markup_cannot_declare_a_reserved_namespace_binding() { + let declarations = [ + "xmlns:xml='wrong'".to_string(), + "xmlns:xmlns='u'".to_string(), + format!("xmlns:xmlns='{XMLNS_NS}'"), + format!("xmlns:p='{XML_NS}'"), + format!("xmlns:p='{XMLNS_NS}'"), + format!("xmlns='{XML_NS}'"), + format!("xmlns='{XMLNS_NS}'"), + ]; + for declaration in declarations { + assert_eq!(thrown_name(&format!("")), "SyntaxError", "{declaration}"); + assert_eq!(thrown_name(&format!("new XML(\"\")")), "SyntaxError", "{declaration}"); + } + assert_eq!(string(&format!("().toXMLString()")), format!("")); + assert_eq!(string("().toXMLString()"), ""); +} + +#[test] +fn add_namespace_rejects_a_reserved_binding_and_keeps_the_element() { + for (prefix, uri) in [("xmlns", "u"), ("xmlns", XMLNS_NS), ("xml", "u"), ("p", XML_NS), ("p", XMLNS_NS)] { + let src = + format!("var x = ; var name = 'none'; try {{ x.addNamespace(new Namespace('{prefix}', '{uri}')); }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{prefix} {uri}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{prefix} {uri}"); + } + assert_eq!( + string(&format!("var x = ; x.addNamespace(new Namespace('xml', '{XML_NS}')); x.toXMLString()")), + format!("") + ); +} + +#[test] +fn names_cannot_write_a_reserved_namespace_binding() { + for setter in [ + "setNamespace(new Namespace('xmlns', 'u'))", + "setName(new QName(new Namespace('xml', 'u'), 'b'))", + "setNamespace(new Namespace('p', 'http://www.w3.org/2000/xmlns/'))", + "setNamespace('http://www.w3.org/2000/xmlns/')", + ] { + let src = format!("var x = ; var name = 'none'; try {{ x.{setter}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}"); + } + let q = |prefix: &str| format!("new QName(new Namespace('{prefix}', 'u'), 'b')"); + assert_eq!(string(&format!("var x = ; x[{}] = '1'; x.toXMLString()", q("xmlns"))), "\n 1\n"); + assert_eq!(string(&format!("var x = ; x.@[{}] = '1'; x.toXMLString()", q("xml"))), ""); +} + +#[test] +fn an_attribute_cannot_be_renamed_xmlns() { + for setter in ["setName('xmlns')", "setLocalName('xmlns')"] { + let src = format!("var x = ; var name = 'none'; try {{ x.@b[0].{setter}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}"); + } +} + +#[test] +fn new_nodes_cannot_take_a_reserved_name() { + let q = format!("new QName('{XMLNS_NS}', 'b')"); + for update in ["x.@xmlns = 'u'".to_string(), format!("x[{q}] = '1'"), format!("x.@[{q}] = '1'"), format!("x[{q}][0] = '1'")] { + let src = format!("var x = ; var name = 'none'; try {{ {update}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{update}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{update}"); + } +} + +#[test] +fn the_xmlns_uri_cannot_be_the_default_namespace() { + assert_eq!(thrown_name(&format!("default xml namespace = '{XMLNS_NS}'")), "TypeError"); + assert_eq!(thrown_name(&format!("setDefaultXMLNamespace('{XMLNS_NS}')")), "TypeError"); +} + +#[test] +fn the_garbage_collector_keeps_trees_and_lists_alive() { + let mut r = realm(); + r.run("var x = text; var l = x.b; var ns = new Namespace('p', 'u'); var q = new QName('u', 'n');").unwrap(); + r.collect_garbage(); + assert_eq!(r.run("x.b.@t == '1' && l[0] == 'text' && l[0].parent() === x && ns.uri === 'u' && q.localName === 'n'"), Ok(JsValue::Bool(true))); + r.run("x = l = ns = q = null;").unwrap(); + let live = r.collect_garbage(); + assert!(live < 100_000, "{live}"); +} + +// ---- hostile input ---- + +#[test] +fn malformed_literals_are_located_syntax_errors() { + let cases = [ + "var a = ", + "", + "", + "", + "", + "", + "&bad;", + "�", + "{1", + "{", + "<1x/>", + "", + "", + ",<", + "", + "", + "<>", + "", + ]; + for src in cases { + let e = parse(src, &e4x()).expect_err(src); + assert!(e.line >= 1 && e.column >= 1, "{src}: {e:?}"); + } + let e = parse("var a =\n ;", &e4x()).unwrap_err(); + assert_eq!(e.line, 2); +} + +#[test] +fn bad_xml_text_is_a_catchable_syntax_error() { + for s in [ + "", + "", + "&nope;", + "", + "", + "", + "", + "]]>", + "", + ] { + assert_eq!(thrown_name(&format!("new XML({s:?})")), "SyntaxError", "{s}"); + } + assert_eq!(thrown_name("new XML('')"), "TypeError"); +} + +#[test] +fn unpaired_surrogates_are_rejected_in_xml_text() { + let lone = "String.fromCharCode(0xD800)"; + let low = "String.fromCharCode(0xDC00)"; + for value in [ + format!("'' + {lone} + ''"), + format!("'' + {low} + ''"), + format!("''"), + format!("''"), + format!("''"), + format!("''"), + format!("'<' + {lone} + '/>'"), + ] { + assert_eq!(thrown_name(&format!("new XML({value})")), "SyntaxError", "{value}"); + } + assert_eq!(thrown_name(&format!("().setLocalName({lone})")), "TypeError"); + let e = parse("var a = <\u{FFFD}/>;", &e4x()).unwrap_err(); + assert_eq!((e.line, e.column), (1, 10)); + truth("var p = String.fromCharCode(0xD83D, 0xDE00); var x = new XML('' + p + ''); x == p && x.@b == p"); + truth("var p = String.fromCharCode(0xD83D, 0xDE00); new XML('<' + p + '/>').localName() === p"); +} + +#[test] +fn collections_during_parsing_and_copying_keep_new_nodes() { + let src = "for (var i = 0; i < 200; i++) { \ + var x = new XML('t'); var c = x.copy(); \ + if (c.@p != '1' || c.b.@r != '3' || c.*.length() !== 3 || x.@q != '2') throw new Error('lost'); \ + } 'done'"; + for spare in (16_000..20_000).step_by(37) { + let mut r = realm(); + let live = r.collect_garbage(); + r.set_limits(Limits { heap_bytes: live + spare, ..Limits::default() }); + assert_eq!(r.run(src), Ok(JsValue::String("done".into())), "{spare}"); + } +} + +#[test] +fn collections_during_updates_and_queries_keep_new_values() { + let src = "for (var i = 0; i < 60; i++) { \ + var x = new XML('t'); var c = x.copy(); \ + var l = <>{x.b}{'t'}; \ + c.appendChild(); c.prependChild('s'); c.insertChildAfter(c.b[0], ); c.insertChildBefore(c.b[0], 'n'); \ + c.replace(0, 1); c.n.o = 'v'; c.@t = 'w'; c.z[0] = ; c.g.h = 'deep'; \ + var f = x.*.(nodeKind() === 'element'); var d = x..*; var j = x + c; var m = x.b + ; \ + var ns = new Namespace('p', 'u'); var qn = new QName(ns, 'a'); c.addNamespace(ns); c.setNamespace(ns); c.setName(qn); \ + var scope = c.inScopeNamespaces(); var decl = c.namespaceDeclarations(); c.normalize(); \ + var text = c.toXMLString(); var again = new XML(text); var children = c.children(); var el = c.elements(); var attrs = c.attributes(); \ + var filled = new XMLList(l); var copied = l.copy(); c.setChildren(); \ + if (x.@p != '1' || x.*.length() !== 3 || f.length() !== 1 || j.length() !== 2 || m.length() !== 2) throw new Error('lost'); \ + if (l.length() !== 3 || copied.length() !== 3 || c.*.length() !== 1 || scope.length < 1 || d.length() !== 3) throw new Error('lost'); \ + } 'done'"; + let mut first_error = None; + let mut completed = 0; + for spare in (40_000..56_000).step_by(211) { + let mut r = realm(); + let live = r.collect_garbage(); + r.set_limits(Limits { heap_bytes: live + spare, ..Limits::default() }); + let got = r.run(src); + if got == Ok(JsValue::String("done".into())) { + completed += 1; + } else if !matches!(&got, Err(e) if e.kind == RunErrorKind::Limit(LimitKind::Heap)) { + first_error = Some(format!("{spare}: {got:?}")); + break; + } + } + assert_eq!(first_error, None); + assert!(completed > 0); +} + +#[test] +fn a_scope_name_skips_a_list_without_that_name() { + truth("var outer = ; var inner = <>; with (outer) with (inner) { @id == '1' }"); + truth("var outer = ; var inner = <>; with (outer) with (inner) { @id == '2' }"); +} + +#[test] +fn a_bracket_key_converts_to_a_string_before_it_names_a_child() { + truth("var x = 1; x[true] == '1' && (true in x) && !(false in x)"); + truth("var x = 1; x[null] == '1' && x[undefined].length() === 0 && x[1.5].length() === 0 && x[-1].length() === 0"); + truth("var x = ; x[false] = 'v'; x['false'] == 'v' && (delete x[false], x.*.length() === 0)"); + assert_eq!(thrown_name("().attribute(true)"), "TypeError"); + assert_eq!(thrown_name("().descendants(null)"), "TypeError"); +} + +fn kind_of(limits: Limits, src: &str) -> RunErrorKind { + match limited(limits).run(src) { + Ok(v) => panic!("{} completed with {v:?}", &src[..src.len().min(80)]), + Err(e) => e.kind, + } +} + +fn on_small_stack(f: impl FnOnce() -> T + Send + 'static) -> T { + std::thread::Builder::new().stack_size(512 << 10).spawn(f).unwrap().join().unwrap() +} + +#[test] +fn deep_trees_end_in_errors_on_a_small_stack() { + on_small_stack(|| { + let tags = 100_000; + let literal = format!("{}{}", "".repeat(tags), "".repeat(tags)); + let e = parse(&literal, &e4x()).unwrap_err(); + assert_eq!(e.message, crate::TOO_DEEP); + let text = format!("new XML('{literal}')"); + assert_eq!(limited(Limits::default()).run(&text).map_err(|e| e.message.contains(crate::TOO_DEEP)), Err(true)); + let chain = "var x = ; for (var i = 0; i < 100000; i++) { var y = ; y.appendChild(x); x = y; }"; + assert_eq!(kind_of(Limits::default(), chain), RunErrorKind::Limit(LimitKind::Heap)); + let within = "var x = ; for (var i = 0; i < 150; i++) { var y = ; y.appendChild(x); x = y; } \ + var c = x.copy(); [c == x, x.descendants().length(), x.toXMLString().length > 0, (x.normalize(), 1)].join()"; + assert_eq!(limited(Limits::default()).run(within), Ok(JsValue::String("true,150,true,1".into()))); + }); +} + +#[test] +fn cyclic_insertion_throws_and_leaves_the_tree_alone() { + for src in [ + "x.appendChild(x)", + "x.b[0].appendChild(x)", + "x.b[0].c[0].appendChild(x.b[0])", + "x.b[0].prependChild(x)", + "x.b[0].insertChildAfter(null, x)", + "x.b[0].insertChildBefore(null, x)", + ] { + let setup = "var x = ; var before = x.toXMLString();"; + let script = format!("{setup} var n; try {{ {src}; n = 'none' }} catch (e) {{ n = e.name }} x.toXMLString() === before ? n : 'changed'"); + assert_eq!(string(&script), "TypeError", "{src}"); + } +} + +#[test] +fn replace_inserts_a_copy_so_an_ancestor_can_be_the_value() { + truth("var x = ; x.b[0].replace(0, x); x.b.a.b.c.length() === 1 && x.b.a[0] !== x && x.descendants().length() === 4"); +} + +const TIGHT: Limits = Limits { fuel: 200_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 }; + +#[test] +fn big_values_end_with_the_heap_limit() { + let attrs = format!("var x = new XML('');", (0..50_000).map(|i| format!("a{i}=\"1\"")).collect::>().join(" ")); + assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, &attrs), RunErrorKind::Limit(LimitKind::Heap)); + let wide = "var l = <>; for (;;) { l = l + l; }"; + assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, wide), RunErrorKind::Limit(LimitKind::Heap)); + let giant = "var s = 'x'; for (var i = 0; i < 40; i++) s += s; new XML('' + s + '')"; + assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, giant), RunErrorKind::Limit(LimitKind::Heap)); + let escaped = "var s = '<'; for (var i = 0; i < 17; i++) s += s; ({s}).toXMLString().length"; + assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, escaped), RunErrorKind::Limit(LimitKind::Heap)); +} + +#[test] +fn catastrophic_queries_end_with_the_fuel_limit() { + let build = "var x = ; for (var i = 0; i < 2000; i++) x.appendChild();"; + let limits = Limits { fuel: 1_500_000, heap_bytes: 64 << 20, frames: 200, reentry: 8 }; + let filter = format!("{build} for (;;) x.a.(true);"); + assert_eq!(kind_of(limits.clone(), &filter), RunErrorKind::Limit(LimitKind::Fuel)); + let descendants = format!("{build} for (;;) x..a;"); + assert_eq!(kind_of(limits.clone(), &descendants), RunErrorKind::Limit(LimitKind::Fuel)); + let serialize = format!("{build} for (;;) x.toXMLString();"); + assert_eq!(kind_of(limits, &serialize), RunErrorKind::Limit(LimitKind::Fuel)); +} + +#[test] +fn limits_cannot_be_caught_by_the_script() { + assert_eq!(kind_of(TIGHT, "try { for (;;) ().toXMLString(); } catch (e) { 1 }"), RunErrorKind::Limit(LimitKind::Fuel)); +} + +/// Collects garbage and sets the heap limit to the live size plus the argument, so a script can +/// run out of heap at a chosen charge. A negative argument puts the heap over its limit, which +/// also happens when a lowered limit meets a larger heap. +fn squeeze( + i: &mut crate::interp::Interp, + _: crate::value::Value, + args: &[crate::value::Value], + _: bool, +) -> crate::interp::JsResult { + i.collect_garbage(); + if let Some(crate::value::Value::Number(spare)) = args.first() { + i.limits.heap_bytes = (i.heap.bytes as f64 + *spare) as usize; + } + Ok(crate::value::Value::Undefined) +} + +/// Runs `setup`, then the action for each step from 0 until one completes, and after each stop +/// asserts that `check` is true with the limits lifted. Returns how many runs stopped at `kind`. +fn stops_with_the_tree_intact(setup: &str, action: impl Fn(usize) -> String, check: &str, kind: LimitKind, limit: impl Fn(usize) -> Limits) -> usize { + let mut r = realm(); + let interp = r.interp(); + let global = interp.global; + assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); + let mut stops = 0; + for step in 0..200_000 { + r.set_limits(Limits::default()); + r.run(setup).unwrap(); + r.set_limits(limit(step)); + let action = action(step); + let got = r.run(&action); + r.set_limits(Limits::default()); + assert_eq!(r.run(check), Ok(JsValue::Bool(true)), "{action}: {got:?}"); + match got { + Ok(_) => return stops, + Err(e) if e.kind == RunErrorKind::Limit(kind) => stops += 1, + Err(_) => {} + } + } + panic!("{check} never completed"); +} + +/// Runs `action`, whose `SPARE` is replaced by a call that leaves each number of bytes free in +/// turn, from 512 too few. Actions put it after the steps whose allocations a collection could +/// free: an argument is evaluated after the callee is looked up. +fn heap_stops(setup: &str, action: &str, check: &str) -> usize { + let action = |spare: usize| action.replace("SPARE", &format!("squeeze({})", spare as i64 - 512)); + stops_with_the_tree_intact(setup, action, check, LimitKind::Heap, |_| Limits::default()) +} + +fn fuel_stops(setup: &str, action: &str, check: &str) -> usize { + stops_with_the_tree_intact(setup, |_| action.to_string(), check, LimitKind::Fuel, |fuel| Limits { fuel: fuel as u64, ..Limits::default() }) +} + +const MOVE_SETUP: &str = "var o = ; var t = ; var n = o.b[0];"; +const MOVE_CHECK: &str = + "o.b.length() + t.b.length() === 1 && (o.b.length() === 0 || o.b[0].parent() === o) && (t.b.length() === 0 || t.b[0].parent() === t)"; + +#[test] +fn a_heap_limit_during_a_move_leaves_the_node_in_its_old_parent() { + for action in + ["t.appendChild((SPARE, n))", "t.prependChild((SPARE, n))", "t.insertChildAfter(null, (SPARE, n))", "t.insertChildBefore(null, (SPARE, n))"] + { + assert!(heap_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}"); + } +} + +#[test] +fn a_fuel_limit_during_a_move_leaves_the_node_in_its_old_parent() { + for action in ["t.appendChild(n)", "t.prependChild(n)"] { + assert!(fuel_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}"); + } + let replaced = "var l = t.*; l[0] = o.b[0]"; + let setup = "var o = ; var t = ;"; + assert!(fuel_stops(setup, replaced, MOVE_CHECK) > 0); +} + +#[test] +fn a_heap_limit_on_replace_past_the_end_leaves_no_child_without_a_parent() { + let check = "var n = x.*.length(); if (n === 0) { true } else { \ + var v = x.*[n - 1]; var name; try { v.appendChild(x); name = 'none' } catch (e) { name = e.name } v.parent() === x && name === 'TypeError' }"; + let action = "x.replace({ toString: function () { SPARE; return '5'; } }, v)"; + assert!(heap_stops("var x = ; var v = ;", action, check) > 0); +} + +#[test] +fn limits_during_deletes_and_puts_keep_the_other_nodes() { + let setup = "var x = old;"; + let others = "x.c.length() === 1 && x.@q == '2'"; + for action in ["delete x.b", "delete x.@p", "x.@p = 'v'", "x.@n = 'v'", "x.b = 'v'"] { + assert!(fuel_stops(setup, action, others) > 0, "{action}"); + } + let held = format!("{others} && x.b.length() === 1 && (x.b == 'old' || x.b == 'v') && x.b.*[0].parent() === x.b[0]"); + assert!(fuel_stops(setup, "x.b = 'v'", &held) > 0); + assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &held) > 0); +} + +/// True when the markup of `tree` is `before` or `after`, ignoring white space. +fn tree_is(tree: &str, before: &str, after: &str) -> String { + format!("(function () {{ var s = {tree}.toXMLString().replace(/\\s/g, ''); return s === '{before}' || s === '{after}'; }})()") +} + +#[test] +fn a_limit_while_a_list_replaces_a_child_leaves_the_children() { + let setup = "var x = ; var v = <>

;"; + let replaced = tree_is("x", "", "

"); + for action in ["x.replace(0, v)", "x.b = v"] { + assert!(fuel_stops(setup, action, &replaced) > 0, "{action}"); + } + let spare = "x.replace({ toString: function () { SPARE; return '0'; } }, v)"; + assert!(heap_stops(setup, spare, &replaced) > 0); + let set = tree_is("x", "", "

"); + assert!(fuel_stops(setup, "x.setChildren(v)", &set) > 0); +} + +#[test] +fn a_limit_while_a_list_is_inserted_leaves_the_children() { + let setup = "var o =

; var x = ; var v = o.*;"; + let check = |after: &str| { + format!("{} && (o.*.length() === 0 ? x.*.length() === 3 : o.*.length() === 2 && x.*.length() === 1)", tree_is("x", "", after)) + }; + for (action, after) in [ + ("x.appendChild(v)", "

"), + ("x.prependChild(v)", "

"), + ("x.insertChildAfter(null, v)", "

"), + ("x.insertChildBefore(null, v)", "

"), + ] { + assert!(fuel_stops(setup, action, &check(after)) > 0, "{action}"); + } + assert!(heap_stops(setup, "x.appendChild((SPARE, v))", &check("

")) > 0); +} + +#[test] +fn a_heap_limit_on_a_list_put_at_an_index_keeps_the_list_and_tree_in_step() { + let setup = "var x = ; var l = x.*; var v = <>

;"; + let check = "var s = x.toXMLString().replace(/\\s/g, ''); \ + (s === '' && l.length() === 2 && l[0].name() == 'b') \ + || (s === '

' && l.length() === 3 && l[0].name() == 'p')"; + assert!(heap_stops(setup, "l[(SPARE, 0)] = v", check) > 0); +} + +#[test] +fn an_index_put_of_an_earlier_sibling_keeps_that_node_in_the_list() { + let setup = "var x = ; var l = x.*;"; + truth(&format!("{setup} l[2] = x.b; l[2] === x.b[0] && l[3] === x.e[0]")); + truth(&format!("{setup} l[2] = x.b[0]; l[2] === x.b[0] && l[3] === x.e[0]")); +} + +/// A realm whose trees may be 32 levels deep, and a script line making `deep`, a tree of 32 levels +/// that cannot go under any element. +fn shallow_with_deep_value() -> (Realm, &'static str) { + let deep = "var s = ''; var e = ''; for (var i = 0; i < 32; i++) { s += ''; e += ''; } var deep = new XML(s + e);"; + (Realm::with_options(Options { max_depth: 32, ..e4x() }, Limits::default()), deep) +} + +#[test] +fn a_too_deep_value_leaves_every_match_in_place() { + for action in ["x.replace('b', deep)", "x.b = deep"] { + let (mut r, deep) = shallow_with_deep_value(); + r.run(deep).unwrap(); + r.run("var x = ;").unwrap(); + assert_eq!(r.run(action).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Heap)), "{action}"); + assert_eq!(r.run("x.b.length() === 2 && x.*.length() === 3"), Ok(JsValue::Bool(true)), "{action}"); + } +} + +#[test] +fn a_limit_while_matches_are_replaced_keeps_every_match() { + let setup = "var x = ; var n = ; var l = <>

;"; + let two = ""; + for (action, after) in [ + ("x.replace('b', n)", ""), + ("x.replace('b', l)", "

"), + ("x.b = n", ""), + ("x.b = l", "

"), + ("x.b = 'v'", "v"), + ] { + assert!(fuel_stops(setup, action, &tree_is("x", two, after)) > 0, "{action}"); + } + let by_name = "x.replace({ toString: function () { SPARE; return 'b'; } }, n)"; + assert!(heap_stops(setup, by_name, &tree_is("x", two, "")) > 0); + assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &tree_is("x", two, "v")) > 0); +} + +#[test] +fn matches_are_replaced_in_document_order() { + truth("var x = 123; x.replace('b', ); x.*.length() === 3 && x.*[0].name() == 'n' && x.d.length() === 1"); + truth("var x = 12; var old = x.b[1]; x.b = 'v'; x.b.length() === 1 && x.b == 'v' && old.parent() === null"); + truth("var x = ; x.b = 'v'; x.b == 'v' && x.*.length() === 2 && x.b[0].parent() === x"); +} + +#[test] +fn an_attribute_put_among_children_goes_in_as_text_and_stays_on_its_owner() { + let setup = "var y = ; var x = ;"; + let check = "x.b.length() === 0 && x.children().length() === 1 && x.children()[0].nodeKind() === 'text' && x.toString() === 'v' \ + && y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y"; + for edit in ["x.replace(0, y.@p)", "x.replace(0, y.@p[0])", "x.b = y.@p", "x.setChildren(y.@p)", "x.*[0] = y.@p"] { + truth(&format!("{setup} {edit}; {check}")); + } + let owner = "y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y"; + for (edit, k) in [ + ("x.prependChild(y.@p)", "0"), + ("x.prependChild(y.@p[0])", "0"), + ("x.appendChild(y.@p)", "1"), + ("x.appendChild(y.@p[0])", "1"), + ("x.insertChildAfter(x.b[0], y.@p)", "1"), + ("x.insertChildBefore(x.b[0], y.@p[0])", "0"), + ] { + truth(&format!( + "{setup} {edit}; x.children().length() === 2 && x.children()[{k}].nodeKind() === 'text' && x.children()[{k}] == 'v' && {owner}" + )); + } +} + +#[test] +fn a_list_assigned_attributes_holds_the_text_nodes_in_the_tree() { + truth( + "var y = ; var x = ; var l = x.*; l[0] = y.@*; \ + l.length() === 2 && l[0].nodeKind() === 'text' && l[0].parent() === x && l[1].nodeKind() === 'text' && l[1].parent() === x \ + && x.toString() === 'vw' && y.@*.length() === 2", + ); +} + +#[test] +fn an_attribute_list_goes_in_as_text_at_the_depth_limit() { + let open = "".repeat(7); + let close = "".repeat(7); + let src = format!( + "var x = new XML('{open}{close}'); var c = x; while (c.a.length()) c = c.a[0]; \ + var y = ; c.prependChild(y.@p); c.toString() === 'v'" + ); + let mut realm = Realm::with_options(Options { max_depth: 8, ..e4x() }, Limits::default()); + assert_eq!(realm.run(&src), Ok(JsValue::Bool(true))); +} + +#[test] +fn a_limit_during_normalize_does_not_repeat_text() { + let setup = "var x = ; x.appendChild('p'); x.appendChild('q'); x.appendChild('r');"; + let check = "x.toString() === 'pqr'"; + assert!(fuel_stops(setup, "x.normalize()", check) > 0); +} + +/// Runs `action` as [`heap_stops`] does, and asserts that every run completes or stops at the heap +/// limit and that `check` is true after it. Returns how many runs stopped. +fn heap_stops_only(setup: &str, action: &str, check: &str) -> usize { + let mut r = realm(); + let interp = r.interp(); + let global = interp.global; + assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); + let mut stops = 0; + for spare in 0..100_000i64 { + r.set_limits(Limits::default()); + r.run(setup).unwrap(); + let action = action.replace("SPARE", &format!("squeeze({})", spare - 512)); + let got = r.run(&action); + r.set_limits(Limits::default()); + assert_eq!(r.run(check), Ok(JsValue::Bool(true)), "{action}: {got:?}"); + match got { + Ok(_) => return stops, + Err(e) if e.kind == RunErrorKind::Limit(LimitKind::Heap) => stops += 1, + Err(e) => panic!("{action}: {e:?}"), + } + } + panic!("{action} never completed"); +} + +#[test] +fn collections_while_a_list_is_copied_keep_every_copy() { + let setup = "var x = ; var v = + ; var w = undefined;"; + let set = tree_is("x", "", ""); + for action in ["x.setChildren((SPARE, v))", "x.* = (SPARE, v)"] { + assert!(heap_stops_only(setup, action, &set) > 0, "{action}"); + } + let copied = "w === undefined || (w.length() === 2 && w[0].name() == 'c' && w[1].name() == 'd' && w[0] !== v[0])"; + assert!(heap_stops_only(setup, "w = (SPARE, v).copy()", copied) > 0); +} + +#[test] +fn a_limit_during_path_assignment_leaves_no_new_element() { + let setup = "var x = ; var v = ;"; + for (target, value, after) in [ + ("x.a.b.c", "v", ""), + ("x.a.b", "'text'", "text"), + ("x.a.@p", "'text'", ""), + ("x.a.b[0]", "'text'", "text"), + ] { + let check = tree_is("x", "", after); + assert!(fuel_stops(setup, &format!("{target} = {value}"), &check) > 0, "{target}"); + assert!(heap_stops_only(setup, &format!("{target} = (SPARE, {value})"), &check) > 0, "{target}"); + } +} + +#[test] +fn comparing_xml_values_is_charged_to_the_heap() { + let setup = "var x = t; var y = x.copy();"; + assert!(heap_stops_only(setup, "(SPARE, x == y)", "x == y && y.@q == '2'") > 0); +} + +/// Runs `setup` with no limit, then `action` in the same realm, and returns how `action` ended. +fn after_setup(setup: &str, action: &str) -> Result { + let mut r = realm(); + let interp = r.interp(); + let global = interp.global; + assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); + r.run(setup).unwrap(); + r.run(action).map_err(|e| e.kind) +} + +#[test] +fn namespace_declarations_and_qname_strings_are_charged_to_the_heap() { + let heap = Err(RunErrorKind::Limit(LimitKind::Heap)); + let distinct = "var x = ; var a = []; for (var i = 0; i < 400; i++) a.push(new Namespace('p' + i, 'u' + i));"; + for method in ["addNamespace", "setNamespace"] { + let action = format!("squeeze(4096); for (var i = 0; i < 400; i++) x.{method}(a[i]); 1"); + assert_eq!(after_setup(distinct, &action), heap, "{method}"); + } + let same = "var x = ; var n = new Namespace('p', 'u');"; + let action = "squeeze(4096); for (var i = 0; i < 400; i++) x.addNamespace(n); 1"; + assert_eq!(after_setup(same, action), Ok(JsValue::Number(1.0))); + let long = "var s = 'x'; for (var i = 0; i < 12; i++) s += s; var q = new QName('u', s);"; + let action = "squeeze(12000); var t1 = q.toString(); var t2 = q.toString(); 1"; + assert_eq!(after_setup(long, action), heap); +} + +#[test] +fn an_interrupt_stops_serialization() { + let mut r = realm(); + r.run("var x = ; for (var i = 0; i < 500; i++) x.appendChild();").unwrap(); + let interrupt = r.interrupt(); + r.define_function("stop", move |_| { + interrupt.set(); + Ok(JsValue::Undefined) + }); + assert_eq!(r.run("stop(); x.toXMLString()").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Interrupted))); +} + +// ---- generated input ---- + +const PIECES: &[&str] = &[ + "<", + ">", + "/", + "", + "", + "", + "", + "", + "<>", + "", + "{", + "}", + "{1}", + "'", + "\"", + "&", + "&", + "A", + "&bad;", + "", + "", + "", + "@", + "::", + "..", + ".(", + ")", + "*", + "/x/", + " ", + "\n", + "a", + "1", + "=", + "ns", + ":", + "xmlns", + "x.@a", + "x..y", + "default xml namespace", +]; + +fn tree_markup(shape: &[u8]) -> String { + let mut out = String::new(); + let mut open: Vec = Vec::new(); + out.push_str(""); + for (i, &b) in shape.iter().enumerate() { + match b % 6 { + 0 if open.len() < 20 => { + out.push_str(&format!("")); + open.push(i); + } + 1 => { + if let Some(e) = open.pop() { + out.push_str(&format!("")); + } + } + 2 => out.push_str(&format!("")), + 3 => out.push_str(&format!("t{b}&")), + 4 => out.push_str(&format!("")), + _ => out.push_str(&format!("")), + } + } + while let Some(e) = open.pop() { + out.push_str(&format!("")); + } + out.push_str(""); + out +} + +const TAG_PIECES: [&str; 11] = ["a", "b", " ", "=", "\"", "'", "<", "/", "<", "a=\"1\"", ">; v.children().length() === 0 && v.attributes().length() === 0"), + format!("var v = ; v.children().length() === 0 && v.name().localName === 'x'"), + format!("(<>).length() === 1"), + format!("var v = ; v.children().length() === 1"), + ] { + let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 }); + match r.run(&src) { + Ok(v) => prop_assert_eq!(v, JsValue::Bool(true), "{}", src), + Err(e) => prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}"), + } + } + } + + #[test] + fn source_mixes_parse_or_give_a_located_error(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) { + let src: String = parts.iter().map(|&i| PIECES[i]).collect(); + if let Err(e) = parse(&src, &e4x()) { + prop_assert!(e.line >= 1 && e.column >= 1, "{src:?}: {e:?}"); + } + } + + #[test] + fn random_programs_never_reach_an_internal_error(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) { + let src: String = format!("var x = ; {}", parts.iter().map(|&i| PIECES[i]).collect::()); + let mut r = limited(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 }); + if let Err(e) = r.run(&src) { + prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}"); + } + } + + #[test] + fn xml_text_parses_or_throws(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) { + let text: String = parts.iter().map(|&i| PIECES[i]).collect(); + let quoted = format!("{text:?}"); + let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 }); + if let Err(e) = r.run(&format!("new XML({quoted})")) { + prop_assert!(e.kind == RunErrorKind::Thrown, "{quoted}: {e:?}"); + } + } + + #[test] + fn serialized_trees_parse_to_an_equal_tree(shape in proptest::collection::vec(any::(), 0..80)) { + let markup = tree_markup(&shape); + let quoted = format!("{markup:?}"); + let plain = format!("XML.prettyPrinting = false; XML.ignoreWhitespace = false; var x = new XML({quoted}); var y = new XML(x.toXMLString()); x.normalize(); y.normalize(); x == y"); + prop_assert_eq!(limited(Limits::default()).run(&plain), Ok(JsValue::Bool(true)), "{}", markup); + let pretty = format!("var x = new XML({quoted}); var y = new XML(x.toXMLString()); x.toXMLString() === y.toXMLString()"); + prop_assert_eq!(limited(Limits::default()).run(&pretty), Ok(JsValue::Bool(true)), "{}", markup); + } + + #[test] + fn insert_sequences_stay_acyclic(steps in proptest::collection::vec((0usize..8, 0usize..8), 0..40)) { + let mut src = String::from("var n = []; for (var i = 0; i < 8; i++) n.push(new XML('')); var bad = 0;"); + for (a, b) in steps { + src.push_str(&format!("try {{ n[{a}].appendChild(n[{b}]); }} catch (e) {{ if (e.name !== 'TypeError') bad++; }}")); + } + src.push_str("var ok = bad === 0; for (var j = 0; j < 8; j++) { ok = ok && n[j].toXMLString().length > 0 && n[j].descendants().length() < 8; } ok"); + let got = limited(Limits::default()).run(&src); + prop_assert_eq!(got, Ok(JsValue::Bool(true)), "{}", src); + } +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e5feae68d..f52cee3a7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -751,6 +751,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - `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`. + - `vectorcraft-js` runs ECMAScript for XML (ECMA-357, 2nd edition, https://ecma-international.org/publications-and-standards/standards/ecma-357/) when `Options::e4x` is set, which it is not by default: XML and XMLList literals with embedded `{}` expressions, the `..`, `.@`, `.*`, `.()` and `::` selectors, assignment and `delete` on selected nodes, `for each`, `default xml namespace`, and the `XML`, `XMLList`, `Namespace` and `QName` objects with their methods and settings (`xpath` is not provided). `` in an XML string gives a `SyntaxError`. Parsing, serializing, comparing and copying trees use no native stack, and each takes fuel and counts against the heap limit. Inserting a node into itself or its descendants throws a `TypeError` before the tree changes, a literal or XML string nested past `Options::max_depth` gives a `SyntaxError`, and a tree built past that depth stops at the heap limit. Tests: `crates/js/src/xml_tests.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.