Repository navigation
bugc: use signed opcodes for signed operands - #339
Merged
Merged
Conversation
Contributor
|
Comparisons always compiled to the EVM's unsigned LT and GT, so a negative value compared greater than any positive one, and `(-56 as int8) < (0 as int8)` was false at every level. Signed values are sign-extended across the word, so the signed opcodes are right at any width: choose them when an operand is a signed integer. Constant folding compares by the same rule.
`/` and `%` always compiled to the EVM's unsigned DIV and MOD, so `(-7 as int8) / (2 as int8)` was a large positive number at every level. Use SDIV and SMOD when an operand is a signed integer: division truncates toward zero and a remainder has the sign of the dividend, as in Solidity. Arithmetic does not check for overflow, so the least int256 divided by -1 is itself, as SDIV gives. Constant folding divides by the same rules.
A write to a mapping value or array element always wrote a full slot, while reads sign-extend from the value's width. With `m: mapping<uint256, int16>`, `m[5] = -2 as int16` stored a word of ff bytes ending in fffe, where Solidity stores fffe. Write only the value's bytes, as variables and struct fields already do.
…gnedness An arithmetic or comparison operator accepted a signed and an unsigned operand, and the signed opcodes then read a uint256 at or above 2^255 as negative. Reject mixed signedness, as Solidity does, with a message that suggests a cast. Every integer literal was a uint256, so that rule alone would reject `x < 0` for a signed `x`; `x == 1` was already rejected while `x < 1` compiled. Now an integer literal operand, or its negation, takes the type of the other operand when that type can hold its value, and is a type error otherwise.
gnidan
force-pushed
the
bugc-signed-ops
branch
from
October 6, 2026 06:38
a9108c2 to
35323e0
Compare
gnidan
added a commit
that referenced
this pull request
Oct 6, 2026
As in Solidity, an array or bytes index and a slice bound must be an unsigned integer. Before, a signed index compiled, and since #339 its comparisons are signed, so -1 would pass a bounds check.
gnidan
added a commit
that referenced
this pull request
Oct 6, 2026
As in Solidity, an array or bytes index and a slice bound must be an unsigned integer. Before, a signed index compiled, and since #339 its comparisons are signed, so -1 would pass a bounds check.
gnidan
added a commit
that referenced
this pull request
Oct 6, 2026
* bugc: add an assert instruction that reverts with Panic An `assert` instruction takes a condition and a panic code. Code generation jumps over a revert when the condition holds; otherwise the code reverts with Solidity's `Panic(uint256)` error data, and the REVERT carries a `revert` context with the panic code and a pointer to that data. Dead code elimination keeps asserts, and constant folding removes an assert whose condition is a true constant. * bugc: give an array literal a memory reference type An array literal's value had a scalar type, so code generation took its length as the scalar's size: [1, 2, 3].length was 32. * bugc: require an unsigned integer index As in Solidity, an array or bytes index and a slice bound must be an unsigned integer. Before, a signed index compiled, and since #339 its comparisons are signed, so -1 would pass a bounds check. * bugc: revert on out-of-bounds indexing Indexing an array or bytes in memory, or a fixed-size array in storage, now reverts with Panic(0x32) when the index is not less than the length, as Solidity does. This holds for reads and writes, at every level of a nested array. A slice x[a:b] reverts unless a <= b <= x.length. The check compares as unsigned words whatever the index's type, so a negative value cast to an index is a huge index and reverts. Before, an index past the end read or wrote whatever memory followed: writing m[0][1] on [[1], [2]] overwrote m[1]'s length word. The locals soundness test that sliced msg.data[0:4] runs with no calldata, so it slices msg.data[0:0] now. * bugc: document bounds checks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
bugc never emitted SLT, SGT, SDIV or SMOD. Comparisons,
/and%used the unsigned opcodes whatever the operand types, so withs.aholding-56,if (s.a < (0 as int8)) { ... }never ran, and(-7 as int8) / (2 as int8)was a large positive number. Since #334 and #337, signed values sit sign-extended across the word on the stack, so the signed opcodes are right at any width.<,<=,>,>=) use SLT and SGT when an operand is a signed integer.==and!=don't depend on sign./and%use SDIV and SMOD on signed integers: division truncates toward zero and a remainder has the sign of the dividend, as in Solidity. BUG arithmetic has no overflow checks (add, sub and mul wrap), so the least int256 divided by -1 wraps to itself, as SDIV gives, which is Solidity'suncheckedresult.x < 0still works for a signedx, an integer literal (or its negation) now takes the type of the other operand when that type can hold it, and is a type error otherwise (x < 128for an int8). Before, every literal was a uint256, sox == 1was rejected whilex < 1compiled.Ir.Utils.isSignedBinary/foldSigned.m[5] = -2 as int16withm: mapping<uint256, int16>stored0xff…fffe; now it stores0xfffe, as Solidity does. (bugc gives each array element its own slot, without Solidity's packing, so only the mapping layout matches Solidity.) Reads were already right.BUG has no shift operators, so there's nothing to do for SAR.
The tests are behavioral, at levels 0 to 3, plus type checker tests for the literal and signedness rules: every comparison on int8, int16, int128 and int256 operands (negative and negative, negative and positive, equal),
/and%with each sign combination, the least int256 against -1, a few unsigned cases above 2^255 to show those don't change, literal versions of each that the optimizer folds, and literal operands such asx < 0,x == 1and-1 < x. The mapping tests check the exact slot word for int8, int16 and int128 values, and read each back.Two things I found along the way and left alone here:
(-128 as int8) / (-1 as int8)is 128 on the stack, where Solidity's unchecked result is -128. The same goes for narrow+,-and*overflow. A follow-up PR, stacked on this one, makes them wrap; the docs and changelog say so.ifstatements in one body (18 take about 0.3 s; 24 ran out of memory), and a body with many narrow mapping writes fails at levels 2 and 3 with "Cannot load value … not in stack or memory". Both happen without this change.