Files
vectorcraft/crates/js/src/lib.rs
T
dotCooCoo f526c5310c JS: XML and XMLList xpath evaluates XPath 1.0 expressions
xml.xpath(expression[, variables]) evaluates an XPath 1.0 expression
(https://www.w3.org/TR/1999/REC-xpath-19991116/) with the XML value as
the context node and returns the selected nodes as an XMLList, in
document order and without repeats. On an XMLList the expression runs
on each element in turn, and the results are joined.

- All 13 axes and the 27 core functions. id() selects nothing, since
  E4X trees carry no DTD data.
- Each property of `variables` binds the variable of its name. A
  prefix in a name test resolves through the context element's
  in-scope namespaces; `xml` is always bound.
- An expression that breaks the grammar gives a SyntaxError. An
  unknown function, a wrong argument count, an unbound prefix or
  variable, a result that is not a node-set, and a result that holds
  the root or a namespace node give a TypeError.
- The parser and the evaluator keep their state on their own stacks,
  so expressions of any depth use no native stack; nesting past
  Options::max_depth gives "nesting too deep". Traversal stacks, node
  lists, strings and the values that string functions build count
  against the heap limit, and every node visited and string read takes
  fuel.

Docs: docs/target-app-parity.md (E4X line), crate doc in lib.rs.
2026-11-11 21:01:32 -08:00

217 lines
9.4 KiB
Rust

//! JavaScript for VectorCraft's scripts: an ECMAScript 5.1 lexer and parser
//! (<https://262.ecma-international.org/5.1/>), with the legacy forms of Annex B that older scripts
//! use (octal literals and escapes outside strict mode, function declarations in blocks, the
//! lenient regular expression patterns).
//!
//! [`parse`] reads a script into a [`Program`]: a syntax tree whose expressions, statements and
//! functions live in arenas and carry their source spans. A script that breaks the grammar or one
//! of its early-error rules (section 16: strict mode's restrictions, duplicate parameters in strict
//! code, invalid assignment targets, `break` and `continue` without a target, regular expression
//! literals that are not valid) gives a [`SyntaxError`] with its line and column.
//!
//! With [`Options::preprocessor_lines`], the lexer also reads the preprocessor lines scripts begin
//! with (`#include`, `#includepath`, `#script`, `#strict`, `#target`, `#targetengine`) into
//! [`Directive`]s. They are kept in [`Program::directives`] and never run.
//!
//! The lexer reads the source as UTF-16 code units, as JavaScript does: columns and spans count
//! code units. Nesting is counted: input nested deeper than [`Options::max_depth`] gives a
//! `SyntaxError` ("nesting too deep") instead of exhausting the stack, binary operators, `if` …
//! `else if` chains, labels and prefix operators are read with loops, and the syntax tree is
//! dropped without recursion.
//!
//! A [`Realm`] runs scripts. It compiles the syntax tree to bytecode, checks the bytecode with a
//! verifier, and runs it on an interpreter that keeps JavaScript calls on its own frame stack, so
//! deep recursion in a script does not use the native stack. Objects and environments live in an
//! arena with a mark-and-sweep collector; strings built by repeated `+` are flattened and dropped
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and these
//! built-ins: the global functions and constants (15.1, with Annex B's `escape` and `unescape`),
//! `Object` (15.2), `Function` with `call`, `apply` and `bind` (15.3), `Array` (15.4), `String`
//! (15.5), `Boolean.prototype`'s `toString` and `valueOf` (15.6.4), `Number` (15.7), `Math` (15.8),
//! `Date` (15.9), `RegExp` (15.10), the error types (15.11) and `JSON` (15.12). The array methods
//! visit only the indices an array or array-like object has, so a sparse array of length 2^32-1
//! costs as much as its elements, and `JSON.parse`, `JSON.stringify` and the reviver walk use no
//! native recursion. Regular expressions run on a backtracking matcher that keeps its choice points
//! on its own stack and takes fuel for every step; `Date` reads the clock and time zone the host
//! sets with [`Realm::set_clock`] and [`Realm::set_time_zone`]. The prototype methods are writable
//! and configurable, so scripts can replace them. Every run is bounded
//! 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`, and XML and XMLList values have an `xpath` method that
//! evaluates an XPath 1.0 expression. 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, and evaluating XPath on them, use no native recursion and take fuel for each
//! node.
#![forbid(unsafe_code)]
pub mod ast;
mod builtins;
mod bytecode;
mod chars;
mod compiler;
mod date_builtins;
mod error;
mod file_classes;
mod heap;
mod host;
mod interp;
mod lexer;
mod number;
mod number_builtins;
mod parser;
mod realm;
mod regexp;
mod regexp_builtins;
mod regexp_vm;
mod string_builtins;
mod token;
mod value;
mod xml;
mod xml_builtins;
mod xpath;
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, XmlNamePart, XmlPart, XmlQualifier,
};
pub use builtins::extensions::Localization;
pub use error::{SyntaxError, TOO_DEEP, TOO_LONG};
pub use host::{Arg, Data, DataLimits, HostError, HostFunction, HostItem, ScriptItem};
pub use lexer::{Goal, Lexer};
pub use number::to_string as number_to_string;
pub use realm::{Interrupt, JsValue, LimitKind, Limits, MAX_UNBROKEN_FUEL, Progress, Realm, RunError, RunErrorKind, UNKNOWN_SOURCE};
pub use token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind};
/// How a script is read.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Options {
/// The script is strict mode code from its start, as code passed to a strict `eval` is.
pub strict: bool,
/// Read preprocessor lines (`#target …` and the other directives) into [`Directive`]s. Off,
/// a `#` is an unexpected character, as ECMA-262 has it.
pub preprocessor_lines: bool,
/// How deeply statements, expressions, functions and regular expression groups may nest.
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, e4x: false }
}
}
/// The default [`Options::max_depth`]: deeper than scripts nest, and shallow enough for the stack
/// of a wasm build or a secondary thread.
pub const DEFAULT_MAX_DEPTH: usize = 200;
/// Parses `source` as a script.
pub fn parse(source: &str, options: &Options) -> Result<Program, SyntaxError> {
let units = to_utf16(source, options)?;
parse_utf16(&units, options)
}
/// Parses `source`, in UTF-16 code units, as a script.
pub fn parse_utf16(source: &[u16], options: &Options) -> Result<Program, SyntaxError> {
check_length(source.len(), options)?;
parser::Parser::new(source, options, false)?.parse_program().map(|(program, _)| program)
}
/// The tokens of `source`, in order, as the parser reads them: preprocessor lines included, and
/// each `/` read as division or as the start of a regular expression literal as the grammar
/// decides. The source must be a valid script.
pub fn tokenize(source: &str, options: &Options) -> Result<Vec<Token>, SyntaxError> {
let units = to_utf16(source, options)?;
check_length(units.len(), options)?;
parser::Parser::new(&units, options, true)?.parse_program().map(|(_, tokens)| tokens)
}
fn to_utf16(source: &str, options: &Options) -> Result<Vec<u16>, SyntaxError> {
// A source's UTF-16 length is at most its UTF-8 length; count only a source that may be too
// long.
if source.len() > options.max_source_units {
check_length(source.encode_utf16().count(), options)?;
}
Ok(source.encode_utf16().collect())
}
fn check_length(units: usize, options: &Options) -> Result<(), SyntaxError> {
if units > options.max_source_units || u32::try_from(units).is_err() {
return Err(SyntaxError { message: TOO_LONG.into(), line: 1, column: 1, offset: 0 });
}
Ok(())
}
/// Where each line of a source starts, for turning offsets into lines and columns.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct LineTable {
starts: Vec<u32>,
/// The source ends with a line terminator.
ends_with_break: bool,
empty: bool,
}
impl LineTable {
pub(crate) fn new(src: &[u16]) -> Self {
let mut starts = vec![0];
let mut i = 0;
while let Some(&u) = src.get(i) {
i += 1;
let terminator = match u {
0x0D if src.get(i) == Some(&0x0A) => {
i += 1;
true
}
0x0A | 0x0D | 0x2028 | 0x2029 => true,
_ => false,
};
if terminator {
starts.push(u32::try_from(i).unwrap_or(u32::MAX));
}
}
let ends_with_break = src.last().is_some_and(|&u| matches!(u, 0x0A | 0x0D | 0x2028 | 0x2029));
Self { starts, ends_with_break, empty: src.is_empty() }
}
pub(crate) fn line_column(&self, offset: u32) -> (u32, u32) {
let line = self.starts.partition_point(|&s| s <= offset).max(1);
let start = self.starts.get(line - 1).copied().unwrap_or(0);
(u32::try_from(line).unwrap_or(u32::MAX), offset.saturating_sub(start) + 1)
}
/// The estimated bytes the table holds.
pub(crate) fn heap_bytes(&self) -> usize {
32 + 4 * self.starts.len()
}
pub(crate) fn line_count(&self) -> u32 {
if self.empty {
return 0;
}
let breaks = self.starts.len() - 1;
u32::try_from(breaks + usize::from(!self.ends_with_break)).unwrap_or(u32::MAX)
}
}
#[cfg(test)]
mod builtins_tests;
#[cfg(test)]
mod extension_tests;
#[cfg(test)]
mod host_tests;
#[cfg(test)]
mod interp_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod text_tests;
#[cfg(test)]
mod xml_tests;