Compare commits

...

12 Commits

Author SHA1 Message Date
Claude Bot
e6b70345bc Fix use-after-poison in StronglyConnectedComponents
The getNeighbors function returns a slice to an internal buffer that
gets cleared on subsequent calls. When recursive calls happen in
strongConnect, the buffer is invalidated while the outer loop is still
iterating over it, causing AddressSanitizer to detect use-after-poison.

Fixed by creating a copy of the neighbors slice before iterating.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 05:23:37 +00:00
autofix-ci[bot]
93f8c3381d [autofix.ci] apply automated fixes 2025-09-05 04:32:58 +00:00
Claude Bot
3ae1e5a6e3 perf: optimize memory usage in TLA SCC implementation
Reduced memory allocation by eliminating per-file ArrayLists. Instead of
creating N ArrayLists (one per file), we now use a single reusable buffer
that's cleared and reused for each getNeighbors() call.

This significantly reduces memory usage for large projects with many files,
as most files have few dependencies but we were allocating an ArrayList
for every single file regardless.

Changes:
- Replace per-file adjacency list with a single reusable buffer
- Use existing import_records directly instead of duplicating data
- Pass EdgeIterator by pointer to allow mutable state (the buffer)

Memory improvement: O(N) ArrayLists → O(1) ArrayList
All existing tests continue to pass.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 04:31:15 +00:00
autofix-ci[bot]
00a1bd5fd5 [autofix.ci] apply automated fixes 2025-09-05 03:35:39 +00:00
Claude Bot
8bd2e79b57 perf: replace O(n²) while(changed) loop with O(n) Tarjan's SCC algorithm for TLA propagation
The previous implementation used a while(changed) loop that kept iterating
until no changes were made when propagating async flags through module
dependencies. This had O(n²) or worse time complexity in pathological cases.

This commit replaces it with Tarjan's strongly connected components algorithm
which runs in O(V + E) time, providing significant performance improvements
for large dependency graphs with cycles.

Changes:
- Add StronglyConnectedComponents.zig with Tarjan's algorithm implementation
- Replace while(changed) loop with SCC-based topological sorting
- Pre-compute adjacency list for better performance
- Process SCCs in topological order for correct async flag propagation

All existing TLA tests continue to pass.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 03:33:41 +00:00
Dylan Conway
cf448b4b51 Update src/bundler/LinkerContext.zig
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-09-04 18:36:33 -07:00
Dylan Conway
a20e7091cb Merge branch 'main' into dylan/fix-bundle-tla 2025-09-05 01:10:03 +00:00
Dylan Conway
729bfa3ef8 add more test 2025-09-04 17:45:46 -07:00
Dylan Conway
d93fe4077d break out of loop earlier 2025-09-04 16:45:30 -07:00
Dylan Conway
020327f650 update 2025-08-29 12:31:10 -07:00
Dylan Conway
2f5a7eee51 update 2025-08-28 19:40:52 -07:00
Dylan Conway
b183cc60c7 update 2025-08-28 19:40:40 -07:00
3 changed files with 306 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ pub const LinkerContext = struct {
pub const OutputFileListBuilder = @import("./linker_context/OutputFileListBuilder.zig");
pub const StaticRouteVisitor = @import("./linker_context/StaticRouteVisitor.zig");
pub const StronglyConnectedComponents = @import("./linker_context/StronglyConnectedComponents.zig").StronglyConnectedComponents;
parse_graph: *Graph = undefined,
graph: LinkerGraph = undefined,
@@ -390,6 +391,68 @@ pub const LinkerContext = struct {
}
}
// Second pass: propagate async flag through cycles using strongly connected components
// This is more efficient than the while(changed) approach: O(V + E) instead of O(V²) or worse
{
const import_records_list: []ImportRecord.List = this.graph.ast.items(.import_records);
const flags: []JSMeta.Flags = this.graph.meta.items(.flags);
const css_asts: []?*bun.css.BundlerStyleSheet = this.graph.ast.items(.css);
// Memory-efficient approach: use existing import_records directly
// instead of creating a separate adjacency list for every file
const EdgeIterator = struct {
import_records_list: []ImportRecord.List,
flags: []JSMeta.Flags,
css_asts: []?*bun.css.BundlerStyleSheet,
// Single reusable buffer for neighbors to avoid per-file allocations
neighbors_buf: *std.ArrayList(u32),
pub fn getNeighbors(self: *@This(), node_idx: u32) []const u32 {
self.neighbors_buf.clearRetainingCapacity();
// Skip runtime
if (node_idx == Index.runtime.get()) return &.{};
// Skip if not a JavaScript AST
if (node_idx >= self.import_records_list.len) return &.{};
// Skip CSS files
if (self.css_asts[node_idx] != null) return &.{};
const import_records = self.import_records_list[node_idx].slice();
for (import_records) |*record| {
const dep_idx = record.source_index;
if (Index.isInvalid(dep_idx) or dep_idx.get() >= self.flags.len or record.kind != .stmt) {
continue;
}
self.neighbors_buf.append(dep_idx.get()) catch continue;
}
return self.neighbors_buf.items;
}
};
var neighbors_buffer = std.ArrayList(u32).init(this.allocator());
defer neighbors_buffer.deinit();
var edge_iterator = EdgeIterator{
.import_records_list = import_records_list,
.flags = flags,
.css_asts = css_asts,
.neighbors_buf = &neighbors_buffer,
};
var scc = try StronglyConnectedComponents.init(this.allocator(), this.graph.files.len);
defer scc.deinit();
// Find all strongly connected components
try scc.findSCCs(EdgeIterator, &edge_iterator, this.graph.files.len);
// Propagate async flags in topological order
scc.propagateAsyncInTopologicalOrder(JSMeta.Flags, flags, EdgeIterator, &edge_iterator);
}
try this.scanImportsAndExports();
// Stop now if there were errors

