Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 USD is 150 USD, and 0.1 + 0.2 is exactly 0.3. Dollars and euros cannot be added.
  • Ask for the data. sum fee of roster adds each fee. 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. count of roster, average fee of roster, and roster sorted by highest fee say how many, the typical fee, and the order. Asking for data shows each form. for stays when each record needs its own step.
  • A missing answer has to be handled. maybe says a value might not be there. found and missing are both required.
  • A failure has to be handled. fails Declined names it, fail Declined leaves with it, and the caller writes on Declined.
  • Functions state their rules. require amount > 0 USD says what a function needs, and ensure result >= 0 USD says what it promises. guests: number where 1..500 gives 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 check line 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

DateMilestone
13 October 2026Work on Quill begins. The first program, say "Hello, World!", runs the same day.
13 October 2026Quill is published on GitHub as an open-source project under the Apache 2.0 license.
13 October 2026v0.1.0, the first release, with installers for macOS, Linux and Windows.
13 October 2026v0.2.0 adds arithmetic with exact decimals, values inside text, var and set, and when.
13 October 2026The Quill book and the language specification are published, and every change to Quill is documented in them from now on.
13 October 2026v0.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 2026v0.3.1 adds lists, records, loops, functions, exact money, maybe, and failures.
20 October 2026v0.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 2026v0.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 2026v0.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 2026v0.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 billing finds billing.ql until 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.

TemplateStart here when you want to
appsee a whole app: a backend with bookings, prices with VAT, data from a file, an AI assistant, and tests, working together
learnlearn Quill by fixing small exercises until quill test passes
startertry the language in one file
backendbuild a service that web, desktop, and mobile apps call
integrationcall another service and read its JSON safely
webhookreceive notices from a payment provider or another service
aiask an AI model and check its answer
ai-backendput an AI model behind a service you can deploy
financework with invoices, VAT, and money that never loses a cent
bankingkeep accounts whose rules can never be broken
automationread a file, work something out, and save a report
librarywrite 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:

WhatColorWhere it comes from
say#F0A35E nib orangethe nib
when and else#7EAEF5 feather bluethe feather
true and false#5ECA8C greena yes on the page
var and set#C9A6F2 lilaca name you can change
check#F0A0B4 rosea test
and, or, not#5EC4C0 tealwords that join a question
for and in#C6D86A yellow-greena loop
get, sum, with, count, average, and sorted#6EC1D6 seaasking for data
trim, uppercase, lowercase, length, contains, starts with, ends with, join, replace#949CF5 inkworking with text
list#D4896A clayvalues in order
record#A8C5A0 sagenamed fields
of#E8C547 goldthe kind of an empty list
at#B07CFF violetone item, by its position
fn and return#E8A0D4 pinka function
maybe, found, missing#F0B090 peacha value that might be missing
fails, fail, on, try#E08A8A reda failure the caller must handle
require, ensure, and result in an ensure#C8A06A brassa function’s rules
where, type and as#6AA8C8 steelwhich values fit: a range, the records to keep, a kind of record
round, split, convert#8FD3B5 mintmoney that never loses a cent
bring, route, respond#A8C0D8 mistanother file, a Quill module, a web route
permit#C47848 rustwhat a program may touch
ask#E57BE0 sparka question for an AI model
Numbers#E7C98A sandcounted things
Text in quotes#F3E6D0 warm paperthe page
Comments#9A9186 faded inkink, 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

CommandWhat it does
quill run FILE.quillRun a program
quill check FILE.quillLook for mistakes without running anything
quill lspUnderline those mistakes in an editor
quill testRun the tests in every file in this folder
quill explain FILE.quill:NShow how Quill understood line N
quill new NAMEStart a new project from a template
quill updateChoose a version of Quill to install
quill helpShow all the commands

Try it

  1. Write a program that says your name, then your favourite food, on two lines.
  2. Add a comment above each line saying what it does.
  3. 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:

KindExamplesUsed for
Text"Amina", "Hello, World!"words and sentences
Number34, 12.50, -3counting and calculating
true/falsetrue, falseanswers 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

  1. Create values for a product: its name, its price, and whether it’s in stock. Show each one with say.
  2. Try to create the same name twice. Read the error.
  3. 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:

SymbolMeaningExampleResult
+add7 + 29
-subtract7 - 25
*multiply7 * 214
/divide7 / 23.5
%remainder after dividing7 % 21
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

  1. A café sells coffee for 3.50 and cake for 4.25. Calculate the cost of 2 coffees and 1 cake.
  2. Add 5% tax to your answer by multiplying by 1.05. How many digits does the answer have after the point? Why?
  3. Use % to find the remainder when 17 is divided by 5.
  4. 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:

WriteTo get
\na new line
\ta 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

  1. Create values for a city and its temperature, then say "It's 31 degrees in Dubai" using braces.
  2. Show a receipt line with the price of 3 items at 4.25 each, calculated inside the braces.
  3. Show the text {curly braces} with the braces visible.
  4. trim the text " hello ", then say it in uppercase.
  5. Make an email address and use a when to say whether it contains "@".
  6. Change every space in "New York City" to - with replace.

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:

  1. You can see every change. Search for set to find every place a value changes. A value without var never changes, so you never need to check.
  2. 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

  1. Make a var called score starting at 0. Add 10 three times, then say the score.
  2. Try to set a value that wasn’t created with var. Read the error and fix your program the way it suggests.
  3. Make a var holding 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:

