mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/fix-semantic-parameter-resolution
696 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0389c4010e |
release(crates): oxc v0.146.0 (#25912)
### 🚀 Features - |
||
|
|
4a4a1fb2e7 |
release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES - |
||
|
|
b6f1458dec |
release(crates): oxc v0.144.0 (#25474)
### 💥 BREAKING CHANGES - |
||
|
|
22a433cbe4 |
test(allocator): add more tests for bump pointer calculation (#25365)
Add more tests for bump pointer calculation in `Arena::try_alloc_layout_fast_impl`. These tests close a gap in test coverage - test that allocation is rejected when subtracting the requested allocation size from current cursor pointer produces 0 exactly (null pointer). All pass. |
||
|
|
24ecee3da1 |
refactor(allocator): Arena::alloc_with write through a &mut MaybeUninit (#24923)
Alter `Arena::alloc_with` and `Arena::try_alloc_with` to write through `&mut MaybeUninit<T>` instead of raw `NonNull<T>` pointers. This is pure refactor at present. It reduces the unsafe code in these methods to a single line, and I feel makes it clearer. Additional motivation is to give LLVM `noalias` information (`&mut MaybeUninit<T>` has the exclusivity guarantee of `&mut` which `NonNull` doesn't). At present compiler produces same assembly before/after this change, so it doesn't seem to capitalize on this `noalias` info, however it might in the future. The change I don't think has any possibility of making codegen worse, and it has at least _potential_ to make it better. It may also be contributing to the gain in #25337. |
||
|
|
fe111eb112 |
docs(allocator): improve doc comments for Arena methods (#24922)
Docs-only change. Add links in doc comments. |
||
|
|
b3c1b308bc |
feat(allocator): add ArenaVec::push_mut (#25338)
Rust's stdlib recently added a new method `push_mut` to `Vec`. https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push_mut Add the same method to `ArenaVec`, and also a `push_fast_mut` method, which is the counterpart to `push_fast`. Before: ```rust fn push_and_do<'a>(vec: &mut ArenaVec<'a, Expression<'a>>, expr: Expression<'a>) { vec.push(expr); let expr_mut_ref: &mut Expression<'a> = vec.last_mut().unwrap(); do_something_with_expr(expr_mut_ref); } ``` After: ```rust fn push_and_do<'a>(vec: &mut ArenaVec<'a, Expression<'a>>, expr: Expression<'a>) { let expr_mut_ref: &mut Expression<'a> = vec.push_mut(expr); do_something_with_expr(expr_mut_ref); } ``` `push_mut` does the same thing as `push` followed by `last`, but removes the `unwrap`, which compiler is unable to optimize out. |
||
|
|
45a17c25d1 |
release(crates): oxc v0.143.0 (#25250)
### 💥 BREAKING CHANGES - |
||
|
|
3c8312e6dc |
feat(allocator): make Box Send + Sync when the type it contains is (#25242)
`ArenaBox` was previously `!Send` and `!Sync`, because it contains a `NonNull` pointer. `ArenaBox<T>` has the same semantics as a `&mut T`, so can be `Send` / `Sync` if `T` is. The also matches stdlib's `Box`. Note: `ArenaVec` is _not_ `Send` because it contains an `&Allocator` reference and `Allocator` is not `Sync`. But `ArenaBox` contains nothing but the pointer - it does not contain an `&Allocator` - so it doesn't share `ArenaVec`'s restriction. This is a further step towards making the whole AST `Sync`, so ASTs can be shared across threads. After this PR, the only remaining blocker is the `Cell`s in AST for semantic IDs (e.g. `Cell<NodeId>`). |
||
|
|
f0a41c879a |
feat(allocator)!: remove ArenaBox::dangling method (#25236)
Remove the `ArenaBox::dangling` method. This was a hack only required for checking enum discriminants in code generated by `inherit_variants!` macro. That code was removed in #23960, so `ArenaBox::dangling` can now be removed too. Any code which for some reason needs to create an invalid `ArenaBox` with a dangling pointer can do so with `ArenaBox::from_non_null(NonNull::dangling())`. |
||
|
|
dd3b56130c |
refactor(allocator): reduce unsafe code in CloneIn impl for Bitset (#25235)
`Layout::for_value` here does exactly the same as the previous unsafe code. |
||
|
|
ec7018ef4f |
refactor(allocator, str): use impl GetAllocator instead of generic params (#25177)
Pure refactor. Along the same lines as #25174 and other PRs in this stack. Replace generic params with `impl` for `GetAllocator` in methods of various types and traits across `oxc_allocator` and `oxc_str`, to make the code easier to read. |
||
|
|
fc702c1fa9 |
release(crates): oxc v0.142.0 (#24977)
### 💥 BREAKING CHANGES - |
||
|
|
14533a3dc1 |
release(crates): oxc v0.141.0 (#24714)
### 💥 BREAKING CHANGES - |
||
|
|
2e24c7e31c |
perf(allocator): speed up CloneIn for Vec and Box<[T]> (#24733)
Speed up `CloneIn` for `Vec<T>` and `Box<[T]>` by constructing source and destination slices and passing them to a separate function to clone between. This formulation tells LLVM that source and destination cannot overlap, which causes it to generate more efficient code. When `T` is a `Copy` or trivially-clonable type with no padding bytes (e.g. `Vec<'a, NodeId>`), the entire clone operation is squashed down to a single `memcpy` call - which is much faster than iterating through the source item by item. |
||
|
|
effd3c8cca |
feat(allocator): add Vec::from_raw_parts_in (#24732)
Add a public `Vec::from_raw_parts_in` method, which replicates the aleady existing API in `InnerVec`. |
||
|
|
d8abaa1fdb |
refactor(allocator): RawVec::from_raw_parts_in take a NonNull pointer (#24731)
`RawVec::from_raw_parts_in` and `InnerVec::from_raw_parts_in` take a `NonNull<T>` pointer, instead of a `*mut T` pointer. `RawVec` ultimately uses `NonNull` internally, and both callers `Vec::from_value_in` and `Vec::from_array_in` have a `NonNull` pointer to start with. So this change avoid converting `NonNull<T>` to `*mut T` only to convert it back to `NonNull<T>` with an unsafe call. |
||
|
|
10fd38eab4 |
test(allocator, data_structures, str): use implements! macro in tests (#24722)
Use `implements!` macro (introduced in #24721) in various tests, including adding some new ones. The most valuable change is replacing a `compile_fail` doctest which checks that `Arena` is not `Sync`. Using `assert!(implements!(Arena: !Sync));` is more robust. |
||
|
|
5293431203 |
feat(str)!: move IdentBuildHasher to oxc_str crate (#24717)
Move all `Ident`\-hashing code from `oxc_allocator` into `oxc_str`, to sit alongside `Ident`'s code. This feels like the natural place for this code, and reduces the public API surface - we no longer need to expose `Ident`'s internals (`ident_hash`). Now only `IdentHasher` and `IdentBuildHasher` are exposed. Only change to code is removing `pack_len_hash`, as it was only used in tests. |
||
|
|
f08b152b48 |
feat(allocator): implement FromIn for array to Vec conversion (#24620)
Implement `FromIn` to convert from `[T; N]` array to an `ArenaVec<'a, T>`. This is used in next PR #24621 to make AST builder methods take arrays. |
||
|
|
40c2f4307c |
fix(allocator): Vec::from_array_in do not allocate zero-length array (#24628)
`Vec::from_array_in` doesn't need to allocate when the array passed to it is zero-length. This wasn't unsound as `Allocator` accepts zero-sized allocations, but it was pointless. Allocating a zero-sized type still aligns the bump cursor to the type's alignment, which is an allocation of sorts - it's not zero-cost. Just delegate to `Vec::new_in` instead. |
||
|
|
2338c13f60 |
feat(track-memory-allocations): track heap deallocs, alloc bytes, and peak growth (#24619)
Adds three heap-side metrics to the allocation snapshots, using the metric machinery from #24587: - `sys deallocs` (exact count) — system deallocations per stage; read against `sys allocs` it shows whether a stage churns or retains heap. - `sys alloc bytes` — cumulative bytes requested from the system allocator per stage (allocation sizes plus reallocation growth). - `sys peak growth` — high-water mark of live heap during a stage relative to the live bytes at stage start, via live-bytes accounting in the `GlobalAlloc` wrapper. Captures transient heap a stage needs even when it frees it before finishing. Arena chunk operations are excluded from all `sys` metrics at the source: `oxc_allocator` marks each chunk allocation/deallocation immediately before calling the system allocator (task-only `track_allocations` feature, compiled out everywhere else), and the task's tracking allocator consumes the marker and skips the call. This replaces the previous global chunk counter + per-stage subtraction: a subtraction can correct counts but not a high-water mark, and chunk sizes are quantized and platform-dependent (whether an arena needs one more chunk depends on byte totals that vary across architectures with type layout — the #22621 mechanism). The first CI run demonstrated this concretely: kitchen-sink's minifier peak differed deterministically by 720 kB between aarch64 and x86_64 because the mangler's temporary arena crossed a chunk-growth boundary only on x86_64; and antd's formatter "peak" was 762 MB of main-arena chunk-doubling history rather than formatter behavior (now 13.8 MB of actual heap use). Cross-platform tolerances: the cumulative byte metrics still drift with the number of heap hashbrown tables allocated (their control-group width is architecture-dependent), so they use a relative tolerance (1%, 4 kB floor) via the committed-value comparison from #24565; `arena size` keeps its tight 1024-byte tolerance. Sample values for checker.ts (2.9 MB source): parser needs only 2.4 kB of transient heap (fully arena-based), semantic peaks at 3.9 MB, the minifier is the pipeline's largest heap consumer at 10.2 MB, and the formatter requests 5.9 MB total while peaking at 3 MB (heavy alloc/free churn). 🤖 Implemented with [Claude Code](https://claude.com/claude-code). |
||
|
|
c35d8abb24 |
perf(allocator): mark ReplaceWith panic path cold (#24515)
## Summary Mark ReplaceWith's write_dummy helper as cold. The helper is only reached while unwinding from a panic in a replacer closure. Telling the optimizer that this path is cold helps keep its allocator and dummy-construction code out of hot code layout and improves inlining decisions in unwind builds. |
||
|
|
ba65790029 |
perf(semantic, allocator): branchless clone_in for semantic IDs (#24564)
Follow-on after #24422. Make cloning semantic IDs (`NodeId`, `ScopeId` etc) use branchless arithmetic. * Cloning `NodeId` now just uses a single OR operation to take into account whether to preserve the existing ID (`clone_in_with_semantic_ids`) or substitute a dummy ID (`clone_in`). * Cloning `Cell<Option<ScopeId>>` is 2 instructions (NOT + AND) on x86_64, or just 1 on aarch64 (NOT-AND). https://godbolt.org/z/1KWdc5hnb The mechanism is a type `CloneInSemanticIds` which is used as `with_semantic_ids` instead of a `bool`: ```rs #[repr(u32)] pub enum CloneInSemanticIds { With = 0, Without = u32::MAX, } ``` This is 4 bytes, rather than `bool`'s 1 byte, but that makes no difference - either way it takes 1 register to pass between functions. The choice of discriminants is what enables the cheap arithmetic. This is even smaller in terms of binary size than the previous solution of marking `Cell::clone_in_impl` as `#[inline(never)]` (OR instruction is smaller than function call), and it's more performant as it loses the function call overhead. It also keeps `Cell`'s blanket implementation of `CloneIn` generalized. Between #24422 and this PR, merging `CloneIn` with/without semantic IDs into a single path has gained us a large binary size reduction, and lost us nothing - or may even have gained us a marginal perf improvement. No visible change on our benchmarks, as we don't use `CloneIn` much, but it may well be a measurable improvement for Rolldown, which clones whole ASTs. |
||
|
|
f297654a68 |
refactor(allocator, ast_tools): generate a single CloneIn traversal via a runtime flag (#24422)
## Summary
The `CloneIn` derive generated **two near-identical traversals over the whole AST** — `clone_in` (resets semantic ids to their default) and `clone_in_with_semantic_ids` (preserves them). Almost every node carries an id field, so the two bodies diverge nearly everywhere and the pair roughly doubled the clone codegen.
This threads *"preserve semantic ids?"* as a **runtime `bool`** through a **single** traversal:
- **`CloneIn::clone_in_impl(&self, with_semantic_ids: bool, allocator)` is the one required method.** `clone_in` / `clone_in_with_semantic_ids` are `#[inline(always)]` default wrappers that call it with `false` / `true`. Hand-written impls provide only `clone_in_impl`.
- The **derive emits a single `clone_in_impl` body per type** — no per-type `clone_in` / `clone_in_with_semantic_ids` wrappers.
- Semantic ids live in `Cell<NodeId>` / `Cell<Option<{Scope,Symbol,Reference}Id>>`. **`Cell<T: Copy + Default>::clone_in_impl` threads the flag**: preserve (`Cell::new(self.get())`) when `with_semantic_ids`, otherwise reset to the default (`Cell::new(T::default())` = `NodeId::DUMMY` / `None`). So the derive emits a plain `clone_in_impl(&self.node_id, …)` per id field — no per-field `if/else`.
- **`Cell::clone_in_impl` is `#[inline(never)]`** so the (for some consumers dead) preserve branch isn't inlined at every id-field site — see Results.
## Results
Measured `f736d87e77` → `db9f2ab41a` on this branch (release, stripped, fat-LTO):
| | before | after | Δ |
|---|--:|--:|--:|
| `oxc_ast` generated `CloneIn` (source lines) | 7,180 | 5,581 | **−1,599 (−22%)** |
| oxlint binary | 14,192,816 B | 14,126,656 B | **−66,160 B (−64.6 KiB)** |
| napi transform cdylib (`--features allocator`) | 3,633,312 B | 3,633,312 B | **0 (neutral)** |
The two binaries move differently because they clone differently:
- **oxlint / rolldown clone *with* semantic ids** (react compiler on `TSType`; whole-`Program` clones). They already carried chunks of both traversals; merging dedups them → **−64.6 KiB**.
- **napi transform / parse clone *without* ids** — `oxc_transformer` never calls `clone_in_with_semantic_ids`, so before this PR `--gc-sections` dropped that whole half. The merged traversal would force the preserve branch live at every id field (~+16 KiB); `#[inline(never)]` on the `Cell` leaf outlines it to one function per `Cell<T>`, keeping these binaries **flat**.
## Behavior & perf
Behavior is identical by construction — `clone_in` resets ids (`Cell::new(None)` / `Cell::new(DUMMY)`), `clone_in_with_semantic_ids` preserves them. The runtime flag is loop-invariant and well predicted; the `#[inline(never)]` leaf adds one outlined call per id-cell on the clone path (expected negligible — worth a benchmark spot-check). Generated files were produced by `just ast`.
---
🤖 AI-assisted (Claude Code), reviewed and measured by the author.
|
||
|
|
8e0ed2ebb9 |
release(crates): oxc v0.140.0 (#24453)
### 🚀 Features - |
||
|
|
d678236105 |
refactor(allocator): remove Layout::repeat polyfill (#24378)
Rust 1.95 stabilized [`Layout::repeat`](https://doc.rust-lang.org/std/alloc/struct.Layout.html#method.repeat) (MSRV bumped in #24359), which the `UnstableLayoutMethods` polyfill trait in `bumpalo_alloc.rs` was waiting on (per its TODO comment). It turns out the whole trait is deletable rather than just replaceable: - The only `.repeat()` call outside the trait itself (`vec2/raw_vec.rs`) is inside a commented-out block. - The live `Layout::array::<T>(n)` calls already resolve to the inherent std method (stable since 1.44), not the trait method. - `padding_needed_for` was only used by the trait's own `repeat` impl. Also drops the two `#![allow(unstable_name_collisions)]` attributes, which only existed because the trait methods collided with the then-unstable std inherent methods. Net: -59 lines, no behavior change. `cargo check/clippy/test -p oxc_allocator` all clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b88909312d |
refactor(allocator): use stabilized AtomicUsize::update (#24382)
Rust 1.95 stabilized [`Atomic*::update`](https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html#method.update) (MSRV bumped in #24359), the infallible sibling of `fetch_update` for closures that always produce a new value. Both `fetch_update` call sites (`oxc_allocator/src/tracking.rs` and `tasks/track_memory_allocations`) used an always-`Some` closure, so they gain nothing from the fallible API — switching to `update` drops the ignored `Result` (`let _ =`) and the "cannot be `Err`" comments. Same CAS-loop semantics; `saturating_add` behavior unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
3ff02349b6 |
perf(allocator): remove unwrap from ReplaceWith (#24365)
#24359 bumped our MSRV to 1.95.0. We can now use `Vec::push_mut`. Use it in `ReplaceWith` to remove an infallible `unwrap` which compiler previously couldn't prove was infallible. Previously it emitted a panicking branch, now it doesn't. This code is on a cold path, so perf impact is miniscule, but why not use the best API for the job? |
||
|
|
66e1a25c0e |
chore(all): fix clippy::manual_assert_eq (#24351)
|
||
|
|
460176ad05 |
fix(track-memory-allocations): exclude arena chunks from Sys allocs (#24292)
## Issue The `Allocations` CI job can fail on PRs with a 1-allocation `Sys allocs` difference that local `cargo allocs` cannot reproduce. #22621 fixed one instance of this in the transformer stage. #24112 and #24017 hit the same failure again in the minifier stage: `kitchen-sink.tsx` measures 2309 Sys allocs on macOS/arm64 but 2310 on Linux/x64, so whichever platform regenerates the snapshot last breaks CI for the other. ## Why The tracker counts every call to the system allocator. That includes the chunks bump arenas request. Whether an arena needs one more chunk depends on the total number of bytes in the arena, and byte totals are not the same on every target. For example, hashbrown tables use 16-byte control groups on x86_64 but 8-byte groups on aarch64. When an arena's content size sits close to a chunk boundary, one target crosses it and the other does not. In the observed failure, the minifier's mangler-phase semantic build creates a fresh `Scoping`. Its arena holds symbol names, resolved-reference vectors, and the per-scope `bindings` hash tables, which are slightly larger on x86_64. The traced Linux-only allocation happened under `Scoping::add_resolved_reference -> ArenaVec::push -> Arena::new_chunk`, when the push overflowed the current chunk and requested a new 720880-byte chunk. On aarch64 the same content fit in the existing chunk, so the count stayed one lower. Every other allocation class in the measurement grows on element counts, which are identical across platforms. Logging every allocation size during the minifier stage on macOS/arm64, Linux/arm64, and Linux/x64 showed the three sequences match one-to-one except for arena chunk requests. Any PR that shifts arena content near a chunk boundary can trip this again in any stage, so fixing one stage at a time does not close the class. ## Fix Count chunk allocations in `oxc_allocator` under the tracker-only `track_allocations` feature, and subtract them from the reported Sys allocs in the tracker. The counter is global rather than per-arena because short-lived arenas (like the one backing `Scoping`) are created and dropped inside the measured operation. Arena usage is still measured by the `Arena allocs` / `Arena reallocs` columns; only the platform-sensitive chunk-request count is excluded from `Sys allocs`. Parser and transformer rows are unchanged (the main arena is pre-warmed, and #22621 already removed transformer scoping growth). Semantic, minifier, and formatter rows drop by their per-stage chunk counts. ## Validation `cargo allocs` produces byte-identical snapshots on macOS/arm64, Linux/arm64 (Docker), and Linux/x64 (Docker), and is idempotent across reruns. `cargo test -p oxc_allocator` passes; clippy is clean with `track_allocations` both on and off, and the counter compiles out entirely without the feature. AI usage: AI assistance was used to investigate, implement, and validate this change. |
||
|
|
69b2dfc181 |
release(crates): oxc v0.139.0 (#24218)
### 🚀 Features - |
||
|
|
7db7a29aab |
feat(allocator): add ReplaceWith trait (#24012)
## The problem
A common pattern we have when mutating an AST (e.g. in transformer) is:
- Take a node from the AST.
- Wrap it in some other node, or extract a field from that node.
- Write the result back into the same place in AST the original came from.
Rust does not make this easy, due to aliasing rules. Currently we do something like this (example from arrow function transform):
```rs
let Statement::ExpressionStatement(expr_stmt) = match stmt else { unreachable!() };
let expr = expr_stmt.expression.take_in(ctx);
*stmt = Statement::new_return_statement(expr.span(), Some(expr), ctx);
```
The problem with this is that `take_in` writes a dummy `Expression` into the AST, in order to get the owned `Expression`. This is pointless, as the whole `Statement` that contains that dummy `Expression` is overwritten in the next line - the dummy is immediately disconnected from the AST. But that dummy node lives on forever in the arena, consuming memory.
## Solution in this PR
Introduce `ReplaceWith` trait which replaces the node in place, without writing a dummy:
```rs
stmt.replace_with(|stmt| {
let Statement::ExpressionStatement(expr_stmt) = stmt else { unreachable!() };
let expr = expr_stmt.unbox().expression;
Statement::new_return_statement(expr.span(), Some(expr), ctx)
});
```
The closure receives an owned copy of the `Statement`, and can do whatever it needs to with it. The closure has to return a new `Statement` which is written to the "hole" the original came from.
This PR only adds the trait. Later PRs in this stack implement the trait on AST types, and then use it in various crates.
## Implementation
The difficulty is in making this sound even if the closure panics, and `std::panic::catch_unwind` is in use, allowing the old value to be observed.
A naive implementation would allow UB as follows:
```rs
use std::panic::{AssertUnwindSafe, catch_unwind};
use oxc_allocator::{Allocator, ArenaBox, ReplaceWith};
let allocator = Allocator::new();
let allocator = &allocator;
let mut boxed: ArenaBox<u32> = ArenaBox::new_in(1, &allocator);
let mut copy: Option<ArenaBox<u32>> = None;
let _ = catch_unwind(AssertUnwindSafe(|| {
boxed.replace_with(|old| {
// Move the copy out of the closure
copy = Some(old);
panic!("Unwind before write-back");
});
}));
let mut copy: ArenaBox<u32> = copy.unwrap();
let boxed_mut: &mut u32 = boxed.as_mut();
let copy_mut: &mut u32 = copy.as_mut();
// `boxed_mut` and `copy_mut` both point to same `u32`.
// We have 2 `&mut u32`s pointing to same place. Aliasing violation!
*boxed_mut = 2;
*copy_mut = 3;
```
The solution is to make it possible to implement `ReplaceWith` only on types that also implement `Dummy`.
If the closure panics, a guard is triggered which writes a dummy into the original slot which the type was moved out of. This happens before any other code can observe the value in the slot.
To keep the API ergonomic, we don't want to have to pass in an `&Allocator` to `replace_with`. But the dummy has to be allocated *somewhere*. So we use a global store of `Allocator`s, stored in a `static`, which live for the entire life of the process. When a panic occurs, a new `Allocator` is created, and the dummy is allocated in it.
This is a memory leak - the `Allocator` containing the dummy will never be freed until the process exits.
However, in practice this is fine, because:
#### 1. It shouldn't happen
The guard only triggers, and these leaked `Allocator`s are only created, if the closure panics - which it never should. If the closure *does* panic, that's a bug, and we'll aim to fix it swiftly.
#### 2. Panic usually exits process
It's unusual to use `catch_unwind`. Generally a panic is not caught and it causes the process to exit. So an `Allocator` can be leaked, but only for the few milliseconds before the process exits, and it's freed again.
#### 3. `panic = "abort"`
Oxc's apps (Oxlint, Oxfmt) and NAPI packages (`oxc-parser` etc) are compiled with `panic = "abort"`.
With `panic = "abort"`, panics are converted to process abort. In that case, the compiler sees that the guard can never fire. It removes the guard and all the code which creates `Allocator`s and allocates dummies into them. So all these mechanics are completely zero cost, and no leak ever occurs.
|
||
|
|
30d17f5864 |
docs(allocator): clarify docs for TakeIn::take_in_box (#24093)
Just clarify doc comment, explaining what the method does. |
||
|
|
8bf69a3076 |
release(crates): oxc v0.138.0 (#23934)
### 💥 BREAKING CHANGES - |
||
|
|
0b3021f884 |
fix(allocator): remove Vec::from_box_in (#23873)
Remove this method, which was added in #23717. The method is sound, and could be useful in some cases, but I realised it's not forward-compatible with planned future changes to allocator. So I think best not to introduce it. This would be a breaking change, but it was only added a few days ago, so hadn't got into a release yet. |
||
|
|
163ed51285 |
refactor(allocator): Box::new_in take &GetAllocator (#23756)
Part of #23043. Same as #23755, but for `Box`. `Box::new_in` previously took an `&'a Allocator`. Instead make it take an `&A where A: GetAllocator<'a>`. This makes it more flexible - you can pass a reference to any type which implements `GetAllocator` e.g. `ParserImpl`, `TraverseCtx` etc. ```rust // Before (`ctx` here is `&mut TraverseCtx`) let boxed = Box::new_in(value, ctx.ast.allocator); // After let boxed = Box::new_in(value, ctx); ``` The main motivation is to make it a workable replacement for `AstBuilder::alloc`, which will be removed in the new `AstBuilder`: ```rust // Before (`ctx` here is `&mut TraverseCtx`) let boxed = ctx.ast.alloc(value); // After let boxed = ArenaBox::new_in(value, ctx); // Instead of let boxed = ArenaBox::new_in(value, ctx.ast.allocator); ``` This has the same advantages and disadvantages as `Vec` methods - see #23755. |
||
|
|
5201522945 |
refactor(allocator): Vec construction methods take &GetAllocator (#23755)
Part of #23043. `Vec::new_in`, `Vec::with_capacity_in`, `Vec::from_array_in` etc previously took an `&'a Allocator`. Instead make them take an `&A where A: GetAllocator<'a>`. This makes them more flexible - you can pass a reference to any type which implements `GetAllocator` e.g. `ParserImpl`, `TraverseCtx` etc. ```rust // Before (`ctx` here is `&mut TraverseCtx`) let vec = ArenaVec::new_in(ctx.ast.allocator); // After let vec = ArenaVec::new_in(ctx); ``` The main motivation is to make them workable replacements for what are currently methods on `AstBuilder`, but will be removed in the new `AstBuilder`: ```rust // Before (`ctx` here is `&mut TraverseCtx`) let vec = ctx.ast.vec(); // After let vec = ArenaVec::new_in(ctx); // Instead of let vec = ArenaVec::new_in(ctx.ast.allocator); ``` This is still longer code than the original, which is not ideal, but it's as short as can make them. The main downside of this change is that when calling these methods with an `&Allocator`, you have to double reference: ```rust let allocator: &'a Allocator = get_allocator_somehow(); // Passing `&&Allocator` let vec = ArenaVec::new_in(&allocator); ``` This has no perf impact - these methods are inlined, and compiler collapses the double-reference like it wasn't there, but it's just ugly. Unfortunately, there's no way to implement `GetAllocator` so it'll accept both `&Allocator` and `&mut TraverseCtx`, and the latter is the much more common use case. |
||
|
|
8c6481bbf9 |
refactor(allocator): export ArenaBox and ArenaVec aliases (#23746)
Pure refactor.
It's a common pattern in repo to import `Vec` and `Box` etc from `oxc_allocator` and alias them to `ArenaBox` / `ArenaVec`.
```rust
use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec};
```
Make this pattern less verbose by re-exporting `Box` and `Vec` as these aliases from `oxc_allocator`.
Ditto other arena types: `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`.
Use those aliases in all other crates to shorten code.
```rust
use oxc_allocator::{ArenaBox, ArenaVec};
```
|
||
|
|
fcbf993df3 |
feat(allocator): add Vec::from_value_in method (#23718)
Add `Vec::from_value_in` which creates a `Vec` containing only a single value. This performs the same role as `AstBuilder::vec1`. ```rust // These 2 are equivalent let v = self.ast.vec1(value); let v = Vec::from_value_in(value, self.ast.allocator); ``` Internally uses `Vec::from_box_in`, introduced in #23717. |
||
|
|
989ddb7fc4 |
feat(allocator): add Vec::from_box_in method (#23717)
Add a method `Vec::from_box_in` which converts a `Box<T>` to a `Vec<T>` containing only the single value, without reallocation or copying. |
||
|
|
9d1aa7fcb6 |
feat(allocator): improve PartialEq for Vec (#23716)
Improve `PartialEq` implementations on `oxc_allocator::Vec`, bringing it up to parity with `std::vec::Vec`. Previously code like this would fail to compile due to missing implementations for comparing `Vec`s to slices: ```rust let v = Vec::from_array_in([1, 2, 3], &allocator); assert_eq!(v, [1, 2, 3]); ``` |
||
|
|
f129b013b3 |
refactor(allocator): rename lifetime in Box impl (#23693)
Pure refactor. Rename lifetime in `Box` impl, for consistency with rest of the code. |
||
|
|
7231d5542f |
fix(allocator): fix unsound lifetime extension in Box::new_in (#23685)
Fix unsoundness in `Box::new_in`.
`Box::new_in` had this signature:
```rust
impl<T> Box<'_, T> {
pub fn new_in(value: T, allocator: &Allocator) -> Self {
Self(NonNull::from(allocator.alloc(value)), PhantomData)
}
}
```
The returned `Box<'alloc, T>`'s lifetime `'alloc` and the `&Allocator` borrow were two independent, unconstrained lifetimes. Nothing tied them together, so the caller was free to choose any `'alloc` - including one that outlives the allocator. In effect you could mint a `Box<'static, T>` from a temporary `Allocator`:
```rust
let boxed = {
let allocator = Allocator::default();
Box::new_in(5, &allocator) // returns Box<'_, i32> with a lifetime unrelated to `allocator`
}; // `allocator` dropped here, freeing the backing memory
assert_eq!(*boxed, 5); // use after free
```
Fix by tying the 2 lifetimes together.
```rust
impl<'alloc, T> Box<'alloc, T> {
pub fn new_in(value: T, allocator: &'alloc Allocator) -> Self { ... }
}
```
This bug crept in in #2943 and has gone unnoticed since - largely because we rarely use `Box::new_in` directly, and `AstBuilder::alloc` was correctly providing a tighter lifetime bound.
|
||
|
|
36009ddf68 |
feat(allocator)!: GetAllocator::allocator take &self (#23676)
Make `GetAllocator::allocator` method take `&self`, instead of `self`. Alter `TakeIn::take_in` accordingly - now takes `&A` where`A:GetAllocator` (previously took owned `A`). This requires changing every `take_in` / `take_in_box` call site. However, this change also means that `GetAllocator` can be implemented on all types which hold an `&Allocator` e.g. `TraverseCtx`, which shortens the code everywhere. - Before: `expr.take_in(ctx.ast)` - After: `expr.take_in(ctx)` Change all those call sites to this shorter version. This matches the pattern that new `AstBuilder` is going to introduce (#23043), and clears the way for that change. |
||
|
|
bd74f9d35c |
feat(allocator)!: rename AllocatorAccessor trait to GetAllocator (#23675)
Pure refactor. Rename `AllocatorAccessor` trait to `GetAllocator`. Rust's naming convention is that trait names are verbs, describing what you can _do_ with the trait (e.g. `Clone`, not `Cloneable`). `GetAllocator` follows this convention, and also follows the naming convention we have for other traits e.g. `GetSpan`. |
||
|
|
6d56d57535 |
release(crates): oxc v0.137.0 (#23591)
### 💥 BREAKING CHANGES - |
||
|
|
7a76cd3dc3 |
feat(estree)!: make whether to include TS fields a runtime option (#23574)
Previously, whether to serialize AST to ESTree JSON with/without TS fields was a compile-time option - via 2 different configs `CompactTSSerializer` and `CompactJSSerializer` (and their pretty-printing and with-fixes counterparts). Turn this into a runtime option instead - This make serialization a little slower (more branches in either mode, and dead code in JS-only mode) but reduces the size of `oxc-parser` binary as only one `ESTreeSerializer` is generated, instead of 2 (JS-only, and with-TS). |
||
|
|
bcb3894d76 |
perf(minifier): incremental scoping refresh, delete LiveUsageCollector (#23197)
> Developed with AI assistance (Claude Code); reviewed, tested, and benchmarked by the contributor. ## Summary Deletes the per-pass `LiveUsageCollector` walk over the entire live program. The typed mutation helpers (previous PR in this stack) now walk only the dropped/replaced subtrees in place, accumulating dead `ReferenceId`s into a per-pass `PassDirty` arena `BitSet` that the `Compressor` driver consumes in one batch (after `Normalize` and after each peephole pass) via the new `Scoping::retain_resolved_references_excluding`. Direct-eval flag refresh runs only when an `eval(...)` call was actually dropped. Cross-crate support riding in this PR: `Scoping::retain_resolved_references_excluding` (oxc_semantic) and `BitSet::capacity()` / `is_empty()` / `contains()` (oxc_allocator). ## Breaking Removes `oxc_semantic::Scoping::retain_resolved_references` — its only consumer was `LiveUsageCollector`, deleted here. No other in-repo or known downstream callers. ## Benchmarks (CodSpeed, this PR vs stack base) | Benchmark | Base → Head | Efficiency | |---|---|---| | `minifier[kitchen-sink.tsx]` | 63.6 ms → 52.5 ms | **+21.2%** | | `pipeline[kitchen-sink.tsx]` | 122.3 ms → 110.7 ms | +10.5% | | `minifier[App.tsx]` | 12.5 ms → 12.1 ms | +3.3% | | everything else | — | unchanged | > **Why earlier runs showed ~2× on kitchen-sink:** the initial cut of this branch had a reference-leak bug — dropped subtrees weren't always marked dead, leaked references made symbols look used, and the minifier silently skipped most of kitchen-sink's DCE, producing output **22% larger than main** (invisible to minsize, which doesn't include that file). Those runs measured a minifier doing less work, not a faster minifier. The leak is fixed (see below); the numbers above are measured at byte-identical output to main. Memory: kitchen-sink.tsx arena allocs 100230 → 90553, sys allocs 2917 → 2639; all other tracked files at or slightly below base. ## Relation to #22736 This re-cuts the approach prototyped and discussed in #22736 (kept open during development as the design record), with three deviations from the POC: 1. **Mark-only `DropDiff`** — the POC's "Resurrect" mode existed for one call site (`substitute_is_object_and_not_null`) that reused `ReferenceId`s in replacement values. That site now mints fresh references (same symbol/resolution, new id), so the resurrect walk and its cross-call aliasing invariant are gone entirely. 2. **`mutated: bool` instead of a `mutations: u64` snapshot counter** — after this PR the signal's only reader is the fixed-point loop, so a private bool with read-and-reset `take_mutated()` suffices. 3. **Bounds-checked mark path** — references minted mid-pass and dropped in the same pass are skipped (treated live → conservative output) instead of the POC's debug-assert + potential release panic. Safety net: `debug_assert_no_over_prune` walks the live program in debug builds after each dirty pass, so the whole test + conformance corpus doubles as an over-prune detector at zero release cost. ## Reference-leak fix + simplification follow-ups The initial cut leaked references at three classes of drop sites. The leak direction is safe (stale extra references block optimizations, never corrupt output), but it cost real output size on `void ident`-heavy inputs — kitchen-sink.tsx minified 22% larger than main: 1. **`Normalize` raw drops** (`void x` → `void 0`, `drop_console`) never reached `PassDirty`. `Normalize` now records drops through the same typed helpers, and the driver flushes once before the fixed-point loop so pass 1 already observes pruned reference counts (no extra peephole pass). 2. **`remove_unused_template_literal`** silently drained elements that `remove_unused_expression` collapsed to nothing, without a drop walk. 3. **Dropped variable declarators** walked only `init`; references in the binding's TS type annotation (e.g. computed keys in a type literal) leaked. New `drop_variable_declarator` helper walks the whole declarator. Each class has a leak-regression test; kitchen-sink.tsx output is verified **byte-identical to main**. Two simplification passes followed: flush hoisted into the `Compressor` driver (no cross-traversal coupling), `DropDiff` reduced to a thin `Visit` over `&mut PassDirty` (no scoping lookup on the mark hot path; marks for unresolved refs are inert), `BitSet::clear()` reuse instead of a per-pass arena realloc, a shared `as_direct_eval_call` predicate for producer and consumer, plain `clone_in` (id-less) at the one site that cloned semantic ids, and removal of dead `drop_console` repair code. ## Verification - `cargo test -p oxc_minifier -p oxc_mangler -p oxc_semantic -p oxc_allocator` — all pass, incl. the new leak-regression tests - `just minsize` — **bit-identical output**; kitchen-sink.tsx (not in minsize) separately verified byte-identical to main - `cargo coverage -- minifier` — test262 44825/44829, babel 1785/1785, snapshots unchanged - clippy (all features/targets), fmt clean ## Test alignment in oxc_transformer_plugins The `replace_global_defines` integration-test helper previously reused RGD's returned scoping directly for DCE — a path production does not take: `crates/oxc/src/compiler.rs` marks scoping dirty whenever RGD changed the AST (`scoping_dirty |= ret.changed`) and rebuilds before compress/DCE, and rolldown minifies freshly-parsed chunks. With the full-walk `LiveUsageCollector` gone, that test-only path surfaced stale ambient-`declare` references. The helper now mirrors the production pipeline (rebuild when `ret.changed`, reuse otherwise). No `ReplaceGlobalDefines` source changes. Two `declare`-define contract tests added. ## Known trade-off: defines-heavy DCE pays O(dropped) drop-walks Profiling `pipeline[react.development.js]` (with `process.env.NODE_ENV` defines) shows the worst case of walk-what-you-drop: DCE drops one `if` subtree covering ~90% of the program's nodes, so the drop-walk approaches a full-program traversal where the old per-pass live walk only covered the small remainder (−3% CodSpeed / +7% local wall on that bench; attribution: ~72% drop-walks, 0% double-marking, ~0% bitset cost). A bounded-walk + live-walk-fallback mitigation was prototyped and validated (recovers to +1.4%, all other wins preserved), but we decided the added mechanism wasn't worth the maintenance cost for a one-bench regression — the same file's plain minification *improves* 12%, and kitchen-sink-class inputs (the load-bearing case) improve ~2×. Revisit if real-world defines-heavy workloads surface it. |
||
|
|
827248a7fb |
release(crates): oxc v0.136.0 (#23443)
### 💥 BREAKING CHANGES - |