Standard library reference

Every module of lib/std, as kest doc prints it: each declaration as it is written, with the comment above it.

std.bytes

Numbers, truths and text written into a run of bytes and read back out of one, which is what a save, a reload and a record on disk are made of. Every number is written low byte first at its own width, a float as its bits, so what is read back is what was written and the same bytes on every machine. Text is the number of bytes it is, as a u32, and then the bytes.

Writing is adding to the end of a run of bytes, so a save is a run of these.

putU8

fn putU8(out: [u8], value: u8) no.host deterministic

putBool

fn putBool(out: [u8], value: bool) no.host deterministic

putU32

fn putU32(out: [u8], value: u32) no.host deterministic

Written out a byte at a time rather than in a loop of four: the loop was most of what writing one cost.

putI32

fn putI32(out: [u8], value: i32) no.host deterministic

putU64

fn putU64(out: [u8], value: u64) no.host deterministic

putI64

fn putI64(out: [u8], value: i64) no.host deterministic

putF32

fn putF32(out: [u8], value: f32) no.host deterministic

putF64

fn putF64(out: [u8], value: f64) no.host deterministic

putText

fn putText(out: [u8], value: text) no.host deterministic

putBytes

fn putBytes(out: [u8], values: [u8]) no.host deterministic

putI32s

fn putI32s(out: [u8], values: [i32]) no.host deterministic

putF32s

fn putF32s(out: [u8], values: [f32]) no.host deterministic

Reader

struct Reader {
    bytes: [u8]
    state: [i32]
}

Reading goes along a run of bytes from the start. Two handles, held in step: the bytes, and where the reading has got to with whether it ever ran short. A read past the end answers nought and marks the reader short, so a restore reads everything and then asks once whether what it read was all there.

reader

fn reader(bytes: [u8]) -> Reader no.host deterministic

short

fn short(r: Reader) -> bool no.alloc no.host deterministic

Whether a read has run past the end.

left

fn left(r: Reader) -> i32 no.alloc no.host deterministic

How many bytes there are still to read.

take

fn take(r: Reader, many: i32) -> i32 no.alloc no.host deterministic

Where the next many bytes start, or minus one when there are not that many left, which marks the reader short and reads nothing.

word

fn word(from: [u8], at: i32) -> u32 no.alloc no.host deterministic

The four bytes at at, low first, as one number.

readU8

fn readU8(r: Reader) -> u8 no.alloc no.host deterministic

readBool

fn readBool(r: Reader) -> bool no.alloc no.host deterministic

readU32

fn readU32(r: Reader) -> u32 no.alloc no.host deterministic

readI32

fn readI32(r: Reader) -> i32 no.alloc no.host deterministic

readU64

fn readU64(r: Reader) -> u64 no.alloc no.host deterministic

readI64

fn readI64(r: Reader) -> i64 no.alloc no.host deterministic

readF32

fn readF32(r: Reader) -> f32 no.alloc no.host deterministic

readF64

fn readF64(r: Reader) -> f64 no.alloc no.host deterministic

readBytes

fn readBytes(r: Reader) -> [u8] no.host deterministic

readI32s

fn readI32s(r: Reader) -> [i32] no.host deterministic

readF32s

fn readF32s(r: Reader) -> [f32] no.host deterministic

readText

fn readText(r: Reader) -> text no.host deterministic

Text as it was written. Bytes that are not there are read as the empty text and mark the reader short; bytes that are there and are not UTF-8 are refused where they are made into text, the way any bytes are.

std.fdlibm

The sine, the cosine, where two numbers point and one number to the power of another, in f64, which std.math hands its own on to in both widths. They are written in Kest rather than declared, so that they are in the deterministic profile: every step of each is an addition, a multiplication or a division, which IEEE-754 rounds the same way on every machine, and the two engines keep each one a step of its own. Declared, they were the platform's libm, which is not required to round them correctly, and two platforms could differ in the last bit and then diverge.

They are fdlibm's algorithms, as musl and FreeBSD carry them, and every constant is written as the bits of the one in those sources: a reduction by quarter turns in three rounds, and a polynomial on the quarter turn. Against glibc's they are within an ulp. Past two to the twentieth quarter turns, about 1.6 million, the reduction musl hands to a longer algorithm is this one still, so a sine or a cosine that far out is the same on every machine and further from the true one than an ulp.

Copyright (C) 1993, 2004 by Sun Microsystems, Inc. All rights reserved. Developed at SunPro, a Sun Microsystems, Inc. business. Permission to use, copy, modify, and distribute this software is freely granted, provided that this notice is preserved.

highWord

fn highWord(value: f64) -> i32 no.alloc no.host deterministic

How fdlibm reads a double: its high thirty-two bits with their sign, which is where the exponent and the top of the fraction are, and its low ones.

lowWord

fn lowWord(value: f64) -> u32 no.alloc no.host deterministic

lowCleared

fn lowCleared(value: f64) -> f64 no.alloc no.host deterministic

The same double with its low thirty-two bits nought, which is how fdlibm cuts a number into a head it can multiply exactly and a tail.

withHighWord

fn withHighWord(value: f64, word: i32) -> f64 no.alloc no.host deterministic

negated

fn negated(value: f64) -> f64 no.alloc no.host deterministic

The other sign, nought included: 0.0 - value is nought with no sign when value is nought, and this is nought with one.

magnitude

fn magnitude(value: f64) -> f64 no.alloc no.host deterministic

The same number without its sign, nought included.

S1

const S1: f64 = float(u64(0xBFC5555555555549))

S2

const S2: f64 = float(u64(0x3F8111111110F8A6))

S3

const S3: f64 = float(u64(0xBF2A01A019C161D5))

S4

const S4: f64 = float(u64(0x3EC71DE357B1FE7D))

S5

const S5: f64 = float(u64(0xBE5AE5E68A2B9CEB))

S6

const S6: f64 = float(u64(0x3DE5D93A5ACFD57C))

