rokdocs
v0.1

9.9. Debug / diagnostic tools

This section defines the debug / diagnostic tools provided by rokc and their integration with external tools.

9.9.1. Intermediate output: --emit

The per-step results of the AOT build pipeline can be dumped.

rokc build --emit=ast           # full AST after meta expansion and desugaring, as rok source
rokc build --emit=hir          # rok-hir text output
rokc build --emit=mir           # rok-mir text output
rokc build --emit=mlir          # standard MLIR text
rokc build --emit=llvmir        # LLVM IR
rokc build --emit=asm           # assembly
rokc build --emit=obj           # object file
rokc build --emit=expanded      # meta expansion result (9.9.10)

mlir and llvmir come from the MLIR → LLVM backend at any optimization level; asm and obj come from the backend the level selects (9.2.10).

The output is written to the following locations.

target/debug/build/foo.ast.rok
target/debug/build/foo.hir
target/debug/build/foo.mir
target/debug/build/foo.mlir
target/debug/build/foo.ll
target/debug/build/foo.s
target/debug/build/foo.o
target/debug/build/foo.expanded.rok

To select several steps at once, separate them with commas, as in --emit=mlir,llvmir,asm. With a single kind, -o <path> changes the location. With --emit, no executable or archive is produced.

9.9.2. ROKHIR inspection

rokc rokhir-text path/to/foo.rokhir      # or libfoo.a - reads the sibling foo.rokhir

This command prints the ROKHIR of a prebuilt library in human-readable form. After the version, producer and meta, each interface file follows as --- <path> and its text (source with concrete function bodies cut down to ;).

rokhir 6
producer: rokc 0.1.0 (1f2a40-17f3a9c2e1b0c4d8)
package: geom (hash: 3f8a1c2b)
source: /home/me/geom
gc types: 4096..4160
features: tls

--- lib.rok
pub struct Point { pub x: f64; pub y: f64; }
pub fn distance(p1: Point, p2: Point) -> f64;
pub fn id<T>(x: T) -> T { x }

This output is useful for verifying the correctness and compatibility of ROKHIR.

9.9.3. Symbol demangle

rokc demangle <symbol>
rokc demangle _Y3f8a1c2b_geom_distance_a7c91d8e44b2f190

This command prints a mangled symbol of Symbol mangling in its original form. Given several symbols, the blocks are separated by blank lines. Mangling does not keep the root rok::, so if the first path segment is core / std / bare, rok:: is prepended to show it as a standard library path.

package:  3f8a1c2b
path:     geom
name:     distance
generics: a7c91d8e44b2f190 (type tuple hash)

9.9.4. Diagnostics / error messages

Standard format

Diagnostic messages are printed in the following standard format.

error[E100]: cannot find value `count` in this scope
  --> src/main.rok:5:13
   |
 2 |     let counter = 0;
   |         ------- similar name defined here
...
 5 |     let x = count + 1;
   |             ^^^^^ not found in this scope
   |
   = help: did you mean `counter`?

This format uses the file + line + column diagnostic units of Source files.

