Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/internals/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The words this project uses with a fixed meaning, in code, comments, diagnostics
| **plot** | a 16 KB region of the heap that hands out places of one width |
| **scratch** | a block of working memory: `scratch { }` in a program, or a host's mark and rewind |
| **promise** | `no.alloc`, `no.host` or `deterministic`, written after a function's result and proved |
| **profile** | the named, versioned set of rules `deterministic` promises (`kest-det 4`) |
| **profile** | the named, versioned set of rules `deterministic` promises (`kest-det 5`) |
| **edition** | the version of the language a program is read in (`2026`) |
| **chunk** | one compiled function in bytecode, with its constants, slot count and source origins |
| **body** | one concrete function in IR; a generic has one body per set of types |
Expand Down
4 changes: 3 additions & 1 deletion docs/user/determinism.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Determinism

A replay, a rollback or a lockstep game between two machines needs the same program to compute the same world everywhere. Kest writes that promise down as a versioned profile, `kest-det 4`, and a function promises to keep it with `deterministic`.
A replay, a rollback or a lockstep game between two machines needs the same program to compute the same world everywhere. Kest writes that promise down as a versioned profile, `kest-det 5`, and a function promises to keep it with `deterministic`.

## Three questions

Expand All @@ -24,6 +24,8 @@ A replay, a rollback or a lockstep game between two machines needs the same prog