C1

const C1: f64 = float(u64(0x3FA555555555554C))

C2

const C2: f64 = float(u64(0xBF56C16C16C15177))

C3

const C3: f64 = float(u64(0x3EFA01A019CB1590))

C4

const C4: f64 = float(u64(0xBE927E4F809C52AD))

C5

const C5: f64 = float(u64(0x3E21EE9EBDB4B1C4))

C6

const C6: f64 = float(u64(0xBDA8FAE9BE8838D4))

TO_INT

const TO_INT: f64 = float(u64(0x4338000000000000))

INV_PIO2

const INV_PIO2: f64 = float(u64(0x3FE45F306DC9C883))

PIO2_1

const PIO2_1: f64 = float(u64(0x3FF921FB54400000))

PIO2_2

const PIO2_2: f64 = float(u64(0x3DD0B4611A600000))

PIO2_2T

const PIO2_2T: f64 = float(u64(0x3BA3198A2E037073))

PIO2_3

const PIO2_3: f64 = float(u64(0x3BA3198A2E000000))

PIO2_3T

const PIO2_3T: f64 = float(u64(0x397B839A252049C1))

sinKernel

fn sinKernel(x: f64, y: f64, withTail: bool) -> f64 no.alloc no.host deterministic

cosKernel

fn cosKernel(x: f64, y: f64) -> f64 no.alloc no.host deterministic

Reduced

struct Reduced {
    head: f64
    tail: f64
    quarter: i32
}

What a number is past the nearest multiple of a quarter turn, as a head and a tail, and which of the four quarters that multiple is.

reduced

fn reduced(x: f64) -> Reduced no.alloc no.host deterministic

sin

fn sin(value: f64) -> f64 no.alloc no.host deterministic

cos

fn cos(value: f64) -> f64 no.alloc no.host deterministic

AT0

const AT0: f64 = float(u64(0x3FD555555555550D))

AT1

const AT1: f64 = float(u64(0xBFC999999998EBC4))

AT2

const AT2: f64 = float(u64(0x3FC24924920083FF))

AT3

const AT3: f64 = float(u64(0xBFBC71C6FE231671))

AT4

const AT4: f64 = float(u64(0x3FB745CDC54C206E))

AT5

const AT5: f64 = float(u64(0xBFB3B0F2AF749A6D))

AT6

const AT6: f64 = float(u64(0x3FB10D66A0D03D51))

AT7

const AT7: f64 = float(u64(0xBFADDE2D52DEFD9A))

AT8

const AT8: f64 = float(u64(0x3FA97B4B24760DEB))

AT9

const AT9: f64 = float(u64(0xBFA2B4442C6A6C2F))

AT10

const AT10: f64 = float(u64(0x3F90AD3AE322DA11))

ATAN_HI0

const ATAN_HI0: f64 = float(u64(0x3FDDAC670561BB4F))

ATAN_HI1

const ATAN_HI1: f64 = float(u64(0x3FE921FB54442D18))

ATAN_HI2

const ATAN_HI2: f64 = float(u64(0x3FEF730BD281F69B))

ATAN_HI3

const ATAN_HI3: f64 = float(u64(0x3FF921FB54442D18))

ATAN_LO0

const ATAN_LO0: f64 = float(u64(0x3C7A2B7F222F65E2))

ATAN_LO1

const ATAN_LO1: f64 = float(u64(0x3C81A62633145C07))

ATAN_LO2

const ATAN_LO2: f64 = float(u64(0x3C7007887AF0CBBD))

ATAN_LO3

const ATAN_LO3: f64 = float(u64(0x3C91A62633145C07))

atan

fn atan(value: f64) -> f64 no.alloc no.host deterministic

PI

const PI: f64 = float(u64(0x400921FB54442D18))

PI_LO

const PI_LO: f64 = float(u64(0x3CA1A62633145C07))

atan2

fn atan2(y: f64, x: f64) -> f64 no.alloc no.host deterministic

Where two numbers point, in radians from the positive x axis and between -pi and pi. A program turning towards something has a difference and wants an angle, and working that out from sin and cos is working backwards.

DP_H1

const DP_H1: f64 = float(u64(0x3FE2B80340000000))

DP_L1

const DP_L1: f64 = float(u64(0x3E4CFDEB43CFD006))

TWO53

const TWO53: f64 = float(u64(0x4340000000000000))

HUGE

const HUGE: f64 = float(u64(0x7E37E43C8800759C))

TINY

const TINY: f64 = float(u64(0x01A56E1FC2F8F359))

L1

const L1: f64 = float(u64(0x3FE3333333333303))

L2

const L2: f64 = float(u64(0x3FDB6DB6DB6FABFF))

L3

const L3: f64 = float(u64(0x3FD55555518F264D))

L4

const L4: f64 = float(u64(0x3FD17460A91D4101))

L5

const L5: f64 = float(u64(0x3FCD864A93C9DB65))

L6

const L6: f64 = float(u64(0x3FCA7E284A454EEF))

P1

const P1: f64 = float(u64(0x3FC555555555553E))

P2

const P2: f64 = float(u64(0xBF66C16C16BEBD93))

P3

const P3: f64 = float(u64(0x3F11566AAF25DE2C))

P4

const P4: f64 = float(u64(0xBEBBBD41C5D26BF1))

P5

const P5: f64 = float(u64(0x3E66376972BEA4D0))

LG2

const LG2: f64 = float(u64(0x3FE62E42FEFA39EF))

LG2_H

const LG2_H: f64 = float(u64(0x3FE62E4300000000))

LG2_L

const LG2_L: f64 = float(u64(0xBE205C610CA86C39))

OVT

const OVT: f64 = float(u64(0x3C971547652B82FE))

CP

const CP: f64 = float(u64(0x3FEEC709DC3A03FD))

CP_H

const CP_H: f64 = float(u64(0x3FEEC709E0000000))

