Very safe

Broken promises

The problem
Every part of a program should be able to rely on what the other parts promise.
An example
A function expects a percentage between 0 and 100 but is called with 140.
What Nikaia checks
You write the expectation as an assert and it becomes a contract. A call that breaks it is rejected when the program is built, a promise about a result is available to the callers, and a contract carries across package boundaries.
The limit
A compiler cannot know whether data from files, arguments or the network is valid. Check it where it enters the program; once checked, the rest of the code can rely on it.
Show the program and what Nikaia says
fn percent(n: int) -> int {
    assert(n >= 0 && n <= 100)
    return n
}

fn main() {
    println(f"{percent(140)}")
}
$ nikaia check main.nika
error[NK1207]: This call breaks `percent`'s precondition `n >= 0 && n <= 100` every time it is reached.
  --> main.nika:7:5
   |
 7 |     println(f"{percent(140)}")
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: Here `n` is 140.
   = help: Pass `percent` arguments for which `n >= 0 && n <= 100` holds, or check them with a guard before the call.

For data from outside, check it where it arrives. After the guard, the claim is proved:

let out = process::run("git", ["rev-parse", "HEAD"])
let n: int = out.stdout.len()
if n != 41 {
    println("not a commit id")
    return
}
assert(n == 41)     // proved: the guard above handled the other case

More under Very safe: Conflicting actionsTasks that get stuckA memory budget for every taskErrors that go unhandled

Very safe · Contracts

An assert is a contract.

There is no separate specification language. You write assert(whole > 0) at the top of percent, and it becomes a precondition: each caller has to show that whole is positive.

share only passes its total on, so the duty passes on with it. The call with 0 two functions up is refused, and the error names the value and the assert it came from.

An assert about the value a function returns is a postcondition. at_least_zero never returns less than 0, so per_row knows that r is at least 1 and its call to percent needs no check.

A call whose arguments are all known is run while the program is built. So assert(fib(10) == 54) does not build: fib(10) is 55.

How a claim becomes a contract →

fn percent(part: int, whole: int) -> int {
    assert(whole > 0)
    return part * 100 / whole
}

fn share(done: int, total: int) -> int {
    return percent(done, total)     // hands the duty on to its caller
}

fn at_least_zero(n: int) -> int {
    return 0 if n < 0
    assert(n >= 0)                  // a claim about the result
    return n
}

fn per_row(rows: int) -> int {
    let r = at_least_zero(rows) + 1
    return percent(50, r)           // r >= 1: proved, no check
}

fn main() {
    println(f"{per_row(4)}% {share(3, 4)}%")
    // and this call breaks it:
    println(f"{share(1, 0)}%")
}
$ nikaia build
error[NK1207]: This call breaks `share`'s precondition `total > 0` every time it is reached.
   --> …/pre/src/main.nika:24:5
    |
 24 |     println(f"{share(1, 0)}%")
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: Here `total` is 0.
    = note: The precondition is `assert(whole > 0)` in `percent`, carried back to `share`'s entry.
    = help: Pass `share` arguments for which `total > 0` holds, or check them with a guard before the call.
1 claim shown false
fn fib(n: int) -> int {
    let mut a = 0
    let mut b = 1
    for _ in 0..<n {
        let next = a + b
        a = b
        b = next
    }
    return a
}

fn main() {
    assert(fib(10) == 54)
    println(f"{fib(10)}")
}
$ nikaia build
error[NK1207]: `fib(10) == 54` is false every time it is reached.
   --> …/fib/src/main.nika:13:5
    |
 13 |     assert(fib(10) == 54)
    |     ^^^^^^^^^^^^^^^^^^^^^
    |
    = note: What is known before it rules the claim out: no value of its own; the claim is false as written.
    = note: `fib(10)` is 55, run while the program was built.
    = help: If the claim is right, the code before it is wrong; if the code is right, the claim is.
1 claim shown false

Very safe · Loops

A loop needs no invariant.

The assert after the loop in steps reads total, which the loop changes. Usually a proof tool wants you to write what holds on every turn. Here nobody did.

The compiler looks for such a fact itself, then checks it three times: it holds before the first turn, each turn keeps it, and when the loop ends it gives the claim. --asserts prints the fact it found and how it found it.

In price, p is followed through each assignment and both sides of the if. Every claim here is proved, so none of them costs anything when the program runs.

How a loop is proved →

fn steps(n: int) -> int {
    assert(n >= 0)
    let mut i = 0
    let mut total = 0
    while i < n {
        i = i + 1
        total = total + 2
    }
    assert(total == 2 * n)          // no invariant written
    return total
}

fn price(base: int, member: bool) -> int {
    assert(base >= 10)
    let mut p = base
    if member {
        p = p - 5
    } else {
        p = p + 1
    }
    assert(p >= 5)                  // followed through both branches
    return p
}

fn main() {
    println(f"{steps(5)} {price(12, true)}")
}
$ nikaia build --asserts
asserts in …/loop/src/main.nika: 4 - not proved 0 (refused 0, checked at run time 0, checked by a test 0); checked where the words arrive 0; proved 4 (proved 2, through a loop 1, preconditions proved at every call 2)
  …/loop/src/main.nika:9  assert(total == 2 * n)  proved through the loop that changes `i`, `total`, with the invariant `i >= 0 && total >= 0 && 2 * i == total && i < n + 1`, found by closed forms and checked by three queries
  …/loop/src/main.nika:21  assert(p >= 5)  proved
  …/loop/src/main.nika:2  assert(n >= 0)  precondition of `steps`: proved at every call the compiler sees; any other reaches the entry that checks it
  …/loop/src/main.nika:14  assert(base >= 10)  precondition of `price`: proved at every call the compiler sees; any other reaches the entry that checks it
