Qu

REPL & Diagnostics

The tables below are generated by reading the interpreter's own builtin dispatch table (fn call_builtin, engine/crates/qu-interp/src/lib.rs) directly — every row reflects a match arm that actually runs today, not aspirational API surface.

Output & Diagnostics

FunctionSignatureDescription
echoecho(v1, v2, ...)Alias of print/disp/writeline. Takes any number of positional arguments v1, v2, ... of any printable type (number, string, matrix, struct, ...) and prints each argument's display form space-joined on one line, followed by a newline. Returns none.
writewrite(v1, v2, ...) or write(fileHandle, s)Dispatches on the type of the first argument. With ordinary values v1, v2, ... (any printable type): prints them space-joined, without a trailing newline (unlike print/echo). With a fileHandle (the handle object returned by fopen) as the first argument and a string s as the second: instead writes the raw string s to that open file, with no newline added and no console output. Returns none in both forms.
errorerror(msg)Takes one argument, msg (a string, typically built with {...} interpolation), and raises an EvalError carrying that message. Never returns normally — control passes to the nearest enclosing try/catch, where the caught exception object exposes the string as .message; uncaught, it aborts the script. Returns nothing, ever — there is no value to receive, because control leaves the call.
warnwarn(msg)Takes one argument, msg (a string), and prints warning: {msg} to the console. Non-fatal — execution continues on the next line, unlike error. Returns Nothing.
explainexplain("fnName")Takes one argument, fnName (a string naming a user-defined function), and returns a string report — it does not print by itself, so wrap it in print(...) to see it. The report says whether that function is running the fast register-only ("soft-compiled") tier or falls back to the ordinary tree-walking interpreter, and — when interpreted — which construct disqualified it (a call to another user function, a loop, indexing, or a non-numeric value). This is the M4 "Phase 0" diagnostic surface: calling explain triggers the same lazy eligibility check an actual call would, so it works even before the function has ever been called, letting a script author ask "why is this function still slow" and get a real answer instead of a black box. Returns a Str, the report itself — the call prints none of it for you.

Overloads: write

CallWhat it does
write(v1, v2, ...)Prints the values space-joined to the console, with no trailing newline — the ordinary-value form.
write(fileHandle, s)Writes the string s verbatim to the already-open file fileHandle; no console output, no newline added.

Which form runs is decided entirely by the type of the first argument, not by argument count — passing a fileHandle as the sole argument, or a string as the first of several ordinary values, stays on the console-printing path.

write("a", 1, "b")
print("")
f = fopen("scratch_write_demo.txt", "w")
write(f, "hello file")
close(f)
g = fopen("scratch_write_demo.txt", "r")
print(read_all(g))
close(g)
output
a 1 b
hello file
a 1 b
hello file

Timers

MATLAB-style tic/toc for quick wall-clock timing, plus a timer/ preciseTimer object for cases where a single global slot isn't enough (nested or concurrent timing regions).

Qu actually has four distinct profiling/timing mechanisms, and picking the wrong one is an easy mistake, so here they are side by side: tic/toc is the single global lap timer — simplest, for timing one region at a time. timer()/preciseTimer() is a standalone object for nested, concurrent, or pausable timing, one per variable instead of one shared global. profile_start/profile_end (below) measures a single named region's wall-clock time *and* memory (RSS delta), returning a plain value rather than mutating shared state, so multiple regions can overlap or nest freely. profiling_mode/profile_stats is a different shape entirely — it attributes call count and cumulative time *per builtin function* across a whole recording window, with no memory tracking; reach for it to find which builtin is slow, not to time one block of your own code. See also profiling_mode/profile_stats further below in this chapter for that fourth mechanism.