View File

@@ -0,0 +1,203 @@
const std = @import("std");
/// Tarjan's strongly connected components algorithm for finding cycles in the dependency graph.
/// This is more efficient than the while(changed) loop approach which has O(n²) or worse complexity.
pub const StronglyConnectedComponents = struct {
allocator: std.mem.Allocator,
// Node information for Tarjan's algorithm
nodes: []Node,
stack: std.ArrayList(u32),
index_counter: u32,
sccs: std.ArrayList([]u32),
pub const Node = struct {
index: u32 = std.math.maxInt(u32),
lowlink: u32 = std.math.maxInt(u32),
on_stack: bool = false,
};
pub fn init(allocator: std.mem.Allocator, node_count: usize) !StronglyConnectedComponents {
return .{
.allocator = allocator,
.nodes = try allocator.alloc(Node, node_count),
.stack = std.ArrayList(u32).init(allocator),
.index_counter = 0,
.sccs = std.ArrayList([]u32).init(allocator),
};
}
pub fn deinit(self: *StronglyConnectedComponents) void {
self.allocator.free(self.nodes);
self.stack.deinit();
for (self.sccs.items) |scc| {
self.allocator.free(scc);
}
self.sccs.deinit();
}
/// Find all strongly connected components using Tarjan's algorithm
pub fn findSCCs(
self: *StronglyConnectedComponents,
comptime EdgeIterator: type,
edges: *EdgeIterator,
node_count: usize,
) !void {
// Initialize all nodes
for (0..node_count) |i| {
self.nodes[i] = .{};
}
// Visit each unvisited node
for (0..node_count) |v| {
if (self.nodes[v].index == std.math.maxInt(u32)) {
try self.strongConnect(EdgeIterator, edges, @intCast(v));
}
}
}
fn strongConnect(
self: *StronglyConnectedComponents,
comptime EdgeIterator: type,
edges: *EdgeIterator,
v: u32,
) !void {
// Set the depth index for v to the smallest unused index
self.nodes[v].index = self.index_counter;
self.nodes[v].lowlink = self.index_counter;
self.index_counter += 1;
try self.stack.append(v);
self.nodes[v].on_stack = true;
// Consider successors of v
// Create a copy of neighbors to avoid use-after-poison when the buffer is reused in recursive calls
const neighbors_slice = edges.getNeighbors(v);
const neighbors = try self.allocator.alloc(u32, neighbors_slice.len);
defer self.allocator.free(neighbors);
@memcpy(neighbors, neighbors_slice);
for (neighbors) |w| {
if (self.nodes[w].index == std.math.maxInt(u32)) {
// Successor w has not yet been visited; recurse on it
try self.strongConnect(EdgeIterator, edges, w);
self.nodes[v].lowlink = @min(self.nodes[v].lowlink, self.nodes[w].lowlink);
} else if (self.nodes[w].on_stack) {
// Successor w is in stack S and hence in the current SCC
self.nodes[v].lowlink = @min(self.nodes[v].lowlink, self.nodes[w].index);
}
}
// If v is a root node, pop the stack and print an SCC
if (self.nodes[v].lowlink == self.nodes[v].index) {
var scc = std.ArrayList(u32).init(self.allocator);
// Pop nodes from stack until we reach v
while (self.stack.items.len > 0) {
const w = self.stack.pop() orelse break;
self.nodes[w].on_stack = false;
try scc.append(w);
if (w == v) break;
}
// Store the SCC (only if it has more than 1 element or is a self-loop)
if (scc.items.len > 1 or self.hasSelfLoop(EdgeIterator, edges, v)) {
try self.sccs.append(try scc.toOwnedSlice());
} else {
scc.deinit();
}
}
}
fn hasSelfLoop(self: *StronglyConnectedComponents, comptime EdgeIteratorType: type, edges: *EdgeIteratorType, node: u32) bool {
_ = self;
const neighbors = edges.getNeighbors(node);
for (neighbors) |neighbor| {
if (neighbor == node) return true;
}
return false;
}
/// Process SCCs in topological order for async propagation
pub fn propagateAsyncInTopologicalOrder(
self: *StronglyConnectedComponents,
comptime FlagType: type,
flags: []FlagType,
comptime EdgeIterator: type,
edges: *EdgeIterator,
) void {
// Process SCCs in reverse order (topological order)
var i: usize = self.sccs.items.len;
while (i > 0) {
i -= 1;
const scc = self.sccs.items[i];
// Check if any node in the SCC has async or any dependency has async
var has_async = false;
for (scc) |node_idx| {
if (flags[node_idx].is_async_or_has_async_dependency) {
has_async = true;
break;
}
// Check dependencies outside the SCC
const neighbors = edges.getNeighbors(node_idx);
for (neighbors) |neighbor| {
// Skip nodes within the same SCC
var in_same_scc = false;
for (scc) |scc_node| {
if (scc_node == neighbor) {
in_same_scc = true;
break;
}
}
if (in_same_scc) continue;
if (flags[neighbor].is_async_or_has_async_dependency) {
has_async = true;
break;
}
}
if (has_async) break;
}
// If any node has async, mark all nodes in SCC as async
if (has_async) {
for (scc) |node_idx| {
flags[node_idx].is_async_or_has_async_dependency = true;
}
}
}
// Final pass: propagate async from dependencies for non-SCC nodes
// Process in reverse topological order (from leaves to roots)
var node_idx: usize = flags.len;
while (node_idx > 0) {
node_idx -= 1;
// Skip if already async
if (flags[node_idx].is_async_or_has_async_dependency) continue;
// Check if this node is in any SCC (already processed)
var in_scc = false;
for (self.sccs.items) |scc| {
for (scc) |scc_node| {
if (scc_node == node_idx) {
in_scc = true;
break;
}
}
if (in_scc) break;
}
if (in_scc) continue;
// Check dependencies
const neighbors = edges.getNeighbors(@intCast(node_idx));
for (neighbors) |neighbor| {
if (flags[neighbor].is_async_or_has_async_dependency) {
flags[node_idx].is_async_or_has_async_dependency = true;
break;
}
}
}
}
};

View File

@@ -1604,6 +1604,46 @@ describe("bundler", () => {
stdout: "hi\n",
},
});
itBundled("default/CircularTLADependency2", {
files: {
"/entry.ts": /* ts */ `
await import("./b.ts");
`,
"/b.ts": /* ts */ `
import { c } from "./c.ts";
console.log(c);
export const b = "b";
`,
"/c.ts": /* ts */ `
import { d } from "./d.ts";
console.log(d);
export const c = "c";
`,
"/d.ts": /* ts */ `
const { e } = await import("./e.ts");
console.log(e);
import { f } from "./f.ts";
console.log(f);
export const d = "d";
`,
"/e.ts": /* ts */ `
export const e = "e";
`,
"/f.ts": /* ts */ `
import { g } from "./g.ts";
console.log(g);
export const f = "f";
`,
"/g.ts": /* ts */ `
import { c } from "./c.ts";
console.log(c);
export const g = "g";
`,
},
run: {
stdout: "c\ng\ne\nf\nd\nc\n",
},
});
itBundled("default/ThisOutsideFunctionRenamedToExports", {
files: {
"/entry.js": /* js */ `