Type Int.div and Int.mod with a nonzero literal divisor as total - #806
Merged
Conversation
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>
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.
Rule
Int.div(x, K)/Int.mod(x, K)whereKis a syntactic nonzero integer literal now type as plainInt(previouslyResult<Int, String>), and every backend emits direct Euclidean division. A0literal and every non-literal divisor are completely unchanged (Resultpath, no error, no warning).Boundary (deliberate, syntactic only): a literal, optionally under one unary minus, plus
BigIntliterals (nonzero by construction — the lexer only produces them pasti64). Identifiers, named constants, constant expressions (8 + 8), and--5do not discharge. Pinned byliteral_divisor_discharge_boundary_is_syntactic_literals_onlyintests/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:Int.div/Int.modspecial-case arm returnsInt(arg checks unchanged; the registeredResultsignature stays for the dynamic path);IntDivEuclid/IntModEuclidintrinsics, which every consumer already handles: VM opcodes, Rustdiv_euclid().unwrap(), the wasm-gc/wasip2 bignum__aint_divmodhelper, Lean(a / k)(Int.ediv), Dafny(a / k)(Euclidean). The resolver keys on syntax, not type stamps — some pipelines resolve without typechecking.Semantics evidence
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 Big2^63), and in-program the discharged value==the dynamic-divisorResult.Okvalue on every pair.Int.div(i64::MIN, -1) = 9223372036854775808.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 / -1overflow — it is the valid Big quotient2^63. Lifted the const-fold's deadk == -1exclusion (unit test flipped to assert the fold fires; an eval differential routesMIN / (0 - 1)through the lifted arm), and corrected the false overflow claim in theInt.div/Int.modheader 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-divrepair 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 bareInt.div(a, K)(value-identical — the default was dead code for nonzero literalK). The literal-divisor termination recognizers (sharedinline_floor_div_shrink, Dafny-localis_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.avkeeps its decline-testing intent via a bound (non-literal) divisor.Out of scope
--self-hoststill gets aResultvalue 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 passedproof_specwith lake + Dafny available locally: 227 passed--features wasm,wasip2): wasm_gc_spec, capture_output, games differential, carrier differential, new cross-backend differential — all greencargo fmt --checkclean; clippy (--features wasm,wasip2 --tests) has no warnings in touched files🤖 Generated with Claude Code