diff --git a/AGENTS.md b/AGENTS.md
index f82b73c32..84dd5d198 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -42,6 +42,10 @@ performance loop below is the contract.
- Burndown recorder: `./scripts/test262-burndown.sh --report
| --entry `
- Performance inner loop (counter screen, symbol sizes, optional trace):
`./scripts/perf-loop.sh --plan tasks/performance-units/.json`
+- Diagnostic A/B, front-end cost, executor placement:
+ `python3 -m tools.benchmark.screen --candidate B --base A --case external`,
+ `python3 -m tools.benchmark.front_end --binary B --reference `,
+ `./scripts/layout-scan.sh --offsets 0x0,0x200` (see docs/performance-workflow.md)
- Formal local comparison (feeds queue/decide):
`./scripts/perf-compare.sh --base --output-dir target/comparison/`
- Source size report: `./scripts/source-size-report.sh [limit] [--vendor]`
diff --git a/crates/qjs-cli/hot-functions.order b/crates/qjs-cli/hot-functions.order
index d62cc83c4..24d7903e2 100644
--- a/crates/qjs-cli/hot-functions.order
+++ b/crates/qjs-cli/hot-functions.order
@@ -11,43 +11,51 @@
# budget 0x700 __RNvMNtNtNtCs9nYd1Hk1rek_11qjs_runtime5value6object10slot_readsNtB4_9ObjectRef22own_data_property_read
# budget 0xa00 __RNvMs6_NtNtCs9nYd1Hk1rek_11qjs_runtime5value6objectNtB5_9ObjectRef32write_existing_own_data_property
# budget 0x300 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute16boxed_truthiness
-# pinned: typed-loop executor at 0x0, interpreter's at 0xc40 mod 4 KiB (python3 -m tools.benchmark.layout_pin), head 38
+# budget 0x300 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute14boxed_equality
+# budget 0x500 __RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
+# pinned: typed-loop executor at 0x200, interpreter's at 0xc40 mod 4 KiB (python3 -m tools.benchmark.layout_pin), head 44
__RNvMsz_NtNtNtCs1OjIl8oxbrv_5alloc11collections5btree3mapINtB5_8IntoIteryNtNtNtCscUtGwbhD4WH_5gimli4read6abbrev12AbbreviationE10dying_nextCsg55jX0GwzBC_3std
__RNvXs7_NtNtCsg55jX0GwzBC_3std2io5errorNtB5_5ErrorNtNtCsl8K0bEFm1U0_4core3fmt7Display3fmt
__RINvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute3runNtNtB6_2vm2VmEB8_
-__RNvNvNtCsg55jX0GwzBC_3std2fs4read5inner
__RNvNtNtNtCsg55jX0GwzBC_3std3sys3env4unix13env_read_lock
+__RNvMs8_NtCsg55jX0GwzBC_3std4pathNtB5_10Components7as_path
+__RNvMs0_NtNtCscUtGwbhD4WH_5gimli4read5dwarfINtB5_5DwarfINtNtB7_12endian_slice11EndianSliceNtNtB9_9endianity12LittleEndianEE11attr_stringCsg55jX0GwzBC_3std
__RINvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute3runNtNtNtNtB6_10compact_fn4wide10loop_frame13WideLoopFrameEB8_
__RNvMs_NtNtNtCsg55jX0GwzBC_3std2io8buffered9bufwriterINtB4_9BufWriterNtNtB8_5stdio9StdoutRawE9flush_bufBa_
__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterNtNtB7_5stdio10StderrLockENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
__RINvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute18try_run_typed_loopNtNtNtNtB6_10compact_fn4wide10loop_frame13WideLoopFrameEB8_
-__RNvXs2_NtNtCsg55jX0GwzBC_3std4hash6randomNtB5_13DefaultHasherNtNtCsl8K0bEFm1U0_4core4hash6Hasher5write
+__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterINtNtCs1OjIl8oxbrv_5alloc3vec3VechEENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
+__RNvYNtNtNtNtCsg55jX0GwzBC_3std3sys5stdio4unix6StderrNtNtBa_2io5Write9write_allBa_
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute16get_named_object
-__RNvMs8_NtCsg55jX0GwzBC_3std4pathNtB5_10Components7as_path
+__RNvNvNtCsg55jX0GwzBC_3std2fs4read5inner
__RNvMs4_NtNtCs9nYd1Hk1rek_11qjs_runtime5value5arrayNtB5_8ArrayRef24direct_dense_index_value
__RNvMsn_NtCs1OjIl8oxbrv_5alloc4syncINtB5_3ArcINtNtNtCscUtGwbhD4WH_5gimli4read5dwarf5DwarfINtNtBL_12endian_slice11EndianSliceNtNtBN_9endianity12LittleEndianEEE9drop_slowCsg55jX0GwzBC_3std
__RNvXs7_NtNtCsg55jX0GwzBC_3std2io5stdioNtB5_9StdinLockNtB7_7BufRead9read_line
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9call_leaf
-__RNvXs_NvNtCsg55jX0GwzBC_3std9panicking13panic_handlerNtB4_19FormatStringPayloadNtNtCsl8K0bEFm1U0_4core5panic12PanicPayload3get
-__RNvMs_NtNtNtCsg55jX0GwzBC_3std2io8buffered9bufwriterINtB4_9BufWriterNtNtB8_5stdio9StdoutRawE14write_all_coldBa_
+__RNvYINtNtNtCscUtGwbhD4WH_5gimli4read12endian_slice11EndianSliceNtNtB9_9endianity12LittleEndianENtNtB7_6reader6Reader17read_sized_offsetCsg55jX0GwzBC_3std
__RNvNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode15vm_numeric_leaf21try_eval_numeric_leaf
__RNvXNvNtCsg55jX0GwzBC_3std2io17default_write_fmtINtB2_7AdapterNtNtNtNtB6_3sys5stdio4unix6StderrENtNtCsl8K0bEFm1U0_4core3fmt5Write9write_strB6_
__RNvMs3_NtNtCs9nYd1Hk1rek_11qjs_runtime5value5arrayNtB5_9ArrayData21has_property_at_index
-__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterINtNtCs1OjIl8oxbrv_5alloc3vec3VechEENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
+__RNvXso_NtNtCsg55jX0GwzBC_3std2io5stdioRNtB5_6StderrNtB7_5Write9write_fmt
__RNvNtNtCsg55jX0GwzBC_3std2io5stdio7__eprint
__RNvNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode15vm_numeric_leaf20direct_number_binary
-__RNvXso_NtNtCsg55jX0GwzBC_3std2io5stdioRNtB5_6StderrNtB7_5Write9write_fmt
+__RNvXse_NtNtCsg55jX0GwzBC_3std2io5stdioRNtB5_6StdoutNtB7_5Write9write_fmt
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute22ordinary_data_property
-__RNvMsn_NtCs1OjIl8oxbrv_5alloc4syncINtB5_3ArcNtNtNtNtCsg55jX0GwzBC_3std3sys2fs4unix12InnerReadDirE9drop_slowBO_
-__RNvXs1g_NtCsl8K0bEFm1U0_4core3fmtRINtNtCs1OjIl8oxbrv_5alloc3vec3VechENtB6_5Debug3fmtCsg55jX0GwzBC_3std
+__RNvXs_NvNtCsg55jX0GwzBC_3std9panicking13panic_handlerNtB4_19FormatStringPayloadNtNtCsl8K0bEFm1U0_4core5panic12PanicPayload3get
+__RNvMs_NtNtNtCsg55jX0GwzBC_3std2io8buffered9bufwriterINtB4_9BufWriterNtNtB8_5stdio9StdoutRawE14write_all_coldBa_
__RNvMNtNtNtCs9nYd1Hk1rek_11qjs_runtime5value6object10slot_readsNtB4_9ObjectRef22own_data_property_read
__RNvMs1_NtNtNtNtCsg55jX0GwzBC_3std3sys4sync6rwlock5queueNtB5_6RwLock21read_unlock_contended
__RNvMs1_NtNtNtNtCsg55jX0GwzBC_3std3sys4sync6rwlock5queueNtB5_6RwLock16unlock_contended
__RNvMs6_NtNtCs9nYd1Hk1rek_11qjs_runtime5value6objectNtB5_9ObjectRef32write_existing_own_data_property
__RNvNtNtNtCsg55jX0GwzBC_3std12backtrace_rs9symbolize5gimli4mmap
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute16boxed_truthiness
-__RNvXse_NtNtCsg55jX0GwzBC_3std2io5stdioRNtB5_6StdoutNtB7_5Write9write_fmt
-__RNvYNtNtNtNtCsg55jX0GwzBC_3std3sys5stdio4unix6StderrNtNtBa_2io5Write9write_allBa_
+__RNvNtNtNtNtCsg55jX0GwzBC_3std3sys2io5error4unix17decode_error_kind
+__RNvXNvNtCsg55jX0GwzBC_3std2io17default_write_fmtINtB2_7AdapterINtNtB4_6cursor6CursorQShEENtNtCsl8K0bEFm1U0_4core3fmt5Write9write_strB6_
+__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute14boxed_equality
+__RNvMsn_NtCs1OjIl8oxbrv_5alloc4syncINtB5_3ArcNtNtNtNtCsg55jX0GwzBC_3std3sys2fs4unix12InnerReadDirE9drop_slowBO_
+__RNvYINtNvNtCsg55jX0GwzBC_3std2io17default_write_fmt7AdapterNtNtNtNtB9_3sys5stdio4unix6StderrENtNtCsl8K0bEFm1U0_4core3fmt5Write10write_charB9_
+__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
+__RNvNtNtCsg55jX0GwzBC_3std3sys2fs8read_dir
__RNvXs1_NtCs9nYd1Hk1rek_11qjs_runtime5valueNtB5_5ValueNtNtCsl8K0bEFm1U0_4core5clone5Clone5clone
__RINvNtCsl8K0bEFm1U0_4core3ptr13drop_in_placeNtNtCs9nYd1Hk1rek_11qjs_runtime5value5ValueEBK_
__RNvNtNtCs9nYd1Hk1rek_11qjs_runtime8function4call25call_direct_leaf_function
@@ -98,7 +106,6 @@ __RINvMs4_NtNtCs9nYd1Hk1rek_11qjs_runtime5value5arrayNtB6_8ArrayRef28with_dense_
__RINvMs4_NtNtCs9nYd1Hk1rek_11qjs_runtime5value5arrayNtB6_8ArrayRef37with_distinct_dense_writable_elementsNtNtNtNtNtBa_8bytecode24vm_numeric_mutation_loop5dense6nested13RegionOutcomeNCNvMs0_B1G_NtB1G_15NestedDensePlan7try_runs0_0EBa_
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10compact_fn8property14get_prop_named
__RNvNtNtNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode24vm_numeric_mutation_loop5dense6legacy11typed_array7try_run
-__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute9get_named
__RNCNvMs7_NtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode24vm_numeric_mutation_loop5denseNtB7_16DynamicDensePlan24try_run_hole_tail_append0Bd_
__RINvMs4_NtNtCs9nYd1Hk1rek_11qjs_runtime5value5arrayNtB6_8ArrayRef37with_distinct_dense_writable_elementsNtNtNtNtBa_8bytecode24vm_numeric_mutation_loop5dense17DynamicProgramRunNCNvMs7_B1G_NtB1G_16DynamicDensePlan7try_runs6_0EBa_
__RNvMs5_NtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode2vmNtB5_2Vm16construct_callee
@@ -149,7 +156,6 @@ __RNvNtNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode24vm_numeric_mutation_loop5dens
__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10compact_fn8property14set_prop_named
__RNvMs4_NtNtCs9nYd1Hk1rek_11qjs_runtime5value6objectNtB5_15PropertyStorage6insert
__RINvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute23fill_hole_or_grow_valueNtNtNtNtB6_10compact_fn4wide10loop_frame13WideLoopFrameEB8_
-__RNvNtNtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode10typed_loop7execute14boxed_equality
__RNvMs4_NtCs1OjIl8oxbrv_5alloc7raw_vecNtB5_11RawVecInner11finish_growCs9nYd1Hk1rek_11qjs_runtime
__RNCNvMs_NtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode8compilerNtB6_8Compiler24compile_into_with_hoists0Ba_
__RNvMs_NtNtCs9nYd1Hk1rek_11qjs_runtime8bytecode8compilerNtB4_8Compiler12compile_stmt
diff --git a/crates/qjs-parser/src/cursor.rs b/crates/qjs-parser/src/cursor.rs
index 6d39f6833..1c4008d8a 100644
--- a/crates/qjs-parser/src/cursor.rs
+++ b/crates/qjs-parser/src/cursor.rs
@@ -5,6 +5,7 @@ use crate::{ParseError, Parser};
impl Parser {
pub(crate) fn new(tokens: Vec, source: String, source_is_wtf16: bool) -> Self {
let closers = matching_closers(&tokens);
+ let bracket_depths = bracket_depths(&tokens);
Self {
source,
source_is_wtf16,
@@ -14,6 +15,8 @@ impl Parser {
goal: crate::Goal::Script,
strict: false,
allow_in: true,
+ no_in_depth: 0,
+ bracket_depths,
in_method: false,
in_derived_constructor: false,
in_field_initializer: false,
@@ -30,6 +33,19 @@ impl Parser {
}
}
+ /// Whether `in` is a relational operator at the cursor (`no_in_depth`).
+ pub(crate) fn in_allowed(&self) -> bool {
+ self.allow_in
+ || self
+ .bracket_depths
+ .get(self.cursor)
+ .is_some_and(|&depth| depth > self.no_in_depth)
+ }
+
+ pub(crate) fn bracket_depth(&self) -> u32 {
+ self.bracket_depths.get(self.cursor).copied().unwrap_or(0)
+ }
+
pub(crate) fn at(&self, kind: &TokenKind) -> bool {
// Every precedence level asks this about each of its operators for
// every operand, and nearly every answer is no: comparing the variant
@@ -97,6 +113,32 @@ impl Parser {
pub(crate) const NO_CLOSER: u32 = u32::MAX;
/// The index of the token closing each bracket token, found in one pass.
+/// The number of brackets (and template substitutions) open at each token;
+/// a closing token counts at the depth it returns to.
+fn bracket_depths(tokens: &[Token]) -> Vec {
+ let mut depth = 0_u32;
+ tokens
+ .iter()
+ .map(|token| match token.kind {
+ TokenKind::LeftParen
+ | TokenKind::LeftBracket
+ | TokenKind::LeftBrace
+ | TokenKind::TemplateHead(_) => {
+ depth += 1;
+ depth - 1
+ }
+ TokenKind::RightParen
+ | TokenKind::RightBracket
+ | TokenKind::RightBrace
+ | TokenKind::TemplateTail(_) => {
+ depth = depth.saturating_sub(1);
+ depth
+ }
+ _ => depth,
+ })
+ .collect()
+}
+
fn matching_closers(tokens: &[Token]) -> Vec {
let mut closers = vec![NO_CLOSER; tokens.len()];
let mut open: Vec = Vec::new();
diff --git a/crates/qjs-parser/src/expression.rs b/crates/qjs-parser/src/expression.rs
index 941ec46b0..74e012a82 100644
--- a/crates/qjs-parser/src/expression.rs
+++ b/crates/qjs-parser/src/expression.rs
@@ -40,10 +40,11 @@ impl Parser {
}
pub(crate) fn expression_no_in(&mut self) -> Result {
- let previous = self.allow_in;
+ let previous = (self.allow_in, self.no_in_depth);
self.allow_in = false;
+ self.no_in_depth = self.bracket_depth();
let result = self.expression();
- self.allow_in = previous;
+ (self.allow_in, self.no_in_depth) = previous;
result
}
diff --git a/crates/qjs-parser/src/expression/binary.rs b/crates/qjs-parser/src/expression/binary.rs
index 11e089ccc..a76a214ab 100644
--- a/crates/qjs-parser/src/expression/binary.rs
+++ b/crates/qjs-parser/src/expression/binary.rs
@@ -3,6 +3,13 @@ use qjs_lexer::TokenKind;
use crate::{ParseError, Parser};
+/// Binary precedence levels, loosest first; the levels between these are
+/// `&&`, `|`, `^`, `&` after `||`, and `+ -`, `* / %` after the shifts.
+const LOGICAL_OR: u8 = 1;
+const EQUALITY: u8 = 6;
+const RELATIONAL: u8 = 7;
+const SHIFT: u8 = 8;
+
impl Parser {
pub(crate) fn nullish_coalescing(&mut self) -> Result {
let mut left_start = self.cursor;
@@ -45,111 +52,69 @@ impl Parser {
}
fn logical_or(&mut self) -> Result {
- self.binary_left_assoc(
- Self::logical_and,
- &[(TokenKind::PipePipe, BinaryOp::LogicalOr)],
- )
- }
-
- fn logical_and(&mut self) -> Result {
- self.binary_left_assoc(
- Self::bitwise_or,
- &[(TokenKind::AmpersandAmpersand, BinaryOp::LogicalAnd)],
- )
- }
-
- fn bitwise_or(&mut self) -> Result {
- self.binary_left_assoc(Self::bitwise_xor, &[(TokenKind::Pipe, BinaryOp::BitwiseOr)])
- }
-
- fn bitwise_xor(&mut self) -> Result {
- self.binary_left_assoc(
- Self::bitwise_and,
- &[(TokenKind::Caret, BinaryOp::BitwiseXor)],
- )
- }
-
- fn bitwise_and(&mut self) -> Result {
- self.binary_left_assoc(
- Self::equality,
- &[(TokenKind::Ampersand, BinaryOp::BitwiseAnd)],
- )
- }
-
- fn equality(&mut self) -> Result {
- self.binary_left_assoc(
- Self::comparison,
- &[
- (TokenKind::EqualEqual, BinaryOp::Eq),
- (TokenKind::EqualEqualEqual, BinaryOp::StrictEq),
- (TokenKind::BangEqual, BinaryOp::Ne),
- (TokenKind::BangEqualEqual, BinaryOp::StrictNe),
- ],
- )
- }
-
- fn comparison(&mut self) -> Result {
- if !self.allow_in {
- return self.binary_left_assoc(
- Self::shift,
- &[
- (TokenKind::Less, BinaryOp::Lt),
- (TokenKind::LessEqual, BinaryOp::Le),
- (TokenKind::Greater, BinaryOp::Gt),
- (TokenKind::GreaterEqual, BinaryOp::Ge),
- (TokenKind::Instanceof, BinaryOp::Instanceof),
- ],
- );
- }
- self.binary_left_assoc(
- Self::shift,
- &[
- (TokenKind::Less, BinaryOp::Lt),
- (TokenKind::LessEqual, BinaryOp::Le),
- (TokenKind::Greater, BinaryOp::Gt),
- (TokenKind::GreaterEqual, BinaryOp::Ge),
- (TokenKind::In, BinaryOp::In),
- (TokenKind::Instanceof, BinaryOp::Instanceof),
- ],
- )
+ self.binary_at_least(LOGICAL_OR)
}
/// Parses a `ShiftExpression`, used as the right operand of a `#x in obj`
/// ergonomic brand check.
pub(crate) fn shift_expression(&mut self) -> Result {
- self.shift()
+ self.binary_at_least(SHIFT)
}
- fn shift(&mut self) -> Result {
- self.binary_left_assoc(
- Self::additive,
- &[
- (TokenKind::LessLess, BinaryOp::Shl),
- (TokenKind::GreaterGreater, BinaryOp::Shr),
- (TokenKind::GreaterGreaterGreater, BinaryOp::UShr),
- ],
- )
- }
-
- fn additive(&mut self) -> Result {
- self.binary_left_assoc(
- Self::multiplicative,
- &[
- (TokenKind::Plus, BinaryOp::Add),
- (TokenKind::Minus, BinaryOp::Sub),
- ],
- )
+ /// The left-associative binary levels from `||` down to `*`, by
+ /// precedence climbing: an operand is parsed once and each operator
+ /// binds by its level. The grammar's one-function-per-level descent
+ /// built the same trees but passed every operand up through all ten
+ /// levels, each returning the expression by value and testing its own
+ /// operator list -- most of parsing a large script.
+ fn binary_at_least(&mut self, minimum: u8) -> Result {
+ let mut expr = self.exponentiation()?;
+ while let Some((op, precedence)) = self.binary_operator()
+ && precedence >= minimum
+ {
+ self.cursor += 1;
+ let right = self.binary_at_least(precedence + 1)?;
+ let span = Span::new(expr.span().start, right.span().end);
+ expr = Expr::Binary {
+ left: Box::new(expr),
+ op,
+ right: Box::new(right),
+ span,
+ };
+ }
+ Ok(expr)
}
- fn multiplicative(&mut self) -> Result {
- self.binary_left_assoc(
- Self::exponentiation,
- &[
- (TokenKind::Star, BinaryOp::Mul),
- (TokenKind::Slash, BinaryOp::Div),
- (TokenKind::Percent, BinaryOp::Rem),
- ],
- )
+ /// The binary operator at the cursor and its precedence; `in` only
+ /// where the grammar's `[In]` parameter allows it.
+ fn binary_operator(&self) -> Option<(BinaryOp, u8)> {
+ let operator = match self.peek()?.kind {
+ TokenKind::PipePipe => (BinaryOp::LogicalOr, LOGICAL_OR),
+ TokenKind::AmpersandAmpersand => (BinaryOp::LogicalAnd, LOGICAL_OR + 1),
+ TokenKind::Pipe => (BinaryOp::BitwiseOr, LOGICAL_OR + 2),
+ TokenKind::Caret => (BinaryOp::BitwiseXor, LOGICAL_OR + 3),
+ TokenKind::Ampersand => (BinaryOp::BitwiseAnd, LOGICAL_OR + 4),
+ TokenKind::EqualEqual => (BinaryOp::Eq, EQUALITY),
+ TokenKind::EqualEqualEqual => (BinaryOp::StrictEq, EQUALITY),
+ TokenKind::BangEqual => (BinaryOp::Ne, EQUALITY),
+ TokenKind::BangEqualEqual => (BinaryOp::StrictNe, EQUALITY),
+ TokenKind::Less => (BinaryOp::Lt, RELATIONAL),
+ TokenKind::LessEqual => (BinaryOp::Le, RELATIONAL),
+ TokenKind::Greater => (BinaryOp::Gt, RELATIONAL),
+ TokenKind::GreaterEqual => (BinaryOp::Ge, RELATIONAL),
+ TokenKind::Instanceof => (BinaryOp::Instanceof, RELATIONAL),
+ TokenKind::In if self.in_allowed() => (BinaryOp::In, RELATIONAL),
+ TokenKind::LessLess => (BinaryOp::Shl, SHIFT),
+ TokenKind::GreaterGreater => (BinaryOp::Shr, SHIFT),
+ TokenKind::GreaterGreaterGreater => (BinaryOp::UShr, SHIFT),
+ TokenKind::Plus => (BinaryOp::Add, SHIFT + 1),
+ TokenKind::Minus => (BinaryOp::Sub, SHIFT + 1),
+ TokenKind::Star => (BinaryOp::Mul, SHIFT + 2),
+ TokenKind::Slash => (BinaryOp::Div, SHIFT + 2),
+ TokenKind::Percent => (BinaryOp::Rem, SHIFT + 2),
+ _ => return None,
+ };
+ Some(operator)
}
fn exponentiation(&mut self) -> Result {
@@ -197,26 +162,6 @@ impl Parser {
})
}
- fn binary_left_assoc(
- &mut self,
- next: fn(&mut Self) -> Result,
- operators: &[(TokenKind, BinaryOp)],
- ) -> Result {
- let mut expr = next(self)?;
- while let Some((kind, op)) = operators.iter().find(|(kind, _)| self.at(kind)) {
- self.expect(kind)?;
- let right = next(self)?;
- let span = Span::new(expr.span().start, right.span().end);
- expr = Expr::Binary {
- left: Box::new(expr),
- op: *op,
- right: Box::new(right),
- span,
- };
- }
- Ok(expr)
- }
-
fn expression_range_is_parenthesized(&self, start: usize, end: usize) -> bool {
let Some(first) = self.tokens.get(start) else {
return false;
diff --git a/crates/qjs-parser/src/expression/unary.rs b/crates/qjs-parser/src/expression/unary.rs
index cc34075a7..70d7e55c7 100644
--- a/crates/qjs-parser/src/expression/unary.rs
+++ b/crates/qjs-parser/src/expression/unary.rs
@@ -167,7 +167,7 @@ impl Parser {
/// name in any other expression position is a syntax error.
fn private_in_expression(&mut self, name: String, span: Span) -> Result {
self.advance();
- if !self.allow_in || !self.at(&TokenKind::In) {
+ if !self.in_allowed() || !self.at(&TokenKind::In) {
return Err(ParseError {
message: format!(
"private name `#{name}` is only valid on the left of `in` or as a member \
diff --git a/crates/qjs-parser/src/lib.rs b/crates/qjs-parser/src/lib.rs
index 88523275a..a60b3bb8d 100644
--- a/crates/qjs-parser/src/lib.rs
+++ b/crates/qjs-parser/src/lib.rs
@@ -227,7 +227,16 @@ struct Parser {
/// top-level `import`/`export` declarations.
goal: Goal,
strict: bool,
+ /// The grammar's `[In]` parameter: cleared for a `for` head's
+ /// initializer (`expression_no_in`), where `in` would start a for-in.
allow_in: bool,
+ /// The bracket depth `allow_in` was cleared at. Every bracketed
+ /// production inside it -- parentheses, array and object literals,
+ /// arguments, member brackets, template substitutions, function bodies --
+ /// takes `[+In]` again, so `in` is only withheld at this depth.
+ no_in_depth: u32,
+ /// Each token's bracket nesting depth (`bracket_depths`).
+ bracket_depths: Vec,
/// Whether `super.prop`/`super[expr]` member access is currently allowed,
/// i.e. the parser is inside a method or accessor body (or an arrow nested
/// in one).
diff --git a/crates/qjs-runtime/src/bytecode/compiler.rs b/crates/qjs-runtime/src/bytecode/compiler.rs
index cb5eb7da2..427e0766a 100644
--- a/crates/qjs-runtime/src/bytecode/compiler.rs
+++ b/crates/qjs-runtime/src/bytecode/compiler.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use crate::value::name_hash::{NameMap, NameSet};
use qjs_ast::{ForInLeft, ForInit, FunctionParams, Script, Stmt, VarKind};
@@ -24,8 +24,8 @@ pub(super) struct Compiler {
pub(super) highest_jump_target: usize,
pub(super) constants: Vec,
pub(super) locals: Vec,
- pub(super) local_slots: HashMap,
- pub(super) lexical_scopes: Vec>,
+ pub(super) local_slots: NameMap,
+ pub(super) lexical_scopes: Vec>,
pub(super) code: Vec,
loop_stack: Vec,
pending_labels: Vec,
@@ -40,7 +40,7 @@ pub(super) struct Compiler {
/// Names of `var`/function declarations hoisted at global script scope.
/// They live in the realm (and on `globalThis`), not frame slots, so
/// closures, direct eval, and promise jobs all observe one binding.
- pub(super) global_hoisted: std::collections::HashSet,
+ pub(super) global_hoisted: NameSet,
annex_b_blocked_function_names: Vec>,
/// Count of `with` scopes currently open around the code being compiled.
/// Break/continue/return that leave one or more of them emit `Op::ExitWith`
@@ -124,8 +124,8 @@ impl Default for Compiler {
highest_jump_target: 0,
constants: Vec::new(),
locals: Vec::new(),
- local_slots: HashMap::new(),
- lexical_scopes: vec![HashMap::new()],
+ local_slots: NameMap::default(),
+ lexical_scopes: vec![NameMap::default()],
code: Vec::new(),
loop_stack: Vec::new(),
pending_labels: Vec::new(),
@@ -134,7 +134,7 @@ impl Default for Compiler {
strict: false,
global_scope: true,
direct_eval_source: false,
- global_hoisted: std::collections::HashSet::new(),
+ global_hoisted: NameSet::default(),
annex_b_blocked_function_names: Vec::new(),
with_depth: 0,
with_base_depth: 0,
diff --git a/crates/qjs-runtime/src/bytecode/compiler_lexical.rs b/crates/qjs-runtime/src/bytecode/compiler_lexical.rs
index 41fa0eba6..ad89ba91c 100644
--- a/crates/qjs-runtime/src/bytecode/compiler_lexical.rs
+++ b/crates/qjs-runtime/src/bytecode/compiler_lexical.rs
@@ -1,5 +1,3 @@
-use std::collections::HashMap;
-
use qjs_ast::{BindingPattern, ForInLeft, ForInit, FunctionParams, Stmt, SwitchCase, VarKind};
use crate::RuntimeError;
@@ -139,7 +137,8 @@ impl Compiler {
&mut self,
compile: impl FnOnce(&mut Self) -> Result,
) -> Result {
- self.lexical_scopes.push(HashMap::new());
+ self.lexical_scopes
+ .push(crate::value::name_hash::NameMap::default());
let result = compile(self);
self.lexical_scopes
.pop()
@@ -238,13 +237,15 @@ impl Compiler {
}
}
- fn current_lexical_scope(&self) -> &HashMap {
+ fn current_lexical_scope(&self) -> &crate::value::name_hash::NameMap {
self.lexical_scopes
.last()
.expect("compiler should always have a lexical scope")
}
- fn current_lexical_scope_mut(&mut self) -> &mut HashMap {
+ fn current_lexical_scope_mut(
+ &mut self,
+ ) -> &mut crate::value::name_hash::NameMap {
self.lexical_scopes
.last_mut()
.expect("compiler should always have a lexical scope")
diff --git a/crates/qjs-runtime/src/bytecode/ir.rs b/crates/qjs-runtime/src/bytecode/ir.rs
index e3901ceab..67d6af11c 100644
--- a/crates/qjs-runtime/src/bytecode/ir.rs
+++ b/crates/qjs-runtime/src/bytecode/ir.rs
@@ -1010,30 +1010,19 @@ impl Bytecode {
&self.global_names
}
- pub(crate) fn referenced_global_names(&self) -> Vec {
- let mut names = BTreeSet::new();
- for name in &self.global_names {
- names.insert(name.clone());
- }
- for local in &self.locals {
- if !local.sloppy_global_fallback && !local.from_env {
- names.remove(&local.name);
- }
- }
- names.into_iter().collect()
- }
-
- pub(crate) fn closure_referenced_global_names(&self) -> Vec {
- self.cached_closure_referenced_global_names.clone()
+ pub(super) fn closure_referenced_global_names_ref(&self) -> &[String] {
+ &self.cached_closure_referenced_global_names
}
fn compute_closure_referenced_global_names(&self) -> Vec {
- let mut names = BTreeSet::new();
- for name in self.referenced_global_names() {
- names.insert(name);
+ let mut names: BTreeSet<&str> = self.global_names.iter().map(String::as_str).collect();
+ for local in &self.locals {
+ if !local.sloppy_global_fallback && !local.from_env {
+ names.remove(local.name.as_str());
+ }
}
super::ir_names::collect_nested_global_names_from_ops(&self.code, &mut names);
- names.into_iter().collect()
+ names.into_iter().map(str::to_owned).collect()
}
pub(crate) fn written_binding_names(&self) -> Vec {
@@ -1055,23 +1044,24 @@ impl Bytecode {
collect_written_binding_names_from_ops(self, &self.code, &mut names);
for local in &self.locals {
if !local.sloppy_global_fallback && !local.from_env {
- names.remove(&local.name);
+ names.remove(local.name.as_str());
}
}
- names.into_iter().collect()
+ names.into_iter().map(str::to_owned).collect()
}
- pub(crate) fn closure_written_binding_names(&self) -> Vec {
- self.cached_closure_written_binding_names.clone()
+ pub(super) fn closure_written_binding_names_ref(&self) -> &[String] {
+ &self.cached_closure_written_binding_names
}
fn compute_closure_written_binding_names(&self) -> Vec {
- let mut names = BTreeSet::new();
- for name in self.written_binding_names() {
- names.insert(name);
- }
+ let mut names: BTreeSet<&str> = self
+ .cached_written_binding_names
+ .iter()
+ .map(String::as_str)
+ .collect();
super::ir_names::collect_nested_written_binding_names_from_ops(&self.code, &mut names);
- names.into_iter().collect()
+ names.into_iter().map(str::to_owned).collect()
}
pub(crate) fn global_lexical_names(&self) -> &[String] {
@@ -1503,7 +1493,8 @@ impl Bytecode {
fn compute_writes_binding_set(&self) -> crate::value::name_hash::NameSet {
let mut direct = BTreeSet::new();
collect_written_binding_names_from_ops(self, &self.code, &mut direct);
- let mut set: crate::value::name_hash::NameSet = direct.into_iter().collect();
+ let mut set: crate::value::name_hash::NameSet =
+ direct.into_iter().map(str::to_owned).collect();
for op in &self.code {
match op {
Op::NewFunction { bytecode, .. } => {
@@ -1610,10 +1601,10 @@ fn op_touches_local_slot(op: &Op, slot: usize) -> bool {
}
}
-fn collect_written_binding_names_from_ops(
- bytecode: &Bytecode,
- code: &[Op],
- names: &mut BTreeSet,
+fn collect_written_binding_names_from_ops<'a>(
+ bytecode: &'a Bytecode,
+ code: &'a [Op],
+ names: &mut BTreeSet<&'a str>,
) {
for op in code {
match op {
@@ -1627,7 +1618,7 @@ fn collect_written_binding_names_from_ops(
| Op::StoreResolvedIdentWith {
name, slot: None, ..
} => {
- names.insert(name.clone());
+ names.insert(name.as_str());
}
Op::StoreLocal(slot)
| Op::AssignLocal(slot)
@@ -1643,7 +1634,7 @@ fn collect_written_binding_names_from_ops(
.get(*slot)
.filter(|local| !local.compiler_temporary)
{
- names.insert(local.name.clone());
+ names.insert(local.name.as_str());
}
}
_ => {}
@@ -1654,10 +1645,10 @@ fn collect_written_binding_names_from_ops(
fn collect_global_names(code: &[Op]) -> Vec {
let mut names = BTreeSet::new();
collect_global_names_from_ops(code, &mut names);
- names.into_iter().collect()
+ names.into_iter().map(str::to_owned).collect()
}
-fn collect_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
+fn collect_global_names_from_ops<'a>(code: &'a [Op], names: &mut BTreeSet<&'a str>) {
for op in code {
match op {
Op::LoadGlobal(name)
@@ -1665,10 +1656,10 @@ fn collect_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
| Op::StoreGlobalSloppy { name, .. }
| Op::AppendStringLiteralGlobal { name, .. }
| Op::TypeofGlobal(name) => {
- names.insert(name.clone());
+ names.insert(name.as_str());
}
Op::StoreLocalOrGlobalSloppy { name, .. } => {
- names.insert(name.clone());
+ names.insert(name.as_str());
}
Op::LoadIdentWith {
name, slot: None, ..
@@ -1686,10 +1677,10 @@ fn collect_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
name, slot: None, ..
}
| Op::TypeofIdentWith { name, slot: None } => {
- names.insert(name.clone());
+ names.insert(name.as_str());
}
Op::NewFunction { bytecode, .. } => {
- names.extend(bytecode.global_names().iter().cloned());
+ names.extend(bytecode.global_names().iter().map(String::as_str));
}
Op::NewClass { definition } => {
names.extend(
@@ -1698,39 +1689,50 @@ fn collect_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
.bytecode
.global_names()
.iter()
- .cloned(),
+ .map(String::as_str),
);
for key in &definition.computed_keys {
if let ClassComputedKeyDef::Deferred { bytecode, .. } = key {
- names.extend(bytecode.global_names().iter().cloned());
+ names.extend(bytecode.global_names().iter().map(String::as_str));
}
}
for element in &definition.elements {
match element {
ClassElementDef::Method(method) => {
- names.extend(method.bytecode.global_names().iter().cloned());
+ names.extend(method.bytecode.global_names().iter().map(String::as_str));
}
ClassElementDef::Field(field) => {
if let Some(initializer) = &field.initializer {
- names.extend(initializer.bytecode.global_names().iter().cloned());
+ names.extend(
+ initializer
+ .bytecode
+ .global_names()
+ .iter()
+ .map(String::as_str),
+ );
}
}
ClassElementDef::Private(element) => match element {
ClassPrivateElementDef::Field { initializer, .. } => {
if let Some(initializer) = initializer {
names.extend(
- initializer.bytecode.global_names().iter().cloned(),
+ initializer
+ .bytecode
+ .global_names()
+ .iter()
+ .map(String::as_str),
);
}
}
ClassPrivateElementDef::Method { def, .. }
| ClassPrivateElementDef::Getter { def, .. }
| ClassPrivateElementDef::Setter { def, .. } => {
- names.extend(def.bytecode.global_names().iter().cloned());
+ names
+ .extend(def.bytecode.global_names().iter().map(String::as_str));
}
},
ClassElementDef::StaticBlock(block) => {
- names.extend(block.bytecode.global_names().iter().cloned());
+ names.extend(block.bytecode.global_names().iter().map(String::as_str));
}
}
}
@@ -1738,13 +1740,19 @@ fn collect_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
match element {
ClassPrivateElementDef::Field { initializer, .. } => {
if let Some(initializer) = initializer {
- names.extend(initializer.bytecode.global_names().iter().cloned());
+ names.extend(
+ initializer
+ .bytecode
+ .global_names()
+ .iter()
+ .map(String::as_str),
+ );
}
}
ClassPrivateElementDef::Method { def, .. }
| ClassPrivateElementDef::Getter { def, .. }
| ClassPrivateElementDef::Setter { def, .. } => {
- names.extend(def.bytecode.global_names().iter().cloned());
+ names.extend(def.bytecode.global_names().iter().map(String::as_str));
}
}
}
diff --git a/crates/qjs-runtime/src/bytecode/ir_names.rs b/crates/qjs-runtime/src/bytecode/ir_names.rs
index 93a044ab3..18168711f 100644
--- a/crates/qjs-runtime/src/bytecode/ir_names.rs
+++ b/crates/qjs-runtime/src/bytecode/ir_names.rs
@@ -2,21 +2,24 @@ use std::collections::BTreeSet;
use super::ir::{ClassElementDef, ClassPrivateElementDef, Op};
-pub(super) fn collect_nested_global_names_from_ops(code: &[Op], names: &mut BTreeSet) {
+pub(super) fn collect_nested_global_names_from_ops<'a>(
+ code: &'a [Op],
+ names: &mut BTreeSet<&'a str>,
+) {
for op in code {
match op {
Op::NewFunction { bytecode, .. } => {
- for name in bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
Op::NewClass { definition } => {
for name in definition
.constructor
.bytecode
- .closure_referenced_global_names()
+ .closure_referenced_global_names_ref()
{
- names.insert(name);
+ names.insert(name.as_str());
}
for element in &definition.elements {
collect_class_element_global_names(element, names);
@@ -27,24 +30,24 @@ pub(super) fn collect_nested_global_names_from_ops(code: &[Op], names: &mut BTre
}
}
-pub(super) fn collect_nested_written_binding_names_from_ops(
- code: &[Op],
- names: &mut BTreeSet,
+pub(super) fn collect_nested_written_binding_names_from_ops<'a>(
+ code: &'a [Op],
+ names: &mut BTreeSet<&'a str>,
) {
for op in code {
match op {
Op::NewFunction { bytecode, .. } => {
- for name in bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
Op::NewClass { definition } => {
for name in definition
.constructor
.bytecode
- .closure_written_binding_names()
+ .closure_written_binding_names_ref()
{
- names.insert(name);
+ names.insert(name.as_str());
}
for element in &definition.elements {
collect_class_element_written_names(element, names);
@@ -55,91 +58,97 @@ pub(super) fn collect_nested_written_binding_names_from_ops(
}
}
-fn collect_class_element_global_names(element: &ClassElementDef, names: &mut BTreeSet) {
+fn collect_class_element_global_names<'a>(
+ element: &'a ClassElementDef,
+ names: &mut BTreeSet<&'a str>,
+) {
match element {
ClassElementDef::Method(def) => {
- for name in def.bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in def.bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
ClassElementDef::Field(def) => {
if let Some(initializer) = &def.initializer {
- for name in initializer.bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in initializer.bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
}
ClassElementDef::Private(def) => collect_private_class_element_global_names(def, names),
ClassElementDef::StaticBlock(def) => {
- for name in def.bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in def.bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
}
}
-fn collect_private_class_element_global_names(
- element: &ClassPrivateElementDef,
- names: &mut BTreeSet,
+fn collect_private_class_element_global_names<'a>(
+ element: &'a ClassPrivateElementDef,
+ names: &mut BTreeSet<&'a str>,
) {
match element {
ClassPrivateElementDef::Field { initializer, .. } => {
if let Some(initializer) = initializer {
- for name in initializer.bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in initializer.bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
}
ClassPrivateElementDef::Method { def, .. }
| ClassPrivateElementDef::Getter { def, .. }
| ClassPrivateElementDef::Setter { def, .. } => {
- for name in def.bytecode.closure_referenced_global_names() {
- names.insert(name);
+ for name in def.bytecode.closure_referenced_global_names_ref() {
+ names.insert(name.as_str());
}
}
}
}
-fn collect_class_element_written_names(element: &ClassElementDef, names: &mut BTreeSet) {
+fn collect_class_element_written_names<'a>(
+ element: &'a ClassElementDef,
+ names: &mut BTreeSet<&'a str>,
+) {
match element {
ClassElementDef::Method(def) => {
- for name in def.bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in def.bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
ClassElementDef::Field(def) => {
if let Some(initializer) = &def.initializer {
- for name in initializer.bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in initializer.bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
}
ClassElementDef::Private(def) => collect_private_class_element_written_names(def, names),
ClassElementDef::StaticBlock(def) => {
- for name in def.bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in def.bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
}
}
-fn collect_private_class_element_written_names(
- element: &ClassPrivateElementDef,
- names: &mut BTreeSet,
+fn collect_private_class_element_written_names<'a>(
+ element: &'a ClassPrivateElementDef,
+ names: &mut BTreeSet<&'a str>,
) {
match element {
ClassPrivateElementDef::Field { initializer, .. } => {
if let Some(initializer) = initializer {
- for name in initializer.bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in initializer.bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
}
ClassPrivateElementDef::Method { def, .. }
| ClassPrivateElementDef::Getter { def, .. }
| ClassPrivateElementDef::Setter { def, .. } => {
- for name in def.bytecode.closure_written_binding_names() {
- names.insert(name);
+ for name in def.bytecode.closure_written_binding_names_ref() {
+ names.insert(name.as_str());
}
}
}
diff --git a/crates/qjs-runtime/src/bytecode/vm_numeric_leaf.rs b/crates/qjs-runtime/src/bytecode/vm_numeric_leaf.rs
index 3ff2f3690..5f25229d5 100644
--- a/crates/qjs-runtime/src/bytecode/vm_numeric_leaf.rs
+++ b/crates/qjs-runtime/src/bytecode/vm_numeric_leaf.rs
@@ -669,6 +669,12 @@ pub(super) fn math_unary(native: NativeFunction, argument: f64) -> Option {
/// A `Math` function of two arguments whose entire effect is a floating-point
/// computation. `max`/`min` follow the spec's NaN and signed-zero rules rather
/// than Rust's, which disagree on both.
+///
+/// Out of line: whether LLVM inlined it into the typed-loop executor flipped
+/// with unrelated edits elsewhere in the crate, and the inlined copy grew
+/// the executor by 176 bytes and re-rolled its code (access-nsieve +4%,
+/// heterogeneous_property_read +5% cycles at equal instructions).
+#[inline(never)]
pub(super) fn math_binary(native: NativeFunction, left: f64, right: f64) -> Option {
let value = match native {
NativeFunction::MathPow => crate::operations::number_exponentiate(left, right),
diff --git a/crates/qjs-runtime/src/string.rs b/crates/qjs-runtime/src/string.rs
index c5edf9049..88951f28d 100644
--- a/crates/qjs-runtime/src/string.rs
+++ b/crates/qjs-runtime/src/string.rs
@@ -224,6 +224,13 @@ pub(crate) fn string_from_code_units(code_units: &[u16]) -> String {
}
pub(crate) fn string_from_utf8_scalars(value: &str) -> String {
+ // Only a scalar at or above U+10000 -- four UTF-8 bytes, led by 0xF0 or
+ // above -- can fall in the surrogate-escape sentinel range; text without
+ // one is already canonical. Function source text is converted this way
+ // for every function a script declares, nested ones included.
+ if value.bytes().all(|byte| byte < 0xF0) {
+ return value.to_owned();
+ }
let mut result = String::with_capacity(value.len());
for character in value.chars() {
push_code_point(&mut result, character as u32);
diff --git a/crates/qjs-runtime/src/tests/expressions.rs b/crates/qjs-runtime/src/tests/expressions.rs
index 764659661..734e2f214 100644
--- a/crates/qjs-runtime/src/tests/expressions.rs
+++ b/crates/qjs-runtime/src/tests/expressions.rs
@@ -1534,3 +1534,41 @@ fn instanceof_respects_a_custom_has_instance() {
Ok(Value::String("true:true".to_owned().into()))
);
}
+
+/// Every binary precedence level against its neighbours and left
+/// associativity within a level, parsed by precedence climbing. Expected
+/// values from V8.
+#[test]
+fn binary_operators_bind_by_precedence_and_associate_left() {
+ let source = r#"var a = 5, b = 3, c = 12, d = 1, e = 2;
+var o = { k: 1 };
+var r = [];
+r.push(a + b * c - d / e % 3);
+r.push(a - b - c - d);
+r.push(c / b / e);
+r.push(a << d + e >> 1 >>> 0);
+r.push(a < b == c > d);
+r.push(a | b ^ c & d);
+r.push(a & b | c ^ d);
+r.push(a || b && 0 || c);
+r.push(0 && a || b);
+r.push(a == 5 === true != false);
+r.push('k' in o && !('z' in o));
+r.push(o instanceof Object == true);
+r.push(2 ** 3 ** 2);
+r.push(-(2 ** 2) + 3 * 2 ** 2);
+r.push(a + b + '' + a + b);
+r.push(null ?? (0 || 7));
+for (var i = ('k' in o) ? 1 : 0, j = 0; j < 1; j++) r.push(i);
+for (var q in { x: 1 }) r.push(q);
+r.push(a > b ? c < d ? 1 : 2 : 3);
+r.join(',');"#;
+ assert_eq!(
+ eval(source),
+ Ok(Value::String(
+ "40.5,-11,2,20,false,7,13,5,3,true,true,true,512,8,853,7,1,x,2"
+ .to_owned()
+ .into()
+ ))
+ );
+}
diff --git a/crates/qjs-runtime/src/tests/statements.rs b/crates/qjs-runtime/src/tests/statements.rs
index 214e5c996..200d625ba 100644
--- a/crates/qjs-runtime/src/tests/statements.rs
+++ b/crates/qjs-runtime/src/tests/statements.rs
@@ -1587,3 +1587,29 @@ fn for_in_over_a_primitive_enumerates_its_wrapper() {
Ok(Value::String("0,1,extra||n|||0,1".to_owned().into()))
);
}
+
+/// A `for` head's initializer withholds `in` (it would start a for-in) only
+/// at its own nesting level: inside parentheses, brackets, braces, call
+/// arguments, template substitutions and function bodies `in` is the
+/// operator again, while an unbracketed arrow body inherits the restriction.
+#[test]
+fn for_initializers_allow_in_inside_brackets() {
+ assert_eq!(
+ eval(
+ "var o = { k: 1 }; var r = [];
+ function f(x) { return x; }
+ for (var a = ('k' in o), b = ['k' in o], c = f('k' in o),
+ d = function () { return 'k' in o; }, e = `${'k' in o}`,
+ g = { v: 'k' in o }, j = 0; j < 1; j++) {
+ r.push(a, b[0], c, d(), e, g.v);
+ }
+ for (var x = 0 in o) r.push(x);
+ r.join();"
+ ),
+ Ok(Value::String(
+ "true,true,true,true,true,true,k".to_owned().into()
+ ))
+ );
+ assert!(eval("var o = {}; for (var f = x => 'k' in o; false;) {}").is_err());
+ assert!(eval("var o = {}; for (var i = 'k' in o; false;) {}").is_err());
+}
diff --git a/docs/performance-knowledge.md b/docs/performance-knowledge.md
index 5cd468d37..8ab8e3b24 100644
--- a/docs/performance-knowledge.md
+++ b/docs/performance-knowledge.md
@@ -37,6 +37,10 @@ The procedure (queue, plan, decision) is in
harness, from the same directory, with the same build recipe, interleaved
on one host. The same script has measured differently from different
filesystem locations; never compare numbers taken along different paths.
+ The same holds for binaries: two byte-identical `qjs` copies at different
+ paths measured ai-astar 1-2% apart, consistently (2026-09-26). Warm each
+ binary once before an A/B too -- a freshly built one runs its first
+ process slower.
- **Keep samples long enough.** Short runs are dominated by startup, timer
resolution and scheduling. Raise iterations until each sample clears the
harness minimum instead of adding repetitions of a too-short sample.
@@ -133,7 +137,11 @@ The procedure (queue, plan, decision) is in
(`# budget` lines) and change only when a function outgrows its slot.
The per-codegen-unit copies go last. Re-run `layout_pin` after every
code change (fillers are sized from the binary it reads), and add a hot
- executor callee to `CALLEES` instead of letting it float.
+ executor callee to `CALLEES` instead of letting it float. With the
+ callees fixed, the executor's offset was re-scanned to 0x200. Check the
+ executor's own size in `nm` between builds as well: a codegen-unit shift
+ inlined `math_binary` into it (+176 bytes, access-nsieve +4% at equal
+ instructions), now kept out of line.
- **Know each case's codegen noise band before blaming a change.** The
functions that hold a dispatch loop are re-compiled differently by edits
anywhere in the crate (an inlined thread-local access, a helper's inline
diff --git a/docs/performance-workflow.md b/docs/performance-workflow.md
index c21438567..6d19f71de 100644
--- a/docs/performance-workflow.md
+++ b/docs/performance-workflow.md
@@ -211,6 +211,34 @@ For multiple receipts, place their directories under one profile root and use
relative paths such as `case-a/receipt.json` in the plan. `check-unit` checks
only plan structure; `validate-unit` checks actual profile artifacts too.
+## Diagnostic helpers
+
+Tools for finding and checking work between formal runs; none of them
+produces decision evidence.
+
+- **Whole-corpus A/B.** `python3 -m tools.benchmark.screen --candidate B
+ --base A --case external --pairs 3` alternates the two executables over
+ every external bundle and reports instruction and cycle ratios. Prefer it
+ to ad-hoc loops: it validates output, holds the measurement lock, and
+ waits for builds. An instruction ratio near 1.0 with a cycles ratio that
+ moves is layout, not the change (see performance-knowledge.md).
+- **Bundles on disk.** `python3 -m tools.benchmark.bundles` writes each
+ case to `target/bundles/--.js` exactly as a run executes it,
+ for `sample` profiles and trace runs.
+- **Front-end cost.** `python3 -m tools.benchmark.front_end --binary B
+ --reference third_party/quickjs-ng/build/qjs` measures lexing, parsing and
+ compiling alone (each bundle wrapped in a function never called, process
+ start subtracted) against QuickJS-NG, with each case's share of its whole
+ run; `--base A` compares two builds instead.
+- **Executor placement.** `scripts/layout-scan.sh --offsets 0x0,0x200,0x400`
+ pins the typed-loop executor at each offset, relinks to a fixed point and
+ screens the layout canaries; run it after editing the executor or a
+ pinned callee (`tools/benchmark/layout_pin.py`).
+- **Typed-loop programs.** On a `perf-counters` build, `QJS_TL_TRACE=4`
+ prints each program as it runs (after register packing, hoisting and
+ copy forwarding) and `QJS_TL_NO_FORWARD=1` disables forwarding, so one
+ binary can A/B it (docs/benchmarking.md).
+
## Acceptance after implementation
Measure the candidate against the same base executable represented by the
diff --git a/scripts/layout-scan.sh b/scripts/layout-scan.sh
new file mode 100755
index 000000000..fdf96193e
--- /dev/null
+++ b/scripts/layout-scan.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Scan the typed-loop executor's pinned offset (tools/benchmark/layout_pin.py).
+#
+# For each offset: pin the executor there, relink until the order file is a
+# fixed point, keep the binary as target/layout-scan/qjs-, and screen
+# it against a base with tools.benchmark.screen on the layout canaries. The
+# order file is restored afterwards; adopt an offset by setting
+# DEFAULT_OFFSET in layout_pin.py and re-pinning. Diagnostic only: run it
+# after editing the executor or its pinned callees, on a quiet host.
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+. "$ROOT_DIR/scripts/lib.sh"
+export PYTHONDONTWRITEBYTECODE=1
+export PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}"
+
+usage() {
+ cat <<'EOF'
+usage: scripts/layout-scan.sh --offsets [--base ] [--case ]... [--pairs ]
+
+ --offsets executor offsets modulo 4 KiB, e.g. 0x0,0x200,0x400
+ --base executable to compare against (default: a copy of the current
+ target/release/qjs, taken before the first relink)
+ --case tools.benchmark.screen case spec; repeatable (default: the six
+ sentinels, ai-astar, imaging-desaturate, access-nsieve,
+ access-nbody -- the cases the executor's placement has moved)
+ --pairs screen pairs per case (default 2)
+EOF
+}
+
+offsets=""
+base=""
+pairs=2
+cases=()
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --offsets) offsets="${2:?}"; shift 2 ;;
+ --base) base="${2:?}"; shift 2 ;;
+ --case) cases+=("${2:?}"); shift 2 ;;
+ --pairs) pairs="${2:?}"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "error: unknown argument $1" >&2; usage >&2; exit 2 ;;
+ esac
+done
+if [ -z "$offsets" ]; then
+ usage >&2
+ exit 2
+fi
+if [ ${#cases[@]} -eq 0 ]; then
+ cases=(sentinel external/kraken-1.1/ai-astar external/kraken-1.1/imaging-desaturate
+ external/sunspider-1.0/access-nsieve external/sunspider-1.0/access-nbody)
+fi
+
+cargo_bin="$(qjs_resolve_cargo)" || { echo "error: cargo not found" >&2; exit 1; }
+cd "$ROOT_DIR"
+order=crates/qjs-cli/hot-functions.order
+scan_dir=target/layout-scan
+mkdir -p "$scan_dir"
+"$cargo_bin" build --release -q -p qjs-cli
+if [ -z "$base" ]; then
+ base="$scan_dir/qjs-base"
+ cp target/release/qjs "$base"
+fi
+saved="$(mktemp "${TMPDIR:-/tmp}/qjs-order.XXXXXX")"
+cp "$order" "$saved"
+trap 'cp "$saved" "$order"; rm -f "$saved"' EXIT
+
+case_args=()
+for spec in "${cases[@]}"; do
+ case_args+=(--case "$spec")
+done
+
+IFS=',' read -r -a offset_list <<< "$offsets"
+for offset in "${offset_list[@]}"; do
+ cp "$saved" "$order"
+ # Fillers are sized from the binary they are computed on, so pin, relink
+ # and pin again until the order file stops changing.
+ for _ in 1 2 3; do
+ before="$(shasum "$order")"
+ python3 -m tools.benchmark.layout_pin --binary target/release/qjs --offset "$offset" >/dev/null
+ "$cargo_bin" build --release -q -p qjs-cli
+ [ "$(shasum "$order")" = "$before" ] && break
+ done
+ cp target/release/qjs "$scan_dir/qjs-$offset"
+ echo "== executor at $offset: $scan_dir/qjs-$offset" >&2
+ python3 -m tools.benchmark.screen --candidate "$scan_dir/qjs-$offset" --base "$base" \
+ --pairs "$pairs" "${case_args[@]}"
+done
diff --git a/tasks/T033-wide-tier-interpreter-exits.md b/tasks/T033-wide-tier-interpreter-exits.md
index 09128fa91..fb7d396af 100644
--- a/tasks/T033-wide-tier-interpreter-exits.md
+++ b/tasks/T033-wide-tier-interpreter-exits.md
@@ -342,6 +342,23 @@ Plan and evidence: `tasks/performance-units/wide-tier-interpreter-exits.json`
0.88, fannkuch 0.92. Liveness counts a site's entries only where an
operation can stop. Differential fuzz (600 random loops with type
changes mid-loop) matches the pass-off build and V8.
+- perf23 units (c88cbb06..04e0e7aa): the front end measured 2-3x
+ QuickJS-NG (`tools.benchmark.front_end`; 1-8% of many cases' totals):
+ binary operators by precedence climbing (imaging-darkroom's parse -21%
+ cycles), the compiler's scope tables on the name hasher (cdjs parse
+ -10%), canonical UTF-8 copied straight through (cdjs -9% instructions),
+ borrowed name sets in bytecode finalization. A parser bug found on the
+ way: `in` inside brackets of a for initializer was rejected. Layout:
+ math_binary kept out of the typed executor (a codegen-unit shift had
+ inlined it: nsieve +4% at equal instructions), boxed_equality and
+ get_named pinned, executor re-scanned to 0x200.
+- Stack run 3babb930 vs main f2b21ab6 (30 blocks, cycles, quiet host;
+ `target/comparison/perf23-3babb930`): external geomean **0.994** against
+ main and **0.772 against QuickJS-NG**; gaussian-blur (JetStream) 0.952,
+ imaging-darkroom 0.963, tinderbox 0.965, desaturate 0.971. Worst:
+ ai-astar 1.023 (byte-identical binaries at two paths differ by 1-2%
+ there), regexp-dna 1.015; sentinels 1.003. Slowest against QuickJS-NG:
+ xparb 1.46, tofte 1.41, validate-input 1.31, binary-trees 1.31.
- Stack run 88e90f89 vs main 8d7c580e (30 blocks, cycles, quiet host;
`target/comparison/perf22-88e90f89`): perf21's and perf22's units.
External geomean **0.962** against main and **0.782 against
diff --git a/tools/benchmark/bundles.py b/tools/benchmark/bundles.py
new file mode 100644
index 000000000..837f71b6c
--- /dev/null
+++ b/tools/benchmark/bundles.py
@@ -0,0 +1,51 @@
+"""Write every external case's bundle to a directory, exactly as a run
+executes it, as `--.js` -- for `sample` profiles, trace runs and
+other diagnostic work. Diagnostic only; formal runs build their own.
+
+Usage:
+ python3 -m tools.benchmark.bundles [--output-dir target/bundles]
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from typing import Sequence
+
+from .external_preview import ExternalPreviewError, _bundle_source, fetch_corpora, load_manifest
+from .screen import DEFAULT_CACHE, EXTERNAL_MANIFEST
+
+
+def write_bundles(manifest_path: Path, cache_root: Path, output_dir: Path) -> int:
+ manifest = load_manifest(manifest_path)
+ fetch_corpora(manifest, cache_root)
+ output_dir.mkdir(parents=True, exist_ok=True)
+ written = 0
+ for suite in manifest.suites:
+ for case in suite.cases:
+ bundle = output_dir / f"{suite.id}--{case.id}.js"
+ bundle.write_text(_bundle_source(suite, case, cache_root.resolve()), encoding="utf-8")
+ written += 1
+ return written
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(prog="python3 -m tools.benchmark.bundles",
+ description=__doc__.split("\n\n")[0])
+ parser.add_argument("--output-dir", type=Path, default=Path("target/bundles"))
+ parser.add_argument("--cache-root", type=Path, default=DEFAULT_CACHE)
+ parser.add_argument("--manifest", type=Path, default=EXTERNAL_MANIFEST)
+ args = parser.parse_args(argv)
+ try:
+ written = write_bundles(args.manifest, args.cache_root, args.output_dir)
+ except (ExternalPreviewError, ValueError, OSError) as error:
+ print(f"error: {error}", file=sys.stderr)
+ return 1
+ print(json.dumps({"bundles": written, "output_dir": str(args.output_dir)}))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tools/benchmark/front_end.py b/tools/benchmark/front_end.py
new file mode 100644
index 000000000..d83eefa7a
--- /dev/null
+++ b/tools/benchmark/front_end.py
@@ -0,0 +1,134 @@
+"""Parse-and-compile cost of the external corpus, against a reference engine.
+
+Every external case runs its whole process, so its time includes lexing,
+parsing and compiling the bundle, which this engine did 2-3x slower than
+QuickJS-NG (2026-09-26; 1-8% of many cases' totals). This measures that
+front end alone: each bundle is wrapped in a function that is never called,
+so the engine parses and compiles all of it and runs nothing, and the cost
+of an empty script (process start) is subtracted. Hardware counters, as in
+`tools.benchmark.screen`; diagnostic only, never decision evidence.
+
+Usage:
+ python3 -m tools.benchmark.front_end --binary B [--reference NG] [--base A]
+ [--case external// ...] [--pairs N]
+
+With `--reference` the table gives the engine's front-end cost over the
+reference's; with `--base` (another build of this engine), candidate over
+base. The `share` column is the front-end excess over the reference as a
+fraction of the case's whole run on `--binary`: what a front end as fast as
+the reference would save.
+"""
+
+from __future__ import annotations
+
+import argparse
+import math
+import sys
+import tempfile
+from pathlib import Path
+from typing import Sequence
+
+from .external_preview import ExternalPreviewError
+from .measure_lock import measurement_lock
+from .screen import (
+ DEFAULT_CACHE,
+ CaseSpec,
+ CounterTool,
+ ScreenError,
+ counter_tool,
+ resolve_cases,
+ sample,
+)
+
+EMPTY_SCRIPT = "1;\n"
+
+
+def _flags(binary: Path, reference: Path | None) -> list[str]:
+ # QuickJS-NG treats a file as a module unless told otherwise.
+ return ["--script"] if reference is not None and binary == reference else ["--raw"]
+
+
+def _cost(tool: CounterTool, binary: Path, flags: list[str], script: Path, pairs: int,
+ timeout: float) -> tuple[float, float]:
+ """Least instructions and cycles of running `script`, after one
+ discarded run: a binary's first process after a build or a pause runs
+ slower, which inflated the empty-script baseline past small front ends.
+ Noise only adds to a cost, so the minimum estimates it best."""
+ argv = [str(binary), *flags, str(script)]
+ sample(tool, argv, timeout)
+ taken = [sample(tool, argv, timeout) for _ in range(pairs)]
+ return (min(s.instructions for s in taken), min(s.cycles for s in taken))
+
+
+def _wrap(bundle: Path, work: Path) -> Path:
+ wrapped = work / f"front-end--{bundle.name}"
+ source = bundle.read_text(encoding="utf-8")
+ wrapped.write_text("function __qjsFrontEndOnly__() {\n" + source + "\n}\n1;\n", encoding="utf-8")
+ return wrapped
+
+
+def _parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(prog="python3 -m tools.benchmark.front_end",
+ description=__doc__.split("\n\n")[0])
+ parser.add_argument("--binary", type=Path, required=True)
+ parser.add_argument("--reference", type=Path,
+ help="a reference engine (QuickJS-NG, run with --script)")
+ parser.add_argument("--base", type=Path, help="another build of this engine")
+ parser.add_argument("--case", action="append", default=[],
+ help="external//; repeatable (default: every external case)")
+ parser.add_argument("--pairs", type=int, default=3)
+ parser.add_argument("--timeout", type=float, default=120.0)
+ parser.add_argument("--cache-root", type=Path, default=DEFAULT_CACHE)
+ return parser
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ args = _parser().parse_args(argv)
+ if (args.reference is None) == (args.base is None):
+ print("error: give exactly one of --reference and --base", file=sys.stderr)
+ return 2
+ binary = args.binary.resolve()
+ other = (args.reference or args.base).resolve()
+ reference = other if args.reference is not None else None
+ try:
+ tool = counter_tool()
+ with measurement_lock("front-end"), tempfile.TemporaryDirectory(prefix="qjs-front-end-") as work:
+ work_dir = Path(work)
+ cases: list[CaseSpec] = resolve_cases(args.case or ["external"], args.cache_root, work_dir)
+ empty = work_dir / "empty.js"
+ empty.write_text(EMPTY_SCRIPT, encoding="utf-8")
+ start = {role: _cost(tool, exe, _flags(exe, reference), empty, args.pairs, args.timeout)
+ for role, exe in (("binary", binary), ("other", other))}
+ rows = []
+ for case in cases:
+ if case.kind != "whole":
+ continue
+ wrapped = _wrap(case.workload, work_dir)
+ front = {}
+ for role, exe in (("binary", binary), ("other", other)):
+ instructions, cycles = _cost(tool, exe, _flags(exe, reference), wrapped,
+ args.pairs, args.timeout)
+ front[role] = (instructions - start[role][0], cycles - start[role][1])
+ _, whole_cycles = _cost(tool, binary, ["--raw"], case.workload, 1, args.timeout)
+ rows.append((case.id, front, whole_cycles))
+ except (ScreenError, ExternalPreviewError) as error:
+ print(f"error: {error}", file=sys.stderr)
+ return 1
+ label = "reference" if reference is not None else "base"
+ print(f"{'case':56s} {'Mcyc':>8s} {label + ' Mcyc':>15s} {'ratio':>6s} {'share':>6s}")
+ logs = []
+ for case_id, front, whole_cycles in rows:
+ mine, theirs = front["binary"][1], front["other"][1]
+ ratio = mine / theirs if theirs > 0 and mine > 0 else math.nan
+ share = (mine - theirs) / whole_cycles if whole_cycles > 0 else math.nan
+ if ratio == ratio:
+ logs.append(math.log(ratio))
+ print(f"{case_id:56s} {mine / 1e6:8.1f} {theirs / 1e6:15.1f} {ratio:6.2f} {share:6.1%}")
+ if logs:
+ print(f"geometric mean front-end cycles ratio: {math.exp(sum(logs) / len(logs)):.3f} "
+ f"over {len(logs)} cases (diagnostic only)")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tools/benchmark/layout_pin.py b/tools/benchmark/layout_pin.py
index 1421af119..43582afd5 100644
--- a/tools/benchmark/layout_pin.py
+++ b/tools/benchmark/layout_pin.py
@@ -39,7 +39,7 @@
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_ORDER = ROOT / "crates/qjs-cli/hot-functions.order"
-DEFAULT_OFFSET = 0x0
+DEFAULT_OFFSET = 0x200
# The same executor instantiated for the interpreter's own loops -- a
# script's top-level `for` -- is pinned too, ahead of its wide twin:
# it is most of access-fannkuch and math-partial-sums, and floating in the
@@ -69,6 +69,8 @@
"slot_readsNtB4_9ObjectRef22own_data_property_read",
"ObjectRef32write_existing_own_data_property",
"typed_loop7execute16boxed_truthiness",
+ "typed_loop7execute14boxed_equality",
+ "typed_loop7execute9get_named",
)
# After the budgeted callees: defined once per codegen unit, in a number of
# copies that changes with unrelated code, so nothing pinned may follow them.
diff --git a/tools/benchmark/tests/test_front_end.py b/tools/benchmark/tests/test_front_end.py
new file mode 100644
index 000000000..5cd219c82
--- /dev/null
+++ b/tools/benchmark/tests/test_front_end.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from tools.benchmark import front_end
+
+
+class FrontEndTests(unittest.TestCase):
+ def test_wrap_puts_the_bundle_in_a_function_that_is_never_called(self):
+ with tempfile.TemporaryDirectory() as work:
+ bundle = Path(work) / "suite--case.js"
+ bundle.write_text("var x = 1;\nprint(x);\n", encoding="utf-8")
+ wrapped = front_end._wrap(bundle, Path(work)).read_text(encoding="utf-8")
+ self.assertTrue(wrapped.startswith("function __qjsFrontEndOnly__() {\n"))
+ self.assertIn("var x = 1;\nprint(x);\n", wrapped)
+ self.assertTrue(wrapped.endswith("\n}\n1;\n"))
+
+ def test_only_the_reference_runs_as_a_script(self):
+ engine, reference = Path("/bin/qjs"), Path("/bin/ng")
+ self.assertEqual(front_end._flags(engine, reference), ["--raw"])
+ self.assertEqual(front_end._flags(reference, reference), ["--script"])
+ self.assertEqual(front_end._flags(engine, None), ["--raw"])
+
+ def test_exactly_one_comparison_is_required(self):
+ self.assertEqual(front_end.main(["--binary", "/bin/qjs"]), 2)
+ self.assertEqual(
+ front_end.main(["--binary", "/bin/qjs", "--reference", "/a", "--base", "/b"]), 2
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()