CP_L

const CP_L: f64 = float(u64(0xBE3E2FE0145B01F5))

IVLN2

const IVLN2: f64 = float(u64(0x3FF71547652B82FE))

IVLN2_H

const IVLN2_H: f64 = float(u64(0x3FF7154760000000))

IVLN2_L

const IVLN2_L: f64 = float(u64(0x3E54AE0BF85DDF44))

THIRD

const THIRD: f64 = float(u64(0x3FD5555555555555))

pow

fn pow(x: f64, y: f64) -> f64 no.alloc no.host deterministic

std.hash

hash(x) answers for one value. A program that wants one for several -- the two fields it means out of a struct, a run of them, a name and a number -- folds them together, and what it folds them with used to be whatever the reference suggested. Two programs folding the same fields differently are two answers to what reads like one question, so the fold is written once, here, and it is the fold this compiler uses for a program made of several files: FNV-1a over the eight bytes of each number.

start

const start: u64 = 14695981039346656037

Where a fold begins. Folding into nought would make a run of noughts fold to nought and every value that starts with one look alike.

join

fn join(mark: u64, one: u64) -> u64 no.alloc no.host deterministic

One number folded into what is there so far. Low byte first, which is the order the compiler folds a file's mark in and the order another machine has to agree with.

std.io

Saying something is the host's to do, not the language's. A command line writes to its output; an engine writes to its console; neither is something this language should have decided for them.

Io.write

extern fn Io.write(value: text) no.alloc

Handing a piece of text over is not making one, so this promises nothing is allocated and a frame that has promised the same may write. What holds the host to it is the machine: a bound function that takes from the heap under this promise is refused where it was called.

write

fn write(value: text) no.alloc

print

fn print(value: text) no.alloc

std.math

Two functions may share a name when they take different things, so there is one min rather than a minInt and a minFloat. Which is meant is settled by what is passed, and nothing converts on its own, so min(1, 2.0) is not a question with a quiet answer.

What cannot be written in Kest is declared instead. A host that a program using this module runs against has to provide these, and a program that never reaches one never asks for it. All three are exact on every machine: IEEE-754 rounds a square root correctly, and a floor and a ceiling round nothing.

Math.sqrt

extern fn Math.sqrt(value: f64) -> f64 no.alloc deterministic

Math.floor

extern fn Math.floor(value: f64) -> f64 no.alloc deterministic

Math.ceil

extern fn Math.ceil(value: f64) -> f64 no.alloc deterministic

min

fn min(a: i32, b: i32) -> i32 no.alloc no.host deterministic

min

fn min(a: i64, b: i64) -> i64 no.alloc no.host deterministic

min

fn min(a: f32, b: f32) -> f32 no.alloc no.host deterministic

min

fn min(a: f64, b: f64) -> f64 no.alloc no.host deterministic

max

fn max(a: i32, b: i32) -> i32 no.alloc no.host deterministic

max

fn max(a: i64, b: i64) -> i64 no.alloc no.host deterministic

max

fn max(a: f32, b: f32) -> f32 no.alloc no.host deterministic

max

fn max(a: f64, b: f64) -> f64 no.alloc no.host deterministic

abs

fn abs(value: i32) -> i32 no.alloc no.host deterministic

The smallest i32 is one further out than the largest, so its distance from nought is not an i32 and abs of it is itself: negating it wraps, which is what arithmetic does at the end of a width. Nothing here refuses it, because a value that wraps is what its type says happens; a program that cannot have that answer holds the value away from the edge before asking.

abs

fn abs(value: i64) -> i64 no.alloc no.host deterministic

abs

fn abs(value: f32) -> f32 no.alloc no.host deterministic

abs

fn abs(value: f64) -> f64 no.alloc no.host deterministic

clamp

fn clamp(value: i32, low: i32, high: i32) -> i32 no.alloc no.host deterministic

clamp

fn clamp(value: i64, low: i64, high: i64) -> i64 no.alloc no.host deterministic

clamp

fn clamp(value: f32, low: f32, high: f32) -> f32 no.alloc no.host deterministic

clamp

fn clamp(value: f64, low: f64, high: f64) -> f64 no.alloc no.host deterministic

isNumber

fn isNumber(value: f64) -> bool no.alloc no.host deterministic

Whether this is a number to go on with. A float has two answers that are not one: infinity, which is what a number too big to hold comes back as, and the value that is not a number, which is what a question with no answer comes back as -- sqrt of a negative is that, and so is nought divided by nought. Both come out of arithmetic without a word said, and neither can be found by comparing, because the one that is not a number is not equal to itself and infinity is equal to itself.

Every number less itself is nought and neither of those is, which is the whole question and needs no constant. This language has no exponent in a literal, so the largest f32 cannot be written down and a check against it could not be written here at all.

isNumber

fn isNumber(value: f32) -> bool no.alloc no.host deterministic

sqrt

fn sqrt(value: f64) -> f64 no.alloc deterministic

A f32 goes through the f64 one and comes back, because a frame works in f32 and widening one by hand at every call is the module not doing its half. Widening is exact and narrowing rounds, so there is one rounding here and one in the host.

What that comes to depends on the function. sqrt is rounded exactly by every machine that has one, and f64 carries more than twice the digits of f32, so the answer rounded twice is the answer rounded once: the nearest f32 to the true one. floor and ceil are exact and round is built out of floor. sin, cos and pow are not promised to be rounded exactly by anybody, so what comes back is the nearest f32 to the host's own answer, which is within a hair of the true one and is the same hair a program would get by asking in f64 itself.

sqrt

fn sqrt(value: f32) -> f32 no.alloc deterministic

floor

fn floor(value: f64) -> f64 no.alloc deterministic

floor

fn floor(value: f32) -> f32 no.alloc deterministic

ceil

fn ceil(value: f64) -> f64 no.alloc deterministic

ceil

fn ceil(value: f32) -> f32 no.alloc deterministic