SymbolQuestionExampleAnswer
==equal to?3 == 3true
!=not equal to?3 != 3false
<less than?2 < 3true
<=less than or equal to?3 <= 3true
>more than?2 > 3false
>=more than or equal to?2 >= 3false
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

WordAnswer is true whenExample
andboth sides are trueage >= 18 and has_ticket
orat least one side is trueis_member or total > 100
notthe value is falsenot 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 than when itself.
  • 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. total is 45, so the first case is false and the second is true. Quill runs the second case and skips the rest, even though else would also match.
  • else is 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. delivery can’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

  1. Make a value age and use when to say “child” under 13, “teenager” under 18, and “adult” otherwise. Change age and run it again.
  2. A shop gives 10% off orders over 200. Make a var price and use when to reduce it with set when it qualifies.
  3. Rewrite FizzBuzz for a different number. What happens if you move the FizzBuzz case to the bottom?
  4. Rewrite exercise 1 as stage = when ..., then say stage. Remove the else case 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.”

  • i is the number for this pass. The first pass it is 1, then 2, then 3.
  • 1..3 includes 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, i moves to the next number by itself. You don’t write set for 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

  1. Say the numbers from 1 through 5.
  2. Add those numbers into a var total, and say the total when the loop is finished. It should be 15.
  3. 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

  1. Make two records with a fee in USD, put them in a list, and say the sum.
  2. get the fees into their own list and say that list.
  3. Add a record whose fee is missing, and check that the sum left it out.
  4. Put two bookings on a desk and say get place of desk.bookings.
  5. Give each record a paid field. say sum fee of roster where paid == false to see what is still owed.
  6. say count of roster where paid == false to see how many still owe, and say average fee of roster for the typical fee.
  7. say get name of roster sorted by highest fee. Then sort by name and 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.
  • at picks one item. Lists count from 1, so hobbies at 1 is "coding" and hobbies at 3 is "traveling".
  • An empty list says its kind, because there is no item to learn it from: list of text, list of number, or list 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

  1. Make a list of three prices and say the second one.
  2. Walk that list with for and add the prices into a var total.
  3. Make a record for a person, with a name and an age, and say the name.
  4. split "red,green,blue" by ",", then join the pieces back with " / ".

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

  1. Write fn triple(n: number) -> number that returns n * 3, and say triple(5).
  2. Write fn hello(name: text) that says hello to that name, and call it with your name.
  3. Change triple so 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

  1. Add 20 USD and 5 USD, and say the result.
  2. Add 20 EUR and 5 USD, and read what Quill says.
  3. Split a 250 USD dinner between 3 friends. Check that the parts add up to 250 USD.
  4. A booking fee is 80000 XAF, and the agency keeps 15%. Say the agency’s part, and what the artist keeps.
  5. 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

  1. Call find_price("cake") and handle both cases. The cake is missing.
  2. Change the function so "cake" is found at 4.
  3. Remove the missing case 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

  1. Charge 0 USD and handle Declined.
  2. Add fails NetworkDown and a second on. Forget one of them and read what Quill says.
  3. Move the charge into a second function with try, and handle Declined at the call.
  4. Change on Declined -> say "Card declined" to on Declined -> "declined", and add a say line 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:

WordWho is wrong when it happensWhat happens
requirethe caller passed a value the function doesn’t acceptthe program stops
wherethe caller passed a value outside the parameter’s rangeQuill stops before running, or the program stops
ensurethe function broke its own promisethe program stops
failsnobody: it is a normal outcome, like a declined cardthe caller handles it with on
checka test found a wrong answerquill 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, and fails go at the top of the function, right under the fn line, in any order. A function can have as many require and ensure lines as it needs.
  • Each one holds one condition that is true or false. Join two with and, or write two lines.
  • require reads the parameters. ensure reads the parameters and result.
  • An ensure is checked at every return, so a function with three return lines keeps its promise on all three.
  • result exists only in ensure. Inside the function, result is an ordinary name you can use for something else.
  • where goes 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. Each where is checked when the function is called, before its require lines.

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

  1. Write fn split_bill(total: USD, people: number) -> USD that divides a bill. Add require people > 0, then call it with 0 people and read what Quill says.
  2. Add ensure result <= total to split_bill. Call it with 1 person, then with 4.
  3. Write a function with two return lines and one ensure. Make one of the returns break the promise, and see which line Quill points to.
  4. Change split_bill so people is number where 1..20. Call it with 25, then with a value worked out while the program runs, like 10 + 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

  1. Write type Student with name: text and score: number where 0..100. Make two students and say each one.
  2. Make a student with a score of 120 and read what Quill says.
  3. Write fn passed(s: Student) -> true/false that gives back whether the score is 50 or more.
  4. 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

  1. Make a folder with two files: prices.quill with fn with_tax(price: USD) -> USD, and shop.quill that brings prices and says with_tax(100 USD).
  2. Add an --- example: above with_tax in prices.quill, and run quill test prices.quill.
  3. Misspell the name after bring and read what Quill suggests.
  4. Add a type Product to prices.quill, and make a Product in shop.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

  1. Make a file notes/hello.txt, and a program that permits reading notes and says what is in it.
  2. Remove the permit, and read what Quill says.
  3. Write a program that writes the date you choose into out/today.txt, then reads it back.
  4. 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

  1. Write type Student with name: text and score: number where 0..100, and a file with a student in JSON. Read it with as Student.
  2. Change the score in the file to 120, and see on Invalid run.
  3. Read a JSON list of students with as list of Student, and say the names of those who passed with get name of students where score >= 50.
  4. Say json of 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 http brings in Quill’s http module, 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 at url and gives back an answer: a record with status, a number like 200 or 404, and body, the text that came back.
  • as Rates reads 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 with on NetworkDown or try, like any failure.
  • The service answers with an error, like 404 (not found) or 500 (the service broke). That’s still an answer. Its status says 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

  1. Write a program that brings http, permits one host, and fetches a page from it. Say the status.
  2. Remove the permit and read what Quill says.
  3. Fetch a page that doesn’t exist on that host, and handle the 404 with when.
  4. Make a record, and send it with post(url, json(record)) to a test service like https://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 ai brings Quill’s ai module, 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:

  1. Quill sends the shape of Request to the model, so it answers with exactly those fields.
  2. Quill reads the answer the same way as reads any JSON: every field must be there, the right kind, and inside its where range.

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:

PermitAsksSettings
permit env "ANTHROPIC_API_KEY"Claude, at api.anthropic.comThe model is claude-opus-5-5 unless QUILL_AI_MODEL names another.
permit env "OPENAI_API_KEY"any service that speaks the OpenAI chat formatQUILL_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

  1. Set ANTHROPIC_API_KEY on your computer, and ask a model to write one sentence about Quill.
  2. Write type City with name: text and country: text, and ask for as list of City with “Name three cities in Cameroon.”
  3. Add a where range to a number field, and ask a question that tempts the model to break it. Watch on Invalid run.
  4. Run the same program against a model on your own computer, with permit env "OPENAI_API_KEY", OPENAI_BASE_URL and QUILL_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 web brings Quill’s web module.
  • route GET "/hello/{name}" says what to do when a request asks for a page with GET at 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: 200 means 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:

StatusMeans
200It worked.
201Something new was made.
400The request was wrong, like a booking without a venue.
404There’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:

  1. 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.
  2. Split into files. Move the types and functions into their own files and bring them in. Keep bring web and the routes in the file you run.
  3. 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.
  4. Other services. Call a payment service or a rates API with bring http.
  5. AI. Ask a model with bring ai, and read its answer as a type with ask ... as Review, the same way a request body is read with as.

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

  1. Write a route GET "/hello/{name}" and open it in your browser with your own name.
  2. Add route GET "/time" that responds with any text you like, and a request to a path you didn’t write. Look at the 404.
  3. Write a type Student with score: number where 0..100, and a POST "/students" route that reads the body as Student. Send a score of 120 with a tool like curl -X POST localhost:8080/students -d '{"name": "Amina", "score": 120}'.
  4. 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

  1. Write a program that calculates the area of a 4 by 2.5 rectangle, with a check that the answer is 10.
  2. Make the check fail on purpose and read the message.
  3. Add a --- example: line above a value, then run quill test.
  4. Run only that file with quill test your-file.quill.
  5. Put the file in a folder called shop, and run quill 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
  1. Where: the file and line, with ^^^ under the exact spot.
  2. What went wrong, in the first sentence.
  3. Why it’s a problem, in the next sentence.
  4. 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

MistakeWhat Quill saysFix
say "HelloThis text starts with a quote but never ends.add the closing "
say gretingThere’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 < 10Quill 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

  1. Make three different mistakes in one program and run quill check. Does Quill find all three?
  2. Run quill explain on a line with a calculation.
  3. 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

KindExamples
Text"Hello", "Hi {name}", """He said "hi"."""
Number42, 0.10, -3.5
true/falsetrue, false, total > 10
listlist "coding", "reading", hobbies at 1
recordrecord city = "Anytown", address.city

Operators, tightest first

OperatorsMeaning
( )brackets
-xnegative
* / %multiply, divide, remainder
+ -add, subtract (numbers only)
== != < <= > >=compare
notnot
andboth
oreither

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

CommandWhat it does
quill run FILE.quillrun a program
quill check FILE.quillfind mistakes without running
quill testrun every test in this folder
quill test shoprun the tests in the folder shop
quill explain FILE.quill:Nshow how Quill read line N
quill new NAMEstart a project; a menu offers the templates
quill new NAME TEMPLATEstart one from a template: app, learn, starter, backend, integration, webhook, ai, ai-backend, finance, banking, automation, library
quill tokens FILE.quillshow the pieces Quill read
quill updatechoose a version of Quill to install (also quill upgrade)
quill update VERSIONinstall that version, or latest
quill helpshow help
quill versionshow 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

  1. Programs and files
  2. Lines, indentation and comments
  3. Words and names
  4. Kinds of values
  5. Writing values
  6. Calculations and conditions
  7. Statements
  8. when and for
  9. Tests: check and doc examples
  10. When Quill finds mistakes
  11. The quill tool
  12. Planned, not yet implemented
  13. 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.

