Builtins

The names a program may not declare, because the language already means something by them. The list comes from is_reserved_name in src/rust-compiler/typeck.rs; every signature below was compiled while this page was generated, so none of them is a signature the compiler would reject.

Call Answers Allocates? Reaches
print_error nothing no —
print_exact nothing no —
bit_and Int no —
bit_or Int no —
bit_xor Int no —
bit_not Int no —
shift_left Int no —
shift_right_zeros Int no —
shift_right_sign Int no —
c_is_null Bool no —
c_string_at String yes —
c_bytes_at [Int] yes —
c_bytes_to Int — how many it wrote no —
print nothing no —
len how many elements, or how many BYTES of a String no —
byte_at the byte at i no —
handle_of a Handle<T> naming value, for a host to carry between calls yes —
handle_value the value the handle names no —
byte_as_string a one-byte String holding n yes —
substring count bytes of s, starting at from yes —
to_string the value, written out yes —
push the new length yes —
truncate the new length no —
read_file the whole file yes touches files
write_file how many bytes went out no touches files
write_bytes how many bytes went out no touches files
argument the nth command-line argument no touches input
argument_count how many arguments there are no touches input
divide_floor a / b, rounded toward negative infinity no —
divide_toward_zero a / b, rounded toward zero no —
remainder what is left over no —
hash a non-negative hash no —
old what the expression was BEFORE the body ran no —
result the value being returned no —
exit nothing — the program ends no —
main nothing — it is not an entry point no —

Allocates means the call builds something, and a value has to be built somewhere. It does not mean you write region. Since v0.0.146 a program has one from the moment it starts and allocates is inferred, so every signature on this page compiles with no region in sight — the probes that verified them have none. You reach for region to release EARLY, around a loop body or a request, which is the whole of keeping a long-running program’s memory flat. See Memory.

print_error(value)

Writes one value and a newline to standard error. The same statement as print with a different destination, so the per-type formatting cannot fork — two statements would mean two formatters, and the first time one learned about a new type the other would print something else.

Answers nothing. Allocates: no.

print_exact(value)

Writes one value to standard output and appends nothing — no newline. The same statement as print and print_error, so the per-type formatting cannot fork; the trailing newline is the only difference.

It exists for output whose length is declared before its bytes. Language Server Protocol framing is Content-Length: N, a blank line, and then exactly N bytes; so is CGI, and so is any protocol a program speaks down its own stdout. print cannot write that, and the routes that look like they can are a different stream: file_write("/dev/stdout", s) and write_bytes("/dev/stdout", b) come out reordered against print, and both truncate when stdout is a regular file rather than a pipe. An editor hands a language server a pipe, so that shape passes its own tests and destroys the output the moment anyone redirects it.

Interpolation works, which is usually how a header is written: print_exact("Content-Length: {n}\r\n\r\n").

One limit, measured rather than assumed. A String may hold a zero byte — len(from_bytes([65, 0, 66])) is 3 — and printing one stops at that byte, in print_exact exactly as in print, because both share the one formatter. So a declared length and a written length can disagree for text built out of arbitrary bytes. JSON cannot contain a raw zero byte, so protocol framing is unaffected; for anything that can, write the bytes with write_bytes rather than declaring a length you did not write. See tests/limitations/.

Answers nothing. Allocates: no.

bit_and

bit_and(a: Int, b: Int)

Bitwise AND. Named rather than an operator, because a & b == c means a & (b == c) in C — a precedence table a reviewer has to remember is the opposite of what this language is for.

Answers Int. Allocates: no.

bit_or

bit_or(a: Int, b: Int)

Bitwise OR.

Answers Int. Allocates: no.

bit_xor

bit_xor(a: Int, b: Int)

Bitwise XOR.

Answers Int. Allocates: no.

bit_not

bit_not(a: Int)

Every bit flipped. bit_not(0) is -1, because an Int is signed and there is nowhere else for the top bit to go.

Answers Int. Allocates: no.

shift_left

shift_left(x: Int, n: Int)

Shifts left by n, which must be 0 to 63. Bits shifted past the top are discarded — the one place in this language where losing information is not an error, because it is what a shift is for. So it is not x * 2^n: multiplication traps on overflow and this does not.

Answers Int. Allocates: no.

shift_right_zeros

shift_right_zeros(x: Int, n: Int)

Shifts right by n, filling with zeros — a logical shift. shift_right_zeros(-1, 63) is 1. Two right shifts exist because on a negative value zero-fill and sign-fill give different answers, and one symbol cannot say which.

Answers Int. Allocates: no.

shift_right_sign

shift_right_sign(x: Int, n: Int)

Shifts right by n, copying the sign bit — an arithmetic shift. shift_right_sign(-1, 63) is -1, and shift_right_sign(x, n) equals divide_floor(x, 2^n).

Answers Int. Allocates: no.

c_is_null

c_is_null(p: CPointer)

