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
1 change: 0 additions & 1 deletion packages/bugc/examples/basic/functions.bug
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ code {
// Store result
result = total;
/*@test function-call-result
fails: stack underflow in internal function calls
variables:
result:
pointer: { location: storage, slot: 0 }
Expand Down
3 changes: 3 additions & 0 deletions packages/bugc/src/evmgen/analysis/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ function dfsOrder(
const trueBranch = dfsOrder(func, term.trueTarget, visited);
const falseBranch = dfsOrder(func, term.falseTarget, visited);
return [blockId, ...trueBranch, ...falseBranch];
} else if (term.kind === "call") {
// Follow continuation block
return [blockId, ...dfsOrder(func, term.continuation, visited)];
} else {
return [blockId];
}
Expand Down
26 changes: 26 additions & 0 deletions packages/bugc/src/evmgen/analysis/liveness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ export namespace Function {
uses.add(retId);
}
lastUse.set(retId, `${blockId}:return`);
} else if (term.kind === "call") {
// Track call arguments as uses
for (const arg of term.arguments) {
const argId = valueId(arg);
if (!defs.has(argId)) {
uses.add(argId);
}
lastUse.set(argId, `${blockId}:call`);
}
}

blockUses.set(blockId, uses);
Expand Down Expand Up @@ -135,6 +144,23 @@ export namespace Function {
}
}
}
} else if (term.kind === "call") {
// Continuation block is a successor
const contIn = liveIn.get(term.continuation);
if (contIn) {
for (const val of contIn) newOut.add(val);
}
// Add phi sources for continuation
const contBlock = func.blocks.get(term.continuation);
if (contBlock) {
for (const phi of contBlock.phis) {
const source = phi.sources.get(blockId);
if (source && source.kind !== "const") {
newOut.add(valueId(source));
crossBlockValues.add(valueId(source));
}
}
}
}

liveOut.set(blockId, newOut);
Expand Down
54 changes: 51 additions & 3 deletions packages/bugc/src/evmgen/analysis/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ export namespace Module {
}
result.main = mainMemory.value;

// Process user-defined functions
// Process user-defined functions.
// Each function's allocations start after the previous
// function's end, so all simultaneously-active frames
// have non-overlapping memory.
let nextFuncOffset = result.main.nextStaticOffset;
for (const [name, func] of module.functions) {
const funcLiveness = liveness.functions[name];
if (!funcLiveness) {
Expand All @@ -103,11 +107,15 @@ export namespace Module {
),
);
}
const funcMemory = Function.plan(func, funcLiveness);
const funcMemory = Function.plan(func, funcLiveness, {
isUserFunction: true,
startOffset: nextFuncOffset,
});
if (!funcMemory.success) {
return funcMemory;
}
result.functions[name] = funcMemory.value;
nextFuncOffset = funcMemory.value.nextStaticOffset;
}

return Result.ok(result);
Expand All @@ -120,6 +128,12 @@ export namespace Function {
allocations: Record<string, Allocation>;
/** Next available memory offset after all static allocations */
nextStaticOffset: number;
/**
* Offset where this function saves its return PC.
* Only set for user-defined functions that need to
* preserve their return address across internal calls.
*/
savedReturnPcOffset?: number;
}

