ladybird/Libraries/LibJS/Rust/src/ast_dump.rs
Andreas Kling 17d3a285a7 LibJS: Move ScopeData into ScopeArena and reference it by ScopeId
The Rust AST kept every scope in Rc<RefCell<ScopeData>>. The Rc made
the AST !Send (cross-thread codegen needed unsafe impl Send), and the
RefCell added a runtime borrow check on every hot-path read.

AST nodes (Block, FunctionBody, Program, SwitchStatement, SwitchCase)
now hold a ScopeId index into ScopeArena. The scope collector and
codegen take &mut/&ScopeArena, so the borrow checker enforces the
previously-implicit invariant that two phases never touch the same
scope at once.

ParsedProgram is now naturally Send. The unsafe impl Send and the
arc_with_non_send_sync allow go away. CompiledProgram keeps its
hand-rolled Send impl because it carries codegen-time state outside
the AST.

FunctionDeclarationData::is_hoisted was a Cell<bool> only because the
old &[ScopeRecord] traversal couldn't get &mut to the AST. It is now
a plain bool.
2026-05-05 13:53:51 +02:00

1432 lines
53 KiB
Rust

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
//! AST dump matching the C++ ASTDump.cpp output format exactly.
//!
//! Produces tree-drawing output to stdout via `println!`, matching
//! the C++ `outln` calls.
use crate::ast::*;
use std::cell::RefCell;
use std::fmt::Write;
unsafe extern "C" {
// FIXME: This FFI workaround exists only to match C++ float-to-string
// formatting in the AST dump. Once the C++ pipeline is removed,
// this can be deleted and we can use our own formatting.
fn rust_format_double(value: f64, buffer: *mut u8, buffer_len: usize) -> usize;
}
/// Defines a function that maps enum variants to static string slices.
macro_rules! op_to_string {
($name:ident, $enum_type:ty, { $($variant:ident => $str:literal),+ $(,)? }) => {
fn $name(op: $enum_type) -> &'static str {
match op {
$(<$enum_type>::$variant => $str),+
}
}
};
}
/// Prints a node header with the node name, optional extras, and source position.
macro_rules! dump_node {
($state:expr_2021, $name:expr_2021, $range:expr_2021) => {
print_node(
$state,
&format!("{}{}", color_node_name($state, $name), format_position($state, $range)),
)
};
($state:expr_2021, $name:expr_2021, $range:expr_2021, $($extra:expr_2021),+ $(,)?) => {
print_node(
$state,
&{
let mut description = color_node_name($state, $name);
$(description.push_str(&format!(" {}", $extra));)+
description.push_str(&format_position($state, $range));
description
},
)
};
}
// ANSI color codes matching C++ ASTDump.cpp.
const RESET: &str = "\x1b[0m";
const DIM: &str = "\x1b[2m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const CYAN: &str = "\x1b[36m";
const MAGENTA: &str = "\x1b[35m";
const WHITE_BOLD: &str = "\x1b[1;37m";
struct DumpState<'a> {
prefix: String,
is_last: bool,
is_root: bool,
use_color: bool,
output: Option<&'a RefCell<String>>,
function_table: &'a FunctionTable,
arena: &'a crate::ast::AstArena,
}
impl DumpState<'_> {
fn function_table(&self) -> &FunctionTable {
self.function_table
}
fn identifier(&self, id: crate::ast::IdentifierId) -> &crate::ast::Identifier {
&self.arena.identifiers[id]
}
fn name_slice(&self, id: crate::ast::IdentifierId) -> &[u16] {
self.arena.name_slice(id)
}
}
fn print_node(state: &DumpState, text: &str) {
let line = if state.is_root {
text.to_string()
} else {
let connector = if state.is_last {
"\u{2514}\u{2500} "
} else {
"\u{251c}\u{2500} "
};
if state.use_color {
format!("{}{}{}{}{}", state.prefix, DIM, connector, RESET, text)
} else {
format!("{}{}{}", state.prefix, connector, text)
}
};
if let Some(output) = state.output {
let _ = writeln!(output.borrow_mut(), "{line}");
} else {
println!("{line}");
}
}
fn child_prefix(state: &DumpState) -> String {
if state.is_root {
return String::new();
}
let branch = if state.is_last { " " } else { "\u{2502} " };
if state.use_color {
format!("{}{}{}{}", state.prefix, DIM, branch, RESET)
} else {
format!("{}{}", state.prefix, branch)
}
}
fn child_state<'a>(state: &DumpState<'a>, is_last: bool) -> DumpState<'a> {
DumpState {
prefix: child_prefix(state),
is_last,
is_root: false,
use_color: state.use_color,
output: state.output,
function_table: state.function_table,
arena: state.arena,
}
}
fn format_position(state: &DumpState, range: &SourceRange) -> String {
if range.start.line == 0 {
return String::new();
}
if state.use_color {
format!(" {}@{}:{}{}", DIM, range.start.line, range.start.column, RESET)
} else {
format!(" @{}:{}", range.start.line, range.start.column)
}
}
fn color_node_name(state: &DumpState, name: &str) -> String {
if !state.use_color {
return name.to_string();
}
format!("{WHITE_BOLD}{name}{RESET}")
}
fn color_string(state: &DumpState, value: &str) -> String {
if !state.use_color {
return format!("\"{value}\"");
}
format!("{GREEN}\"{value}\"{RESET}")
}
fn color_string_utf16(state: &DumpState, value: &[u16]) -> String {
color_string(state, &utf16_to_string(value))
}
fn color_number_f64(state: &DumpState, value: f64) -> String {
// Match C++ format: integers print as integers, floats as floats.
let s = format_f64(value);
if !state.use_color {
return s;
}
format!("{MAGENTA}{s}{RESET}")
}
fn color_number_bool(state: &DumpState, value: bool) -> String {
let s = if value { "true" } else { "false" };
if !state.use_color {
return s.to_string();
}
format!("{MAGENTA}{s}{RESET}")
}
fn color_number_str(state: &DumpState, value: &str) -> String {
if !state.use_color {
return value.to_string();
}
format!("{MAGENTA}{value}{RESET}")
}
fn color_op(state: &DumpState, op: &str) -> String {
if !state.use_color {
return format!("({op})");
}
format!("({YELLOW}{op}{RESET})")
}
fn color_label(state: &DumpState, label: &str) -> String {
if !state.use_color {
return label.to_string();
}
format!("{DIM}{label}{RESET}")
}
fn color_local(state: &DumpState, kind: &str, index: u32) -> String {
if !state.use_color {
return format!("[{kind}:{index}]");
}
format!("{CYAN}[{kind}:{index}]{RESET}")
}
fn color_global(state: &DumpState) -> String {
if !state.use_color {
return "[global]".to_string();
}
format!("{YELLOW}[global]{RESET}")
}
fn color_flag(state: &DumpState, flag: &str) -> String {
if !state.use_color {
return format!("[{flag}]");
}
format!("{DIM}[{flag}]{RESET}")
}
/// Convert UTF-16 to a valid UTF-8 string, replacing lone surrogates with U+FFFD.
fn utf16_to_string(s: &[u16]) -> String {
char::decode_utf16(s.iter().copied())
.map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER))
.collect()
}
/// Format f64 matching the C++ AK::Formatter<double> output exactly.
///
/// FIXME: This calls into C++ via FFI to guarantee identical output.
/// Once the C++ pipeline is removed, this can be replaced with
/// a native implementation.
fn format_f64(value: f64) -> String {
// C++ AST dump formats JS::Value which uses to_string_without_side_effects(),
// producing "Infinity"/"-Infinity"/"NaN". The rust_format_double FFI uses
// AK's double formatter which produces "inf"/"-inf"/"nan" instead.
if value.is_nan() {
return "NaN".to_string();
}
if value.is_infinite() {
return if value > 0.0 {
"Infinity".to_string()
} else {
"-Infinity".to_string()
};
}
let mut buffer = [0u8; 128];
let length = unsafe { rust_format_double(value, buffer.as_mut_ptr(), buffer.len()) };
std::str::from_utf8(&buffer[..length])
.expect("C++ produced invalid UTF-8")
.to_string()
}
op_to_string!(binary_op_to_string, BinaryOp, {
Addition => "+", Subtraction => "-", Multiplication => "*", Division => "/",
Modulo => "%", Exponentiation => "**", StrictlyEquals => "===",
StrictlyInequals => "!==", LooselyEquals => "==", LooselyInequals => "!=",
GreaterThan => ">", GreaterThanEquals => ">=", LessThan => "<",
LessThanEquals => "<=", BitwiseAnd => "&", BitwiseOr => "|", BitwiseXor => "^",
LeftShift => "<<", RightShift => ">>", UnsignedRightShift => ">>>",
In => "in", InstanceOf => "instanceof",
});
op_to_string!(logical_op_to_string, LogicalOp, {
And => "&&", Or => "||", NullishCoalescing => "??",
});
op_to_string!(unary_op_to_string, UnaryOp, {
BitwiseNot => "~", Not => "!", Plus => "+", Minus => "-",
Typeof => "typeof", Void => "void", Delete => "delete",
});
op_to_string!(assignment_op_to_string, AssignmentOp, {
Assignment => "=", AdditionAssignment => "+=", SubtractionAssignment => "-=",
MultiplicationAssignment => "*=", DivisionAssignment => "/=",
ModuloAssignment => "%=", ExponentiationAssignment => "**=",
BitwiseAndAssignment => "&=", BitwiseOrAssignment => "|=",
BitwiseXorAssignment => "^=", LeftShiftAssignment => "<<=",
RightShiftAssignment => ">>=", UnsignedRightShiftAssignment => ">>>=",
AndAssignment => "&&=", OrAssignment => "||=", NullishAssignment => "??=",
});
op_to_string!(update_op_to_string, UpdateOp, {
Increment => "++", Decrement => "--",
});
op_to_string!(declaration_kind_to_string, DeclarationKind, {
Let => "let", Var => "var", Const => "const",
});
op_to_string!(optional_mode_str, OptionalChainMode, {
Optional => "optional", NotOptional => "not optional",
});
op_to_string!(class_method_kind_to_string, ClassMethodKind, {
Method => "method", Getter => "getter", Setter => "setter",
});
fn dump_labeled_expression(label: &str, expression: &Expression, is_last: bool, state: &DumpState) {
let label_state = child_state(state, is_last);
print_node(&label_state, &color_label(state, label));
dump_expression(expression, &child_state(&label_state, true));
}
fn dump_labeled_statement(label: &str, statement: &Statement, is_last: bool, state: &DumpState) {
let label_state = child_state(state, is_last);
print_node(&label_state, &color_label(state, label));
dump_statement(statement, &child_state(&label_state, true));
}
// ============================================================================
// Entry point
// ============================================================================
pub fn dump_program(
program: &Statement,
use_color: bool,
function_table: &FunctionTable,
arena: &crate::ast::AstArena,
) {
let state = DumpState {
prefix: String::new(),
is_last: false,
is_root: true,
use_color,
output: None,
function_table,
arena,
};
dump_statement(program, &state);
println!();
}
pub fn dump_program_to_string(
program: &Statement,
function_table: &FunctionTable,
arena: &crate::ast::AstArena,
) -> String {
let output = RefCell::new(String::new());
let state = DumpState {
prefix: String::new(),
is_last: false,
is_root: true,
use_color: false,
output: Some(&output),
function_table,
arena,
};
dump_statement(program, &state);
output.into_inner()
}
// ============================================================================
// Statement dumpers
// ============================================================================
fn dump_statement(statement: &Statement, state: &DumpState) {
match &statement.inner {
StatementKind::Empty => {
dump_node!(state, "EmptyStatement", &statement.range);
}
StatementKind::Debugger => {
dump_node!(state, "DebuggerStatement", &statement.range);
}
StatementKind::Expression(expression) => {
dump_node!(state, "ExpressionStatement", &statement.range);
dump_expression(expression, &child_state(state, true));
}
StatementKind::Block(scope) => {
let s = &state.arena.scopes[*scope];
// The parser wraps for-loops in a Block for scope. The C++
// parser does not, so skip the wrapper and dump the child directly.
if s.children.len() == 1 && matches!(s.children[0].inner, StatementKind::For(_) | StatementKind::ForInOf(_))
{
dump_statement(&s.children[0], state);
return;
}
dump_scope_node("BlockStatement", s, &statement.range, state);
}
StatementKind::FunctionBody { scope, .. } => {
let s = &state.arena.scopes[*scope];
dump_scope_node("FunctionBody", s, &statement.range, state);
}
StatementKind::Program(data) => {
let scope = &state.arena.scopes[data.scope];
let mut desc = color_node_name(state, "Program");
let type_str = if data.program_type == ProgramType::Module {
"module"
} else {
"script"
};
desc.push_str(&format!(" {}", color_op(state, type_str)));
if data.is_strict_mode {
desc.push_str(&format!(" {}", color_flag(state, "strict")));
}
if data.has_top_level_await {
desc.push_str(&format!(" {}", color_flag(state, "top-level-await")));
}
desc.push_str(&format_position(state, &statement.range));
print_node(state, &desc);
let children = &scope.children;
for (i, child) in children.iter().enumerate() {
dump_statement(child, &child_state(state, i == children.len() - 1));
}
}
StatementKind::If(data) => {
dump_node!(state, "IfStatement", &statement.range);
let has_alternate = data.alternate.is_some();
dump_labeled_expression("test", &data.test, false, state);
dump_labeled_statement("consequent", &data.consequent, !has_alternate, state);
if let Some(alt) = &data.alternate {
dump_labeled_statement("alternate", alt, true, state);
}
}
StatementKind::While(data) => {
dump_node!(state, "WhileStatement", &statement.range);
dump_labeled_expression("test", &data.test, false, state);
dump_labeled_statement("body", &data.body, true, state);
}
StatementKind::DoWhile(data) => {
dump_node!(state, "DoWhileStatement", &statement.range);
dump_labeled_statement("body", &data.body, false, state);
dump_labeled_expression("test", &data.test, true, state);
}
StatementKind::For(data) => {
dump_node!(state, "ForStatement", &statement.range);
if let Some(init) = &data.init {
let init_state = child_state(state, false);
print_node(&init_state, &color_label(state, "init"));
match init {
ForInit::Expression(expr) => {
dump_expression(expr, &child_state(&init_state, true));
}
ForInit::Declaration(decl) => {
dump_statement(decl, &child_state(&init_state, true));
}
}
}
if let Some(test) = &data.test {
dump_labeled_expression("test", test, false, state);
}
if let Some(update) = &data.update {
dump_labeled_expression("update", update, false, state);
}
dump_labeled_statement("body", &data.body, true, state);
}
StatementKind::ForInOf(data) => {
let name = match data.kind {
ForInOfKind::ForIn => "ForInStatement",
ForInOfKind::ForOf => "ForOfStatement",
ForInOfKind::ForAwaitOf => "ForAwaitOfStatement",
};
dump_node!(state, name, &statement.range);
let lhs_state = child_state(state, false);
print_node(&lhs_state, &color_label(state, "lhs"));
dump_for_in_of_lhs(&data.lhs, &child_state(&lhs_state, true));
dump_labeled_expression("rhs", &data.rhs, false, state);
dump_labeled_statement("body", &data.body, true, state);
}
StatementKind::Switch(data) => {
dump_node!(state, "SwitchStatement", &statement.range);
dump_labeled_expression("discriminant", &data.discriminant, data.cases.is_empty(), state);
for (i, case) in data.cases.iter().enumerate() {
dump_switch_case(case, &child_state(state, i == data.cases.len() - 1), state);
}
}
StatementKind::With(data) => {
dump_node!(state, "WithStatement", &statement.range);
dump_labeled_expression("object", &data.object, false, state);
dump_labeled_statement("body", &data.body, true, state);
}
StatementKind::Labelled(data) => {
dump_node!(
state,
"LabelledStatement",
&statement.range,
color_string_utf16(state, &data.label)
);
dump_statement(&data.item, &child_state(state, true));
}
StatementKind::Break { .. } => {
dump_node!(state, "BreakStatement", &statement.range);
}
StatementKind::Continue { .. } => {
dump_node!(state, "ContinueStatement", &statement.range);
}
StatementKind::Return(argument) => {
dump_node!(state, "ReturnStatement", &statement.range);
if let Some(argument) = argument {
dump_expression(argument, &child_state(state, true));
}
}
StatementKind::Throw(argument) => {
dump_node!(state, "ThrowStatement", &statement.range);
dump_expression(argument, &child_state(state, true));
}
StatementKind::Try(data) => {
dump_node!(state, "TryStatement", &statement.range);
let has_handler = data.handler.is_some();
let has_finalizer = data.finalizer.is_some();
dump_labeled_statement("block", &data.block, !has_handler && !has_finalizer, state);
if let Some(ref handler) = data.handler {
let handler_state = child_state(state, !has_finalizer);
print_node(&handler_state, &color_label(state, "handler"));
dump_catch_clause(handler, &child_state(&handler_state, true), state);
}
if let Some(ref finalizer) = data.finalizer {
dump_labeled_statement("finalizer", finalizer, true, state);
}
}
StatementKind::VariableDeclaration(data) => {
dump_node!(
state,
"VariableDeclaration",
&statement.range,
color_op(state, declaration_kind_to_string(data.kind))
);
for (i, declaration) in data.declarations.iter().enumerate() {
dump_variable_declarator(
declaration,
&child_state(state, i == data.declarations.len() - 1),
state,
);
}
}
StatementKind::UsingDeclaration(declarations) => {
dump_node!(state, "UsingDeclaration", &statement.range);
for (i, declaration) in declarations.iter().enumerate() {
dump_variable_declarator(declaration, &child_state(state, i == declarations.len() - 1), state);
}
}
StatementKind::FunctionDeclaration(data) => {
let function_data = state.function_table().get(data.function_id);
dump_function(function_data, "FunctionDeclaration", &statement.range, state);
}
StatementKind::ClassDeclaration(class_data) => {
dump_node!(state, "ClassDeclaration", &statement.range);
dump_class(class_data, &statement.range, &child_state(state, true), state);
}
StatementKind::Import(data) => {
let module_spec = utf16_to_string(&data.module_request.module_specifier);
let assert_clauses = format_assert_clauses(&data.module_request);
dump_node!(
state,
"ImportStatement",
&statement.range,
format!("from {}{}", color_string(state, &module_spec), assert_clauses)
);
if !data.entries.is_empty() {
for (i, entry) in data.entries.iter().enumerate() {
let import_name = match &entry.import_name {
Some(name) => utf16_to_string(name),
None => "None".to_string(),
};
let local_name = utf16_to_string(&entry.local_name);
print_node(
&child_state(state, i == data.entries.len() - 1),
&format!("ImportName: {import_name}, LocalName: {local_name}"),
);
}
}
}
StatementKind::Export(data) => {
dump_node!(state, "ExportStatement", &statement.range);
let has_statement = data.statement.is_some();
let has_entries = !data.entries.is_empty();
if has_entries {
print_node(&child_state(state, !has_statement), &color_label(state, "entries"));
let entries_state = child_state(state, !has_statement);
for (i, entry) in data.entries.iter().enumerate() {
let export_name = match &entry.export_name {
Some(name) => format!("\"{}\"", utf16_to_string(name)),
None => "null".to_string(),
};
// When the entry is a module re-export, C++ prints
// "null" for LocalName regardless of the stored value.
let local_name = if data.module_request.is_some() {
"null".to_string()
} else {
match &entry.local_or_import_name {
Some(name) => format!("\"{}\"", utf16_to_string(name)),
None => "null".to_string(),
}
};
let mut desc = format!("ExportName: {export_name}, LocalName: {local_name}");
if let Some(ref module_request) = data.module_request {
desc.push_str(&format!(
", ModuleRequest: {}{}",
utf16_to_string(&module_request.module_specifier),
format_assert_clauses(module_request)
));
}
print_node(&child_state(&entries_state, i == data.entries.len() - 1), &desc);
}
}
if let Some(ref statement) = data.statement {
print_node(&child_state(state, true), &color_label(state, "statement"));
let inner_state = &child_state(&child_state(state, true), true);
// For `export default <expression>`, the C++ AST stores the
// expression directly without an ExpressionStatement wrapper.
// Match that by unwrapping StatementKind::Expression here.
if let StatementKind::Expression(ref expression) = statement.inner {
dump_expression(expression, inner_state);
} else {
dump_statement(statement, inner_state);
}
}
}
StatementKind::ClassFieldInitializer(_) => {
// This should not be dumped as it is never part of an actual AST.
}
StatementKind::Error | StatementKind::ErrorDeclaration => {
dump_node!(state, "ErrorStatement", &statement.range);
}
}
}
// ============================================================================
// Expression dumpers
// ============================================================================
fn dump_expression(expression: &Expression, state: &DumpState) {
match &expression.inner {
ExpressionKind::NumericLiteral(value) => {
dump_node!(
state,
"NumericLiteral",
&expression.range,
color_number_f64(state, *value)
);
}
ExpressionKind::StringLiteral(value) => {
dump_node!(
state,
"StringLiteral",
&expression.range,
color_string_utf16(state, value)
);
}
ExpressionKind::BooleanLiteral(value) => {
dump_node!(
state,
"BooleanLiteral",
&expression.range,
color_number_bool(state, *value)
);
}
ExpressionKind::NullLiteral => {
dump_node!(state, "NullLiteral", &expression.range);
}
ExpressionKind::BigIntLiteral(value) => {
dump_node!(
state,
"BigIntLiteral",
&expression.range,
color_number_str(state, value)
);
}
ExpressionKind::RegExpLiteral(data) => {
let pattern = utf16_to_string(&data.pattern);
let flags = utf16_to_string(&data.flags);
dump_node!(
state,
"RegExpLiteral",
&expression.range,
format!("/{}/{}", pattern, flags)
);
}
ExpressionKind::Identifier(ident) => {
dump_identifier(state.identifier(*ident), &expression.range, state);
}
ExpressionKind::PrivateIdentifier(ident) => {
dump_node!(
state,
"PrivateIdentifier",
&expression.range,
color_string_utf16(state, &ident.name)
);
}
ExpressionKind::Binary(data) => {
dump_node!(
state,
"BinaryExpression",
&expression.range,
color_op(state, binary_op_to_string(data.op))
);
dump_expression(&data.lhs, &child_state(state, false));
dump_expression(&data.rhs, &child_state(state, true));
}
ExpressionKind::Logical(data) => {
dump_node!(
state,
"LogicalExpression",
&expression.range,
color_op(state, logical_op_to_string(data.op))
);
dump_expression(&data.lhs, &child_state(state, false));
dump_expression(&data.rhs, &child_state(state, true));
}
ExpressionKind::Unary { op, operand } => {
dump_node!(
state,
"UnaryExpression",
&expression.range,
color_op(state, unary_op_to_string(*op))
);
dump_expression(operand, &child_state(state, true));
}
ExpressionKind::Update(data) => {
let prefix_str = if data.prefixed { "prefix" } else { "postfix" };
dump_node!(
state,
"UpdateExpression",
&expression.range,
format!("({}, {})", update_op_to_string(data.op), prefix_str)
);
dump_expression(&data.argument, &child_state(state, true));
}
ExpressionKind::Assignment(data) => {
dump_node!(
state,
"AssignmentExpression",
&expression.range,
color_op(state, assignment_op_to_string(data.op))
);
match &data.lhs {
AssignmentLhs::Expression(expression) => {
dump_expression(expression, &child_state(state, false));
}
AssignmentLhs::Pattern(pattern) => {
dump_binding_pattern(pattern, &child_state(state, false), state);
}
}
dump_expression(&data.rhs, &child_state(state, true));
}
ExpressionKind::Conditional(data) => {
dump_node!(state, "ConditionalExpression", &expression.range);
dump_labeled_expression("test", &data.test, false, state);
dump_labeled_expression("consequent", &data.consequent, false, state);
dump_labeled_expression("alternate", &data.alternate, true, state);
}
ExpressionKind::Sequence(expressions) => {
dump_node!(state, "SequenceExpression", &expression.range);
for (i, child) in expressions.iter().enumerate() {
dump_expression(child, &child_state(state, i == expressions.len() - 1));
}
}
ExpressionKind::Member(data) => {
let name = if data.computed {
"MemberExpression [computed]"
} else {
"MemberExpression"
};
dump_node!(state, name, &expression.range);
dump_expression(&data.object, &child_state(state, false));
dump_expression(&data.property, &child_state(state, true));
}
ExpressionKind::OptionalChain(data) => {
dump_node!(state, "OptionalChain", &expression.range);
dump_expression(&data.base, &child_state(state, data.references.is_empty()));
for (i, reference) in data.references.iter().enumerate() {
let ref_state = child_state(state, i == data.references.len() - 1);
match reference {
OptionalChainReference::Call { arguments, mode } => {
print_node(&ref_state, &format!("Call({})", optional_mode_str(*mode)));
for (j, argument) in arguments.iter().enumerate() {
dump_expression(&argument.value, &child_state(&ref_state, j == arguments.len() - 1));
}
}
OptionalChainReference::ComputedReference { expression, mode } => {
print_node(&ref_state, &format!("ComputedReference({})", optional_mode_str(*mode)));
dump_expression(expression, &child_state(&ref_state, true));
}
OptionalChainReference::MemberReference { identifier, mode } => {
print_node(&ref_state, &format!("MemberReference({})", optional_mode_str(*mode)));
dump_identifier_id(*identifier, &child_state(&ref_state, true));
}
OptionalChainReference::PrivateMemberReference {
private_identifier,
mode,
} => {
print_node(
&ref_state,
&format!("PrivateMemberReference({})", optional_mode_str(*mode)),
);
print_node(
&child_state(&ref_state, true),
&format!(
"{} {}{}",
color_node_name(state, "PrivateIdentifier"),
color_string_utf16(state, &private_identifier.name),
format_position(state, &private_identifier.range)
),
);
}
}
}
}
ExpressionKind::Call(data) => {
dump_node!(state, "CallExpression", &expression.range);
dump_expression(&data.callee, &child_state(state, data.arguments.is_empty()));
for (i, argument) in data.arguments.iter().enumerate() {
dump_expression(&argument.value, &child_state(state, i == data.arguments.len() - 1));
}
}
ExpressionKind::New(data) => {
dump_node!(state, "NewExpression", &expression.range);
dump_expression(&data.callee, &child_state(state, data.arguments.is_empty()));
for (i, argument) in data.arguments.iter().enumerate() {
dump_expression(&argument.value, &child_state(state, i == data.arguments.len() - 1));
}
}
ExpressionKind::SuperCall(data) => {
dump_node!(state, "SuperCall", &expression.range);
for (i, argument) in data.arguments.iter().enumerate() {
dump_expression(&argument.value, &child_state(state, i == data.arguments.len() - 1));
}
}
ExpressionKind::Spread(target) => {
dump_node!(state, "SpreadExpression", &expression.range);
dump_expression(target, &child_state(state, true));
}
ExpressionKind::This => {
dump_node!(state, "ThisExpression", &expression.range);
}
ExpressionKind::Super => {
dump_node!(state, "SuperExpression", &expression.range);
}
ExpressionKind::Function(function_id) => {
let function_data = state.function_table().get(*function_id);
dump_function(function_data, "FunctionExpression", &expression.range, state);
}
ExpressionKind::Class(class_data) => {
dump_class(class_data, &expression.range, state, state);
}
ExpressionKind::Array(elements) => {
dump_node!(state, "ArrayExpression", &expression.range);
for (i, element) in elements.iter().enumerate() {
let cs = child_state(state, i == elements.len() - 1);
if let Some(element) = element {
dump_expression(element, &cs);
} else {
print_node(&cs, "<elision>");
}
}
}
ExpressionKind::Object(properties) => {
dump_node!(state, "ObjectExpression", &expression.range);
for (i, property) in properties.iter().enumerate() {
dump_object_property(property, &child_state(state, i == properties.len() - 1), state);
}
}
ExpressionKind::TemplateLiteral(data) => {
dump_node!(state, "TemplateLiteral", &expression.range);
for (i, child) in data.expressions.iter().enumerate() {
dump_expression(child, &child_state(state, i == data.expressions.len() - 1));
}
}
ExpressionKind::TaggedTemplateLiteral(data) => {
dump_node!(state, "TaggedTemplateLiteral", &expression.range);
dump_labeled_expression("tag", &data.tag, false, state);
dump_labeled_expression("template", &data.template_literal, true, state);
}
ExpressionKind::MetaProperty(meta_type) => {
let name = match meta_type {
MetaPropertyType::NewTarget => "new.target",
MetaPropertyType::ImportMeta => "import.meta",
};
dump_node!(state, "MetaProperty", &expression.range, name);
}
ExpressionKind::ImportCall(data) => {
dump_node!(state, "ImportCall", &expression.range);
dump_expression(&data.specifier, &child_state(state, data.options.is_none()));
if let Some(ref opts) = data.options {
dump_labeled_expression("options", opts, true, state);
}
}
ExpressionKind::Yield(data) => {
let mut desc = color_node_name(state, "YieldExpression");
if data.is_yield_from {
desc.push_str(&format!(" {}", color_flag(state, "yield*")));
}
desc.push_str(&format_position(state, &expression.range));
print_node(state, &desc);
if let Some(ref argument) = data.argument {
dump_expression(argument, &child_state(state, true));
}
}
ExpressionKind::Await(argument) => {
dump_node!(state, "AwaitExpression", &expression.range);
dump_expression(argument, &child_state(state, true));
}
ExpressionKind::Error => {
dump_node!(state, "ErrorExpression", &expression.range);
}
}
}
// ============================================================================
// Identifier dumper
// ============================================================================
fn dump_identifier_id(id: crate::ast::IdentifierId, state: &DumpState) {
let ident = state.identifier(id);
let range = ident.range;
dump_identifier(ident, &range, state);
}
fn dump_identifier(ident: &Identifier, range: &SourceRange, state: &DumpState) {
let mut desc = color_node_name(state, "Identifier");
desc.push_str(&format!(
" {}",
color_string_utf16(state, state.arena.strings[ident.name].as_slice())
));
if ident.is_local() {
let kind = if ident.local_type == Some(LocalType::Argument) {
"argument"
} else {
"variable"
};
desc.push_str(&format!(" {}", color_local(state, kind, ident.local_index)));
} else if ident.is_global {
desc.push_str(&format!(" {}", color_global(state)));
}
if let Some(declaration_kind) = ident.declaration_kind {
desc.push_str(&format!(
" {}",
color_op(state, declaration_kind_to_string(declaration_kind))
));
}
if ident.is_inside_scope_with_eval {
desc.push_str(&format!(" {}", color_flag(state, "in-eval-scope")));
}
desc.push_str(&format_position(state, range));
print_node(state, &desc);
}
// ============================================================================
// Helper dumpers
// ============================================================================
fn dump_scope_node(class_name: &str, scope: &ScopeData, range: &SourceRange, state: &DumpState) {
dump_node!(state, class_name, range);
for (i, child) in scope.children.iter().enumerate() {
dump_statement(child, &child_state(state, i == scope.children.len() - 1));
}
}
fn dump_function(function_data: &FunctionData, class_name: &str, range: &SourceRange, state: &DumpState) {
let mut desc = color_node_name(state, class_name);
let is_async = function_data.kind == FunctionKind::Async || function_data.kind == FunctionKind::AsyncGenerator;
let is_generator =
function_data.kind == FunctionKind::Generator || function_data.kind == FunctionKind::AsyncGenerator;
if is_async {
desc.push_str(" async");
}
if is_generator {
desc.push('*');
}
let name_str = match function_data.name {
Some(id) => utf16_to_string(state.name_slice(id)),
None => String::new(),
};
desc.push_str(&format!(" {}", color_string(state, &name_str)));
if function_data.is_strict_mode {
desc.push_str(&format!(" {}", color_flag(state, "strict")));
}
if function_data.is_arrow_function {
desc.push_str(&format!(" {}", color_flag(state, "arrow")));
}
if function_data.parsing_insights.contains_direct_call_to_eval {
desc.push_str(&format!(" {}", color_flag(state, "direct-eval")));
}
if function_data.parsing_insights.uses_this {
desc.push_str(&format!(" {}", color_flag(state, "uses-this")));
}
if function_data.parsing_insights.uses_this_from_environment {
desc.push_str(&format!(" {}", color_flag(state, "uses-this-from-environment")));
}
if function_data.parsing_insights.might_need_arguments_object {
desc.push_str(&format!(" {}", color_flag(state, "might-need-arguments")));
}
desc.push_str(&format_position(state, range));
print_node(state, &desc);
if !function_data.parameters.is_empty() {
print_node(&child_state(state, false), &color_label(state, "parameters"));
let parameters_state = child_state(state, false);
for (i, parameter) in function_data.parameters.iter().enumerate() {
let parameter_state = child_state(&parameters_state, i == function_data.parameters.len() - 1);
let has_default = parameter.default_value.is_some();
if parameter.is_rest {
print_node(&parameter_state, &color_label(state, "rest"));
match &parameter.binding {
FunctionParameterBinding::Identifier(id) => {
dump_identifier_id(*id, &child_state(&parameter_state, !has_default));
}
FunctionParameterBinding::BindingPattern(pattern) => {
dump_binding_pattern(pattern, &child_state(&parameter_state, !has_default), state);
}
}
} else {
match &parameter.binding {
FunctionParameterBinding::Identifier(id) => {
dump_identifier_id(
*id,
&child_state(&parameters_state, i == function_data.parameters.len() - 1),
);
}
FunctionParameterBinding::BindingPattern(pattern) => {
dump_binding_pattern(
pattern,
&child_state(&parameters_state, i == function_data.parameters.len() - 1),
state,
);
}
}
}
if has_default {
print_node(&child_state(&parameter_state, true), &color_label(state, "default"));
dump_expression(
parameter.default_value.as_ref().expect("guarded by is_some check"),
&child_state(&child_state(&parameter_state, true), true),
);
}
}
}
print_node(&child_state(state, true), &color_label(state, "body"));
dump_statement(&function_data.body, &child_state(&child_state(state, true), true));
}
fn dump_class(class_data: &ClassData, range: &SourceRange, state: &DumpState, root_state: &DumpState) {
let name_str = match class_data.name {
Some(id) => utf16_to_string(state.name_slice(id)),
None => String::new(),
};
print_node(
state,
&format!(
"{} {}{}",
color_node_name(root_state, "ClassExpression"),
color_string(root_state, &name_str),
format_position(root_state, range),
),
);
let has_super = class_data.super_class.is_some();
let has_elements = !class_data.elements.is_empty();
if has_super {
print_node(&child_state(state, false), &color_label(root_state, "super class"));
dump_expression(
class_data
.super_class
.as_ref()
.expect("guarded by has_super_class check"),
&child_state(&child_state(state, false), true),
);
}
if let Some(ref constructor) = class_data.constructor {
print_node(
&child_state(state, !has_elements),
&color_label(root_state, "constructor"),
);
dump_expression(constructor, &child_state(&child_state(state, !has_elements), true));
}
if has_elements {
print_node(&child_state(state, true), &color_label(root_state, "elements"));
for (i, element) in class_data.elements.iter().enumerate() {
dump_class_element(
&element.inner,
&element.range,
&child_state(&child_state(state, true), i == class_data.elements.len() - 1),
root_state,
);
}
}
}
fn dump_class_element(element: &ClassElement, range: &SourceRange, state: &DumpState, root_state: &DumpState) {
match element {
ClassElement::Method {
key,
function,
kind,
is_static,
} => {
let mut desc = color_node_name(root_state, "ClassMethod");
if *is_static {
desc.push_str(" static");
}
if *kind != ClassMethodKind::Method {
desc.push_str(&format!(
" {}",
color_op(root_state, class_method_kind_to_string(*kind))
));
}
desc.push_str(&format_position(root_state, range));
print_node(state, &desc);
dump_expression(key, &child_state(state, false));
dump_expression(function, &child_state(state, true));
}
ClassElement::Field {
key,
initializer,
is_static,
} => {
let mut desc = color_node_name(root_state, "ClassField");
if *is_static {
desc.push_str(" static");
}
desc.push_str(&format_position(root_state, range));
print_node(state, &desc);
dump_expression(key, &child_state(state, initializer.is_none()));
if let Some(init) = initializer {
print_node(&child_state(state, true), &color_label(root_state, "initializer"));
dump_expression(init, &child_state(&child_state(state, true), true));
}
}
ClassElement::StaticInitializer { body } => {
print_node(
state,
&format!(
"{}{}",
color_node_name(root_state, "StaticInitializer"),
format_position(root_state, range)
),
);
dump_statement(body, &child_state(state, true));
}
}
}
fn dump_binding_pattern(pattern: &BindingPattern, state: &DumpState, root_state: &DumpState) {
let kind_str = match pattern.kind {
BindingPatternKind::Array => "array",
BindingPatternKind::Object => "object",
};
print_node(
state,
&format!(
"{} {}",
color_node_name(root_state, "BindingPattern"),
color_op(root_state, kind_str)
),
);
for (i, entry) in pattern.entries.iter().enumerate() {
let entry_state = child_state(state, i == pattern.entries.len() - 1);
if pattern.kind == BindingPatternKind::Array && is_elision(entry) {
print_node(&entry_state, &color_node_name(root_state, "Elision"));
continue;
}
let mut label = "entry".to_string();
if entry.is_rest {
label.push_str(" (rest)");
}
print_node(&entry_state, &color_label(root_state, &label));
let has_alias = entry.alias.is_some();
let has_initializer = entry.initializer.is_some();
if pattern.kind == BindingPatternKind::Object {
match &entry.name {
Some(BindingEntryName::Identifier(ident)) => {
print_node(
&child_state(&entry_state, !has_alias && !has_initializer),
&color_label(root_state, "name"),
);
dump_identifier_id(
*ident,
&child_state(&child_state(&entry_state, !has_alias && !has_initializer), true),
);
}
Some(BindingEntryName::Expression(expression)) => {
print_node(
&child_state(&entry_state, !has_alias && !has_initializer),
&color_label(root_state, "name (computed)"),
);
dump_expression(
expression,
&child_state(&child_state(&entry_state, !has_alias && !has_initializer), true),
);
}
None => {}
}
}
if let Some(ref alias) = entry.alias {
print_node(
&child_state(&entry_state, !has_initializer),
&color_label(root_state, "alias"),
);
match alias {
BindingEntryAlias::Identifier(ident) => {
dump_identifier_id(*ident, &child_state(&child_state(&entry_state, !has_initializer), true));
}
BindingEntryAlias::BindingPattern(sub) => {
dump_binding_pattern(
sub,
&child_state(&child_state(&entry_state, !has_initializer), true),
root_state,
);
}
BindingEntryAlias::MemberExpression(expression) => {
dump_expression(
expression,
&child_state(&child_state(&entry_state, !has_initializer), true),
);
}
}
}
if has_initializer {
print_node(
&child_state(&entry_state, true),
&color_label(root_state, "initializer"),
);
dump_expression(
entry.initializer.as_ref().expect("guarded by is_some check"),
&child_state(&child_state(&entry_state, true), true),
);
}
}
}
fn is_elision(entry: &BindingEntry) -> bool {
entry.name.is_none() && entry.alias.is_none() && entry.initializer.is_none() && !entry.is_rest
}
fn dump_variable_declarator(declaration: &VariableDeclarator, state: &DumpState, root_state: &DumpState) {
print_node(
state,
&format!(
"{}{}",
color_node_name(root_state, "VariableDeclarator"),
format_position(root_state, &declaration.range)
),
);
let has_init = declaration.init.is_some();
match &declaration.target {
VariableDeclaratorTarget::Identifier(ident) => {
dump_identifier_id(*ident, &child_state(state, !has_init));
}
VariableDeclaratorTarget::BindingPattern(pattern) => {
dump_binding_pattern(pattern, &child_state(state, !has_init), root_state);
}
}
if let Some(ref init) = declaration.init {
dump_expression(init, &child_state(state, true));
}
}
fn dump_object_property(property: &ObjectProperty, state: &DumpState, root_state: &DumpState) {
if property.property_type == ObjectPropertyType::Spread {
print_node(
state,
&format!(
"{} {}{}",
color_node_name(root_state, "ObjectProperty"),
color_op(root_state, "spread"),
format_position(root_state, &property.range)
),
);
dump_expression(&property.key, &child_state(state, true));
} else {
let mut desc = color_node_name(root_state, "ObjectProperty");
if property.is_method {
desc.push_str(&format!(" {}", color_op(root_state, "method")));
} else if property.property_type == ObjectPropertyType::Getter {
desc.push_str(&format!(" {}", color_op(root_state, "getter")));
} else if property.property_type == ObjectPropertyType::Setter {
desc.push_str(&format!(" {}", color_op(root_state, "setter")));
}
desc.push_str(&format_position(root_state, &property.range));
print_node(state, &desc);
dump_expression(&property.key, &child_state(state, false));
if let Some(ref value) = property.value {
dump_expression(value, &child_state(state, true));
}
}
}
fn dump_catch_clause(clause: &CatchClause, state: &DumpState, root_state: &DumpState) {
print_node(
state,
&format!(
"{}{}",
color_node_name(root_state, "CatchClause"),
format_position(root_state, &clause.range)
),
);
if let Some(parameter) = &clause.parameter {
match parameter {
CatchBinding::Identifier(ident) => {
print_node(&child_state(state, false), &color_label(root_state, "parameter"));
dump_identifier_id(*ident, &child_state(&child_state(state, false), true));
}
CatchBinding::BindingPattern(pattern) => {
print_node(&child_state(state, false), &color_label(root_state, "parameter"));
dump_binding_pattern(pattern, &child_state(&child_state(state, false), true), root_state);
}
}
}
dump_statement(&clause.body, &child_state(state, true));
}
fn dump_switch_case(case: &SwitchCase, state: &DumpState, root_state: &DumpState) {
if let Some(ref test) = case.test {
print_node(
state,
&format!(
"{}{}",
color_node_name(root_state, "SwitchCase"),
format_position(root_state, &case.range)
),
);
print_node(&child_state(state, false), &color_label(root_state, "test"));
dump_expression(test, &child_state(&child_state(state, false), true));
} else {
print_node(
state,
&format!(
"{} {}{}",
color_node_name(root_state, "SwitchCase"),
color_op(root_state, "default"),
format_position(root_state, &case.range)
),
);
}
print_node(&child_state(state, true), &color_label(root_state, "consequent"));
let consequent_state = child_state(&child_state(state, true), true);
let scope = &state.arena.scopes[case.scope];
let children = &scope.children;
for (i, child) in children.iter().enumerate() {
dump_statement(child, &child_state(&consequent_state, i == children.len() - 1));
}
}
fn dump_for_in_of_lhs(lhs: &ForInOfLhs, state: &DumpState) {
match lhs {
ForInOfLhs::Declaration(declaration) => dump_statement(declaration, state),
ForInOfLhs::Expression(expression) => dump_expression(expression, state),
ForInOfLhs::Pattern(pattern) => {
dump_binding_pattern(pattern, state, state);
}
}
}
fn format_assert_clauses(request: &ModuleRequest) -> String {
if request.attributes.is_empty() {
return String::new();
}
let mut result = " [".to_string();
for (i, attr) in request.attributes.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(&format!(
"{}: {}",
utf16_to_string(&attr.key),
utf16_to_string(&attr.value)
));
}
result.push(']');
result
}