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
52 changes: 52 additions & 0 deletions packages/bugc/src/evmgen/analysis/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,56 @@ describe("Memory Planning", () => {
// Free pointer should be after all allocations
expect(memory.nextStaticOffset).toBeGreaterThanOrEqual(0x80 + 64);
});

it("gives each sub-word scalar its own word (no byte-packing)", () => {
// Sub-word scalars (address, bool, uint8, bytes4) must not be
// byte-packed into a shared 32-byte word: each is homed by a
// full-word MSTORE, so two in one word would clobber each other's
// bytes at runtime. Every allocation must occupy a distinct word.
const func: Ir.Function = {
name: "packed",
parameters: [
{ name: "a", tempId: "%a", type: Ir.Type.Scalar.address },
{ name: "b", tempId: "%b", type: Ir.Type.Scalar.bool },
{ name: "c", tempId: "%c", type: Ir.Type.Scalar.uint8 },
{ name: "d", tempId: "%d", type: Ir.Type.Scalar.bytes4 },
],
entry: "entry",
blocks: new Map([
[
"entry",
{
id: "entry",
phis: [],
instructions: [],
terminator: { kind: "return", operationDebug: {} },
predecessors: new Set(),
debug: {},
} as Ir.Block,
],
]),
};

const liveness = Liveness.Function.analyze(func);
const memoryResult = Memory.Function.plan(func, liveness);
expect(memoryResult.success).toBe(true);
if (!memoryResult.success) throw new Error("Memory planning failed");
const { allocations } = memoryResult.value;

// All four parameters are allocated.
for (const id of ["%a", "%b", "%c", "%d"]) {
expect(id in allocations, `alloc for ${id}`).toBe(true);
}

// No two allocations share a 32-byte word.
const words = Object.values(allocations).map((a) =>
Math.floor(a.offset / 32),
);
expect(new Set(words).size, "each value owns its word").toBe(words.length);

// And each is word-aligned (a sole occupant reads its whole word).
for (const a of Object.values(allocations)) {
expect(a.offset % 32, "word-aligned offset").toBe(0);
}
});
});
45 changes: 13 additions & 32 deletions packages/bugc/src/evmgen/analysis/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,49 +186,30 @@ export namespace Function {
);
}

// Sort values by size (largest first) for better packing
// Allocate largest-first for deterministic, stable offsets.
const sortedValues = Array.from(needsMemory.entries()).sort(
([_a, typeA], [_b, typeB]) => getTypeSize(typeB) - getTypeSize(typeA),
);

// Track current slot usage for packing
let currentSlotOffset = nextStaticOffset;
let currentSlotUsed = 0;
const SLOT_SIZE = 32;
let currentSlotOffset = nextStaticOffset;

for (const [valueId, type] of sortedValues) {
const size = getTypeSize(type);

// If this value needs a full slot or won't fit in current slot, start new slot
if (size >= SLOT_SIZE || currentSlotUsed + size > SLOT_SIZE) {
if (currentSlotUsed > 0) {
// Move to next slot if current slot has something
currentSlotOffset += SLOT_SIZE;
currentSlotUsed = 0;
}
}

// Allocate in current slot
// One full 32-byte word per value; sub-word scalars are NOT
// byte-packed into a shared word. A memory-homed value is read
// and written with full-word MLOAD/MSTORE, so packing two
// sub-word scalars into one word makes each one's store clobber
// the other's bytes (a real runtime corruption). A word is the
// EVM's natural granularity and memory is not scarce at O0, so
// the only cost of one-per-word is a slightly larger frame.
allocations[valueId] = {
offset: currentSlotOffset + currentSlotUsed,
size: size,
offset: currentSlotOffset,
size: getTypeSize(type),
};

currentSlotUsed += size;

// If we filled the slot exactly, prepare for next slot
if (currentSlotUsed >= SLOT_SIZE) {
currentSlotOffset += SLOT_SIZE;
currentSlotUsed = 0;
}
currentSlotOffset += SLOT_SIZE;
}

// Update next static offset to next available slot
if (currentSlotUsed > 0) {
nextStaticOffset = currentSlotOffset + SLOT_SIZE;
} else {
nextStaticOffset = currentSlotOffset;
}
nextStaticOffset = currentSlotOffset;

// For user functions, the saved return PC lives at a
// fixed offset in the frame header. frameSize is the
Expand Down
Loading