use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use hermes_ast::context::Context;
use hermes_ast::dump::{ESTreeDumpMode, ESTreeRawProp, LocationDumpMode, dump_estree_json_with_sm};
use hermes_ast::node::{Node, NodeKind};
use hermes_parser::js::{JSParserImpl, ParserPass};
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_support::manager::SourceErrorManager;
struct FuncEntry {
kind: NodeKind,
is_lazy_stub: bool,
param_yield: bool,
param_await: bool,
body_start_offset: u32,
}
fn block_info(node: &Node<'_>) -> (bool, bool, bool) {
if let Node::BlockStatement(b) = node {
(
b.is_lazy_function_body.get(),
b.param_yield.get(),
b.param_await.get(),
)
} else {
(false, false, false)
}
}
fn func_value_block_info<'gc>(value: &'gc Node<'gc>) -> Option<(bool, bool, bool)> {
if let Node::FunctionExpression(fe) = value {
if let Node::BlockStatement(_) = fe.body {
return Some(block_info(fe.body));
}
}
None
}
fn collect_funcs<'gc>(node: &'gc Node<'gc>, out: &mut BTreeMap<u32, FuncEntry>) {
match node {
Node::FunctionDeclaration(fd) => {
if fd.id.is_none() {
collect_funcs(fd.body, out);
return;
}
let (is_stub, py, pa) = block_info(fd.body);
out.insert(
node.range().start.offset,
FuncEntry {
kind: NodeKind::FunctionDeclaration,
is_lazy_stub: is_stub,
param_yield: py,
param_await: pa,
body_start_offset: fd.body.range().start.offset,
},
);
collect_funcs(fd.body, out);
}
Node::FunctionExpression(fe) => {
let (is_stub, py, pa) = block_info(fe.body);
out.insert(
node.range().start.offset,
FuncEntry {
kind: NodeKind::FunctionExpression,
is_lazy_stub: is_stub,
param_yield: py,
param_await: pa,
body_start_offset: fe.body.range().start.offset,
},
);
collect_funcs(fe.body, out);
}
Node::ArrowFunctionExpression(afe) => {
if let Node::BlockStatement(_) = afe.body {
let (is_stub, py, pa) = block_info(afe.body);
out.insert(
node.range().start.offset,
FuncEntry {
kind: NodeKind::ArrowFunctionExpression,
is_lazy_stub: is_stub,
param_yield: py,
param_await: pa,
body_start_offset: afe.body.range().start.offset,
},
);
collect_funcs(afe.body, out);
} else {
collect_funcs(afe.body, out);
}
}
Node::Property(prop) => {
if let Some((is_stub, py, pa)) = func_value_block_info(prop.value) {
let body_start = if let Node::FunctionExpression(fe) = prop.value {
fe.body.range().start.offset
} else {
0
};
out.insert(
node.range().start.offset,
FuncEntry {
kind: NodeKind::Property,
is_lazy_stub: is_stub,
param_yield: py,
param_await: pa,
body_start_offset: body_start,
},
);
if let Node::FunctionExpression(fe) = prop.value {
collect_funcs(fe.body, out);
}
} else {
collect_children(node, out);
}
}
Node::MethodDefinition(md) => {
if let Some((is_stub, py, pa)) = func_value_block_info(md.value) {
let body_start = if let Node::FunctionExpression(fe) = md.value {
fe.body.range().start.offset
} else {
0
};
out.insert(
node.range().start.offset,
FuncEntry {
kind: NodeKind::MethodDefinition,
is_lazy_stub: is_stub,
param_yield: py,
param_await: pa,
body_start_offset: body_start,
},
);
if let Node::FunctionExpression(fe) = md.value {
collect_funcs(fe.body, out);
}
} else {
collect_children(node, out);
}
}
_ => collect_children(node, out),
}
}
fn collect_children<'gc>(node: &'gc Node<'gc>, out: &mut BTreeMap<u32, FuncEntry>) {
node.visit_children(&mut ChildVisitor(out));
}
struct ChildVisitor<'a>(pub &'a mut BTreeMap<u32, FuncEntry>);
impl<'gc> hermes_ast::visitor::Visitor<'gc> for ChildVisitor<'_> {
fn visit_node(&mut self, node: &'gc Node<'gc>) {
collect_funcs(node, self.0);
}
}
fn collect_eager_body_strings<'gc>(
node: &'gc Node<'gc>,
atoms: &hermes_atom_table::AtomTable,
sm: &hermes_support::manager::SourceErrorManager,
out: &mut BTreeMap<u32, String>,
) {
match node {
Node::FunctionDeclaration(fd) => {
if fd.id.is_none() {
collect_eager_body_strings(fd.body, atoms, sm, out);
return;
}
let start = node.range().start.offset;
out.insert(start, dump_node(fd.body, atoms, sm));
collect_eager_body_strings(fd.body, atoms, sm, out);
}
Node::FunctionExpression(fe) => {
let start = node.range().start.offset;
out.insert(start, dump_node(fe.body, atoms, sm));
collect_eager_body_strings(fe.body, atoms, sm, out);
}
Node::ArrowFunctionExpression(afe) => {
if let Node::BlockStatement(_) = afe.body {
let start = node.range().start.offset;
out.insert(start, dump_node(afe.body, atoms, sm));
collect_eager_body_strings(afe.body, atoms, sm, out);
} else {
collect_eager_body_strings(afe.body, atoms, sm, out);
}
}
Node::Property(prop) => {
if func_value_block_info(prop.value).is_some() {
let start = node.range().start.offset;
if let Node::FunctionExpression(fe) = prop.value {
out.insert(start, dump_node(fe.body, atoms, sm));
collect_eager_body_strings(fe.body, atoms, sm, out);
}
} else {
collect_eager_body_string_children(node, atoms, sm, out);
}
}
Node::MethodDefinition(md) => {
if func_value_block_info(md.value).is_some() {
let start = node.range().start.offset;
if let Node::FunctionExpression(fe) = md.value {
out.insert(start, dump_node(fe.body, atoms, sm));
collect_eager_body_strings(fe.body, atoms, sm, out);
}
} else {
collect_eager_body_string_children(node, atoms, sm, out);
}
}
_ => collect_eager_body_string_children(node, atoms, sm, out),
}
}
fn collect_eager_body_string_children<'gc>(
node: &'gc Node<'gc>,
atoms: &hermes_atom_table::AtomTable,
sm: &hermes_support::manager::SourceErrorManager,
out: &mut BTreeMap<u32, String>,
) {
node.visit_children(&mut EagerBodyChildVisitor { atoms, sm, out });
}
struct EagerBodyChildVisitor<'a> {
atoms: &'a hermes_atom_table::AtomTable,
sm: &'a hermes_support::manager::SourceErrorManager,
out: &'a mut BTreeMap<u32, String>,
}
impl<'gc> hermes_ast::visitor::Visitor<'gc> for EagerBodyChildVisitor<'_> {
fn visit_node(&mut self, node: &'gc Node<'gc>) {
collect_eager_body_strings(node, self.atoms, self.sm, self.out);
}
}
fn dump_node<'a>(
node: &'a Node<'a>,
atoms: &hermes_atom_table::AtomTable,
sm: &hermes_support::manager::SourceErrorManager,
) -> String {
let mut out = String::new();
dump_estree_json_with_sm(
&mut out,
node,
false,
ESTreeDumpMode::HideEmpty,
sm,
LocationDumpMode::LocAndRange,
ESTreeRawProp::Exclude,
atoms,
);
out
}
fn reparsed_body<'gc>(entry: &FuncEntry, reparsed: &'gc Node<'gc>) -> &'gc Node<'gc> {
match entry.kind {
NodeKind::FunctionDeclaration => {
let Node::FunctionDeclaration(fd) = reparsed else {
panic!("expected FunctionDeclaration, got {:?}", reparsed.kind())
};
fd.body
}
NodeKind::FunctionExpression => {
let Node::FunctionExpression(fe) = reparsed else {
panic!("expected FunctionExpression, got {:?}", reparsed.kind())
};
fe.body
}
NodeKind::ArrowFunctionExpression => {
let Node::ArrowFunctionExpression(afe) = reparsed else {
panic!("expected ArrowFunctionExpression, got {:?}", reparsed.kind())
};
afe.body
}
NodeKind::Property | NodeKind::MethodDefinition => {
let Node::FunctionExpression(fe) = reparsed else {
panic!(
"Property/Method: expected FunctionExpression, got {:?}",
reparsed.kind()
)
};
fe.body
}
_ => panic!("unexpected kind {:?} in FuncEntry", entry.kind),
}
}
fn check_file(src: &[u8], label: &str, threshold: u32) -> usize {
let mut sm = SourceErrorManager::new();
let id = sm.add_buffer_bytes(label, src);
let mut sm_dump = SourceErrorManager::new();
let id_dump = sm_dump.add_buffer_bytes(label, src);
assert_eq!(id, id_dump, "buffer id mismatch between managers");
let sm_dump = sm_dump;
let eager_bodies: BTreeMap<u32, String>;
let eager_offsets: BTreeSet<u32>;
{
let mut ctx1 = Context::new();
let gc1 = ctx1.lock();
let lexer = JSLexer::new(id, &mut sm, &gc1.ctx().atom_table, GrammarContext::AllowRegExp);
let mut p = JSParserImpl::new_with_pass(&gc1, lexer, ParserPass::FullParse);
let root = p.parse().unwrap_or_else(|| panic!("[{label}] eager parse failed"));
let mut bodies: BTreeMap<u32, String> = BTreeMap::new();
collect_eager_body_strings(root, &gc1.ctx().atom_table, &sm_dump, &mut bodies);
eager_bodies = bodies;
eager_offsets = eager_bodies.keys().copied().collect();
}
let table = {
let mut ctx2 = Context::new();
let gc2 = ctx2.lock();
let lexer = JSLexer::new(id, &mut sm, &gc2.ctx().atom_table, GrammarContext::AllowRegExp);
let mut pp = JSParserImpl::new_with_pass(&gc2, lexer, ParserPass::PreParse);
pp.parse().unwrap_or_else(|| panic!("[{label}] preparse failed"));
pp.take_pre_parsed()
};
let table_ref = table.clone();
let mut ctx3 = Context::new();
ctx3.set_preemptive_function_compilation_threshold(threshold);
let gc3 = ctx3.lock();
let lexer = JSLexer::new(id, &mut sm, &gc3.ctx().atom_table, GrammarContext::AllowRegExp);
let mut lp = JSParserImpl::new_with_pass(&gc3, lexer, ParserPass::LazyParse);
lp.set_pre_parsed(table);
let lazy_root = lp.parse().unwrap_or_else(|| panic!("[{label}] lazyparse failed"));
let mut lazy_map: BTreeMap<u32, FuncEntry> = BTreeMap::new();
collect_funcs(lazy_root, &mut lazy_map);
let lazy_offsets: BTreeSet<u32> = lazy_map.keys().copied().collect();
let extra_in_lazy: Vec<u32> = lazy_offsets.difference(&eager_offsets).copied().collect();
assert!(
extra_in_lazy.is_empty(),
"[{label}] threshold={threshold}: lazy has offsets absent in eager: {extra_in_lazy:?}"
);
let mut n_compared = 0usize;
let mut queue: std::collections::VecDeque<(u32, FuncEntry)> = lazy_map
.into_iter()
.filter(|(_, e)| e.is_lazy_stub)
.collect();
while let Some((offset, entry)) = queue.pop_front() {
let start = hermes_support::location::SMLoc { source: id, offset };
let strict = table_ref
.function_info
.get(&entry.body_start_offset)
.map(|info| info.strict_mode)
.unwrap_or(false);
lp.set_strict_mode(strict);
let reparsed = lp
.parse_lazy_function(
entry.kind,
entry.param_yield,
entry.param_await,
start,
)
.unwrap_or_else(|| {
panic!(
"[{label}] threshold={threshold}: parse_lazy_function failed at offset {offset}"
)
});
let re_body = reparsed_body(&entry, reparsed);
let mut nested: BTreeMap<u32, FuncEntry> = BTreeMap::new();
collect_funcs(re_body, &mut nested);
let has_nested_stubs = nested.values().any(|e| e.is_lazy_stub);
if !has_nested_stubs {
let re_dump = dump_node(re_body, &gc3.ctx().atom_table, &sm_dump);
let eg_dump = eager_bodies
.get(&offset)
.unwrap_or_else(|| panic!("[{label}] no eager body at offset {offset}"));
assert_eq!(
eg_dump,
&re_dump,
"[{label}] threshold={threshold} offset={offset}: body mismatch\n\
EAGER:\n{eg_dump}\nREPARSED:\n{re_dump}"
);
n_compared += 1;
}
for (nested_offset, nested_entry) in nested {
if nested_entry.is_lazy_stub {
queue.push_back((nested_offset, nested_entry));
}
}
}
n_compared
}
fn corpus_files(dir: &str) -> Vec<PathBuf> {
let base = Path::new(env!("CARGO_MANIFEST_DIR")).join(dir);
let mut files: Vec<PathBuf> = std::fs::read_dir(&base)
.unwrap_or_else(|e| panic!("cannot read corpus dir {}: {e}", base.display()))
.map(|e| e.unwrap().path())
.filter(|p| p.extension().map(|e| e == "js").unwrap_or(false))
.collect();
files.sort();
files
}
const THRESHOLDS: [u32; 2] = [0, 20];
#[test]
fn lazy_corpus_reparse_equivalence() {
let files = corpus_files("tests/parser_corpus_lazy");
assert!(!files.is_empty(), "lazy corpus is empty");
let mut total_files = 0usize;
let mut total_comparisons = 0usize;
for path in &files {
let src = std::fs::read(path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let label = path.file_name().unwrap().to_string_lossy().into_owned();
for &thresh in &THRESHOLDS {
let n = check_file(&src, &label, thresh);
total_comparisons += n;
}
total_files += 1;
}
eprintln!(
"lazy_corpus_reparse_equivalence: {total_files} files × {} thresholds, \
{total_comparisons} body comparisons — all passed",
THRESHOLDS.len(),
);
}
#[test]
fn parser_corpus_reparse_equivalence() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(parser_corpus_reparse_equivalence_impl)
.expect("failed to spawn the corpus-reparse thread")
.join()
.expect("the corpus-reparse thread panicked");
}
fn parser_corpus_reparse_equivalence_impl() {
let files = corpus_files("tests/parser_corpus");
assert!(!files.is_empty(), "parser corpus is empty");
let mut total_files = 0usize;
let mut total_comparisons = 0usize;
for path in &files {
let src = std::fs::read(path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let label = path.file_name().unwrap().to_string_lossy().into_owned();
for &thresh in &THRESHOLDS {
let n = check_file(&src, &label, thresh);
total_comparisons += n;
}
total_files += 1;
}
assert!(
total_comparisons >= 10,
"parser_corpus_reparse_equivalence: only {total_comparisons} body \
comparisons — expected at least 10; check that corpus files with \
functions still exist"
);
eprintln!(
"parser_corpus_reparse_equivalence: {total_files} files × {} thresholds, \
{total_comparisons} body comparisons — all passed",
THRESHOLDS.len(),
);
}