Skip to content

Type Int.div and Int.mod with a nonzero literal divisor as total - #806

Merged
jasisz merged 2 commits into
mainfrom
feat/literal-divisor-discharge
Aug 7, 2026
Merged

Type Int.div and Int.mod with a nonzero literal divisor as total#806
jasisz merged 2 commits into
mainfrom
feat/literal-divisor-discharge

Conversation

@jasisz

@jasisz jasisz commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Rule

Int.div(x, K) / Int.mod(x, K) where K is a syntactic nonzero integer literal now type as plain Int (previously Result<Int, String>), and every backend emits direct Euclidean division. A 0 literal and every non-literal divisor are completely unchanged (Result path, no error, no warning).

Boundary (deliberate, syntactic only): a literal, optionally under one unary minus, plus BigInt literals (nonzero by construction — the lexer only produces them past i64). Identifiers, named constants, constant expressions (8 + 8), and --5 do not discharge. Pinned by literal_divisor_discharge_boundary_is_syntactic_literals_only in tests/typechecker_spec.rs, so widening the rule later requires touching a test named after the boundary. The typechecker stays solver-free.

Mechanism

One shared predicate (ast::is_literal_nonzero_int_divisor) drives two seams:

  • the typechecker's Int.div/Int.mod special-case arm returns Int (arg checks unchanged; the registered Result signature stays for the dynamic path);
  • the HIR resolver lowers the same shape to the pre-existing IntDivEuclid/IntModEuclid intrinsics, which every consumer already handles: VM opcodes, Rust div_euclid().unwrap(), the wasm-gc/wasip2 bignum __aint_divmod helper, Lean (a / k) (Int.ediv), Dafny (a / k) (Euclidean). The resolver keys on syntax, not type stamps — some pipelines resolve without typechecking.

Semantics evidence

  • New cross-backend differential (cross_literal_divisor_discharge_vm_vs_wasm_gc_vs_rust): VM, wasm-gc, and generated Rust are byte-identical over the sign/boundary matrix (7 dividends × 6 literal divisors, incl. -1, i64::MIN, and Big 2^63), and in-program the discharged value == the dynamic-divisor Result.Ok value on every pair.
  • wasip2 smoke: byte-identical to the VM, incl. Int.div(i64::MIN, -1) = 9223372036854775808.
  • Proof export: tests/fixtures/discharged_div_law.av (half(a) = Int.div(a, 2) rendered as (a / 2)) — the doubling law certifies universally in Lean (0 sorries, universal: true) and verifies in Dafny (7 verified, 0 errors). Both new proof_spec tests are toolchain-gated like their neighbors and were run locally with lake (Lean 4.32) and Dafny 4.11.

Stale-premise rider

Over ℤ there is no i64::MIN / -1 overflow — it is the valid Big quotient 2^63. Lifted the const-fold's dead k == -1 exclusion (unit test flipped to assert the fold fires; an eval differential routes MIN / (0 - 1) through the lifted arm), and corrected the false overflow claim in the Int.div/Int.mod header docs (src/types/int.rs, which also claimed truncating div and sign-of-b mod), the VM opcode caller contract, and the /-operator diagnostic (message, pinned test, int-div repair hint).

Migration and proof coverage

The old idiom Result.withDefault(Int.div(a, K), d) is a type error under the new rule; all 66 example sites and the test corpus migrated mechanically to bare Int.div(a, K) (value-identical — the default was dead code for nonzero literal K). The literal-divisor termination recognizers (shared inline_floor_div_shrink, Dafny-local is_literal_div_shrink) now accept the bare shape alongside the legacy wrapper, so the floor-window family and the bigint base-10⁹ digit peel keep their well-founded emissions. tests/fixtures/result_default_cone.av keeps its decline-testing intent via a bound (non-literal) divisor.

Out of scope

  • Self-hosted pipeline (same scoping as the sendBytes precedent): a separate Aver-written implementation with no typechecker and zero literal-divisor sites in its own sources. A discharged-form program evaluated under --self-host still gets a Result value from builtin div — divergence documented here.
  • tools/website/playground/sources/ copies of the migrated examples: they pair with the checked-in playground WASM and both regenerate at release time, so they are deliberately untouched.

Tests

  • cargo test --workspace: 2512 passed
  • full proof_spec with lake + Dafny available locally: 227 passed
  • wasm suites (--features wasm,wasip2): wasm_gc_spec, capture_output, games differential, carrier differential, new cross-backend differential — all green
  • cargo fmt --check clean; clippy (--features wasm,wasip2 --tests) has no warnings in touched files

🤖 Generated with Claude Code

