Printing and panics
Writing output with print and println and their interpolation, checking with assert, and what panic does.
fn main() { let name = "Steve" let hp = 18 print("{name} has ") println("{hp} health, {hp * 5}%") println() assert(hp > 0, "{name} is dead")}Steve has 18 health, 90%
| Function | Does |
|---|---|
print(s) |
Writes s to standard output |
println(s), println() |
Writes s, then a newline |
eprint(s), eprintln(s), eprintln() |
The same, to standard error |
assert(cond), assert(cond, s) |
Panics if cond is false, with the message assertion failed: s |
panic(s) |
Panics with the message s |
The argument is a str or a String. A string literal there can interpolate: {expr} writes the value of expr, with no string built in between. Every value can be interpolated except a pointer, and a generic parameter without the bound Display: numbers, bool, text, and structs, enums, tuples, arrays, optionals and lists of them (comparing and printing). Floats print as Java prints them (floats), and a struct prints as its literal, such as Point { x: 1, y: 2 }, unless it writes its own Display. An interpolating literal anywhere else builds a String (strings).
Every interpolation runs, left to right, before anything is written, as Java builds the whole string first. So a function called inside {...} that prints writes its output before the line it’s in.
A value that’s Copy, such as a number, is copied when its {...} runs. A variable, field or element that owns memory, such as a list or a map, is written as it is once they’ve all run, so a later {...} can’t change it or move it, which is a compile error. Work that one out first, into a variable. Reading from it, as {xs.len()} does, holds nothing:
fn main() { var xs: List<i32> = [1, 2, 3] println("{xs.len()} {xs.pop()} {xs}") let last = xs.pop() println("{xs} {last}")}3 some(3) [1, 2] [1] some(2)
Standard output is buffered and written at exit, or when a panic starts. It’s the same bytes on Windows and Linux: \n is never turned into \r\n.
Standard error isn’t buffered. eprint and eprintln first write out whatever standard output has waiting, so when both go to one terminal they appear in the order they were written. A program whose standard output is a protocol, such as the rig’s adapter, logs with them. std.io reads and writes bytes.
Panics
Section titled “Panics”A panic is an unrecoverable bug: an index out of bounds, a division by zero, opt! on none, a failed assert, a call deeper than the stack holds, or panic(...). The program writes its output so far, then prints panic at file:line:column: message to standard error and exits with status 101.
Every thread’s stack is 8 MiB (std.thread). A call that would go past its end panics like the rest, on any thread and in any build. That’s no one line’s fault, so the place is <stack>, and the trace shows the calls that got there:
panic at <stack>: the calls went deeper than the thread's 8 MiB stack holds at example.walk(example.calx:4) at example.walk(example.calx:9) ... repeated 126 more timesInside a parallel for, what’s printed and which panic is reported are the same as if the iterations ran one after another (parallel loops).
fn main() { let xs = [1, 2, 3] var i: usize = 0 i += 5 println("{xs[i]}") // panic: index 5 is out of bounds for length 3}panic(...) never returns, so code after it is unreachable, and it fits wherever a value is needed:
fn check(n: i32) -> i32 { let half = if n % 2 == 0 { n / 2 } else { panic("{n} is odd") } return half}
fn main() { println("{check(8)}")}4
Stack traces
Section titled “Stack traces”After the message, a panic writes the Calx functions it was in, innermost first, each at the line it had reached, as Java writes a stack trace:
fn half(n: i32) -> i32 { assert(n % 2 == 0, "{n} is odd") // panic: assertion failed: 7 is odd return n / 2}
fn main() { println("{half(8)}") println("{half(7)}")}4
Saved as example.calx and run, that writes:
4panic at example.calx:2:5: assertion failed: 7 is odd at example.half(example.calx:2) at example.main(example.calx:8)- A frame is the function’s module and name, with a method’s type between them, such as
world.Chunk.block. A generic function has one name, whatever it was called with. - A call the optimizer inlined is a frame of its own, so a trace is the same in debug and release builds, on Windows and on Linux.
- A frame that comes again right after itself, as a recursion’s does, is written once, then
... repeated N more times. Once more is just written again. - Only Calx functions are written, the standard library’s too, not the runtime’s, the C library’s or the system’s.
- A file inside the folder the program runs in is written relative to it, as in the message; any other in full.
- A
@track_callerfunction’s message names its caller, and its trace still starts in the function, at its own line. - Inside a
parallel for, the trace is the one a single thread running the loop would write: the body at its line, the loop at its line, then out tomain, loops inside loops too, whichever threads ran what. Each thread writes the part of it that’s on its stack. - A panic in a library is the program’s, and its trace goes through both, the library’s functions under their module’s name.
- The program reads them from its own line tables when it panics. Every build has those, and they don’t change the generated code, so until a panic a trace costs nothing. On Windows they’re in a PDB, which
calx buildputs beside the program (command). It looks at the innermost 128 calls, not counting inlined ones, so the trace of a panic deep in a recursion stops beforemain.
Panics that name the caller
Section titled “Panics that name the caller”A function marked @track_caller reports its panics at the place that called it, not at its own lines. That’s for a library’s checks, such as an index that’s out of range for a grid, where the bug is in the code that called it:
struct Grid { cells: List<i32> width: usize}
impl Grid { @track_caller fn at(self, x: usize, y: usize) -> i32 { if x >= self.width { panic("x is {x}, but the grid is {self.width} wide") } return self.cells[y * self.width + x] }}
fn main() { let g = Grid { cells: [1, 2, 3, 4, 5, 6], width: 3 } println("{g.at(2, 1)}") println("{g.at(3, 0)}") // panic: x is 3, but the grid is 3 wide}6
- Every panic in the function names its caller:
panic, a failedassert, an index out of bounds,!onnone. - A
@track_callerfunction that calls another passes its own caller on, so a check two calls deep still names the code outside them. - A
parallel forinside one names its own places, since its body runs apart from the call. - It’s for functions and a type’s own methods. A method of an interface’s impl can’t have it, since the interface’s callers don’t pass a place, and neither can
main, since no Calx code calls it. - In C, the function takes the place as a last parameter, a string such as
"src/main.calx:20:16", and its panics use that string, so it costs one pointer per call.
@track_caller, @export and @attribute are the attributes the compiler knows. Any other is a function a library marks @attribute (attributes), and one that’s neither is an error.