Did the C call fail? One of only two things that may be done with a CPointer. There is no == on one, no arithmetic and no printing — a pointer is a token to hand back to C, not a value to reason about.

Answers Bool. Allocates: no.

c_string_at

c_string_at(p: CPointer)

Copies NUL-terminated bytes from C into a Burxt String. The copy is the wall: afterwards Burxt owns the bytes and the pointer is not kept, so who frees it stops being a question. A null pointer dies here rather than answering "" — unset and empty are different facts.

Answers String. Allocates: yes.

c_bytes_at

c_bytes_at(p: CPointer, n: Int)

Copies n bytes from C into a growable array, one byte per element, zero-extended so 0xFF arrives as 255 rather than -1. The counterpart to c_string_at, and it is what makes OS entropy reachable: /dev/urandom is a character device, so read_file measures it and gets nothing. Where the length comes from is the pointer wall’s one soft edge — n is your claim, and nothing in the type can check it. A null pointer and a negative count are refused.

Answers [Int]. Allocates: yes.

c_bytes_to

c_bytes_to(p: CPointer, bytes: [Int])

Writes an array of bytes into memory C owns, the mirror of c_bytes_at. It is what lets a program hand C a struct: bind wants a sockaddr_in, nanosleep wants a timespec, setsockopt wants a flag — all of them small, all of them by pointer, and until this existed Burxt could read C’s memory and never write it. The length is not a claim here, it is len(bytes); what stays yours is whether the destination is big enough, which belongs to C. A null pointer is refused, and a number outside 0..=255 exits 70 rather than truncating — 256 quietly becoming 0 is a corrupt port or a corrupt length prefix.

Answers Int — how many it wrote. Allocates: no.

print

print(value)

Writes one value and a newline. Takes an Int, a Bool, a String or a Decimal<S>, and an interpolated string goes out piece by piece with nothing built — which is why printing costs no memory.

Answers nothing. Allocates: no.

len

len(xs) -> Int

On a String this counts bytes, not characters. That is deliberate and it is the same decision byte_at makes: the byte-versus-character question is one a program has to answer, and a name that hid it would answer it wrongly for somebody.

Answers how many elements, or how many BYTES of a String. Allocates: no.

byte_at

byte_at(s, i) -> Int

Bounds are always checked. The name says BYTE so that nothing has to guess whether an index into text means a byte or a character.

Answers the byte at i. Allocates: no.

handle_of

handle_of(value) -> Handle<T>

A value a host can hold while Burxt is not running. A browser calls in on every click, and between two calls there is nowhere for the application’s state to be — so it crosses as text, comes back as Json, and something hand-validates it into a class. That surrenders the checking property exactly where a program works hardest.

handle_of files the value and answers an integer the host keeps. It is an index into a table, never an address: a pointer handed out is a pointer a host can invent, and a wrong one is type confusion with no diagnostic. The table costs a slot and a lookup, and buys three named refusals instead.

The value is COPIED into the region, because what outlives a call cannot live in a stack slot. Takes a class — the state a host carries — since a scalar has no place to point at.

Answers a Handle<T> naming value, for a host to carry between calls. Allocates: yes.

handle_value

handle_value(handle) -> T

The other half of handle_of. Three failures, three messages, because a check that cannot tell them apart sends the reader to the wrong one:

  • never issued by this module — the integer did not come from a call in here;
  • replaced by a later call — it did, and then something newer superseded it. The message names the generation the handle carried and the generation that is live, since the fix is to keep the handle the last call answered with;
  • names a value of a different type, or came from a different module.

The second is the one an index alone cannot catch, and the one that actually happens: a slot stays live across an update, so an index check passes and the host reads the wrong value silently. That is the use-after-free this exists to refuse.

Answers the value the handle names. Allocates: no.

byte_as_string

byte_as_string(n) -> String

The exact inverse of byte_at: byte_at(byte_as_string(n), 0) is n for every one of the 256 values. n must be 0 to 255 — a literal outside that is refused when the program is compiled, and anything computed is checked when it runs.

It is the ONLY way to turn a number into text, and the reason it had to be a builtin rather than a library function: substring of a literal was the only Int-to-String path there was, and a source file must be valid UTF-8, so a byte above 127 could only be written down inside a complete multi-byte character. to_string(233) is a different conversion — three digit characters, "233".

It is also the one builtin that can build a String is_valid_utf8 rejects. byte_as_string(0xC3) on its own is a UTF-8 lead byte with no continuation after it. That is what it is FOR — assembling a sequence one byte at a time — but it means the caller owns the validity of what comes out. For text, use from_codepoint in lib/string.bx, which emits a whole character or refuses.

A zero byte is ORDINARY, not a terminator: a Burxt String carries its length in a header, so byte_as_string(0) has length 1 and the full 0..255 range needs no special case.

Answers a one-byte String holding n. Allocates: yes.

It refuses this:

print(byte_as_string(256));
error: `byte_as_string(256)` has no answer: a byte is 0 to 255. A codepoint above 255 is more than one byte — `from_codepoint` in lib/string.bx encodes it
 --> byte_as_string.bx:1:7
  |
