use oxc_allocator::Allocator;
use oxc_span::SourceType;
use crate::{ParseOptions, ParserImpl, UniquePromise, config::TokensParserConfig};
fn parser<'a>(
allocator: &'a Allocator,
source: &'a str,
source_type: SourceType,
enabled: bool,
) -> ParserImpl<'a, TokensParserConfig> {
let mut parser = ParserImpl::new(
allocator,
source,
source_type,
ParseOptions { parse_regular_expression: true, ..ParseOptions::default() },
TokensParserConfig,
UniquePromise::new_for_tests_and_benchmarks(),
);
parser.state.disable_failed_subproblems = !enabled;
parser
}
fn packet(source: &str, source_type: SourceType, enabled: bool) -> String {
let allocator = Allocator::default();
let result = parser(&allocator, source, source_type, enabled).parse();
format!(
"{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}",
result.program,
result.diagnostics,
result.tokens,
result.module_record,
result.irregular_whitespaces,
(
result.panicked,
result.is_flow_language,
result.has_legacy_literals,
result.has_jsdoc_comments
),
)
}
#[test]
fn complete_program_and_expression_packets_match_without_memoization() {
let atoms = [
"T",
"[T]",
"[[T, U], V]",
"{ [x < y]: T }",
"typeof import('m')",
"{ [import.meta.url]: T }",
"{ [import('m')]: T }",
"{ [(() => { return /x>y/.test(x) })()]: T }",
"{ [x ? y : z]: T }",
"{ [({ a = 1 } = obj)]: T }",
"{ [({ a = b ? c : d } = obj)]: T }",
"{ [await value]: T }",
"[T /* > */]",
"[T, /* @__PURE__ */ f()]",
"[T, /* @__NO_SIDE_EFFECTS__ */ (() => 0)]",
"`head ${T} >`",
"'greater >'",
"\\u0054",
"[...T[], U?]",
"T extends U ? V : W",
"([x, { y }]: T) => U",
"([x = y ? z : w]: T) => U",
"[",
"[[,]<x",
"{[[,]<x",
"{ [a < [b < [c]]]: T }",
];
let mut sources = Vec::new();
for atom in atoms {
for suffix in [">(x)", ">", ">= x", "]", "\n// >", ">`tail`", ">.x", "> + x"] {
for prefix in ["f<", "a < b <", "f<[{[", "/* @__PURE__ */ f<"] {
sources.push(format!("{prefix}{atom}{suffix}"));
}
}
}
sources.extend(
[
"a < [{ [(() => { import('m'); export {}; })()]: T }] > x",
"a < [{ [(() => { const { a = 1 } = x; return a; })()]: T }] > x",
"f<T>() < U",
"<T,>(x: T) => x",
"<Widget<T> value={a > b} />",
"f<T>(x) /* @__PURE__ */ .foo()",
"f<T>(x)\u{00a0}< U",
]
.map(str::to_owned),
);
for source in sources {
for source_type in [
SourceType::ts(),
SourceType::tsx(),
SourceType::jsx(),
SourceType::ts().with_script(true),
SourceType::ts().with_unambiguous(true),
] {
let baseline = packet(&source, source_type, false);
let candidate = packet(&source, source_type, true);
assert_eq!(baseline, candidate, "complete program packet: {source:?} {source_type:?}");
let allocator = Allocator::default();
let baseline =
format!("{:?}", parser(&allocator, &source, source_type, false).parse_expression());
let allocator = Allocator::default();
let candidate =
format!("{:?}", parser(&allocator, &source, source_type, true).parse_expression());
assert_eq!(
baseline, candidate,
"complete expression packet: {source:?} {source_type:?}"
);
}
}
}
#[test]
fn overlapping_fatal_subproblems_are_shared_without_changing_recovery() {
let source = include_str!("fixtures/issue-10845.input");
let mut observations = Vec::new();
for enabled in [false, true] {
let allocator = Allocator::default();
let mut parser = parser(&allocator, source, SourceType::ts(), enabled);
parser.bump_any();
let expression = parser.parse_expr();
assert!(parser.fatal_error.is_some());
if enabled {
assert!(parser.state.failed_speculative_subproblems.is_allocated());
assert!(parser.state.failed_speculative_subproblems.len() > 0);
assert!(parser.state.failed_speculative_subproblems.has_assignment());
} else {
assert!(!parser.state.failed_speculative_subproblems.is_allocated());
}
observations.push((
format!("{:?}", parser.fatal_error.as_ref().unwrap().error),
parser.state.subproblem_visits,
parser.state.subproblem_reuses,
allocator.used_bytes(),
));
std::hint::black_box(expression);
}
assert_eq!(observations[0].0, observations[1].0);
assert_eq!(observations[0].2, 0);
assert!(observations[1].2 > 0, "no failed subproblem was reused: {observations:?}");
assert!(
observations[0].1 > observations[1].1 * 8,
"overlapping work did not fall by 8x: {observations:?}"
);
assert!(
observations[0].3 > observations[1].3 * 8,
"repeated arena allocation was not eliminated: {observations:?}"
);
eprintln!("exact #10845 (diagnostic, visits, reuses, arena bytes): {observations:?}");
assert_eq!(packet(source, SourceType::ts(), false), packet(source, SourceType::ts(), true));
}
#[test]
fn assignment_entry_bypasses_inactive_memoization() {
let allocator = Allocator::default();
let mut inactive_parser = parser(&allocator, "x + y", SourceType::ts(), true);
inactive_parser.bump_any();
let _ = inactive_parser.parse_assignment_expression_or_higher();
assert_eq!(inactive_parser.state.memo_entry_calls, 0);
assert!(!inactive_parser.state.failed_speculative_subproblems.is_allocated());
let allocator = Allocator::default();
let mut fatal_parser = parser(&allocator, "x", SourceType::ts(), true);
fatal_parser.bump_any();
fatal_parser.state.type_argument_speculation_depth = 1;
fatal_parser.set_unexpected();
let _ = fatal_parser.parse_assignment_expression_or_higher();
assert_eq!(fatal_parser.state.memo_entry_calls, 0);
assert!(!fatal_parser.state.failed_speculative_subproblems.is_allocated());
let allocator = Allocator::default();
let mut active_parser = parser(&allocator, "x + y", SourceType::ts(), true);
active_parser.bump_any();
active_parser.state.type_argument_speculation_depth = 1;
active_parser.state.reuse_failed_type_argument_subproblems = true;
let _ = active_parser.parse_assignment_expression_or_higher();
assert_eq!(active_parser.state.memo_entry_calls, 1);
assert!(!active_parser.state.failed_speculative_subproblems.is_allocated());
}
#[test]
fn irreversible_side_effects_are_fenced_even_when_collection_lengths_match() {
use super::SpeculativeProduction;
use oxc_allocator::{Dummy, GetAllocator};
use oxc_ast::ast::Expression;
for writer in 0..6 {
let allocator = Allocator::default();
let mut parser = parser(&allocator, "[x]", SourceType::ts(), true);
parser.bump_any();
parser.state.type_argument_speculation_depth = 1;
parser.state.reuse_failed_type_argument_subproblems = true;
let _: Expression =
parser.memoize_failed_speculative_subproblem(SpeculativeProduction::Type, |p| {
match writer {
0 => p.state.speculation_effects_revision += 1,
1 => p.module_record_builder.speculation_effects_revision += 1,
2 => p.lexer.trivia_builder.speculation_effects_revision += 1,
3 => p.state.encountered_await_identifier = true,
4 => p.ctx.toggle(crate::Context::Decorator),
5 => {}
_ => unreachable!(),
}
p.set_unexpected();
Dummy::dummy(p.allocator())
});
assert!(parser.fatal_error.is_some(), "writer {writer} did not produce a fatal error");
assert_eq!(
parser.state.failed_speculative_subproblems.len(),
usize::from(writer == 5),
"writer {writer} memoization mismatch"
);
assert_eq!(
parser.state.failed_speculative_subproblems.is_allocated(),
writer == 5,
"writer {writer} allocated a fenced table"
);
}
}
#[test]
fn ordinary_successful_programs_do_not_allocate_failed_subproblem_tables() {
let allocator = Allocator::default();
let mut parser =
parser(&allocator, "f<Map<string, Array<number>>>(value)", SourceType::ts(), true);
parser.bump_any();
let _ = parser.parse_expr();
assert!(parser.fatal_error.is_none());
assert!(!parser.state.failed_speculative_subproblems.is_allocated());
assert_eq!(parser.state.failed_speculative_subproblems.capacity(), 0);
assert_eq!(parser.state.type_argument_speculation_depth, 0);
}