KindExamplesWhat it’s for
Text"Amina", "Total: {total}"words and sentences
Number42, 0.10, -3.5exact decimal numbers
true/falsetrue, false, total > 10conditions
listlist "coding", "reading", list of textvalues in order, all one kind
recordrecord 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 / 3 is 0.33333333333333333333, and 2 / 3 is 0.66666666666666666667.
  • The remainder a % b has the sign of a: 7 % 3 is 1 and -7 % 3 is -1. It works on decimals too: 7.5 % 2 is 1.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:

CalculationShowsRule
0.100.10as written
0.10 + 0.20.30adding and subtracting keep the most digits after the point
2.5 - 0.52.0
1.50 * 23.00multiplying keeps at least as many digits as the longer side
10 / 42.5dividing drops trailing zeros…
9.00 / 33.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, not 7.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 true or false.
  • list: the items separated by commas, or an empty list when there are none. list "coding", "reading" is shown as coding, 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:

WriteTo get
\na new line
\ta 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 TEXT is TEXT without spaces, tabs, or new lines at the start and the end. uppercase TEXT and lowercase TEXT change the case of every letter. Each gives back new text; the value it read doesn’t change.
  • length of TEXT is how many characters TEXT has, as a number. An accented letter such as é is one. length of a list is a mistake that suggests count of (§5.6).
  • TEXT contains PART, TEXT starts with PART, and TEXT ends with PART are true/false. Capital and small letters are different. LIST contains ITEM is true when an item of the list equals ITEM, and ITEM must be the kind of the list’s items. These bind like a comparison (§6.1): email contains "@" and paid is (email contains "@") and paid.
  • replace OLD with NEW in TEXT is TEXT with every OLD changed to NEW. OLD can’t be empty.
  • split TEXT by SEPARATOR is a list of the texts between each SEPARATOR, in order, with the separators left out. Two separators side by side give an empty text between them. SEPARATOR can’t be empty, and split TEXT into ... is a mistake; into shares out money (§8.7).
  • join LIST with SEPARATOR is one text: the items of LIST, each shown as say shows it (§4.3), with SEPARATOR between 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, and replace are tools only when a value follows them on the same line. Right against (, as in trim(x), they call a function of that name; with a space, trim (parts at 1) is the tool. A call to trim(x) with no function trim is a mistake that says the tool takes no brackets. length is a tool only before of, contains only between two values, and starts and ends only before with. 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: .5 is not a number. Write 0.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. Write 3.0 for 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", 2 is a mistake.
  • An empty list names its kind: list of text, list of number, or list of true/false.
  • VALUE at POSITION reads one item. POSITION is a whole number, and 1 is the first item. 0 and 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, record is followed by an indented block. Each field is NAME = 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.NAME reads 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 LIST adds NAME from each record. NAME is a number or money in one currency. The result is that number, or money in that currency. When the value after of is one record, the sum is that one amount: sum fees of school.
  • get NAME of LIST gives a list of NAME from each record.
  • get NAME { "," NAME } of LIST gives 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 after of is one record, one field comes back as that value, and several fields come back as one record. A dot after of reaches a field further in: get gpa of school.acceptances, get place of desk.bookings. record = list at 1 then record.name reads a field of that item. (list at 1).name is the same value.
  • A field that is maybe contributes only when it is found. missing is left out. When every money amount is missing, sum is 0 in that currency.
  • get NAME with NESTED in SOURCE gives the list NAME. Each record in that list has the field NESTED. SOURCE is one record, or a list of records that each hold NAME. A list of records is gathered into one list.
  • LIST where CONDITION gives the records of LIST whose CONDITION is true, in their order. Inside CONDITION, each field of the records is a name, and it hides a value of the same name; other values are read as usual. CONDITION must be true/false. When nothing matches, the result is an empty list. where may follow again: students where score >= 50 where paid keeps records for which both are true.
  • where binds after a whole value, so its condition takes in and and or: students where score >= 50 and paid. After of, a where applies to the list before get or sum reads it: sum fee of roster where paid == false adds the unpaid fees.
  • count of LIST is how many items LIST has, as a number. LIST is any list; one record, text, or a number is a mistake. count of roster where paid == false counts the records that match.
  • average NAME of LIST is the sum of NAME divided by how many records have it. NAME is a number or money in one currency, and a missing amount 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 NAME gives the records of LIST in order of NAME, lowest first. sorted by highest NAME puts the highest first, and sorted by lowest NAME is the same as sorted by NAME. NAME is 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. NAME that might be missing, a true/false field, or a list that is not records is a mistake. sorted by binds like where, and either may follow the other: students where paid == false sorted by highest fee.
  • count, average, sorted, by, highest, and lowest are ordinary names everywhere else. count is a word only right before of, average only before a field and of, and sorted only before by: var count = 0 and fn 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 sum of text is a mistake, and nothing runs. In a where, 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:

LevelOperatorsWorks onGives
1( ... )anythingthe value inside
2-aNumberNumber
3a * b, a / b, a % bNumber, NumberNumber
4a + b, a - bNumber, NumberNumber
5a == b, a != btwo values of the same kindtrue/false
5a < b, a <= b, a > b, a >= bNumber, Numbertrue/false
6not atrue/falsetrue/false
7a and btrue/false, true/falsetrue/false
8a or btrue/false, true/falsetrue/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, like 5 == "5", is a mistake rather than false, because it is almost never what was meant.
  • <, <=, > and >= compare numbers only.
  • Comparisons can’t be chained: 1 < x < 10 is a mistake. Write 1 < 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, if a is false, b is not worked out.
  • In a or b, if a is true, b is 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 + 1 is a mistake when x doesn’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 marked var is 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 = value and set NAME at POSITION = value change one field or one item. The list or record itself must be marked var. The new value must match that field or item.
  • name = ... on a name that already exists is a mistake, even for a var. Use set to 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 inside when. 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

  1. Quill tests each condition in order, from the top.
  2. The first case whose condition is true runs. No other case runs, even if its condition is also true.
  3. else matches when no case above it was true. It is optional, and if present it must be the last case.
  4. 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 when must have at least one case.
  • A when whose cases are conditions stands on its own line. when VALUE on one line opens a maybe (§8.8).
  • A case that comes after else can 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 =, say and return. The cases are on the lines below, indented.
  • Each case is CONDITION -> VALUE or else -> VALUE. The value is an expression, not a statement: say, set and the other statement words are a mistake there.
  • The last case must be else, so there is always a value. A case after else is a mistake.
  • Every condition must be true/false. Every case must give the same kind of value. missing fits only when the other cases give found values; the when is 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 VALUE on 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
  • START and END are numbers. They are worked out once, before the first pass.
  • The range is two dots. Three dots (1...3) is a mistake; write 1..3.
  • NAME holds the number for this pass. It exists only on the lines under the for. 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..12 when i is 10 counts from 10 through 12.
  • The loop moves NAME to the next number by adding 1. set on that name is a mistake. To keep a total, create a var before the loop and change it with set.
  • A line under for can’t create a value (name = ... or var name = ...).
  • If START is greater than END, the body never runs. for i in 5..5 runs once.
  • The body is indented further than for, and for stands on its own line.

for also walks a list, once for each item:

for hobby in hobbies
  say hobby
  • hobbies is a list. It is worked out once, before the first pass.
  • hobby holds the item for this pass. It exists only on the lines under the for, and set on 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 1 with 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, and fn stands on its own line.
  • -> kind means the function gives a value of that kind back. Every path through the body has to reach return VALUE, and that value has to be the named kind. Parameters may each have their own kind. The kind after -> is the only kind return may give back. Quill does not convert one kind into another.
  • With no -> kind, the function gives nothing back. Call it on its own line. return on its own leaves the function early. A value after return is 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.
  • return outside a function is a mistake.
  • A parameter’s kind is text, number, true/false, a currency such as USD, a type such as Booking (§8.11), maybe one of those, or list of one of those. Kinds are written in lowercase; Int and Text are not kinds.
  • The first line of the body may be fails NAME { "," NAME }. A failure name starts with a capital letter. fail NAME leaves 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. Write USD.
  • “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 VALUE rounds a number or money to a whole number. to N places keeps N digits after the point, with N a whole number from 0 to 20; the result has exactly N digits. to cents is for money and keeps the currency’s smallest unit. Plain round goes to the nearest value, and exactly half goes to the even digit (10.125 → 10.12, 10.135 → 10.14). round up goes to the next larger value, and round down to the next smaller one (round up -2.1 is -2).
  • split AMOUNT into COUNT gives a list of COUNT amounts in whole cents that add up to AMOUNT. COUNT is a whole number, at least 1. Each part is AMOUNT / COUNT rounded down to cents, and the cents left over go one each to the first parts: split 100 USD into 3 is 33.34 USD, 33.33 USD, 33.33 USD.
  • split AMOUNT by SHARES gives one amount for each number in the list SHARES, 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.
  • split needs an amount with no part smaller than a cent; otherwise the program stops. Round it first.
  • When the value after split is text, split TEXT by SEPARATOR cuts it into a list of text instead (§5.2.1).
  • P% of VALUE is VALUE * P / 100, for a number or money. P is a number.
  • convert AMOUNT to CURRENCY at RATE is money in CURRENCY: AMOUNT * RATE. RATE is a number above zero. Converting to the same currency is a mistake.
  • These are mistakes found before running: round of something that isn’t a number or money; to cents on a number; places that aren’t a whole number from 0 to 20; split of something that isn’t money, into a count that isn’t a whole number of at least 1, or by something that isn’t a list of numbers; % of a value that isn’t a number or money; convert of 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, split and convert are reserved words (§3.2). up, down, to, cents, places, into and by are ordinary names everywhere else.

8.8 Maybe

-> maybe KIND means the function gives back that kind, or gives back nothing.

  • return found VALUE gives the value back. The value’s kind is KIND.
  • return missing gives nothing back. It is allowed when the function’s result is a maybe.
  • when VALUE opens a maybe. The cases are found NAME -> STATEMENT and missing -> STATEMENT. Both are required. NAME exists only on the found line.
  • Using a maybe with say, or as a number, is a mistake. Open it with when first.

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"
  • fails goes at the top of the function, with any require and ensure lines (§8.10).
  • var name = charge(...) (or name =, or set name =), then indented on NAME -> ... for every declared failure. On success the value is stored and the program continues. On failure the matching on line 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 must return on 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 (missing fits a maybe).
    • Each on line chooses for itself, so one failure can give a value while another stops.
  • A call on its own line gives nothing back, so the on lines under it run statements. A call after -> there, like on Oops -> log("x"), is run as a statement; any other value is a mistake.
  • try call is an expression inside a function whose fails list includes every failure of that call. It passes those failures upward. try on a line by itself is a mistake (§7.7); a call on its own line passes a failure upward with on NAME -> fail NAME.
  • A call that can fail, with no on and no try, 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 VALUE and ensure VALUE go at the top of the function, before its first other line. They may be mixed with fails in any order. A function can have any number of each.
  • VALUE is a condition: it must be true/false (§6). Only one condition fits on a line; join conditions with and.
  • require can read the parameters. Every require is tested in order when the function is called, before its first line runs.
  • ensure can read the parameters and result, the value the function gives back. Every ensure is tested in order at each return, after the value is worked out and before the function leaves. A function that leaves with fail does not test its ensure lines.
  • result is defined only inside ensure. Elsewhere it is an ordinary name.
  • A parameter can carry a range: guests: number where 1..500. The parameter’s kind must be number or 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 every require.
  • 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 require also names the line of the call.
  • These are mistakes: ensure on a function with no ->; require that reads result; a parameter called result in a function with an ensure; a condition that isn’t true/false; require or ensure outside a function or below its first other line; a second fails line; where on 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 test does 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. A maybe field is given missing or found VALUE. The record’s fields are kept in the type’s order.
  • A record of a type is a record (§5.5): ., get, sum and where read 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 own require lines.
  • 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 of or 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 NAME reads NAME.quill in the folder of the file that writes it. bring "PATH" reads the file at PATH, counted from that folder, with / between folders; it must end in .quill. bring goes at the top level of a file.
  • A brought file holds fn, type and bring lines, comments, and its own tests. Its functions and types can be used by name in the program, as if written above its first line. Its check and --- 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, ai and http name Quill’s modules. bring web brings 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. KIND is read or write (a file or folder), net (a host), or env (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 net names a host such as api.stripe.com, or *.example.com for every host below that domain, without https:// or a path. A host without a port allows any port.
  • permit env names one setting, made of letters, digits and _.
  • Quill’s own functions:
FunctionGives backFails withNeeds
read_file(path: text)the file’s textFileMissingpermit read
write_file(path: text, text: text)nothing; makes missing foldersCantWritepermit write
file_exists(path: text)true/falsepermit read
env(name: text)maybe textpermit 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, post or send.

8.14 JSON

booking = body as Booking
  on Invalid -> say "That isn't a booking."
say json(booking)
  • TEXT as KIND reads JSON text as KIND, any kind a function can name (§8.6). It is a call that can fail with Invalid, so it needs on Invalid or try (§8.9). as follows a value, like . and at.
  • 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, like 1e3, is Invalid.
  • An object is read as a record of a type: every field of the type must be there, except maybe fields, which may be null or left out and are then missing. Fields the type doesn’t name are left out. Each field’s range (§8.11) must hold. A JSON array is read as list 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, a found value its value, and missing null.

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
FunctionSends
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 a permit net (§8.13) covering the URL’s host. A URL starts with https:// or http://.
  • Each gives back an answer: a record with status, a number, and body, text. Any answer the service gives is an answer, including statuses such as 404 and 500.
  • Each fails with NetworkDown when 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 VALUE sends the text VALUE to a model and gives back its answer as text. ask VALUE as KIND gives back the answer as KIND, any kind a function can name (§8.6). VALUE is everything up to as or the end of the value.
  • ask is a call. It fails with ModelFailed when the model can’t be reached, answers with an error, declines, or gives no text. ask ... as KIND also fails with Invalid when the answer doesn’t fit KIND as as reads it (§8.14), including a where range.
  • It needs bring ai, and a permit env for a key, which chooses the provider: ANTHROPIC_API_KEY asks Claude; otherwise OPENAI_API_KEY asks a service that speaks the OpenAI chat format. The service’s host needs a permit net. For Claude, a program with no permit net at 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 is claude-opus-5-5, and an OpenAI-compatible service needs it), ANTHROPIC_BASE_URL and OPENAI_BASE_URL (the service’s address). The request still needs a permit net for that address.
  • With as KIND, the model is sent the kind as a JSON schema: each field is required, a maybe field may be null, 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. METHOD is GET, POST, PUT, PATCH or DELETE. PATH starts with /; each {NAME} in it matches one part of a requested path and is a text value named NAME inside the route, with % escapes worked out.
  • Inside a route, request is a record with method, path, query (after ?, or "") and body, all text. A route doesn’t see values created outside it, like a function (§8.6).
  • respond STATUS, VALUE ends the route with its answer, as return ends a function. STATUS is a whole number from 100 through 599. Text is sent as text/plain; any other value as JSON (§8.14), as application/json. Every path through a route must reach respond, which may also stand after on ... ->. return in a route and respond outside one are mistakes.
  • Request and Response are types the web module gives; a program can’t write types with those names.
  • quill run runs the program’s top level, then answers requests on port PORT (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 answered 404; a path a route has with another method 405. A route that stops the program (§10.2) is answered 500, its problem is shown where the server runs, and the server keeps answering. quill check and quill test don’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
  • set on a value that isn’t marked var, or that changes its kind
  • operations on the wrong kinds of values, like price + "5"
  • division by a written 0
  • when conditions that aren’t true/false
  • require, ensure and where that break the rules in §8.10, and a written value passed outside a parameter’s where range
  • a for range whose ends aren’t numbers, a for that doesn’t walk a list, or set on 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 check or --- example: that is false
  • a require or ensure that is false, or a value outside a parameter’s where range (§8.10)
  • a list position past the end, or a position in an empty list
  • an average of a list with no amount in that field (§5.6)
  • a split separator or a replace old 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

CommandWhat it does
quill run FILE.quillCheck the program, then run it
quill FILE.quillSame as quill run
quill check FILE.quillCheck for mistakes without running anything
quill lspSpeak 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.quillRun the selected file, including files with no checks. Missing files and non-.quill paths are errors.
quill testRun 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 FOLDERThe same as quill test, for the files in FOLDER and its subfolders.
quill explain FILE.quill:NShow 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.quillShow 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 helpShow help
quill versionShow 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.

FeatureREADME section
require and ensure proven before running (static verification)Contracts
Permissions: permitPermissions
Modules: use, web, db, route, respondA 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.

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 quill program. Quill code in a ```quill block inside a Markdown file, such as a README, has Quill’s colors too.
  • quill new shop app makes 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 new makes have the same line endings as on macOS and Linux.

v0.4.5 (3 November 2026)

  • Templates for quill new. quill new shop asks what kind of program it is, with the arrow keys, and quill new shop backend skips the menu. Eleven templates, in five groups: learn and starter for learning; backend, integration and webhook for backend work; ai and ai-backend for AI; finance and banking for business and finance; automation and library for automation and general programs. Each one runs and passes its tests straight away, and comes with a README.md and QUILL.md, a one-page tour of the language. With no terminal, quill new shop makes starter. 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 called trim; when there is none, Quill explains that the tool takes no brackets.
  • Working with text. trim name takes away the spaces at the ends, and uppercase and lowercase change the letters. length of name counts the letters, with é as one. email contains "@", code starts with "INV", and file ends with ".csv" are true or false, and work in a when and a where; 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 a list back into one text. An empty separator is a mistake. None of these words is reserved, so var length = 0 and fn join(...) still work. They are ink (#949CF5 on a dark editor, #3A44A0 on 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..100 is colored like the first.

v0.4.2 (3 November 2026)

  • How many, the average, and in order. count of students says how many records there are, and count of students where score >= 50 counts the ones that match, with no counter in a loop. average score of students is 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 score puts the lowest first, sorted by highest score the highest, and text goes in alphabetical order; records with the same value keep their order. sorted by works after where and in for. count, average, and sorted are words only before of, a field and of, and by, so var count = 0 and fn average(...) still work. They are sea, like get and sum. In Cursor and VS Code, get, sum, and with had lost their color, because the AI model’s ask took their place in the color list; in Emacs they showed the ask color. Each kind has its own color again.
  • Quill files end in .quill. .ql was already GitHub’s extension for CodeQL, so GitHub counted Quill files as CodeQL. quill new makes hello.quill, bring billing reads billing.quill, and the editors color .quill files. A .ql file still runs, with a note asking you to rename it (“Note:” in nib orange in a terminal; NO_COLOR=1 turns color off), and bring still finds billing.ql; a later version will stop reading .ql.
  • quill test shop tests one folder: it runs the files in shop and its subfolders that contain a check or --- example:. Before, naming a folder was refused as a path without .quill.
  • try on a line by itself, like try write_file(...), explains that try goes where a call’s answer is used, and shows on CantWrite -> fail CantWrite for passing a failure on from a call on its own line. Before, Quill suggested say try ..., which can’t work for a call that gives nothing back.
  • Web services. bring web, then route GET "/hello/{name}" and respond 200, "Hello, {name}!", turn a program into a web service. {name} parts of the path are values, request holds the method, path, query and body, and a record is sent as JSON. quill run serves on PORT (8080). Unknown paths get 404, the wrong method 405, and a route that stops 500, with the problem shown and the server still running. route and respond are mist, like bring.
  • Asking an AI model. bring ai, then ask "Summarize: {text}" gives back the model’s answer, and ask "..." as Review gives back a Review: the model is sent the type’s shape, and the answer is checked against every field and where range. ModelFailed and Invalid are handled like any failure. permit env "ANTHROPIC_API_KEY" chooses Claude (claude-opus-5-5 unless QUILL_AI_MODEL says otherwise); permit env "OPENAI_API_KEY" chooses any OpenAI-compatible service, including one on your own computer. ask is spark.
  • Web requests. bring http adds fetch(url), post(url, body) and send(method, url, body, headers). Each needs permit net for the host, gives back an answer with status and body, and fails with NetworkDown when the service can’t be reached. A 404 or 500 is still an answer, so the program decides.
  • JSON. body as Booking reads JSON text as any kind, checking every field, its kind, and its where range; money is read exactly, even from text like "500.00". Text that doesn’t fit fails with Invalid, handled with on Invalid or try. json(value) writes any value as JSON. as is steel, like type.
  • Permits, files and settings. permit read "config", permit write "reports", permit env "STRIPE_KEY" and permit net "api.stripe.com" say what a program may touch; anything else is refused. read_file, write_file, file_exists and env are Quill’s own functions, and their failures (FileMissing, CantWrite) are handled with on or try. 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. permit is rust.
  • bring puts a program together from files. bring billing reads billing.quill next to the file, and bring "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. bring is mist blue, the color saved for modules.
  • Money that never loses a cent. round total to cents rounds half to even, as banks do; round up and round down choose a direction, and round x to 2 places works on numbers. split 100 USD into 3 gives 33.34 USD, 33.33 USD, 33.33 USD, and split fee by list 70, 30 shares by weights; the parts always add up. 20% of fee takes a percent. convert 100 USD to EUR at 0.92 changes 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, split and convert are mint.
  • Types. type Booking, then one field per line, like guests: number where 1..500, names what every booking has. Booking venue = "Town Hall", guests = 120, fee = 500 USD makes 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. type is steel, like where.

v0.4.0 (20 October 2026)

  • Contracts. require amount > 0 USD under fn says what has to be true when the function is called. ensure result >= 0 USD says what is true each time it gives back, and result is the value it gives back. A broken one stops the program, names the values it read, and says whose mistake it was; a broken require names the line of the call. require, ensure, and fails go at the top of the function in any order. quill explain describes contract lines. require, ensure, and result inside an ensure are brass (#C8A06A on a dark editor, #7A5A20 on 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, like book(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 explain describes the range. where is steel (#6AA8C8 on a dark editor, #1A5270 on a light one), the color saved for rules on types.
  • Filtering with where. students where score >= 50 keeps the records whose condition is true, like Laravel’s where. Inside the condition a field is read by its name. It works inside get and sum (sum fee of roster where paid == false), after for ... 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 roster adds the fee of each record. get fee of roster takes that field as a list. get name, age, fee of roster keeps those fields together on each record. The same line reads one record: get name, fees of school is one record with those fields. sum fees of school is that one amount. A dot after of reads 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, then first.place. get bookings with acts in roster is the longer form, for a list and the field embedded in it. A missing amount is left out of a single field. for remains when each record needs its own step. get, sum, and with are sea (#6EC1D6 on a dark editor, #15556A on a light one).
  • when gives a value. delivery = when, then indented cases like total > 100 -> 0, ending with else -> 10. The first true case gives the value. It works after =, set, say, and return. else is required, and every case gives the same kind of value. quill explain describes each case.
  • on can 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. Each on line chooses for itself.
  • Examples above a function test it. --- example: with_vat(100 USD) == 105 USD directly above fn with_vat runs 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.ql runs 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 of Money<USD>. Contracts stay require and ensure. Its error and quill explain examples show Quill’s real output.
  • Money examples use dollars. 100 USD + 50 USD is 150 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...25 is a mistake, and Quill tells you to write 18..25. -> belongs inside a when, so say takes the text directly.
  • Money. 100 USD + 50 USD is 150 USD. Two currencies cannot be added. A function writes the currency as the kind: price: USD.
  • Maybe. -> maybe number means the answer might be missing. return found 2.50 or return missing. when opens it with found and missing, and both are required. maybe, found, and missing are peach.
  • Failures. fails Declined names how a function can fail. fail Declined leaves with that failure. The caller writes on Declined under the call, or try inside a function that fails the same way. fails, fail, on, and try are red.
  • Functions. fn double(n: number) -> number names a calculation, and return gives the value back. A call is double(21). A function with no -> does work and gives nothing back. fn and return are pink.
  • list, record, of, and at each have their own editor color: clay, sage, gold, and violet.
  • Lists and records. list "coding", "reading" keeps values in order, and hobbies at 1 is the first. record keeps named fields, and address.city reads one. for hobby in hobbies walks a list. at picks one item, and of is required on an empty list (list of text). The book says where each word is used, and search finds of and at.
  • for loops. for i in 1..3 repeats 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 finds for, 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, true and false, var and set, check, and the logic words), the feather on .ql files (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. maybe and failures have their colors now. Cursor and VS Code get them the first time Quill runs. Vim, Emacs, and Sublime detect .ql files from the editors folder. quill lsp reports the same mistakes to any editor. Set QUILL_NO_EDITOR_SETUP=1 to leave Cursor and VS Code alone.

v0.3.0 (13 October 2026)

  • quill update shows every version of Quill in a menu: choose one with ↑ ↓ and Enter. quill update latest or quill update 0.2.0 installs a version directly, and quill upgrade is 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=1 to 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.2 is exactly 0.3.
  • Comparisons ==, !=, <, <=, >, >=, and conditions with and, or and not.
  • Values inside text: "Hi {name}". See Text.
  • var and set for values that change. See Values That Change.
  • when to choose what to do, with -> and else. 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.
  • check accepts any comparison, not only ==.
  • quill explain describes calculations, var, set and when.
  • New reserved words: var, set, when, else, true, false, and, or, not.

v0.1.0 (13 October 2026)

The first release.

  • say to show a value, and name = value to name one.
  • Text in double quotes, with escapes and three-quote text; numbers written exactly as typed.
  • Comments (--) and doc comments (---).
  • Tests: check a == b and --- example:.
  • Errors in the README style: where, what, why, and “Did you mean”.
  • The quill tool: run, check, test, explain, new, tokens, help, version.
  • Installers for macOS, Linux and Windows.