Issue #798: Facts about data, steps 2-3: a rule over every element the prover reads (xs.all(|x| ...)), and std’s sorted contracts (decision)

Open and answer this issue on GitHub.

decision · opened 2026-10-24 · updated 2026-10-24 · 2 comments

How it came up

Owner, 2026-10-24 (#796, claim (b)): a program should be able to say this list is sorted and this tree has only valid nodes, and the compiler should use it. Step 1 is decided and ready: ADR-381 and #797. A property is proved by name, carried to callers and checked once. It removes only its own check, because the property’s body is not read. The basis, the probes and the ladder are in docs/facts-about-data.md. The owner takes one question at a time.

Q2 - decided 2026-10-24 (owner)

binary_search requires is_sorted(). Where the order cannot be shown (data from outside, or an order lost over many steps), the program has two ways:

Record: ADR-381 D5. D2 now also counts a guard and an assume. Spec: Part I (lists, after sort) and Part III 14.2.

Facts checked:

Marking proofs that rest on an assume - decided 2026-10-24 (owner: option B)

How it came up: Q2 made assume a common way out, and claim (b) is counted from --bounds. Probed on 0.0.1204: with unsafe { assume(0 <= i && i < v.len()) }, --bounds reports v[i] proved, and --asserts reports assumed 1. Nothing ties the proof to the assume, and nikaia.proofs does not record it either.

Decision: --bounds and --asserts report a check or claim that is proved only with an assume’s fact as proved from an assume (line N), and scripts/proved.nika counts these apart. Record: ADR-381 D6, Part III 14.2 (the assume paragraph). Option A (no marking) is rejected.

Does the certificate already name the facts it used? Partly.

To build (after #797 steps 1-2)

  1. std: Vec::is_sorted() (natural order, every element at most the next). sort() ensures it in std.contracts; sort_by_key ensures nothing. Tests:
    • xs.sort(); assert(xs.is_sorted()) is proved;
    • xs.sort(); xs.push(0); assert(xs.is_sorted()) is not proved.
  2. std: Vec::binary_search(x) with the precondition self.is_sorted(). Its result shape follows the existing find/position entries; stop and report if none fits. Tests:
    • after sort(), no check;
    • with no fact, NK1300 naming the property;
    • at warn, one check at the call;
    • after a guard return … if !xs.is_sorted(), no check;
    • after unsafe { assume(xs.is_sorted()) }, no check with remove-bounds-checks:on, and a check with it off.
  3. D6, proved from an assume. Where an assume’s fact is in scope and a goal is proved, ask the goal once more without the assumed facts. If it is still proved, report proved; otherwise proved from an assume with the assume’s line. Do the same for the walk’s own proofs that read such a fact. The extra query is asked only where an assume is in scope. Tests in crates/nikaia/tests/it/prove.rs:
    • assume(0 <= i && i < v.len()); v[i] gives proved from an assume (line 2);
    • the same with a guard return 0 if i < 0 || i >= v.len() before it gives proved;
    • an assume that the goal does not need leaves proved;
    • --asserts’s summary counts them apart.
  4. scripts/proved.nika (owed for #796): count proved from an assume apart from proved.

Open

Comments

keywan-ghadami, 2026-10-24

Q2 prepared for the owner (Q1 is not asked yet: it waits for the count from scripts/proved.nika).

Today: std has no binary_search, is_sorted or binary_search_by (std.contracts). A program writes the loop itself. On an unsorted list that loop silently returns a wrong position: no crash, no message.

Recommendation: binary_search requires self.is_sorted() (an ADR-381 property). sort() ensures it, so xs.sort(); xs.binary_search(x) costs nothing extra. Reason: the language’s promise is that a program does not silently compute a wrong answer where the compiler can know it might. A contract is how that promise is kept, and once ADR-381 is built it costs no new word.

Who pays: the callers whose list is not provably sorted.

If the decision is wrong:

Elsewhere (only what is certain):

Dependency: this builds after #797 (ADR-381 properties).


Generated by Claude Code

keywan-ghadami, 2026-10-24

Next question for the owner: should a check removed because of an assume be shown as “assumed, not proved”?

From zero. The compiler keeps records of what it knows about a program. nikaia.contracts (the ledger, Part I 6.7) publishes each function’s borrows, requires and ensures. nikaia.proofs stores each answer the prover found, with a certificate that is checked again on every build. nikaia check --bounds and --asserts print, line by line, which checks were proved. unsafe { assume(c) } (ADR-376) is the programmer’s word that c holds. The prover then uses c like a proved fact. Example (probed on 0.0.1204):

fn at(v: ref Vec[i64], i: i64) -> i64 sync {
    unsafe { assume(0 <= i && i < v.len()) }
    return v[i]
}

--asserts reports assumed 1: a fact the program vouches for. But --bounds reports v[i] proved. Nothing in that line, and nothing in nikaia.proofs, says that the proof rests on the assume.

Where it comes from. Claim (b) of #796 is counted from --bounds, which scripts/proved.nika will sum. Q2 (ADR-381 D5) gave binary_search a second way out, assume(xs.is_sorted()), so assumed facts will now feed proofs more often. As things stand, every assume raises the “proved” count. No program is wrong because of this today. What is at stake is how honest the count is.

Facts.

Options.

Elsewhere.

Recommendation: (B), plus (D) for the compiler’s own code. Claim (b) then means “proved”, and every program still has its way out. (C) is not decided here: no caller in another package needs it yet. The risk of (B): the walk gets a new property, the origin of each fact, that must stay right. If it is wrong, an assumed proof shows as proved again.


Generated by Claude Code

Back to Open Issues