How Quoin code is written and how the most basic pieces fit together: comments, literals, names, message sends, and operators.
Each section opens with a Rules box (the terse version for lookup) followed by prose and examples. ⚠ Gotcha boxes flag behavior that is surprising or a common source of wrong code.
Nav: Foundations · Blocks & control · Objects · Patterns & errors · Concurrency & iteration · Networking & the web · Types · Tooling · Library & reference · Appendices
Rules
- Everything is an object. All computation is messages (method calls) sent to objects with
..- A leading dot with no receiver —
.foo— meansself.foo.- Classes are defined with
<-, methods with->. Blocks{ … }are first-class closures and objects.- There are no control-flow keywords.
if:,whileDo:,case:are ordinary methods.- Inline comments are wrapped in
". Strings are single-quoted'…'.- A newline ends a statement when unambiguous; use
;when the next line would otherwise continue the expression (i.e. it starts with.or an operator). The formatter (qn fmt) will generally figure this out for you.
Quoin is a small, uniformly object-oriented language in the Smalltalk lineage. If you can read "send the message m to the object x" for every x.m, you can read almost all of it. A method with no explicit receiver is sent to self, written with a bare leading dot:
.print: 'hello' "* sends print: to self person.greet "* sends greet to person (1..5).collect:{ |n| n * 10 }
Because control flow is just methods (Part II), there is very little dedicated syntax to memorize — most of this document is about which messages exist, not about statements.
Rules
- Comments (two forms):
"* …— line comment, runs to end of line."…"— block comment; may span multiple lines; ends at the next unescaped".- No double-quoted strings exist — a
"always begins a comment.- Separators: a newline ends a statement when unambiguous. A
;is required when the next line would otherwise continue the expression — i.e. it begins with.(a message send) or an infix operator.- Identifiers: start with a letter or
_, then letters/digits/_/?. Sodone?,flush!andmy_varare valid names.- Reserved identifiers: only
nil,true,false(can't be reassigned). The keywords are all soft keywords (reserved only as a statement prefix, so ordinary uses of the word are unaffected):use(§47) andvar/let(local declarations; §4). Identifiers are case-sensitive.
"* A line comment — everything to the end of this line is ignored. " A block comment. It opens with a quote that is not followed by '*', spans as many lines as you like, and closes at the next quote. " var x = 1 "this trailing block comment ends here" + 2 "* x is 3
A newline ends a statement when the result is unambiguous. A ; is needed when the next line would otherwise continue the previous expression — specifically when it begins with . (a message send) or an infix operator. The formatter (qn fmt) will generally figure this out for you, but a common gotcha is:
A <- { method -> {}; "* the ; ends the statement .mix:Mixin }
Without the ; on the method line, this parses as (method -> {}).mix:Mixin — the .mix: send attaches to the method definition instead of starting a new statement.
Rules
Kind Syntax Example Integer digits ( 0, or1–9then digits)42,1000000Double digits with a .and fractional digits3.14,42.0,.5String '…'(single quotes only)'hello'Symbol #name,#multi:part:,#'…'#x,#when:do:,#'+:'List #( … )space-separated#(1 2 3),#()Map #{ key: value … }#{ 'a': 1 'b': 2 }Set #< … >space-separated, unique#<1 2 3>,#<>Range a..b(half-open)1..5,5..1Regex #/…/#/^[a-z]+$/User string #Name'…'#ANSI'…'User list #Name( … )Block { … }{ |n| n * 2 }(Part II)Booleans / nil reserved identifiers true,false,nil
. followed by digits. There is no negative literal — -3 is the prefix operator - applied to 3 (see §6).\t \n \r \" \' \\, plus \uXXXX and \xXXXX (four hex digits). Plain strings do not interpolate; interpolation is a separate % form (see §45).#name, multi-part #when:do:, or a quoted form #'+:' for operators and otherwise-unspellable names. They are a distinct type (#foo.class == Symbol), compared by identity — #foo == #foo is true, but #foo == 'foo' is false; #foo.s yields the name 'foo'. Block#name and Method#selector (alias Method#name) return symbols.#(1 2 3). Maps pair key: value: #{ 'foo': 100 'bar': 200 #sym: 300 }. Sets are space-separated and hold unique elements (deduplicated by hash + ==: — a user class that overrides ==: must also override hash, or its instances dedup by identity; mirrors the any-key Map contract): #<1 2 3>, empty #<>.⚠ Gotcha — inside
#< … >, a bare>ends the set. Because the closing>would otherwise collide with the greater-than operator,>and>=are not treated as operators inside a set literal — the first bare>terminates it. To use them in an element, parenthesize:#<(a > b) c>is a two-element set. Every other operator works unparenthesized (#<a + b c>).
Rules
- Declare a local with
var(mutable) orlet(immutable), always with an initializer:var x = 5,let pi = 3.14. A plainname = exprreassigns an already-declared local — assigning an undeclared name, or reassigning alet, is a compile error.- Declaration/assignment is a statement, not an expression — you cannot nest it (
b = (a = 5)is a parse error) or use it as a condition.- Scope is lexical; blocks are closures that capture the enclosing scope.
var/letmay shadow an outer binding but cannot redeclare a name in the same scope. A recursive reference works —var f = { … f … }sees its own name.- A single-target declaration may carry a type:
var n: Integer = 5(drives the typed/unboxed tier). The type may be namespaced —var f: [IO]File = …; a bare name means the root namespace. Destructuring targets are untyped._discards a value on the left-hand side.- Destructuring:
vardeclares multiple targets from a list —var a b c = #(1 2 3). One splat*rest(or*_) may appear in any position; sub-patterns nest with( … ). Plain (keyword-less)a b c = …reassigns already-declared targets.@nameis an instance variable (only meaningful inside class/method bodies — Part III; declared in the class header).[Ns]name/[/]nameare namespaced globals (§46).Name <- exprdefines a constant (redefining one throws).
var x = 10 let greeting = 'hi' x = x + 1 "* reassign a `var` var n: Integer = 42 "* typed local var a b c = #(1 2 3) "* a=1, b=2, c=3 var first *rest = #(1 2 3 4) "* first=1, rest=#(2 3 4) var p q *_ = #(1 2 3 4 5) "* p=1, q=2, rest discarded var head (x2 y) = #(1 #(2 3)) "* head=1, x2=2, y=3 (nested) Pi <- 3.14159 "* a constant; a second `Pi <- …` would throw
Only one splat is allowed per pattern level (a compile error otherwise; each nested ( … ) sub-pattern may carry its own), but it may lead, sit in the middle, or trail. Targets after a splat bind from the end of the list:
var a *mid z = #(10 20 30 40 50) #(a mid z) "* -> #(10 #(20 30 40) 50) var *init last = #('x' 'y') #(init last) "* -> #(#(x) y) var lead (p *q) *tail = #(1 #(2 3 4) 5 6) #(lead p q tail) "* -> #(1 2 #(3 4) #(5 6))
Destructuring never errors on length: a missing position reads nil and a splat clamps to #() (on a too-short list the end-relative targets can re-read leading elements). _ (and *_) ignore the corresponding element(s).
Rules
- Unary send:
receiver.selector→42.abs,list.first.- Keyword send:
receiver.selector:arg. Multi-part selectors are one name:map.at:'k' put:vsendsat:put:.- A keyword argument is a whole expression (greedy) —
obj.m: 1 + 2passes1 + 2.- Leading dot
.selectorsends toself.- Suffix selectors:
name!(e.g..sealed!) andname?(e.g.fiber.done?) are ordinary method names.- Method (postfix) sends bind tighter than infix operators:
a.b + c.dis(a.b) + (c.d).
42.abs "* unary 'a,b,c'.split:',' "* one keyword arg scores.at:'amy' put:95 "* selector is at:put:, two args .print:'sum =' and:(2 + 2) "* leading dot = self; parenthesize operator args
Because a keyword argument greedily consumes a full expression, you usually parenthesize an operator expression that you want to pass as a single argument (as with (2 + 2) above). Multi-part selectors let methods read like prose: coll.when:cond do:action is a single send of when:do:.
Rules
- Prefix operators are no-argument sends on the operand:
-x→-,+x→+(identity),!x→!,%x→%.- Infix operators are one-argument sends and are all left-associative.
- Most infix operators are overridable methods on the receiver's type;
&&/||are special short-circuit forms (not method sends).- Precedence, loosest → tightest:
||·&&·== !=·< <= > >=·~·..·+ -·* / %·<--. Postfix sends (.method) bind tighter than any infix operator.
| You write | Compiles to | Notes |
|---|---|---|
a + b a - b a * b a / b a % b | Send("+:"…) etc. | the overridable +: -: *: /: %: method on the receiver's type (resolved class-first; no global fallback) |
a == b a != b a < b a <= b a > b a >= b | Send("==:"…) etc. | overridable ==: !=: <: <=: >: >=: methods |
a ~ b | Send("~:"…) | the match protocol — dispatches ~: on the left operand (Part IV) |
a .. b | Send("..:"…) | builds a NumberRange |
a && b a || b | short-circuit jumps | not method sends; right side is skipped when the left decides the result |
-x | Send("-") | unary minus is the no-arg - method (binary - is -:); +x is Send("+"), the identity + method |
!x | Send("!") | boolean negation (Object#'!' / Nil#'!' / the booleans) |
Operators are therefore per-type customizable: define +: on your class and + works on its instances.
Note — precedence is conventional, with two specifics worth knowing. Multiplicative (
* / %) binds tighter than additive (+ -), which binds tighter than comparison, which binds tighter than&&/||— as you'd expect, so2 + 3 * 4is14and1 + 2 == 3istrue. Beyond that: range..is looser than arithmetic, so2 .. n + 1means2 .. (n + 1); and postfix.methodbinds tighter than every infix operator, so1 .. list.countis1 .. (list.count)anda.x * b.yis(a.x) * (b.y).
Next: Part II — Blocks & control flow — closures, if:/whileDo:, truthiness, and the ^ / ^^ return operators.