sin

fn sin(value: f64) -> f64 no.alloc no.host deterministic

The sine, the cosine, where two numbers point and one number to the power of another are std.fdlibm's, written in Kest and inside the deterministic profile, in both widths here.

cos

fn cos(value: f64) -> f64 no.alloc no.host deterministic

pow

fn pow(base: f64, exponent: f64) -> f64 no.alloc no.host deterministic

atan2

fn atan2(y: f64, x: f64) -> f64 no.alloc no.host deterministic

Where two numbers point, in radians from the positive x axis and between -pi and pi. A program turning towards something has a difference and wants an angle, and working that out from sin and cos is working backwards.

atan

fn atan(value: f64) -> f64 no.alloc no.host deterministic

The angle whose tangent is value, between -pi/2 and pi/2.

atan

fn atan(value: f32) -> f32 no.alloc no.host deterministic

sin

fn sin(value: f32) -> f32 no.alloc no.host deterministic

cos

fn cos(value: f32) -> f32 no.alloc no.host deterministic

pow

fn pow(base: f32, exponent: f32) -> f32 no.alloc no.host deterministic

atan2

fn atan2(y: f32, x: f32) -> f32 no.alloc no.host deterministic

tan

fn tan(value: f64) -> f64 no.alloc no.host deterministic

A tangent is a sine over a cosine.

tan

fn tan(value: f32) -> f32 no.alloc no.host deterministic

asin

fn asin(value: f64) -> f64? no.alloc deterministic

Nothing outside -1 to 1 has an angle, and asking for one is a question with no answer rather than a number to make up.

acos

fn acos(value: f64) -> f64? no.alloc deterministic

asin

fn asin(value: f32) -> f32? no.alloc deterministic

And in the width a frame works in, widened and asked of the same atan2. What is outside -1 to 1 in one width is outside it in the other.

acos

fn acos(value: f32) -> f32? no.alloc deterministic

round

fn round(value: f64) -> f64 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.

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.

round

fn round(value: f32) -> f32 no.alloc deterministic

sign

fn sign(value: i32) -> i32 no.alloc no.host deterministic

Written here rather than declared, because they are what the host already gives with an operator in front.

sign

fn sign(value: i64) -> i32 no.alloc no.host deterministic

A sign is one of three answers whatever width it was asked about, so this gives back the width those three fit in rather than the one it was handed.

There is no sign for a float. Two of the values a float has are not on one side of nought or the other: what is not a number is on neither, and this shape would call it nought, which is the answer for a number that is exactly nought and is a made-up one here. A program that wants it asks isNumber first and then compares, which is the two questions it is really asking.

lerp

fn lerp(from: f32, to: f32, amount: f32) -> f32 no.alloc no.host deterministic

lerp

fn lerp(from: f64, to: f64, amount: f64) -> f64 no.alloc no.host deterministic

std.os

What only a host can do. Everything in here is a door the host binds, which is the whole of this language's answer to the question of who may touch a file, a clock or the words a program was started with: nobody, unless a host says so. There is no global filesystem and no ambient clock, and there is not going to be one.

That is not caution, it is the same rule the rest of the language is built on. A program that promises no.host cannot reach any of this, the compiler proves it, and what that promise then means is exactly what a reader wants it to mean: this runs the same on every machine and touches nothing outside itself. A program that wants a file says so in its type.

A host that binds none of these runs programs that do not use them and refuses the ones that do, before they run, by name. That is the capability model: the list of what a host provides is the list of what a program may do.

kest binds all of them. Another host binds what it wants to allow.

Host.fileRead

extern fn Host.fileRead(path: text) -> text?

Reading a whole file. Nothing rather than empty text when it cannot be read, because a file that is not there and a file with nothing in it are different answers and a program that cannot tell them apart will one day have to.

Host.fileWrite

extern fn Host.fileWrite(path: text, bytes: text) -> bool

Writing one, answering whether it arrived. The bytes are text, which is what this language has: a file of anything else is a lend, and that is the host's own door rather than this one.

Host.fileExists

extern fn Host.fileExists(path: text) -> bool

And whether it is there at all, which is a question a program asks before it does something rather than after it has failed.

Host.argCount

extern fn Host.argCount() -> i32 no.alloc

How many words the program was started with, and which. The name the program was invoked by is not one of them: what a program wants is what it was asked to do, and a host that is not a command line has no such name to give.

Host.arg

extern fn Host.arg(at: i32) -> text?

Host.clock

extern fn Host.clock() -> i64 no.alloc

A clock that measures elapsed time and only goes forwards, in microseconds, from somewhere nobody promises anything about. It is for measuring how long something took and for nothing else: two runs do not start it at the same place, and what it says is not a date.

What a host binds to it is the host's. kest binds the platform's monotonic clock where there is one, and says so: on a platform with none it falls back to a wall clock, which can go backwards. It is not determinism and a body that promises no.host cannot read it.

read

fn read(path: text) -> text?

write

fn write(path: text, bytes: text) -> bool

exists

fn exists(path: text) -> bool

argCount

fn argCount() -> i32 no.alloc

arg

fn arg(at: i32) -> text?

args

fn args() -> [text]

All of them in one array, for a program that wants to walk them rather than count them. It allocates, which is why it is not the only way to ask.

now

fn now() -> i64 no.alloc

Microseconds since the clock's own start. now() - before is the only thing to do with two of these.

millisBetween

fn millisBetween(before: i64, after: i64) -> i64 no.alloc no.host deterministic

And what that is in milliseconds, because a frame budget is written in them and dividing by a thousand in three places is three places to get it wrong.

std.random

Numbers that look random, out of a state a program holds. Nothing here is global and nothing asks the host: a simulation that wants to run twice and see the same thing has to be able to say where its numbers came from, and a generator hidden behind a name nobody passes cannot say that.