jasisz and others added 2 commits August 7, 2026 22:33
When the divisor of Int.div / Int.mod is a syntactic nonzero integer
literal (including negative literals under a single unary minus, and
BigInt literals, which are nonzero by construction), the call cannot
fail: it now types as plain Int instead of Result<Int, String>, and
every backend emits direct Euclidean division.

- Typechecker: an Int.div/Int.mod arm in the FnCall special-case block
  returns Int when the shared predicate matches; the registered Result
  signature is unchanged for zero-literal and dynamic divisors.
- HIR resolver: the same shape lowers to the existing IntDivEuclid /
  IntModEuclid intrinsics, so the VM opcodes, Rust div_euclid, the
  wasm-gc/wasip2 bignum helper, and the Lean/Dafny bare `/` and `%`
  renderings all fire with no backend edits. The shared predicate
  (ast::is_literal_nonzero_int_divisor) keys on syntax, not type
  stamps, because some pipelines resolve without typechecking.
- Boundary is deliberately syntactic: a 0 literal, identifiers, named
  constants, constant expressions (8 + 8), and double negation stay on
  the Result path, pinned by boundary-named typechecker tests.
- Proof export: discharged calls render as bare Euclidean ops (Lean
  ediv/emod, Dafny Euclidean int division); a discharged-div law
  fixture certifies universally on both backends (toolchain-gated).
- Termination recognizers (shared floor-div shrink and the Dafny-local
  one) accept the bare Int.div(p, k) shape alongside the legacy
  Result.withDefault wrapper, keeping floor-window and bigint proof
  coverage intact.
- Stale-premise cleanup: over mathematical Int there is no
  i64::MIN / -1 overflow, so the const-fold's k == -1 exclusion is
  lifted (test flipped, eval differential added), and the div/mod
  header docs, VM opcode contract, and the `/` diagnostic drop the
  overflow claim.
- Migrate all literal-divisor call sites (66 example sites, the test
  corpus, proof fixtures) from Result.withDefault(Int.div(a, k), d) to
  bare Int.div(a, k); add a VM/wasm-gc/generated-Rust differential
  over the sign/boundary matrix asserting byte-identical output and
  discharged == dynamic Ok values.
- Self-hosted pipeline is out of scope (separate Aver-written
  implementation with no literal-divisor sites in its sources).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f-host

The literal-divisor typing rule — Int.div / Int.mod with a syntactic
nonzero integer literal divisor types as plain Int — had two gaps left.

Playground sources: tools/website/playground/sources/ still held the
pre-migration copies of the game examples, so the playground
compile/proof tests, which build those sources with the current
compiler, failed on Result.withDefault applied to a now-discharged Int.
The game mirror is re-synced from examples/ through the rebuild
script's own sync step, and the hand-maintained sticky-1996 sample
drops its Result match over Int.mod(k, 2) for a direct comparison. The
mirror is only ever read out of the repo, and a release ships sources
and playground wasm from the same build, so the live site stays
consistent.

Self-hosted pipeline: the Aver-in-Aver resolver treated Int.div and
Int.mod as ordinary builtins and its interpreter always built a guest
Result, so a program using the discharged form printed Result.Ok(4)
where every other backend printed 4, or failed later on an arithmetic
type mismatch. The self-hosted resolver now recognises the same
syntactic shape and rewrites it to the Result.withDefault form its own
evaluator already collapses to a bare Int — byte for byte the shape the
old source idiom produced, so the existing fusion and evaluation paths
apply unchanged. src/self_host is regenerated from the updated sources.
A cross-backend test runs the sign matrix, a negative literal divisor
and a mixed discharged/dynamic body on the VM, wasm-gc and the
self-host, and requires identical output.

Proof coverage grows to a discharged mod, a negative literal divisor,
and a body mixing a discharged divisor with a dynamic-divisor Result
call. The mixed law closes only if the discharged call became a bare
Euclidean operator the arithmetic tactic can crunch while the dynamic
call stayed an opaque Result term in the same body. Negative divisors
are law-covered on the quotient only: the tactic relates a / -k to
a / k but has no rule for a % -k, so the negative remainder is pinned
by examples instead.

Typechecker tests pin the parenthesised boundary — (16), (-16) and
-(16) all discharge, because the parser erases parentheses around a
single expression, while (0), a parenthesised identifier, 8 + 8, --5
and an interpolated string do not — and the language guide says so.

Also fixes a pre-existing race in the cross-backend property harness:
the generated Rust project used one fixed crate name inside a shared
target directory, so two tests running in parallel overwrote each
other's binary and one read the other's output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jasisz
jasisz merged commit 3f02d3f into main Aug 7, 2026
35 checks passed
@jasisz
jasisz deleted the feat/literal-divisor-discharge branch August 7, 2026 22:05
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.

1 participant