Guide

Language tour

Values

KindWritten
Null and booleansnull, true, false
Numbers42, 3.14, 1e-9, 6.02e23
Decimals12.50m, 5m: exact, for money (see Decimals)
Strings"strings" or 'strings': plain text, so JSON is easy to write: '{"a": 1}'. Escapes: \n \t \r \0 \\ \" \' \ $ { }, and \u00e9or\u{1F600}` for any character
Template strings`Hello, ${name}`: the only strings that interpolate; they can span lines
Lists[1, 2, 3]
Maps{ name: "Ada", "content-type": "text/plain" }; { id } is short for { id: id }
Othersfunctions, HTML and error values
ExpressionGives
xs[0]the first item; first(xs) says the same in words
xs[-1]the last item (last(xs)): negative indexes count from the end
xs[99], a missing map key, a field of nullnull
"héllo"[1]"é": strings index by character
"abc".foo, 5.foo, xs[1.5]runtime errors: only maps have fields, and lists index by int

A name right before a template calls it with the text: shout`hi ${name}` is shout(`hi ${name}`). The builtins css and sql return the text as it is, and tell editors to highlight it as CSS or SQL.

Operators are as in JavaScript, with these meanings:

OperatorMeans
+numbers add; a string with anything is text ("n=" + 1 is "n=1", "a" + null is "anull"); two lists join; two maps merge, the right one's values winning; true + 1 and null + 1 are errors
- * %as in JavaScript on numbers; nothing else is converted ("5" * 2 is an error)
/exact division: 7 / 2 is 3.5, and a whole result stays an integer (6 / 2 is 3); floor(7 / 2) is integer division, done in ints past 2^53 too: floor, ceil, round, math.floor, math.ceil and math.trunc of an int divided by an int, written in the call (floor(a / b), a / b | floor, (a / b).floor(), math.floor(a / b), a builtin held in a variable), give the exact int, as Python's a // b does; a quotient kept in a variable first (let q = a / b) is a float, and its digits past 2^53 are gone; math has no round (write round(a / b)), on both sides
== !=deep equality, like comparing JSON: 1 == 1.0 and 0.1m == 0.1, but 1 == "1" is false (nothing is converted); an int and a float compare exactly, as in Python (9007199254740993 == 9007199254740992.0 is false), in compiled code and in the browser too; functions are equal only to themselves
< <= > >=comparison of two numbers (ints, floats and decimals mix, exactly; not a decimal and a float), or two strings by code point; a string and a number, booleans, or NaN is an error
!, &&, ||not, and, or; a && b and a || b give back one of their operands, so {loading && <Spinner/>} works
??the right side when the left is null or an error value
cond ? a : ba if cond is truthy, else b
a..bthe ints from a up to, not including, b (both ints; at most ten million)
FalsyTruthy
null, false and error valueseverything else, 0 and "" included

Because "" is truthy, name || "anonymous" keeps an empty name: write filled(name) ?? "anonymous" (since 0.7) to replace a blank or a missing name, or name ?? "anonymous" to replace only null (quillo check points these out).

Ints are 64-bit: +, -, * and unary - past their range are a runtime error (integer overflow), never a wrap. The smallest, -9223372036854775808, can be written, in a pattern too; - comes after a call, so a method on it takes brackets, (-9223372036854775808).str(), as quillo check says. An int divided by int 0, or % 0, is an error; a float divided by zero is inf or NaN, written so, and ordering either is an error. There is no **, no bitwise & | ^ << >>, no ++, no *= or /= (only =, +=, -=), no ?., no ===, no in; pow(a, b) and math.* do the arithmetic, and bit.* the bits (| is the pipe). Comparisons do not chain: 1 < x < 3 is a syntax error.

From loosest to tightest: ? :, ??, | (a pipe), ||, &&, comparisons, .., + -, * / %, unary - and !, then calls, .field, [index] and ?. So a pipe binds looser than everything but ?? and ? :: xs | find(f) != null is xs | (find(f) != null), and (xs | find(f)) != null is what was meant; xs | len ?? 0 is (xs | len) ?? 0. ?? and pipes group to the left.

Coming from JavaScript

Most expressions read as they would in JavaScript. The differences:

The built-in functions are free functions taking the list, map or string first (so xs | map(f) reads left to right), and some differ from JavaScript's methods:

JavaScriptQuillo
xs.map(f), xs.filter(f), xs.find(f)map(xs, f), filter(xs, f), find(xs, f) (null when nothing matches)
xs.reduce(f, init)reduce(xs, init, f): the start value comes before the function
Math.max(a, b)max([a, b]): max, min and sum take one list; max([]) is null
s.replaceAll(a, b)replace(s, a, b) replaces every occurrence
s.indexOf(x)index_of(s, x): null, not -1, when it is not there
parseInt(s)int(s): an error value, not NaN, for text that is not a number
s.lengthlen(s) counts characters: len("é") and len("😀") are 1
xs.sort((a, b) => a.n - b.n)sort_by(xs, (x) => x.n); sort(xs) sorts numbers as numbers
x.toFixed(2)round(x, 2) gives a number
s.match(/re/g), s.replace(/re/g, ...)re.find_all(s, "re"), re.replace(s, "re", ...); re.test, re.find, re.split
encodeURIComponent(s)url_encode(s), which also encodes !'()*; url_encode(map) gives a query string

Variables, functions, views

let todos = []todos = todos | push("x")        // reassign with =, += or -=fn add(a: int, b: int = 10) -> int = a + bfn area(w, h) {                   // block body: the last expression is the result    let a = w * h    a}let double = (x) => x * 2          // arrow functions, as in JavaScriptlet triple = x => x * 3let point = (x: int, y: int = 0) => { x, y }      // `{ x, y }` here is a map,let same = (x) => { x }                           // but `{ x }` alone is a blockview Card(title: str, children) = <section.card><h2>{title}</h2>{children}</section>

Type annotations are checked when the function is called and when it returns, and quillo check checks them before the program runs: int float dec num str date bool list map html fn response any (num is int | float, not dec). Unions are written int | str; add ? or | null to also allow null (str?, int | null). list<int> says what a list holds, and { title: str, tags: list<str>, note: str? } a map's fields; quillo check holds the program to them (a call with a map missing title, a list of strings for list<int>), and at run time they are list and map. A capitalised name in an annotation is a record's (below); one that no type declares is not reported by quillo check, and fails at the first call. A function with a capital first letter (fn Card()) is taken for a view.

fn titles(posts: list<{ id: int, title: str }>) = posts | map((p) => p.title)

Records

type names a shape, so it can be used by name, checked when the program runs, and made from what a client sends:

type Address = { city: str, zip: str? }type User = {    name: str,    email: str,    role: "admin" | "member" = "member",    // one of these strings; a default    address: Address?,    tags: list<str> = [],}type Id = int | str                         // another name for a typelet ada = User({ name: "Ada", email: "ada@example.com" })// { name: "Ada", email: "ada@example.com", role: "member", tags: [] }fn greet(u: User) -> str = `Hi ${u.name}`route POST /users { json: User } = save(json)    // the body, made a User; else no matchroute POST /users = respond(422, "not a user")match v {    User as u => u.role    _ => "not a user"}

Pipelines and method calls

a | f(b) is f(a, b). x.f(y) calls f(x, y) when x has no field f, and x.len is len(x):

orders    | filter((o) => o.total > 100)    | sort_by((o) => o.total)    | map((o) => o.id)orders.filter(o => o.paid).map(o => o.id)[3, 1, 2].sort().first()

A line starting with |, ??, &&, ||, ?, :, .method or else continues the previous line.

Control flow

n > 0 ? "pos" : "neg"if n > 0 { "pos" } else if n == 0 { "zero" } else { "neg" }   // also a valuefor x in [1, 2, 3] => x * x                 // a list: [1, 4, 9]for [key, value] in { a: 1 } => `${key}=${value}`while i < n { i += 1 }                      // null; `return` leaves it earlymatch value { ... }return x

while runs its block for as long as the condition holds (anything but null, false or an error, as with if). Numeric functions with while loops are compiled to machine code like those with for (see Compiled numbers).

Everything is an expression: if without else is null when the condition fails, while is null, for is the list of its body's values (with a block body too: for i in 0..3 { i * i } is [0, 1, 4]), and match is its arm's. A body in braces ends at its }, after => too, so what follows takes the list: for n in ns => { ... } | flatten() flattens the list of every round's values. for runs over a list, a map (as [key, value] pairs), a string (character by character), a range (not made into a list first) or null (no rounds); anything else is an error. A match that no arm takes is a runtime error, and so is a let pattern that does not match.

