Quill Language Specification
Version 0.1 draft, describing Quill as implemented in release v0.4.8.
This document says exactly what Quill programs mean. The README shows where the language is going; this spec covers what works today, and marks planned features clearly in §12. When the implementation and this document disagree, that is a bug in one of them. Please open an issue.
Contents
- Programs and files
- Lines, indentation and comments
- Words and names
- Kinds of values
- Writing values
- Calculations and conditions
- Statements
whenandfor- Tests:
checkand doc examples - When Quill finds mistakes
- The
quilltool - Planned, not yet implemented
- Grammar summary
1. Programs and files
A Quill program is a text file ending in .quill, written in UTF-8. Quill reads it from top to bottom and runs each statement in order.
Before 0.8, Quill files ended in .ql. Such a file still runs, checks and tests, with a note on standard error asking to rename it (in color in a terminal, unless the setting NO_COLOR is set), and bring NAME reads NAME.ql when there’s no NAME.quill. A later version will stop reading .ql files.
Before anything runs, Quill reads the whole file and checks it (§10). If it finds any mistake, nothing runs at all.
2. Lines, indentation and comments
2.1 One statement per line
Each statement sits on its own line. There are no semicolons and no braces for blocks. A line ends at a new line character (\n, or \r\n on Windows).
name = "Amina"
say name
Blank lines are ignored.
2.2 Indentation
Indentation marks a block: lines that belong to the line above them. Today, the cases of a when, the body of a for (§8), and the fields of a record (§5.5) are indented.
- A block moves in by exactly 2 spaces from the line above it. One space, four spaces, or any other width is a mistake.
- A deeper block moves in by another 2 spaces: 2, then 4, then 6.
- A block ends when a line returns to an earlier level. That line must land on a level of 2 spaces, in the same column as the line that opened the block.
- Indent with spaces. A tab is a mistake, because editors show tabs at different widths.
- Blank lines and comment lines never start or end a block.
when
total > 100 -> say "Free delivery" -- in the block
else -> say "Delivery: 5" -- in the block
say "Thanks!" -- back at the left: the block has ended
2.3 Comments
--starts a comment. Everything after it on the line is ignored.---starts a doc comment, which documents the code that follows it. It is ignored when the program runs.--- example:is not a comment. It is a test (§9.2).
--- Adds 5% VAT to a price.
--- example: 100 * 1.05 == 105
vat = 1.05 -- the UAE rate
3. Words and names
3.1 Names
A name starts with a letter or _, followed by letters, digits or _. Letters from any language are allowed (café, prix). Names are case-sensitive: total and Total are different names.
A name can’t start with a digit: 2x is a mistake.
3.2 Reserved words
These words have a meaning in Quill and can’t be used as names:
say check var set when else for in list record of at get sum with fn return maybe found missing fails fail on try require ensure where type round split convert bring permit as ask route respond true false and or not
4. Kinds of values
Every value has a kind. The checker knows the kind of every value before the program runs, so it can catch mistakes like adding text to a number.
| Kind | Examples | What it’s for |
|---|---|---|
| Text | "Amina", "Total: {total}" | words and sentences |
| Number | 42, 0.10, -3.5 | exact decimal numbers |
| true/false | true, false, total > 10 | conditions |
| list | list "coding", "reading", list of text | values in order, all one kind |
| record | record city = "Anytown" | named fields kept together |
A name keeps the kind of the value it was created with for the whole program (§7.4).
Quill never turns one kind into another by itself. "5" is text, and 5 is a number.
4.1 Numbers are exact
Numbers are decimals, not binary floating point, so they never round by surprise:
say 0.1 + 0.2 -- 0.3
say 100 * 1.05 -- 105.00
- A number holds up to 38 digits in total, before and after the point. A number written with more digits is a mistake, and a calculation whose result would need more stops the program with an error (§10.2).
- Adding, subtracting and multiplying are always exact.
- Dividing keeps up to 20 digits after the point. When the answer needs more, the last digit is rounded half to even:
1 / 3is0.33333333333333333333, and2 / 3is0.66666666666666666667. - The remainder
a % bhas the sign ofa:7 % 3is1and-7 % 3is-1. It works on decimals too:7.5 % 2is1.5.
4.2 How numbers are shown
A number keeps the digits after the point that it was written with, so prices stay looking like prices:
| Calculation | Shows | Rule |
|---|---|---|
0.10 | 0.10 | as written |
0.10 + 0.2 | 0.30 | adding and subtracting keep the most digits after the point |
2.5 - 0.5 | 2.0 | |
1.50 * 2 | 3.00 | multiplying keeps at least as many digits as the longer side |
10 / 4 | 2.5 | dividing drops trailing zeros… |
9.00 / 3 | 3.00 | …but keeps at least as many digits as the longer side |
Comparing ignores trailing zeros: 0.10 == 0.1 is true.
4.3 How values are shown
say and {} inside text show values like this:
- Text: its characters, without quotes.
- Number: as described in §4.2.
- Money: the amount, a space, and the currency. The amount is shown in whole units (
90 USD), or with every digit of the currency’s smallest unit (7.50 USD, not7.5 USD;1.500 KWD). Digits smaller than that unit stay visible (0.125 USD). Most currencies have 2 digits. BIF, CLP, DJF, GNF, ISK, JPY, KMF, KRW, PYG, RWF, UGX, VND, VUV, XAF, XOF and XPF have none. BHD, IQD, JOD, KWD, LYD, OMR and TND have 3. - true/false: the word
trueorfalse. - list: the items separated by commas, or
an empty listwhen there are none.list "coding", "reading"is shown ascoding, reading. - record: one field per line, as
name: value.
5. Writing values
5.1 Text
Text is written between double quotes on one line: "Hello, World!". Single quotes are a mistake.
Inside text, a backslash starts an escape:
| Write | To get |
|---|---|
\n | a new line |
\t | a tab |
\" | " |
\\ | \ |
\{ | { |
\} | } |
Any other character after a backslash is a mistake.
Text with quotes inside. Text between three quotes holds " as itself, with no backslash needed. Escapes and {} still work:
say """She said "hi"."""
5.2 Values inside text
{ and } inside text hold a value. Quill works out the value and puts it into the text, shown as in §4.3:
name = "Amina"
qty = 3
say "Hi {name}, that's {qty * 2} items" -- Hi Amina, that's 6 items
- Any value or calculation can go inside
{}, as long as it fits on the line. - Only one value goes inside each pair of braces.
- Braces can’t be empty, can’t hold another
{, and can’t hold text in quotes. To use text, give it a name first. - A
}that doesn’t close a{is a mistake. Write\{and\}to show braces themselves.
5.2.1 Working with text
typed = " Amina Okafor "
name = trim typed
say length of name
say uppercase name
say name contains "Oka" and name starts with "Am"
say replace " " with "_" in name
parts = split "Amina,80,paid" by ","
say join parts with " / "
say list "coding", "reading" contains "coding"
12
AMINA OKAFOR
true
Amina_Okafor
Amina / 80 / paid
true
trim TEXTisTEXTwithout spaces, tabs, or new lines at the start and the end.uppercase TEXTandlowercase TEXTchange the case of every letter. Each gives back new text; the value it read doesn’t change.length of TEXTis how many charactersTEXThas, as a number. An accented letter such aséis one.length ofa list is a mistake that suggestscount of(§5.6).TEXT contains PART,TEXT starts with PART, andTEXT ends with PARTare true/false. Capital and small letters are different.LIST contains ITEMis true when an item of the list equalsITEM, andITEMmust be the kind of the list’s items. These bind like a comparison (§6.1):email contains "@" and paidis(email contains "@") and paid.replace OLD with NEW in TEXTisTEXTwith everyOLDchanged toNEW.OLDcan’t be empty.split TEXT by SEPARATORis a list of the texts between eachSEPARATOR, in order, with the separators left out. Two separators side by side give an empty text between them.SEPARATORcan’t be empty, andsplit TEXT into ...is a mistake;intoshares out money (§8.7).join LIST with SEPARATORis one text: the items ofLIST, each shown assayshows it (§4.3), withSEPARATORbetween them. The items are text, numbers, money, or true/false.- Every value a tool reads must be text where the tool says text. A number becomes text inside braces:
length of "{year}". trim,uppercase,lowercase,join, andreplaceare tools only when a value follows them on the same line. Right against(, as intrim(x), they call a function of that name; with a space,trim (parts at 1)is the tool. A call totrim(x)with no functiontrimis a mistake that says the tool takes no brackets.lengthis a tool only beforeof,containsonly between two values, andstartsandendsonly beforewith. Everywhere else they are ordinary names.
5.3 Numbers
A number is written as digits, with an optional decimal point followed by more digits: 42, 0.10, 3.14159.
- There must be a digit before the point:
.5is not a number. Write0.5. - A point with no digit after it is a field dot (§5.5), not part of the number.
3.with no field name is a mistake. Write3.0for a number. - Negative numbers are written with
-in front (§6.1).
5.4 true and false
true and false are the two true/false values. Comparisons (§6.2) also produce them.
5.5 Lists and records
A list is values in order, all of one kind. The first item is at position 1.
hobbies = list "coding", "reading", "traveling"
say hobbies at 1
- Items are separated by commas. A comma with nothing after it is a mistake.
- Every item has the same kind.
list "coding", 2is a mistake. - An empty list names its kind:
list of text,list of number, orlist of true/false. VALUE at POSITIONreads one item.POSITIONis a whole number, and1is the first item.0and numbers that aren’t whole are mistakes. A position past the end is found while running, unless both the list and the position are written out, in which case it is found before running.- Square brackets are a mistake. The word is
list.
A record is named fields.
address = record
street = "123 Main St"
city = "Anytown"
- On its own line,
recordis followed by an indented block. Each field isNAME = value, indented by exactly 2 spaces (§2.2). - A short record can be written on one line:
record city = "Anytown", state = "CA". - A record has at least one field. The same field name twice is a mistake.
VALUE.NAMEreads one field. A field that isn’t there is a mistake, and Quill names the fields the record does have.- Two records are the same kind when they have the same field names and each matching field has the same kind. The order of the fields does not change the kind.
- Braces are a mistake here. The word is
record. Braces inside text still show a value (§5.2).
set can replace one item or one field when the list or record was created with var:
set hobbies at 2 = "writing"
set address.city = "Springfield"
The new value must match the item’s or the field’s kind. for NAME in LIST walks a list (§8.5).
5.6 Asking for a field
get, sum, count, average, and sorted by read a list of records without a loop. for still walks the list when each record needs its own step (§8.5). A program can use either.
sisters = record name = "Nchang Sisters", fee = 80000 USD
brass = record name = "Bamenda Brass", fee = 150000 USD
roster = list sisters, brass
till = sum fee of roster
fees = get fee of roster
say till
say fees
230000 USD
80000 USD, 150000 USD
A missing amount is left out of the sum:
fn fee_for(name: text) -> maybe USD
when
name == "Quiet Hour" -> return missing
else -> return found 40000 USD
quiet = record name = "Quiet Hour", fee = fee_for("Quiet Hour")
drums = record name = "Market Drums", fee = fee_for("Market Drums")
roster = list quiet, drums
say sum fee of roster
40000 USD
Several fields stay together on a smaller record. Name the record before the dot:
sisters = record name = "Nchang Sisters", age = 24, fee = 80000 USD
people = get name, age, fee of list sisters
person = people at 1
say person.name
say person.fee
Nchang Sisters
80000 USD
A dot after of reads a field further in. One nested record gives that one value. A list on a record gives a list:
school = record
name = "Harvard"
acceptances = record gpa = 4.0
say get gpa of school.acceptances
up = record place = "Up Station", acts = list "Bamenda Brass", "Hilltop Duo"
old = record place = "Old Town", acts = list "Market Drums"
desk = record bookings = list up, old
say get place of desk.bookings
4.0
Up Station, Old Town
get can also name the list and the field embedded in each record. One record, or a list of records, can hold that list:
up = record place = "Up Station", acts = list "Bamenda Brass", "Hilltop Duo"
old = record place = "Old Town", acts = list "Market Drums"
desk = record bookings = list up, old
tonight = get bookings with acts in desk
first = tonight at 1
say first.place
saturday = record day = "Saturday", bookings = list up
sunday = record day = "Sunday", bookings = list old
nights = list saturday, sunday
all = get bookings with acts in nights
second = all at 2
say second.place
Up Station
Old Town
count of says how many items a list has. average divides the sum of one field by how many records have it. sorted by puts records in order of one field:
amina = record name = "Amina", score = 80, fee = 500 USD
omar = record name = "Omar", score = 64, fee = 300 USD
john = record name = "John", score = 45, fee = 200 USD
students = list amina, omar, john
say count of students where score >= 50
say average score of students
say average fee of students
say get name of students sorted by score
say get name of students sorted by highest fee
2
63
333.33 USD
John, Omar, Amina
Amina, Omar, John
sum NAME of LISTaddsNAMEfrom each record.NAMEis a number or money in one currency. The result is that number, or money in that currency. When the value afterofis one record, the sum is that one amount:sum fees of school.get NAME of LISTgives a list ofNAMEfrom each record.get NAME { "," NAME } of LISTgives a list of records. Each record keeps only the named fields, in the order they were written. The same field twice is a mistake. When the value afterofis one record, one field comes back as that value, and several fields come back as one record. A dot afterofreaches a field further in:get gpa of school.acceptances,get place of desk.bookings.record = list at 1thenrecord.namereads a field of that item.(list at 1).nameis the same value.- A field that is
maybecontributes only when it isfound.missingis left out. When every money amount is missing,sumis0in that currency. get NAME with NESTED in SOURCEgives the listNAME. Each record in that list has the fieldNESTED.SOURCEis one record, or a list of records that each holdNAME. A list of records is gathered into one list.LIST where CONDITIONgives the records ofLISTwhoseCONDITIONis true, in their order. InsideCONDITION, each field of the records is a name, and it hides a value of the same name; other values are read as usual.CONDITIONmust be true/false. When nothing matches, the result is an empty list.wheremay follow again:students where score >= 50 where paidkeeps records for which both are true.wherebinds after a whole value, so its condition takes inandandor:students where score >= 50 and paid. Afterof, awhereapplies to the list beforegetorsumreads it:sum fee of roster where paid == falseadds the unpaid fees.count of LISTis how many itemsLISThas, as a number.LISTis any list; one record, text, or a number is a mistake.count of roster where paid == falsecounts the records that match.average NAME of LISTis the sum ofNAMEdivided by how many records have it.NAMEis a number or money in one currency, and amissingamount is left out of both. Money is rounded to its currency’s smallest unit, half to even (§8.7); a number keeps up to 20 digits after the point, as division does (§4.1). One record gives that one amount. When no record has an amount, the program stops (§10.2).LIST sorted by NAMEgives the records ofLISTin order ofNAME, lowest first.sorted by highest NAMEputs the highest first, andsorted by lowest NAMEis the same assorted by NAME.NAMEis a number, money, or text. Numbers and money go from smallest to largest; text goes in alphabetical order, with capital and small letters compared together. Records with the same value keep their order.NAMEthat might bemissing, a true/false field, or a list that is not records is a mistake.sorted bybinds likewhere, and either may follow the other:students where paid == false sorted by highest fee.count,average,sorted,by,highest, andlowestare ordinary names everywhere else.countis a word only right beforeof,averageonly before a field andof, andsortedonly beforeby:var count = 0andfn average(total: number, many: number)still work.- The field names are checked before anything runs. A missing field, a list that is not records, or a
sumof text is a mistake, and nothing runs. In awhere, a name that is neither a field nor a value, a list that is not records, or a condition that is not true/false is a mistake.
6. Calculations and conditions
6.1 Operators
From the tightest binding to the loosest:
| Level | Operators | Works on | Gives |
|---|---|---|---|
| 1 | ( ... ) | anything | the value inside |
| 2 | -a | Number | Number |
| 3 | a * b, a / b, a % b | Number, Number | Number |
| 4 | a + b, a - b | Number, Number | Number |
| 5 | a == b, a != b | two values of the same kind | true/false |
| 5 | a < b, a <= b, a > b, a >= b | Number, Number | true/false |
| 6 | not a | true/false | true/false |
| 7 | a and b | true/false, true/false | true/false |
| 8 | a or b | true/false, true/false | true/false |
Operators on the same level work from left to right: 10 - 4 - 3 is (10 - 4) - 3, which is 3.
So 1 + 2 * 3 is 7, n % 15 == 0 compares the remainder with 0, and not a == b means not (a == b).
6.2 Comparing
==and!=compare two values of the same kind. Comparing different kinds, like5 == "5", is a mistake rather thanfalse, because it is almost never what was meant.<,<=,>and>=compare numbers only.- Comparisons can’t be chained:
1 < x < 10is a mistake. Write1 < x and x < 10.
6.3 and, or and not
and and or stop as soon as the answer is known:
- In
a and b, ifaisfalse,bis not worked out. - In
a or b, ifaistrue,bis not worked out.
So a condition can protect a calculation:
say count != 0 and total / count > 10 -- never divides by zero
6.4 + doesn’t join text
+ only adds numbers. To build text from values, put them inside text (§5.2):
say "Hi " + name -- mistake: Quill won't guess whether you meant to add or join
say "Hi {name}" -- right
7. Statements
7.1 say
say VALUE
Shows the value (§4.3), followed by a new line.
7.2 Creating a value: name = value
total = price * qty
Creates a new name for a value. This value can’t change.
- A name must be created before it is used. Quill reads from top to bottom.
- A name can only be created once in a program.
- A value can’t be made from itself:
x = x + 1is a mistake whenxdoesn’t exist yet.
7.3 Values that can change: var
var visits = 0
Creates a value that can be changed later with set. Otherwise it follows the same rules as §7.2.
7.4 Changing a value: set
set visits = visits + 1
Changes a value created with var.
- The name must already exist, and it must have been created with
var. Changing a value that isn’t markedvaris a mistake. This makes every change visible when you read the code. - The new value must be the same kind as the old one. A Number stays a Number.
set NAME.FIELD = valueandset NAME at POSITION = valuechange one field or one item. The list or record itself must be markedvar. The new value must match that field or item.name = ...on a name that already exists is a mistake, even for avar. Usesetto change it.
7.5 check and --- example:
Tests. See §9.
7.6 when
Chooses what to do. See §8.
7.7 A value on its own
A line holding only a value or calculation, like price * 2, is a mistake, because nothing would happen to it. Quill suggests say price * 2, or check ... for a comparison.
8. when
when runs the first case whose condition is true:
when
total > 100 -> say "Free delivery"
total > 30 -> say "Delivery: 5"
else -> say "Delivery: 10"
8.1 Cases and ->
The cases go on the lines below when, indented further than the when. Each case is one line:
CONDITION -> STATEMENT
-> separates the question from the answer. It reads as “then”: when total > 100 is true, then say "Free delivery".
- On the left of
->is a condition, which must be true/false. - On the right of
->is the statement to run when that case is chosen, on the same line. ->is only used insidewhen. Anywhere else it is a mistake.
-> is used instead of : or then so the condition and the action are clearly separated even when lined up in a column, as above. The README uses the same arrow for matching maybe values and failures (§12), so it will always mean “in this case, do this”.
8.2 How a when runs
- Quill tests each condition in order, from the top.
- The first case whose condition is
trueruns. No other case runs, even if its condition is also true. elsematches when no case above it was true. It is optional, and if present it must be the last case.- If no case is true and there is no
else, nothing happens.
8.3 What a case can do
The statement after -> can be say, set, check, --- example: or another when:
when
logged_in -> when
is_admin -> say "Welcome, admin"
else -> say "Welcome back"
else -> say "Please log in"
A case can’t create a value (name = ... or var name = ...), because the value would only exist when that case runs. Create it before the when with var, then change it inside with set, or use a when that gives a value (§8.4.1):
var label = ""
when
total > 100 -> set label = "big order"
else -> set label = "order"
say label
8.4 Rules
- A
whenmust have at least one case. - A
whenwhose cases are conditions stands on its own line.when VALUEon one line opens a maybe (§8.8). - A case that comes after
elsecan never run, so it is a mistake.
8.4.1 A when that gives a value
when at the end of a line, in the place of a value, chooses a value instead of a statement:
delivery = when
total > 100 -> 0
total > 30 -> 5
else -> 10
- It can stand wherever a whole value is written at the end of a line: after
=,var NAME =,set NAME =,sayandreturn. The cases are on the lines below, indented. - Each case is
CONDITION -> VALUEorelse -> VALUE. The value is an expression, not a statement:say,setand the other statement words are a mistake there. - The last case must be
else, so there is always a value. A case afterelseis a mistake. - Every condition must be true/false. Every case must give the same kind of value.
missingfits only when the other cases givefoundvalues; thewhenis then a maybe. - The cases are tested in order, and the value of the first true case is the value of the
when. Only that case’s value is worked out. when VALUEon one line still opens a maybe (§8.8); it does not give a value.
8.5 for
for repeats the lines under it for each number from the start through the end, including both:
for i in 1..3
say i
This shows 1, then 2, then 3.
for NAME in START .. END
STATEMENT
STARTandENDare numbers. They are worked out once, before the first pass.- The range is two dots. Three dots (
1...3) is a mistake; write1..3. NAMEholds the number for this pass. It exists only on the lines under thefor. After the loop, the name is gone, unless a value with that name already existed. In that case the loop uses the name while it runs, and the earlier value is unchanged afterwards. The range sees the earlier value:for i in i..12wheniis10counts from 10 through 12.- The loop moves
NAMEto the next number by adding 1.seton that name is a mistake. To keep a total, create avarbefore the loop and change it withset. - A line under
forcan’t create a value (name = ...orvar name = ...). - If
STARTis greater thanEND, the body never runs.for i in 5..5runs once. - The body is indented further than
for, andforstands on its own line.
for also walks a list, once for each item:
for hobby in hobbies
say hobby
hobbiesis a list. It is worked out once, before the first pass.hobbyholds the item for this pass. It exists only on the lines under thefor, andseton that name is a mistake.- The first pass takes the item at position 1. An empty list runs the body zero times.
for i in 1with no..and no list is a mistake. A range needs...
8.6 Functions
A function names a piece of work. A call runs it.
fn double(n: number) -> number
return n * 2
say double(21)
This shows 42.
fn NAME "(" [ parameter { "," parameter } ] ")" [ "->" kind ]
STATEMENT
parameter = NAME ":" kind [ "where" bound ".." bound ]
kind = "text" | "number" | "true/false" | "list" "of" kind
- The body is indented further than
fn, andfnstands on its own line. -> kindmeans the function gives a value of that kind back. Every path through the body has to reachreturn VALUE, and that value has to be the named kind. Parameters may each have their own kind. The kind after->is the only kindreturnmay give back. Quill does not convert one kind into another.- With no
-> kind, the function gives nothing back. Call it on its own line.returnon its own leaves the function early. A value afterreturnis a mistake until the function names a kind after->. - A parameter exists only inside the function. It is not a
var. Names created in the body exist only inside the function, and they are created again on every call. - A function does not see values created outside it. Pass them in as parameters.
- A function can call itself, and it can call a function written above it.
returnoutside a function is a mistake.- A parameter’s kind is
text,number,true/false, a currency such asUSD, a type such asBooking(§8.11),maybeone of those, orlist ofone of those. Kinds are written in lowercase;IntandTextare not kinds. - The first line of the body may be
fails NAME { "," NAME }. A failure name starts with a capital letter.fail NAMEleaves the function with that failure, and the name has to be in the list.
8.7 Money
An amount is a number, a space, and a currency of three capital letters.
say 100 USD + 50 USD
This shows 150 USD.
+and-need two amounts in the same currency. Different currencies are a mistake.*and/can combine an amount with a plain number:100 USD * 0.90,100 USD / 2. A number can also come first in a multiplication.- The currency is the kind in a function:
price: USD. - A currency written in lowercase, such as
usd, is a mistake. WriteUSD. - “Cents” below means the currency’s smallest unit (§4.3).
round [ "up" | "down" ] VALUE [ "to" ( "cents" | VALUE "places" ) ]
split VALUE "into" VALUE
split VALUE "by" VALUE
VALUE "%" "of" VALUE
convert VALUE "to" CURRENCY "at" VALUE
round VALUErounds a number or money to a whole number.to N placeskeepsNdigits after the point, withNa whole number from 0 to 20; the result has exactlyNdigits.to centsis for money and keeps the currency’s smallest unit. Plainroundgoes to the nearest value, and exactly half goes to the even digit (10.125→10.12,10.135→10.14).round upgoes to the next larger value, andround downto the next smaller one (round up -2.1is-2).split AMOUNT into COUNTgives a list ofCOUNTamounts in whole cents that add up toAMOUNT.COUNTis a whole number, at least 1. Each part isAMOUNT / COUNTrounded down to cents, and the cents left over go one each to the first parts:split 100 USD into 3is33.34 USD, 33.33 USD, 33.33 USD.split AMOUNT by SHARESgives one amount for each number in the listSHARES, in proportion to it. Shares are zero or more, and at least one is above zero. Each part is rounded down to cents, and the cents left over go one each to the first parts.splitneeds an amount with no part smaller than a cent; otherwise the program stops. Round it first.- When the value after
splitis text,split TEXT by SEPARATORcuts it into a list of text instead (§5.2.1). P% of VALUEisVALUE * P / 100, for a number or money.Pis a number.convert AMOUNT to CURRENCY at RATEis money inCURRENCY:AMOUNT * RATE.RATEis a number above zero. Converting to the same currency is a mistake.- These are mistakes found before running:
roundof something that isn’t a number or money;to centson a number; places that aren’t a whole number from 0 to 20;splitof something that isn’t money,intoa count that isn’t a whole number of at least 1, orbysomething that isn’t a list of numbers;% ofa value that isn’t a number or money;convertof something that isn’t money, to the same currency, at a rate that isn’t a number or is a written value of zero or less. When those values are only known while running, the same mistakes stop the program (§10.2). round,splitandconvertare reserved words (§3.2).up,down,to,cents,places,intoandbyare ordinary names everywhere else.
8.8 Maybe
-> maybe KIND means the function gives back that kind, or gives back nothing.
return found VALUEgives the value back. The value’s kind isKIND.return missinggives nothing back. It is allowed when the function’s result is a maybe.when VALUEopens a maybe. The cases arefound NAME -> STATEMENTandmissing -> STATEMENT. Both are required.NAMEexists only on thefoundline.- Using a maybe with
say, or as a number, is a mistake. Open it withwhenfirst.
8.9 Failures
A function names the failures it can leave with. The caller handles each one, or passes them on.
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
failsgoes at the top of the function, with anyrequireandensurelines (§8.10).var name = charge(...)(orname =, orset name =), then indentedon NAME -> ...for every declared failure. On success the value is stored and the program continues. On failure the matchingonline decides:on NAME -> STATEMENT(say,set,check,when,fail,return, …): the statement runs and the program stops, so later lines cannot use a value the call never gave back. Inside a function, it leaves the function, and a function that gives a value back mustreturnon that line.on NAME -> VALUE: the value is stored in place of the call’s answer, and the program continues. It must be the kind the call gives back (missingfits a maybe).- Each
online chooses for itself, so one failure can give a value while another stops.
- A call on its own line gives nothing back, so the
onlines under it run statements. A call after->there, likeon Oops -> log("x"), is run as a statement; any other value is a mistake. try callis an expression inside a function whosefailslist includes every failure of that call. It passes those failures upward.tryon a line by itself is a mistake (§7.7); a call on its own line passes a failure upward withon NAME -> fail NAME.- A call that can fail, with no
onand notry, is a mistake.
8.10 Contracts: require, ensure and where
A function can state what has to be true when it is called, and what it promises is true when it gives back.
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
ensure result >= 0 USD
return balance - amount
require VALUEandensure VALUEgo at the top of the function, before its first other line. They may be mixed withfailsin any order. A function can have any number of each.VALUEis a condition: it must be true/false (§6). Only one condition fits on a line; join conditions withand.requirecan read the parameters. Everyrequireis tested in order when the function is called, before its first line runs.ensurecan read the parameters andresult, the value the function gives back. Everyensureis tested in order at eachreturn, after the value is worked out and before the function leaves. A function that leaves withfaildoes not test itsensurelines.resultis defined only insideensure. Elsewhere it is an ordinary name.- A parameter can carry a range:
guests: number where 1..500. The parameter’s kind must benumberor a currency. Each end is a written number or amount, optionally with a leading-(-40,0 USD), of the same kind as the parameter, and the first is not larger than the second. The range includes both ends. When the function is called, each parameter’s range is tested in order, before everyrequire. - When a call passes a written number or amount outside a range, that is a mistake found before running (§10.1). Any other value outside the range stops the program (§10.2) with the function’s name, the parameter, the range, its value, and the line of the call.
- When a contract is true, nothing happens. When it is false, the program stops (§10.2) with the function’s name, the condition, and the value of every name it read. A broken
requirealso names the line of the call. - These are mistakes:
ensureon a function with no->;requirethat readsresult; a parameter calledresultin a function with anensure; a condition that isn’t true/false;requireorensureoutside a function or below its first other line; a secondfailsline;whereon a parameter that isn’t a number or a currency, on the kind after->, with an end that isn’t a written value, with an end of another kind, or with its ends in the wrong order. - Contracts are not tests:
quill testdoes not count them.
8.11 Types
A type names the fields every record of that kind has.
type Booking
venue: text
guests: number where 1..500
fee: USD
hall = Booking venue = "Town Hall", guests = 120, fee = 500 USD
type NAME NEWLINE INDENT { NAME ":" kind [ "where" bound ".." bound ] NEWLINE } DEDENT
NAME field { "," field }
NAME NEWLINE INDENT { field NEWLINE } DEDENT
- A type is written at the top level of a file. Its name starts with a capital letter and is not three capitals (those are currencies). Each field is on its own line, with a kind as in §8.6 and an optional range as in §8.10. A type can be used above the line that writes it.
NAME field, ...makes a record of the type. It has every field of the type, each of the field’s kind, and no others. Amaybefield is givenmissingorfound VALUE. The record’s fields are kept in the type’s order.- A record of a type is a record (§5.5):
.,get,sumandwhereread it, and it is shown like any record. - A field’s range is tested each time a record of the type is made. A written number or amount outside it is a mistake found before running; any other value stops the program (§10.2), naming the type, the field, the range and the value.
- Where a kind names a type (
b: Booking,-> Booking,list of Booking), a record with the same fields and kinds fits. A function with a parameter of a type tests that type’s ranges when it is called, before its ownrequirelines. - These are mistakes: a type name that isn’t capitalized; two types with the same name; a type with no fields, or two fields with the same name; a type inside a function; a kind naming a type that doesn’t exist; a type that holds itself, directly or through
maybe,list ofor another type; a record of a type with a field missing, an unknown field, or a field of the wrong kind.
8.12 Bringing in files: bring
A program can use the functions and types written in other files.
bring billing
bring "shared/money.quill"
bring NAMEreadsNAME.quillin the folder of the file that writes it.bring "PATH"reads the file atPATH, counted from that folder, with/between folders; it must end in.quill.bringgoes at the top level of a file.- A brought file holds
fn,typeandbringlines, comments, and its own tests. Its functions and types can be used by name in the program, as if written above its first line. Itscheckand--- example:lines run only when that file itself is run or tested. Any other line in a brought file is a mistake. - A file brought twice, directly or through other files, is read once. Files that bring each other in a circle are a mistake.
- A function or type name written in two different files is a mistake.
- A problem in a brought file is reported with that file’s name and line.
web,db,aiandhttpname Quill’s modules.bring webbrings the module, not a file.
8.13 Permits, files and settings
A program touches nothing outside itself unless it permits it.
permit read "config"
permit write "reports"
permit env "STRIPE_KEY"
permit net "api.stripe.com"
permit KIND "TARGET"goes at the top level of the program that is run.KINDisreadorwrite(a file or folder),net(a host), orenv(a setting). A brought file (§8.12) can’t contain a permit.- A file or folder permit covers that path and everything inside it, with
.and..worked out first, so..can’t leave a permitted folder."."covers the program’s folder and everything below it. Paths are counted from the program’s folder. permit netnames a host such asapi.stripe.com, or*.example.comfor every host below that domain, withouthttps://or a path. A host without a port allows any port.permit envnames one setting, made of letters, digits and_.- Quill’s own functions:
| Function | Gives back | Fails with | Needs |
|---|---|---|---|
read_file(path: text) | the file’s text | FileMissing | permit read |
write_file(path: text, text: text) | nothing; makes missing folders | CantWrite | permit write |
file_exists(path: text) | true/false | permit read | |
env(name: text) | maybe text | permit env | |
json(value) | the value as JSON text (§8.14) |
- A call whose path or name is written as text in the call is checked against the permits before running; without a permit it is a mistake. Any other value is checked when the call runs, and without a permit the program stops (§10.2).
- These names belong to Quill: a function can’t be called
read_file,write_file,file_exists,env,json,fetch,postorsend.
8.14 JSON
booking = body as Booking
on Invalid -> say "That isn't a booking."
say json(booking)
TEXT as KINDreads JSON text asKIND, any kind a function can name (§8.6). It is a call that can fail withInvalid, so it needson Invalidortry(§8.9).asfollows a value, like.andat.- A JSON number or text such as
"500.00"is read as money of the kind’s currency, digit for digit. Numbers are never read through floating point; a number with an exponent, like1e3, isInvalid. - An object is read as a record of a type: every field of the type must be there, except
maybefields, which may benullor left out and are thenmissing. Fields the type doesn’t name are left out. Each field’s range (§8.11) must hold. A JSON array is read aslist of KIND. - Anything else that doesn’t fit is
Invalid: text that isn’t JSON, a value of another kind, a missing field, or a value outside a range. json(VALUE)gives back the value as JSON text. Text becomes a string, numbers and money exact numbers, true/false booleans, lists arrays, records objects with their fields in order, afoundvalue its value, andmissingnull.
8.15 Web requests: bring http
bring http
permit net "api.example.com"
answer = fetch("https://api.example.com/rates")
on NetworkDown -> say "offline"
say answer.status
| Function | Sends |
|---|---|
fetch(url: text) | a GET request |
post(url: text, body: text) | a POST request with the header Content-Type: application/json |
send(method: text, url: text, body: text, headers: list of text) | any of GET, POST, PUT, PATCH, DELETE, HEAD; "" is no body; each header is "Name: value" |
- Each needs
bring http(§8.12) and apermit net(§8.13) covering the URL’s host. A URL starts withhttps://orhttp://. - Each gives back an answer: a record with
status, a number, andbody, text. Any answer the service gives is an answer, including statuses such as404and500. - Each fails with
NetworkDownwhen the service can’t be reached or doesn’t answer within 30 seconds. - A method or header that isn’t valid stops the program (§10.2).
8.16 Asking a model: bring ai
bring ai
permit net "api.anthropic.com"
permit env "ANTHROPIC_API_KEY"
summary = ask "Summarize: {text}"
on ModelFailed -> "none"
review = ask "Rate it: {text}" as Review
on ModelFailed -> say "no answer"
on Invalid -> say "an answer that broke Review's rules"
ask VALUEsends the textVALUEto a model and gives back its answer as text.ask VALUE as KINDgives back the answer asKIND, any kind a function can name (§8.6).VALUEis everything up toasor the end of the value.askis a call. It fails withModelFailedwhen the model can’t be reached, answers with an error, declines, or gives no text.ask ... as KINDalso fails withInvalidwhen the answer doesn’t fitKINDasasreads it (§8.14), including awhererange.- It needs
bring ai, and apermit envfor a key, which chooses the provider:ANTHROPIC_API_KEYasks Claude; otherwiseOPENAI_API_KEYasks a service that speaks the OpenAI chat format. The service’s host needs apermit net. For Claude, a program with nopermit netat all is a mistake found before running. - Settings read without a permit, because they only choose where the request goes:
QUILL_AI_MODEL(the model; Claude’s default isclaude-opus-5-5, and an OpenAI-compatible service needs it),ANTHROPIC_BASE_URLandOPENAI_BASE_URL(the service’s address). The request still needs apermit netfor that address. - With
as KIND, the model is sent the kind as a JSON schema: each field is required, amaybefield may benull, and a range is described in words. A kind that isn’t a record is asked for as{"value": ...}. - When Claude’s own API declines a question for a model that offers it, Anthropic’s recommended model answers instead.
- A key permitted but not set, or an OpenAI-compatible service with no
QUILL_AI_MODEL, stops the program (§10.2).
8.17 A web service: bring web
bring web
route GET "/hello/{name}"
respond 200, "Hello, {name}!"
route METHOD "PATH" NEWLINE INDENT { statement } DEDENT
respond STATUS "," VALUE
- A route is written at the top level of a program that brings
web.METHODisGET,POST,PUT,PATCHorDELETE.PATHstarts with/; each{NAME}in it matches one part of a requested path and is a text value namedNAMEinside the route, with%escapes worked out. - Inside a route,
requestis a record withmethod,path,query(after?, or"") andbody, all text. A route doesn’t see values created outside it, like a function (§8.6). respond STATUS, VALUEends the route with its answer, asreturnends a function.STATUSis a whole number from 100 through 599. Text is sent astext/plain; any other value as JSON (§8.14), asapplication/json. Every path through a route must reachrespond, which may also stand afteron ... ->.returnin a route andrespondoutside one are mistakes.RequestandResponseare types the web module gives; a program can’t write types with those names.quill runruns the program’s top level, then answers requests on portPORT(8080 when the setting isn’t set) until it is stopped. A request runs the route whose method and path match. A path no route has is answered404; a path a route has with another method405. A route that stops the program (§10.2) is answered500, its problem is shown where the server runs, and the server keeps answering.quill checkandquill testdon’t serve.
9. Tests: check and doc examples
9.1 check
check total == 37.80
check visits >= 1
A check compares two values with any comparison from §6.2. When it is true, nothing happens. When it is false, the program stops and Quill says what each side was:
This check failed.
One side is 37.75 and the other is 37.80.
Both sides of a check are calculations. A check can’t use and, or or not directly: use two checks instead.
9.2 --- example:
--- example: 0.1 + 0.2 == 0.3
A doc example is a check written inside documentation. It runs exactly like check, so examples in documentation can’t drift out of date.
An example runs where it is written, with one exception: examples directly above a fn (with only comments and blank lines between) document that function, and run just after the function is written, so they can call it. When an example fails, Quill points at the line where it was written.
--- example: with_vat(100 USD) == 105 USD
fn with_vat(price: USD) -> USD
return price * 1.05
9.3 Running tests
check lines run whenever the program runs. quill test (§11) runs every file in a folder that contains tests.
10. When Quill finds mistakes
10.1 Before running
Quill reads the whole program and checks it before running anything. If there are mistakes, it reports all of them and runs nothing. It finds:
- text it can’t read: text that never ends, unknown escapes, unknown characters, indentation that doesn’t line up
- lines that aren’t valid statements
- names that don’t exist, are used before they are created, or are created twice
seton a value that isn’t markedvar, or that changes its kind- operations on the wrong kinds of values, like
price + "5" - division by a written
0 whenconditions that aren’t true/falserequire,ensureandwherethat break the rules in §8.10, and a written value passed outside a parameter’swhererange- a
forrange whose ends aren’t numbers, aforthat doesn’t walk a list, orseton the loop’s own name - a list whose items aren’t the same kind, a list position that isn’t a whole number from 1, or a record field that isn’t there
10.2 While running
A few mistakes can only be found while running. When one happens, the program stops, and the lines before it have already run:
- dividing by a value that turns out to be zero
- a calculation whose result is too big for a number (§4.1)
- a
checkor--- example:that is false - a
requireorensurethat is false, or a value outside a parameter’swhererange (§8.10) - a list position past the end, or a position in an empty list
- an
averageof a list with no amount in that field (§5.6) - a
splitseparator or areplaceold text that is empty (§5.2.1) - a file, host or setting that no permit covers (§8.13)
10.3 How mistakes are reported
Every problem is reported the same way: where it is, what went wrong, why, and, when Quill can tell, what you probably meant:
Problem on line 2 of shop.quill:
total = price + "5"
^^^
You're adding Number (price) to Text ("5").
Quill won't guess which one you meant.
Did you mean: total = price + 5
Integer remainder overflow produces an arithmetic diagnostic rather than a host panic.
Quill never crashes on a mistake in a program. If it ever does, that is a bug in Quill.
11. The quill tool
| Command | What it does |
|---|---|
quill run FILE.quill | Check the program, then run it |
quill FILE.quill | Same as quill run |
quill check FILE.quill | Check for mistakes without running anything |
quill lsp | Speak the Language Server Protocol on standard input and output. Report the same mistakes as quill check, so an editor can underline them. Do not run the program. |
quill test FILE.quill | Run the selected file, including files with no checks. Missing files and non-.quill paths are errors. |
quill test | Run every .quill file in this folder and its subfolders that contains a check or --- example:, and report how many passed. Folders starting with ., and target, are skipped. |
quill test FOLDER | The same as quill test, for the files in FOLDER and its subfolders. |
quill explain FILE.quill:N | Show how Quill read line N: what each part means, its kind, and where names were created |
quill new NAME [TEMPLATE] | Create a folder NAME from a template: its program, a README.md about it, and QUILL.md, a short tour of the language. With no template, shows a menu of every template to choose from with the arrow keys (Esc creates nothing), or makes starter when there’s no terminal. The templates are app, which puts the others’ ideas together; learn and starter; backend, integration and webhook; ai and ai-backend; finance and banking; automation and library. An unknown template is a mistake that names the closest one. NAME must not exist yet. |
quill tokens FILE.quill | Show the pieces Quill read the file as, with their line and column |
quill update [VERSION] | Install a release of Quill. With no version, shows a menu of every release to choose from with the arrow keys (or installs the latest when there’s no terminal). VERSION is a version like 0.2.0, or latest. Works for copies installed with the installer; a copy built from source explains how to update it with cargo. quill upgrade is another name for this command. |
quill help | Show help |
quill version | Show the version |
Test reports count successful assertions actually executed, including doc examples, function calls and each loop iteration. Unexecuted assertions do not count. A failing file stops at its first failure; its earlier passes remain in the summary. Project testing continues with other files.
quill exits with code 0 on success and 1 when it reports a problem.
Update check. At most once a day, quill asks GitHub for the latest release, in a separate background process so commands are never slowed down, and remembers the answer in ~/.quill/update-check. When a newer version exists, commands end with a one-line notice on standard error. Quill never updates itself. The check is skipped when standard error isn’t a terminal, when the CI environment variable is set, when curl isn’t available, and whenever QUILL_NO_UPDATE_CHECK is set.
12. Planned, not yet implemented
The README describes these features. They are part of Quill’s design but not of this version of the language. Using them today is a mistake.
| Feature | README section |
|---|---|
require and ensure proven before running (static verification) | Contracts |
Permissions: permit | Permissions |
Modules: use, web, db, route, respond | A small web API |
13. Grammar summary
This grammar describes the shape of programs. The rules above add the meaning and the mistakes Quill checks for. NEWLINE, INDENT and DEDENT come from §2.
program = { statement | NEWLINE } ;
statement = say | create | change | check | when | match | for | function | type | return | call | fail ;
type = "type" NAME NEWLINE INDENT parameter NEWLINE { parameter NEWLINE } DEDENT ;
say = "say" value ;
create = [ "var" ] NAME "=" value ;
change = "set" NAME { "." NAME | "at" sum } "=" value ;
check = ( "check" | "--- example:" ) sum COMPARE sum ;
when = "when" NEWLINE INDENT case { case } DEDENT ;
case = ( value | "else" ) "->" case_body NEWLINE ;
when_value = "when" NEWLINE INDENT value_case { value_case } DEDENT ; (the last case is else)
value_case = ( value | "else" ) "->" value NEWLINE ;
handler = "on" NAME "->" ( statement | value ) NEWLINE ;
case_body = say | change | check | when | for ;
for = "for" NAME "in" ( value ".." value | value ) NEWLINE INDENT { for_body } DEDENT ;
for_body = say | change | check | when | for | return ;
function = "fn" NAME "(" [ parameter { "," parameter } ] ")" [ "->" kind ] NEWLINE INDENT { header } { fn_body } DEDENT ;
header = ( "fails" NAME { "," NAME } | "require" value | "ensure" value ) NEWLINE ;
parameter = NAME ":" kind [ "where" bound ".." bound ] ;
bound = [ "-" ] NUMBER [ CURRENCY ] ;
kind = "text" | "number" | "true/false" | CURRENCY | TYPE | "maybe" kind | "list" "of" kind ;
match = "when" value NEWLINE INDENT catch { catch } DEDENT ;
catch = ( "found" NAME | "missing" ) "->" statement ;
fail = "fail" NAME ;
CURRENCY = three capital letters ;
fn_body = say | create | change | check | when | for | return ;
return = "return" [ value ] ;
call = NAME "(" [ value { "," value } ] ")" ;
value = or { shaping } ;
shaping = "where" or | "sorted" "by" [ "highest" | "lowest" ] NAME ;
or = and { "or" and } ;
and = not { "and" not } ;
not = "not" not | comparison ;
comparison = sum [ COMPARE sum | ( "contains" | "starts" "with" | "ends" "with" ) sum ] ;
sum = product { ( "+" | "-" ) product } ;
product = negative { ( "*" | "/" | "%" ) negative | "%" "of" negative } ;
negative = "-" negative | atom { "." NAME | "at" sum } ;
atom = TEXT | NUMBER | "true" | "false" | NAME | call | "(" value ")" | list | record | typed | ask | total | count | average | text_tool | when_value | money ;
money = "round" [ "up" | "down" ] sum [ "to" ( "cents" | negative "places" ) ]
| "split" sum ( "into" | "by" ) sum
| "convert" sum "to" CURRENCY "at" negative ;
typed = TYPE ( field { "," field } | NEWLINE INDENT field { field } DEDENT ) ;
ask = "get" NAME { "," NAME } "of" negative { shaping } | "get" NAME "with" NAME "in" negative ;
total = "sum" NAME "of" negative { shaping } ;
count = "count" "of" negative { shaping } ;
average = "average" NAME "of" negative { shaping } ;
text_tool = ( "trim" | "uppercase" | "lowercase" ) negative
| "length" "of" negative
| "join" negative "with" negative
| "replace" negative "with" negative "in" negative ;
list = "list" ( "of" ( "text" | "number" | "true/false" ) | sum { "," sum } ) ;
record = "record" ( NEWLINE INDENT field { field } DEDENT | field { "," field } ) ;
field = NAME "=" value ;
COMPARE = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
TEXT = '"' { character | escape | "{" value "}" } '"'
| '"""' { character | escape | "{" value "}" } '"""' ;
NUMBER = digit { digit } [ "." digit { digit } ] ;
NAME = ( letter | "_" ) { letter | digit | "_" } ; (not a reserved word)