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