Patterns

The same patterns work in match, let, for, and route declarations:

PatternMatches
_, nameanything (name binds it)
42, "order", true, nullthat literal
int, str, list, ...any value of that type (dec also takes an int, numeric text or a JSON number, made a decimal; date takes text that is a date, "2026-11-20", not a date-time)
"order" | "refund"either alternative
int as nbinds what matched
[first, second, ..rest]lists
{ type: "user", name, customer.id: id, limit: int = 20 }maps (extra keys are allowed); a.b looks inside nested maps; = default fills a missing field
User, User as uwhat User(v) makes of it (see Records)

In a map pattern, key: int (a bare type) also binds key, and = default fills a field that is null as well as one that is missing. A name written like a type (Item, Max: a capital letter, then a small one) is always a type test, never a binding: let Max = 1 fails, and match x { Foo => ... } with no type Foo never matches. In "a" | "b" alternatives, a name bound in one alternative only is unbound when the other matched.

A name binds once in a pattern: [x, x], { p: a, q: a } and let [y, y] = ... are errors, in quillo check and as the program runs (`x` is bound twice in this pattern: to test that two parts are equal, use `[x, x2] if x == x2`), where they matched anything and kept the last part. Each alternative of | binds its own. A tested field binds its last name by itself, so { user.id: int, order.id: int } may test two ids; but using id there is an error, since two fields bind it: name the one you mean, user.id: int as user_id. A route that binds one name by two of its path, its query pattern and its request pattern (/a/:id ? { id: str }) is an error too.

match event {    { type: "click", x: int, y: int } => `click at ${x},${y}`    { type: "key", key: "Enter" | "Return" } => "enter"    int as n if n < 0 => "negative"    _ => "other"}

Errors

err(x) makes an error value. Builtins such as int("abc") and json.parse return errors instead of crashing. ? after an expression returns the error from the current function, and ?? falls back when the left side is null or an error, or could not be worked out: 1m / 0m ?? 0m, date.of(y, m, d) ?? today, re.test(s, typed) ?? false, v.foo ?? "none" for a v that is text, xs[i] ?? 0 for an i that is not an int, in the browser as on the server. Without ??, such a stop ends the request with a 500, so a total that overflows cannot go on as a value and reach the database unseen; with it, you have said what to do. is_err(r) asks whether a value failed and error(r) (or r.error, r.message) what it says, and on any other value that is null. Field access on null gives null (so there is no need for ?.):

fn parse_age(s: str) -> int ! str =    match int(s) {        int as n if n >= 0 => n        _ => err(`bad age: ${s}`)    }fn next_year(s) = parse_age(s)? + 1         // err("bad age: x") for "x"let age = int(request.query.age) ?? 18

At the top of a program, or of a file it uses, an error that ? passes on has no one to take it: the program stops, saying what it was, with status 1, and a server does not start half made (FEEDBACK29.md, Q-350).

An error value a statement gives and nothing takes, as a call alone on a line whose value is an error, is not lost: the log says where it was dropped, where it was made and the request it was in, and the program goes on.

main.qwe:10: an error no statement took, made at main.qwe:6: odd (pass it on with ?, give a value with ??, or take it with let _ =)

In a script it goes to stderr, and the script goes on. f()? passes it on, f() ?? x gives a value in its place, and let _ = f() takes it and says it is not wanted. Inside db.transaction(() => ...), a dropped error rolls the transaction back (see Database). quillo check warns when a call of one of the program's own functions that can fail is dropped: one that declares ! T, or whose body calls err( or uses ?. A builtin's dropped error (db.exec(...) alone on a line) is not warned of; it is logged when it happens (FEEDBACK27.md, Q-332, Q-333).

A builtin given an error where it wants something else says what the error was:

json.parse expects a str as argument 1, got error (it failed before this call, main.qwe:3: read_file: does-not-exist.json: No such file or directory)

