Introduction
Welcome to The Quill Programming Language, a book for learning Quill from the very beginning.
Quill is a programming language where intent is clear, failures are explicit, and correctness is part of the code. It is for backend engineering, financial software, automation, reliable business applications, and AI and machine learning. The short form of that idea is write what you mean.
The words stay close to English. when chooses a path. found and missing both have to be handled. fails and on name a failure and what to do about it. check is a test written next to the code. Those words are Quill’s voice.
A bank withdrawal is one kind of service. An account, a booking, an order, a school record, and a job that can fail are others. Asking a model is the same kind of care: the answer may be missing, and the call may fail. Quill can serve requests, read and write JSON, call other services, and ask a model. Storing rows in a database is the next step.
price = 12.50
qty = 3
total = price * qty
say "You bought {qty} items for {total}"
You bought 3 items for 37.50
Who this book is for
This book is for anyone who wants to learn Quill:
- New programmers. You don’t need to know any other language. Every idea is explained the first time it appears.
- Experienced programmers. You can move quickly through the early chapters. Appendix A is a one-page summary of the language.
What makes Quill different
Many languages let small mistakes slip through until a program is already running, sometimes in front of real customers. Quill tries to catch them first:
- Money is exact.
100 USD + 50 USDis150 USD, and0.1 + 0.2is exactly0.3. Dollars and euros cannot be added. - Ask for the data.
sum fee of rosteradds each fee.get place of desk.bookingsreads a field of a list on a record, andget gpa of school.acceptancesreads a field of a nested record.count of roster,average fee of roster, androster sorted by highest feesay how many, the typical fee, and the order. Asking for data shows each form.forstays when each record needs its own step. - A missing answer has to be handled.
maybesays a value might not be there.foundandmissingare both required. - A failure has to be handled.
fails Declinednames it,fail Declinedleaves with it, and the caller writeson Declined. - Functions state their rules.
require amount > 0 USDsays what a function needs, andensure result >= 0 USDsays what it promises.guests: number where 1..500gives a parameter its range. When one is broken, the program stops and says which value broke it. - Nothing changes by accident. A value can only change if you mark it with
var, so every change is visible when you read the code. - Mistakes are found before anything runs. If you add text to a number, Quill tells you before the program starts.
- Errors teach. Every error says what went wrong, why, and usually what you probably meant.
- Tests live with the code. A
checkline is a test, written right next to the code it tests.
How to read this book
The chapters build on each other, so read them in order the first time. Each one introduces a few ideas with small examples, shows the mistakes people commonly make, and ends with something to try.
Every example in this book is run by Quill’s own test suite each time Quill changes. If the book shows that a program prints something, that is exactly what it prints.
Code you can type looks like this:
say "Hello!"
And what it prints looks like this:
Hello!
Quill is young
Quill is in early development. This book grows with the language: as each feature is built, a chapter explains it. What’s Coming Next lists the features that are designed but not built yet, and Appendix C records what changed in each version.
Let’s begin by installing Quill and running your first program.
The Story of Quill
Why Quill exists
Building a backend today means stitching together a framework, a database library, a validation library, a test runner and a pile of dependencies. Then production goes down anyway, because of a null nobody checked, a failure nobody handled, or a price that rounded wrong.
Quill was started to make that everyday work dramatically less painful: a language where intent is clear, failures are explicit, and correctness is part of the code.
The founder
Ngwang Shalom Tamnjong, also known as Json, is from Bamenda, in the Northwest Region of Cameroon. In October 2026 he was building a talent-booking application, the full stack: the part people use, and the service behind it. That work is where Quill began.
How it started
He wanted a language a beginner could start with, and one powerful enough for backend applications and AI-powered systems. A simple language for the next generation.
The talent-booking application made the problem plain. Languages already in use were hard to learn. A backend meant installing many pieces and combining them before the real work could run, and the learning curve was steep. On 13 October 2026 he started Quill, so that beginning would stay simple, and so the same language could carry serious backend and AI work. The first program, say "Hello, World!", ran the same day.
The name
He chose a short name that would be easy to remember: Quill. The feather in the logo carries the same meaning. The language should feel light: easy to get started, easy to learn, easy to adopt, and still very powerful.
Timeline
| Date | Milestone |
|---|---|
| 13 October 2026 | Work on Quill begins. The first program, say "Hello, World!", runs the same day. |
| 13 October 2026 | Quill is published on GitHub as an open-source project under the Apache 2.0 license. |
| 13 October 2026 | v0.1.0, the first release, with installers for macOS, Linux and Windows. |
| 13 October 2026 | v0.2.0 adds arithmetic with exact decimals, values inside text, var and set, and when. |
| 13 October 2026 | The Quill book and the language specification are published, and every change to Quill is documented in them from now on. |
| 13 October 2026 | v0.3.0 adds quill update, to choose and install any version from a menu, and a notice when a new version is out. |
| 14 October 2026 | v0.3.1 adds lists, records, loops, functions, exact money, maybe, and failures. |
| 20 October 2026 | v0.4.0 adds contracts (require, ensure), where for ranges and for filtering records, get and sum for asking data without a loop, a when that gives a value, on that carries on, and examples that test a function. |
| 3 November 2026 | v0.4.2 adds count, average, and sorted by, types, exact rounding and splitting of money, files brought together with bring, permits, JSON, web requests, asking an AI model, and web services. Quill files end in .quill. |
| 3 November 2026 | v0.4.5 adds eleven templates for quill new, from learning exercises to backends, AI services, finance, and automation, and text tools: trim, contains, split, join, replace, and more. The book’s examples now have the editor’s colors. |
| 3 November 2026 | v0.4.8 adds the app template, a whole app with a backend, an AI assistant, and exact money working together. Quill’s colors work in VS Code’s Restricted Mode and in Markdown files. |
Where Quill is going
From the start, Quill has been for everyone who builds software: developers, system engineers, script engineers, and AI developers.
His hope for the years ahead is that Quill stands among the most adopted languages, with a large community, and that it is used in production mobile applications.
Join in
Quill is built in the open, and this story is still being written. If you’d like to be part of it, read the contributor guide.
Getting Started
In this chapter you’ll install Quill, write your first program, and learn the commands you’ll use every day.
Installing Quill
On macOS or Linux, open a terminal and run:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/Ngwangshalom/Quill/releases/latest/download/quill-installer.sh | sh
On Windows, open PowerShell and run:
powershell -ExecutionPolicy Bypass -c "irm https://github.com/Ngwangshalom/Quill/releases/latest/download/quill-installer.ps1 | iex"
You can also download Quill for your system from the Releases page.
To check that it worked, open a new terminal and run:
quill version
It prints the version you installed, such as quill 0.2.0.
Updating Quill
Quill gets better with every release. To update, run:
quill update
Quill shows every version you can install. Move with the ↑ and ↓ keys, press Enter to install the one you want, or press Esc to leave everything as it is:
Which version of Quill do you want? (↑ ↓ to move, Enter to install, Esc to cancel):
> 0.4.8 3 Nov 2026 latest
0.4.5 3 Nov 2026 installed
0.4.2 3 Nov 2026
0.4.0 20 Oct 2026
0.3.1 14 Oct 2026
0.3.0 13 Oct 2026
0.2.0 13 Oct 2026
0.1.0 13 Oct 2026
You can also name the version you want, which is handy in scripts:
quill update latest
quill update 0.2.0
Choosing an older version is how you go back if a new release causes a problem. Versions before 0.3.0 don’t have quill update, so Quill asks before installing one of them: to come back to a newer version from there, you run the installer again.
quill upgrade does exactly the same as quill update, for anyone used to that word from other tools.
You don’t have to remember to check. At most once a day, Quill quietly asks whether a new version is out, and if one is, it tells you after your command finishes:
Quill 0.4.8 is available (you have 0.4.5). Run quill update to get it.
Quill never updates itself without asking, so a program that works today won’t change behaviour tomorrow unless you choose to update. The check runs in the background and never slows Quill down. If you’d rather Quill didn’t check at all, set QUILL_NO_UPDATE_CHECK=1 in your environment.
quill update works for copies installed with the commands above. If you built Quill from its source code with cargo, update it by getting the latest code and running cargo install --path . --force in the Quill folder.
Hello, World!
It’s a tradition to start with a program that shows the words “Hello, World!”. In Quill, that program is one line.
Create a file called hello.quill (Quill programs always end in .quill) and write:
say "Hello, World!"
Then run it:
quill run hello.quill
Hello, World!
say shows a value. Here the value is the text "Hello, World!". The quotes mark where the text starts and ends; they aren’t shown.
Files from an older Quill. Before 0.8, Quill files ended in
.ql. They still run, and Quill reminds you to rename them:Note: Quill files now end in .quill. Rename hello.ql to hello.quill.Rename each file once, and the note goes away.
bring billingfindsbilling.qluntil you do.
A program can have many lines. Quill runs them from top to bottom:
say "Hello, World!"
say "Welcome to Quill."
Hello, World!
Welcome to Quill.
Starting a project
quill new creates a folder with a program in it that runs and passes its tests straight away. It asks what kind of program you want:
quill new shop
What kind of program is shop? (↑ ↓ to move, Enter to choose, Esc to cancel)
Everything app a whole app: backend, AI, money, data, and tests
Learning learn exercises with checks that fail until you fix them
> Learning starter one file: values, when, for, a function, a test
Backend backend an API for web, desktop, and mobile apps
Backend integration call another service and read its JSON safely
Backend webhook receive payment notices and check every field
AI ai ask a model and check its answer against a type
AI ai-backend a service route that asks a model
Business finance invoices with exact money, VAT, and contracts
Business banking accounts that can never go below zero
Automation automation read a file, count and sort, save a report
Automation library reusable functions with tests, used with bring
Move with ↑ and ↓, and press Enter. To skip the menu, name the template after the folder:
quill new shop starter
quill new orders backend
Created shop, a starter project.
Run it with: quill run shop/hello.quill
Test it with: quill test shop
Read shop/README.md to see what's inside, and shop/QUILL.md for a tour of Quill.
Every project has a README.md that says how to run it and what to try next, and a QUILL.md with a one-page tour of the language. When there’s no menu to show, such as in a script, quill new shop makes a starter project.
| Template | Start here when you want to |
|---|---|
app | see a whole app: a backend with bookings, prices with VAT, data from a file, an AI assistant, and tests, working together |
learn | learn Quill by fixing small exercises until quill test passes |
starter | try the language in one file |
backend | build a service that web, desktop, and mobile apps call |
integration | call another service and read its JSON safely |
webhook | receive notices from a payment provider or another service |
ai | ask an AI model and check its answer |
ai-backend | put an AI model behind a service you can deploy |
finance | work with invoices, VAT, and money that never loses a cent |
banking | keep accounts whose rules can never be broken |
automation | read a file, work something out, and save a report |
library | write functions that other programs bring in |
Inside shop/hello.quill, the starter, you’ll find:
-- Your first Quill program. Run it: quill run hello.quill
-- Change a value, run it again, and watch what changes.
name = "Amina"
say "Hello, {name}!"
scores = list 80, 64, 45
say "{count of scores} scores"
for score in scores
when
score >= 50 -> say "{score}: a pass"
else -> say "{score}: try again"
--- Doubles a number. The example below is a test: quill test runs it.
--- example: double(21) == 42
fn double(n: number) -> number
return n * 2
say "Double 21 is {double(21)}"
check name == "Amina"
Hello, Amina!
3 scores
80: a pass
64: a pass
45: try again
Double 21 is 42
Each part has a chapter of its own in this book. The check line and the --- example: are tests, which you’ll learn about in Testing Your Code.
Comments
A comment is a note for people reading the code. Quill ignores it. A comment starts with -- and runs to the end of the line:
-- Show a welcome message.
say "Welcome!" -- this part is ignored too
Welcome!
Colors in your editor
Quill has its own colors, taken from the feather, the nib, and ink. say, when, and true each have their own color. The first time you run Quill, it quietly gives those colors to Cursor and VS Code, puts the feather on every .quill file, and underlines mistakes while you type. Reload the editor window once to see it, and again after you update Quill. Other languages keep the colors they already had.
The feather follows the editor. On a dark window it is pale, so it shows against the dark file list. On a light window it is the deep blue feather from the logo.
On a dark editor:
| What | Color | Where it comes from |
|---|---|---|
say | #F0A35E nib orange | the nib |
when and else | #7EAEF5 feather blue | the feather |
true and false | #5ECA8C green | a yes on the page |
var and set | #C9A6F2 lilac | a name you can change |
check | #F0A0B4 rose | a test |
and, or, not | #5EC4C0 teal | words that join a question |
for and in | #C6D86A yellow-green | a loop |
get, sum, with, count, average, and sorted | #6EC1D6 sea | asking for data |
trim, uppercase, lowercase, length, contains, starts with, ends with, join, replace | #949CF5 ink | working with text |
list | #D4896A clay | values in order |
record | #A8C5A0 sage | named fields |
of | #E8C547 gold | the kind of an empty list |
at | #B07CFF violet | one item, by its position |
fn and return | #E8A0D4 pink | a function |
maybe, found, missing | #F0B090 peach | a value that might be missing |
fails, fail, on, try | #E08A8A red | a failure the caller must handle |
require, ensure, and result in an ensure | #C8A06A brass | a function’s rules |
where, type and as | #6AA8C8 steel | which values fit: a range, the records to keep, a kind of record |
round, split, convert | #8FD3B5 mint | money that never loses a cent |
bring, route, respond | #A8C0D8 mist | another file, a Quill module, a web route |
permit | #C47848 rust | what a program may touch |
ask | #E57BE0 spark | a question for an AI model |
| Numbers | #E7C98A sand | counted things |
| Text in quotes | #F3E6D0 warm paper | the page |
| Comments | #9A9186 faded ink | ink, faded |
On a light editor the same ideas use deeper shades, so they stay readable on paper: say #C45C12, when #1D408A, true #157A42, list #8C4E32, record #3E5C32, of #8A5A00, at #5C2E9A, get #15556A, trim #3A44A0, fn #8A3D78, maybe #A85A38, fails #A33B3B, require #7A5A20, where and type #1A5270, round #2E7A5C, bring #3D5270, permit #8C4E20, ask #8E2C8A, numbers #9A4A12, text #6B3A22. Where to write of and at is in Lists and records. Asking for a field is in Asking for data.
Every word Quill has today has its color. A word is painted only once the language has it, so a name you invent is never colored by mistake.
This is installed on each computer where Quill runs. Set QUILL_NO_EDITOR_SETUP=1 to leave the editor alone.
Quill code inside a Markdown file, in a ```quill block like the ones in a project’s README.md and QUILL.md, gets the same colors.
In a folder VS Code or Cursor doesn’t trust yet, the editor shows Restricted Mode. Quill’s colors and the feather still work there. Underlining mistakes runs the quill program, so it starts once you trust the folder: choose Manage on the Restricted Mode bar, then Trust.
Vim, Emacs, and Sublime also know .quill files. The files are in the editors folder. Any editor that can talk to a language server, including Neovim, Helix, and Zed, gets the underlines from quill lsp. That is the same check as quill check, and it does not run the program.
The commands you’ll use
| Command | What it does |
|---|---|
quill run FILE.quill | Run a program |
quill check FILE.quill | Look for mistakes without running anything |
quill lsp | Underline those mistakes in an editor |
quill test | Run the tests in every file in this folder |
quill explain FILE.quill:N | Show how Quill understood line N |
quill new NAME | Start a new project from a template |
quill update | Choose a version of Quill to install |
quill help | Show all the commands |
Try it
- Write a program that says your name, then your favourite food, on two lines.
- Add a comment above each line saying what it does.
- Remove one of the quotes from your program and run it. Read what Quill says. You’ll learn more about errors in Reading Quill’s Errors.
Values
Programs work with values: pieces of information like a name, a price, or whether a customer is logged in. In this chapter you’ll learn how to give values names and what kinds of values there are.
Giving a value a name
Write a name, then =, then the value:
name = "Amina"
age = 34
say name
say age
Amina
34
After this line runs, name means "Amina" everywhere below it. Using a name instead of repeating the value makes programs easier to read and to change.
Names can use letters, digits and _, but must start with a letter or _. Good names say what the value means: total_price is clearer than tp. Names are case-sensitive, so total and Total are different names.
Values don’t change
In Quill, a value created with = can’t change. Once name is "Amina", it stays "Amina". Creating the same name again is a mistake:
name = "Amina"
name = "Omar"
Problem on line 2 of example.quill:
name = "Omar"
^^^^
name already exists. It was created on line 1.
Values in Quill can't change once they're created, so give this one a different name.
This might seem strict, but it makes programs much easier to understand: when you read name, you know it is the value from the line that created it. When you do need a value that changes, like a counter, you say so with var. You’ll learn how in Values That Change.
Create before you use
Quill reads a program from top to bottom, so a name must be created before it’s used:
say greeting
greeting = "Hello"
Problem on line 1 of example.quill:
say greeting
^^^^^^^^
greeting is used here, but it isn't created until line 2.
Quill reads from top to bottom, so create values before using them.
Kinds of values
Every value has a kind. Quill starts with three:
| Kind | Examples | Used for |
|---|---|---|
| Text | "Amina", "Hello, World!" | words and sentences |
| Number | 34, 12.50, -3 | counting and calculating |
| true/false | true, false | answers to yes-or-no questions |
city = "Dubai"
guests = 120
confirmed = true
say city
say guests
say confirmed
Dubai
120
true
A list and a record gather several values into one. They have their own chapter, Lists and records.
A name keeps its kind for the whole program. Quill knows the kind of every value before the program runs, which is how it catches mistakes like adding text to a number. You’ll see that in the next chapter.
Quill never turns one kind into another on its own: "5" is text, while 5 is a number.
Try it
- Create values for a product: its name, its price, and whether it’s in stock. Show each one with
say. - Try to create the same name twice. Read the error.
- Use a name before creating it. What does Quill say?
Numbers and Calculations
Quill’s numbers are built for real-world work like prices, quantities and money. In this chapter you’ll learn how to calculate, and why Quill’s answers are always exact.
Calculating
Quill uses the symbols you know from maths:
| Symbol | Meaning | Example | Result |
|---|---|---|---|
+ | add | 7 + 2 | 9 |
- | subtract | 7 - 2 | 5 |
* | multiply | 7 * 2 | 14 |
/ | divide | 7 / 2 | 3.5 |
% | remainder after dividing | 7 % 2 | 1 |
price = 12.50
qty = 3
total = price * qty
say total
say total - 5
37.50
32.50
Order of operations
As in maths, *, / and % happen before + and -. Use brackets to change the order:
say 1 + 2 * 3
say (1 + 2) * 3
7
9
A - in front of a value makes it negative:
balance = 20
say -balance
say 5 - 8
-20
-3
Numbers are exact
Many programming languages store numbers in a way that can’t represent most decimals exactly, so 0.1 + 0.2 comes out as 0.30000000000000004. That’s a real problem when the numbers are money.
Quill’s numbers are exact decimals:
say 0.1 + 0.2
say 100 * 1.05
0.3
105.00
Digits after the point
A number keeps the digits after the point that it was written with, so prices keep looking like prices:
say 0.10
say 0.10 + 0.2
say 1.50 * 2
0.10
0.30
3.00
The rule is simple: the answer keeps as many digits after the point as the number that had the most.
Comparing ignores those extra zeros, so 0.10 and 0.1 are equal.
Dividing
Some divisions never end, like 1 ÷ 3 = 0.3333… Quill keeps up to 20 digits after the point:
say 10 / 4
say 1 / 3
2.5
0.33333333333333333333
How big?
A Quill number can hold up to 38 digits in total, before and after the point. That’s more than enough for any amount of money. If a calculation grows bigger than that, Quill stops and tells you, rather than quietly giving a wrong answer.
Dividing by zero
Dividing by zero has no answer. If you write it directly, Quill catches it before running:
say 10 / 0
Problem on line 1 of example.quill:
say 10 / 0
^
This divides by zero, which has no answer.
Dividing by zero isn't allowed, so use a number other than 0.
If a value only turns out to be zero while the program runs, Quill stops at that line and explains:
var count = 0
say "Starting"
say 100 / count
Starting
Problem on line 3 of example.quill:
say 100 / count
^^^^^^^^^^^
This divides by zero, which has no answer.
The number on the right was 0 when this line ran. Use when to handle zero before dividing.
You’ll learn how to handle this with when in Making Decisions.
Numbers and text don’t mix
Calculations only work on numbers. Adding text to a number is a mistake, and Quill finds it before running anything:
price = 100
total = price + "5"
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
"5" in quotes is text, not a number. Quill won’t guess whether you wanted to add numbers (105) or join text (“1005”), so it asks you to say which.
Try it
- A café sells coffee for 3.50 and cake for 4.25. Calculate the cost of 2 coffees and 1 cake.
- Add 5% tax to your answer by multiplying by 1.05. How many digits does the answer have after the point? Why?
- Use
%to find the remainder when 17 is divided by 5. - Write
say 2 * "3"and read the error.
Text
Text is how programs talk to people. In this chapter you’ll learn how to write text, put values inside it, include special characters, and work with the text you have: tidy it, measure it, look inside it, and change parts of it.
Writing text
Text goes between double quotes, on one line:
say "Hello, World!"
Hello, World!
Single quotes don’t work in Quill. Text always uses double quotes, so there’s only one way to write it.
Values inside text
Put a value between { and } inside text, and Quill shows the value there:
name = "Amina"
qty = 3
price = 12.50
say "Hi {name}!"
say "{qty} items at {price} each"
say "That's {qty * price} in total"
Hi Amina!
3 items at 12.50 each
That's 37.50 in total
Anything can go inside the braces: a name, a number, a calculation, or a comparison. Each pair of braces holds one value.
This is how you build text from values in Quill. + only adds numbers, so "Hi " + name is a mistake, and Quill suggests the braces instead:
name = "Amina"
say "Hi " + name
Problem on line 2 of example.quill:
say "Hi " + name
^^^^^^^^^^^^
You're adding Text ("Hi ") to Text (name).
Quill won't guess which one you meant.
Did you mean: say "Hi {name}"
Special characters
Some characters can’t be typed directly inside text. Write them with a backslash \ in front:
| Write | To get |
|---|---|
\n | a new line |
\t | a tab |
\" | a double quote |
\\ | a backslash |
\{ and \} | a brace, instead of a value |
say "Line one\nLine two"
say "She said \"hi\""
say "Use \{name\} to show a name"
Line one
Line two
She said "hi"
Use {name} to show a name
Text with quotes inside
When text has many quotes in it, the backslashes get hard to read. Wrap the text in three quotes instead, and quotes inside need no backslash:
name = "Amina"
say """She said "hi" to {name}."""
She said "hi" to Amina.
Tidying text
Text that people type often has spaces at the ends, or capital letters where you don’t want them. trim, uppercase, and lowercase give back a tidy copy:
typed = " Amina Okafor "
name = trim typed
say "[{name}]"
say uppercase name
say lowercase "AMINA@SCHOOL.ORG"
[Amina Okafor]
AMINA OKAFOR
amina@school.org
trim takes away spaces, tabs, and new lines at the start and the end. The spaces between words stay. The text you started with doesn’t change: typed still has its spaces, and name is the tidy copy.
A tool works inside braces too: say "Hello, {trim typed}!".
A tool takes no brackets. To tidy a longer value, put a space before the bracket: trim (parts at 1). Written right against it, trim(x) is a call to a function named trim, and Quill says so when there isn’t one.
How long: length of
length of says how many letters a text has. Spaces and punctuation count, and so does each letter with an accent:
code = "INV-2026"
say length of code
say length of "café"
8
4
Read it as: “the length of the code.” It is a number, so it works in a when:
password = "quill"
when
length of password < 8 -> say "Use at least 8 letters."
else -> say "That's long enough."
Use at least 8 letters.
Looking inside text: contains, starts with, ends with
These three answer true or false:
email = "amina@school.org"
code = "INV-2026-001"
say email contains "@"
say code starts with "INV"
say code ends with ".pdf"
true
true
false
They are conditions, so they go straight into a when and join with and, or, and not:
email = "amina@school.org"
when
email contains "@" and email ends with ".org" -> say "A school address."
not email contains "@" -> say "That isn't an email address."
else -> say "Some other address."
A school address.
Capital and small letters are different: "Amina" contains "amina" is false. To ignore them, compare the lowercase of both.
Changing part of the text: replace
replace OLD with NEW in TEXT changes every OLD in the text to NEW:
code = "INV-2026-001"
say replace "-" with " " in code
say replace "Mr" with "Dr" in "Mr Okafor"
INV 2026 001
Dr Okafor
Like the other tools, replace gives back a new text. code is still "INV-2026-001".
trim, uppercase, lowercase, length, contains, starts, ends, and replace are Quill words only when they are used this way. A value of your own can still be called length or trim.
Common mistakes
Forgetting the closing quote:
say "Hello
Problem on line 1 of example.quill:
say "Hello
^^^^^^
This text starts with a quote but never ends.
Text must open and close with " on the same line.
Did you mean: "Hello"
Forgetting the closing brace:
name = "Amina"
say "Hi {name"
Problem on line 2 of example.quill:
say "Hi {name"
^
This { inside text is never closed.
A value inside text goes between { and }, and text can't go inside the braces.
Did you mean: {name}
The text tools work on text. A number has to become text first, and braces do that:
year = 2026
say length of year
Problem on line 2 of example.quill:
say length of year
^^^^
length of works on text, but year is a Number.
Put it inside text first, like "{year}".
say length of "{year}" is 4.
Try it
- Create values for a city and its temperature, then say
"It's 31 degrees in Dubai"using braces. - Show a receipt line with the price of 3 items at 4.25 each, calculated inside the braces.
- Show the text
{curly braces}with the braces visible. trimthe text" hello ", then say it inuppercase.- Make an email address and use a
whento say whether itcontains "@". - Change every space in
"New York City"to-withreplace.
Values That Change
So far, every value has stayed the same once it was created. But some values need to change: a visitor counter, a running total, a game score. In this chapter you’ll learn how Quill handles change.
var and set
Mark a value with var when you create it, and you can change it later with set:
var visits = 0
say visits
set visits = visits + 1
say visits
set visits = visits + 1
say visits
0
1
2
Read set visits = visits + 1 as: “work out visits + 1, then make that the new value of visits”.
Why two words?
Most languages let you change any value at any time. Quill makes you mark changes for two reasons:
- You can see every change. Search for
setto find every place a value changes. A value withoutvarnever changes, so you never need to check. - Mistakes are caught. Changing a value that wasn’t meant to change is usually a bug.
The README calls this “every mutation is visible when you read the code”.
Values without var can’t be set
name = "Amina"
set name = "Omar"
Problem on line 2 of example.quill:
set name = "Omar"
^^^^
name can't change, because it isn't marked var.
It was created on line 1 as a value that stays the same. To let it change, write line 1 as:
var name = "Amina"
set only changes, it never creates
A value must be created with var before set can change it. When you misspell its name, Quill suggests the one you meant:
var visits = 0
set vistis = 2
Problem on line 2 of example.quill:
set vistis = 2
^^^^^^
There's no value called vistis.
set changes a value that was created with var, so create it first.
Did you mean: set visits = 2
Creating a var a second time is also a mistake. Use set to change it:
var count = 0
count = 5
Problem on line 2 of example.quill:
count = 5
^^^^^
count already exists. It was created on line 1 with var.
To change it, use set instead of creating it again.
Did you mean: set count = 5
A value keeps its kind
A var can change its value, but not its kind. A Number stays a Number:
var total = 0
set total = "10"
Problem on line 2 of example.quill:
set total = "10"
^^^^
total holds a Number, so it can't be set to Text ("10").
A value keeps the same kind for the whole program.
Did you mean: set total = 10
A running total
Here’s a typical use: adding up a bill one item at a time.
var total = 0
set total = total + 3.50 -- coffee
set total = total + 4.25 -- cake
set total = total + 3.50 -- another coffee
say "Total: {total}"
Total: 11.25
Try it
- Make a
varcalledscorestarting at 0. Add 10 three times, then say the score. - Try to
seta value that wasn’t created withvar. Read the error and fix your program the way it suggests. - Make a
varholding text, and try to set it to a number.
Making Decisions with when
Programs often need to do different things in different situations: free delivery for big orders, a warning when stock runs low, a different greeting for new visitors. In this chapter you’ll learn how to ask questions and act on the answers.
Asking questions: comparisons
A comparison asks a yes-or-no question. Its answer is true or false:
| Symbol | Question | Example | Answer |
|---|---|---|---|
== | equal to? | 3 == 3 | true |
!= | not equal to? | 3 != 3 | false |
< | less than? | 2 < 3 | true |
<= | less than or equal to? | 3 <= 3 | true |
> | more than? | 2 > 3 | false |
>= | more than or equal to? | 2 >= 3 | false |
total = 120
say total > 100
say total == 50
true
false
== and != work on any two values of the same kind, including text. <, <=, > and >= work on numbers.
Notice the difference between = and ==: = gives a value a name, while == asks whether two values are equal.
Combining questions: and, or, not
| Word | Answer is true when | Example |
|---|---|---|
and | both sides are true | age >= 18 and has_ticket |
or | at least one side is true | is_member or total > 100 |
not | the value is false | not sold_out |
age = 25
has_ticket = true
sold_out = false
say age >= 18 and has_ticket
say not sold_out
true
true
To check that a number is between two others, use and: write 1 <= qty and qty <= 10. Quill doesn’t allow 1 <= qty <= 10, because it’s easy to misread.
and stops as soon as it knows the answer. In count != 0 and total / count > 10, if count is zero, the division never happens. That makes and a safe way to protect a calculation.
Acting on answers: when
when checks a list of cases from the top and runs the first one that’s true:
total = 45
when
total > 100 -> say "Free delivery!"
total > 30 -> say "Delivery: 5"
else -> say "Delivery: 10"
Delivery: 5
Here’s how to read it:
- The cases go on the lines below
when, indented (moved in with spaces) further thanwhenitself. - Each case is a condition, then
->, then what to do. - Read
->as “then”: “when total is more than 100, then say Free delivery”. - Only the first true case runs.
totalis 45, so the first case is false and the second is true. Quill runs the second case and skips the rest, even thoughelsewould also match. elseis the case for “none of the above”. It’s optional, and if you use it, it must come last.
If no case is true and there’s no else, nothing happens:
stock = 50
when
stock < 10 -> say "Running low!"
say "Done"
Done
Why indentation matters
Indentation shows which lines belong to the when. The block ends when a line moves back to where when started:
temperature = 31
when
temperature > 30 -> say "It's hot"
else -> say "It's pleasant"
say "Have a nice day" -- not part of the when: this always runs
It's hot
Have a nice day
A block moves in by exactly 2 spaces. Not one space, not four, and not a tab. The cases of a when line up with each other, and a line that ends the block comes back to the same column as the when.
temperature = 31
when
temperature > 30 -> say "It's hot"
Problem on line 4 of example.quill:
temperature > 30 -> say "It's hot"
^^^^
This line is indented by 4 spaces.
A block moves in by exactly 2 spaces from the line above it, so this line should be indented by 2 spaces.
Changing values in a when
A case can change a var with set, but it can’t create a new value, because that value would only exist when that case runs. Create the value before the when, then set it inside:
total = 120
var delivery = 10
when
total > 100 -> set delivery = 0
total > 30 -> set delivery = 5
say "Delivery: {delivery}"
Delivery: 0
If you try to create a value inside a case, Quill explains what to do instead:
total = 120
when
total > 100 -> delivery = 0
Problem on line 3 of example.quill:
total > 100 -> delivery = 0
^^^^^^^^
A case of a when can't create a new value.
delivery would only exist when this case runs. Create it before the when with var delivery = ..., then change it here with set.
Did you mean: set delivery = 0
A when that gives a value
Often a when only exists to choose a value. Then you don’t need var and set at all. Put when where the value goes, and give a value after each ->:
total = 120
delivery = when
total > 100 -> 0
total > 30 -> 5
else -> 10
say "Delivery: {delivery}"
Delivery: 0
Read it out loud: “delivery is, when total is over 100, 0; when it’s over 30, 5; otherwise 10.” Quill picks the first case whose condition is true and uses its value. delivery never changes after that, so it doesn’t need var.
This works anywhere a value fits on its own: after =, after set, after say, and after return. A function that chooses between values can return the when directly:
fn grade(score: number) -> text
return when
score >= 70 -> "A"
score >= 50 -> "Pass"
else -> "Fail"
say grade(85)
say grade(40)
A
Fail
Two rules keep the value certain to exist:
- The last case is
else. Without it, no case might be true, and there would be no value:
total = 20
delivery = when
total > 100 -> 0
Problem on line 2 of example.quill:
delivery = when
^^^^
A when that gives a value needs else as its last case.
Without else, no case might be true, and there would be no value.
Did you mean: else -> "..."
- Every case gives the same kind of value.
deliverycan’t be a number in one case and text in another:
total = 20
delivery = when
total > 100 -> 0
else -> "ten"
Problem on line 4 of example.quill:
else -> "ten"
^^^^^
Every case of this when has to give the same kind of value.
0 on line 3 is a Number, but "ten" is Text.
When each case should do something, like say, keep when on its own line as before. When each case should be something, put when after =.
A when inside a when
A case can hold another when, indented further:
logged_in = true
is_admin = false
when
logged_in -> when
is_admin -> say "Welcome, admin"
else -> say "Welcome back"
else -> say "Please log in"
Welcome back
FizzBuzz, almost
FizzBuzz is a classic exercise: for a number, say “Fizz” if it divides by 3, “Buzz” if it divides by 5, “FizzBuzz” if both, and otherwise the number itself. % gives the remainder, so n % 3 == 0 asks “does n divide by 3?”:
n = 15
when
n % 15 == 0 -> say "FizzBuzz"
n % 3 == 0 -> say "Fizz"
n % 5 == 0 -> say "Buzz"
else -> say n
FizzBuzz
The order of the cases matters: 15 also divides by 3, but the FizzBuzz case comes first. A for loop, in Repeating with for, runs this for every number from 1 to 100.
Conditions must be true or false
A case needs a yes-or-no question. A number on its own isn’t one:
count = 3
when
count -> say "We have some"
Problem on line 3 of example.quill:
count -> say "We have some"
^^^^^
A case of a when needs a condition that is true or false, but count is a Number.
Compare it to something, like count > 0.
Try it
- Make a value
ageand usewhento say “child” under 13, “teenager” under 18, and “adult” otherwise. Changeageand run it again. - A shop gives 10% off orders over 200. Make a
var priceand usewhento reduce it withsetwhen it qualifies. - Rewrite FizzBuzz for a different number. What happens if you move the
FizzBuzzcase to the bottom? - Rewrite exercise 1 as
stage = when ..., thensay stage. Remove theelsecase and read what Quill says.
Repeating with for
Some work has to happen more than once: say every number from 1 through 10, add up a list of prices, print a row for each guest. A for loop repeats the lines under it, once for each number in a range. When those things are already gathered together, Lists and records shows how for walks a list. When you want one field, a total, how many there are, an average, the records in order, or the records inside other records, Asking for data does that in one line. for stays for the step each record needs on its own.
Counting from one number to another
for i in 1..3
say i
1
2
3
Read it as: “for each number i in 1 through 3, say i.”
iis the number for this pass. The first pass it is1, then2, then3.1..3includes both ends. The loop runs for 1, 2, and 3, and then stops.- The lines to repeat go below
for, indented by exactly 2 spaces. - Each pass,
imoves to the next number by itself. You don’t writesetfor it.
A range that starts after it ends does nothing. The lines under it are skipped, and the program continues:
for i in 3..1
say i
say "done"
done
for i in 5..5 runs once, with i as 5.
Keeping a total
The loop’s own name can’t be changed with set, and a line inside the loop can’t create a new value, because that value would be created again on every pass. Create a var before the loop, then change it inside:
var total = 0
for n in 1..4
set total = total + n
say total
10
That adds 1, then 2, then 3, then 4. 10 is their sum.
The end of the range is decided once, before the first pass. Changing a value inside the loop doesn’t move the finish line.
The name lives inside the loop
i exists on the lines under the for, and nowhere else:
for i in 1..3
say i
say i
If a value with that name already exists, the loop borrows the name while it runs. Afterwards the earlier value is still there:
i = 10
for i in 1..2
say i
say i
1
2
10
The range itself sees the earlier value. for i in i..12, when i is 10, counts from 10 through 12.
Common mistakes
The repeated lines have to be indented by exactly 2 spaces. On the same line as for, or back at the left edge, they aren’t part of the loop.
A range is two dots. Three dots is a mistake:
for i in 1...3
say i
Both ends of the range have to be numbers:
for i in 1.."3"
say i
And the loop can’t create a value:
for i in 1..3
label = i
Create label before the loop with var, then use set inside.
Try it
- Say the numbers from 1 through 5.
- Add those numbers into a
var total, and say the total when the loop is finished. It should be 15. - Try
for i in 5..5. How many lines does it print?
Asking for data
A large program, such as a bank, often wants one field from many records, the total of those amounts, how many records there are, their average, or the records in order. get, sum, count, average, and sorted by ask for that data in one line. for is still there when each record needs its own step. One record stays a record. A list wraps several records, and a record wraps a list when one thing owns many items. When a value wraps another shows each case.
get and sum are the usual choice for reading and totaling. You name the field. Quill walks the list. A missing amount is left out. for is the choice when this row is confirmed and the next row is declined, because each row does different work.
The total
sum adds one field of every record in a list:
sisters = record name = "Nchang Sisters", fee = 80000 USD
brass = record name = "Bamenda Brass", fee = 150000 USD
roster = list sisters, brass
say sum fee of roster
230000 USD
Read it as: “sum the fee of the roster.” Each record has fee. Quill adds the amounts. You do not write the adding.
The same line reads one record. The sum is that one amount:
school = record name = "Harvard", fees = 10000
say sum fees of school
10000
When an amount might be missing, sum adds the ones that are there:
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
Quiet Hour has no fee, so it is left out. The till is the money that exists. If every amount is missing, the sum is 0 in that currency.
sum only adds a number or an amount of money:
sisters = record name = "Nchang Sisters", fee = 80000 USD
roster = list sisters
say sum name of roster
Problem on line 3 of example.quill:
say sum name of roster
^^^^
sum adds amounts, but name is Text.
Name a number or an amount of money: sum fee of roster.
One field from every record
get takes one field and gives you a list:
sisters = record name = "Nchang Sisters", fee = 80000 USD
brass = record name = "Bamenda Brass", fee = 150000 USD
roster = list sisters, brass
say get fee of roster
80000 USD, 150000 USD
A missing amount is left out, the same way sum leaves it out. The list holds the fees that exist.
Several fields
Name every field you need, separated by commas. get gives a list of records that keep only those fields, in the order you wrote them:
sisters = record name = "Nchang Sisters", age = 24, fee = 80000 USD
brass = record name = "Bamenda Brass", age = 31, fee = 150000 USD
roster = list sisters, brass
people = get name, age, fee of roster
person = people at 1
say person.name
say person.age
say person.fee
Nchang Sisters
24
80000 USD
person is the first record. person.name reads its name. (people at 1).name is the same value. The parentheses are there because a dot binds to the value just before it, and that value would be the number 1.
The first record in people has name, age, and fee. It does not have any other field the original record had.
The same line reads one record. The result is one record with only the fields you named:
school = record
name = "Harvard"
location = "United Kingdom"
year = 2026
fees = 10000
data = get name, location, year, fees of school
say data.name
say data.fees
Harvard
10000
Name each field once:
sisters = record name = "Nchang Sisters", fee = 80000 USD
roster = list sisters
say get name, name of roster
Problem on line 3 of example.quill:
say get name, name of roster
^^^^
name is already in this get.
Name each field once: get name, fee of roster.
A field further in
The value after of can be a field of another record. Write the path.
school = record
name = "Harvard"
acceptances = record gpa = 4.0, sat = 1500
say get gpa of school.acceptances
4.0
school.acceptances is one record, so this is that one number.
A list can sit on a record the same way. The place and the acts are already on each booking:
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
first = desk.bookings at 1
say first.place
say first.acts
Up Station, Old Town
Up Station
Bamenda Brass, Hilltop Duo
desk.bookings is the list. get place of desk.bookings is every place. first is the first booking, so first.place and first.acts need no parentheses.
Records inside other records
with and in name the list and the field embedded in it, starting from the record that holds the list. The acts come back with the bookings:
up = record place = "Up Station", acts = list "Bamenda Brass", "Hilltop Duo"
old = record place = "Old Town", acts = list "Market Drums"
roster = record bookings = list up, old
tonight = get bookings with acts in roster
first = tonight at 1
say first.place
Up Station
bookings is the list you want. acts is the data embedded in each booking. in roster is where those records live. tonight is the bookings, and each one still has its acts.
The same line works when the bookings are inside a list of records. Quill gathers them into one list:
up = record place = "Up Station", acts = list "Bamenda Brass"
old = record place = "Old Town", acts = list "Market Drums"
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
Old Town
The embedded field has to be there:
up = record place = "Up Station", acts = list "Bamenda Brass"
roster = record bookings = list up
say get bookings with guest in roster
Problem on line 3 of example.quill:
say get bookings with guest in roster
^^^^^
This record has no field called guest.
Its fields are place, acts.
Only some records: where
Often you want only some of the records: the students who passed, the fees that are not paid yet. where keeps the records whose condition is true, in the same order:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
passed = students where score >= 50
say get name of passed
Amina, Omar
Read it as: “the students where the score is 50 or more.” Inside the condition, score is the field of each record. You don’t write student.score, and you don’t write the loop.
where works right inside get and sum, so one line can pick the records and ask for their data:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
say get name of students where score >= 50
say sum fee of students where paid == false
Amina, Omar
500 USD
Join conditions with and and or, or write where again. Both lines below keep only Omar:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
say get name of students where score >= 50 and paid == false
say get name of students where score >= 50 where paid == false
Omar
Omar
The text tools work in a condition too, so where can look inside text:
amina = record name = "Amina Okafor", email = "amina@school.org"
omar = record name = "Omar Diallo", email = "omar@mail.com"
students = list amina, omar
say get name of students where email ends with "school.org"
say get name of students where name contains "Omar"
Amina Okafor
Omar Diallo
The condition can read values from outside, too. A field comes first: if the records have a score, then score in the condition is the record’s score.
amina = record name = "Amina", score = 80
omar = record name = "Omar", score = 64
students = list amina, omar
pass_mark = 70
say get name of students where score >= pass_mark
Amina
When no record matches, the answer is an empty list, not a problem:
amina = record name = "Amina", score = 80
students = list amina
say students where score > 100
an empty list
for can walk the records that match: for s in students where paid == false.
How many: count
count of says how many items a list has:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
say count of students
say count of students where score >= 50
say count of students where paid == false
3
2
2
Read it as: “the count of the students where the score is 50 or more.” With where, only the records that match are counted. You don’t keep a counter and add 1 to it in a loop.
A count is a number, so it fits wherever a number does: inside text, in a when, or in a calculation:
amina = record name = "Amina", score = 80
omar = record name = "Omar", score = 64
students = list amina, omar
say "{count of students} students"
when
count of students >= 30 -> say "The class is full."
else -> say "There is room for {30 - count of students} more."
2 students
There is room for 28 more.
count is a Quill word only right before of. A value of your own can still be called count:
var count = 0
set count = count + 1
say count
1
The average
average adds one field of every record and divides by how many there are:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
say average score of students
say average fee of students
say average score of students where score >= 50
63
333.33 USD
72
The fees add up to 1000 USD, and a third of that is 333.333… USD. Money is rounded to whole cents, half to even, the way round total to cents rounds it. A number keeps every digit. To show fewer, round it: round average score of students to 1 place.
A missing amount is left out, the same way sum leaves it out. The average is of the amounts that are there.
An average of nothing has no answer, so Quill stops and says so:
amina = record name = "Amina", score = 80
students = list amina
say average score of students where score > 90
Problem on line 4 of example.quill:
say average score of students where score > 90
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There is nothing to average: students where score > 90 has no score.
An average needs at least one. Check count of students where score > 90 first, in a when.
When the list might be empty, check its count first:
amina = record name = "Amina", score = 80
students = list amina
top = students where score > 90
when
count of top > 0 -> say average score of top
else -> say "No one scored above 90."
No one scored above 90.
Like count, average is a Quill word only when a field and of come after it. fn average(total: number, many: number) is still a function you can write.
In order: sorted by
sorted by puts the records of a list in order of one field. The lowest comes first:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
say get name of students sorted by score
say get name of students sorted by highest score
say get name of students sorted by name
John, Omar, Amina
Amina, Omar, John
Amina, John, Omar
sorted by highest score puts the highest first. sorted by lowest score is the same as sorted by score, for when you want the line to say it. Numbers and money go from smallest to largest. Text goes in alphabetical order, with capital and small letters together. Two records with the same value keep the order they had.
sorted by goes after where, so one line can pick the records and put them in order:
amina = record name = "Amina", score = 80, fee = 500 USD, paid = true
omar = record name = "Omar", score = 64, fee = 300 USD, paid = false
john = record name = "John", score = 45, fee = 200 USD, paid = false
students = list amina, omar, john
for student in students where paid == false sorted by highest fee
say "{student.name} owes {student.fee}"
Omar owes 300 USD
John owes 200 USD
The first record of a sorted list is the top one. Name it, then read its fields:
amina = record name = "Amina", score = 80
omar = record name = "Omar", score = 64
students = list amina, omar
ranked = students sorted by highest score
top = ranked at 1
say "Top of the class: {top.name}"
Top of the class: Amina
When each row needs its own step
for still walks every item. Use it when the work is different for each one: say the name, or decide that this booking is declined.
sisters = record name = "Nchang Sisters", fee = 80000 USD
brass = record name = "Bamenda Brass", fee = 150000 USD
roster = list sisters, brass
for act in roster
say act.name
Nchang Sisters
Bamenda Brass
Ask with get, sum, count, or average when you want the data, the total, how many, or the typical amount. Add where to keep only some records, and sorted by to put them in order. Repeat with for when each record needs a step of its own. A program can use both.
Common mistakes
A dot binds to the value just before it. people at 1.name asks the number 1 for a field:
sisters = record name = "Nchang Sisters", age = 24, fee = 80000 USD
roster = list sisters
people = get name, age, fee of roster
say people at 1.name
Problem on line 4 of example.quill:
say people at 1.name
^^^^
1 is Number, so it has no fields.
A dot reads a field of a record, like address.city.
Give the record a name first: person = people at 1, then say person.name.
A name in a where condition has to be a field of the records, or a value you made:
amina = record name = "Amina", score = 80
students = list amina
say students where scor >= 50
Problem on line 4 of example.quill:
say students where scor >= 50
^^^^^
The records in students have no field called scor, and there's no value called scor.
Their fields are name, score.
Did you mean: score
A where condition runs to the end of the line. In a check, name the answer first, so the comparison isn’t read as part of the condition:
amina = record name = "Amina", score = 80
omar = record name = "Omar", score = 64
students = list amina, omar
pass_total = sum score of students where score >= 50
check pass_total == 144
where keeps records, so it needs a list of records. For a list of numbers or text, use for and when:
scores = list 80, 64, 45
say scores where scores > 50
Problem on line 3 of example.quill:
say scores where scores > 50
^^^^^
where keeps the records whose fields match, but scores holds Number.
Use where on a list of records, like students where score >= 50. To pick from other lists, use for and when.
sorted by names a field, so it puts records in order. A list of numbers has no fields to name:
scores = list 80, 64, 45
say scores sorted by score
Problem on line 3 of example.quill:
say scores sorted by score
^^^^^^
sorted by puts records in order of one field, but scores holds Number.
Sort a list of records, like roster sorted by fee.
Keep each number on a record, like record name = "Amina", score = 80, and sort the records.
count of counts a list. One record is not a list, so there is nothing to count:
amina = record name = "Amina", score = 80
say count of amina
Problem on line 3 of example.quill:
say count of amina
^^^^^
amina is one record, and count of counts the items in a list.
count of roster is how many records roster has.
Try it
- Make two records with a
feein USD, put them in a list, andsaythesum. getthe fees into their own list andsaythat list.- Add a record whose fee is
missing, and check that the sum left it out. - Put two bookings on a desk and
say get place of desk.bookings. - Give each record a
paidfield.say sum fee of roster where paid == falseto see what is still owed. say count of roster where paid == falseto see how many still owe, andsay average fee of rosterfor the typical fee.say get name of roster sorted by highest fee. Then sort bynameand read the order.
Lists and records
Sometimes one value is really several. A person’s hobbies are a list: values in order, all of the same kind. An address is a record: named fields kept together. Quill uses those two words, so the code says what it is.
A list
Write list, then the items, separated by commas:
hobbies = list "coding", "reading", "traveling"
say hobbies
say hobbies at 1
coding, reading, traveling
coding
- Every item is the same kind. A list of text stays a list of text.
atpicks one item. Lists count from 1, sohobbies at 1is"coding"andhobbies at 3is"traveling".- An empty list says its kind, because there is no item to learn it from:
list of text,list of number, orlist of true/false.
Where of and at are used says when each word is required.
A list of numbers works the same way: prices = list 12.50, 4, 8.25.
Mixing kinds is a mistake:
hobbies = list "coding", 2
Problem on line 1 of example.quill:
hobbies = list "coding", 2
^
Every item in a list has to be the same kind.
This list holds Text, but 2 is Number.
Square brackets are a mistake too. Quill’s name for this is list:
hobbies = ["coding", "reading"]
Problem on line 1 of example.quill:
hobbies = ["coding", "reading"]
^
Quill writes a list with the word list, not with square brackets.
A list is values of one kind, in order: list "coding", "reading".
Did you mean: list "coding", "reading"
Where of and at are used
of and at are reserved words, so they can’t be names. Each one has a single job, and in that job it is required.
at picks one item
Write the list, then at, then the position. The position is a whole number, and 1 is the first item:
hobbies = list "coding", "reading", "traveling"
say hobbies at 2
reading
Use at whenever you want one item by its place. To change that one item, the list has to be a var, and at stays in the set:
var hobbies = list "coding", "reading"
set hobbies at 2 = "writing"
say hobbies at 2
writing
at is for one position. Walking every item is for hobby in hobbies, which uses in. A field of a record is a dot: address.city. Replacing the whole list is set hobbies = list "cooking", with no at.
of names an empty list
A list that already has items leaves of out. The items show the kind: list "coding", "reading" is a list of text.
An empty list has no items to learn from, so of is required. The kind after it is text, number, or true/false:
waiting = list of text
prices = list of number
flags = list of true/false
say waiting
say prices
say flags
an empty list
an empty list
an empty list
A list with nothing after it is a mistake. Quill needs the items, or of and a kind:
waiting = list
Problem on line 1 of example.quill:
waiting = list
^^^^
list needs its items after it, separated by commas.
An empty list names its kind: list of text, list of number, or list of true/false.
Did you mean: list "coding", "reading"
Walking a list
for can walk a list, once for each item. The name holds the item, and it exists only under the for:
hobbies = list "coding", "reading", "traveling"
for hobby in hobbies
say hobby
coding
reading
traveling
The list itself is not changed. To keep a total, create a var before the loop, the same way a counting loop does.
Text and lists: split, join, contains
split TEXT by SEPARATOR cuts text into a list of texts, at each separator. It is how a line from a file or a form becomes pieces you can use:
line = "Amina,80,paid"
parts = split line by ","
say parts at 1
say parts at 2
say count of parts
Amina
80
3
The separator is left out of the pieces. Two separators side by side give an empty piece between them, so split "a,,b" by "," has three pieces: "a", "", and "b".
join LIST with SEPARATOR goes the other way. It puts the items of a list into one text, with the separator between them:
hobbies = list "coding", "reading", "traveling"
prices = list 3, 12.50, 7
say join hobbies with " and "
say join prices with " | "
coding and reading and traveling
3 | 12.50 | 7
A list of numbers or money joins too. Each item is shown the way say shows it.
contains works on a list as well as on text. It is true when the item is in the list:
hobbies = list "coding", "reading"
say hobbies contains "coding"
say hobbies contains "chess"
true
false
The item has to be the same kind as the list’s items, so hobbies contains 5 is a mistake.
split works on money too, where it shares an amount out without losing a cent. That is in Money. For text, split always takes by and the text between the pieces.
A record
A record is a group of named fields. Put record on its own line, then each field underneath, indented by exactly 2 spaces. A field is a name, then =, then a value:
address = record
street = "123 Main St"
city = "Anytown"
state = "CA"
zip = "12345"
say address.city
Anytown
A dot reads one field: address.city is "Anytown". say address shows every field:
address = record
street = "123 Main St"
city = "Anytown"
say address
street: 123 Main St
city: Anytown
A short record can sit on one line: address = record city = "Anytown", state = "CA". Braces are not how Quill writes a record. The word is record.
Fields can be different kinds. city can be text while another field is a number. Two records are the same kind when they have the same field names and each field has the same kind. The order you write the fields does not matter for that.
When a value wraps another
One school is a record. The fields belong to that one school, so sum reads them directly:
school = record name = "Harvard", fees = 10000
say sum fees of school
10000
A list wraps several records when they are the same kind of thing and one line should read all of them:
harvard = record name = "Harvard", fees = 10000
oxford = record name = "Oxford", fees = 12000
schools = list harvard, oxford
say sum fees of schools
22000
schools exists so the two fees can be added together. One school has no list around it.
A record wraps a list when one thing owns many items:
up = record place = "Up Station"
old = record place = "Old Town"
desk = record bookings = list up, old
say get place of desk.bookings
Up Station, Old Town
desk is one desk. bookings is the list it owns. get place of desk.bookings reads a field from that list.
Changing one piece
The whole list or record has to be marked var. Then set can replace one item or one field:
var hobbies = list "coding", "reading"
set hobbies at 2 = "writing"
say hobbies
var address = record city = "Anytown"
set address.city = "Springfield"
say address.city
coding, writing
Springfield
set hobbies = list "cooking" replaces the whole list. The new list still has to be a list of the same kind.
A record that isn’t marked var stays as it was written. Change it by writing var where it is created, then set the field.
Asking for one field
When the list holds records, sum and get read one field from all of them. One record gives that one amount: sum fees of school. get name, fee of roster keeps several fields together. A dot reaches a field further in: get place of desk.bookings, or get gpa of school.acceptances. count of roster says how many records the list has, average fee of roster gives the typical fee, and roster sorted by fee puts them in order. Asking for data shows each form. for stays when each record needs its own step.
sisters = record name = "Nchang Sisters", fee = 80000 USD
brass = record name = "Bamenda Brass", fee = 150000 USD
roster = list sisters, brass
say sum fee of roster
say get fee of roster
230000 USD
80000 USD, 150000 USD
Try it
- Make a list of three prices and say the second one.
- Walk that list with
forand add the prices into avar total. - Make a record for a person, with a name and an age, and say the name.
split "red,green,blue" by ",", thenjointhe pieces backwith " / ".
Functions
A function names a piece of work so you can run it again. You write it once, then call it by name.
fn double(n: number) -> number
return n * 2
say double(21)
42
fn starts the function. double is its name. Inside the parentheses, n: number is a parameter: a name, then :, then the kind of value it takes. -> number is the kind the function gives back. The lines under fn are indented by exactly 2 spaces, and return gives the value back.
double(21) is a call. The value inside the parentheses is what n holds for that call.
Why you use return
A function can work out an answer. return hands that answer to the line that called the function, so the rest of the program can use it.
say only prints. The next line cannot use what was printed. return gives the answer back, so you can print it, add to it, or check it:
fn shown(n: number)
say n * 2
fn doubled(n: number) -> number
return n * 2
shown(21)
var answer = doubled(21)
say answer + 1
42
43
shown(21) prints 42 and gives nothing back. doubled(21) gives 42 back. That is what answer holds, so answer + 1 is 43.
return only goes inside a function. On its own line, outside any fn, use say to show a value.
The words in the parentheses and the word after -> are separate. Each parameter names the kind you pass in. The arrow names the one kind that comes back. Those parameters can be different kinds from each other:
fn dataHope(name: text, age: number) -> text
return "{name} is {age}"
say dataHope("Amina", 18)
Amina is 18
name is text and age is a number. -> text means the call gives text back, so return has to give text. "{name} is {age}" is text: the number is written into the text, and the whole thing is one piece of text.
The arrow names one kind
-> number means every return in that function gives a number. -> text means every return gives text. Quill will not turn a number into text, or text into a number, to make the kinds match.
fn double(n: number) -> number
return "hi"
Problem on line 2 of example.quill:
return "hi"
^^^^
double gives back a Number, but "hi" is Text.
return has to give back a number.
A kind is text, number, true/false, or list of one of those:
fn owed(prices: list of number) -> number
var total = 0
for price in prices
set total = total + price
return total
say owed(list 3.50, 4.25)
7.75
var total lives inside the function. It is created each time the function runs, and it disappears when the function returns. A parameter is not a var: n stays the value you passed in.
A function that only does work
Leave off -> when the function gives nothing back. Call it on its own line:
fn greet(name: text)
say "Hello, {name}"
greet("Ngwang")
Hello, Ngwang
There is no value to give back, so return cannot hand one out. A value after return is a mistake:
fn greet(name: text)
return "Hello"
Problem on line 2 of example.quill:
return "Hello"
^^^^^^^
greet doesn't give a value back, so return stands on its own.
To give "Hello" back, write what greet gives back after ->.
To give "Hello" back, add -> text and keep the return.
return on its own, with no value after it, only stops the function early:
fn greet(name: text)
when
name == "" -> return
say "Hello, {name}"
greet("")
greet("Ngwang")
Hello, Ngwang
The check for "" is only an example of when you might stop early. A function with no -> does not have to test anything. Delete the when, and greet("Ngwang") still says hello. greet("") stops at return and says nothing. greet("Ngwang") skips that line, because the name is not empty, and reaches say.
Calling a function from a function
A function can call one written above it, and it can call itself:
fn fact(n: number) -> number
when
n <= 1 -> return 1
else -> return n * fact(n - 1)
say fact(5)
120
Every path has to reach return when the function gives a value back. Here both cases of the when return, so fact always gives a number back.
A function does not see names from outside it. Pass those values in as parameters.
Common mistakes
Forgetting return is a mistake. Quill has to know the function gives its kind back:
fn double(n: number) -> number
say n
Problem on line 1 of example.quill:
fn double(n: number) -> number
^^^^^^
double has to give back a Number.
End every path with return, like return 1.
Did you mean: return 1
A parameter’s kind is written with the words Quill has today: number, text, true/false. Names like Int and Text arrive later, with types.
Try it
- Write
fn triple(n: number) -> numberthat returnsn * 3, and saytriple(5). - Write
fn hello(name: text)that says hello to that name, and call it with your name. - Change
tripleso it returns text, and read what Quill says.
Money
A price is not just a number. 38.25 could be dollars or euros. Money names the currency, and Quill refuses to mix two currencies.
say 100 USD + 50 USD
150 USD
Write the amount, a space, then three capital letters. USD is dollars. EUR is euros. XAF is the Central African franc. The amount stays exact, the same way other numbers do.
You can subtract two amounts in the same currency, and you can multiply or divide an amount by a plain number:
say 100 USD * 0.90
say 100 USD / 8
say 7.5 USD
90 USD
12.50 USD
7.50 USD
An amount is shown in whole units (90 USD), or with every digit of its cents (7.50 USD, never 7.5 USD). If a calculation leaves a part of a cent, Quill shows it, so nothing is hidden. The next sections show how to decide what happens to it.
Currencies without cents
Some currencies have no cents. The Central African franc (XAF), the West African franc (XOF) and the yen (JPY) count in whole units. A few, like the Kuwaiti dinar (KWD), have three digits. Quill knows which is which:
say 5000 XAF + 250 XAF
say 1.5 KWD
5250 XAF
1.500 KWD
In this chapter, “cents” means the currency’s smallest unit, whatever it is called.
Rounding: round
round decides what happens to the digits you don’t want. round total to cents keeps whole cents:
say round 10.125 USD to cents
say round 10.135 USD to cents
10.12 USD
10.14 USD
When a value is exactly halfway, Quill rounds to the even digit. This is how banks round, and it means that over many amounts, rounding doesn’t push the total up or down.
Say which way when you need it. round up goes to the next larger value, and round down to the next smaller one:
say round up 10.121 USD to cents
say round down 10.129 USD to cents
10.13 USD
10.12 USD
Numbers round to a count of places, or to a whole number:
say round 2.71828 to 2 places
say round 2.5
say round 3.5
2.72
2
4
Quill never rounds money by itself. You write round where the rule matters, and a reader can see exactly what happens.
Sharing an amount: split
Dividing 100 USD between 3 people gives 33.333… USD each, which no one can pay. Rounding each share loses a cent. split gives whole cents, and the parts always add up to the amount:
say split 100 USD into 3
33.34 USD, 33.33 USD, 33.33 USD
The cent that is left over goes to the first part. Check it yourself:
parts = split 100 USD into 3
first = parts at 1
say first
33.34 USD
To share by percentages or weights, split by a list of shares. Here an artist keeps 70 and the agency 30:
say split 1000 USD by list 70, 30
say split 100 USD by list 1, 1, 1
700 USD, 300 USD
33.34 USD, 33.33 USD, 33.33 USD
Part of an amount: %
20% of fee is a fifth of the fee. It works on money and on plain numbers:
fee = 500 USD
say 20% of fee
say 15% of 80
say 12.5% of 1000 USD
100 USD
12
125 USD
A part of a percent can leave a part of a cent. Round it when it does: round 7.5% of fee to cents.
Changing currency: convert
Quill never guesses an exchange rate, because rates change every minute. convert changes an amount to another currency at the rate you write:
say convert 100 USD to EUR at 0.92
say convert 100 USD to XAF at 605.5
92 EUR
60550 XAF
at 0.92 means one dollar is worth 0.92 euros. The rate usually comes from a bank or a service, so in a real program it is a value you were given: convert price to EUR at today_rate.
Money in functions
A function names money by the currency:
fn sale_total(loaves: number, price: USD) -> USD
return loaves * price
say sale_total(3, 2.50 USD)
7.50 USD
Common mistakes
Two currencies cannot be added. Quill will not guess an exchange rate:
say 100 USD + 20 EUR
Problem on line 1 of example.quill:
say 100 USD + 20 EUR
^^^^^^^^^^^^^^^^
You're adding money in USD (100 USD) and money in EUR (20 EUR).
Quill won't mix currencies. Change one with convert, at the rate you were given: convert 20 EUR to USD at 0.92.
A currency is written in capitals. 100 usd is a mistake. Write 100 USD.
split works in whole cents. An amount with part of a cent has to be rounded first:
say split 10.005 USD into 2
Problem on line 1 of example.quill:
say split 10.005 USD into 2
^^^^^^^^^^^^^^^^^^^^^^^
split shares out whole cents, but 10.005 USD has a part smaller than that.
Round it first: round it to cents, then split it.
convert needs the rate:
say convert 100 USD to EUR
Problem on line 1 of example.quill:
say convert 100 USD to EUR
^^^
convert needs the rate, after at.
Quill never guesses a rate: at 0.92 means one unit is worth 0.92 EUR.
Did you mean: convert 100 USD to EUR at 0.92
round, split and convert are Quill’s own words, so they can’t name a value or a function. Call a function split_bill, not split.
Try it
- Add
20 USDand5 USD, and say the result. - Add
20 EURand5 USD, and read what Quill says. - Split a 250 USD dinner between 3 friends. Check that the parts add up to 250 USD.
- A booking fee is 80000 XAF, and the agency keeps 15%. Say the agency’s part, and what the artist keeps.
- Convert 100 USD to EUR at a rate of 0.92, then round the answer to cents.
Maybe
Some questions have no answer. A price list might not contain the name you asked for. maybe says the answer might be missing, and Quill will not run until you handle both cases.
fn find_price(name: text) -> maybe number
when
name == "bread" -> return found 2.50
else -> return missing
when find_price("bread")
found price -> say price
missing -> say "none"
2.50
-> maybe number means the function gives back a number, or it gives back nothing. found 2.50 is the number. missing is the gap. return missing is allowed only inside a function that gives a maybe back.
when followed by the call opens that maybe. found price holds the number for that one line. missing is the other case. Both cases are required:
fn find_price(name: text) -> maybe number
when
name == "bread" -> return found 2.50
else -> return missing
when find_price("bread")
found price -> say price
Problem on line 6 of example.quill:
when find_price("bread")
^^^^
This when doesn't handle missing.
A maybe can be missing, and that case has to be handled too.
Did you mean: missing -> say "none"
say find_price("bread") is a mistake too. say would hide the gap. Open the maybe with when first.
The kind after maybe can be text, number, true/false, a currency such as USD, or a list of one of those.
Try it
- Call
find_price("cake")and handle both cases. The cake is missing. - Change the function so
"cake"is found at4. - Remove the
missingcase and read what Quill says.
Failures
A payment can be declined. The function says so, and the caller has to decide what happens. Quill will not let the failure disappear.
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
var receipt = charge(250 USD)
on Declined -> say "Card declined"
say receipt
paid
fails Declined goes at the top of the function, right under fn. A failure’s name starts with a capital letter. fail Declined leaves the function with that failure. It does not give the text back.
Under the call, on Declined says what to do. Every failure named in fails needs its own on. When the call succeeds, Quill skips the on lines and continues. receipt holds "paid", and the next line says it.
When the call fails, the on line runs and the program stops. The lines after the call do not run, so they cannot use a receipt that was never created:
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
var receipt = charge(0 USD)
on Declined -> say "Card declined"
say receipt
Card declined
There is no paid line. The charge was declined, the on line said so, and the program stopped.
Carrying on with a value
Stopping is right when nothing sensible can happen next. Often something can: a declined card still deserves a receipt that says so. Put a value after -> instead of a statement. That value takes the place of the call’s answer, and the program carries on:
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
receipt = charge(0 USD)
on Declined -> "declined"
say "Receipt: {receipt}"
say "Thank you for shopping"
Receipt: declined
Thank you for shopping
Read it out loud: “receipt is the charge, or on Declined, declined.” The value has to be the same kind the call gives back. charge gives back text, so on Declined -> 0 is a mistake:
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
receipt = charge(0 USD)
on Declined -> 0
Problem on line 8 of example.quill:
on Declined -> 0
^
charge gives back Text, but 0 is a Number.
The value after -> takes the place of the call's answer, so it has to be text too.
Each on chooses for itself. One failure can give a value while another stops:
fn charge(amount: USD) -> text
fails Declined, NetworkDown
when
amount <= 0 USD -> fail Declined
amount > 1000 USD -> fail NetworkDown
return "paid"
receipt = charge(5000 USD)
on Declined -> "declined"
on NetworkDown -> say "The bank is offline. Try again later."
say "Receipt: {receipt}"
The bank is offline. Try again later.
So the rule after -> is: a statement like say runs and then stops, and a value is used and the program carries on. A call on its own line gives nothing back, so under it on always runs a statement.
A function can pass a failure upward instead of handling it. try is allowed only inside a function that fails the same way:
fn charge(amount: USD) -> text
fails Declined
when
amount <= 0 USD -> fail Declined
else -> return "paid"
fn checkout(amount: USD) -> text
fails Declined
return try charge(amount)
var receipt = checkout(250 USD)
on Declined -> say "Card declined"
say receipt
paid
checkout declares Declined too, so try charge(amount) can pass that failure on. The caller of checkout still handles it.
Calling charge with no on and no try is a mistake. Quill names the failure you forgot.
try goes where the call’s answer is used: after return, = or say. A call that gives nothing back, like write_file, stands on its own line, and try can’t stand there. Pass its failure upward with on instead:
permit write "out"
fn save(note: text)
fails CantWrite
try write_file("out/note.txt", note)
permit write "out"
fn save(note: text)
fails CantWrite
write_file("out/note.txt", note)
on CantWrite -> fail CantWrite
on CantWrite -> fail CantWrite does what try would do: save leaves with the same failure, and its caller handles it.
Try it
- Charge
0 USDand handleDeclined. - Add
fails NetworkDownand a secondon. Forget one of them and read what Quill says. - Move the charge into a second function with
try, and handleDeclinedat the call. - Change
on Declined -> say "Card declined"toon Declined -> "declined", and add asayline after the call. Which version reaches that line?
Contracts
A bank never lets you withdraw a negative amount, and it never leaves you owing money after a withdrawal. Those are rules about the function itself. In Quill, you write them at the top of the function, where every reader sees them first.
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
ensure result >= 0 USD
return balance - amount
say withdraw(100 USD, 30 USD)
70 USD
require says what has to be true when the function is called. Here, the amount has to be more than zero.
ensure says what is true when the function gives back. result is the value it gives back. Here, the balance that is left is never below zero.
Together they are the function’s contract: what it needs, and what it promises. When both are true, nothing happens and the program carries on. The rules cost one line each, and they don’t change what the function does.
When a require is broken
If the caller passes a value the function doesn’t accept, the program stops at the require line. Quill says what the value was, and which call passed it:
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
ensure result >= 0 USD
return balance - amount
say withdraw(100 USD, -5 USD)
Problem on line 2 of bank.quill:
require amount > 0 USD
^^^^^^^^^^^^^^^^^^^^^^
withdraw requires amount > 0 USD, but amount is -5 USD.
The call on line 6 gave withdraw a value it doesn't accept. Check the value before calling withdraw.
A broken require is the caller’s mistake. withdraw never ran a line of its own work, so nothing was taken from the balance.
When an ensure is broken
If the function is about to give back a value that breaks its own promise, the program stops at the ensure line instead:
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
ensure result >= 0 USD
return balance - amount
say withdraw(10 USD, 30 USD)
Problem on line 3 of bank.quill:
ensure result >= 0 USD
^^^^^^^^^^^^^^^^^^^^^^
withdraw ensures result >= 0 USD, but result is -20 USD.
The mistake is inside withdraw: it was about to give back a value that breaks its own ensure.
A broken ensure is the function’s own mistake. Here withdraw let someone take more than they had. One way to fix it is a second require, so the caller has to check first:
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
require amount <= balance
ensure result >= 0 USD
return balance - amount
say withdraw(100 USD, 100 USD)
0 USD
Another way is a failure, when running out of money is something the caller should handle and carry on from:
fn withdraw(balance: USD, amount: USD) -> USD
require amount > 0 USD
ensure result >= 0 USD
fails InsufficientFunds
when
amount > balance -> fail InsufficientFunds
return balance - amount
left = withdraw(100 USD, 30 USD)
on InsufficientFunds -> say "Not enough money"
say left
70 USD
A range on a parameter: where
Many rules are a range: a number of guests from 1 to 500, a score from 0 to 100, a payment that is never more than the limit. Write those right on the parameter with where:
fn book(guests: number where 1..500) -> text
return "Booked for {guests} guests"
say book(120)
Booked for 120 guests
where 1..500 means from 1 through 500, including both ends. It works the same way as require guests >= 1 and guests <= 500, but it sits next to the parameter, so a reader sees the rule and the kind together.
Money works too. Both ends are amounts in the parameter’s currency:
fn pay(amount: USD where 1 USD..10000 USD) -> USD
return amount
say pay(250 USD)
250 USD
When the call passes a value written right there, like book(600), Quill catches it before the program runs:
fn book(guests: number where 1..500) -> text
return "Booked for {guests} guests"
say book(600)
Problem on line 4 of hall.quill:
say book(600)
^^^
book takes guests from 1 through 500, but this call gives 600.
Line 1 says guests where 1..500. Give book a value in that range.
When the value is only known while the program runs, the call stops at the rule, the same way a broken require does:
fn book(guests: number where 1..500) -> text
return "Booked for {guests} guests"
guests = 200 + 400
say book(guests)
Problem on line 1 of hall.quill:
fn book(guests: number where 1..500) -> text
^^^^^^^^^^^^
book takes guests from 1 through 500, but guests is 600.
The call on line 5 gave book a value outside that range. Check the value before calling book.
Use where for a range, and require for every other rule: one parameter compared to another (require amount <= balance), text that must not be empty, or a number that only has a lower limit (require amount > 0 USD).
Where contracts help
Add a contract when a wrong value would quietly give a wrong answer instead of an error. Some places where that happens:
- Money. A payment above zero, a balance that never goes below zero, a discount that is never more than the price.
- Dividing. A count that is more than zero before you divide by it.
- Ranges. A score from 0 to 100, a quantity from 1 to 50, a percentage from 0 to 100. Write these with
where. - Promises about the answer. A grade that is never empty, a total that is never less than its first item.
Here is a school that keeps all three kinds of rule:
fn average(total: number, count: number) -> number
require count > 0
ensure result >= 0
return total / count
fn pay_fees(owed: USD, paid: USD) -> USD
require paid > 0 USD
require paid <= owed
ensure result >= 0 USD
return owed - paid
fn grade(score: number where 0..100) -> text
ensure result != ""
when
score >= 70 -> return "A"
score >= 50 -> return "Pass"
return "Fail"
say average(240, 3)
say pay_fees(500 USD, 200 USD)
say grade(85)
say grade(40)
80
300 USD
A
Fail
Without require count > 0, average(240, 0) would stop with a divide-by-zero deep inside the function. With it, Quill says which call passed the 0. Without require paid <= owed, pay_fees(500 USD, 900 USD) would give back -400 USD, and the school would owe the student money.
A contract is worth writing when you can say the rule in one short sentence. If you can’t, the rule probably belongs in the function’s work, or in a failure.
A contract, a failure, or a check?
They look alike, but each one answers a different question:
| Word | Who is wrong when it happens | What happens |
|---|---|---|
require | the caller passed a value the function doesn’t accept | the program stops |
where | the caller passed a value outside the parameter’s range | Quill stops before running, or the program stops |
ensure | the function broke its own promise | the program stops |
fails | nobody: it is a normal outcome, like a declined card | the caller handles it with on |
check | a test found a wrong answer | quill test reports it |
Use fails when the caller can do something about it. Use require and ensure for things that should never happen in a correct program.
Writing contracts
require,ensure, andfailsgo at the top of the function, right under thefnline, in any order. A function can have as manyrequireandensurelines as it needs.- Each one holds one condition that is true or false. Join two with
and, or write two lines. requirereads the parameters.ensurereads the parameters andresult.- An
ensureis checked at everyreturn, so a function with threereturnlines keeps its promise on all three. resultexists only inensure. Inside the function,resultis an ordinary name you can use for something else.wheregoes after a parameter’s kind:guests: number where 1..500. It works on a number or a currency, both ends are written values of that kind, and the smaller one comes first. Eachwhereis checked when the function is called, before itsrequirelines.
Common mistakes
ensure on a function that gives nothing back has no result to check:
fn greet(name: text)
ensure name != ""
say "Hello, {name}"
Problem on line 2 of example.quill:
ensure name != ""
^^^^^^^^^^^^^^^^^
ensure promises something about the value greet gives back, but greet gives nothing back.
Add -> and a kind to the fn line, or use require for what has to be true when greet is called.
A require can’t read result, because it is checked before there is one:
fn double(n: number) -> number
require result > 0
return n * 2
Problem on line 2 of example.quill:
require result > 0
^^^^^^^^^^^^^^^^^^
result is the value double gives back, and it doesn't exist yet when double is called.
Check result with ensure instead of require.
A contract goes at the top. After the function has started its work, it is too late:
fn double(n: number) -> number
say n
require n > 0
return n * 2
Problem on line 3 of example.quill:
require n > 0
^^^^^^^
require goes at the top of a function, right under the fn line.
require says what has to be true when the function is called, and ensure says what is true when it gives back. Move this line up, above the function's other lines.
A where range is written smallest first:
fn grade(score: number where 100..0) -> text
return "A"
Problem on line 1 of example.quill:
fn grade(score: number where 100..0) -> text
^^^^^^^^^^^^
score where 100..0 starts above where it ends, so no value fits it.
Write the smallest value first, then .., then the largest.
Did you mean: where 0..100
Both ends of the range have the same kind as the parameter. For money, that means writing the currency on each end:
fn pay(amount: USD where 0..100 USD) -> USD
return amount
Problem on line 1 of example.quill:
fn pay(amount: USD where 0..100 USD) -> USD
^
amount is money in USD, so each end of its range is too, but 0 is a number.
A rule can only compare values of the same kind.
Did you mean: 0 USD
quill explain shows how Quill reads a contract line. On the ensure line of withdraw, it says result is “the value withdraw gives back”. On a fn line with where, it says when the range is checked.
Try it
- Write
fn split_bill(total: USD, people: number) -> USDthat divides a bill. Addrequire people > 0, then call it with0people and read what Quill says. - Add
ensure result <= totaltosplit_bill. Call it with1person, then with4. - Write a function with two
returnlines and oneensure. Make one of the returns break the promise, and see which line Quill points to. - Change
split_billsopeopleisnumber where 1..20. Call it with25, then with a value worked out while the program runs, like10 + 15. Compare the two messages.
Types
A booking always has a venue, a number of guests, and a fee. A record can hold those, but nothing stops one booking from forgetting its fee, or another from having 9000 guests in a hall for 500. A type writes down, once, what every booking has and what each field allows.
type Booking
venue: text
guests: number where 1..500
fee: USD
hall = Booking venue = "Town Hall", guests = 120, fee = 500 USD
say hall.venue
say hall.guests
Town Hall
120
type Booking names a kind of record. Each line under it is one field: a name, then :, then its kind, the same way a function writes a parameter. A field can have a range with where, as in Contracts.
Booking venue = ..., guests = ..., fee = ... makes a new booking. It reads like record, with the type’s name in place of the word record. A type’s name starts with a capital letter, so it stands apart from values.
Making a record of a type
Every field has to be there, and each one has to be the right kind. A long record can put one field on each line:
type Booking
venue: text
guests: number where 1..500
fee: USD
garden = Booking
venue = "Garden"
guests = 40
fee = 200 USD
say garden
venue: Garden
guests: 40
fee: 200 USD
A field that might not have a value is a maybe, and you write missing or found for it, as in Maybe:
type Guest
name: text
phone: maybe text
amina = Guest name = "Amina", phone = found "555-0101"
omar = Guest name = "Omar", phone = missing
say omar.name
Omar
Types in functions
A function can take a type, and give one back. Write the type’s name where a kind goes:
type Booking
venue: text
guests: number where 1..500
fee: USD
fn deposit(b: Booking) -> USD
return b.fee / 5
fn bigger(b: Booking, more: number) -> Booking
return Booking venue = b.venue, guests = b.guests + more, fee = b.fee
hall = Booking venue = "Town Hall", guests = 120, fee = 500 USD
say deposit(hall)
say bigger(hall, 30).guests
100 USD
150
A list of a type is written list of Booking. Everything in Asking for data works on it:
type Booking
venue: text
guests: number where 1..500
fee: USD
hall = Booking venue = "Town Hall", guests = 120, fee = 500 USD
garden = Booking venue = "Garden", guests = 40, fee = 200 USD
bookings = list hall, garden
say get venue of bookings where guests > 100
say sum fee of bookings
Town Hall
700 USD
The rules always hold
A field’s where is checked every time a record of the type is made. When the value is written right there, Quill finds the mistake before the program runs:
type Booking
venue: text
guests: number where 1..500
big = Booking venue = "Stadium", guests = 9000
Problem on line 5 of hall.quill:
big = Booking venue = "Stadium", guests = 9000
^^^^
A Booking takes guests from 1 through 500, but this one gives 9000.
Line 3 says guests where 1..500.
When the value is worked out while the program runs, making the record stops the program:
type Booking
venue: text
guests: number where 1..500
crowd = 400 + 200
big = Booking venue = "Stadium", guests = crowd
Problem on line 6 of hall.quill:
big = Booking venue = "Stadium", guests = crowd
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A Booking takes guests from 1 through 500, but guests is 600.
A Booking can't be made with a value outside that range. Check the value before making the Booking.
A function that takes a Booking checks the rules too, so even a plain record with the same fields can’t bring a value outside the range into it. Inside the function, b.guests is always from 1 through 500.
Common mistakes
Every field has to be there:
type Booking
venue: text
guests: number
hall = Booking venue = "Town Hall"
Problem on line 5 of example.quill:
hall = Booking venue = "Town Hall"
^^^^^^^
This Booking is missing the field guests.
Every Booking has all of its fields. Add it, or use missing for a maybe field.
A misspelled field is found before the program runs:
type Booking
venue: text
guests: number
hall = Booking venue = "Town Hall", gests = 120
Problem on line 5 of example.quill:
hall = Booking venue = "Town Hall", gests = 120
^^^^^
A Booking has no field called gests.
Its fields are venue, guests.
Did you mean: guests
A type can’t hold itself, because that one would hold another, and so on forever. Hold something that names the other one, like its id:
type Person
name: text
manager: maybe Person
Problem on line 3 of example.quill:
manager: maybe Person
^^^^^^^^^^^^
A Person can't hold a Person inside it, because that one would hold another, forever.
Hold something that names the other Person instead, like its id.
quill explain shows what a type line means. On type Booking, it lists each field and when its range is checked.
Try it
- Write
type Studentwithname: textandscore: number where 0..100. Make two students andsayeach one. - Make a student with a score of
120and read what Quill says. - Write
fn passed(s: Student) -> true/falsethat gives back whether the score is 50 or more. - Put your students in a list, and
say get name of students where score >= 50.
Bringing in Other Files
A small program fits in one file. A real one doesn’t: the billing rules, the booking types and the program that uses them are easier to read, test and change apart. bring puts a program together from several files.
Say the billing rules live in their own file, billing.quill:
-- billing.quill: what a booking costs.
--- example: deposit(1000 USD) == 200 USD
fn deposit(fee: USD) -> USD
return 20% of fee
type Invoice
to: text
amount: USD
Another file, in the same folder, brings it in and uses what it holds:
bring billing
invoice = Invoice to = "Amina", amount = deposit(500 USD)
say "{invoice.to} pays {invoice.amount}"
Amina pays 100 USD
bring billing reads billing.quill, the file with that name next to this one. Its functions and types are then used by name, as if they were written here. Write the name without .quill.
What a brought file holds
A brought file holds functions and types: the pieces other files use. It can also bring files of its own.
Its tests stay with it. The --- example: above deposit runs when you test billing.quill with quill test billing.quill, not every time app.quill runs. That way each file proves itself, and a program that brings it doesn’t repeat that work.
A brought file doesn’t do anything when it is brought in: a line like say would run in every program that brings it, so it is a mistake:
say "Loading billing..."
fn fee() -> USD
return 10 USD
bring noisy
say fee()
Problem on line 1 of noisy.quill:
say "Loading billing..."
^^^
noisy.quill is brought into app.quill, so it holds functions and types, but this line does something.
Move it into the program that brings noisy, or into a function.
Files in other folders
bring billing looks next to the file that brings it. For a file in another folder, write its path in quotes, with .quill:
bring "shared/money.quill"
The path is counted from the folder of the file that brings it, with / between folders on every system.
Common mistakes
A misspelled name is found before anything runs, and Quill suggests the file you probably meant:
bring biling
Problem on line 1 of app.quill:
bring biling
^^^^^^
app.quill brings biling.quill, but there's no file biling.quill in this folder.
bring biling looks for biling.quill next to app.quill.
Did you mean: bring billing
bring takes the name without .quill:
bring billing.quill
Problem on line 1 of app.quill:
bring billing.quill
^
bring names the file without .quill.
bring billing brings billing.quill. For a file in another folder, write its path in quotes: bring "shared/billing.quill".
Did you mean: bring billing
Two files can’t write a function or a type with the same name, because a reader couldn’t tell which one a call means:
bring billing
fn deposit(fee: USD) -> USD
return fee
Problem on line 3 of app.quill:
fn deposit(fee: USD) -> USD
^^^^^^^
deposit is already a function, written in billing.quill on line 4.
Each function needs its own name, across every file the program brings. Rename one of them.
Two files can’t bring each other. Move what both need into a third file, and bring that one from each.
web, db, ai and http are the names of Quill’s own modules, so bring web brings the web module, not a file called web.quill.
Try it
- Make a folder with two files:
prices.quillwithfn with_tax(price: USD) -> USD, andshop.quillthat bringspricesand sayswith_tax(100 USD). - Add an
--- example:abovewith_taxinprices.quill, and runquill test prices.quill. - Misspell the name after
bringand read what Quill suggests. - Add a
type Producttoprices.quill, and make aProductinshop.quill.
Files, Settings and Permits
So far, programs only worked with values written inside them. Real programs read a price list from a file, write a report, and use a secret key that must never be written in the code. Quill can do all of that, but only what the program permits.
Permits
A permit line at the top of a program says one thing it may touch outside itself:
permit read "config" -- read files in the config folder
permit write "reports" -- write files in the reports folder
permit env "STRIPE_KEY" -- read the setting STRIPE_KEY
permit net "api.stripe.com" -- talk to this host (used by http and ai)
Anything else is refused. A reader sees everything a program can reach in its first lines, and a mistake, or a file you brought in from someone else, can’t quietly read your secrets or send your data somewhere.
A folder permit covers everything inside it: permit read "config" allows config/rates.txt and config/2026/may.txt. permit read "." allows the program’s own folder and everything below it. Paths are counted from the folder the program is in.
Reading a file
Say config/rates.txt holds one line:
USD 1
read_file gives back the whole file as text:
permit read "config"
rates = read_file("config/rates.txt")
on FileMissing -> "none"
say "Rates: {rates}"
Rates: USD 1
The file ends with a new line, like most text files, so say shows an empty line after it.
A file might not be there, so read_file can fail with FileMissing, and the program has to say what happens then, the same as any failure. Here the program carries on with "none".
file_exists answers whether a file is there, without reading it:
permit read "config"
say file_exists("config/rates.txt")
say file_exists("config/old.txt")
true
false
Writing a file
write_file writes text to a file, replacing what was there. It makes the folder first if it isn’t there yet:
permit write "reports"
permit read "reports"
write_file("reports/today.txt", "All 3 bookings confirmed.")
on CantWrite -> say "Couldn't save the report."
done = read_file("reports/today.txt")
on FileMissing -> "nothing"
say done
All 3 bookings confirmed.
Writing can fail too, for example when the disk is full, so write_file fails with CantWrite.
Settings and secrets: env
A secret key belongs outside the code, in a setting of the computer the program runs on (an environment variable). env reads one. A setting might not be set, so env gives back a maybe:
permit env "QUILL_BOOK_EXAMPLE_KEY"
when env("QUILL_BOOK_EXAMPLE_KEY")
found key -> say "The key is set."
missing -> say "Set QUILL_BOOK_EXAMPLE_KEY before running."
Set QUILL_BOOK_EXAMPLE_KEY before running.
Keys don’t belong in code, because code gets shared, copied and published. A setting stays on the one computer that needs it.
When a permit is missing
When the path or name is written in the call, Quill finds a missing permit before the program runs, and says which line to add:
key = read_file("secrets/key.txt")
on FileMissing -> "none"
say key
Problem on line 1 of example.quill:
key = read_file("secrets/key.txt")
^^^^^^^^^^^^^^^^^
read_file can't read secrets/key.txt, because the program has no permit for it.
A program touches only what its permits allow. If it should, add this at the top of the program.
Did you mean: permit read "secrets"
When the path is only known while the program runs, the call stops the program instead. A path can’t climb out of a permitted folder with ..:
permit read "config"
path = "config/../secrets.txt"
key = read_file(path)
on FileMissing -> "none"
say key
Problem on line 4 of example.quill:
key = read_file(path)
^^^^^^^^^^^^^^^
read_file can't read config/../secrets.txt, because the program has no permit for it.
A program touches only what its permits allow. If it should, add this at the top of the program: permit read "secrets.txt"
Permits belong to the program you run. A file you bring in can’t give itself permits, so everything a program may touch is in one place.
Common mistakes
A failure has to be handled. Forgetting on FileMissing is found before running:
permit read "config"
say read_file("config/rates.txt")
Problem on line 3 of example.quill:
say read_file("config/rates.txt")
^^^^^^^^^
read_file can fail with FileMissing.
Handle each failure with on under the call, or write try read_file(...) inside a function that fails the same way.
permit net names a host, not a web address:
permit net "https://api.stripe.com/v1"
Problem on line 1 of example.quill:
permit net "https://api.stripe.com/v1"
^^^^^^^^^^^^^^^^^^^^^^^^^^^
permit net names a host, like api.stripe.com, without https:// or a path.
Every path on that host is allowed.
Did you mean: permit net "api.stripe.com"
read_file, write_file, file_exists and env are Quill’s own functions, so a function you write can’t use those names.
Try it
- Make a file
notes/hello.txt, and a program that permits readingnotesand says what is in it. - Remove the permit, and read what Quill says.
- Write a program that writes the date you choose into
out/today.txt, then reads it back. - Read a setting with
env, and say whether it is set.
Reading and Writing JSON
Programs talk to each other in JSON: a web form sends it, a payment service answers with it, a file stores it. It looks like this:
{"venue": "Town Hall", "guests": 120, "fee": "500.00"}
JSON from outside can’t be trusted. A field may be missing, a number may be text, a guest count may be 9000. Quill reads JSON as a kind, and checks all of it before your program uses any of it.
Reading JSON as a type
Say a booking arrives in a file, inbox/booking.json:
{"venue": "Town Hall", "guests": 120, "fee": "500.00"}
as Booking reads the text as a Booking:
permit read "inbox"
type Booking
venue: text
guests: number where 1..500
fee: USD
body = read_file("inbox/booking.json")
on FileMissing -> "none"
booking = body as Booking
on Invalid -> Booking venue = "unknown", guests = 1, fee = 0 USD
say booking.venue
say booking.fee
Town Hall
500 USD
as Booking checks that every field is there, that each one is the right kind, and that guests is from 1 through 500. Money is read exactly, from a number or from text like "500.00", so no cent is lost. Fields the type doesn’t name are left out.
When the text doesn’t fit, as fails with Invalid, and the program says what happens, like any failure. Here a booking for 9000 guests is refused:
{"venue": "Stadium", "guests": 9000, "fee": 1}
permit read "inbox"
type Booking
venue: text
guests: number where 1..500
fee: USD
body = read_file("inbox/huge.json")
on FileMissing -> "none"
booking = body as Booking
on Invalid -> Booking venue = "refused", guests = 1, fee = 0 USD
say booking.venue
refused
A program never holds a Booking that breaks its type’s rules, wherever the data came from.
Other kinds
as reads any kind a function can name: as number, as text, as list of Booking, as maybe number. A field that is maybe can be null or left out of the JSON, and is then missing:
[1.10, 2.25, 0.10]
permit read "inbox"
body = read_file("inbox/prices.json")
on FileMissing -> "[]"
prices = body as list of number
on Invalid -> list of number
say prices
1.10, 2.25, 0.10
Writing JSON
json turns any value into JSON text: a record becomes an object, a list an array, money an exact number, and missing becomes null:
type Booking
venue: text
guests: number
fee: USD
hall = Booking venue = "Town Hall", guests = 120, fee = 500.50 USD
say json(hall)
say json(list 1, 2, 3)
{"venue":"Town Hall","guests":120,"fee":500.50}
[1,2,3]
JSON written inside a program
JSON usually comes from outside: a file, a request, an answer. When you write it inside your own text, remember that { starts a value in Quill text. Write \{ and \} for the braces themselves:
type Point
x: number
y: number
point = "\{\"x\": 3, \"y\": 4\}" as Point
on Invalid -> Point x = 0, y = 0
say point.x + point.y
7
Common mistakes
Reading JSON can fail, so the failure has to be handled:
type Point
x: number
point = "[]" as Point
say point.x
Problem on line 4 of example.quill:
point = "[]" as Point
^^^^^^^^
Invalid isn't handled.
Reading text as Point can fail with Invalid, so the call needs on Invalid.
Did you mean: on Invalid -> say "Invalid"
as reads text. A value that is already a number or a record doesn’t need reading:
type Point
x: number
point = 5 as Point
on Invalid -> Point x = 0
Problem on line 4 of example.quill:
point = 5 as Point
^
as Point needs text for the JSON, but 5 is a Number.
as reads JSON text, like body as Point.
Try it
- Write
type Studentwithname: textandscore: number where 0..100, and a file with a student in JSON. Read it withas Student. - Change the score in the file to
120, and seeon Invalidrun. - Read a JSON list of students with
as list of Student, and say the names of those who passed withget name of students where score >= 50. - Say
jsonof one of your students.
Talking to Web Services
Most backends talk to other services: an exchange rate service, a payment provider, a delivery company. They do it with web requests, and the answers usually come back as JSON. Quill’s http module sends them.
bring http
permit net "api.example.com"
type Rates
usd_to_eur: number
answer = fetch("https://api.example.com/rates")
on NetworkDown -> say "The rate service can't be reached."
when
answer.status == 200 -> say "Got the rates."
else -> say "The service answered {answer.status}."
rates = answer.body as Rates
on Invalid -> Rates usd_to_eur = 0
say convert 100 USD to EUR at rates.usd_to_eur
Each part has a job:
bring httpbrings in Quill’shttpmodule, so a reader sees at the top that the program uses the network.permit net "api.example.com"lets the program talk to that one host, and no other. Without it, the call is refused (see Permits).fetch(url)asks for the page aturland gives back an answer: a record withstatus, a number like200or404, andbody, the text that came back.as Ratesreads the body as a type, checking every field (see JSON).
The examples in this chapter talk to services on the internet, so the book shows them without running them.
When a request goes wrong
There are two different ways for a request to go wrong, and Quill keeps them apart:
- The service can’t be reached: no network, a wrong host name, or no answer within 30 seconds. That’s the failure
NetworkDown, handled withon NetworkDownortry, like any failure. - The service answers with an error, like
404(not found) or500(the service broke). That’s still an answer. Itsstatussays what happened, and your program decides what to do.
Check the status before trusting the body. 200 to 299 mean it worked.
Sending data: post and send
post(url, body) sends text to a service, usually JSON made with json(...). It says the body is JSON for you:
bring http
permit net "api.example.com"
type Order
item: text
amount: USD
order = Order item = "Bamenda Brass", amount = 150000 USD
reply = post("https://api.example.com/orders", json(order))
on NetworkDown -> say "Couldn't send the order."
say reply.status
send(method, url, body, headers) sends any kind of request, with your own headers. Each header is text with a name, a colon, and a value. A key from a setting goes in a header, not in the code:
bring http
permit net "api.example.com"
permit env "SHOP_KEY"
fn cancel_order(key: text) -> number
reply = send("DELETE", "https://api.example.com/orders/7", "", list "Authorization: Bearer {key}")
on NetworkDown -> return 0
return reply.status
when env("SHOP_KEY")
found key -> say cancel_order(key)
missing -> say "Set SHOP_KEY first."
The methods are GET, POST, PUT, PATCH, DELETE and HEAD. Use "" for no body.
Common mistakes
A program has to bring the module before it uses it:
permit net "api.example.com"
answer = fetch("https://api.example.com/rates")
on NetworkDown -> say "offline"
Problem on line 3 of example.quill:
answer = fetch("https://api.example.com/rates")
^^^^^
fetch is part of Quill's http module, which this program doesn't bring.
Add bring http at the top, so a reader sees what the program uses.
Did you mean: bring http
Without a permit for the host, the call is refused before the program runs:
bring http
answer = fetch("https://api.example.com/rates")
on NetworkDown -> say "offline"
Problem on line 3 of example.quill:
answer = fetch("https://api.example.com/rates")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
fetch can't reach https://api.example.com/rates, because the program has no permit for it.
A program touches only what its permits allow. If it should, add this at the top of the program.
Did you mean: permit net "api.example.com"
A web address starts with https:// or http://:
bring http
permit net "api.example.com"
answer = fetch("api.example.com/rates")
on NetworkDown -> say "offline"
Problem on line 4 of example.quill:
answer = fetch("api.example.com/rates")
^^^^^^^^^^^^^^^^^^^^^^^
api.example.com/rates isn't a web address.
A web address starts with https:// or http://, like https://api.example.com/rates.
Try it
- Write a program that brings
http, permits one host, and fetches a page from it. Say the status. - Remove the permit and read what Quill says.
- Fetch a page that doesn’t exist on that host, and handle the
404withwhen. - Make a record, and send it with
post(url, json(record))to a test service likehttps://httpbin.org/post.
Asking an AI Model
A program can ask an AI model a question in one line: summarize a message, sort a support request, pull the details out of an email, or rate a review. Quill’s ai module does it with one word, ask.
bring ai
permit net "api.anthropic.com"
permit env "ANTHROPIC_API_KEY"
message = "Hi! We'd like the brass band for our wedding on 12 June, about 150 guests."
summary = ask "Summarize this booking request in one sentence: {message}"
on ModelFailed -> "No summary right now."
say summary
Each line has a job:
bring aibrings Quill’saimodule, so a reader sees at the top that the program asks a model.permit net "api.anthropic.com"lets the program reach the model’s service.permit env "ANTHROPIC_API_KEY"lets it read the key, and says which provider it uses: Claude. The key stays in a setting on your computer, never in the code (see Permits).ask "..."sends the question and gives back the answer as text.
The examples in this chapter need a real model and a key, so the book shows them without running them.
A model can fail
A model lives somewhere else. It can be out of reach, too busy, or it can decline a question. All of these are the failure ModelFailed, and like every failure, the program has to say what happens then: carry on with a value, stop, or pass it on with try.
Answers you can trust: ask … as
Text is fine for a person to read, but a program needs answers it can use. Add as and a kind, and the model must answer in that shape:
bring ai
permit net "api.anthropic.com"
permit env "ANTHROPIC_API_KEY"
type Request
act: text
guests: number where 1..500
date: text
email = "Hi! We'd like the brass band for our wedding on 12 June, about 150 guests."
request = ask "Pull out the act, the number of guests and the date: {email}" as Request
on ModelFailed -> Request act = "unknown", guests = 1, date = "unknown"
on Invalid -> Request act = "check by hand", guests = 1, date = "unknown"
say "{request.act} for {request.guests} guests on {request.date}"
as Request does two things:
- Quill sends the shape of
Requestto the model, so it answers with exactly those fields. - Quill reads the answer the same way
asreads any JSON: every field must be there, the right kind, and inside itswhererange.
A model can still get something wrong, like 900 guests for a hall of 500. Then ask fails with Invalid, and the program never holds a Request that breaks its rules. Any kind works after as: as number, as true/false, as list of text, or a type.
Choosing the provider
The key a program permits chooses who answers:
| Permit | Asks | Settings |
|---|---|---|
permit env "ANTHROPIC_API_KEY" | Claude, at api.anthropic.com | The model is claude-opus-5-5 unless QUILL_AI_MODEL names another. |
permit env "OPENAI_API_KEY" | any service that speaks the OpenAI chat format | QUILL_AI_MODEL names the model. OPENAI_BASE_URL points at the service, like http://localhost:11434/v1 for a model running on your own computer. |
The host still needs its permit net: "api.anthropic.com", "api.openai.com", or "localhost" for a model on your computer.
When Claude’s safeguards decline a question, Quill asks Anthropic’s recommended model to answer instead, so a cautious answer doesn’t stop your program. If no model answers, it is ModelFailed.
Putting a model to work
A model answers well when the question says exactly what you want and gives it the facts. Put the facts in with {}, and keep the kind small:
bring ai
permit net "api.anthropic.com"
permit env "ANTHROPIC_API_KEY"
type Mood
happy: true/false
reason: text
fn mood_of(review: text) -> Mood
fails ModelFailed, Invalid
return try ask "Is this customer happy? Give a short reason. Review: {review}" as Mood
reviews = list "The band was wonderful!", "They arrived two hours late."
var mood = Mood happy = false, reason = "not asked yet"
for review in reviews
set mood = mood_of(review)
on ModelFailed -> Mood happy = false, reason = "no answer"
on Invalid -> Mood happy = false, reason = "unclear"
say "{mood.happy}: {mood.reason}"
try passes a failure up to the function’s caller, so each review decides for itself what an unanswered question means.
Common mistakes
A program has to bring the module:
permit env "ANTHROPIC_API_KEY"
answer = ask "Hello"
on ModelFailed -> "no answer"
Problem on line 3 of example.quill:
answer = ask "Hello"
^^^
ask is part of Quill's ai module, which this program doesn't bring.
Add bring ai at the top, so a reader sees that the program asks a model.
Did you mean: bring ai
It needs a permit for a key, so a reader knows which provider it uses:
bring ai
answer = ask "Hello"
on ModelFailed -> "no answer"
Problem on line 3 of example.quill:
answer = ask "Hello"
^^^
ask needs a key for a model, and the program permits none.
Add permit env "ANTHROPIC_API_KEY" to ask Claude, or permit env "OPENAI_API_KEY" for an OpenAI-compatible service. The key itself stays in the setting, out of the code.
Did you mean: permit env "ANTHROPIC_API_KEY"
And a permit to reach the service:
bring ai
permit env "ANTHROPIC_API_KEY"
answer = ask "Hello"
on ModelFailed -> "no answer"
Problem on line 4 of example.quill:
answer = ask "Hello"
^^^
ask sends the question to Claude at api.anthropic.com, and the program has no permit to reach it.
Add a permit for the host at the top of the program.
Did you mean: permit net "api.anthropic.com"
ask can fail, so the failure has to be handled:
bring ai
permit net "api.anthropic.com"
permit env "ANTHROPIC_API_KEY"
say ask "Hello"
Problem on line 5 of example.quill:
say ask "Hello"
^^^
Asking a model can fail with ModelFailed: the model may be out of reach, or say no.
Handle each with on under this line, or write try in front, inside a function that fails the same way.
Try it
- Set
ANTHROPIC_API_KEYon your computer, and ask a model to write one sentence about Quill. - Write
type Citywithname: textandcountry: text, and ask foras list of Citywith “Name three cities in Cameroon.” - Add a
whererange to a number field, and ask a question that tempts the model to break it. Watchon Invalidrun. - Run the same program against a model on your own computer, with
permit env "OPENAI_API_KEY",OPENAI_BASE_URLandQUILL_AI_MODEL.
Building a Web Service
A backend answers requests: a phone app asks for a booking, a website sends a new order, another service checks a price. Quill’s web module turns a program into a web service with two words: route and respond.
bring web
route GET "/hello/{name}"
respond 200, "Hello, {name}!"
Run it with quill run hello.quill, and Quill answers:
Serving 1 routes on http://localhost:8080. Press Ctrl+C to stop.
Open http://localhost:8080/hello/Amina in a browser, and the page says Hello, Amina!.
bring webbrings Quill’swebmodule.route GET "/hello/{name}"says what to do when a request asks for a page withGETat a path like/hello/Amina.{name}matches any part of the path, and inside the route it is a value: here, the text"Amina".respond 200, "Hello, {name}!"answers. The number is the status:200means it worked. Text is sent as it is.
The examples in this chapter start a server that keeps running, so the book shows them without running them.
Respond
Every route ends each path with respond STATUS, VALUE, the same way a function ends with return. The statuses you’ll use most:
| Status | Means |
|---|---|
200 | It worked. |
201 | Something new was made. |
400 | The request was wrong, like a booking without a venue. |
404 | There’s nothing there. |
Text is sent as plain text. Any other value, like a record or a list, is sent as JSON:
bring web
type Act
name: text
fee: USD
route GET "/acts"
brass = Act name = "Bamenda Brass", fee = 150000 USD
sisters = Act name = "Nchang Sisters", fee = 80000 USD
respond 200, list brass, sisters
GET /acts answers [{"name":"Bamenda Brass","fee":150000},{"name":"Nchang Sisters","fee":80000}].
The request
Inside a route, request holds what was asked: request.method (like "POST"), request.path, request.query (what comes after ? in the address), and request.body, the text that was sent.
A body is usually JSON. Read it as a type with as, and every rule is checked before your code uses it:
bring web
type Booking
venue: text
guests: number where 1..500
fee: USD
route POST "/bookings"
booking = request.body as Booking
on Invalid -> respond 400, "That isn't a booking."
respond 201, booking
A request with 9000 guests gets 400 That isn't a booking., and the route never holds a Booking that breaks its rules. on Invalid -> respond ... answers and ends the route there, the same way on ends a function.
When something goes wrong
Quill answers some requests by itself:
- A path no route has gets
404. - A path a route has, with another method, gets
405. - A route that stops with a problem, like dividing by zero, gets
500, and the problem is shown where the server was started, with its line. The server keeps answering other requests.
Putting it together
Routes can use everything else in Quill: functions, types, money, files, other services, and AI models. Here a booking desk works out the deposit:
bring web
type Booking
venue: text
guests: number where 1..500
fee: USD
fn deposit(booking: Booking) -> USD
return round 20% of booking.fee to cents
say "The booking desk is open."
route POST "/quote"
booking = request.body as Booking
on Invalid -> respond 400, "Send a venue, guests from 1 to 500, and a fee."
respond 200, "The deposit is {deposit(booking)}."
The lines outside routes run once, when the program starts. Then Quill serves the routes.
The server listens on port 8080. Set the setting PORT to use another one, like PORT=3000 quill run desk.quill.
Growing your service
A first service fits in one file. As it grows, add one idea at a time, in this order:
- Rules in functions. Work out prices, checks and decisions in plain
fns, and prove them with--- example:lines (Testing). A route then only reads the request, calls a function, and responds. - Split into files. Move the types and functions into their own files and bring them in. Keep
bring weband the routes in the file you run. - Files and settings. Read a config file or a secret with permits. The permits go in the file you run, never in a brought file.
- Other services. Call a payment service or a rates API with
bring http. - AI. Ask a model with
bring ai, and read its answer as a type withask ... as Review, the same way a request body is read withas.
Quill doesn’t have a database yet; bring db is coming. Until then, a service keeps its data only while it runs, or in files it writes with write_file.
Common mistakes
A program has to bring the module:
route GET "/hello"
respond 200, "Hello"
Problem on line 1 of example.quill:
route GET "/hello"
^^^^^^^^^^^^
route is part of Quill's web module, which this program doesn't bring.
Add bring web at the top, so a reader sees that the program serves requests.
Did you mean: bring web
A route has to respond on every path:
bring web
route GET "/hello"
say "Someone said hello."
Problem on line 3 of example.quill:
route GET "/hello"
^^^^^^^^^^^^
GET /hello has to respond on every path.
End each path with respond, like respond 200, "OK".
Did you mean: respond 200, "OK"
A route answers with respond, not return:
bring web
route GET "/hello"
return "Hello"
Problem on line 4 of example.quill:
return "Hello"
^^^^^^
A route answers with respond, not return.
respond gives the status and what to send, like respond 200, booking.
Did you mean: respond 200, "OK"
A route’s method is written in capitals:
bring web
route post "/bookings"
respond 201, "made"
Problem on line 3 of example.quill:
route post "/bookings"
^^^^
A route's method is written in capitals, like POST.
Did you mean: POST
Try it
- Write a route
GET "/hello/{name}"and open it in your browser with your own name. - Add
route GET "/time"that responds with any text you like, and a request to a path you didn’t write. Look at the404. - Write a
type Studentwithscore: number where 0..100, and aPOST "/students"route that reads the bodyas Student. Send a score of120with a tool likecurl -X POST localhost:8080/students -d '{"name": "Amina", "score": 120}'. - Respond with a list of records, and see the JSON your browser shows.
Testing Your Code
A test is a line of code that checks that your program does what you think it does. In Quill, tests are part of the language and live right next to the code they test.
check
check compares two values. If the comparison is true, nothing happens. If it’s false, the program stops and tells you what each side was:
price = 12.50
qty = 3
total = price * qty
check total == 37.50
check total > 0
say "All good"
All good
When a check fails:
price = 12.50
qty = 3
total = price + qty -- oops: should be *
check total == 37.50
Problem on line 5 of example.quill:
check total == 37.50
^^^^^^^^^^^^^^^^^^^^
This check failed.
One side is 15.50 and the other is 37.50.
A check passes only when both sides are the same value, so change one of them to match.
check works with any comparison: ==, !=, <, <=, > and >=.
Examples in documentation
A doc comment starts with --- and describes the code below it. A doc comment that starts with --- example: is also a test:
vat = 1.05
--- Prices include VAT, so 100 becomes 105.
--- example: 100 * vat == 105
price = 250 * vat
say price
262.50
Because the example is run like a check, the documentation can’t quietly become wrong: if someone changes vat, the example fails.
An example runs where it’s written, so it can only use values created above it, like any other line. Inside a function, an example runs each time that function is called.
Examples for a function
The best place to document a function is right above it. Examples written directly above a fn run just after that function is written, so they can call it:
--- Adds 5% VAT to a price.
--- example: with_vat(100 USD) == 105 USD
--- example: with_vat(0 USD) == 0 USD
fn with_vat(price: USD) -> USD
return price * 1.05
say with_vat(20 USD)
21 USD
Anyone reading with_vat sees what it does and what it gives back before reading a line of its code, and quill test proves the examples are still true.
“Directly above” means nothing but comments and blank lines between the examples and the fn. If another line comes between them, the example runs where it is, before the function exists, and Quill tells you to move it:
--- example: with_vat(100 USD) == 105 USD
say "Prices"
fn with_vat(price: USD) -> USD
return price * 1.05
Problem on line 1 of example.quill:
--- example: with_vat(100 USD) == 105 USD
^^^^^^^^
with_vat is used here, but the function isn't written until line 3.
An example runs after the fn right below it. Move this example directly above fn with_vat, with no other lines between them.
Running every test: quill test
quill test looks through the current folder and its subfolders, runs every .quill file that contains a check or --- example:, and reports the results:
$ quill test
shop/hello.quill: 1 check passed
shop/prices.quill: 4 checks passed
5 checks passed.
To run just one file, use quill test shop/prices.quill. The file runs even if it contains no checks; in that case, the report says 0 checks passed. A missing file or a path without .quill reports a problem.
Testing one folder
A bigger project keeps its parts in folders. To test one part, name its folder: quill test shop runs the files in shop and its subfolders that contain a check or --- example:, and leaves the rest of the project alone.
Here is a small shop. The prices live in one file, with an example above each function:
-- shop/prices.quill: what things cost.
--- example: with_vat(100 USD) == 105 USD
fn with_vat(price: USD) -> USD
return price * 1.05
--- example: bulk_price(10 USD, 12) == 108 USD
--- example: bulk_price(10 USD, 3) == 30 USD
fn bulk_price(each: USD, count: number) -> USD
return when
count >= 10 -> each * count * 0.90
else -> each * count
The cart, in the same folder, brings the prices in and checks its own total:
bring prices
total = with_vat(bulk_price(10 USD, 12))
check total == 113.40 USD
say "Total: {total}"
Total: 113.40 USD
Next to shop, a folder called notes holds a file you haven’t finished, with a check that fails:
check 1 + 1 == 3
quill test on its own tests everything, so the draft fails the run. Name the folder instead, and only the shop is tested:
$ quill test shop
shop/cart.quill: 1 check passed
shop/prices.quill: 3 checks passed
4 checks passed.
Each file reports its own checks. cart.quill counts only its check; the three examples in prices.quill run when prices.quill itself is tested, not when cart.quill brings it in.
The count is the number of successful checks actually executed. Checks inside called functions count, and a check inside a loop counts each time it passes. Skipped branches and functions that are never called do not count. Doc examples count the same way. A failed check stops that file; earlier passes still count in the summary, and project testing continues with the other files.
Run it often, especially before sharing your code.
Try it
- Write a program that calculates the area of a 4 by 2.5 rectangle, with a
checkthat the answer is 10. - Make the check fail on purpose and read the message.
- Add a
--- example:line above a value, then runquill test. - Run only that file with
quill test your-file.quill. - Put the file in a folder called
shop, and runquill test shop.
Reading Quill’s Errors
Everyone makes mistakes when programming, beginners and experts alike. What matters is how quickly you can understand and fix them. Quill’s errors are designed to teach. In this chapter you’ll learn how to read them and which tools help.
The shape of an error
Every Quill error has the same parts:
greeting = "Hello"
say greting
Problem on line 2 of shop.quill:
say greting
^^^^^^^
There's no value called greting.
Values must be created with greting = ... before they can be used.
Did you mean: say greeting
- Where: the file and line, with
^^^under the exact spot. - What went wrong, in the first sentence.
- Why it’s a problem, in the next sentence.
- Did you mean: what Quill thinks you meant, when it can tell. Often you can copy it straight into your program.
Quill checks before it runs
Before running anything, Quill reads your whole program and checks it. If it finds mistakes, it shows all of them and runs nothing, so a program never stops halfway through because of a typo on line 50:
say total
name = "Amina"
name = "Omar"
Problem on line 1 of example.quill:
say total
^^^^^
There's no value called total.
Values must be created with total = ... before they can be used.
Problem on line 3 of example.quill:
name = "Omar"
^^^^
name already exists. It was created on line 2.
Values in Quill can't change once they're created, so give this one a different name.
Quill found 2 problems.
A few mistakes can only be found while running, like dividing by a value that turns out to be zero, or a failing check. Then the program stops at that line, with the same kind of explanation.
quill check
quill check looks for mistakes without running anything. It’s useful when a program does something you don’t want to happen yet, like sending emails:
$ quill check shop.quill
No problems found in shop.quill.
quill explain
When you’re not sure how Quill understood a line, ask it. quill explain takes the file and a line number:
$ quill explain shop.quill:3
Line 3: total = price * qty
total a new value (can't change)
= store the value on the right in total
price * qty a calculation, Number
price Number, created on line 1
qty Number, created on line 2
If the line has a problem, quill explain shows it too.
Common mistakes
| Mistake | What Quill says | Fix |
|---|---|---|
say "Hello | This text starts with a quote but never ends. | add the closing " |
say greting | There’s no value called greting. | fix the spelling |
total = price + "5" | You’re adding Number (price) to Text (“5”). | remove the quotes: 5 |
set name = "Omar" | name can’t change, because it isn’t marked var. | create it with var |
sya "hi" | Quill doesn’t know what to do with sya here. | say "hi" |
x = 1 < y < 10 | Quill compares two values at a time. | 1 < y and y < 10 |
If Quill ever crashes
Quill should never crash, whatever you type. If it does, that’s a bug in Quill, not in your program. Please report it with the program that caused it.
Try it
- Make three different mistakes in one program and run
quill check. Does Quill find all three? - Run
quill explainon a line with a calculation. - Find an error message you think could be clearer, and suggest a better one on GitHub.
What’s Coming Next
Quill is young, and these features are designed but not built yet. The examples on this page show how they’re planned to work. They don’t run yet.
Functions, money, maybe, failures, contracts with ranges like where 1..500, and asking for data with get, sum, count, average, and sorted by, and text tools such as trim, contains, split, and join are in the language now. for is still there when each record needs its own step. A when can give a value, and on can give a value and carry on. A parameter’s kind is number, text, true/false, a currency such as USD, a type such as Booking, maybe one of those, or list of one of those.
And more
Types with rules on their fields are here, and so are bringing in files, permits, JSON, web requests, AI models and web services. Still ahead: a built-in module for databases (bring db), and contracts that Quill proves before the program runs. See the README for the full design.
Each of these will get its own chapter in this book as it’s built. Want to help build one? Read the contributor guide.
Appendix A: Quick Reference
All of Quill on one page.
-- A comment
--- A doc comment
--- example: 1 + 1 == 2
name = "Amina" -- a value that can't change
price = 12.50 -- numbers are exact decimals
in_stock = true -- true or false
var total = 0 -- a value that can change
set total = total + price * 2 -- change it
say "Hi {name}, total {total}" -- {} puts a value inside text
say """She said "hi".""" -- three quotes: no backslash needed
say uppercase trim " amina " -- tidy text: trim, uppercase, lowercase
say length of name -- how many letters
say name starts with "Am" -- also contains, ends with
say replace "a" with "4" in name
say join split "a,b,c" by "," with " / "
when
total > 100 -> say "Free delivery"
total > 20 -> say "Delivery: 5"
else -> say "Delivery: 10"
check total == 25.00 -- a test
size = when -- a when that gives a value
total > 20 -> "big"
else -> "small"
say size
for i in 1..3 -- 1, then 2, then 3
say i
hobbies = list "coding", "reading"
say hobbies at 1
address = record city = "Anytown"
say address.city
fn double(n: number) -> number
return n * 2
say double(21)
fn half(n: number) -> number
require n >= 0 -- what the caller must pass
ensure result <= n -- what half promises to give back
return n / 2
say half(21)
fn book(guests: number where 1..500) -> text -- 1 through 500
return "{guests} guests"
say book(120)
type Act -- every Act has these fields
name: text
fee: USD where 1 USD..500000 USD
act = Act name = "Brass", fee = 150000 USD
roster = list act
say sum fee of roster
say get fee of roster
say get name of roster where fee > 100000 USD -- only some records
say count of roster -- how many
say average fee of roster -- the typical amount
say get name of roster sorted by highest fee -- in order, highest first
say split 100 USD into 3 -- no cent lost
say round 10.125 USD to cents -- half to even
say 20% of 500 USD
say convert 100 USD to EUR at 0.92 -- the rate is written
up = record place = "Up Station", acts = list "Bamenda Brass"
old = record place = "Old Town", acts = list "Market Drums"
desk = record bookings = list up, old
say get place of desk.bookings
Hi Amina, total 25.00
She said "hi".
AMINA
5
true
Amin4
a / b / c
Delivery: 5
big
1
2
3
coding
Anytown
42
10.5
120 guests
150000 USD
150000 USD
Brass
1
150000 USD
Brass
33.34 USD, 33.33 USD, 33.33 USD
10.12 USD
100 USD
92 EUR
Up Station, Old Town
Kinds of values
| Kind | Examples |
|---|---|
| Text | "Hello", "Hi {name}", """He said "hi".""" |
| Number | 42, 0.10, -3.5 |
| true/false | true, false, total > 10 |
| list | list "coding", "reading", hobbies at 1 |
| record | record city = "Anytown", address.city |
Operators, tightest first
| Operators | Meaning |
|---|---|
( ) | brackets |
-x | negative |
* / % | multiply, divide, remainder |
+ - | add, subtract (numbers only) |
== != < <= > >= | compare |
not | not |
and | both |
or | either |
Escapes inside text
\n new line · \t tab · \" quote · \\ backslash · \{ \} braces
Blocks (when, for, fn, record) are indented by exactly 2 spaces.
Reserved words
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
fn double(n: number) -> number names a calculation. return n * 2 is the value the call becomes, and double(21) calls it. Leave off -> when the function only does work, and call it on its own line. Each parameter has its own kind (text, number, true/false, a currency such as USD, maybe one of those, or list of one of those). The kind after -> is the only kind return may give back.
100 USD + 50 USD is money. Two currencies cannot be added. -> maybe number might be found or missing, and when has to handle both. fails Declined names a failure, fail Declined leaves with it, and the caller writes on Declined or try. on Declined -> say "..." runs and stops; on Declined -> "declined" uses that value and carries on. require says what has to be true when a function is called, and ensure says what is true when it gives back; result is the value it gives back. guests: number where 1..500 gives a parameter its range, ends included. round total to cents rounds half to even (round up/round down choose a side); split 100 USD into 3 and split fee by list 70, 30 never lose a cent; 20% of fee takes a percent; convert 100 USD to EUR at 0.92 changes currency at the rate you write. bring billing uses the functions and types in billing.quill, next to the file. permit read "config" (also write, net, env) says what the program may touch; read_file, write_file, file_exists and env need it. body as Booking reads JSON as a type and fails with Invalid when it doesn’t fit; json(value) writes JSON. bring http adds fetch(url), post(url, body) and send(method, url, body, headers); each gives back status and body, needs permit net, and fails with NetworkDown. bring ai adds ask "question" (text) and ask "question" as Review (checked against the type); it fails with ModelFailed (and Invalid with as), and permit env "ANTHROPIC_API_KEY" or "OPENAI_API_KEY" chooses the provider. bring web adds route GET "/path/{name}" and respond 200, value (text as is, anything else as JSON); quill run serves on PORT (8080). type Act names the fields every Act has; Act name = ..., fee = ... makes one, with every field and its rules checked.
trim, uppercase, and lowercase tidy text. length of name counts its letters. name contains "Dr", starts with, and ends with are true or false, and hobbies contains "coding" looks in a list. replace "-" with " " in code changes every -. split line by "," cuts text into a list, and join names with ", " puts it back together. None of these words is reserved.
at picks one list item: hobbies at 1 reads the first, and set hobbies at 2 = "writing" changes that one item. of is required only for an empty list: list of text, list of number, or list of true/false. A list that already has items leaves of out.
sum fee of roster adds the fee of each record. get fee of roster takes that field as a list. get place of desk.bookings reads a field of a list on a record, and get gpa of school.acceptances reads a field of a nested record. Give that record a name before the dot: first = desk.bookings at 1, then first.place. get bookings with acts in desk is the longer form: it names the list and the field embedded in it. roster where fee > 100000 USD keeps the records whose condition is true; inside it, a field is read by its name, and it works after get ... of and sum ... of. count of roster is how many records there are. average fee of roster is the sum divided by how many, with money rounded to cents. roster sorted by fee puts the lowest first, and sorted by highest fee the highest. count, average, and sorted are words only in those places, so they stay free as names. A missing amount is left out. for stays when each record needs its own step.
Commands
| Command | What it does |
|---|---|
quill run FILE.quill | run a program |
quill check FILE.quill | find mistakes without running |
quill test | run every test in this folder |
quill test shop | run the tests in the folder shop |
quill explain FILE.quill:N | show how Quill read line N |
quill new NAME | start a project; a menu offers the templates |
quill new NAME TEMPLATE | start one from a template: app, learn, starter, backend, integration, webhook, ai, ai-backend, finance, banking, automation, library |
quill tokens FILE.quill | show the pieces Quill read |
quill update | choose a version of Quill to install (also quill upgrade) |
quill update VERSION | install that version, or latest |
quill help | show help |
quill version | show the version |
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)
Appendix C: Version History
What changed in each version of Quill. The newest version is at the top.
Unreleased
Changes that are finished but not yet in a release.
- This book is published at https://ngwangshalom.github.io/Quill/, tested and updated after every change to it. The
QUILL.mdin each new project links there.
v0.4.8 (3 November 2026)
- In VS Code and Cursor, Quill’s colors and the feather work in Restricted Mode, in a folder the editor doesn’t trust yet; before, the editor turned Quill off there. Underlining mistakes starts once the folder is trusted, because it runs the
quillprogram. Quill code in a```quillblock inside a Markdown file, such as a README, has Quill’s colors too. quill new shop appmakes a whole app for a small events company: a backend with bookings read from a file and checked against a type, prices with VAT and payments that add up to the cent, an AI assistant whose answer is checked too, and tests. Without an AI key, everything works except the assistant, which answers 503. It sits at the top of the menu, under “Everything”. Every line of the menu now fits an 80-column terminal.- Quill no longer prints “Quill added colors and the feather icon…” after a command. It still sets up Cursor and VS Code the same way, quietly; reload the editor window after installing or updating Quill to see the colors.
- On Windows, the files
quill newmakes have the same line endings as on macOS and Linux.
v0.4.5 (3 November 2026)
- Templates for
quill new.quill new shopasks what kind of program it is, with the arrow keys, andquill new shop backendskips the menu. Eleven templates, in five groups:learnandstarterfor learning;backend,integrationandwebhookfor backend work;aiandai-backendfor AI;financeandbankingfor business and finance;automationandlibraryfor automation and general programs. Each one runs and passes its tests straight away, and comes with aREADME.mdandQUILL.md, a one-page tour of the language. With no terminal,quill new shopmakesstarter. A misspelled template names the one you meant. trim (parts at 1), with a space before the bracket, is the text tool.trim(x), right against the bracket, calls a function calledtrim; when there is none, Quill explains that the tool takes no brackets.- Working with text.
trim nametakes away the spaces at the ends, anduppercaseandlowercasechange the letters.length of namecounts the letters, withéas one.email contains "@",code starts with "INV", andfile ends with ".csv"are true or false, and work in awhenand awhere;hobbies contains "coding"looks in a list.replace "-" with " " in codechanges every-.split line by ","cuts text into a list, andjoin names with ", "puts a list back into one text. An empty separator is a mistake. None of these words is reserved, sovar length = 0andfn join(...)still work. They are ink (#949CF5on a dark editor,#3A44A0on a light one). - The examples in this book have the editor’s colors: the light ones on the Quill, Light and Rust themes, and the dark ones on Coal, Navy and Ayu. Before, the book’s code was one color, and the code after the first Quill example on a page wasn’t colored at all. In Cursor and VS Code, the second number of a range such as
0..100is colored like the first.
v0.4.2 (3 November 2026)
- How many, the average, and in order.
count of studentssays how many records there are, andcount of students where score >= 50counts the ones that match, with no counter in a loop.average score of studentsis the sum divided by how many; a missing amount is left out, money is rounded to cents, half to even, and an average of nothing stops with a problem that says to check the count first.students sorted by scoreputs the lowest first,sorted by highest scorethe highest, and text goes in alphabetical order; records with the same value keep their order.sorted byworks afterwhereand infor.count,average, andsortedare words only beforeof, a field andof, andby, sovar count = 0andfn average(...)still work. They are sea, likegetandsum. In Cursor and VS Code,get,sum, andwithhad lost their color, because the AI model’sasktook their place in the color list; in Emacs they showed theaskcolor. Each kind has its own color again. - Quill files end in
.quill..qlwas already GitHub’s extension for CodeQL, so GitHub counted Quill files as CodeQL.quill newmakeshello.quill,bring billingreadsbilling.quill, and the editors color.quillfiles. A.qlfile still runs, with a note asking you to rename it (“Note:” in nib orange in a terminal;NO_COLOR=1turns color off), andbringstill findsbilling.ql; a later version will stop reading.ql. quill test shoptests one folder: it runs the files inshopand its subfolders that contain acheckor--- example:. Before, naming a folder was refused as a path without.quill.tryon a line by itself, liketry write_file(...), explains thattrygoes where a call’s answer is used, and showson CantWrite -> fail CantWritefor passing a failure on from a call on its own line. Before, Quill suggestedsay try ..., which can’t work for a call that gives nothing back.- Web services.
bring web, thenroute GET "/hello/{name}"andrespond 200, "Hello, {name}!", turn a program into a web service.{name}parts of the path are values,requestholds the method, path, query and body, and a record is sent as JSON.quill runserves onPORT(8080). Unknown paths get 404, the wrong method 405, and a route that stops 500, with the problem shown and the server still running.routeandrespondare mist, likebring. - Asking an AI model.
bring ai, thenask "Summarize: {text}"gives back the model’s answer, andask "..." as Reviewgives back aReview: the model is sent the type’s shape, and the answer is checked against every field andwhererange.ModelFailedandInvalidare handled like any failure.permit env "ANTHROPIC_API_KEY"chooses Claude (claude-opus-5-5unlessQUILL_AI_MODELsays otherwise);permit env "OPENAI_API_KEY"chooses any OpenAI-compatible service, including one on your own computer.askis spark. - Web requests.
bring httpaddsfetch(url),post(url, body)andsend(method, url, body, headers). Each needspermit netfor the host, gives back an answer withstatusandbody, and fails withNetworkDownwhen the service can’t be reached. A404or500is still an answer, so the program decides. - JSON.
body as Bookingreads JSON text as any kind, checking every field, its kind, and itswhererange; money is read exactly, even from text like"500.00". Text that doesn’t fit fails withInvalid, handled withon Invalidortry.json(value)writes any value as JSON.asis steel, liketype. - Permits, files and settings.
permit read "config",permit write "reports",permit env "STRIPE_KEY"andpermit net "api.stripe.com"say what a program may touch; anything else is refused.read_file,write_file,file_existsandenvare Quill’s own functions, and their failures (FileMissing,CantWrite) are handled withonortry. A path or name written in the call is checked before running, and..can’t leave a permitted folder. A brought file can’t give itself permits.permitis rust. bringputs a program together from files.bring billingreads billing.quill next to the file, andbring "shared/money.quill"reads a file by its path. Its functions and types are used by name; its own tests run when that file is tested. A problem in a brought file names that file. Misspelled names suggest the file you meant; files that bring each other in a circle, a brought file that does something, and a name written in two files are explained.bringis mist blue, the color saved for modules.- Money that never loses a cent.
round total to centsrounds half to even, as banks do;round upandround downchoose a direction, andround x to 2 placesworks on numbers.split 100 USD into 3gives33.34 USD, 33.33 USD, 33.33 USD, andsplit fee by list 70, 30shares by weights; the parts always add up.20% of feetakes a percent.convert 100 USD to EUR at 0.92changes currency at a rate you write, and adding two currencies now suggests it. Money is shown in whole units or with every cent (90 USD,7.50 USD), and Quill knows currencies without cents, like XAF and JPY, and with three digits, like KWD.round,splitandconvertare mint. - Types.
type Booking, then one field per line, likeguests: number where 1..500, names what every booking has.Booking venue = "Town Hall", guests = 120, fee = 500 USDmakes one; every field must be there and be the right kind, and each field’s range is checked as it is made. A type works as a kind (b: Booking,-> Booking,list of Booking), and a function that takes one checks the type’s ranges. A misspelled field is reported once.typeis steel, likewhere.
v0.4.0 (20 October 2026)
- Contracts.
require amount > 0 USDunderfnsays what has to be true when the function is called.ensure result >= 0 USDsays what is true each time it gives back, andresultis the value it gives back. A broken one stops the program, names the values it read, and says whose mistake it was; a brokenrequirenames the line of the call.require,ensure, andfailsgo at the top of the function in any order.quill explaindescribes contract lines.require,ensure, andresultinside anensureare brass (#C8A06Aon a dark editor,#7A5A20on a light one). - Ranges with
where.fn book(guests: number where 1..500)takes 1 through 500 guests, both ends included. Money works too:amount: USD where 1 USD..10000 USD. A written value outside the range, likebook(600), is caught before the program runs; any other value stops the call and names the value and the line of the call. Both ends are written values of the parameter’s kind, smallest first.quill explaindescribes the range.whereis steel (#6AA8C8on a dark editor,#1A5270on a light one), the color saved for rules on types. - Filtering with
where.students where score >= 50keeps the records whose condition is true, like Laravel’swhere. Inside the condition a field is read by its name. It works insidegetandsum(sum fee of roster where paid == false), afterfor ... in, and again after itself (where score >= 50 where paid). No match gives an empty list. A misspelled field names the record’s fields. - Asking for data.
sum fee of rosteradds the fee of each record.get fee of rostertakes that field as a list.get name, age, fee of rosterkeeps those fields together on each record. The same line reads one record:get name, fees of schoolis one record with those fields.sum fees of schoolis that one amount. A dot afterofreads a field further in:get place of desk.bookings,get gpa of school.acceptances. Name the record before the dot:first = desk.bookings at 1, thenfirst.place.get bookings with acts in rosteris the longer form, for a list and the field embedded in it. A missing amount is left out of a single field.forremains when each record needs its own step.get,sum, andwithare sea (#6EC1D6on a dark editor,#15556Aon a light one). whengives a value.delivery = when, then indented cases liketotal > 100 -> 0, ending withelse -> 10. The first true case gives the value. It works after=,set,say, andreturn.elseis required, and every case gives the same kind of value.quill explaindescribes each case.oncan carry on.on Declined -> "declined"uses that value in place of the call’s answer, and the program continues.on Declined -> say "..."still runs and then stops. Eachonline chooses for itself.- Examples above a function test it.
--- example: with_vat(100 USD) == 105 USDdirectly abovefn with_vatruns once the function is written, so a function’s documentation and its tests sit together above it. A failed example says “An example”, not “A example”. quill test FILE.qlruns one file. Test reports count executed checks, including functions, loops, and passes before a failure.- Remainder overflow reports a Quill arithmetic problem instead of crashing.
- The README marks the type checker and contracts as implemented.
- The README writes money as
USD, the way Quill does, instead ofMoney<USD>. Contracts stayrequireandensure. Its error andquill explainexamples show Quill’s real output. - Money examples use dollars.
100 USD + 50 USDis150 USD. - Quill’s public description is this: intent is clear, failures are explicit, and correctness is part of the code. It is for backend engineering, financial software, automation, reliable business applications, and AI and machine learning. Asking a model and training one are part of that. Operating systems, device drivers, and 3D games stay with Rust and C++.
v0.3.1 (14 October 2026)
- Indentation is exact. A block moves in by 2 spaces. A tab, or any other width, is a mistake.
- A range is two dots.
18...25is a mistake, and Quill tells you to write18..25.->belongs inside a when, sosaytakes the text directly. - Money.
100 USD + 50 USDis150 USD. Two currencies cannot be added. A function writes the currency as the kind:price: USD. - Maybe.
-> maybe numbermeans the answer might be missing.return found 2.50orreturn missing.whenopens it withfoundandmissing, and both are required.maybe,found, andmissingare peach. - Failures.
fails Declinednames how a function can fail.fail Declinedleaves with that failure. The caller writeson Declinedunder the call, ortryinside a function that fails the same way.fails,fail,on, andtryare red. - Functions.
fn double(n: number) -> numbernames a calculation, andreturngives the value back. A call isdouble(21). A function with no->does work and gives nothing back.fnandreturnare pink. list,record,of, andateach have their own editor color: clay, sage, gold, and violet.- Lists and records.
list "coding", "reading"keeps values in order, andhobbies at 1is the first.recordkeeps named fields, andaddress.cityreads one.for hobby in hobbieswalks a list.atpicks one item, andofis required on an empty list (list of text). The book says where each word is used, and search findsofandat. forloops.for i in 1..3repeats the lines under it for 1, then 2, then 3. Both ends count. The name exists only inside the loop, and the loop moves it to the next number itself. Search in the book findsfor,when,say, and the other Quill words that ordinary search skips.- The book has a Quill theme: warm paper, ink, and a brass mark for the chapter you are reading. Pick it from the paintbrush menu. When your system is set to light, the book opens in Quill.
- The book uses the Quill feather as its icon, in the browser tab and beside the title.
- Quill has its own editor colors, one for each kind of word (
say,when,trueandfalse,varandset,check, and the logic words), the feather on.qlfiles (pale on a dark editor, deep blue on a light one), and underlines for mistakes. Colors for types and the other planned kinds are reserved until those words exist.maybeand failures have their colors now. Cursor and VS Code get them the first time Quill runs. Vim, Emacs, and Sublime detect.qlfiles from theeditorsfolder.quill lspreports the same mistakes to any editor. SetQUILL_NO_EDITOR_SETUP=1to leave Cursor and VS Code alone.
v0.3.0 (13 October 2026)
quill updateshows every version of Quill in a menu: choose one with ↑ ↓ and Enter.quill update latestorquill update 0.2.0installs a version directly, andquill upgradeis another name for the same command.- Quill checks for a new version at most once a day, in the background, and says when one is out. It never updates by itself. Set
QUILL_NO_UPDATE_CHECK=1to turn the check off.
v0.2.0 (13 October 2026)
- Arithmetic with
+,-,*,/and%, brackets, and the usual order of operations. See Numbers and Calculations. - Exact decimal numbers:
0.1 + 0.2is exactly0.3. - Comparisons
==,!=,<,<=,>,>=, and conditions withand,orandnot. - Values inside text:
"Hi {name}". See Text. varandsetfor values that change. See Values That Change.whento choose what to do, with->andelse. See Making Decisions with when.- Indentation marks the cases of a
when. - The checker knows the kind of every value and catches mistakes like
price + "5"before running. checkaccepts any comparison, not only==.quill explaindescribes calculations,var,setandwhen.- New reserved words:
var,set,when,else,true,false,and,or,not.
v0.1.0 (13 October 2026)
The first release.
sayto show a value, andname = valueto name one.- Text in double quotes, with escapes and three-quote text; numbers written exactly as typed.
- Comments (
--) and doc comments (---). - Tests:
check a == band--- example:. - Errors in the README style: where, what, why, and “Did you mean”.
- The
quilltool:run,check,test,explain,new,tokens,help,version. - Installers for macOS, Linux and Windows.