The state is a value like any other, so a source is carried the way a count is: source = random.next(source), and what comes back is both the next state and the next number. That is one thing to hold rather than two, and it is why next gives a Source and not a number.

This is xorshift64, which is small, fast and good enough to place things with. It is not for anything that has to be unguessable.

Source

struct Source {
    state: u64
}

from

fn from(seed: u64) -> Source no.alloc no.host deterministic

A source that starts where it is told. Nought is not a state this can leave, so it is turned into one that it can.

next

fn next(s: Source) -> Source no.alloc no.host deterministic

number

fn number(s: Source) -> u64 no.alloc no.host deterministic

What it is on, as a whole number. Every bit of it is worth as much as any other, which is what lets the ones below take a part of it.

below

fn below(s: Source, count: i32) -> i32 no.alloc no.host deterministic

Below count, and nought when count is not above nought, because there is no number below one of those to give.

This is a remainder, so a count that does not divide the whole range leaves the first few numbers very slightly likelier than the rest -- by about count in eighteen billion billion, which is not a difference anything counting them will see. Throwing away the numbers that cause it is the other way, and it makes this a loop that might go round again, which is worse in a language where a frame has a budget.

between

fn between(s: Source, from: i32, to: i32) -> i32 no.alloc no.host deterministic

Between two numbers, the first included and the second not, which is how a count is written everywhere else in this language.

fraction

fn fraction(s: Source) -> f32 no.alloc no.host deterministic

Between nought and one, as a fraction. The top bits are used because they are the ones a shift leaves alone.

chance

fn chance(s: Source, hundredths: i32) -> bool no.alloc no.host deterministic

True about that many times in a hundred.

one

fn one<T>(s: Source, xs: [T]) -> T? no.alloc no.host deterministic

One of them, and nothing when there are none. The source is used as it stands, so a caller that wants a different one asks for the next source first, the same as everything else here.

shuffle

fn shuffle<T>(s: Source, xs: [T]) -> Source no.alloc no.host deterministic

Every arrangement as likely as any other, in place and allocating nothing. The source that comes back is where the next call starts, because this used as many numbers as the array is long.

std.sort

Sorting needs to be told what comes first, and being told is a function value: fn(T, T) -> bool no.alloc no.host. The promise is part of the type, so a sort that promises both can call one and the compiler can still prove it . Both, because a sort that promises what it will not do has to be handed something that promises the same: what it is told to compare with is the one body it cannot read.

One body, one copy per type it is called with. Gapped insertion -- Shell's, with Knuth's 3h+1 gaps -- which is in place, allocates nothing, recurses nowhere, and costs the same on any two runs of the same length and shape. Plain insertion is faster on a run that is already nearly in order and is quadratic on one that is not: sorting four thousand numbers out of order cost two hundred and sixty million instructions, which is most of a second, and costs a million now.

by

fn by<T>(items: [T], before: fn(T, T) -> bool no.alloc no.host) no.alloc no.host

sort.by(items, sort.ascending): what it is sorted by is the second thing, which is what the name says. It was sort.sort, because a module and its one function had a name between them, and a call read as a stammer.

byWith

fn byWith<T, C>(items: [T], with: C, before: fn(T, T, C) -> bool no.alloc no.host) no.alloc no.host

And the same sort told something beside what it compares: byWith(npcs, player, nearer) where nearer reads the player's position out of what it was handed. This language has no closures and does not want any, so what a predicate would have captured is written beside it, where a reader sees it and where the promise can still be proved. Without this, sorting by a distance from a point meant building an array of pairs and sorting that, which is an allocation and a copy for what is the commonest sort a simulation does. The same walk as by with one more thing passed through it, written out rather than shared. by(items, before) could have been byWith(items, before, callIt) over a two-line adapter, and that is what a language with closures would do -- it costs 15 % of every sort.by in every program, measured, because every comparison goes through a call whose body is not known. Fourteen lines written twice is the cheaper of the two.

ascending

fn ascending<T: orders>(a: T, b: T) -> bool no.alloc no.host deterministic

The usual order for anything that has one. A copy per type it is asked for, checked as that type -- so sort.by(words, sort.ascending) works for text and for every number, and a shape with no order of its own is refused in the copy that was asked for rather than by a rule written here.

descending

fn descending<T: orders>(a: T, b: T) -> bool no.alloc no.host deterministic

std.table

A hash table, written in Kest on a generic struct and generic functions . hash is the one thing the language provides, and it applies exactly where == does, so a copy over a key that does not compare is refused where it is made.

Open addressing, linear probing. The keys and the values sit next to each other in two arrays and slots says where each one is: nought for empty, -1 for a key that was taken out, and otherwise one more than the position. Probing forward means a lookup reads memory in order, which is what a frame budget wants and what a chain of pointers is not.

A struct is a value, so a table handed to a function is a copy of the four handles rather than of what they hold. Everything that changes is therefore behind one of them: slots is emptied and refilled rather than replaced, and how many pairs there are is len(keys) rather than a number beside it. A field written here would be written on the copy.

All four are written own, so the handles are this module's and nothing outside can be handed one. That is what makes the invariants above hold against a program rather than merely against a careful one.

The pairs are walked over keys and values, which are packed and in step: values[i] is what keys[i] is set to, and there are count(t) of both. The order is the order they went in, except where something was taken out and the last pair moved into the hole -- so it is an order, and not one to rely on.

A table is not made smaller when things are taken out of it. Nothing here frees anything: making the slots smaller would make a new array and leave the old one where it is, so shrinking costs memory rather than giving it back. What a table has held, it keeps room for until the heap is thrown away.

EMPTY

const EMPTY: i32 = 0

TAKEN

const TAKEN: i32 = -1

Table

struct Table<K, V> {
    own keys: [K]
    own values: [V]
    own slots: [i32]
    // How many of the slots are marks. It is an array of one rather than a
    // number, for the same reason everything else here is behind a handle: a
    // number written in a function would be written on that function's copy.
    own marks: [i32]
}

