Skip to content
GitHub
ReferencePrinting and panics

Printing and panics

Writing output with print and println and their interpolation, checking with assert, and what panic does.

All 6 examples tested by CIEdit this pageReport a problem
Tested
fn main() {
let name = "Steve"
let hp = 18
print("{name} has ")
println("{hp} health, {hp * 5}%")
println()
assert(hp > 0, "{name} is dead")
}
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:

Tested
fn main() {
var xs: List<i32> = [1, 2, 3]
println("{xs.len()} {xs.pop()} {xs}")
let last = xs.pop()
println("{xs} {last}")
}

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.

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:

Text
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 times

Inside a parallel for, what’s printed and which panic is reported are the same as if the iterations ran one after another (parallel loops).

Tested
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:

Tested
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)}")
}

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:

Tested
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)}")
}

Saved as example.calx and run, that writes:

Text
4
panic 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_caller function’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 to main, 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 build puts 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 before main.

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:

Tested
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
}
  • Every panic in the function names its caller: panic, a failed assert, an index out of bounds, ! on none.
  • A @track_caller function that calls another passes its own caller on, so a check two calls deep still names the code outside them.
  • A parallel for inside 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.