Skip to content

Commit 372e974

Browse files
authored
bugc: reject == and != on arrays and structs (#397)
* bugc: reject == and != on arrays and structs As in Solidity, `==` and `!=` on an array (fixed or dynamic), a struct or a mapping are now type errors. Before, an array or struct in memory, or a struct copied from storage, compared by its address in memory, so equal contents in two places were unequal, and an array in storage failed in IR generation. Bytes and strings still compare by contents. * bugc: changelog for #397 * bugc: say what main did with a mapping's ==
1 parent dffaa94 commit 372e974

4 files changed

Lines changed: 83 additions & 10 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ support. Changes to the specification itself are tracked in the root
4343
- `==`, `!=` and a mapping key with bytes or a string in calldata are
4444
now type errors. Before, they compared or hashed the calldata
4545
reference's word (offset and length), not the bytes ([#379]).
46+
- `==` and `!=` on arrays, structs and mappings are now type errors,
47+
as in Solidity. Before, an array or struct in memory, or a struct
48+
copied from storage, compared by its address in memory, so equal
49+
contents in two places were unequal; a mapping compared its own
50+
slot, so it always equaled itself; an array in storage failed in IR
51+
generation ([#397]).
4652
- A struct member of a struct in storage now takes all the slots its
4753
struct needs, as in Solidity, and the member after it starts at the
4854
next free slot. Before, it took one slot, so the members after a
@@ -581,3 +587,4 @@ First publication.
581587
[#388]: https://github.com/ethdebug/format/pull/388
582588
[#391]: https://github.com/ethdebug/format/pull/391
583589
[#395]: https://github.com/ethdebug/format/pull/395
590+
[#397]: https://github.com/ethdebug/format/pull/397

‎packages/bugc/src/typechecker/checker.test.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,61 @@ describe("checkProgram", () => {
194194
});
195195
});
196196

197+
it("should type check == and != on elementary types", () => {
198+
const result = check(`
199+
name Test;
200+
storage { [0] owner: address; [1] flag: bool; [2] n: uint256; }
201+
code {
202+
let b1 = owner == msg.sender;
203+
let b2 = flag != true;
204+
let b3 = n == 1;
205+
let b4 = n != n;
206+
}
207+
`);
208+
209+
expect(result.success).toBe(true);
210+
expect(Result.hasMessages(result)).toBe(false);
211+
});
212+
213+
it("should reject == and != on arrays, structs and mappings", () => {
214+
const comparisons: Record<string, [string, string]> = {
215+
"a dynamic array in storage": ["nums == nums", "array<uint256>"],
216+
"a fixed array in storage": ["fixed != fixed", "array<uint256, 3>"],
217+
"an array in memory": ["local == local", "array<uint256>"],
218+
"an array and a storage array": ["local != nums", "array<uint256>"],
219+
"a struct in storage": ["point == point", "Point"],
220+
"a struct and a mapping value": ["point != points[0]", "Point"],
221+
"a mapping": ["balances == balances", "mapping<address, uint256>"],
222+
};
223+
224+
for (const [name, [comparison, type]] of Object.entries(comparisons)) {
225+
const result = check(`
226+
name Test;
227+
define {
228+
struct Point { x: uint256; y: uint256; };
229+
}
230+
storage {
231+
[0] nums: array<uint256>;
232+
[1] fixed: array<uint256, 3>;
233+
[4] point: Point;
234+
[6] points: mapping<uint256, Point>;
235+
[7] balances: mapping<address, uint256>;
236+
}
237+
code {
238+
let local: array<uint256> = [1, 2, 3];
239+
let b = ${comparison};
240+
}
241+
`);
242+
243+
expect(result.success, name).toBe(false);
244+
expect(Result.countErrors(result), name).toBe(1);
245+
expect(result).toHaveMessage({
246+
severity: Severity.Error,
247+
message: `Cannot compare ${type}`,
248+
});
249+
}
250+
});
251+
197252
it("should type check logical operators", () => {
198253
const result = check(`
199254
name Test;

‎packages/bugc/src/typechecker/expressions.ts‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,11 @@ export const expressionChecker: Pick<Visitor<Report, Context>, "expression"> = {
250250

251251
case "==":
252252
case "!=":
253-
// Bytes or a string in calldata is a word of offset and
254-
// length, which compares with no other value's
255-
if (
256-
Type.Elementary.isCalldata(leftType) ||
257-
Type.Elementary.isCalldata(rightType)
258-
) {
259-
const type = Type.Elementary.isCalldata(leftType)
260-
? leftType
261-
: rightType;
253+
// Arrays, structs and mappings do not compare, as in
254+
// Solidity. Bytes or a string in calldata is a word of
255+
// offset and length, which compares with no other value's
256+
if (isUncomparable(leftType) || isUncomparable(rightType)) {
257+
const type = isUncomparable(leftType) ? leftType : rightType;
262258
errors.push(
263259
new TypeError(
264260
`Cannot compare ${Type.format(type)}`,
@@ -1210,6 +1206,20 @@ function isValidCast(fromType: Type, toType: Type): boolean {
12101206
return false;
12111207
}
12121208

1209+
/**
1210+
* Whether `==` and `!=` reject an operand of this type: an array, a
1211+
* struct, a mapping, or bytes or a string in calldata
1212+
*/
1213+
function isUncomparable(type: Type | undefined): type is Type {
1214+
return (
1215+
!!type &&
1216+
(Type.isArray(type) ||
1217+
Type.isStruct(type) ||
1218+
Type.isMapping(type) ||
1219+
Type.Elementary.isCalldata(type))
1220+
);
1221+
}
1222+
12131223
const integerOperators = new Set([
12141224
"+",
12151225
"-",

‎packages/web/docs/implementation-guides/compiler/case-study-bug.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ data of each operand. As for a mapping key, this assumes that two
114114
different contents do not have the same hash. Both operands must have
115115
the same type, in memory: bytes or a string in calldata do not
116116
compare, so cast or copy them to memory first. A `bytesN` is a value,
117-
and compares as a word.
117+
and compares as a word. Arrays, structs and mappings do not compare,
118+
as in Solidity: `==` and `!=` on them are type errors.
118119

119120
```bug
120121
let a: bytes = msg.data[0:4];

0 commit comments

Comments
 (0)