int(r) of an error says cannot convert error to int (...) with the same reason.

? leaves the function it is written in, and in a lambda that is the lambda: map(xs, (x) => f(x)?) gives a list with the errors in it and goes on, and quillo check says so. To stop at the first failure, use a for loop, where ? leaves the function around it. filter, find, any, all and count give back an error their function gives, since it has said neither yes nor no, so filter(xs, f)? stops there; map keeps each result, an error too. An error is never made into text where a value is wanted: get, has, set and remove given one as the key, group_by and re.replace given one by their function, and url_encode given one or a value holding one at any depth (a list, a map in a list) give that error back, the first in order, where its text became a key, a part of the text or of the URL. In a component's body, ? and return are ways out of it, so a body with one is made again whole on each change, which loses the focus of an input; x?.y is such a ?, and quillo check says so. A field of null is null, so write x.y.

! T in a signature declares what the function's errors contain. It is checked: a function declared ! str that returns err(42) is a runtime error, and quillo check reports it, and a ? that would pass on the wrong kind of error, before the program runs.

Mistakes in the code itself (calling a non-function, adding a map to an int, a failed type check, recursion more than 10,000 calls deep) are runtime errors. In a route they become a 500 response, and the server logs them.

A runtime error says where it happened and how it got there, in the terminal, the log and, under --watch (the option you start a program with while working on it, quillo run main.qwe --watch), on the page. A visitor sees only internal error:

quillo: price.qwe:2: division by zero  in share, called at main.qwe:4  in split_bill, called at main.qwe:7

A deep recursion keeps its first and last five calls. Under --log json the calls are a calls field beside error. The chain is built as the error leaves each call, so calls that succeed pay nothing, and a function in machine code gives the same chain, as what fails there runs again in the interpreter. An error value is not a failure yet and has no chain, but the log says where one was made when it ends a request: main.qwe:8: db: no such column: nope. A program does not see that place: two errors with the same contents are equal wherever they were made.

In a page under --watch, the browser's console says the same, from the error's stack: the .qwe file and line, and the calls. In production it says the message alone. The browser hides a builtin's error in production as the server does (see What a route returns): shown as text, in a template, in markup or an attribute, or written by json.stringify, it is internal error, so a component made again in the browser shows no more than the server's page did; so are a builtin's errors in attribute JSON and in channel messages. Errors the program wrote, a server fn's errors (which the server has judged already) and the page's own JavaScript's are shown as they are.

Effects

with declares what a function may do besides computing a value:

fn load_user(id: int) -> map? with db = db.one("select * from users where id = ?", id)fn fetch_avatar(url: str) with http = http.get(url).bodyfn profile(id: int) -> map with db, http = { user: load_user(id), avatar: fetch_avatar(...) }

The effects, and the builtins behind each:

EffectBuiltins
dbevery db.* but db.error_kind and db.sql_name, and the same on a connection from db.connect (analytics.query); idempotent, later.status
httphttp.*, and calls to services
fsfile, files, read_file, write_file, delete_file, save_upload, check_program, image.info, image.convert
envenv, args, stdin, read_line, exit
timenow, sleep, date.today, date.now
randomrandom, random_int, uuid, crypto.token, crypto.hash_password (its salt)
netudp.listen, udp.send, udp.reply, media.rtp, channel.send, channel.broadcast

Inside a with function, a builtin that needs another effect, or a function declared with more effects, is an error, both at runtime and in quillo check (which also warns when an undeclared helper may use one). The limit holds for everything the function calls, including the lambdas it passes to map: the effects allowed are narrowed on the way in, and nothing called can widen them. Functions without with may do anything.

Routes and route groups take with too (since 0.7), written before the body, after the patterns. It holds for everything the route runs: its body, what it calls, and the middleware of its groups. A group's with is what its routes may use at most; a route in it may narrow it, and one that asks for more is an error:

