Files
vectorcraft/crates/js/src/ast.rs
T
dotCooCoo e592e8277b JavaScript: parser for ES5 scripts, with cargo xtask test262
New crate vectorcraft-js reads ECMAScript 5.1 source into a syntax tree
with source positions: a UTF-16 lexer (line and column tracking, automatic
semicolon insertion, the regexp and division goals), preprocessor lines
read as directive tokens, a recursive-descent parser to an arena tree, and
the ES5 early errors, strict mode included. Nesting deeper than
Options::max_depth (200 by default) gives a SyntaxError with its line and
column rather than overflowing the stack. Nothing runs scripts yet.

cargo xtask test262 downloads test262 at a pinned commit, checks its
SHA-256, unpacks it into the gitignored corpus/ folder and runs the ES5
syntax tests. A differential test in the engine compares the lexer with
scriptcheck on line counts, directives and unterminated literals.
2026-11-08 13:17:13 +07:00

335 lines
8.1 KiB
Rust

//! The syntax tree: expressions, statements and functions kept in arenas ([`Ast`]) and referred to
//! by index, each with its source span. Nodes hold indices rather than boxes, so dropping a deeply
//! nested tree does not recurse.
use crate::token::Span;
/// An expression's index in [`Ast::exprs`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExprId(pub u32);
/// A statement's index in [`Ast::stmts`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct StmtId(pub u32);
/// A function's index in [`Ast::functions`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FunctionId(pub u32);
/// An identifier as written, with its span.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Name {
pub name: String,
pub span: Span,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnaryOp {
Delete,
Void,
Typeof,
Plus,
Minus,
BitNot,
Not,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdateOp {
Increment,
Decrement,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BinaryOp {
Mul,
Div,
Mod,
Add,
Sub,
Shl,
Shr,
UShr,
Lt,
Gt,
LtEq,
GtEq,
Instanceof,
In,
Eq,
NotEq,
StrictEq,
StrictNotEq,
BitAnd,
BitXor,
BitOr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogicalOp {
And,
Or,
}
/// `=` (`None`) or a compound assignment's operator (`+=` is `Some(BinaryOp::Add)`).
pub type AssignOp = Option<BinaryOp>;
/// A property name in an object literal.
#[derive(Clone, Debug, PartialEq)]
pub enum PropertyKey {
Ident(String),
String(Vec<u16>),
Number(f64),
}
impl PropertyKey {
/// The key as the string a property is stored under (a number through ToString).
pub fn to_utf16(&self) -> Vec<u16> {
match self {
PropertyKey::Ident(name) => name.encode_utf16().collect(),
PropertyKey::String(s) => s.clone(),
PropertyKey::Number(n) => crate::number::to_string(*n).encode_utf16().collect(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PropertyValue {
Init(ExprId),
Get(FunctionId),
Set(FunctionId),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Property {
pub key: PropertyKey,
pub value: PropertyValue,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
This,
Ident(String),
Null,
Bool(bool),
Number(f64),
String(Vec<u16>),
RegExp {
pattern: Vec<u16>,
flags: String,
},
/// An array literal; `None` is a hole (`[1, , 3]`).
Array(Vec<Option<ExprId>>),
Object(Vec<Property>),
Function(FunctionId),
/// `object.property`.
Member {
object: ExprId,
property: Name,
},
/// `object[index]`.
Index {
object: ExprId,
index: ExprId,
},
Call {
callee: ExprId,
args: Vec<ExprId>,
},
New {
callee: ExprId,
args: Vec<ExprId>,
},
Update {
op: UpdateOp,
prefix: bool,
target: ExprId,
},
Unary {
op: UnaryOp,
arg: ExprId,
},
Binary {
op: BinaryOp,
left: ExprId,
right: ExprId,
},
Logical {
op: LogicalOp,
left: ExprId,
right: ExprId,
},
Conditional {
test: ExprId,
consequent: ExprId,
alternate: ExprId,
},
Assign {
op: AssignOp,
target: ExprId,
value: ExprId,
},
/// Comma-separated expressions.
Sequence(Vec<ExprId>),
/// A parenthesized expression, kept so a rule about the bare form (a label, a directive) can
/// tell them apart.
Paren(ExprId),
}
#[derive(Clone, Debug, PartialEq)]
pub struct VarDecl {
pub name: Name,
pub init: Option<ExprId>,
}
/// What a `for (… in …)` statement assigns each property name to.
#[derive(Clone, Debug, PartialEq)]
pub enum ForInTarget {
/// `for (var x in o)`, with the declaration's initializer if it has one.
Var(VarDecl),
Expr(ExprId),
}
#[derive(Clone, Debug, PartialEq)]
pub enum ForInit {
Var(Vec<VarDecl>),
Expr(ExprId),
}
#[derive(Clone, Debug, PartialEq)]
pub struct SwitchCase {
/// `None` for `default`.
pub test: Option<ExprId>,
pub body: Vec<StmtId>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CatchClause {
pub param: Name,
pub body: Vec<StmtId>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Stmt {
Var(Vec<VarDecl>),
FunctionDecl(FunctionId),
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 },
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 },
Throw(ExprId),
Try { block: Vec<StmtId>, handler: Option<CatchClause>, finalizer: Option<Vec<StmtId>> },
Debugger,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FunctionKind {
Declaration,
Expression,
Getter,
Setter,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Function {
pub kind: FunctionKind,
pub name: Option<Name>,
pub params: Vec<Name>,
pub body: Vec<StmtId>,
/// The function's code is strict mode code: its own directive prologue holds `"use strict"`,
/// or the code around it is strict.
pub strict: bool,
pub span: Span,
}
/// The arenas of a parsed script.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Ast {
pub exprs: Vec<(Expr, Span)>,
pub stmts: Vec<(Stmt, Span)>,
pub functions: Vec<Function>,
}
impl Ast {
pub fn expr(&self, id: ExprId) -> Option<&Expr> {
self.exprs.get(id.0 as usize).map(|(e, _)| e)
}
pub fn expr_span(&self, id: ExprId) -> Span {
self.exprs.get(id.0 as usize).map(|(_, s)| *s).unwrap_or_default()
}
pub fn stmt(&self, id: StmtId) -> Option<&Stmt> {
self.stmts.get(id.0 as usize).map(|(s, _)| s)
}
pub fn stmt_span(&self, id: StmtId) -> Span {
self.stmts.get(id.0 as usize).map(|(_, s)| *s).unwrap_or_default()
}
pub fn function(&self, id: FunctionId) -> Option<&Function> {
self.functions.get(id.0 as usize)
}
/// The expression with any parentheses around it removed.
pub fn unparenthesized(&self, mut id: ExprId) -> ExprId {
while let Some(Expr::Paren(inner)) = self.expr(id) {
id = *inner;
}
id
}
pub(crate) fn push_expr(&mut self, expr: Expr, span: Span) -> ExprId {
self.exprs.push((expr, span));
ExprId(u32::try_from(self.exprs.len() - 1).unwrap_or(u32::MAX))
}
pub(crate) fn push_stmt(&mut self, stmt: Stmt, span: Span) -> StmtId {
self.stmts.push((stmt, span));
StmtId(u32::try_from(self.stmts.len() - 1).unwrap_or(u32::MAX))
}
pub(crate) fn push_function(&mut self, function: Function) -> FunctionId {
self.functions.push(function);
FunctionId(u32::try_from(self.functions.len() - 1).unwrap_or(u32::MAX))
}
}
/// A parsed script.
#[derive(Clone, Debug, PartialEq)]
pub struct Program {
pub ast: Ast,
pub body: Vec<StmtId>,
/// The script is strict mode code (its directive prologue holds `"use strict"`, or it was
/// parsed with [`crate::Options::strict`]).
pub strict: bool,
/// Its preprocessor lines, in source order.
pub directives: Vec<crate::token::Directive>,
pub(crate) lines: crate::LineTable,
}
impl Program {
/// The line and column (both from 1, the column in UTF-16 code units) of a source offset.
pub fn line_column(&self, offset: u32) -> (u32, u32) {
self.lines.line_column(offset)
}
/// The number of lines in the source: its line terminators plus one for a last line without
/// one.
pub fn line_count(&self) -> u32 {
self.lines.line_count()
}
}