1 | print(byte_as_string(256));
  |       ^^^^^^^^^^^^^^^^^^^

substring

substring(s, from, count) -> String

Builds a new String, so it needs somewhere to put it.

Answers count bytes of s, starting at from. Allocates: yes.

to_string

to_string(value) -> String

Takes an Int, a Bool or a Decimal<S>. It shares its formatter with print, so the two can never disagree about what a number looks like. A Bool allocates nothing, because both answers are constants.

There is no way for a class of yours to have one: to_string is a builtin rather than an interface a type can implement, so a user type has no display form. That is a real gap rather than a decision.

Answers the value, written out. Allocates: yes.

It refuses this:

let s: String = "already text";
print(to_string(s));
error: to_string(...) on a String would just copy it — use the value directly.
 --> to_string.bx:2:7
  |
2 | print(to_string(s));
  |       ^^^^^^^^^^^^

push

push(xs, value) -> Int

Appends to a growable array. The array lives in a region, which is what makes growing it a bump rather than a reallocation someone has to own.

Answers the new length. Allocates: yes.

truncate

truncate(xs, n) -> Int

Shortens a growable array to n. Nothing is freed — the region owns the storage, and it goes when the region does.

Answers the new length. Allocates: no.

read_file

read_file(path) -> String

Reaches the filesystem, so it is registered as touches files — a function that calls it must say so, and one that does not may not call it.

Answers the whole file. Allocates: yes. Carries touches files, so a caller must declare it.

write_file

write_file(path, contents) -> Int

Replaces the file. Reaches the filesystem, so it carries touches files.

Answers how many bytes went out. Allocates: no. Carries touches files, so a caller must declare it.

write_bytes

write_bytes(path, buffer) -> Int

Writes a growable [Int] where each element is one byte. For output that is not text — and a value outside 0–255 is a refusal rather than a truncation.

Answers how many bytes went out. Allocates: no. Carries touches files, so a caller must declare it.

argument

argument(n) -> String

Reads the command line, which is touches input.

Answers the nth command-line argument. Allocates: no. Carries touches input, so a caller must declare it.

argument_count

argument_count() -> Int

Reads the command line, which is touches input.

Answers how many arguments there are. Allocates: no. Carries touches input, so a caller must declare it.

divide_floor

divide_floor(a, b) -> Int

Int / Int is refused, because the two reasonable answers for a negative numerator disagree and an operator cannot ask which you meant. This is one of them; divide_toward_zero is the other. Dividing by zero stops the program.

Answers a / b, rounded toward negative infinity. Allocates: no.

divide_toward_zero

divide_toward_zero(a, b) -> Int

What C and most CPUs do. The other half of the pair Int / Int refuses to guess between.

Answers a / b, rounded toward zero. Allocates: no.

remainder

remainder(a, b) -> Int

Keeps the sign of its LEFT operand, like C’s %. Worth knowing when the result indexes something: a negative index is a bounds failure, not a wrong answer.

Answers what is left over. Allocates: no.

hash

hash(key) -> Int

Defined on the Equatable types — Int, Bool, String, Decimal<S> — which is exactly the set == works on. The sign bit is cleared, so it can index a table through remainder without producing a negative index. This is the only compiler support lib/map.bx needs.

Answers a non-negative hash. Allocates: no.

old

old(expression)

Legal only inside an ensures clause, where it is what lets a postcondition talk about change rather than only about the answer.

One limit worth knowing, and it is the compiler’s rather than the design’s: an ensures on a method that returns a class is refused today. A class travels back through a hidden pointer into the caller’s storage, and binding result to that needs care a scalar does not. Return a scalar, or drop the clause.

Answers what the expression was BEFORE the body ran. Allocates: no.

It refuses this:

class Counter {
    n: Int,

    function (self) bumped() -> Counter
        ensures result.n == old(self.n) + 1
    { return Counter { n: self.n + 1 }; }
}
error: `ensures` on `Counter.bumped` is not supported yet: it returns a Counter, which travels through a hidden pointer into the caller's storage, so binding `result` to it needs care a scalar does not. Return a scalar, or drop the clause.
 --> old.bx:5:17
  |
5 |         ensures result.n == old(self.n) + 1
  |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^

result

result

A name, not a call, and in scope only inside an ensures clause. A declaration that takes a parameter called result and also writes result in an ensures is refused for the collision rather than shadowed quietly.

Answers the value being returned. Allocates: no.

exit

exit(code)

Reserved because the runtime calls libc’s exit to end a program on a failed contract or a bounds violation. A program that shadowed it would change what a failure does.

Answers nothing — the program ends. Allocates: no.

main

main

Burxt has no entry point: the whole file is the program, and statements at the top level run in order. So a function called main would look like an entry point and not be one — a trap rather than a crash, which is the kind of thing this language refuses. The name is reserved so it cannot be set.

Answers nothing — it is not an entry point. Allocates: no.