Issue #632: The prover: a quotient, and a function’s computed result without a written postcondition (decision basis)

Open and answer this issue on GitHub.

ready · opened 2026-10-17 · updated 2026-10-17 · 3 comments

What the feature is

An assert is proved while the program is built where the prover can read it (ADR-269 D4, D9), and shown false - refused with NK1207 - where what is known rules it out (D8). What the prover reads: comparisons of whole numbers built from names, literals, +, -, unary - and a * by a constant; names are immutable whole-number bindings; a call’s result is known only by the callee’s postconditions (D17), and a sync function whose body is one return e ensures result == e (ADR-314 D3).

Where it comes from

The owner’s program (reported in #631):

fn main() {
   let b = 4/example(2)
   assert(b>10)          // checked at run time; the owner expects a refusal
}
fn positive(whole: i64) -> i64 {
    assert(whole > 0)
    return whole
}
fn example(a:i64) -> i64 {
    let whole = 10 + a
    return positive(whole)
}

example(2) is 12 and 4 / 12 is 0, so b > 10 is false every time it is reached. A refusal needs two things nothing decides today:

  1. example(2) == 12. example’s body is not one return e (ADR-314 D3), and no assert states its result (D17). positive’s assert(whole > 0) before return whole gives positive the postcondition result > 0, nothing more. ADR-269 §6 leaves interprocedural facts beyond contracts undecided; ADR-314 rejected Option D (reading each callee’s body at each call).
  2. 4 / r. A quotient is outside D9; ADR-314 §7 leaves products and quotients of two unknowns open. The solver has no division term (ADR-270’s certificate format has the extension step for div by a constant, 0 <= L - m*q <= m - 1, but the terms do not use it).

#631 built what is already decided (a call inside a let’s value is read with its callee’s ensures, and the report names the unread binding). With it, let b = 2 * example(2) over an expression-function example is refused today.

Options

Computed results

Quotients

Risks

A wrong summary or quotient rule refuses a correct program (an error with a bypass, refuted-claims = "warn") or proves a false claim (a check removed only under remove-*-checks:on). Truncation toward zero for negative dividends is the place a rule would be wrong.

Recommendation

D now (small, linear, decided by ADR-270’s step already), B within a package as an observation, E after D. Together they refuse the owner’s assert(b > 10). Not decided here: whether B’s summaries are published across packages.

Comments

keywan-ghadami, 2026-10-17

Decided (October 17, 2026)

How it came up. The owner’s program: let b = 4 / example(2) with fn example(a: i64) -> i64 { let whole = 10 + a; return positive(whole) }, then assert(b > 10), is false every time it is reached (example(2) is 12, 4 / 12 is 0), and it was only checked at run time. #631 fixed one defect (a call inside a let’s value); this issue held the rest. A decision basis checked the code:

It also found that ADR-314’s Option D was rejected for two reasons: every call paid for it, and the body crossed the package boundary. Neither reason holds for on-demand use within a package.

Decision (ADR-359, specification Part III 13.3, 14.2 and C.3, 0.0.978): the guiding principle is that only who needs it pays.

Not decided here: evaluating a call with constant arguments while building, quotients by a divisor that is not pinned (intervals), and loops or recursion in a summary.

To build (in this order, ADR-359 D10)

  1. Folding and comptime (D5, D7). prove_terms.nika: / and % of two literals fold, with the same semantics as Part I 2.2’s folder. prover_walk.nika: an integer comptime binding is the fact B == v. Tests (prove.rs):
    • let b = 4 / 12; assert(b > 10) gives NK1207 with b = 0;
    • comptime B: i64 = 4 / example(2); assert(B > 10) gives NK1207.
  2. Quotient by a positive constant (D5). e / k becomes a fresh name with ADR-270 D5’s bounds (truncation toward zero), and e % k is e - k·q. Tests:
    • n / 2 < n is proved for n > 0;
    • (-7) / 2 == -3 and (-7) % 2 == -1 are proved;
    • n / 2 > n is refused for n == 4;
    • for n in -10..<10 { assert(n / 3 * 3 + n % 3 == n) } is proved (no wrong refusal for a negative dividend).
  3. Result summaries (D1). prover_walk.nika/prover_passes.nika record path → result == e at every exit and compose them through calls, leaves first. A loop that changes a read name, a match, recursion or the term bound means no summary. Unit tests:
    • positive gives result == whole;
    • example gives result == 10 + a;
    • clamp gives (n < 0 → result == 0) ∧ (n >= 0 → result == n);
    • a body with a while gives none.
  4. On demand (D2). A second query only for a claim the first one left open and that reads a call result. Tests:
    • the number of second queries is 0 for a program whose claims are all decided without summaries;
    • example(2) - 1 == 11 is proved.
  5. Message (D8). NK1207’s note lists the call values it read and where they came from.
  6. Ledger (D3). ledger.nika and ledger_text.nika read and write inferred = [...], and only the package itself reads it. Tests (contracts_across.rs):
    • in an app, assert(lib::example(2) > 30) stays a run-time check;
    • NK1208 is not given when an inferred line changes.
  7. Pinned divisor (D6). Substitute linear equalities; otherwise a model plus a proof query. Test: the owner’s program (a tests/ program with NAME.stderr) is refused with NK1207, and the note names b = 0 and example(2) = 12.
  8. Hint across a package (D4), NK1263. A path dependency gets the hint, naming the fact and the assert to write; a fetched package gets none. Tests in contracts_across.rs.
  9. Measure and check. Count instructions with callgrind (at most +5 % for lower over the standalone files and lower-std) and record them in docs/solver-workload.md. Fix every new refutation in the corpus and in tools/*.nika in the code (D9), not with warn. Then Fixes #632.

Version raises and new NK codes are the supervisor’s (CLAUDE.md); NK1263 is already assigned.


Generated by Claude Code

keywan-ghadami, 2026-10-17

Step 1 built (8246f86)

Folding and comptime (D5, D7).

Tests:

Fixed on the way: --asserts now checks the way the build does, with the program’s files, reads and workshop. Before, it ran a second check with no workshop, so a comptime that a call computes reported differently than in the build.

New refutations: none in examples/, tests/samples, tests/language, guide/ or benches/.

Steps 2-9 are still open.


Generated by Claude Code

keywan-ghadami, 2026-10-17

Steps 2-7 built (767cfc1); step 8 (D4, NK1263) open

Built

Still open


Generated by Claude Code

Back to Open Issues