Merge pull request #6612 from dotCooCoo/feat/js-e4x-xml

JavaScript: run E4X XML literals and the XML, XMLList, Namespace and QName objects behind an option
This commit is contained in:
BFlat
2026-11-12 00:01:49 +07:00
committed by GitHub
15 changed files with 6274 additions and 52 deletions
+102 -9
View File
@@ -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 (`<a b="c">d</a>`), or with `list` an XMLList literal (`<>…</>`).
XmlLiteral {
parts: Vec<XmlPart>,
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<XmlQualifier>,
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<u16>),
/// `{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<StmtId>),
Empty,
If { test: ExprId, consequent: StmtId, alternate: Option<StmtId> },
DoWhile { body: StmtId, test: ExprId },
While { test: ExprId, body: StmtId },
For { init: Option<ForInit>, test: Option<ExprId>, update: Option<ExprId>, body: StmtId },
ForIn { target: ForInTarget, object: ExprId, body: StmtId },
If {
test: ExprId,
consequent: StmtId,
alternate: Option<StmtId>,
},
DoWhile {
body: StmtId,
test: ExprId,
},
While {
test: ExprId,
body: StmtId,
},
For {
init: Option<ForInit>,
test: Option<ExprId>,
update: Option<ExprId>,
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<Name>),
Break(Option<Name>),
Return(Option<ExprId>),
With { object: ExprId, body: StmtId },
Switch { discriminant: ExprId, cases: Vec<SwitchCase> },
Labeled { label: Name, body: StmtId },
With {
object: ExprId,
body: StmtId,
},
Switch {
discriminant: ExprId,
cases: Vec<SwitchCase>,
},
Labeled {
label: Name,
body: StmtId,
},
Throw(ExprId),
Try { block: Vec<StmtId>, handler: Option<CatchClause>, finalizer: Option<Vec<StmtId>> },
Try {
block: Vec<StmtId>,
handler: Option<CatchClause>,
finalizer: Option<Vec<StmtId>>,
},
Debugger,
}
+11 -7
View File
@@ -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<u16>,
/// 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<u16> = 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))
}
+36 -1
View File
@@ -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<u32>]) -> Result<Effect, String> {
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),
})
}
+113 -8
View File
@@ -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<String>) -> CResult<()> {
fn for_in(&mut self, target: &ForInTarget, object: ExprId, body: StmtId, labels: Vec<String>, 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<XmlQualifier>, 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::<Vec<u16>>());
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"),
}
}
+114
View File
@@ -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<XmlNode>),
/// An XMLList value (ECMA-357 section 9.2).
XmlList(Box<XmlListData>),
Namespace(Box<XmlNs>),
/// 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<XmlName>,
attribute: bool,
reference: bool,
},
/// A `.( )` filter in progress.
XmlFilter(Box<XmlFilterState>),
}
/// 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<Key>,
pub(crate) local: Key,
pub(crate) prefix: Option<Key>,
}
/// A namespace. `prefix` is `None` when it is undefined.
#[derive(Clone, Debug)]
pub(crate) struct XmlNs {
pub(crate) prefix: Option<Key>,
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<XmlName>,
pub(crate) parent: Option<ObjId>,
pub(crate) value: Str,
pub(crate) attrs: Vec<ObjId>,
pub(crate) children: Vec<ObjId>,
pub(crate) ns: Vec<XmlNs>,
}
/// 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<ObjId>,
pub(crate) target: Option<ObjId>,
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<ObjId>,
pub(crate) pos: usize,
pub(crate) kept: Vec<ObjId>,
}
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<u32>, 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::<usize>();
}
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<u32>, strings: &mut StringSizes) -> usize {
match e {
Env::Decl { bindings, outer } => {
+114 -16
View File
@@ -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<HeldCount>,
/// 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<std::cell::Cell<bool>>,
/// 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<Value> {
pub(crate) fn pop(&mut self) -> JsResult<Value> {
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<Value> {
pub(crate) fn peek(&self, n: usize) -> JsResult<Value> {
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<Resolved> {
fn resolve(&mut self, name: &[u16]) -> JsResult<Resolved> {
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<Value> {
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<Vec<Key>> {
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<bool> {
pub(crate) fn loose_eq(&mut self, a: &Value, b: &Value) -> JsResult<bool> {
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) {
+21 -1
View File
@@ -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<Punct> {
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();
+15 -2
View File
@@ -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;
+51 -8
View File
@@ -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<Self> {
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<StmtId> {
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<StmtId> {
fn parse_for_in(&mut self, target: ForInTarget, start: Span, each: bool) -> Result<StmtId> {
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<StmtId> {
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<ExprId> {
@@ -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(),
}
}
+439
View File
@@ -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<u16>,
parts: Vec<XmlPart>,
}
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<StmtId> {
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<ExprId> {
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<ExprId> {
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<ExprId> {
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<ExprId> {
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<ExprId> {
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<XmlNamePart> {
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<ExprId> {
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<Option<Vec<u16>>> = 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, "<!--") {
pos = self.xml_comment(pos, &mut scan)?;
} else if self.starts_with(pos, "<![CDATA[") {
pos = self.xml_through(pos, "]]>", &mut scan, "unterminated CDATA section")?;
} else if self.starts_with(pos, "<?") {
pos = self.xml_instruction(pos, &mut scan)?;
} else {
pos = self.xml_open_tag(pos, &mut open, &mut scan)?;
}
} else if c == LBRACE {
if open.is_empty() && !list {
return Err(self.xml_error(pos, "expected an XML tag"));
}
scan.flush();
let (e, after) = self.xml_embedded(pos)?;
scan.parts.push(XmlPart::Content(e));
pos = after;
} else {
if open.is_empty() && !list {
return Err(self.xml_error(pos, "expected an XML tag"));
}
pos = self.xml_text(pos, &mut scan)?;
}
}
scan.flush();
// Reads the token after the literal for the division goal.
let (line, column) = self.lines.line_column(pos as u32);
self.lex.seek(pos, line, column);
self.prev_end = pos as u32;
self.tok = self.next_significant()?;
if let (Some(slot), Some(tokens)) = (token_slot, &mut self.tokens)
&& let Some(token) = tokens.get_mut(slot)
{
token.kind = TokenKind::XmlLiteral;
token.span = Span::new(start.start, pos as u32);
}
Ok(self.ast.push_expr(Expr::XmlLiteral { parts: scan.parts, list }, Span::new(start.start, pos as u32)))
}
fn unit(&self, pos: usize) -> Option<u16> {
self.src.get(pos).copied()
}
fn starts_with(&self, pos: usize, s: &str) -> bool {
self.src.get(pos..).is_some_and(|rest| rest.iter().copied().take(s.len()).eq(s.encode_utf16()))
}
fn xml_error(&self, pos: usize, message: &str) -> crate::SyntaxError {
self.error_at_offset(pos as u32, message)
}
/// Reads `{expression}` with `pos` at its `{`; returns the expression and the offset after `}`.
fn xml_embedded(&mut self, pos: usize) -> Result<(ExprId, usize)> {
let (line, column) = self.lines.line_column(pos as u32 + 1);
self.lex.seek(pos + 1, line, column);
self.tok = self.next_significant()?;
let e = self.parse_expression(false)?;
if !self.at(Punct::RBrace) {
return Err(self.error_at_token(&self.tok, format!("expected '}}' but found {}", describe(&self.tok))));
}
Ok((e, self.tok.span.end as usize))
}
fn xml_copy(&self, from: usize, to: usize, scan: &mut Scan) {
if let Some(units) = self.src.get(from..to) {
scan.text.extend_from_slice(units);
}
}
/// Copies from `pos` through the first `end`, which must be there.
fn xml_through(&self, pos: usize, end: &str, scan: &mut Scan, message: &str) -> Result<usize> {
let mut at = pos;
while at < self.src.len() {
if self.starts_with(at, end) {
let after = at + end.len();
self.xml_copy(pos, after, scan);
return Ok(after);
}
at += 1;
}
Err(self.xml_error(pos, message))
}
fn xml_comment(&self, pos: usize, scan: &mut Scan) -> Result<usize> {
let mut at = pos + 4;
while at < self.src.len() {
if self.starts_with(at, "--") {
if self.unit(at + 2) != Some(GT) {
return Err(self.xml_error(at, "'--' cannot appear in an XML comment"));
}
self.xml_copy(pos, at + 3, scan);
return Ok(at + 3);
}
at += 1;
}
Err(self.xml_error(pos, "unterminated XML comment"))
}
fn xml_instruction(&self, pos: usize, scan: &mut Scan) -> Result<usize> {
if !self.unit(pos + 2).is_some_and(is_name_start) {
return Err(self.xml_error(pos + 2, "invalid processing instruction name"));
}
self.xml_through(pos, "?>", scan, "unterminated processing instruction")
}
/// Reads a static name at `pos`, which must be one, and returns the offset after it.
fn xml_static_name(&self, pos: usize) -> Result<usize> {
scan_name(self.src, pos).ok_or_else(|| self.xml_error(pos, "invalid XML name"))
}
fn xml_skip_space(&self, mut pos: usize, scan: &mut Scan) -> usize {
let from = pos;
while self.unit(pos).is_some_and(is_xml_space) {
pos += 1;
}
self.xml_copy(from, pos, scan);
pos
}
fn xml_open_tag(&mut self, pos: usize, open: &mut Vec<Option<Vec<u16>>>, scan: &mut Scan) -> Result<usize> {
if open.len() >= self.max_depth {
return Err(self.xml_error(pos, TOO_DEEP));
}
scan.text.push(LT);
let mut at = pos + 1;
let name = if self.unit(at) == Some(LBRACE) {
scan.flush();
let (e, after) = self.xml_embedded(at)?;
scan.parts.push(XmlPart::Name(e));
at = after;
None
} else {
let end = self.xml_static_name(at)?;
self.xml_copy(at, end, scan);
let name = self.src.get(at..end).map(<[u16]>::to_vec);
at = end;
name
};
loop {
let before = at;
at = self.xml_skip_space(at, scan);
match self.unit(at) {
None => return Err(self.xml_error(at, "unterminated XML tag")),
Some(SLASH) if self.unit(at + 1) == Some(GT) => {
scan.text.extend_from_slice(&[SLASH, GT]);
return Ok(at + 2);
}
Some(GT) => {
scan.text.push(GT);
open.push(name);
return Ok(at + 1);
}
Some(LBRACE) => {
if at == before {
return Err(self.xml_error(at, "expected white space before attributes"));
}
scan.flush();
let (e, after) = self.xml_embedded(at)?;
let mut next = after;
while self.unit(next).is_some_and(is_xml_space) {
next += 1;
}
if self.unit(next) == Some(EQ) {
scan.parts.push(XmlPart::Name(e));
at = self.xml_attribute_value(after, scan)?;
} else {
scan.parts.push(XmlPart::Attributes(e));
at = after;
}
}
Some(_) if at == before => return Err(self.xml_error(at, "expected white space before an attribute")),
Some(_) => at = self.xml_attribute(at, scan)?,
}
}
}
/// Reads an attribute at `pos` and returns the offset after it.
fn xml_attribute(&mut self, pos: usize, scan: &mut Scan) -> Result<usize> {
let end = self.xml_static_name(pos)?;
self.xml_copy(pos, end, scan);
self.xml_attribute_value(end, scan)
}
/// Reads the `=` and value of an attribute whose name ends at `pos` and returns the offset after them.
fn xml_attribute_value(&mut self, pos: usize, scan: &mut Scan) -> Result<usize> {
let at = self.xml_skip_space(pos, scan);
if self.unit(at) != Some(EQ) {
return Err(self.xml_error(at, "expected '=' after an attribute name"));
}
scan.text.push(EQ);
let at = self.xml_skip_space(at + 1, scan);
match self.unit(at) {
Some(LBRACE) => {
scan.text.push(DQUOTE);
scan.flush();
let (e, after) = self.xml_embedded(at)?;
scan.parts.push(XmlPart::Attribute(e));
scan.text.push(DQUOTE);
Ok(after)
}
Some(q @ (DQUOTE | SQUOTE)) => {
let mut end = at + 1;
loop {
match self.unit(end) {
None => return Err(self.xml_error(at, "unterminated attribute value")),
Some(c) if c == q => break,
Some(LT) => return Err(self.xml_error(end, "'<' cannot appear in an attribute value")),
Some(AMP) => end = self.xml_reference(end)?,
Some(_) => end += 1,
}
}
self.xml_copy(at, end + 1, scan);
Ok(end + 1)
}
_ => Err(self.xml_error(at, "expected an attribute value")),
}
}
fn xml_reference(&self, pos: usize) -> Result<usize> {
match scan_reference(self.src, pos) {
Some((after, _)) => Ok(after),
None => Err(self.xml_error(pos, "invalid XML character or entity reference")),
}
}
fn xml_close_tag(&mut self, pos: usize, open: &mut Vec<Option<Vec<u16>>>, scan: &mut Scan) -> Result<usize> {
scan.text.extend_from_slice(&[LT, SLASH]);
let mut at = pos + 2;
let name = if self.unit(at) == Some(LBRACE) {
scan.flush();
let (e, after) = self.xml_embedded(at)?;
scan.parts.push(XmlPart::Name(e));
at = after;
None
} else {
let end = self.xml_static_name(at)?;
self.xml_copy(at, end, scan);
let name = self.src.get(at..end).map(<[u16]>::to_vec);
at = end;
name
};
at = self.xml_skip_space(at, scan);
if self.unit(at) != Some(GT) {
return Err(self.xml_error(at, "expected '>' at the end of a closing tag"));
}
scan.text.push(GT);
if let (Some(Some(opened)), Some(closed)) = (open.last(), &name)
&& opened != closed
{
return Err(self.xml_error(pos, "the closing tag does not match the open tag"));
}
open.pop();
Ok(at + 1)
}
fn xml_text(&self, pos: usize, scan: &mut Scan) -> Result<usize> {
let mut at = pos;
while let Some(c) = self.unit(at) {
if c == LT || c == LBRACE {
break;
}
at = if c == AMP { self.xml_reference(at)? } else { at + 1 };
}
self.xml_copy(pos, at, scan);
Ok(at)
}
}
+11
View File
@@ -204,6 +204,12 @@ pub enum Punct {
CaretEq,
Slash,
SlashEq,
/// `..`, read only with [`crate::Options::e4x`].
DotDot,
/// `::`, read only with [`crate::Options::e4x`].
ColonColon,
/// `@`, read only with [`crate::Options::e4x`].
At,
}
impl Punct {
@@ -233,6 +239,9 @@ impl Punct {
("|=", Punct::PipeEq),
("^=", Punct::CaretEq),
("/=", Punct::SlashEq),
("..", Punct::DotDot),
("::", Punct::ColonColon),
("@", Punct::At),
("{", Punct::LBrace),
("}", Punct::RBrace),
("(", Punct::LParen),
@@ -341,6 +350,8 @@ pub enum TokenKind {
},
/// A preprocessor line.
Directive(Directive),
/// An XML or XMLList literal; the tokens of its embedded expressions follow it.
XmlLiteral,
}
/// A token, with where it is and the facts the grammar's rules need about it.
+2950
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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). `<!DOCTYPE>` 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.