Part VIII — Tooling

Everything ships in one binary: qn runs programs, and its subcommands are the REPL, the test runner, the static checker, the formatter, the documentation generator, the debugger, and a syntax highlighter. This part is a tour of each, with real sessions. Shell transcripts are shown as plain fences (they are terminal text, not Quoin); runnable Quoin examples are tagged and verified like everywhere else in this book.

Nav: Foundations · Blocks & control · Objects · Patterns & errors · Concurrency & iteration · Networking & the web · Types · Tooling · Library & reference · Appendices


36. Running programs — qn

Rules

A program is just a .qn file; there is no required entry point — top-level statements run in order (Part I). Command-line arguments arrive as a list of strings:

Runtime.arguments.print
$ qn args.qn --verbose input.txt
#(--verbose input.txt)

With the shebang and the execute bit, a .qn file is a command:

$ head -1 tool.qn
#!/usr/bin/env qn
$ chmod +x tool.qn
$ ./tool.qn --verbose input.txt

-e is the one-liner form — handy in shell pipelines, and it prints the expression's value exactly as the REPL would:

$ qn -e '(1..5).collect:{ |n| n * n }'
#(1 4 9 16)
$ qn -e 'nil.foo'
Message not understood: receiver=Nil, selector='foo', args=[]
  at <eval>:1:1
  |
  | nil.foo
  |
  at (top) in <eval>:1:0                                < nil.foo
$ echo $?
1

That second run shows the exit-code contract: an error nobody catches is reported on stderr and the process exits 1, so shell scripts and CI can gate on any qn invocation. To choose the status yourself, Runtime.exit:3 ends the process with code 3 — it cannot be caught, but sockets, extensions, and other resources are still torn down normally.


37. The REPL — qn repl

Rules

CommandDoes
$type <expr>show the class of an expression's result
$inspect <expr>evaluate and show the value's class + fields
$time <expr>evaluate and report wall-clock time
$globals [pre]list defined classes and values (optional prefix)
$class <Name>show a class: parent, mixins, ivars, methods
$doc <Name>[.sel]show a class's or method's reference doc
$load <file.qn>run a .qn file into the session
$ps (or $ps tree)show tasks, fibers, workers, and waits
$resetclear session locals (definitions stay)
$help, $quit/$exithelp; leave (also Ctrl-D)

A session, verbatim:

$ qn repl
qn> var nums = #(3 1 4 1 5)
=> #(3 1 4 1 5)
qn> nums.sort
=> #(1 1 3 4 5)
qn> $type nums.first
class Integer
qn> $inspect 1..5
NumberRange(1..5)  (class NumberRange)
  @start: Integer
  @end: Integer
  @n: Integer
qn> $time nums.sort
=> #(1 1 3 4 5)
   (1.873 ms)
qn> $globals Str
Classes (2):
  String  StringStream
qn> $reset
Session locals cleared.

Every line runs under the same scheduler a program uses, so top-level I/O, an Async.sleep:, a spawned Task, or a fiber resume all work at the prompt — and $ps shows what is running or parked while they do.

$doc is the reference at your fingertips: it answers with the same documentation qn doc publishes (see below), for stdlib and your own loaded classes alike:

qn> $doc List.sort
Sort in place, ascending by the elements' `>:`, and answer the receiver; nils sort last.
...

38. Tests — qn test

Rules

A complete test file:

(TestSuite.new:{ var name = 'Math' }).add:{
    .test:
    addition -> {
        .is:{ 2 + 2 } equalTo:4;
    };

    .test:
    division -> {
        .is:{ 10 / 4 } equalTo:2;
        .does:{ 1 / 0 } throw:ArithmeticError;
    };
}
$ qn test tests
[Math] Running 2 tests
[Math]   Test addition . 7µs (119µs) 1 passed
[Math]   Test division .. 10µs (1.5ms) 2 passed
[Math] Finished in 17µs (11.4ms) : 3 passes / 0 failures
All suites finished in 17µs (38.6ms) : 3 passes / 0 failures / 0 skipped
$ echo $?
0

A failure names the assertion's expected/actual and its source location, and flips the exit code:

[Failing]   Test arithmetic ! 8µs (138µs) 1 of 1 assertions failed:
[Failing]     5 != 4 at self:failing/fail_test.qn:4:13
[Failing] Finished in 8µs (10.5ms) : 0 passes / 1 failures
All suites finished in 8µs (15.6ms) : 0 passes / 1 failures / 0 skipped
$ echo $?
1

A fixture every test reads belongs in setupAll:, its removal in teardownAll: — built once, cleaned up even when tests fail; use setup: / teardown: instead when a test mutates the fixture and the next one needs it fresh:

(TestSuite.new:{ var name = 'Config' }).add:{
    var path = '/tmp/hooks_example.json';
    .setupAll:{ [IO]File.write:'{"port": 8080}' to:path };
    .teardownAll:{ [IO]File.delete:path };

    .test:
    readsPort -> {
        .is:{ (JSON.parse:([IO]File.read:path)).at:'port' } equalTo:8080;
    };
}

The framework itself is ordinary, documented Quoin — for the full assertion reference (including how to build custom assertions on recordResult:evidence:block:), see the TestSuite, BuiltinAssertions, and IterateAssertions pages of the generated API reference (qn doc, below), or ask the REPL directly with $doc TestSuite.

⚠ Gotcha — the test directory's name is spliced into a use path. qn test DIR synthesizes use self:DIR/*, so DIR must be spellable as a Quoin load path: qn test my-tests fails with a parse error (the - reads as an operator). Prefer plain names like tests.

Coverage, for CI dashboards:

$ qn test --coverage tests > coverage.lcov
coverage: 188/868 lines (21.7%)

The summary lands on stderr; the redirected stdout is a standard LCOV report (--coverage=cobertura for Cobertura XML instead).


39. Static checking — qn check

Rules

$ qn check typo.qn
typo.qn:1:22: warning: type mismatch: expected `Integer`, found `String`
    |
  1 | var count: Integer = 'three'
    |                      ^^^^^^^
typo.qn:3:1: warning: `NumberRange` does not respond to `nopeMethod`
    |
  3 | r.nopeMethod
    | ^
$ echo $?
1

Quoin's types are gradual (annotations are optional, checking is best-effort), so a clean qn check is not a soundness proof — but it catches the classic mistakes before they run, and it is cheap enough to sit in a pre-commit hook next to qn fmt --check.


40. Formatting — qn fmt

Rules

$ qn fmt --diff messy.qn
--- messy.qn	2026-07-09 23:30:36
+++ messy.qn (formatted)	2026-07-09 23:30:36
@@ -1,3 +1,3 @@
-var total=0
-(1..4).each:{|n| total = total+n}
+var total=0;
+(1..4).each:{ |n| total = total+n };
 total.print
$ qn fmt messy.qn
formatted messy.qn
$ qn fmt --check messy.qn
$ echo $?
0

Note what the diff fixed silently: the missing ; after the first two statements. Part I's separator rule (a line starting with . or an operator continues the previous statement) makes a dropped ; a real hazard, and the canonical style always writes them between statements — one of several ways the formatter removes a whole class of surprises.

The self-verification is why formatting is safe to run blind over a tree: the guarantee is checked at write time on your actual file, not merely promised by the formatter's own test suite.


41. Documenting a project — qn doc

Rules

Documenting your own code is just commenting it (Part I's "* line comment), directly above the thing described:

"* A circle with a radius, in whatever unit you like.
Circle <- { |@radius|
    "* The enclosed area.
    area -> { @radius * @radius * 3.14159 };
    diameter -> { @radius * 2 };
}

(Circle.new:{ var radius = 2 }).area     "* -> 12.56636
$ qn doc
qn doc: skipping Quernfile.qn (does not load standalone): undefined name `Quern` — …
qn doc: 8 classes, 1 extended classes, 1 commands -> qn-docs
$ qn doc --coverage
undocumented: Circle diameter
doc coverage: 73/74 (98.6%)

The coverage report is the safety net for the adjacency rule: a blank line sneaking in between a doc block and its definition silently detaches the doc, and --coverage is how that shows up.

The doc-example harness — qn doc --check

Documentation rots unless something executes it. qn doc --check PATH… finds every fenced code block tagged quoin in the given markdown files and runs each one in a fresh session, statement by statement like the REPL. A block tagged quoin norun is display-only; an untagged fence (shell transcripts, program output) never runs. Within a running block, every "* -> value annotation is asserted: the statement's rendered result must match. So this fence is a test, executed on every change to this book:

(1..5).collect:{ |n| n * n }    "* -> #(1 4 9 16)
$ qn doc --check docs/language/08-tooling.md
qn doc --check: 5 examples, 2 annotations checked, 0 failed

The chapter you are reading — all of docs/language/ — is checked exactly this way in CI, which is why its examples can promise their annotations are true. Run bare (qn doc --check, no paths), the same engine executes the annotated examples inside your project's own doc comments — doc-tested libraries for free. (The stdlib's reference is generated and checked with the same tool; the publishing flags it uses aren't part of the everyday surface.)


42. The debugger — qn debug

Rules

CommandDoes
$continue, $cresume execution
$step/$s · $next/$n · $finish/$finstep into · over · out
$break FILE:LINE ($b; $break LINE = current file)set a breakpoint
$delete FILE:LINE ($d)clear a breakpoint
$frames/$bt · $up · $downbacktrace; move the focus frame
$locals/$llocals, self, and self's @ivars
$list · $source on|offsource around the focus; auto-show toggle
$print EXPR, $pevaluate in the focus frame (or just type it)
$quit/$q · $helpleave; help

A session against the Tally class from earlier chapters' mold:

Tally <- { |@total|
    init -> { @total = 0 };
    add: -> { |n|
        @total = @total + n;
        @total
    };
    total -> { @total };
}

var tally = Tally.new
#(3 4 5).each:{ |n| tally.add:n }
('total: ' + tally.total.s).print
$ qn debug tally.qn
Quoin debugger — $help for commands, $continue to run, $quit to exit.
→ paused at tally.qn:1  (in <block>)
→    1 │ Tally <- { |@total|
     2 │     init -> { @total = 0 };
     3 │     add: -> { |n|
$ $break 4
breakpoint set at tally.qn:4
$ $continue
→ paused at tally.qn:4  (in add:)
     2 │     init -> { @total = 0 };
     3 │     add: -> { |n|
→    4 │         @total = @total + n;
     5 │         @total
     6 │     };
$ $locals
  n = 3
  self = Tally{@total: 0}
  @total = 0
$ @total + n
3
$ $continue
→ paused at tally.qn:4  (in add:)
...
$ $delete tally.qn:4
breakpoint cleared at tally.qn:4
$ $continue
total: 12

Note the second pause: a breakpoint fires on every arrival at its line — each each: iteration — until deleted. The @total + n line is the expression-first design: anything that isn't a $-command is evaluated in the focus frame, so inspecting state is just writing Quoin.

Exception breakpoints pause with the throwing stack still live, before any unwinding, so $frames, $locals, and eval-in-frame see the world exactly as the throw left it:

var half = { |n|
    (n % 2 == 0).if:{ n / 2 } else:{ ValueError.throw:('odd: ' + n.s) }
}
#(4 6 7).each:{ |n| (half.value:n).print }
$ qn debug --break-on-throw ValueError oops.qn
...
$ $continue
2
3
→ broke on throw: ValueError{@message: 'odd: 7' @payload: nil}
→ paused at core/00-bootstrap.qn:115  (in throw:)
$ $frames
→ #2  core/00-bootstrap.qn:115  throw:
  #1  oops.qn:2  value:
  #0  oops.qn:4  <block>

$continue from a throw pause simply lets the exception keep propagating (or get caught) exactly as it would have. Had this run used --break-on-uncaught and the ValueError been caught somewhere, no pause would fire at all — that mode is for the error that actually escapes.


43. Syntax highlighting — qn highlight

Rules

$ qn highlight --html greet.qn | head -3
<!doctype html>
<html><head><meta charset="utf-8">
<title>greet.qn</title>

Next: Part IX — The standard library — the core types, string formatting, namespaces, and the stdlib map.