mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 22:22:25 +00:00
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:
+102
-9
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user