What it holds, held in step: the pair at a place is keys[i] beside values[i], and slots says where a key's place is. Every one of them is this module's own, because a handle handed out is a handle written through and two lines of ordinary Kest used to be enough to leave the four of them disagreeing -- pop(t.keys) never wrote a field, it read one and changed what it named. What reaches them from outside is what is declared below.

empty

fn empty<K, V>() -> Table<K, V> no.host deterministic

What a table starts as. table.empty() rather than table.table(), which is the same stammer sort.sort was.

count

fn count<K, V>(t: Table<K, V>) -> i32 no.alloc no.host deterministic

keysOf

fn keysOf<K, V>(t: Table<K, V>) -> [K] no.host deterministic

The keys, copied, for a program that wants them in an order of its own. The pair at a place is keys[i] and values[i], and a sort that moves one and not the other leaves a table that answers about a key with another key's value -- measured, and once no refusal anywhere, because a handle handed out is a handle written through. Now the arrays are this module's own and nothing outside can be handed one, so what a program sorts is this copy because it is the only thing there is to sort. Walking the pairs where they are is keyAt and valueAt, which copy nothing.

keyAt

fn keyAt<K, V>(t: Table<K, V>, at: i32) -> K no.alloc no.host deterministic

The pair at a place, which is where a program walks what a table holds. The places are nought up to count(t) and they are the order the pairs went in, except where something was taken out and the last pair moved into the hole -- so it is an order, and not one to rely on. Asking past the last of them is reading past the end of an array and says so, the way an array does.

These are what find answers for: it gives the place a key is kept at, and nothing outside this module can read the arrays it is a place in. A program that wants the pairs walks them here rather than reaching in, because the fields are the module's own.

valueAt

fn valueAt<K, V>(t: Table<K, V>, at: i32) -> V no.alloc no.host deterministic

slotOf

fn slotOf<K: compares, V>(t: Table<K, V>, key: K) -> i32? no.alloc no.host deterministic

Which slot a key is kept in, or nothing. Everything that looks for a key looks here: find wants the pair, remove wants the slot, and a second probe written for the second of those would be a second place to get the walk past a mark wrong.

find

fn find<K: compares, V>(t: Table<K, V>, key: K) -> i32? no.alloc no.host deterministic

Where a key is, or nothing. Reads only what is already there, so a lookup costs no allocation whatever the table holds.

get

fn get<K: compares, V>(t: Table<K, V>, key: K) -> V? no.alloc no.host deterministic

What is under a key, or nothing. It answers the way the builtin get on a store answers, because it is the same question asked of a different thing: one name, one meaning. A fallback: V beside the key would make a program build a value to throw away, and make a miss read as a hit whose value happened to be that one. The default is orElse beside it.

orElse

fn orElse<K: compares, V>(t: Table<K, V>, key: K, fallback: V) -> V no.alloc no.host deterministic

And what is under a key or this instead, which is a different question and not a second way to ask the first: one asks what is there, and this one says what to use when there is nothing. A program that means "nought when nobody has counted it" says it here rather than at every place it looks.

has

fn has<K: compares, V>(t: Table<K, V>, key: K) -> bool no.alloc no.host deterministic

place

fn place<K: compares, V>(t: Table<K, V>, key: K, where: i32) no.alloc no.host deterministic

Puts a position in the first slot that will hold it, walking past whatever is already there. Nothing is compared: this is only used where the key is known to be new.

refill

fn refill<K: compares, V>(t: Table<K, V>, howMany: i32) no.host deterministic

That much room, and every key put back where its hash says it goes now. The same array is emptied and refilled, because replacing it would replace the copy's handle and leave the caller's table where it was. The marks go with it: what they were for is a probe that has to carry on past a hole, and after this there are no holes to carry on past.

A program that knows how many pairs are coming calls this before it puts them in, and what that saves is all of it: a pair costs 51 bytes of heap in a table that grows into them and nothing at all in one that was told, because room makes the slots, the keys and the values once rather than doubling any of them on the way up. Measured by check-costs.sh, which prints both.

compact

fn compact<K: compares, V>(t: Table<K, V>) -> Table<K, V> no.host deterministic

What a table has held it keeps room for: nothing here frees anything, and making the arrays smaller would make new ones and leave the old where they are. That is the right default -- a table that shrank on every removal would make a frame's cost depend on what that frame took out -- and it is wrong for a world that spikes. A hundred thousand pairs taken down to a thousand still holds 3,244,192 bytes, which is what a hundred thousand need.

So this is the cold path, and it answers a new table rather than making this one smaller: a table is a value and replacing a field of it replaces the copy's handle, which is the reason everything here is behind one. The caller writes by = table.compact(by), the way source = random.next(source) is written. What it costs is both tables at once for as long as the call takes, and the old one is gone at the next walk.

set

fn set<K: compares, V>(t: Table<K, V>, key: K, value: V) no.host deterministic

Setting a key that is there writes over it, so a key appears once.

A table is refilled while more than half of it is spoken for, because probing past a full one is what makes a lookup slow. A mark counts as spoken for: it is not a pair and a probe does not stop at it, so a table that has had things taken out of it can be all marks and no room while holding almost nothing. Twice the room is for the pairs; the same room again is for the marks, and which of the two is asked is which of them is crowding it.

fit

fn fit<K: compares, V>(t: Table<K, V>, key: K, value: V) -> bool no.alloc no.host deterministic

set with the growth taken out, which is what fit is to push: it writes where the key already is, answers false where it is not, and never reaches the heap. Without it a table cannot be written to inside a promise at all -- set may grow, and a promise cannot be kept by hoping it does not -- so a frame keeping a count per thing had to keep it somewhere that was not a table. A program that means "this key is already here" says so here, and one that does not know calls refill before the frame and set inside it and keeps neither promise.

A value that is not equal to itself is refused the way set refuses one, because a key that cannot be found again is a key this cannot write to either.

