Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/bugc/src/evmgen/behavioral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,63 @@ code { result = factorial(5); }`;
expect(result.callSuccess).toBe(true);
expect(await result.getStorage(0n)).toBe(120n);
});

it("should support recursion at optimization level 2", async () => {
const source = `name RecursionOpt;

define {
function succ(n: uint256) -> uint256 {
return n + 1;
};
function count(
n: uint256, target: uint256
) -> uint256 {
if (n < target) {
return count(succ(n), target);
} else {
return n;
}
};
}

storage { [0] result: uint256; }
create { result = 0; }
code { result = count(0, 5); }`;

const result = await executeProgram(source, {
calldata: "",
optimizationLevel: 2,
});

expect(result.callSuccess).toBe(true);
expect(await result.getStorage(0n)).toBe(5n);
});

it("should support factorial at optimization level 3", async () => {
const source = `name FactorialOpt;

define {
function factorial(n: uint256) -> uint256 {
if (n < 2) {
return 1;
} else {
return n * factorial(n - 1);
}
};
}

storage { [0] result: uint256; }
create { result = 0; }
code { result = factorial(5); }`;

const result = await executeProgram(source, {
calldata: "",
optimizationLevel: 3,
});

expect(result.callSuccess).toBe(true);
expect(await result.getStorage(0n)).toBe(120n);
});
});

describe("loops", () => {
Expand Down
29 changes: 24 additions & 5 deletions packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,37 @@ export function generate<S extends Stack>(
}
}

// Process phi nodes if we have a predecessor
if (predecessor && block.phis.length > 0) {
result = result.then(generatePhis(block.phis, predecessor));
}
// Phi resolution happens at predecessors, not at the
// target. Each predecessor stores its phi source values
// into the phi destination memory slots before jumping.
// This is necessary for back-edges (loops, TCO) where
// the runtime predecessor differs from the layout-order
// predecessor.

// Process regular instructions
for (const inst of block.instructions) {
result = result.then(Instruction.generate(inst));
}

// Emit phi copies for successor blocks before the
// terminator. For jump terminators, check if the
// target has phis and store the source values for
// this block.
if (func && block.terminator.kind === "jump") {
const target = func.blocks.get(block.terminator.target);
if (target && target.phis.length > 0) {
const relevant = target.phis.filter((phi) =>
phi.sources.has(block.id),
);
if (relevant.length > 0) {
result = result.then(generatePhis(relevant, block.id));
}
}
}

// Process terminator
// Handle call terminators specially (they cross function boundaries)
// Handle call terminators specially
// (they cross function boundaries)
if (block.terminator.kind === "call") {
result = result.then(
generateCallTerminator(block.terminator, functions),
Expand Down
1 change: 1 addition & 0 deletions packages/bugc/src/optimizer/steps/jump-optimization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class JumpOptimizationStep extends BaseOptimizationStep {
for (const [blockId, block] of func.blocks) {
if (
block.instructions.length === 0 &&
block.phis.length === 0 &&
block.terminator.kind === "jump"
) {
jumpTargets.set(blockId, block.terminator.target);
Expand Down
26 changes: 18 additions & 8 deletions packages/bugc/src/optimizer/steps/tail-call-optimization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,25 +78,35 @@ describe("TailCallOptimizationStep", () => {

// Count call terminators after optimization
let callsAfterCount = 0;
let hasLoopHeader = false;
let hasPreEntry = false;

for (const [blockId, block] of optimizedFunc.blocks) {
if (block.terminator.kind === "call") {
callsAfterCount++;
}
// Look for the loop header block
if (blockId.includes("_loop")) {
hasLoopHeader = true;
// Loop header should have phi nodes for parameters
expect(block.phis.length).toBe(factorialFunc.parameters.length);
// Look for the pre_entry trampoline block
if (blockId.includes("_pre")) {
hasPreEntry = true;
}
}

// Tail-recursive calls should be eliminated
expect(callsAfterCount).toBe(0);

// Should have created a loop header
expect(hasLoopHeader).toBe(true);
// Should have created a pre_entry trampoline
expect(hasPreEntry).toBe(true);

// Original entry block should have phi nodes
// for parameters
const origEntry = optimizedFunc.blocks.get(optimizedFunc.entry);
const origEntryTarget =
origEntry?.terminator.kind === "jump"
? origEntry.terminator.target
: undefined;
const entryBlock = origEntryTarget
? optimizedFunc.blocks.get(origEntryTarget)
: undefined;
expect(entryBlock?.phis.length).toBe(factorialFunc.parameters.length);

// Should have recorded transformations
const transformations = context.getTransformations();
Expand Down
101 changes: 54 additions & 47 deletions packages/bugc/src/optimizer/steps/tail-call-optimization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,77 +80,78 @@ export class TailCallOptimizationStep extends BaseOptimizationStep {
tailCallBlocks.push(blockId);
}

// If we found tail calls, create a loop structure
// If we found tail calls, transform into a loop.
//
// Strategy: create a trampoline "pre_entry" block as
// the new function entry. Add phi nodes to the original
// entry block that select between the initial param
// values (from pre_entry) and the tail-call arguments
// (from each tail-call site). Tail-call blocks jump
// directly to the original entry.
if (tailCallBlocks.length > 0) {
// Create a new loop header block that will contain phis for parameters
const loopHeaderId = `${func.entry}_loop`;
const originalEntry = func.blocks.get(func.entry);

if (!originalEntry) {
return; // Should not happen
}
const origEntryId = func.entry;
const origEntry = func.blocks.get(origEntryId);
if (!origEntry) return;

// Create trampoline that becomes the new func.entry
const preEntryId = `${origEntryId}_pre`;
const preEntry: Ir.Block = {
id: preEntryId,
phis: [],
instructions: [],
terminator: {
kind: "jump",
target: origEntryId,
operationDebug: {},
},
predecessors: new Set<string>(),
debug: {},
};
func.blocks.set(preEntryId, preEntry);
func.entry = preEntryId;

// Create phi nodes for each parameter
// Build phi nodes on the original entry block.
// Sources: preEntry → original param, each tail
// call block → the call's corresponding argument.
const paramPhis: Ir.Block.Phi[] = [];
for (let i = 0; i < func.parameters.length; i++) {
const param = func.parameters[i];
const phiSources = new Map<string, Ir.Value>();

// Initial value from function entry
phiSources.set(func.entry, {
const sources = new Map<string, Ir.Value>();
sources.set(preEntryId, {
kind: "temp",
id: param.tempId,
type: param.type,
});

paramPhis.push({
kind: "phi",
sources: phiSources,
dest: `${param.tempId}_loop`,
sources,
dest: param.tempId,
type: param.type,
operationDebug: { context: param.loc ? undefined : undefined },
operationDebug: {},
});
}

// Create the loop header block
const loopHeader: Ir.Block = {
id: loopHeaderId,
phis: paramPhis,
instructions: [],
terminator: {
kind: "jump",
target: func.entry,
operationDebug: {},
},
predecessors: new Set([func.entry, ...tailCallBlocks]),
debug: {},
};

func.blocks.set(loopHeaderId, loopHeader);

// Transform each tail call
// Transform each tail call: replace call with jump
// to origEntry, add phi sources for arguments.
for (const blockId of tailCallBlocks) {
const block = func.blocks.get(blockId)!;
const callTerm = block.terminator as Ir.Block.Terminator & {
kind: "call";
};
const contBlock = func.blocks.get(callTerm.continuation)!;

// Update phi sources with arguments from this tail call
for (let i = 0; i < func.parameters.length; i++) {
if (i < callTerm.arguments.length) {
paramPhis[i].sources.set(blockId, callTerm.arguments[i]);
}
}

// Replace call with jump to loop header
block.terminator = {
kind: "jump",
target: loopHeaderId,
target: origEntryId,
operationDebug: callTerm.operationDebug,
};

// Track the transformation
context.trackTransformation({
type: "replace",
pass: this.name,
Expand All @@ -159,30 +160,36 @@ export class TailCallOptimizationStep extends BaseOptimizationStep {
...Ir.Utils.extractContexts(contBlock),
],
result: Ir.Utils.extractContexts(block),
reason: `Optimized tail-recursive call to ${funcName} into loop`,
reason:
`Optimized tail-recursive call to ` + `${funcName} into loop`,
});

// Mark continuation block for removal if it has no other
// predecessors
const otherPredecessors = Array.from(contBlock.predecessors).filter(
(pred) => pred !== blockId,
// Remove continuation if no other predecessors
const otherPreds = Array.from(contBlock.predecessors).filter(
(p) => p !== blockId,
);

if (otherPredecessors.length === 0) {
if (otherPreds.length === 0) {
blocksToRemove.add(callTerm.continuation);

context.trackTransformation({
type: "delete",
pass: this.name,
original: Ir.Utils.extractContexts(contBlock),
result: [],
reason: `Removed unused continuation block ${callTerm.continuation}`,
reason:
`Removed unused continuation block ` + callTerm.continuation,
});
} else {
// Update predecessors
contBlock.predecessors.delete(blockId);
}
}

// Install phis and update predecessors
origEntry.phis = [...paramPhis, ...origEntry.phis];
origEntry.predecessors.add(preEntryId);
for (const blockId of tailCallBlocks) {
origEntry.predecessors.add(blockId);
}
}

// Remove marked blocks
Expand Down
Loading