/**
Expand All @@ -128,10 +142,14 @@ export namespace Function {
export function plan(
func: Ir.Function,
liveness: Liveness.Function.Info,
options: {
isUserFunction?: boolean;
startOffset?: number;
} = {},
): Result<Function.Info, MemoryError> {
try {
const allocations: Record<string, Allocation> = {};
let nextStaticOffset = regions.STATIC_MEMORY_START;
let nextStaticOffset = options.startOffset ?? regions.STATIC_MEMORY_START;

const needsMemory = identifyMemoryValues(func, liveness);

Expand Down Expand Up @@ -194,9 +212,19 @@ export namespace Function {
nextStaticOffset = currentSlotOffset;
}

// Reserve a slot for the saved return PC in user
// functions. This is needed because nested calls
// overwrite memory[0x60] with their own return PC.
let savedReturnPcOffset: number | undefined;
if (options.isUserFunction) {
savedReturnPcOffset = nextStaticOffset;
nextStaticOffset += SLOT_SIZE;
}

return Result.ok({
allocations,
nextStaticOffset,
savedReturnPcOffset,
});
} catch (error) {
return Result.err(
Expand Down Expand Up @@ -396,6 +424,11 @@ function getValueType(valueId: string, func: Ir.Function): Ir.Type | undefined {
}
}
}

// Check call terminator dest (return value)
if (block.terminator.kind === "call" && block.terminator.dest === valueId) {
return Ir.Type.Scalar.uint256;
}
}

return undefined;
Expand Down Expand Up @@ -485,6 +518,21 @@ function identifyMemoryValues(
}
}
}

// Call terminator arguments need memory because the
// call convention cleans the stack before loading args.
// Non-const args must be reloaded from memory.
if (term.kind === "call") {
for (const arg of term.arguments) {
if (arg.kind !== "const") {
const argId = valueId(arg);
const type = getValueType(argId, func);
if (type) {
needsMemory.set(argId, type);
}
}
}
}
}

return needsMemory;
Expand Down
134 changes: 131 additions & 3 deletions packages/bugc/src/evmgen/behavioral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ code {
});

describe("internal functions", () => {
// Internal function calls currently fail at runtime
// (stack underflow). Tracked as known issue.
it.skip("should call defined functions", async () => {
it("should call defined functions", async () => {
const source = `name FuncCall;

define {
Expand Down Expand Up @@ -132,6 +130,136 @@ code {
expect(result.callSuccess).toBe(true);
expect(await result.getStorage(0n)).toBe(30n);
});

it("should call a single-arg function", async () => {
const source = `name SingleArgFunc;

define {
function double(x: uint256) -> uint256 {
return x + x;
};
}

storage {
[0] result: uint256;
}

create {
result = 0;
}

code {
result = double(7);
}`;

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

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

it("should call multiple functions", async () => {
const source = `name MultiFuncCall;

define {
function double(x: uint256) -> uint256 {
return x + x;
};
function triple(x: uint256) -> uint256 {
return x + x + x;
};
}

storage {
[0] a: uint256;
[1] b: uint256;
}

create {
a = 0;
b = 0;
}

code {
a = double(7);
b = triple(5);
}`;

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

expect(result.callSuccess).toBe(true);
expect(await result.getStorage(0n)).toBe(14n);
expect(await result.getStorage(1n)).toBe(15n);
});

it("should call a function from another function", async () => {
const source = `name FuncFromFunc;

define {
function add(a: uint256, b: uint256) -> uint256 {
return a + b;
};
function addThree(x: uint256, y: uint256, z: uint256) -> uint256 {
let sum1 = add(x, y);
let sum2 = add(sum1, z);
return sum2;
};
}

storage {
[0] result: uint256;
}

create {
result = 0;
}

code {
result = addThree(10, 20, 30);
}`;

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

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

it("should call a function in a loop", async () => {
const source = `name FuncInLoop;

define {
function increment(x: uint256) -> uint256 {
return x + 1;
};
}

storage {
[0] total: uint256;
}

create {
total = 0;
}

code {
for (let i = 0; i < 3; i = i + 1) {
total = increment(total);
}
}`;

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

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

describe("loops", () => {
Expand Down
28 changes: 25 additions & 3 deletions packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,38 @@ export function generate<S extends Stack>(
result = result.then(JUMPDEST());
}

// Annotate TOS with dest variable if this is a continuation with return value
// Annotate TOS with dest variable if this is a continuation with return value.
// Also spill to memory if allocated, so the value survives stack cleanup
// before any subsequent call terminators.
if (func && predecessor) {
const predBlock = func.blocks.get(predecessor);
if (
predBlock?.terminator.kind === "call" &&
predBlock.terminator.continuation === block.id &&
predBlock.terminator.dest
) {
// TOS has the return value, annotate it
result = result.then(annotateTop(predBlock.terminator.dest));
const destId = predBlock.terminator.dest;
result = result.then(annotateTop(destId)).then((s) => {
const allocation = s.memory.allocations[destId];
if (!allocation) return s;
// Spill return value to memory: DUP1, PUSH offset, MSTORE
return {
...s,
instructions: [
...s.instructions,
{ mnemonic: "DUP1" as const, opcode: 0x80 },
{
mnemonic: "PUSH2" as const,
opcode: 0x61,
immediates: [
(allocation.offset >> 8) & 0xff,
allocation.offset & 0xff,
],
},
{ mnemonic: "MSTORE" as const, opcode: 0x52 },
],
};
});
}
}
}
Expand Down
Loading
Loading