remove

fn remove<K: compares, V>(t: Table<K, V>, key: K) -> bool no.alloc no.host deterministic

The one taken out leaves a mark rather than a hole, because a hole would end a probe that has to carry on past it. The last pair moves into its place so the two arrays stay packed.

Two slots change and both are found by probing for the key that is kept in them, which is what a hash is for. Walking every slot to find two of them is the whole table for one pair, and a table is walked when it is refilled and not when something is taken out of it.

std.text

What every program that reads text writes for itself. Nothing here is a builtin: it is all written in the language, out of len, find, slice, rest, matches and the bytes.

number

fn number(digits: text) -> i32? no.alloc no.host deterministic

The number a piece of text spells, or nothing when it spells something else. Reads nothing but bytes, so it allocates nothing.

real

fn real(digits: text) -> f32? no.alloc no.host deterministic

The number with a point in it that a piece of text spells, or nothing when it spells something else. What this reads is what this language writes: 12, -0.5 and 3. are numbers, and so are 1.2216752e-06, 1e+20, inf, -inf and nan, because those are what a hole in a string writes for the numbers it cannot spell plainly. A program that writes a number into text and reads it back gets the number it had; it did not, and 1e3 was refused with a rule saying a program that means that can say it another way -- which is not true of a program handed the text by this language. .5, +1.5, a space in front and a second point are still nothing, because nothing here writes those.

Worked out a digit at a time in f64 and narrowed once, which is the same two roundings every other number in this module goes through.

fixed

fn fixed(value: f32, places: i32) -> text no.host deterministic

A number written to a chosen number of places, which is what a line of a file wants: 1.5 and 1.50 are the same number and not the same line. A hole in a string writes the shortest spelling that reads back as the same number, which is the other question and the one a log asks.

Places below nought or above nine are held to that, and a number too big to count in whole parts is written the way a hole would write it, because nothing after the point is worth anything at that size. A number that rounds to nothing is written without a sign in front of it: -0.0 is a number this language has and not a thing anybody wants in a file.

fn right(subject: text, width: i32) -> text no.host deterministic

Pushed to the right of a column that wide, which is where a number belongs: a column of them lines up on the point when every one is written to the same places and the same width.

A width is in bytes, because text is its bytes. hız is four of them and hiz is three, so two lines holding those do not line up and nothing here pretends otherwise: what a program means by a character is the program's to say. Text already that wide comes back as it is, because losing the end of something to fit a column is worse than a column that does not fit.

left

fn left(subject: text, width: i32) -> text no.host deterministic

And to the left of it, which is where a name belongs.

contains

fn contains(subject: text, needle: text) -> bool no.alloc no.host deterministic

starts

fn starts(subject: text, prefix: text) -> bool no.alloc no.host deterministic

ends

fn ends(subject: text, suffix: text) -> bool no.alloc no.host deterministic

charBytes

fn charBytes(first: u8) -> i32 no.alloc no.host deterministic

How many bytes the character starting with this byte is written in, and nought when it is not the start of one. UTF-8 says it in the first byte: under 128 is one byte, and above that the number of ones at the top is the number of bytes. A byte in the middle of a character says nothing about where the character began, which is why this answers nought for one rather than guessing.

charWidth

fn charWidth(piece: text) -> i32 no.alloc no.host deterministic

How many bytes the character at that place takes, which is what its first byte says unless what follows disagrees. A character three bytes wide is three bytes only if the two after it are the middles of one: a byte that begins a character of its own ends the one before it, and a piece of text that stops sooner ends it too. So this is the width to walk by, and charBytes is the question about one byte on its own.

charBack

fn charBack(subject: text, at: i32) -> i32 no.alloc no.host deterministic

Where the character before this place begins, counted in bytes. Walking back is what UTF-8 is for: the middle of a character says so in every one of its bytes, so a walk back is at most three steps and needs nothing kept from the walk forwards. What it lands on has to reach where it started from -- bytes that disagree are a byte on its own, which is what walking forwards makes of them too, so the two walks agree about every place in any text at all.

chars

fn chars(subject: text) -> i32 no.alloc no.host deterministic

How many characters a piece of text holds, which is not how many bytes it holds and is what a program means when it says how long a word is. Bytes that begin no character are counted as one each, because text that is not UTF-8 is still text and a count that stops at the first of them is a count nobody can use.

charsOf

fn charsOf(subject: text) -> [text] no.host deterministic

Every character of it, in order, each as text of its own. Written as a walk with an index rather than as charAt in a loop: charAt counts from the start every time it is asked, so asking it for every character in turn is the whole of the text walked once per character. This is one walk, and it pays for one piece of text each, which is what a program that wants them all is asking for.

charAt

fn charAt(subject: text, index: i32) -> text? no.alloc no.host deterministic

The character at that place, counted in characters rather than in bytes, or nothing when there is no such place. It is text of its own, because a character is not a number here: what a byte means is a program's to say, and what a character means is Unicode's.

isSpace

fn isSpace(byte: u8) -> bool no.alloc no.host deterministic

trim

fn trim(subject: text) -> text no.alloc no.host deterministic

Without the spaces at either end. Cuts, so it reaches the heap.

split

fn split(subject: text, separator: text) -> [text] no.host deterministic

The pieces between each separator: one more of them than there are separators, always. That is what makes this and join the two halves of one thing -- join(split(s, sep), sep) is s for every s -- and it is the rule that says what the ends are: an empty subject is one empty piece, and a subject that ends with a separator has an empty piece after it. Stopping where the text ran out instead lost the last piece whenever it was the empty one, so split("a,b,", ",") was two pieces and joining them back gave a,b.

bytes

fn bytes(subject: text) -> [u8] no.host deterministic

Text is its bytes, so a run of bytes is what it is built from. Gathering them costs nothing and text(bytes) pays for the copy once, which is what a loop that builds a string a piece at a time needs.

