Skip to content

Commit 2324c4f

Browse files
Compiler: Fix register exhaustion by freeing argument registers after calls
Function calls and constructor invocations were reserving registers for arguments via reserve_registers() but never freeing them afterwards. This caused register count to grow unboundedly with each call, eventually hitting the 255 register limit in code with many function calls. Added free_registers() method to BytecodeBuilder and updated emit_call(), compile_new_expression(), and SuperCall handling to free argument registers after the call completes. - Add BytecodeBuilder::free_registers(start, count) to free consecutive registers - Free argument registers after Call/CallSpread opcodes in emit_call() - Free argument registers after Construct/ConstructSpread in compile_new_expression() - Free argument registers after SuperCall This fixes the "Too many registers needed (max 255)" error when running examples/collections/main.ts which has ~70 function calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d6ad8b7 commit 2324c4f

File tree

4 files changed

+1567
-1
lines changed

4 files changed

+1567
-1
lines changed

src/compiler/builder.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,14 @@ impl BytecodeBuilder {
562562
pub fn reserve_registers(&mut self, count: u8) -> Result<Register, JsError> {
563563
self.registers.reserve_range(count)
564564
}
565+
566+
/// Free a range of consecutive registers (frees from end to start for optimal reuse)
567+
pub fn free_registers(&mut self, start: Register, count: u8) {
568+
// Free in reverse order so the allocator can reclaim them contiguously
569+
for i in (0..count).rev() {
570+
self.registers.free(start + i);
571+
}
572+
}
565573
}
566574

567575
impl Default for BytecodeBuilder {

src/compiler/compile_expr.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1713,13 +1713,20 @@ impl Compiler {
17131713
// Handle super() call
17141714
if matches!(call.callee.as_ref(), Expression::Super(_)) {
17151715
// Compile arguments (spread not supported for super calls yet)
1716-
let (args_start, argc, _has_spread) = self.compile_arguments(&call.arguments)?;
1716+
let (args_start, argc, has_spread) = self.compile_arguments(&call.arguments)?;
17171717

17181718
self.builder.emit(Op::SuperCall {
17191719
dst,
17201720
args_start,
17211721
argc,
17221722
});
1723+
1724+
// Free argument registers
1725+
if has_spread {
1726+
self.builder.free_register(args_start);
1727+
} else if argc > 0 {
1728+
self.builder.free_registers(args_start, argc);
1729+
}
17231730
return Ok(());
17241731
}
17251732

@@ -2159,6 +2166,8 @@ impl Compiler {
21592166
args_start,
21602167
argc,
21612168
});
2169+
// For spread calls, args_start is a single array register
2170+
self.builder.free_register(args_start);
21622171
} else {
21632172
self.builder.emit(Op::Call {
21642173
dst,
@@ -2167,6 +2176,10 @@ impl Compiler {
21672176
args_start,
21682177
argc,
21692178
});
2179+
// Free the argument registers after the call
2180+
if argc > 0 {
2181+
self.builder.free_registers(args_start, argc);
2182+
}
21702183
}
21712184
}
21722185

@@ -2220,13 +2233,19 @@ impl Compiler {
22202233
args_start,
22212234
argc,
22222235
});
2236+
// For spread, args_start is a single array register
2237+
self.builder.free_register(args_start);
22232238
} else {
22242239
self.builder.emit(Op::Construct {
22252240
dst,
22262241
callee: callee_reg,
22272242
args_start,
22282243
argc,
22292244
});
2245+
// Free the argument registers after the construct
2246+
if argc > 0 {
2247+
self.builder.free_registers(args_start, argc);
2248+
}
22302249
}
22312250

22322251
self.builder.free_register(callee_reg);

tests/compiler.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,3 +705,84 @@ fn test_register_usage_many_variables() {
705705
}
706706
}
707707
}
708+
709+
#[test]
710+
fn test_register_usage_many_functions() {
711+
// Many function declarations at module level
712+
let mut code = String::new();
713+
for i in 0..50 {
714+
code.push_str(&format!(
715+
"function func{}(x: number): number {{ return x + {}; }}\n",
716+
i, i
717+
));
718+
}
719+
code.push_str("func49(1)");
720+
721+
let result = std::panic::catch_unwind(|| compile(&code));
722+
match result {
723+
Ok(chunk) => {
724+
println!("50 functions register count: {}", chunk.register_count);
725+
assert!(
726+
chunk.register_count < 100,
727+
"Register count {} too high for 50 function declarations",
728+
chunk.register_count
729+
);
730+
}
731+
Err(e) => {
732+
panic!("Compilation failed with 50 functions: {:?}", e);
733+
}
734+
}
735+
}
736+
737+
#[test]
738+
fn test_register_usage_graph_like_module() {
739+
// Simulate the graph.ts pattern - many exported functions
740+
let chunk = compile(
741+
r#"
742+
function createGraph() { return { nodes: new Map() }; }
743+
function addNode(graph, node) { if (!graph.nodes.has(node)) { graph.nodes.set(node, new Set()); } }
744+
function addEdge(graph, from, to) { addNode(graph, from); addNode(graph, to); graph.nodes.get(from).add(to); }
745+
function hasEdge(graph, from, to) { const n = graph.nodes.get(from); return n !== undefined && n.has(to); }
746+
function getNeighbors(graph, node) { return graph.nodes.get(node) || new Set(); }
747+
function removeEdge(graph, from, to) { const n = graph.nodes.get(from); if (n) { n.delete(to); } }
748+
function bfs(graph, start) {
749+
const visited = new Set();
750+
const result = [];
751+
const queue = [start];
752+
while (queue.length > 0) {
753+
const current = queue.shift();
754+
if (visited.has(current)) continue;
755+
visited.add(current);
756+
result.push(current);
757+
const neighbors = getNeighbors(graph, current);
758+
for (const neighbor of neighbors) {
759+
if (!visited.has(neighbor)) queue.push(neighbor);
760+
}
761+
}
762+
return result;
763+
}
764+
function dfs(graph, start) {
765+
const visited = new Set();
766+
const result = [];
767+
function visit(node) {
768+
if (visited.has(node)) return;
769+
visited.add(node);
770+
result.push(node);
771+
for (const neighbor of getNeighbors(graph, node)) visit(neighbor);
772+
}
773+
visit(start);
774+
return result;
775+
}
776+
const g = createGraph();
777+
addEdge(g, "A", "B");
778+
bfs(g, "A");
779+
"#,
780+
);
781+
782+
println!("Graph-like module register count: {}", chunk.register_count);
783+
assert!(
784+
chunk.register_count < 150,
785+
"Register count {} too high for graph-like module",
786+
chunk.register_count
787+
);
788+
}

0 commit comments

Comments
 (0)