The header is <severity>[<code>]: <message> and --> path:line:col gives the primary location. The primary span is underlined with ^, secondary spans with -; a secondary span in another file is introduced by ::: path:line:col. Trailing = note: lines add context and = help: lines suggest a fix. For a name not found that an item elsewhere has, the help gives the use lines that bring it in (up to three: the package's own items first, then the standard library's, then the dependencies'), or the missing pub when the package's only such item is hidden (add `use geo::Point;` ; `geo::secret` has that name, but it is private to module `geo`: add `pub` to it).

JSON format (tool integration)

For tool integration, output can also be produced as JSON.

rokc build --message-format=json

Each diagnostic is one line of JSON object on stdout (the human format goes to stderr).

{"type":"error","code":"E200","kind":"type","message":"...","spans":[{"file_name":"src/main.rok","line_start":5,"col_start":10,"byte_start":71,"byte_end":76,"is_primary":true,"label":"..."}],"notes":["..."]}
FieldMeaning
typeerror / fatal / warning / note
codeCode of the error kind (table below)
kindName of the error kind
messageMessage
spansList of locations. The first element is the primary location (is_primary: true); the rest are secondary
notesList of note strings; a help entry starts with help:

A span has file_name, line_start, col_start, byte_start, byte_end, is_primary and label. A diagnostic that knows only the file and no position has just {"file_name": .., "is_primary": true}.

This format lets IDEs and CI systems read diagnostics mechanically.

Diagnostic categories

Diagnostic codes are classified by area as follows.

Code prefixArea
E001 ~ E099Lexical / syntax
E100 ~ E199Name resolution / visibility
E200 ~ E299Type system
E300 ~ E399Meta / magic
E400 ~ E499Build / link
E500 ~Additional categories

There is one code per error kind. Diagnostics of the same kind have the same code.

CodekindKind
E001lexicalLexical
E002parseSyntax
E100resolveName resolution
E101visibilityVisibility
E102moduleModule / file
E200typeType
E201patternPattern / exhaustiveness
E202handoffUse after hand-off (stdlib 2.13.9)
E300metaMeta block / attribute / keyword magic
E400loweringHIR / MIR lowering
E401codegenCode generation
E402linkageExternal tool / link
E500unsupportedSyntax the compiler does not handle
E501internalCompiler internal
E502otherOther
E503lintlint (6.2.9, rokc lint)

This specification guarantees the per-area code prefixes. When individual codes are introduced, they are subdivided within the same area.

9.9.5. Logging / progress display

Standard output

Default build output looks like this.

   Compiling foo (path/to/foo)
    Building target/debug/libfoo.a
    Finished in 1.23s

Progress lines go to stderr; the list of verbs is in 9.1.8.

--verbose / -v

Verbose mode also shows per-stage timing.

   Compiling foo (path/to/foo)
   Stage: front end (134ms)
    Building target/debug/foo
   Stage: codegen + link (812ms)
    Finished in 0.95s

Showing the time of each stage is useful for diagnosing build bottlenecks. The stages are front end and codegen + link (emit <ext> with --emit).

ROK_TIME_PASSES

ROK_TIME_PASSES=1 prints the wall time of each compiler pass on stderr.

-vv (very verbose)

In addition to -v, prints a Reading <file> line for each file processed and a Specialized <path>(sig) line for each (item, type) pair specialized.

9.9.6. Debugger integration: gdb / lldb

DWARF debug information

A debug profile build, and a release build with -g (9.2.15), emits DWARF debug information with the following properties. For Windows targets it emits CodeView in a PDB instead, carrying the same information.

  • A function's DW_AT_name is its rok path (geom::distance); no linkage name (mangled symbol) is emitted, because debuggers do not understand _Y mangling.
  • Line numbers refer to lines of the original .rok file.
  • Local variables appear with their original names and types. Besides primitive types (integers, floating-point, bool, char), DWARF types are emitted for structs (field names), tuples, str, any, sum types and arrays. A sum type or enum is a DWARF variant part: an artificial discriminant member, and one DW_TAG_variant per variant (dataless variants included) with DW_AT_discr_value, each holding a struct named <Sum>::<Variant>. Both back ends write the same form, and compile units use DW_LANG_Rust so debuggers read variant parts (there is no DWARF language code for rok). A GcBuf<T> reference is described as a pointer to its object (header, then the T elements). An any value is a variant part over the types stored into it, with the stored tag as the discriminant. The discriminant is an artificial i32 (the stored tag; a throws carrier's bool tag is a byte). A function value (a fn type or a closure) is a structure named for its type (fn(i32) -> i32) with members code, a pointer to a DW_TAG_subroutine_type (the environment, then the parameters), and env, its environment (a pointer to the environment object when the closure's type is known), so a debugger shows the target function. A variable captured by reference shows as its value. Functions the compiler makes (the C main, static initializer guards) have subprograms too. Compiler-generated locals (names beginning # or __) are omitted.
  • A lexical block scope is emitted for each block, so shadowed names are visible only inside their block.
  • Locals with stack slots are placed in a single llvm.alloca in both debug and release, so -g attaches debug info to the same slot regardless of optimization level.

Using gdb / lldb

Standard gdb and lldb commands work on rok code as is.

gdb ./target/debug/my_binary
(gdb) break main
(gdb) run
(gdb) print x
$1 = (i32) 42          # primitive-type variables only
(gdb) bt
#0  main at src/main.rok:5
...

Formatters

gdb shows only the active variant of a sum, an enum or an any from the DWARF alone. lldb has no plugin for the unit's language and lists every variant; the toolchain ships lldb formatters, lib/rok/lldb/rok_lldb.py, that show the active variant only (its name as the summary, its fields as the children; a boxed any payload through its box). rokc --print lldbinit (9.1.4) prints the lldb command that loads them:

lldb -O "$(rokc --print lldbinit)" ./target/debug/my_binary
rokc --print lldbinit >> ~/.lldbinit

macOS / Windows debuggers

Each platform's native debugger handles rok binaries.

  • On macOS, use lldb or the Xcode debugger.
  • On Windows, use the Visual Studio debugger or WinDbg (PDB debug information).

9.9.7. Profiler integration

perf (Linux)

CPU profiling can be done with perf.

perf record ./target/release/my_binary
perf report

rok functions are shown by their demangled names.

Profile-guided optimization

Profile-guided optimization (9.1.2) takes three steps:

rokc build --release -C profile-generate=prof    # 1. instrumented build
./target/release/my_binary <representative input> # 2. run; writes prof/default_*.profraw
rokc profdata merge -o app.profdata prof          # 3. merge, then
rokc build --release -C profile-use=app.profdata  #    rebuild with the profile

An optional context-sensitive stage builds with -C profile-use=app.profdata -C cs-profile-generate=cs, runs again, and merges cs with app.profdata into the final profile. wasm32-wasi is supported; wasm32-unknown-unknown is not.

Tracy / Chrome trace (optional)

Through the standard library's tracing surface, traces compatible with tracy or Chrome's chrome://tracing can be produced. The choice of a specific library is outside the scope of this specification and is covered by a separate library spec.

9.9.8. Memory / GC tools

The runtime reads the following environment variables.

VariableEffect
ROK_GC_LOG=1Prints the kind (minor / full), pause time, mark count and sweep result of each collection to stderr. A concurrent full collection logs its start pause (a minor collection) and its finish pause separately; the sweep result is the live bytes before -> after
ROK_GC_STATS=1At exit, prints collection counts (minor / full), pause time and collector CPU time to stderr. The second line gives block count / resident size / evacuation count / bytes moved
ROK_GC_CONCURRENT=auto|always|offConcurrent marking for full collections. auto applies when old objects are 32 MiB or more (7.5)
ROK_GC_EVAC=alwaysFor testing. Evacuation on every full collection (7.3.1)
ROK_GC_STRESS=NFor testing. Collects every N allocations (every 8th of those is full)
ROK_HEAP_DUMP=on_oomOn OOM, prints to stderr the totals, the top 32 types by bytes and the 32 largest large objects. Best effort: it allocates nothing and takes no lock. Format is runtime-defined
ROK_WORKERS=<n>Number of executor workers N (default: the online CPUs) (7.6.8)
ROK_THREAD_PER_CORE=1Thread-per-core: tasks pinned to their worker, no stealing (7.6.8)
ROK_DYNAMIC_WORKERS=1Idle workers park and are taken back when every active worker is busy (7.7.8)

An unset or empty variable has its default. A value outside those listed prints one warning line on stderr naming the variable, the value and the accepted values, and the variable has its default.

9.9.9. REPL / Jupyter debugging

AidEffect
:type <expr>Prints the type of the expression (without running it)
:resetResets the session to its initial state (the use of a library path is inserted again)
:add <dep> ..Adds dependencies (rok install into the current environment, or rok add in a package's session) and attaches them to the session (9.10.5)
:help, :quitHelp, quit
ROK_REPL_TIME=1Per cell, per-stage timing and the number of newly compiled functions / functions reused from earlier cells (9.6.4)

To see a cell's MLIR / LLVM IR, put the same code in main.rok and use rokc build --emit=mlir,llvmir (9.9.1).

9.9.10. Meta block debugging

You can check what code the derives / attributes of user-defined meta blocks generate.

rokc build --emit=expanded      # → target/<profile>/build/<name>.expanded.rok

This option prints the result of meta expansion of the user package as source text. Each expansion's generated source follows a comment like the one below; when generated code is expanded again, that result follows the outer expansion.

// expanded from this #[derive(MyHash)] (src/geom.rok:12:1)
impl MyHash for Point { ... }

To see the full AST after all meta expansion and desugaring as rok source, use --emit=ast (<name>.ast.rok).

9.9.11. Benchmarks

Microbenchmarks are run with the following command.

rok bench

rokc bench builds the benchmark harness and rok bench (9.10) runs it (9.1.2), measuring the functions marked #[bench] one at a time in an -O2 build. Each takes a Bench handle and times a closure through b.iter, so setup outside the closure is not measured. The measurement is:

  1. Clock. A monotonic nanosecond clock. Its fixed overhead is amortized by timing iters calls per sample, never a single call.
  2. Calibration. Unless --iters is given, iters is raised until one sample takes about 100 ms, so clock quantization and overhead are negligible.
  3. Warm-up. Calls are run for about one sample's worth of time and discarded, to settle caches, branch predictors and paging.
  4. Sampling. --samples samples (default 100) are taken; each records the total time of iters calls, and the per-call time is that total divided by iters.
  5. Statistics. Mean, median, standard deviation, median absolute deviation (MAD), min and max are computed. Samples outside the Tukey fences (1.5 IQR mild, 3 IQR severe) are reported as outliers but kept out of the summary figure. The reported figure is the median (+/- MAD), which is robust to an occasional slow sample.
  6. GC awareness. The runtime's collection counter is read around each sample; a sample during which a collection ran is classed as an outlier and counted separately, so GC pauses do not distort the median.

black_box(x) is an identity the optimizer cannot see through; apply it to the inputs and result of the timed closure so the work is neither hoisted out nor folded away. b.bytes(n) / b.items(n) turn the per-call time into a throughput (MiB/s, Melem/s).

Terminal output is aligned and in human units; non-terminal output is a stable one-line-per-bench machine form for regression tools. --iters / --samples adjust the sampling.

9.9.12. CI / test integration

CI mode

In CI environments, machine-readable output is recommended.

rokc build --message-format=json --no-color > build.log

This output is suitable for CI systems to parse.

Exit codes

The exit codes of the rokc CLI interface are defined as follows.

CodeMeaning
0Success
1User code error
2CLI error
101rokc internal panic

rokc test builds the harness; it does not run the tests. rok test runs them and exits with 1 if any fails. Each test runs in a separate process, so a test's panic (101) does not become the runner's 101.

CI can apply different handling per code (retry, notification, etc.).

9.9.13. LSP server

rokc provides an LSP (Language Server Protocol) mode. The same server is also provided as the standalone binary rokls (= rok lsp). It communicates over stdio and provides diagnostics, hover (including members), go-to-definition (returning the range of the defining name), references, rename, document symbols, member / path completion and formatting. References and rename are buffer-local.

rokc lsp
rokls

In this mode, LSP clients such as VS Code, Vim and Emacs receive completion, hover, go-to-definition, diagnostics and so on. Internally it uses the incremental results of rokc check.

The detailed LSP specification is outside this specification (the LSP standard + rokc implementation decisions) and is defined separately.

9.9.14. Package inspection

Information about a prebuilt library package can be viewed at once.

rokc inspect path/to/libfoo.a        # or foo.rokhir - the sibling file is read too

This command shows information from the ROKHIR (9.4) and the archive as follows.

Package: foo
Hash: 3f8a1c2b
rokc version: 0.1.0
ROKHIR version: 1
GC type ids: 4096..4160

Exports:
  pub fn distance(p1: Point, p2: Point) -> f64
  pub struct Point
  ...

Generic items (ROKHIR):
  pub fn id<T>(x: T) -> T
  ...

Concrete .o files:
  foo.o (15 KB)

rokc rokhir-text <file> prints the ROKHIR's version, producer, meta and all interface files (9.9.2).

This gives an at-a-glance view of what the package exposes externally.

9.9.15. Summary

The tools above form the core of rok's development and debugging experience. Integration with standard tools (gdb, perf, jupyter) combined with rok-specific tools (rokc demangle, rokc rokhir-text, etc.) provides a rich diagnostic environment.

This concludes chapter 9 (execution and tools) and completes the overall picture of this specification.

Edit this page on GitHub