append

fn append(out: [u8], piece: text) no.host deterministic

fitting

fn fitting(out: [u8], piece: text) -> bool no.alloc no.host deterministic

The same, into a buffer that was given room, which is what a frame writing a line every tick wants: push may double the block and so can never be inside a promise to reach no heap, and this cannot. It answers whether the piece fitted, and a piece that did not is a buffer that has not changed at all -- all of it goes in or none of it does, because a piece half written is a piece nobody can take back. It used to go in a byte at a time and leave what fitted, which is what a loop can do and a move cannot.

fittingNumber

fn fittingNumber(out: [u8], value: i32) -> bool no.alloc no.host deterministic

And a whole number written into one the same way, most significant digit first, without making any text on the way. It is the piece a tally line is mostly made of.

join

fn join(pieces: [text], separator: text) -> text no.host deterministic

The pieces with the separator between them, which is what split undoes. Measured first and given its room once, so the bytes are moved once rather than again every time the run doubles, and each piece is pushed where it is rather than through a call.

repeat

fn repeat(subject: text, times: i32) -> text no.host deterministic

upper

fn upper(subject: text) -> text no.host deterministic

ASCII only, and deliberately: a byte is a byte, and what an upper case ı is depends on a language rather than on a table this could carry.

lower

fn lower(subject: text) -> text no.host deterministic

std.vec

What is built on the language's vec2, vec3 and vec4. The arithmetic is the language's -- a + b, v * k -- a component at a time; what is here is what is made of it: a dot product, a length, a distance, a direction. Every one is one of those operations after another, so it answers the same bits in the machine and in a release, and each that asks nothing of the host is deterministic.

dot

fn dot(a: vec2, b: vec2) -> f32 no.alloc no.host deterministic

dot

fn dot(a: vec3, b: vec3) -> f32 no.alloc no.host deterministic

dot

fn dot(a: vec4, b: vec4) -> f32 no.alloc no.host deterministic

lengthSquared

fn lengthSquared(v: vec2) -> f32 no.alloc no.host deterministic

The one that costs nothing, which is the one a frame budget reaches for: comparing two of these orders the same way comparing their lengths does, without the square root. What it hands back is a square, so it runs off the end of the width where a square does -- the square of 1e20 is infinity -- and comparing works up to the size where the squares stop fitting. length is the one that answers for every vector this width holds an answer for.

lengthSquared

fn lengthSquared(v: vec3) -> f32 no.alloc no.host deterministic

lengthSquared

fn lengthSquared(v: vec4) -> f32 no.alloc no.host deterministic

length

fn length(v: vec2) -> f32 no.alloc deterministic

The square root is the host's, declared by std.math and provided by whatever the program runs against.

Divided by its largest component first, the way direction is and for the same reason: the square of a vector runs off the end of an f32 long before the vector does. The length of (1e20, 1e20) is 1.41e20, which this width holds perfectly well, and squaring first answered with infinity; at the other end the length of (1e-21, 1e-21) came back a part in five thousand out, because the squares landed among the numbers an f32 keeps badly.

A largest component that is not a number is the answer itself: the length of a vector with an infinite component is infinite, and dividing by it would make that nothing rather than everything.

length

fn length(v: vec3) -> f32 no.alloc deterministic

length

fn length(v: vec4) -> f32 no.alloc deterministic

distanceSquared

fn distanceSquared(a: vec2, b: vec2) -> f32 no.alloc no.host deterministic

And the one that costs nothing between two points, which is the question a frame actually asks: what is nearest, and is it inside a radius. Both of those order the same way without the square root, and lengthSquared was here for that reason while the two-point form of it was not.

distanceSquared

fn distanceSquared(a: vec3, b: vec3) -> f32 no.alloc no.host deterministic

distanceSquared

fn distanceSquared(a: vec4, b: vec4) -> f32 no.alloc no.host deterministic

distance

fn distance(a: vec2, b: vec2) -> f32 no.alloc deterministic

distance

fn distance(a: vec3, b: vec3) -> f32 no.alloc deterministic

distance

fn distance(a: vec4, b: vec4) -> f32 no.alloc deterministic

direction

fn direction(v: vec2) -> vec2? no.alloc deterministic

A vector of nought length points nowhere, and there is no direction to give back. That is a question with no answer rather than a zero to return quietly, so it is an optional.

Divided by its largest component before anything is squared, because squaring is where a direction gets lost: a vector of 1e20s squares to infinity and came back as (0, 0), which is not a direction and does not say so, and a vector of 1e-21s squares into what an f32 keeps badly and came back a thousandth short of a length of one. Dividing first puts every component between -1 and 1 with one of them at 1, so what is squared is between 1 and 3 whatever it was handed.

direction

fn direction(v: vec3) -> vec3? no.alloc deterministic

direction

fn direction(v: vec4) -> vec4? no.alloc deterministic

lerp

fn lerp(a: vec2, b: vec2, amount: f32) -> vec2 no.alloc no.host deterministic

lerp

fn lerp(a: vec3, b: vec3, amount: f32) -> vec3 no.alloc no.host deterministic

lerp

fn lerp(a: vec4, b: vec4, amount: f32) -> vec4 no.alloc no.host deterministic

perpendicular

fn perpendicular(v: vec2) -> vec2 no.alloc no.host deterministic

Perpendicular in two, and the one perpendicular to both in three.

cross

fn cross(a: vec3, b: vec3) -> vec3 no.alloc no.host deterministic

cross

fn cross(a: vec2, b: vec2) -> f32 no.alloc no.host deterministic

A cross in two dimensions is not a vector: what is perpendicular to both is out of the plane, and the whole of it is the one number saying how far. That number is the z the three-dimensional one gives back, and what a program reads off it is which side of a the vector b is on -- positive one way, negative the other, and nought when they lie along each other. It is dot with one of them turned a quarter, which is the other thing this module gives for two dimensions and did not put together.