FunctionSignatureDescription
tictic()Takes no arguments. Starts (or restarts) the single global stopwatch. timer.start() is sugar for this — see below. Returns Nothing.
toctoc()Takes no arguments. Returns a number: elapsed seconds (float, fractional) since the last tic(). Does not clear the start, so repeated toc() calls measure from the same tic() — useful for lap-timing a loop. Errors if called before any tic(). timer.elapsed() and timer.stop() are both sugar for this (see below) — toc() was already a non-resetting peek, so both aliases inherit that for free.
timer / preciseTimertimer([interval]) / preciseTimer([interval])Takes one optional positional argument, interval (a number, in seconds; irrelevant for plain profiling use) — the tick period on elapsed(t)/on elapsedOnce(t) fire at. Returns a standalone, controllable timer object (a handle, conventionally bound to a variable t) — the fix for tic/toc's single-global-slot limitation. Starts not running; start(t) begins counting. preciseTimer sets an internal precise marker on the returned object but is otherwise identical to timer — both use the same monotonic clock today; the distinct name is for intent-signaling and forward compatibility. Not the same feature as the bare timer.start()/timer.elapsed()/timer.stop() sugar below — this row produces a real, independent, per-variable object (t = timer(), t.start(), t.pause(), ...); the sugar is the single-global-slot tic/toc MATLAB-style convenience wearing nicer verbs. Pick the object form for nested/concurrent/pausable timing, the bare sugar for quick single-shot profiling.
startstart(t)Takes one argument, t (a timer object from timer()/preciseTimer()). Begins counting, or resumes from pause. Returns none; idempotent on an already-running timer.
pausepause(t)Takes one argument, t (a timer object). Freezes the count, keeping the accumulated total so start resumes from where it left off. Returns none; idempotent on an already-paused timer.
elapsedelapsed(t)Takes one argument, t (a timer object). Returns a number: elapsed seconds so far. A pure read — never stops or resets the timer, callable regardless of running state.
restartrestart(t)Takes one argument, t (a timer object). Returns a number: the elapsed time up to this call (the completed "lap"), then resets the object to zero and immediately starts it running again — a one-call lap/checkpoint pattern for timing each iteration of a loop.
stopstop(t)Takes one argument, t (a timer object). Freezes the timer and resets the count to zero (unlike pause, which keeps it); returns none. Also stops any on elapsed(t)/on elapsedOnce(t) registration from firing until started again. The handle remains valid and reusable. Returns Nothing.
profile_startprofile_start()Takes no arguments. Starts a named profiling window and returns a Record handle -- t0 (wall-clock seconds) and mem0 (RSS bytes, NaN if unsupported) -- to pass unmodified to profile_end. t0/mem0 are semi-internal (read by profile_end, not meant to be inspected directly); mem0 is NaN on a platform current_rss_bytes doesn't support (anything but Windows/Linux). Because the handle is an ordinary value rather than mutated global state (unlike tic), multiple windows — p1 = profile_start(), p2 = profile_start() — can overlap or nest without clobbering each other. Uses SystemTime rather than a monotonic clock, trading tic/toc's clock-step immunity for a value that can round-trip through a Record field — prefer tic/toc for sub-millisecond or clock-step-sensitive timing. See also timer/preciseTimer above for a controllable object, and profiling_mode/profile_stats below for per-builtin call-count and time attribution (no memory) across a whole recording window instead of one region's time and memory.
profile_endprofile_end(handle)Takes one argument, handle (the Record returned by profile_start(), unmodified). Closes the window and returns a Record with fields time (elapsed seconds) and mem (RSS delta in bytes, NaN if either sample was NaN, e.g. on an unsupported platform). mem is mem_now - mem0, sampled once at each end rather than polled, so a large allocation that also frees before profile_end runs is invisible to it — the same coarse-sampling trade-off qu-cli's own --profile/--max-memory watchdog documents for itself. Errors if handle is missing t0/mem0 or either field isn't a number, naming which field and why. See also tic/toc above for simpler lap timing with no memory tracking, and profiling_mode/profile_stats below for call-count/time attribution across many builtin calls instead of one region.
estimate_complexityestimate_complexity(sizes, times)Takes two same-length numeric vectors/lists: sizes (the problem sizes swept over) and times (the matching elapsed times in seconds, e.g. from toc() at each size) — at least 3 matching points are required in both. Fits several candidate growth models (O(1), O(log n), O(n), O(n log n), O(n^2), O(n^3)) to the data by least squares and picks whichever has the best R², rather than a naive log-log slope (which misclassifies O(log n)/O(n log n)). As a side effect it prints the fit (label, R², coefficient); it also returns the label as a string (e.g. "O(n)"). Meant to pair with tic/toc: sweep over increasing problem sizes, record elapsed time at each, then call this on the two vectors. Returns a Str, the winning model's label (e.g. "O(n)").

timer.start() / timer.elapsed() / timer.stop() — Stopwatch-style sugar for tic/toc

