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
6 changes: 6 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ support. Changes to the specification itself are tracked in the root

## Unreleased

### Added

- The `%` operator, with the precedence of `*` and `/`. It compiles to
the EVM's unsigned `MOD`, so `x % 0` is `0`, as `x / 0` is ([#321]).

### Fixed

- At optimization level 3, block merging now renames the incoming block of
Expand Down Expand Up @@ -52,3 +57,4 @@ First publication.
[#300]: https://github.com/ethdebug/format/pull/300
[#305]: https://github.com/ethdebug/format/pull/305
[#320]: https://github.com/ethdebug/format/pull/320
[#321]: https://github.com/ethdebug/format/pull/321
59 changes: 59 additions & 0 deletions packages/bugc/src/evmgen/behavioral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,63 @@ code {
expect(result.returnValue.length).toBe(0);
});
});

describe("modulo", () => {
const program = (expr: string) => `name Modulo;

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

create {
a = 1071;
b = 462;
result = 99;
}

code { result = ${expr}; }`;

for (const level of [0, 1, 2, 3] as const) {
it(`should compute % at optimization level ${level}`, async () => {
const result = await executeProgram(program("a % b"), {
calldata: "",
optimizationLevel: level,
});

expect(result.callSuccess).toBe(true);
expect(await result.getStorage(2n)).toBe(147n);
});

it(`should yield 0 for x % 0 at optimization level ${level}`, async () => {
const result = await executeProgram(program("a % (b - b)"), {
calldata: "",
optimizationLevel: level,
});

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

it(`should match / for a zero divisor at level ${level}`, async () => {
const result = await executeProgram(program("a / (b - b)"), {
calldata: "",
optimizationLevel: level,
});

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

it(`should fold constant % at optimization level ${level}`, async () => {
const result = await executeProgram(program("1071 % 462"), {
calldata: "",
optimizationLevel: level,
});

expect(await result.getStorage(2n)).toBe(147n);
});
}
});
});
15 changes: 15 additions & 0 deletions packages/bugc/src/irgen/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ describe("generateModule", () => {
// We no longer generate "add 0" for assignments - the result is directly used
});

it("should generate a mod instruction for %", () => {
const ir = buildIR(`
name Modulo;
storage {}
code {
let x = 1071 % 462;
}
`);
const entry = ir.main.blocks.get("entry")!;

expect(entry.instructions).toContainEqual(
expect.objectContaining({ kind: "binary", op: "mod" }),
);
});

it("should generate IR for comparison expressions", () => {
const source = `
name Comparison;
Expand Down
50 changes: 50 additions & 0 deletions packages/bugc/src/optimizer/steps/constant-folding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,54 @@ describe("ConstantFoldingStep", () => {
dest: "t3",
});
});

describe("mod", () => {
function foldMod(left: bigint, right: bigint) {
const uint = Ir.Type.Scalar.uint256;
const module = createTestModule([
{
kind: "const",
value: left,
type: uint,
dest: "t0",
operationDebug: {},
},
{
kind: "const",
value: right,
type: uint,
dest: "t1",
operationDebug: {},
},
{
kind: "binary",
op: "mod",
left: { kind: "temp", id: "t0", type: uint },
right: { kind: "temp", id: "t1", type: uint },
dest: "t2",
operationDebug: {},
},
]);
const context: OptimizationContext = {
trackTransformation: () => {},
getTransformations: () => [],
getAnalysis: () => undefined,
setAnalysis: () => {},
};
return step.run(module, context).main.blocks.get("entry")!
.instructions[2];
}

it("should fold constant operands", () => {
expect(foldMod(1071n, 462n)).toMatchObject({
kind: "const",
value: 147n,
dest: "t2",
});
});

it("should leave a zero divisor to run time", () => {
expect(foldMod(7n, 0n)).toMatchObject({ kind: "binary", op: "mod" });
});
});
});
23 changes: 23 additions & 0 deletions packages/bugc/src/parser/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,29 @@ code {}`;
expect(exprs[3].operands).toHaveLength(1);
});

it("should parse % with the precedence of * and /", () => {
const parseResult = parse(`
name Test;
storage {}
code {
a + b % c * d;
}
`);

expect(parseResult.success).toBe(true);
if (!parseResult.success) throw new Error("Parse failed");
const stmts = parseResult.value.body?.items as Ast.Statement.Express[];
const sum = stmts[0].expression as Ast.Expression.Operator;
expect(sum.operator).toBe("+");

// left-associative with *: (b % c) * d
const product = sum.operands[1] as Ast.Expression.Operator;
expect(product.operator).toBe("*");
const mod = product.operands[0] as Ast.Expression.Operator;
expect(mod.operator).toBe("%");
expect(mod.operands).toHaveLength(2);
});

it("should parse access expressions", () => {
const input = `
name Test;
Expand Down
3 changes: 2 additions & 1 deletion packages/bugc/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const Lang = {
minus: token("-"),
multiply: token("*"),
divide: token("/"),
modulo: token("%"),
lt: token("<"),
gt: token(">"),
lte: token("<="),
Expand Down Expand Up @@ -705,7 +706,7 @@ const binaryOperators = [
["==", "!="],
["<", ">", "<=", ">="],
["+", "-"],
["*", "/"],
["*", "/", "%"],
];

// Build precedence parser
Expand Down
26 changes: 26 additions & 0 deletions packages/bugc/src/typechecker/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,32 @@ describe("checkProgram", () => {
expect(Result.hasMessages(result)).toBe(false);
});

it("should type check the modulo operator", () => {
const result = check(`
name Test;
storage {}
code {
let x = 10 % 3;
let y = x % 2;
}
`);

expect(result.success).toBe(true);
expect(Result.hasMessages(result)).toBe(false);
});

it("should reject the modulo operator on non-numeric operands", () => {
const result = check(`
name Test;
storage {}
code {
let x = true % 3;
}
`);

expect(result.success).toBe(false);
});

it("should type check comparison operators", () => {
const result = check(`
name Test;
Expand Down
1 change: 1 addition & 0 deletions packages/bugc/src/typechecker/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export const expressionChecker: Pick<Visitor<Report, Context>, "expression"> = {
case "-":
case "*":
case "/":
case "%":
if (
!Type.isElementary(leftType) ||
!Type.isElementary(rightType) ||
Expand Down
Loading