Avoid emitting consecutive source map entries when they carry the
same source range. The bytecode offset for the previous entry remains
valid for later PCs because source lookup now uses the largest source
map entry whose offset is not greater than the program counter.
This keeps stack traces stable while allowing statement-sized runs of
bytecode to share one source map entry.
Add Rust and C++ integration points for cloning lazy function compile
payloads, compiling them to GC-free bytecode off the main thread, and
materializing the result later on the main thread.
The cloned payload lets background compilation race with lazy
main-thread compilation without sharing AST ownership between threads.
Compiled function artifacts recursively include nested functions, so
materialization can discard the corresponding AST subtree.
The std default RandomState (SipHash) was using ~9 percentage points
of CPU on hash_one and write across the parse hot path, with the
string interner adding another ~3 pp on top. The cost was spread
across the interner, the scope collector's IndexMap<Utf16String, _>,
and several parser-side HashSet<Utf16String> declarations.
Use foldhash::quality::RandomState for the parser, scope collector,
and string interner via a new fast_hash module. Quality keeps
HashDoS resistance (keys are lexer tokens, attacker-controlled in a
browser context) while shedding SipHash's per-byte cost, and on this
workload it benchmarks slightly faster than foldhash::fast.
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.
Identifier::name was SharedUtf16String (Rc<Utf16String>), so equality
checks against literals walked the slice and the Rc made the AST
!Send.
Replace it with a StringId (u32 index) backed by a StringInterner on
AstArena. Repeated names dedupe to the same id, so name comparisons
collapse to u32 == u32. The lexer's short/recent identifier caches
and the shared_identifier_value field on Token go away; the interner
already deduplicates everything.
Methods that previously took &mut IdentifierArena now also take
&StringInterner so they can resolve names from StringId during
analyze. Codegen helpers in bytecode/codegen.rs uniformly take
&AstArena. Generator gains intern_identifier_id, intern_property_key_id,
and intern_string_id helpers.
scope_collector now reaches Identifier through &mut IdentifierArena
indexing instead of through Rc<Identifier>'s shared reference, so the
Cell<> wrappers on local_type, local_index, is_global,
is_inside_scope_with_eval, and declaration_kind no longer earn their
keep.
Replace each Cell<T> with a plain T. The borrow checker now enforces
the existing "only scope_collector mutates these post-parse"
invariant. Shrinks Identifier and removes a layer of indirection on
hot-path field reads in codegen and ast_dump.
Replace per-AST-node Rc<Identifier> with a Copy IdentifierId index
into a Vec<Identifier> arena, plumbed through Parser, scope_collector,
codegen, ast_dump, and the FFI. The arena lives on the parser during
parse, ships out via Arc<AstArena> on ParsedProgram, and is shared by
each child Generator and FunctionPayload through Arc clones.
Eliminates the per-occurrence Rc::new in the parser: every identifier
reference, parameter binding, function name, class name, and
binding-pattern target lands in the arena's Vec instead of getting its
own malloc plus Rc control block. Identifier field reads in codegen
become direct array indexing.
Identifier still carries Cell<>-wrapped scope-analysis state, so
AstArena is not yet Send + Sync; the existing unsafe-impl-Send wrapper
on ParsedProgram covers cross-thread handoff. Removing the Cells is
the next step.
The regex literal handle is shared between AST clones (e.g. class
field initializers reuse the same compiled regex), so shared ownership
has to stay. Switch from Rc to Arc and from Cell<*mut c_void> to
AtomicPtr<c_void> so the regex can travel with a function payload to
a worker thread without UB on the non-atomic Rc refcount.
FunctionTable::extract_reachable() used to rediscover a function's
nested functions by walking the full body and parameter list during
bytecode generation. This is hot during page loading because creating
every lazy SFD pays for an extra structural AST traversal.
Record each parser-created function's direct child function ids while
parsing instead. Extraction can then recursively move that known
subtree without scanning the enclosing function again.
Keep the old structural scan for codegen-synthesized wrappers, such as
class field initializers, where no parser function context exists.
This preserves the sparse FunctionTable storage while making the common
extraction path proportional to the nested function count.