Part of the same small, closed table./timer. "namespace-dot" alias set as table.load/table.write (§ ergonomic API layer, 2026-08-31): the parser recognizes exactly timer.start(/timer.elapsed(/timer.stop( and rewrites each, at parse time, straight to tic()/toc()/toc() — so evaluation is byte-identical to calling those directly. timer is not a namespace value; it still works as an ordinary variable (timer = 5), and this is completely unrelated to the real timer([interval]) object constructor above (a bare timer.start() never constructs one).

timer.start()               # same as tic()

heavy_computation()
mid = timer.elapsed()       # peek — does NOT reset, callable again

more_work()
total = timer.stop()        # same as toc() — also a non-resetting read

A small reverse-mode autodiff tape. param/track are the same underlying operation; param is the spec's name for trainable weights and track is for "differentiate through this too" on an ordinary intermediate value — the distinction is documentation, not mechanism, until optimizers exist to read it.

FunctionSignatureDescription
track (alias param)track(v)Takes one argument, v — a plain number or a matrix (a bare [...] list literal is promoted to a column, matching to_matrix's convention). Returns a tracked value: internally a fresh leaf node on the gradient tape with no inputs, so grad treats it as a differentiation root, while remaining numerically transparent (a tracked scalar still reports type(...) as number; a tracked matrix prints as tensor([...], node #N)). Calling track on an already-tracked value is a no-op.
stop_gradstop_grad(v)Takes one argument, v — a tracked value produced by track/param. Returns the plain, untracked inner value (same numeric content and shape) detached from the tape, so gradients don't flow through it — e.g. holding a target/label steady, or truncating backprop through time.
gradgrad(loss, wrt=X) or grad(loss, X, Y, ...)Takes loss — a tracked scalar that is the result of a computation built from tracked inputs — plus either a keyword argument wrt=X (one tracked tensor X) or one or more positional tracked tensors X, Y, .... Runs the backprop walk over the tape. The keyword form wrt=X returns a single gradient value with the same shape as X (a number for a scalar X, a matrix for a matrix X); the positional form grad(loss, X, Y, ...) returns a List of such gradients, one per argument, in the same order (positional rather than wrt=[X, Y], because Qu's [...] bracket literal is numeric-matrix syntax and would silently flatten a list of tensors down to plain numbers, losing the node identity grad needs).

Overloads: grad

CallReturns
grad(loss, wrt=X)A single gradient value shaped like X — a number for a scalar X, a matrix for a matrix X.
grad(loss, X, Y, ...)A List of gradient values, one per positional argument, in the same order.
x = track(3.0)
y = track(2.0)
loss = x * x + y * y
dloss_dx = grad(loss, wrt=x)          # 2*x = 6

grads = grad(loss, x, y)              # [2*x, 2*y] = (6, 4)

print("wrt=x form: {dloss_dx}")
print("positional form: {grads}")
output
wrt=x form: 6
positional form: (6, 4)
wrt=x form: 6
positional form: (6, 4)

RNG Control

The base random-number-generator controls. The named distributions built on top of these (rand/randn shape helpers, and the wider distribution library) are documented in the Statistics & ML chapter — not duplicated here.

FunctionSignatureDescription
rand / randnrand(rows[, cols]) / randn(rows[, cols])Takes rows (a number, the row count) and an optional cols (a number, the column count; when omitted, defaults to 1, giving a rows×1 column vector). Draws rows*cols samples — uniform on [0, 1) for rand, standard-normal (mean 0, variance 1) for randn — and returns them as a rows×cols numeric matrix, drawn from the persistent RNG stream. An optional seed= keyword argument (a number) switches to a fresh, independent, reproducible local stream instead of the persistent one, without disturbing the persistent stream's state.
seedseed(n)Takes one argument, n (a number, the seed value). Reseeds the interpreter's persistent random stream and returns none, so subsequent unseeded rand/randn calls (and anything downstream that draws from self.rng, including spawn/pmap worker seeds) become reproducible — two calls to seed(n) with the same n are followed by identical draws.

Overloads: rand / randn

CallBehavior
rand(rows) / randn(rows)cols defaults to 1 — returns a rows×1 column vector, drawn from the persistent stream.
rand(rows, cols) / randn(rows, cols)Returns a rows×cols matrix, drawn from the persistent stream.
rand(rows, cols, seed=n)Draws from a fresh, independent, local stream seeded with n instead — the persistent stream's position is left completely untouched, which is the surprising part.
a = rand(3)
print("shape a: {size(a)}")

seed(5)
p = rand(2, 2)
junk = rand(2, 2, seed=42)
q = rand(2, 2)

seed(5)
p2 = rand(2, 2)
q2 = rand(2, 2)

print("q == q2: {q == q2}")
output
shape a: [3, 1]
q == q2: [1, 1; 1, 1]
shape a: [3, 1]
q == q2: [1, 1; 1, 1]

The seed=42 draw of junk never touches the persistent stream: q (drawn after it) still matches q2, drawn from a freshly-reseeded stream with no interloping seed= call at all.

Units

Qu numeric literals can carry a physical-unit suffix directly (e.g. 5 kHz), which normalizes the value to SI base units at parse/eval time — opt-in dimensional bookkeeping on plain numbers, not a wrapper type. This table lists every suffix the interpreter's apply_unit function actually recognizes and its normalization scale; anything not listed (dB, degC, cycles, samples, ...) passes through unscaled (factor 1), as confirmed by 3 dB evaluating to plain 3.

SuffixMeaning
HzHertz, base unit (×1)
kHzKilohertz (×1e3)
MHzMegahertz (×1e6)
GHzGigahertz (×1e9)
sSeconds, base unit (×1)
msMilliseconds (×1e-3)
usMicroseconds (×1e-6)
nsNanoseconds (×1e-9)
VVolts, base unit (×1)
mVMillivolts (×1e-3)
kVKilovolts (×1e3)
AAmperes, base unit (×1)
mAMilliamperes (×1e-3)
uAMicroamperes (×1e-6)
OhmOhms, base unit (×1)
mOhmMilliohms (×1e-3)
kOhmKiloohms (×1e3)
MOhmMegaohms (×1e6)
FFarads, base unit (×1)
uFMicrofarads (×1e-6)
nFNanofarads (×1e-9)
pFPicofarads (×1e-12)
HHenries, base unit (×1)
mHMillihenries (×1e-3)
uHMicrohenries (×1e-6)
WWatts, base unit (×1)
mWMilliwatts (×1e-3)
kWKilowatts (×1e3)
kBare SI prefix, no base unit (×1e3)
MBare SI prefix, no base unit (×1e6)
GBare SI prefix, no base unit (×1e9)
mBare SI prefix, no base unit (×1e-3)
uBare SI prefix, no base unit (×1e-6)
pBare SI prefix, no base unit (×1e-12)
aBare SI prefix, no base unit (×1e-18)

User-defined units

unit <name> = <expr> declares a named scale factor, for a physical unit this table doesn't already cover (ppm, a lab-specific calibration constant, ...). unit is contextual, not a reserved word — a plain identifier everywhere else, only parsed as this declaration when it's immediately followed by another bare identifier and then =.

unit ppm = 1e-6
x = 5 * ppm            # 5e-6 -- ordinary arithmetic, works immediately

y = 5 ppm               # 5e-6 -- literal-suffix adjacency also works

print(unit_scale("ppm"))   # 0.000001 -- reads the declaration back

output
1e-6

The grammar already parses <number> <identifier> for any trailing identifier, not just the statically-known spellings in this chapter's table above (that widening — and why a truly unknown unit is a runtime error naming it, not a parse failure — predates user-defined units). unit name = value is what makes a PREVIOUSLY-unknown identifier resolve instead of erroring "unknown unit": it registers name in a runtime table that 5 <name> literal adjacency, ordinary * arithmetic, and unit_scale(name) all consult.

FunctionSignatureDescription
unit_scaleunit_scale(name)name: a string. Returns the scale factor a unit name = ... declaration registered, or errors naming name if nothing was ever declared under it.

LaTeX Formatting

General LaTeX-formatting of any value — distinct from the plotting subsystem's own figure-export formats. tex mirrors str; printtex mirrors print.

FunctionSignatureDescription
textex(x)Takes one argument, x — any value (number, complex number, vector, or matrix). Returns a string: a complete LaTeX math-mode snippet wrapped in $...$ delimiters. Numbers/complex values reuse the same scientific-notation splitting as ordinary display formatting but rendered as \times 10^{n} (e.g. 4.7e-8 becomes $4.7 \times 10^{-8}$); vectors and matrices become a bmatrix environment (e.g. [1, 2, 3] becomes $\begin{bmatrix} 1 & 2 & 3 \end{bmatrix}$).
printtexprinttex(x)Takes one argument, x — any value, same acceptance as tex. Same rendering as tex(x), printed directly to the console (with a trailing newline) instead of returned. Returns none.

Overloads: tex

Case: scalar

x a real number. Small/large magnitudes get split into \times 10^{n} scientific notation; otherwise it's printed as an ordinary decimal.

print(tex(4.7e-8))
output
$4.7 \times 10^{-8}$

Case: complex

x a Complex value. Each component is formatted the same way a real scalar would be (including the scientific-notation split when a component is small/large), then joined as re + im i.

print(tex(3 + 4j))
output
$3 + 4j$

Case: vector or matrix

x a Vec/Mat. Returns a bmatrix environment instead of a bare number, with & separating entries.

print(tex([1, 2, 3]))
output
$\begin{bmatrix} 1 & 2 & 3 \end{bmatrix}$

printtex follows the exact same three cases — it just prints instead of returning the string.

Examples

tic/toc are the quickest way to answer "how long did that take": start the single global stopwatch with tic(), run the code, then read elapsed seconds with toc(). Because toc() is a non-resetting peek, the same tic() can be read multiple times to lap a loop without restarting the clock each iteration. The example below times a million square-root calls and prints the elapsed seconds:

tic()
for i in 1 to 1000000
    x = sqrt(i)
end
elapsed_s = toc()
print("took {elapsed_s:.4f}s")
output
took 0.0079s

track and grad are the entry points to Qu's reverse-mode autodiff tape: wrap the inputs you want a derivative with respect to in track (or its alias param), build an ordinary arithmetic expression from them, and then call grad on the result to walk the tape backwards. Here x is tracked, y = x^2 + 2x is computed from it, and grad returns dy/dx evaluated at x = 3 (analytically 2x + 2 = 8), which is then printed:

x = track(3.0)
y = x * x + 2 * x
dy_dx = grad(y, wrt=x)      # 2*x + 2 = 8

print("dy/dx = {dy_dx}")

A numeric literal followed directly by a recognized suffix is normalized to its SI base value at the moment it is parsed — no explicit conversion call needed. 5 kHz becomes 5000 (hertz, the base unit) and 50m uses the bare SI-magnitude prefix form (no base unit attached) to mean 50 * 1e-3. Both results are ordinary numbers from that point on, so they print like any other value:

Fs = 5 kHz        # normalizes to 5000

dt = 50m          # bare SI-magnitude prefix: 50 * 1e-3 = 0.05 (no base unit)

print("Fs = {Fs}, dt = {dt}")
Fs = 5000, dt = 0.05

estimate_complexity turns a handful of (size, time) measurements into an empirical Big-O label, which is the natural finishing move after lap-timing a sweep with tic/toc. The example below times summing the first n integers at four increasing sizes, collecting each elapsed time into a times list with append, iterating the sizes list by value with for n in sizes rather than by index. estimate_complexity prints its own fit summary as a side effect and also returns the label string, which is printed again explicitly here for clarity:

sizes = [100, 1000, 10000, 100000]
times = []
for n in sizes
    tic()
    total = 0
    for j in 1 to n
        total = total + j
    end
    dt = toc()
    times = append(times, dt)
end
label = estimate_complexity(sizes, times)   # e.g. "O(n)"

print("estimated complexity label = {label}")

echo prints several values space-joined on one line, which is handy for quick ad-hoc labeled output without building an interpolated string. warn is for a non-fatal notice: it prints a warning:-prefixed line and execution continues immediately afterward, unlike error, which aborts the current call unless caught:

echo("temperature =", 23.5, "C")
warn("input clipped to [0, 1]")
print("done")

error is how a function signals "this input is invalid" in a way a caller can recover from with try/catch. The raised exception's .message field carries the exact interpolated string passed to error, which is pulled out and printed in the catch block below:

function check_positive(x)
    if x < 0
        error("check_positive: x must be >= 0, got {x}")
    end
    return x
end

try
    check_positive(-3)
catch err
    print("caught: {err.message}")
end

explain is the transparency tool for the soft-compile tier: point it at the name of a user-defined function to find out whether calls to it are running the fast register-only path or falling back to the tree-walking interpreter, and why. Here a trivial numeric function is defined and then explained, printing its soft-compile report:

function double(x)
    return x * 2
end
print(explain("double"))

A standalone timer() object is the tool for lap-timing when a single global tic/toc slot isn't enough — for example, timing nested or concurrent regions independently. restart returns the elapsed time for the lap that just finished and immediately starts a new one, while pause simply freezes the running total in place without resetting it:

t = timer()
start(t)
lap1 = restart(t)
lap2 = restart(t)
pause(t)
print("lap1={lap1}, lap2={lap2}")

stop_grad detaches a tracked value from the autodiff tape so gradients stop flowing through it — the typical use is holding a label or target value steady while still letting gradients flow through the prediction it is compared against. Below, target is tracked and then immediately detached with stop_grad, so only x (the prediction) contributes to dloss/dx:

target = stop_grad(track(5.0))
x = track(2.0)
loss = (x - target) * (x - target)
dloss_dx = grad(loss, wrt=x)
print("dloss/dx = {dloss_dx}")

seed makes the persistent random stream reproducible: reseeding with the same number before each draw guarantees the same sequence of values out of rand/randn. The example below draws two 1x3 uniform vectors around a re-seed and prints both to show they match exactly:

seed(42)
a = rand(1, 3)
seed(42)
b = rand(1, 3)
print("a = {a}")
print("b = {b}")

tex/printtex are for dropping a Qu value straight into a LaTeX document. A very small or large magnitude number gets rendered in \times 10^{n} scientific notation, and a vector or matrix becomes a bmatrix environment. The example prints the LaTeX string for a tiny number with tex, then prints a vector's LaTeX form directly with printtex:

c = 4.7e-8
print("tex string: {tex(c)}")
printtex([1, 2, 3])

builtins, apropos, and help are the engine's own self-description tools — useful when you know roughly what you want but not the exact name. builtins() lists every name the engine answers to, apropos filters that list by a substring, and help reports what is known about one specific name. The example counts the total builtin surface, searches it for anything filter-related, and asks for help on curve_fit:

names = builtins()
print("total builtins: {len(names)}")
hits = apropos("filter")
print("filter-related: {hits}")
help("curve_fit")

device_used and gpu_probe_info answer "did that actually run on the GPU?" after a computation that's eligible for GPU dispatch, such as a large matrix multiply. device_used reports where the last dispatch actually went; gpu_probe_info reports what the one-time startup probe found about the hardware. Together they catch the failure mode of a GPU that silently fell back to the CPU:

A = randn(200, 200)
B = randn(200, 200)
C = A * B
d = device_used()
print("device = {d.device}, gpu_dispatches = {d.gpu_dispatches}")
p = gpu_probe_info()
print("gpu available = {p.available}, crossover = {p.crossover}")

More functions

FunctionSignatureDescription
builtinsbuiltins()Takes no arguments. Returns a List of strings: every name the engine answers to (hundreds of entries; the exact count grows as builtins are added). The builtin index is this list joined against the chapters.
aproposapropos("filter")Takes one argument, a search substring (a string, e.g. "filter"). Returns a List of strings: every builtin name containing that substring, in engine-registration order. Returns a list rather than printing, so a script can count (len(...)) or loop over the hits.
helphelp("curve_fit")Takes one argument, a builtin name (a string, e.g. "curve_fit"). Prints what is known about that name directly to the console and returns none — that it exists, near matches when it does not (e.g. help("not_a_real_fn") suggests close spellings), and where the worked examples live. Honest about its limits — the engine carries no per-builtin prose. Returns Nothing; the report goes straight to the console.
sandbox_modesandbox_mode(on)Takes one argument, on (a boolean). Refuses file and network access for the rest of the session and returns none. One way only — passing sandbox_mode(false) does not turn it back off, which is the point; e.g. after sandbox_mode(true), a subsequent fopen(path, "w") raises a catchable error instead of opening the file. Returns Nothing.
profiling_mode, profile_statsprofiling_mode(true)profiling_mode takes one argument, a boolean (true to start recording, false to stop), and returns none. profile_stats() takes no arguments and returns a Table value with columns function, calls, and seconds, one row per builtin called while recording was on. Record per-builtin time and call counts, then read them back; off by default, because the recording itself costs something. Returns a Table from profile_stats, with columns function, calls and seconds; profiling_mode hands back no value. See also the Timers section above, which lays out all four of Qu's profiling/timing mechanisms side by side (tic/toc, timer/preciseTimer, profile_start/profile_end, and this one) and says when to reach for which.
device_useddevice_used()Takes no arguments. Returns a struct-like value with two fields: .device (a string, e.g. "cpu" or a GPU device name) and .gpu_dispatches (a number, the count of operations actually dispatched to the GPU). Reports where the accelerated work actually went — a GPU that quietly fell back to the CPU is the failure this catches.
gpu_probe_infogpu_probe_info()Takes no arguments. Returns a struct-like value with three fields: .available (a boolean, whether a GPU backend was found), .crossover (a number, the measured matrix/problem size above which the GPU wins over the CPU — inf on a build without GPU support), and .samples (a list of the timed sample points used to compute the crossover). Reports what the GPU probe found at startup.