Every module of lib/std, as kest doc prints it: each declaration as it is written, with the comment above it.
std.bytesNumbers, 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.
putU8fn putU8(out: [u8], value: u8) no.host deterministic
putBoolfn putBool(out: [u8], value: bool) no.host deterministic
putU32fn 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.
putI32fn putI32(out: [u8], value: i32) no.host deterministic
putU64fn putU64(out: [u8], value: u64) no.host deterministic
putI64fn putI64(out: [u8], value: i64) no.host deterministic
putF32fn putF32(out: [u8], value: f32) no.host deterministic
putF64fn putF64(out: [u8], value: f64) no.host deterministic
putTextfn putText(out: [u8], value: text) no.host deterministic
putBytesfn putBytes(out: [u8], values: [u8]) no.host deterministic
putI32sfn putI32s(out: [u8], values: [i32]) no.host deterministic
putF32sfn putF32s(out: [u8], values: [f32]) no.host deterministic
Readerstruct 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.
readerfn reader(bytes: [u8]) -> Reader no.host deterministic
shortfn short(r: Reader) -> bool no.alloc no.host deterministic
Whether a read has run past the end.
leftfn left(r: Reader) -> i32 no.alloc no.host deterministic
How many bytes there are still to read.
takefn 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.
wordfn word(from: [u8], at: i32) -> u32 no.alloc no.host deterministic
The four bytes at at, low first, as one number.
readU8fn readU8(r: Reader) -> u8 no.alloc no.host deterministic
readBoolfn readBool(r: Reader) -> bool no.alloc no.host deterministic
readU32fn readU32(r: Reader) -> u32 no.alloc no.host deterministic
readI32fn readI32(r: Reader) -> i32 no.alloc no.host deterministic
readU64fn readU64(r: Reader) -> u64 no.alloc no.host deterministic
readI64fn readI64(r: Reader) -> i64 no.alloc no.host deterministic
readF32fn readF32(r: Reader) -> f32 no.alloc no.host deterministic
readF64fn readF64(r: Reader) -> f64 no.alloc no.host deterministic
readBytesfn readBytes(r: Reader) -> [u8] no.host deterministic
readI32sfn readI32s(r: Reader) -> [i32] no.host deterministic
readF32sfn readF32s(r: Reader) -> [f32] no.host deterministic
readTextfn 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.fdlibmThe 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.
highWordfn 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.
lowWordfn lowWord(value: f64) -> u32 no.alloc no.host deterministic
lowClearedfn 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.
withHighWordfn withHighWord(value: f64, word: i32) -> f64 no.alloc no.host deterministic
negatedfn 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.
magnitudefn magnitude(value: f64) -> f64 no.alloc no.host deterministic
The same number without its sign, nought included.
S1const S1: f64 = float(u64(0xBFC5555555555549))
S2const S2: f64 = float(u64(0x3F8111111110F8A6))
S3const S3: f64 = float(u64(0xBF2A01A019C161D5))
S4const S4: f64 = float(u64(0x3EC71DE357B1FE7D))
S5const S5: f64 = float(u64(0xBE5AE5E68A2B9CEB))
S6const S6: f64 = float(u64(0x3DE5D93A5ACFD57C))
C1const C1: f64 = float(u64(0x3FA555555555554C))
C2const C2: f64 = float(u64(0xBF56C16C16C15177))
C3const C3: f64 = float(u64(0x3EFA01A019CB1590))
C4const C4: f64 = float(u64(0xBE927E4F809C52AD))
C5const C5: f64 = float(u64(0x3E21EE9EBDB4B1C4))
C6const C6: f64 = float(u64(0xBDA8FAE9BE8838D4))
TO_INTconst TO_INT: f64 = float(u64(0x4338000000000000))
INV_PIO2const INV_PIO2: f64 = float(u64(0x3FE45F306DC9C883))
PIO2_1const PIO2_1: f64 = float(u64(0x3FF921FB54400000))
PIO2_2const PIO2_2: f64 = float(u64(0x3DD0B4611A600000))
PIO2_2Tconst PIO2_2T: f64 = float(u64(0x3BA3198A2E037073))
PIO2_3const PIO2_3: f64 = float(u64(0x3BA3198A2E000000))
PIO2_3Tconst PIO2_3T: f64 = float(u64(0x397B839A252049C1))
sinKernelfn sinKernel(x: f64, y: f64, withTail: bool) -> f64 no.alloc no.host deterministic
cosKernelfn cosKernel(x: f64, y: f64) -> f64 no.alloc no.host deterministic
Reducedstruct 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.
reducedfn reduced(x: f64) -> Reduced no.alloc no.host deterministic
sinfn sin(value: f64) -> f64 no.alloc no.host deterministic
cosfn cos(value: f64) -> f64 no.alloc no.host deterministic
AT0const AT0: f64 = float(u64(0x3FD555555555550D))
AT1const AT1: f64 = float(u64(0xBFC999999998EBC4))
AT2const AT2: f64 = float(u64(0x3FC24924920083FF))
AT3const AT3: f64 = float(u64(0xBFBC71C6FE231671))
AT4const AT4: f64 = float(u64(0x3FB745CDC54C206E))
AT5const AT5: f64 = float(u64(0xBFB3B0F2AF749A6D))
AT6const AT6: f64 = float(u64(0x3FB10D66A0D03D51))
AT7const AT7: f64 = float(u64(0xBFADDE2D52DEFD9A))
AT8const AT8: f64 = float(u64(0x3FA97B4B24760DEB))
AT9const AT9: f64 = float(u64(0xBFA2B4442C6A6C2F))
AT10const AT10: f64 = float(u64(0x3F90AD3AE322DA11))
ATAN_HI0const ATAN_HI0: f64 = float(u64(0x3FDDAC670561BB4F))
ATAN_HI1const ATAN_HI1: f64 = float(u64(0x3FE921FB54442D18))
ATAN_HI2const ATAN_HI2: f64 = float(u64(0x3FEF730BD281F69B))
ATAN_HI3const ATAN_HI3: f64 = float(u64(0x3FF921FB54442D18))
ATAN_LO0const ATAN_LO0: f64 = float(u64(0x3C7A2B7F222F65E2))
ATAN_LO1const ATAN_LO1: f64 = float(u64(0x3C81A62633145C07))
ATAN_LO2const ATAN_LO2: f64 = float(u64(0x3C7007887AF0CBBD))
ATAN_LO3const ATAN_LO3: f64 = float(u64(0x3C91A62633145C07))
atanfn atan(value: f64) -> f64 no.alloc no.host deterministic
PIconst PI: f64 = float(u64(0x400921FB54442D18))
PI_LOconst PI_LO: f64 = float(u64(0x3CA1A62633145C07))
atan2fn 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_H1const DP_H1: f64 = float(u64(0x3FE2B80340000000))
DP_L1const DP_L1: f64 = float(u64(0x3E4CFDEB43CFD006))
TWO53const TWO53: f64 = float(u64(0x4340000000000000))
HUGEconst HUGE: f64 = float(u64(0x7E37E43C8800759C))
TINYconst TINY: f64 = float(u64(0x01A56E1FC2F8F359))
L1const L1: f64 = float(u64(0x3FE3333333333303))
L2const L2: f64 = float(u64(0x3FDB6DB6DB6FABFF))
L3const L3: f64 = float(u64(0x3FD55555518F264D))
L4const L4: f64 = float(u64(0x3FD17460A91D4101))
L5const L5: f64 = float(u64(0x3FCD864A93C9DB65))
L6const L6: f64 = float(u64(0x3FCA7E284A454EEF))
P1const P1: f64 = float(u64(0x3FC555555555553E))
P2const P2: f64 = float(u64(0xBF66C16C16BEBD93))
P3const P3: f64 = float(u64(0x3F11566AAF25DE2C))
P4const P4: f64 = float(u64(0xBEBBBD41C5D26BF1))
P5const P5: f64 = float(u64(0x3E66376972BEA4D0))
LG2const LG2: f64 = float(u64(0x3FE62E42FEFA39EF))
LG2_Hconst LG2_H: f64 = float(u64(0x3FE62E4300000000))
LG2_Lconst LG2_L: f64 = float(u64(0xBE205C610CA86C39))
OVTconst OVT: f64 = float(u64(0x3C971547652B82FE))
CPconst CP: f64 = float(u64(0x3FEEC709DC3A03FD))
CP_Hconst CP_H: f64 = float(u64(0x3FEEC709E0000000))
CP_Lconst CP_L: f64 = float(u64(0xBE3E2FE0145B01F5))
IVLN2const IVLN2: f64 = float(u64(0x3FF71547652B82FE))
IVLN2_Hconst IVLN2_H: f64 = float(u64(0x3FF7154760000000))
IVLN2_Lconst IVLN2_L: f64 = float(u64(0x3E54AE0BF85DDF44))
THIRDconst THIRD: f64 = float(u64(0x3FD5555555555555))
powfn pow(x: f64, y: f64) -> f64 no.alloc no.host deterministic
std.hashhash(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.
startconst 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.
joinfn 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.ioSaying 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.writeextern 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.
writefn write(value: text) no.alloc
printfn print(value: text) no.alloc
std.mathTwo 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.sqrtextern fn Math.sqrt(value: f64) -> f64 no.alloc deterministic
Math.floorextern fn Math.floor(value: f64) -> f64 no.alloc deterministic
Math.ceilextern fn Math.ceil(value: f64) -> f64 no.alloc deterministic
minfn min(a: i32, b: i32) -> i32 no.alloc no.host deterministic
minfn min(a: i64, b: i64) -> i64 no.alloc no.host deterministic
minfn min(a: f32, b: f32) -> f32 no.alloc no.host deterministic
minfn min(a: f64, b: f64) -> f64 no.alloc no.host deterministic
maxfn max(a: i32, b: i32) -> i32 no.alloc no.host deterministic
maxfn max(a: i64, b: i64) -> i64 no.alloc no.host deterministic
maxfn max(a: f32, b: f32) -> f32 no.alloc no.host deterministic
maxfn max(a: f64, b: f64) -> f64 no.alloc no.host deterministic
absfn 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.
absfn abs(value: i64) -> i64 no.alloc no.host deterministic
absfn abs(value: f32) -> f32 no.alloc no.host deterministic
absfn abs(value: f64) -> f64 no.alloc no.host deterministic
clampfn clamp(value: i32, low: i32, high: i32) -> i32 no.alloc no.host deterministic
clampfn clamp(value: i64, low: i64, high: i64) -> i64 no.alloc no.host deterministic
clampfn clamp(value: f32, low: f32, high: f32) -> f32 no.alloc no.host deterministic
clampfn clamp(value: f64, low: f64, high: f64) -> f64 no.alloc no.host deterministic
isNumberfn 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.
isNumberfn isNumber(value: f32) -> bool no.alloc no.host deterministic
sqrtfn 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.
sqrtfn sqrt(value: f32) -> f32 no.alloc deterministic
floorfn floor(value: f64) -> f64 no.alloc deterministic
floorfn floor(value: f32) -> f32 no.alloc deterministic
ceilfn ceil(value: f64) -> f64 no.alloc deterministic
ceilfn ceil(value: f32) -> f32 no.alloc deterministic
sinfn 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.
cosfn cos(value: f64) -> f64 no.alloc no.host deterministic
powfn pow(base: f64, exponent: f64) -> f64 no.alloc no.host deterministic
atan2fn 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.
atanfn atan(value: f64) -> f64 no.alloc no.host deterministic
The angle whose tangent is value, between -pi/2 and pi/2.
atanfn atan(value: f32) -> f32 no.alloc no.host deterministic
sinfn sin(value: f32) -> f32 no.alloc no.host deterministic
cosfn cos(value: f32) -> f32 no.alloc no.host deterministic
powfn pow(base: f32, exponent: f32) -> f32 no.alloc no.host deterministic
atan2fn atan2(y: f32, x: f32) -> f32 no.alloc no.host deterministic
tanfn tan(value: f64) -> f64 no.alloc no.host deterministic
A tangent is a sine over a cosine.
tanfn tan(value: f32) -> f32 no.alloc no.host deterministic
asinfn 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.
acosfn acos(value: f64) -> f64? no.alloc deterministic
asinfn 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.
acosfn acos(value: f32) -> f32? no.alloc deterministic
roundfn 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.
roundfn round(value: f32) -> f32 no.alloc deterministic
signfn 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.
signfn 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.
lerpfn lerp(from: f32, to: f32, amount: f32) -> f32 no.alloc no.host deterministic
lerpfn lerp(from: f64, to: f64, amount: f64) -> f64 no.alloc no.host deterministic
std.osWhat 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.fileReadextern 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.fileWriteextern 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.fileExistsextern 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.argCountextern 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.argextern fn Host.arg(at: i32) -> text?
Host.clockextern 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.
readfn read(path: text) -> text?
writefn write(path: text, bytes: text) -> bool
existsfn exists(path: text) -> bool
argCountfn argCount() -> i32 no.alloc
argfn arg(at: i32) -> text?
argsfn 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.
nowfn now() -> i64 no.alloc
Microseconds since the clock's own start. now() - before is the only thing
to do with two of these.
millisBetweenfn 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.randomNumbers 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.
Sourcestruct Source {
state: u64
}
fromfn 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.
nextfn next(s: Source) -> Source no.alloc no.host deterministic
numberfn 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.
belowfn 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.
betweenfn 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.
fractionfn 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.
chancefn chance(s: Source, hundredths: i32) -> bool no.alloc no.host deterministic
True about that many times in a hundred.
onefn 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.
shufflefn 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.sortSorting 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.
byfn 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.
byWithfn 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.
ascendingfn 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.
descendingfn descending<T: orders>(a: T, b: T) -> bool no.alloc no.host deterministic
std.tableA 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.
EMPTYconst EMPTY: i32 = 0
TAKENconst TAKEN: i32 = -1
Tablestruct 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.
emptyfn 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.
countfn count<K, V>(t: Table<K, V>) -> i32 no.alloc no.host deterministic
keysOffn 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.
keyAtfn 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.
valueAtfn valueAt<K, V>(t: Table<K, V>, at: i32) -> V no.alloc no.host deterministic
slotOffn 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.
findfn 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.
getfn 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.
orElsefn 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.
hasfn has<K: compares, V>(t: Table<K, V>, key: K) -> bool no.alloc no.host deterministic
placefn 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.
refillfn 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.
compactfn 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.
setfn 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.
fitfn 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.
removefn 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.textWhat 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.
numberfn 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.
realfn 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.
fixedfn 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.
rightfn 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.
leftfn left(subject: text, width: i32) -> text no.host deterministic
And to the left of it, which is where a name belongs.
containsfn contains(subject: text, needle: text) -> bool no.alloc no.host deterministic
startsfn starts(subject: text, prefix: text) -> bool no.alloc no.host deterministic
endsfn ends(subject: text, suffix: text) -> bool no.alloc no.host deterministic
charBytesfn 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.
charWidthfn 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.
charBackfn 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.
charsfn 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.
charsOffn 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.
charAtfn 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.
isSpacefn isSpace(byte: u8) -> bool no.alloc no.host deterministic
trimfn trim(subject: text) -> text no.alloc no.host deterministic
Without the spaces at either end. Cuts, so it reaches the heap.
splitfn 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.
bytesfn 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.
appendfn append(out: [u8], piece: text) no.host deterministic
fittingfn 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.
fittingNumberfn 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.
joinfn 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.
repeatfn repeat(subject: text, times: i32) -> text no.host deterministic
upperfn 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.
lowerfn lower(subject: text) -> text no.host deterministic
std.vecWhat 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.
dotfn dot(a: vec2, b: vec2) -> f32 no.alloc no.host deterministic
dotfn dot(a: vec3, b: vec3) -> f32 no.alloc no.host deterministic
dotfn dot(a: vec4, b: vec4) -> f32 no.alloc no.host deterministic
lengthSquaredfn 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.
lengthSquaredfn lengthSquared(v: vec3) -> f32 no.alloc no.host deterministic
lengthSquaredfn lengthSquared(v: vec4) -> f32 no.alloc no.host deterministic
lengthfn 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.
lengthfn length(v: vec3) -> f32 no.alloc deterministic
lengthfn length(v: vec4) -> f32 no.alloc deterministic
distanceSquaredfn 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.
distanceSquaredfn distanceSquared(a: vec3, b: vec3) -> f32 no.alloc no.host deterministic
distanceSquaredfn distanceSquared(a: vec4, b: vec4) -> f32 no.alloc no.host deterministic
distancefn distance(a: vec2, b: vec2) -> f32 no.alloc deterministic
distancefn distance(a: vec3, b: vec3) -> f32 no.alloc deterministic
distancefn distance(a: vec4, b: vec4) -> f32 no.alloc deterministic
directionfn 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.
directionfn direction(v: vec3) -> vec3? no.alloc deterministic
directionfn direction(v: vec4) -> vec4? no.alloc deterministic
lerpfn lerp(a: vec2, b: vec2, amount: f32) -> vec2 no.alloc no.host deterministic
lerpfn lerp(a: vec3, b: vec3, amount: f32) -> vec3 no.alloc no.host deterministic
lerpfn lerp(a: vec4, b: vec4, amount: f32) -> vec4 no.alloc no.host deterministic
perpendicularfn perpendicular(v: vec2) -> vec2 no.alloc no.host deterministic
Perpendicular in two, and the one perpendicular to both in three.
crossfn cross(a: vec3, b: vec3) -> vec3 no.alloc no.host deterministic
crossfn 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.