$ target/nikaia/bin/loop
10 7

Very safe · Data from outside

A guard, not an assert.

What another program prints is not up to your program. If it is not what you expected, that is not a bug in your code. It is a case your code has to handle.

So an assert about such data is refused, and the error says what to write instead. After an if that handles the bad case, the same assert is proved.

use std::process

fn main() throws {
    let out = process::run("git", ["rev-parse", "HEAD"])
    let n: int = out.stdout.len()
    assert(n == 41)
    println(f"commit {out.stdout.trim()}")
}
$ nikaia build
error[NK1202]: `n == 41` is a claim about data from outside the program.
  --> …/outa/src/main.nika:6:5
   |
 6 |     assert(n == 41)
   |     ^^^^^^^^^^^^^^^
   |
   = note: `n` comes from outside the program, so nothing the compiler can see says what it holds.
   = note: That it is wrong is a case the program has to handle, not a defect an `assert` catches.
   = help: Check it where it arrives, with a guard the program handles: `throw BadInput(n) if !(n == 41)`, or `return … if !(n == 41)`. After that line, this `assert` is proved.
1 type error
use std::process

fn main() throws {
    let out = process::run("git", ["rev-parse", "HEAD"])
    let n: int = out.stdout.len()
    if n != 41 {
        println("not a commit id")
        return
    }
    assert(n == 41)
    println(f"commit {out.stdout.trim()}")
}
$ nikaia build --asserts
asserts in …/outb/src/main.nika: 1 - not proved 0 (refused 0, checked at run time 0, checked by a test 0); checked where the words arrive 0; proved 1 (proved 1, preconditions proved at every call 0)
  …/outb/src/main.nika:10  assert(n == 41)  proved
$ target/nikaia/bin/outb
commit 7e84ae5c523c8b9937bb4faacc6d029bf49d4f75

Very safe · The report

Ask what was proved.

nikaia build --asserts lists every assert and what became of it. Proved means no check is left in the program. A precondition is each caller's to show; here every call shows it.

Refused means the prover could not read the claim. It reads sums, differences and comparisons, not the product of two names like w * h. Such a claim stops the build with NK1300: not proved is not false, but nothing has shown it. A build that says unproved-claims = "warn" builds it with a warning, and the program checks it when it runs.

fn average(total: int, count: int) -> int {
    assert(count > 0)
    return total / count
}

fn area(w: int, h: int) -> int {
    assert(w >= 0 && h >= 0)
    let a = w * h
    assert(a >= 0)
    return a
}

fn main() {
    let scores = [3, 5, 9]
    let n = scores.len()
    assert(n == 3)
    println(f"{average(17, n)} {area(4, 5)}")
}
$ nikaia build --asserts
asserts in …/rep/src/main.nika: 4 - not proved 1 (refused 1, checked at run time 0, checked by a test 0); checked where the words arrive 0; proved 3 (proved 1, preconditions proved at every call 2)
  …/rep/src/main.nika:9  assert(a >= 0)  refused, not proved: `a` is not a whole number the prover reads
  …/rep/src/main.nika:16  assert(n == 3)  proved
  …/rep/src/main.nika:2  assert(count > 0)  precondition of `average`: proved at every call the compiler sees; any other reaches the entry that checks it
  …/rep/src/main.nika:7  assert(w >= 0 && h >= 0)  precondition of `area`: proved at every call the compiler sees; any other reaches the entry that checks it
error[NK1300]: `assert(a >= 0)` is not proved: `a` is not a whole number the prover reads.
  --> …/rep/src/main.nika:9:5
1 type error

Very safe · The command line

main's parameters are its command line.

a is a word the program needs. times comes after the ;, so it is named: on the command line it is the option --times, and 4 where it is left out.

The words a program is started with come from outside, so an assert about them cannot be proved. It is checked once the words are read, before the body runs. A wrong word ends the program with exit status 2 and a line that says what is wrong. --help lists the precondition next to the parameter.

How main reads its words →

/// Divides 100 by a, a few times.
fn main(a: int; times: int = 4) {
    assert(a > 0)
    for i in 0..<times {
        println(f"{i}: {100 / a}")
    }
}
$ nikaia build --asserts
asserts in …/mainp/src/main.nika: 1 - not proved 0 (refused 0, checked at run time 0, checked by a test 0); checked where the words arrive 1; proved 0 (proved 0, preconditions proved at every call 0)
  …/mainp/src/main.nika:3  assert(a > 0)  precondition of `main`, checked where the words arrive
$ ./mainp 5 --times 2
0: 20
1: 20
$ ./mainp 0
error: `a` is 0, which breaks `main`'s precondition `a > 0`.
usage: mainp [--times <int>] <a>
For more, try `mainp --help`.
$ echo $?
2
$ ./mainp five
error: `a` is an `int`, and `five` is not one.
usage: mainp [--times <int>] <a>
For more, try `mainp --help`.
$ ./mainp --help
mainp
Divides 100 by a, a few times.

usage: mainp [--times <int>] <a>

arguments:
  <a>                  int; requires `a > 0`

options:
  --times <int>        int, default 4
  -h, --help           prints this help