route GET /report/:day with db = db.query("select * from orders where day = ?", day)route POST /notes ? { text: str } with db {    db.exec("insert into notes (text) values (?)", text)    redirect("/notes")}routes /admin | signed_in with db, time {    route GET /users = list_users()                  // db and time, as the group allows    route GET /status with time = str(now())         // narrower: no database here}

A read-only page declared with db cannot post to another site by mistake, and a check in its middleware is held to the same: [`http.post`](/docs/reference/http.post/) needs the http effect, but `route GET /report/:day` is declared `with db`. A route without with may do anything, as a function without it may. A job is a function given to schedule, so it takes with as a function does (see Scheduled jobs).

use "store.qwer"            // //! effects: fsuse "hi.qwer"               // no effects linefn keep(text: str) with fs = store_put(text)        // allowed: store.qwer says fsfn stamp() with time = store_put(str(now()))        // error: store_put needs fsfn greet() with time = hi("Ana")                    // error: hi.qwer says nothingprint(hi("Ana"))                                    // outside any `with`: runs

HTML literals

HTML is a value, not a string. It is written like JSX, with no <> fragments: text goes in as it is, expressions go in {...}, and everything is escaped unless it is already HTML:

<li.todo #todo-{t.id} class={t.done && "done"} data-id={t.id}>    {t.done ? <s>{t.text}</s> : t.text}    <button disabled={!editable}>Edit</button>    Added {t.age} days ago &mdash; {t.owner}</li>

From HTML

HTML pasted into a literal mostly stays as it is: entities, bare attributes, onclick="...", SVG's viewBox, custom elements, <template>, <!-- comments -->, 'single-quoted' and unquoted attribute values, and a <!DOCTYPE html> before <html>. A <template>'s markup is its content (tpl.content in the page's JavaScript), from the server and made in the browser alike. A form's reset brings the fields a component made in the browser back to their first values, as it does the server's. What to change:

HTML as writtenIn Quillo
<p>one<p>two, <li>one<li>two, closed by what followsclose each one: the error says where (`<p>` is never closed: add `</p>` before `</div>` on line 1)
<style>body { color: red }</style><style>{css`body { color: red }`}</style>: braces in markup are code
<script>if (a < b) { go() }</script>the code in a string, <script>{raw(CODE)}</script>, or in a file of its own
<DIV CLASS="a">lowercase, <div class="a">: a capital starts a view's name
{ and } in text, as a code sample in a <pre> has them{"{"} and {"}"}, or the sample in a string, <pre>{SAMPLE}</pre>
a $ before a { in text{"$"}
text and an inline element on lines of their own, Hello, then <b>Ann</b>{" "} at the end of the line, as HTML's space between them is none here (quillo check says where)

From JSX

Markup is written like JSX, and React's habits mostly work: className is told, onClick={f} is a handler, {cond && <p/>}, xs.map(x => <li/>), style={{ fontSize: 12 }}, {/* comments */}, <img></img>. What to change, where quillo check or the error says it too:

JSX as writtenIn Quillo
className="a", htmlFor="n"class="a", for="n"
<>...</>, <Fragment>, <React.Fragment>one element around them, like <div>, or a list
function Card(props), props.title; function Card({ title })view Card(title): a view's props are its parameters, by name
<Card.Header/>, a member tag<Header/>, imported by its own name
const Tag = "h1"; <Tag/>, a dynamic taga match choosing between written tags, or a view per tag
{children} without declaring ita children parameter: a body given to a view without one is an error
xs.map((x, i) => ...)for [i, x] in enumerate(xs) => ..., as map passes one item
{...xs} among children{xs}: a list shows each item
ref={el}@mount={(el) => ...}, given the element
dangerouslySetInnerHTML={{ __html: s }}{raw(s)} between the tags
defaultValue, defaultCheckedvalue, checked: a field with no state keeps what is typed
onClickCaptureonClick (or @click): there is no capture phase
SVG's strokeWidth, fillRuleSVG's own stroke-width, fill-rule
{cond && " (admin)"} with 0 or ""0 and "" are truthy in Quillo: compare, {n > 0 && ...}
<input value={x} onChange={(e) => setX(e.target.value)}/>works as is; or value={x} alone, which follows typing by itself

To send an HTML file unchanged, serve it with file() or files(), or a whole folder with quillo serve (see Static files); raw(read_file("page.html")) puts it in a page of the program's, with Quillo's scripts.

Thread
28Useful? Like it, or add to its thread below.

Thread 0 posts

On Language tour: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.

    Edit this page on GitHub · made from the user guide · questions on Discord or r/quillo