Skip to content

to-wasm: check overflow for every integer width - #275

Merged
rvcas merged 2 commits into
mainfrom
rvcas/overflow-all-widths
Oct 22, 2026
Merged

rvcas merged 2 commits into
mainfrom
rvcas/overflow-all-widths

Conversation

@rvcas

@rvcas rvcas commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

The spec says integer overflow and underflow trap for every integer type (docs/language-spec.md, "Overflow and underflow"). Codegen only checked i64 and u64. The checks in #110 predate the narrower types added in #113, and u64 went through the signed i64 checks.

What was wrong

  • i8 through u32: +, - and * wrapped silently.
  • u64: used signed checks, so 1 + 9223372036854775807 trapped while 18446744073709551615 + 1 wrapped to 0.
  • Unary - was never checked, not even -i64::MIN.
  • i8/i16 MIN / -1 left an out-of-range value, such as 128 in an i8, because it fits in the i32 it's computed in. 32- and 64-bit division traps natively.

Fix

Every checked operation now goes through get_or_create_checked(op, width) in intrinsics.rs. It keeps #110's pattern (a small check function generated once and called) and the existing i64 functions unchanged. Per operation and type, it generates:

  • u64: unsigned checks: sum <u x, x <u y, and y != 0 && p /u y != x.
  • i32/u32: compute exactly in i64, and trap if the result doesn't fit in 32 bits.
  • 8/16-bit: compute exactly in i32, and trap if normalizing the result to the type changes it.

Negation goes through the checked 0 - x. With check_overflows: false everything still wraps as before, and narrow signed division now wraps too instead of leaving an out-of-range value.

Testing

  • tests/overflow.rs gains two tests that run all eight types through add, sub, mul, div and negation at their boundaries: one in checked mode and one in wrapping mode. Both fail on main and pass with this change.
  • The shared compile_program helper asserted on the parse errors twice instead of the compile errors, so codegen errors went unnoticed. It now checks the compile errors.
  • One snapshot changed: score.star does u64 arithmetic, which now uses the unsigned checks.
  • The starstream-to-wasm, starstream-proving-runtime, starstream-runtime and starstream-ledger tests pass.

Side effect

The trace-row census drops from 6,001 to 5,890 rows. The test contracts mostly use u64, and the correct unsigned add check costs 13 rows where the signed one cost 23. The census's checked-intrinsic columns undercount now, because its operator-pattern matcher doesn't recognize the new u64 multiply. The totals are accurate.

Not in this PR

Signed / and % truncate, but the spec says they're floored: -3 / 16 gives 0, where the spec says -1. That's a separate bug.

The spec says integer overflow and underflow trap for every integer
type, but codegen only checked i64 and u64 (#110 predates the narrower
types added in #113), and u64 went through the signed i64 checks:

- i8 through u32 `+`, `-` and `*` wrapped silently;
- u64 used signed checks, so `1 + 9223372036854775807` trapped while
  `18446744073709551615 + 1` wrapped to 0;
- unary `-` was never checked, not even `-i64::MIN`;
- i8/i16 `MIN / -1` left an out-of-range value (`128` in an i8).

Every checked operation now goes through `get_or_create_checked(op,
width)`, which keeps the existing i64 intrinsics and generates, once per
operation and type:

- u64: unsigned checks (`sum <u x`, `x <u y`, `y != 0 && p /u y != x`);
- i32/u32: compute exactly in i64, trap if the result doesn't fit;
- 8/16-bit: compute exactly in i32, trap if normalizing changes it.

Negation goes through the checked `0 - x`. With `check_overflows: false`
everything still wraps, and narrow signed division now wraps too.

The overflow tests' `compile_program` helper also checked the parse
errors twice instead of the compile errors; it now asserts on the
compile errors.

Signed-off-by: rvcas <x@rvcas.dev>
@rvcas
rvcas requested a review from SpaceManiac October 21, 2026 03:47
@rvcas
rvcas added this pull request to stack #277 October 21, 2026 04:13
@SpaceManiac

Copy link
Copy Markdown
Contributor

the existing i64 functions unchanged

surface thought: should the old functions also be merged in? not sure what value there is in special-casing them ("handled above" type stuff)

The i64 add/sub/mul checks were the only ones with their own cached
get-or-create methods, special-cased at the top of
get_or_create_checked. They're now body builders dispatched like every
other width, cached in the same map. The generated Wasm is unchanged.

Signed-off-by: rvcas <x@rvcas.dev>

@SpaceManiac SpaceManiac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not rigorously analyzed, but looks like it gets the job done

@rvcas
rvcas merged commit ab577e1 into main Oct 22, 2026
5 checks passed
@rvcas
rvcas deleted the rvcas/overflow-all-widths branch October 22, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants