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
qnRules
qn app.qn [args…]runs a program. Everything after the file — flags included — lands inRuntime.arguments(a List of Strings), verbatim;qn's own flags go before the file.- A program may start with a shebang (
#!/usr/bin/env qn): the line is trivia to the language, so achmod +x'd script runs directly, and its arguments still arrive verbatim. Diagnostics keep true line numbers.qn -e EXPRevaluates one expression and prints its value (anilresult prints nothing) — the same rendering the REPL uses.- Exit codes:
0on normal completion; an uncaught error prints its annotated report to stderr and exits1;Runtime.exit:Nexits withN(it is uncatchable, and teardown still runs).- Environment:
QUOIN_STDLIB=DIRloads the stdlib fromDIRinstead of the copy embedded in the binary;QUOIN_PATH=DIRSadds extra roots to the extension-package search. InternalQN_*tuning knobs are catalogued indocs/internal/ENV_FLAGS.mdand are not part of the language surface.
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.
qn replRules
qn replopens an interactive session; the prompt isqn>. Results echo as=> value; anilresult is suppressed.- State persists across lines: class definitions and constants become ordinary globals; lowercase
var/letlocals live for the whole session.- Line editing with history (
~/.quoin_history), syntax highlighting as you type, multiline continuation (an incomplete line keeps reading), and tab completion — global and session-local names, namespace names inside[…], andreceiver.selectors when the receiver's class is knowable (a class name, a session local, or a literal).- A line whose first non-space character is
$is a REPL command, not Quoin:
Command Does $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 .qnfile 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)
~/.quoinrc, if present, runs into the session before the first prompt — interactive sessions only (piped input andqn -eskip it, like a shell rc file).
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. ...
qn testRules
qn test [DIR](defaulttests) loads the test framework, then every.qnfile inDIRin sorted order, then runs every suite that registered.- A test file builds suites; constructing a
TestSuiteregisters it (into the[Test]Suitesregistry) — no export or main needed.- Structure:
(TestSuite.new:{ var name = '…' }).add:{ … }; inside theadd:block, each.test: name -> { … }declares one test.- Assertions take the value under test as a block. The vocabulary:
isTrue:/isFalse:;is:equalTo:/is:notEqualTo:(the workhorse);is:a:/is:an:;is:closeTo:(…within:);is:lessThan:/is:greaterThan:(and the…OrEqualTo:pair);does:match:/does:notMatch:(the~pattern protocol, §14);does:resultIn:(effects);does:throw:;elementsOf:areEqualTo:..skip:'reason'in anadd:block skips the suite without failing the run.- Lifecycle hooks, declared in the
add:block like tests:.setup:{ … }/.teardown:{ … }run around each test;.setupAll:{ … }/.teardownAll:{ … }run once per suite. Share fixture state by capturing avar. Several of the same hook accumulate: setups run in declaration order, teardowns in reverse (they unwind). A skipped suite runs no hooks.- Exit code:
0when everything passes,1on any failure — the CI gate.--coverage[=lcov|cobertura]collects Quoin-level coverage: the report goes to stdout (or--coverage-out PATH), a one-line summary to stderr. The same flags work on a plainqn program.qnrun.
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
usepath.qn test DIRsynthesizesuse self:DIR/*, soDIRmust be spellable as a Quoin load path:qn test my-testsfails with a parse error (the-reads as an operator). Prefer plain names liketests.
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).
qn checkRules
qn check PATH…parses and type-checks each file (directories recurse) without running anything.- It reports parse errors, plus the gradual type checker's diagnostics: unknown types, type mismatches (declarations, reassignments, returns), messages a typed receiver does not understand, possibly-
nilreceivers and operands, return-type covariance violations, and generics misuse.- Diagnostics are warnings: the checker is best-effort and never blocks
qnfrom running the program.qn checkis where they become a gate.- Exit codes:
0and silent when clean;1if any diagnostic or parse error was reported.
$ 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.
qn fmtRules
qn fmt PATH…formats files in place (directories recurse;-reads stdin and writes the result to stdout). Changed files are named; untouched ones are silent.- Opinionated and zero-config: one canonical style (4-space indent, width-driven line breaks,
;separators normalized,{ |x| … }delimiter padding), nothing to tune.- Self-verifying: the formatter re-parses its own output and refuses to write if the AST changed or a comment was lost — a bug can produce an error, never a meaning-changing write.
--checkexits1and lists any file not already formatted (the CI gate);--dry-runprints the would-be result without writing;--diffprints a unified diff (and also exits1when anything would change).
$ 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.
qn docRules
qn docdocuments the current directory's project: it loads the tree's.qnunits (skippingtests/,bin/, hidden directories, and shebang scripts — a command's top level is the program) and generates one HTML page per class plus an index intoqn-docs/(--out DIR).PATH…narrows the roots.--jsonalso writes the raw model asmodel.json({"version": 2, …}) for other renderers.- Docs are comments: a contiguous run of
"*lines immediately above a definition — method, class, or class extension — is that definition's documentation. A blank line detaches it. The first line is the summary.- Your extensions are API. Methods your project adds to platform classes (
[OS]Process <-- { … }) document under your project as "Extensions to …" — provenance is per method, so the platform's own surface never bleeds in.- The index opens with your
README.md(headers, lists, links, andquoin-tagged fences render), and listsbin/entries whose#!line mentionsqnas the project's commands.- A discovered unit that doesn't load standalone (a
Quernfile.qn-style config script) is skipped with a warning; files you name explicitly fail hard. Well-formed library filesusewhat they need, so loading them standalone just works.- The same docs answer
$docin the REPL anddoc/docFor:in code.--coveragelists every class/selector of yours with no doc and the overall percentage. It is a report, not a gate — exit0either way.--checkruns the documentation's examples instead of generating: with PATHs, fenced blocks taggedquoinin markdown files/dirs; without, the annotated examples inside your own doc comments — with the project preloaded, so examples needn'tusethe code they document. Exit1if any fail.
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.
qn doc --checkDocumentation 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.)
qn debugRules
qn debug app.qn [args…]runs the program under the debugger, paused at the first line. The prompt is$; a bare expression evaluates in the focus frame (self,@ivars, and locals all resolve).
Command Does $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, andself'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
- Exception breakpoints:
--break-on-throw TYPESpauses when a matching exception is thrown, even one that would be caught (first-chance);--break-on-uncaught TYPESpauses only when it will escape. Both take a comma-separated type list — matching is hierarchy-aware (Errorcatches every structured error;Objectis the explicit catch-everything), and a Rust-raisedTypeErrorand your ownError.throw:behave identically.qn debug --dapspeaks the Debug Adapter Protocol on stdio instead of the$-command loop — point VS Code,nvim-dap, or any DAP client at it for editor breakpoints and stepping.- A breakpoint pauses the whole scheduler, so the paused world holds still while you inspect it.
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.
qn highlightRules
qn highlight FILEprints the source ANSI-colorized to the terminal — the same highlighting the REPL applies as you type.qn highlight --html FILEemits a standalone HTML page instead, using the same code styles as the pagesqn docgenerates (one style table, two consumers).
$ 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.