**Mathematics.** `sin`, `cos`, `pow` and `atan2` in `std.math` are written in Kest out of additions, multiplications and divisions (fdlibm's algorithms, in `std.fdlibm`), so they are the same steps on every machine. They are within an ulp of the C library's for ordinary arguments. `sqrt`, `floor` and `ceil` are host doors that IEEE 754 requires to be exact, and `std.math` declares them `deterministic`.

**Rounding.** `round` in `std.math` is the nearest whole number with a half going up (`round(2.5)` is 3, `round(-2.5)` is -2), for every `f64` and `f32`, a whole number answering itself. It is worked out from `floor` and what is left above it rather than as `floor(value + 0.5)`, whose addition is rounded before `floor` sees it.

**Order.** An array walks by index, a store by place, text by byte, a range from first to last, and a table in the order its puts and removes left, which the same operations always reproduce.

**Hashing and randomness.** `hash` is FNV-1a from a fixed start for text and fixed arithmetic for everything else, with nothing taken from the run. `std.random` is a source the program holds and seeds; the same seed gives the same numbers everywhere, and none of its functions call the host.
Expand Down
4 changes: 2 additions & 2 deletions docs/user/editions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
`kest --version` prints five numbers, and each moves for a different reason:

```
kest 0.0.1, abi 4, json 4, profile kest-det 4, edition 2026
kest 0.0.1, abi 4, json 4, profile kest-det 5, edition 2026
```

| Number | What it versions | Read by |
| --- | --- | --- |
| `kest 0.0.1` | the release, by Semantic Versioning | people |
| `abi 4` | the shape of the C API in `include/kest.h` (`KEST_ABI_VERSION`, `kest_abi_version()`) | hosts |
| `json 4` | the shape of the JSON every command writes under `--json` (`schema` in each object, `KEST_JSON_SCHEMA`); `lsp` and `dap` speak their own protocols instead | tools |
| `profile kest-det 4` | what `deterministic` promises (`kest_profile()`), see [Determinism](determinism.md) | hosts keeping replays and saves |
| `profile kest-det 5` | what `deterministic` promises (`kest_profile()`), see [Determinism](determinism.md) | hosts keeping replays and saves |
| `edition 2026` | the language a program is read in (`KEST_EDITION`) | programs |

## What a program is promised
Expand Down
2 changes: 1 addition & 1 deletion docs/user/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ That gives a manifest, a program in `src/main.kest` and a test in `tests/adding.
| `source src` | a directory the project's modules live under; one line per directory |
| `tests tests` | the directory `kest test` runs |
| `kest 0.0.1` | the version the project was written against |
| `profile kest-det 4` | the deterministic profile it is written under, see [Determinism](determinism.md) |
| `profile kest-det 5` | the deterministic profile it is written under, see [Determinism](determinism.md) |
| `edition 2026` | the edition of the language it is written in, see [Versions and editions](editions.md) |

An unknown name in the manifest is an error rather than a line that is skipped: every command run in the project refuses it with `K0709`, and `kest doctor` says what it read.
Expand Down
2 changes: 1 addition & 1 deletion examples/slice/kest.project
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ source src
tests tests

kest 0.0.1
profile kest-det 4
profile kest-det 5
edition 2026
2 changes: 1 addition & 1 deletion include/kest.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
// replay or ships a save reads this and writes it down beside them, because a
// run under one profile and a run under another are two runs.
#define KEST_PROFILE_NAME "kest-det"
#define KEST_PROFILE_VERSION 4
#define KEST_PROFILE_VERSION 5

// Which edition of the language a program is written in, which is a fifth
// thing and the one a program chooses: a change that would stop a program
Expand Down
18 changes: 15 additions & 3 deletions lib/std/math.kest
Original file line number Diff line number Diff line change
Expand Up @@ -234,17 +234,29 @@ fn acos(value: f32) -> f32? no.alloc deterministic {
}

// The nearest whole number, with a half going up: `round(2.5)` is 3 and
// `round(-2.5)` is -2, which is what `floor(value + 0.5)` says and what a
// program that wanted anything else would write for itself.
// `round(-2.5)` is -2, which is what a program that wanted anything else would
// write for itself.
//
// What is left above the whole number below is asked rather than half added
// first, because the addition is rounded before `floor` sees it: the `f64`
// just under a half and a half make exactly halfway to 1, which goes up, and
// past 2^52 an odd whole number and a half go to the even one above. What is
// left is exact from nought up and from -1/2 down, and between them it is more
// than a half, which rounding cannot take under one.
//
// Written here rather than declared. Every `extern` in this module is a
// function every host of every program that imports it has to provide, so one
// more is a thing every host has to be changed for; what can be built out of
// what is already declared is built.
fn round(value: f64) -> f64 no.alloc deterministic {
return floor(value + 0.5)
let below = floor(value)
if value - below >= 0.5 {
return below + 1.0
}
return below
}

// An `f32` and a half are added in `f64`, which holds every such sum exactly.
fn round(value: f32) -> f32 no.alloc deterministic {
return f32(floor(f64(value) + 0.5))
}
Expand Down
80 changes: 80 additions & 0 deletions test/regress/roundnear.kest
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Guards against `round` of an `f64` answering one more than the nearest whole
// number: it was `floor(value + 0.5)`, and the addition is rounded first, so
// the `f64` just under a half answered 1 and an odd whole number past 2^52
// answered the even one above it.

module test.regress.roundnear

import std.math

// Worked out while running, so nothing here is folded before `round` sees it.
fn rounded(value: f64) -> f64 {
return math.round(value)
}

fn main() -> i32 {
let under: f64 = 0.49999999999999994
if rounded(under) != 0.0 || rounded(0.0 - under) != 0.0 {
return 1
}
let odd: f64 = 4503599627370497.0
let top: f64 = 9007199254740991.0
let edge: f64 = 4503599627370496.0
if rounded(edge - 1.0) != edge - 1.0 ||
rounded(edge) != edge ||
rounded(0.0 - edge - 1.0) != 0.0 - edge - 1.0 ||
rounded(odd) != odd ||
rounded(0.0 - odd) != 0.0 - odd ||
rounded(top) != top ||
rounded(odd + 0.5) != odd + 1.0 {
return 2
}
// A half goes up, on either side of nought.
if rounded(2.5) != 3.0 ||
rounded(0.0 - 2.5) != 0.0 - 2.0 ||
rounded(0.5) != 1.0 ||
rounded(0.0 - 0.5) != 0.0 ||
rounded(1.5) != 2.0 ||
rounded(0.0 - 1.5) != 0.0 - 1.0 {
return 3
}
// Just either side of a half, and just under nought, where what is left
// above the whole number below is more than a half and not exact.
let tiny: f64 = 0.0000000000000000001
if rounded(2.4999999999999996) != 2.0 ||
rounded(2.5000000000000004) != 3.0 ||
rounded(0.0 - tiny) != 0.0 ||
rounded(0.0 - 0.30000000000000004) != 0.0 ||
rounded(0.0 - 0.7) != 0.0 - 1.0 {
return 4
}
// What is already whole, or not a number at all, answers itself.
let wide: f64 = 1.0e300
let none_of_them: f64 = 0.0 / 0.0
if rounded(wide) != wide ||
rounded(1.0 / 0.0) != 1.0 / 0.0 ||
rounded(0.0 - 1.0 / 0.0) != 0.0 - 1.0 / 0.0 ||
math.isNumber(rounded(none_of_them)) {
return 5
}
// And `f32`, which was right and stays so: an `f32` and a half are added
// in `f64`, which holds every such sum exactly.
let narrow: f32 = 0.49999997
let whole: f32 = 8388609.0
if math.round(narrow) != 0.0 ||
math.round(f32(0.0) - narrow) != 0.0 ||
math.round(whole) != whole ||
math.round(f32(0.0) - whole) != f32(0.0) - whole ||
math.round(f32(2.5)) != 3.0 ||
math.round(f32(0.0 - 2.5)) != 0.0 - 2.0 ||
math.round(f32(0.0 - 0.5)) != 0.0 {
return 6
}
// Nought keeps its sign: the whole number below it is itself, and nothing
// is left above it. The sum it was took nought under nought to nought.
let under_nought: f64 = 0.0 * (0.0 - 1.0)
if 1.0 / rounded(under_nought) > 0.0 || 1.0 / rounded(0.0) < 0.0 {
return 7
}
return 0
}
2 changes: 1 addition & 1 deletion tools/fuzz.c
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,7 @@ static const char MANIFEST[] = "# a project\n"
"tests tests\n"
"\n"
"kest 0.0.1\n"
"profile kest-det 4\n"
"profile kest-det 5\n"
"edition " KEST_EDITION_STRING "\n";

static const char *const MANIFEST_PIECES[] = {
Expand Down
Loading