Need help? Join our Discord — ask questions, share projects, and connect with the Quill community.

Learn Quill, A Beginner's Guide

Welcome! This guide will teach you everything you need to start writing real programs in Quill. No prior programming experience is required, we will explain every concept from the ground up.

What is Quill?

Quill is a programming language designed to read like plain English. Instead of cryptic symbols and jargon, Quill uses everyday words like is, say, give back, and for each. Under the hood, Quill compiles to JavaScript, which means your Quill programs can do anything JavaScript can, build websites, create servers, automate tasks, and more.

Think of Quill as a translator: you write instructions in something close to English, and Quill converts them into code that your computer understands.

Your first program

Every programming journey begins with a "Hello, World!" program. In Quill, it is just one line:

say "Hello, World!"

That is it! The word say tells Quill to print something to the screen, and "Hello, World!" is the text you want to display.

How to run it

1. Create a file called hello.quill and type the line above into it.

2. Open your terminal (command prompt) and run:

quill run hello.quill

3. You should see Hello, World! printed on the screen. Congratulations, you just ran your first program!

Try it yourself: Change the text inside the quotes to your own name, like say "Hello, my name is Alex!", and run it again.

Comments

What are comments? Comments are notes you write inside your code that the computer completely ignores. They exist solely for humans, to explain what your code does, why you made a certain decision, or to leave a reminder for your future self.

Think of comments like sticky notes on a recipe. The oven does not read them, but they help you remember why you added extra garlic.

In Quill, comments start with two dashes (--) and continue to the end of the line:

-- This is a comment. Quill ignores this entire line.
say "Hello!"  -- You can also put comments at the end of a line

Why use comments?

-- TODO: add error handling here later
-- Calculate the user's age from their birth year
age is 2026 - birthYear
Tip: Good comments explain why you did something, not what the code does. The code itself shows the "what", your comments should add the "why."

Variables

What is a variable? A variable is like a labeled box where you store information. You give the box a name and put a value inside it. Later, you can look at what is inside the box, change its contents, or use it somewhere else in your program.

In Quill, you create a variable with the word is:

name is "Alice"
age is 25
active is yes

Reading that out loud: "name is Alice, age is 25, active is yes." It reads like English!

Using are for lists

When your variable holds a collection of things (a list), you can use are instead of is for better readability. They work exactly the same way, it is purely a style choice:

colors are ["red", "green", "blue"]
scores are [95, 87, 100, 72]

Reassigning variables

You can change the value inside a variable at any time by using is again:

count is 0
say count        -- prints 0

count is count + 1
say count        -- prints 1

count is count + 1
say count        -- prints 2

Think of it like erasing the old label on your box and writing a new value inside.

What kinds of values can go in a variable?

Quill has six types of values (we will cover each in detail in the next section):

greeting is "Hello there"     -- Text (a string of characters)
temperature is 72.5           -- Number (whole or decimal)
isRaining is no               -- Boolean (yes or no)
fruits are ["apple", "pear"]  -- List (an ordered collection)
person is {name: "Bob"}       -- Object (a collection of named values)
result is nothing             -- Nothing (absence of a value)
Common mistake: Variable names cannot contain spaces. Use firstName or first_name instead of first name.
Try it yourself: Create variables for your own name, age, and favorite color, then use say to print each one.

Data Types in Depth

Every value in Quill has a type, it is a category that describes what kind of information the value holds. Understanding types will help you use them correctly.

Text (Strings)

Text is any sequence of characters wrapped in double quotes. Programmers often call these "strings" (as in, a string of characters).

greeting is "Hello, World!"
empty is ""                      -- an empty string (no characters)
sentence is "I have 3 cats."   -- numbers inside quotes are still text

String interpolation lets you embed variables and expressions inside a string using curly braces {}:

name is "World"
say "Hello, {name}!"          -- Hello, World!
say "2 + 2 = {2 + 2}"          -- 2 + 2 = 4
say "Name length: {length(name)}"  -- Name length: 5

Escape sequences let you include special characters:

say "She said \"hello\" to me"   -- She said "hello" to me
say "Line one\nLine two"         -- prints on two separate lines

Multiline Strings

For text that spans multiple lines, use triple quotes ("""):

poem is """
  Roses are red,
  Violets are blue,
  Quill is great,
  And so are you.
"""

say poem

Triple-quoted strings preserve newlines and indentation. They are useful for:

-- SQL query
query is """
  SELECT name, email
  FROM users
  WHERE active = true
  ORDER BY name
"""

-- HTML template
html is """
  <div class="card">
    <h2>{title}</h2>
    <p>{description}</p>
  </div>
"""

-- Multi-line error message
message is """
  Something went wrong!

  Please check:
  - Your internet connection
  - Your API key
  - The server status
"""
Under the hood: Multiline strings compile to JavaScript template literals (backticks). String interpolation with {variable} works inside them, just like regular strings.

Escaping Braces

If you need a literal { or } in a string without triggering interpolation, use \{ and \}:

-- Literal braces (not interpolation)
say "CSS: \{color: red\}"
-- Output: CSS: {color: red}

-- Inline JavaScript
say "function()\{location.reload()\}"
-- Output: function(){location.reload()}

-- Mix interpolation and literal braces
name is "Alice"
say "{name} says \{hello\}"
-- Output: Alice says {hello}

Numbers

Numbers in Quill work like you would expect. They can be whole numbers (integers) or decimals:

wholeNumber is 42
decimal is 3.14
negative is -10
zero is 0

You can do math with numbers (covered in the Operators section), and you can mix them into strings with interpolation:

price is 9.99
say "The total is ${price}"  -- The total is $9.99

Booleans (Yes / No)

A boolean is a value that is either true or false, like a light switch that is either on or off. In Quill, you can write booleans as yes/no or true/false:

isLoggedIn is yes
hasPermission is no
gameOver is false
ready is true

Booleans are essential for making decisions in your code (which we will cover in the Control Flow section).

Lists

A list is an ordered collection of values, wrapped in square brackets and separated by commas. Think of it like a numbered shopping list:

fruits are ["apple", "banana", "cherry"]
numbers are [10, 20, 30]
mixed are ["hello", 42, yes]     -- lists can hold different types
empty are []                          -- an empty list

Access individual items by their position (index). Important: counting starts at 0, not 1:

colors are ["red", "green", "blue"]
say colors[0]   -- red   (first item)
say colors[1]   -- green (second item)
say colors[2]   -- blue  (third item)
Common mistake: Lists start counting at 0, not 1. The first item is at position [0], the second at [1], and so on. This catches almost every beginner off guard!

Objects

An object is a collection of named values (called "properties"), wrapped in curly braces. Think of it like a contact card where each piece of info has a label:

user is {name: "Alice", age: 30, active: yes}

Access properties using a dot:

say user.name     -- Alice
say user.age      -- 30
say user.active   -- yes

Nothing

nothing represents the absence of a value. It is like an empty box, the box exists, but there is nothing inside it. In other programming languages, this concept is called null or nil.

result is nothing

if result is nothing:
  say "No result yet"

Functions that do not explicitly return a value give back nothing automatically.

Try it yourself: Create a variable for each data type and use say to print them all. Try using string interpolation to build a sentence like "My name is {name} and I am {age} years old."

Operators

What are operators? Operators are symbols that perform actions on values, like the + sign on a calculator. They take one or more values and produce a result.

Math operators

These work just like the math you already know:

OperatorWhat it doesExampleResult
+Addition2 + 35
-Subtraction10 - 46
*Multiplication3 * 721
/Division15 / 43.75
%Modulo (remainder)10 % 31

Here is a practical example:

price is 29.99
quantity is 3
subtotal is price * quantity
tax is subtotal * 0.08
total is subtotal + tax
say "Your total is ${total}"  -- Your total is $97.1676

What is modulo? The % operator gives you the remainder after division. For example, 10 % 3 is 1 because 10 divided by 3 is 3 with a remainder of 1. This is useful for checking if a number is even or odd:

number is 7
if number % 2 is 0:
  say "Even"
otherwise:
  say "Odd"         -- prints "Odd"

Negative numbers

x is -5
y is -x   -- y is 5 (flips the sign)

String concatenation with +

The + operator also works with text. It joins (concatenates) two strings together:

first is "Hello"
second is " World"
combined is first + second
say combined   -- Hello World
Tip: For combining text and variables, string interpolation ("Hello, {name}!") is usually cleaner than concatenation ("Hello, " + name + "!").
Try it yourself: Write a program that calculates the area of a rectangle. Create variables for width and height, multiply them, and print the result.

Comparisons

What are comparisons? Comparisons check the relationship between two values and give back a boolean (yes or no). They are how your program asks questions like "is the user old enough?" or "does this list contain the item?"

Quill comparisons read like English sentences:

QuillWhat it meansExample
x is greater than yIs x larger than y?10 is greater than 5 gives yes
x is less than yIs x smaller than y?3 is less than 7 gives yes
x is equal to yAre x and y the same?5 is equal to 5 gives yes
x is yShorthand for equalname is "Alice" gives yes if name is Alice
x is not yAre x and y different?5 is not 3 gives yes
list contains xIs x inside the list?colors contains "red"

Here are several examples:

age is 20

if age is greater than 18:
  say "You are an adult"

if age is equal to 20:
  say "You are exactly 20"

if age is not 21:
  say "You are not 21"

Using contains

The contains keyword checks whether a list includes a particular item, or whether a string includes a particular substring:

colors are ["red", "blue", "green"]

if colors contains "red":
  say "Red is in the list!"

if colors contains "purple":
  say "This will not print"

message is "Hello, World!"
if message contains "World":
  say "Found it!"
Try it yourself: Create a list of your favorite foods and use contains to check if "pizza" is in the list. Print a different message depending on the result.

Logic Operators

What are logic operators? Logic operators let you combine multiple conditions together. They answer questions like "is the user logged in AND an admin?" or "is it Saturday OR Sunday?"

OperatorWhat it meansGives yes when...
andBoth must be trueyes and yes
orAt least one must be trueyes or no, no or yes, yes or yes
notFlips the valuenot no

Simple example:

age is 25
name is "Alice"

if age is greater than 18 and name is "Alice":
  say "Hello, adult Alice!"

Using or:

day is "Saturday"

if day is "Saturday" or day is "Sunday":
  say "It's the weekend!"
otherwise:
  say "It's a weekday."

Using not:

isLoggedIn is no

if not isLoggedIn:
  say "Please log in first."

Real-world example: Checking if someone can rent a car (must be at least 21 and have a license):

age is 23
hasLicense is yes

if age is greater than 20 and hasLicense:
  say "You can rent a car!"
otherwise:
  say "Sorry, you cannot rent a car."
Tip: and is stricter than or. With and, ALL conditions must be true. With or, just ONE needs to be true.

Output

What is output? Output is how your program communicates with the outside world, specifically, by printing text to the screen. In Quill, you use the say keyword.

You can say anything, text, numbers, variables, expressions, even entire lists:

say "Hello!"             -- prints text
say 42                    -- prints a number
say 2 + 2                 -- prints 4 (evaluates the expression first)

name is "Alice"
say name                  -- prints Alice
say "Hi, {name}!"         -- prints Hi, Alice!

colors are ["red", "blue"]
say colors                -- prints ["red", "blue"]

Control Flow, if / otherwise

What is control flow? Control flow is how your program makes decisions. Without it, your code would run every line from top to bottom with no ability to react to different situations. With if and otherwise, your program can choose different paths based on conditions.

Think of it like a fork in the road: "IF it is raining, take an umbrella. OTHERWISE, wear sunglasses."

Basic if

The simplest form, do something only when a condition is true:

age is 20

if age is greater than 18:
  say "You are an adult"

Notice the colon : at the end of the if line, and that the next line is indented (shifted to the right with spaces). This indentation tells Quill which code belongs inside the if block.

if / otherwise

When you want to do one thing if the condition is true and a different thing if it is false:

temperature is 35

if temperature is greater than 30:
  say "It's hot outside!"
otherwise:
  say "It's not too hot."

Chained conditions: if / otherwise if / otherwise

When you have more than two possibilities, chain them together:

temp is 72

if temp is greater than 80:
  say "It's hot!"
otherwise if temp is greater than 60:
  say "It's nice"
otherwise if temp is greater than 40:
  say "It's chilly"
otherwise:
  say "It's cold!"

Quill checks each condition from top to bottom and runs the first one that is true. If none are true, it runs the otherwise block.

Nested ifs

You can put an if inside another if for more complex logic:

isLoggedIn is yes
isAdmin is no

if isLoggedIn:
  say "Welcome back!"
  if isAdmin:
    say "You have admin access."
  otherwise:
    say "You have regular access."
otherwise:
  say "Please log in."

Real-world example: grading system

score is 85

if score is greater than 89:
  say "Grade: A"
otherwise if score is greater than 79:
  say "Grade: B"
otherwise if score is greater than 69:
  say "Grade: C"
otherwise if score is greater than 59:
  say "Grade: D"
otherwise:
  say "Grade: F"
Common mistake: Forgetting the colon : at the end of the if, otherwise if, or otherwise line. Every condition line must end with a colon.
Try it yourself: Write a program that checks a variable called hour (a number from 0 to 23) and prints "Good morning", "Good afternoon", or "Good evening" depending on the time of day.

Loops

What is a loop? A loop repeats a block of code multiple times. Instead of writing the same thing over and over, you tell the computer "do this for each item" or "keep doing this while some condition is true."

Think of it like a factory assembly line: the same action is performed on each item that comes down the belt.

for each, iterating over a list

The for each loop goes through every item in a list, one at a time:

fruits are ["apple", "banana", "cherry"]

for each fruit in fruits:
  say "I like {fruit}"

This prints:

-- I like apple
-- I like banana
-- I like cherry

The variable fruit automatically takes on the value of each item as the loop runs. You can name it anything you want.

Use range() to loop over a sequence of numbers:

for each i in range(1, 6):
  say i   -- prints 1, 2, 3, 4, 5

while, repeating until something changes

A while loop keeps running as long as its condition is true:

count is 0

while count is less than 5:
  say count
  count is count + 1

This prints 0, 1, 2, 3, 4 and then stops because count is no longer less than 5.

break, stopping a loop early

Sometimes you want to stop a loop before it finishes naturally. The break keyword exits the loop immediately:

numbers are [3, 7, 2, 9, 1, 4]

for each n in numbers:
  if n is 9:
    say "Found 9!"
    break           -- stop searching, we found it
  say "Checking {n}..."

continue, skipping an item

The continue keyword skips the rest of the current iteration and jumps to the next one:

for each i in range(1, 6):
  if i is 3:
    continue       -- skip the number 3
  say i            -- prints 1, 2, 4, 5

Real-world example: finding a user

users are [
  {name: "Alice", role: "admin"},
  {name: "Bob", role: "user"},
  {name: "Charlie", role: "user"}
]

for each user in users:
  if user.role is "admin":
    say "Admin found: {user.name}"
    break

Real-world example: summing numbers

prices are [9.99, 24.50, 3.75, 15.00]
total is 0

for each price in prices:
  total is total + price

say "Total: ${total}"   -- Total: $53.24
Common mistake: infinite loops. A while loop that never has its condition become false will run forever and freeze your program. Always make sure something inside the loop changes the condition. For example, forgetting count is count + 1 inside a while count is less than 5: loop means count stays at 0 forever.
Try it yourself: Create a list of numbers and write a loop that prints only the even ones. (Hint: use % and continue.)

Functions

What is a function? A function is a reusable block of code that performs a specific task. Think of it like a recipe: you define it once with a name and instructions, then you can use (or "call") it whenever you need it.

Without functions, you would have to copy and paste the same code every time you wanted to use it. Functions let you write it once and reuse it everywhere.

Defining a function

In Quill, you define a function with the word to, followed by the function name and its parameters:

to greet name:
  say "Hello, {name}!"

Read it like English: "To greet (someone by) name: say Hello, name!"

Calling a function

To use a function you have defined, write its name followed by the values you want to pass in (called "arguments"), wrapped in parentheses:

greet("Alice")    -- Hello, Alice!
greet("Bob")      -- Hello, Bob!
greet("Charlie")  -- Hello, Charlie!

See how we wrote the greeting logic once but used it three times with different names? That is the power of functions.

Returning values with give back

Functions can calculate something and give the result back to you using give back:

to add a b:
  give back a + b

result is add(3, 4)
say result   -- 7

The function add takes two numbers, adds them, and gives the result back. You can store that result in a variable or use it directly.

Multiple parameters

Functions can accept any number of parameters, listed by name in the definition:

to fullName first last:
  give back "{first} {last}"

say fullName("John", "Doe")   -- John Doe

Functions with no parameters

to sayHello:
  say "Hello, World!"

sayHello()   -- Hello, World!

Real-world example: a simple calculator

to calculate a operation b:
  if operation is "add":
    give back a + b
  otherwise if operation is "subtract":
    give back a - b
  otherwise if operation is "multiply":
    give back a * b
  otherwise if operation is "divide":
    give back a / b
  otherwise:
    say "Unknown operation"
    give back nothing

say calculate(10, "add", 5)       -- 15
say calculate(20, "divide", 4)    -- 5
Common mistake: Forgetting to use give back in a function that should return a value. Without it, the function returns nothing by default.
Try it yourself: Write a function called isEven that takes a number and gives back yes if the number is even, or no if it is odd.

Lambda Functions

What is a lambda? A lambda is a small, anonymous (unnamed) function that you write in a single line. Think of it as a mini function, a quick shortcut when you need a simple operation and do not want to define a whole named function.

In Quill, you create a lambda with the with keyword:

double is with x: x * 2
say double(5)   -- 10
say double(3)   -- 6

This is equivalent to writing a full function:

-- This does the same thing as the lambda above
to double x:
  give back x * 2

Multiple parameters

add is with a, b: a + b
say add(3, 4)   -- 7

Using lambdas with list operations

Lambdas really shine when used with functions like map, filter, and reduce that transform lists:

numbers are [1, 2, 3, 4, 5]

-- Double every number
doubled is map(numbers, with x: x * 2)
say doubled   -- [2, 4, 6, 8, 10]

-- Keep only even numbers
evens is filter(numbers, with x: x % 2 is 0)
say evens   -- [2, 4]

-- Sum all numbers
total is reduce(numbers, with a, b: a + b, 0)
say total   -- 15
Tip: Use lambdas for short, simple operations. If your logic is more than one line, use a regular function with to instead, it will be easier to read.

Type Annotations

What are type annotations? Type annotations are optional labels you add to your function parameters and return values to specify what kind of data they expect. They are like signs on the doors of a building: "Numbers Only" or "Text Required."

They do not change how your code runs, but they help catch mistakes early and serve as built-in documentation.

Basic syntax

Use as after a parameter name to declare its type, and -> before the colon to declare the return type:

to add a as number, b as number -> number:
  give back a + b

This says: "The add function takes two numbers and gives back a number."

More examples

to greet name as text -> text:
  give back "Hello, {name}!"

to isAdult age as number -> boolean:
  give back age is greater than 17

Generic types with of

For lists, you can specify what type of items they contain using of:

to sum items as list of number -> number:
  total is 0
  for each item in items:
    total is total + item
  give back total

to first items as list of text -> text:
  give back items[0]

Checking types with quill check

Type annotations are optional and do not affect how your program runs. However, you can use the quill check command to analyze your code and catch potential type mismatches before you run it:

quill check myprogram.quill
Tip: You do not need to add types everywhere. Start without them while learning, and add them later when you want more safety and documentation in larger programs.

Pattern Matching

What is pattern matching? Pattern matching is like a smart switch statement. Instead of writing a long chain of if/otherwise if, you say "match this value" and list the possible cases. It is cleaner and easier to read when you are comparing one value against many options.

Think of it like a vending machine: you insert a code, and the machine matches it to the right product.

Basic syntax

status is "active"

match status:
  when "active":
    say "User is active"
  when "inactive":
    say "User is inactive"
  when "banned":
    say "User is banned"
  otherwise:
    say "Unknown status"

Matching numbers

code is 404

match code:
  when 200:
    say "OK"
  when 404:
    say "Not found"
  when 500:
    say "Server error"
  otherwise:
    say "Unexpected code: {code}"

When to use match vs if/otherwise

Use match when you are comparing one value against several specific options. Use if/otherwise when your conditions involve different variables, ranges, or complex expressions.

-- Good use of match: one value, many options
match dayOfWeek:
  when "Monday":
    say "Start of the work week"
  when "Friday":
    say "Almost weekend!"
  otherwise:
    say "Just another day"

-- Better as if/otherwise: complex conditions
if age is greater than 18 and hasLicense:
  say "Can drive"
Tip: The otherwise clause is optional but recommended. It catches any value you did not explicitly list, preventing unexpected behavior.

Error Handling

What are errors? Errors happen when something goes wrong while your program is running, like trying to read a file that does not exist, dividing by zero, or receiving unexpected data. Without error handling, your program would crash. With it, you can catch the problem and respond gracefully.

Think of it like a safety net under a tightrope walker. If they fall, the net catches them instead of letting disaster happen.

Basic syntax: try / if it fails

try:
  result is riskyOperation()
if it fails error:
  say "Something went wrong: {error}"

The code inside try: runs normally. If anything inside it causes an error, Quill immediately jumps to the if it fails block. The variable after if it fails (here called error) holds the error message.

Real-world example: loading a configuration file

try:
  data is readFile("config.json")
  say "Config loaded successfully"
if it fails error:
  say "Could not load config, using defaults"
  data is defaultConfig()

Real-world example: parsing user input

to parseAge input:
  try:
    age is toNumber(input)
    if age is less than 0:
      say "Age cannot be negative"
      give back nothing
    give back age
  if it fails err:
    say "'{input}' is not a valid number"
    give back nothing
Tip: Only wrap code in try if it might genuinely fail. Do not use it to hide bugs, fix the bug instead.

Raising Errors

Use raise to throw an error. This immediately stops execution and can be caught with try/if it fails.

-- Raise an error with a message
raise "something went wrong"

-- Raise inside a function
to divide a b:
  if b is 0:
    raise "cannot divide by zero"
  give back a / b

-- Catch a raised error
try:
  result is divide(10, 0)
if it fails err:
  say "Error: " + err
Tip: raise with a string creates a new Error object automatically. You can also raise a variable that already holds an error: raise err.

Classes

What is a class? A class is a blueprint for creating things (called "objects" or "instances"). Imagine you are building houses: the blueprint (class) describes what every house looks like, how many rooms, what color the door is, how to open it. Each actual house you build from that blueprint is an instance.

Defining a class

In Quill, you define a class with describe:

describe Dog:
  name is ""
  breed is ""
  age is 0

  to bark:
    say "{my.name} says: Woof!"

  to describe:
    say "{my.name} is a {my.age}-year-old {my.breed}"

Notice the my keyword, it refers to the specific instance's own properties. Think of it like saying "my own name" or "my own age."

Creating instances

Create a new instance of a class using new:

rex is new Dog()
rex.name is "Rex"
rex.breed is "German Shepherd"
rex.age is 5

rex.bark()       -- Rex says: Woof!
rex.describe()   -- Rex is a 5-year-old German Shepherd

Inheritance

A class can inherit (extend) another class, gaining all of its properties and methods. The child class can add new ones or override existing ones:

describe Animal:
  name is ""
  sound is ""

  to speak:
    say "{my.name} says {my.sound}"

describe Cat extends Animal:
  sound is "Meow"

  to purr:
    say "{my.name} purrs..."

kitty is new Cat()
kitty.name is "Whiskers"
kitty.speak()   -- Whiskers says Meow
kitty.purr()    -- Whiskers purrs...

Real-world example: a game character

describe Character:
  name is "Hero"
  health is 100
  attackPower is 10

  to attack target:
    say "{my.name} attacks {target.name} for {my.attackPower} damage!"
    target.health is target.health - my.attackPower

  to isAlive:
    give back my.health is greater than 0

  to status:
    say "{my.name}: {my.health} HP"

hero is new Character()
hero.name is "Aria"
hero.attackPower is 25

villain is new Character()
villain.name is "Goblin"
villain.health is 50

hero.attack(villain)    -- Aria attacks Goblin for 25 damage!
villain.status()        -- Goblin: 25 HP
Common mistake: Forgetting to use my. when accessing a class's own properties inside its methods. Without my., Quill looks for a regular variable instead of the instance's property.
Try it yourself: Create a BankAccount class with a balance property and deposit/withdraw methods.

Algebraic Data Types

What are algebraic data types? They let you define your own custom categories of data. Think of them like creating a custom label maker: you define a set of specific variants that a value can be, and nothing else.

For example, a traffic light can only be Red, Yellow, or Green, never Purple. Algebraic data types let you express exactly that.

Simple variants (enumerations)

The simplest form lists the possible values:

define Color:
  Red
  Green
  Blue

Now Color can only ever be Red, Green, or Blue.

Variants with data

Some variants can carry additional information using of:

define Shape:
  Circle of radius
  Rectangle of width, height
  Triangle of base, height

A Shape is always one of these three things, and each one carries exactly the data it needs.

Using with pattern matching

Algebraic types work naturally with match/when:

to area shape:
  match shape:
    when Circle radius:
      give back 3.14159 * radius * radius
    when Rectangle width height:
      give back width * height
    when Triangle base height:
      give back 0.5 * base * height

say area(Circle(5))              -- 78.53975
say area(Rectangle(4, 6))       -- 24

When to use define vs describe

Use define (algebraic types) when you have a fixed set of distinct categories, especially when different variants carry different data. Use describe (classes) when you need objects with shared behavior (methods), properties that change over time, and inheritance.

Try it yourself: Define a Result type with two variants: Success of value and Error of message. Write a function that returns one or the other and use match to handle both cases.

Pipe Operator

What is the pipe operator? The pipe operator (|) lets you chain transformations together in a readable left-to-right sequence. Think of it like an assembly line in a factory: raw material goes in one end, gets transformed at each station, and the finished product comes out the other end.

Basic usage

Instead of nesting function calls inside each other (which reads inside-out), you can pipe values through functions:

-- Without pipes (nested, reads inside-out):
result is trim(upper("  hello  "))

-- With pipes (reads left-to-right):
result is "  hello  " | upper | trim

Both produce the same result, but the pipe version reads in the order the operations happen: take the string, make it uppercase, then trim the whitespace.

Pipes with arguments

When a function takes extra arguments, the piped value becomes the first argument:

result is "hello world" | replace_text("world", "Quill")
say result   -- hello Quill

Chaining multiple steps

You can chain as many pipes as you need. For readability, you can put each step on its own line:

output is rawInput
  | trim
  | lower
  | replace_text(" ", "-")

-- If rawInput is "  Hello World  ", output is "hello-world"
Tip: Pipes are especially useful for data processing tasks where you transform a value through several steps. If you find yourself writing deeply nested function calls, consider using pipes instead.

Object Literals

What is an object? An object is a way to group related pieces of information together using named labels (keys). Think of it like a contact card: instead of having separate variables for name, phone, and email, you bundle them into one object.

Creating objects

person is {name: "Alice", age: 25, active: yes}

say person.name    -- Alice
say person.age     -- 25

Multi-line objects

For objects with many properties, spread them across multiple lines for readability:

config is {
  host: "localhost",
  port: 3000,
  debug: yes
}

Nested objects

Objects can contain other objects. This is useful for organizing related settings together:

config is {
  db: {
    host: "localhost",
    port: 5432
  },
  app: {
    name: "MyApp",
    debug: yes
  }
}

say config.db.host   -- localhost
say config.db.port   -- 5432
say config.app.name  -- MyApp

Modifying properties

You can change the value of a property after the object is created, just like reassigning a variable:

user is {name: "Alice", age: 30}
say user.name   -- Alice

-- Change the name
user.name is "Bob"
say user.name   -- Bob

-- Add a new property
user.email is "bob@example.com"
say user.email  -- bob@example.com

Accessing properties with bracket notation

Besides dot notation (user.name), you can also use bracket notation with a string. This is useful when the property name is stored in a variable or contains special characters:

user is {name: "Alice", age: 30}

-- These two are the same:
say user.name         -- Alice
say user["name"]      -- Alice

-- Bracket notation lets you use a variable as the key
field is "age"
say user[field]       -- 30

Checking if a key exists

To check whether an object has a particular property, you can check if the value is nothing:

user is {name: "Alice", age: 30}

if user.email is nothing:
  say "No email on file"

if user.name is not nothing:
  say "Name: {user.name}"
Try it yourself: Create an object representing your favorite book with properties for title, author, and pages. Then print each property with say.

Spread Operator

What is the spread operator? The spread operator (...) "unpacks" a list or object, spreading its contents into another list or object. Think of it like pouring the contents of one box into another.

Combining lists

first are [1, 2, 3]
second are [4, 5, 6]
combined are [...first, ...second]
say combined   -- [1, 2, 3, 4, 5, 6]

You can mix spread with individual items:

middle are [3, 4]
full are [1, 2, ...middle, 5, 6]
say full   -- [1, 2, 3, 4, 5, 6]

Merging objects

Spread also works for combining objects. If both objects have the same key, the later one wins:

defaults is {color: "blue", size: 10}
overrides is {size: 20, bold: yes}
merged is {...defaults, ...overrides}
say merged   -- {color: "blue", size: 20, bold: yes}

Notice that size is 20 (from overrides), not 10 (from defaults), because overrides was spread second.

Lists (Detailed)

We introduced lists earlier, but here is a deeper look at working with them.

Creating lists

numbers are [1, 2, 3, 4, 5]
mixed are ["hello", 42, yes]
empty are []

Accessing items by index

colors are ["red", "green", "blue"]
say colors[0]   -- red   (first item)
say colors[1]   -- green (second item)
say colors[2]   -- blue  (third item)

Getting the length

say length(colors)   -- 3

Iterating over a list

fruits are ["apple", "banana", "cherry"]

for each fruit in fruits:
  say "I like {fruit}"

Modifying elements

You can change any item in a list by assigning to its index:

colors are ["red", "green", "blue"]
colors[0] is "purple"
say colors   -- ["purple", "green", "blue"]

Adding items

Use push to add an item to the end of a list:

colors are ["red", "green"]
push(colors, "yellow")
say colors   -- ["red", "green", "yellow"]

Removing items

Use pop to remove the last item, or splice to remove items at a specific position:

colors are ["red", "green", "blue"]

-- Remove the last item
last is pop(colors)
say last     -- blue
say colors   -- ["red", "green"]

-- Remove 1 item at index 0
colors.splice(0, 1)
say colors   -- ["green"]

Finding items

Check if a list contains a specific item:

colors are ["red", "green", "blue"]

say includes(colors, "red")     -- true
say includes(colors, "yellow")  -- false
say indexOf(colors, "green")    -- 1

Using dot notation

say "hello".length   -- 5

Imports

What are imports? As your programs grow, you will want to split your code into multiple files and use code that other people have written. Imports let you bring code from other files into your current file.

Importing your own Quill files

If you have a file called helpers.quill with useful functions, you can import it:

use "helpers.quill"

This brings all definitions from helpers.quill into your current file.

Selective imports

If you only need specific functions from a file, use from:

from "math.quill" use add, subtract
from "utils.quill" use formatDate, parseJSON

This keeps your namespace clean by only bringing in what you actually need.

Importing npm packages

Since Quill compiles to JavaScript, you can use packages from the npm ecosystem:

use "express" as app
from "express" use Router, json
Imports are resolved at compile time and bring definitions from the imported file into scope.

Testing

Why does testing matter? Testing is how you make sure your code works correctly, not just right now, but also after you make changes in the future. Think of tests like a checklist: "Does the add function still return 5 when I give it 2 and 3?"

Writing tests

In Quill, you write tests with the test keyword followed by a description:

test "addition works correctly":
  result is add(2, 3)
  expect(result).toBe(5)

Multiple tests

to isEven n:
  give back n % 2 is 0

test "isEven returns yes for even numbers":
  expect(isEven(4)).toBe(yes)
  expect(isEven(0)).toBe(yes)

test "isEven returns no for odd numbers":
  expect(isEven(3)).toBe(no)
  expect(isEven(7)).toBe(no)

Running tests

Run your tests from the terminal:

quill test

Quill will find all test blocks in your project and run them, reporting which ones passed and which ones failed.

Tip: Write tests as you build your program, not after. It is much easier to catch bugs early when you test each function right after writing it.

Generics

What are generics? Generics let you write type annotations that work with any type of content. For example, instead of writing separate functions for "a list of numbers" and "a list of text," you can write one function that works with "a list of anything."

Use the of keyword to parameterize collection types:

to sum items as list of number -> number:
  total is 0
  for each item in items:
    total is total + item
  give back total

to first items as list of text -> text:
  give back items[0]

to hasAny items as list of boolean -> boolean:
  for each item in items:
    if item:
      give back yes
  give back no
Like other type annotations in Quill, generics are optional and serve as documentation. They do not change runtime behavior.

Reactive Web Components

Quill includes a Svelte-inspired component system that lets you build interactive web interfaces directly in .quill files. Components combine state, logic, and a declarative HTML-like template into a single, readable unit.

Core concepts

Example: Counter

component Counter:
  state count is 0

  to increment:
    count is count + 1

  to render:
    h1 "Count: {count}"
    button onClick=increment "Add one"

mount Counter to "#app"

Example: Todo App

component TodoApp:
  state todos is []
  state newTodo is ""

  to addTodo:
    if newTodo is not "":
      push(todos, {text: newTodo, done: no})
      newTodo is ""

  to render:
    h1 "My Todos"
    input bind:value=newTodo placeholder="Add a task..."
    button onClick=addTodo "Add"
    ul:
      for each todo in todos:
        li "{todo.text}"

mount TodoApp to "#app"

Building for the browser

To compile a component file into a JavaScript bundle that runs in a browser, use the --browser flag:

quill build app.quill --browser

This produces an optimised .js file you can load in an HTML page with a <script> tag.

Components are compiled away at build time — there is no virtual DOM or heavy runtime. The generated JavaScript updates only the parts of the page that actually changed.

Union & Nullable Types

Sometimes a value can be one of several types, or it might not exist at all. Quill supports union types and nullable types to express this clearly in your type annotations.

Union types

Use the pipe | character to say "this value can be one type or another":

to format value as number | text -> text:
  give back toText(value)

say format(42)       -- "42"
say format("hi")     -- "hi"

Nullable types

Prefix a type with ? to indicate the value might be nothing:

to findUser id as number -> ?object:
  if id is 0:
    give back nothing
  give back {name: "Alice", id: id}

Static checking

Run quill check to validate that union and nullable types are used consistently. The checker will warn you if you pass a text value to a function that only accepts number, or if you forget to handle a nothing case.

Union and nullable annotations are optional, but they help catch mistakes early — especially in larger codebases.

Source Maps

When Quill compiles your code to JavaScript, it also generates a source map file (.js.map) alongside the output. Source maps tell the browser how to translate the generated JavaScript back to your original .quill source.

This means you can open your browser's DevTools, set breakpoints, and step through your code while seeing the original Quill source — not the compiled JavaScript. No extra configuration is needed; modern browsers detect source maps automatically.

quill build app.quill --browser
-- produces app.js and app.js.map
Source maps are generated for all build targets (--browser, --standalone, etc.). You can disable them with the --no-sourcemap flag if you do not want to ship them in production.

Concurrency

Sometimes your program needs to do more than one thing at the same time. Maybe you want to download three files at once, or process data while also listening for user input. This is called concurrency, doing multiple things that overlap in time.

Think of it like cooking dinner: you can boil pasta on one burner while the sauce simmers on another. You are one person, but you are managing multiple things at once. That is concurrency.

New to concurrency? Don't worry, Quill makes it much simpler than most languages. Read each section below in order, and try the examples as you go. The key idea is: sometimes you want to start a job, keep doing other stuff, and come back for the result later.

spawn task, Running code in the background

Imagine you ask someone to go fetch a package from the post office while you keep working at your desk. You gave them a task, and you did not stop what you were doing. That is what spawn task does, it starts a piece of code running in the background so your program can keep going.

-- Start a background task
spawn task fetchData:
  result is fetch("https://api.example.com/data")
  say "Got the data!"

-- This runs immediately -- we didn't wait for fetchData
say "Still working while data is being fetched..."

The code inside spawn task fetchData: runs in the background. Your program continues to the next line without waiting. The task has a name (fetchData) so you can refer to it later if you need the result.

-- A practical example: processing items in the background
spawn task sendEmails:
  users is getUsers()
  for each user in users:
    sendEmail(user.email, "Welcome!")
  say "All emails sent."

spawn task generateReport:
  data is loadSalesData()
  report is buildReport(data)
  saveFile("report.pdf", report)
  say "Report saved."

say "Both tasks started! The program continues here."

await, Waiting for a task to finish

Sometimes you need the result of a background task before you can continue. The await keyword pauses your code until the task is done, like waiting at the door for your package to arrive before you can open it.

spawn task loadUser:
  give back fetch("https://api.example.com/user/1")

-- Wait for the task to finish and get the result
user is await loadUser
say "Hello, {user.name}!"
-- You can await multiple tasks one after another
spawn task getWeather:
  give back fetch("https://api.weather.com/today")

spawn task getNews:
  give back fetch("https://api.news.com/headlines")

weather is await getWeather
news is await getNews
say "Weather: {weather.summary}"
say "Top headline: {news[0].title}"

Real-world example: fetching data from an API

Here is a complete example that fetches JSON data from a web API and handles errors:

-- Fetch a list of users from an API
try:
  response is await fetch("https://jsonplaceholder.typicode.com/users")
  users is await response.json()

  for each user in users:
    say "{user.name} - {user.email}"
if it fails error:
  say "Failed to fetch users: {error}"

Running multiple async operations

When you need to fetch several pieces of data, you can use parallel: (covered below) or simply await them one by one:

-- Fetch user data and their posts at the same time
try:
  parallel:
    userRes is fetch("https://api.example.com/user/1")
    postsRes is fetch("https://api.example.com/user/1/posts")

  user is await userRes.json()
  posts is await postsRes.json()

  say "{user.name} has {length(posts)} posts"
if it fails error:
  say "Something went wrong: {error}"

parallel:, Doing multiple things at once

What if you want to start several tasks and wait for all of them to finish? The parallel: block does exactly that. Think of it like telling three friends to each go buy one item at the store, you wait until everyone comes back, then you have everything you need.

Under the hood, this works like JavaScript's Promise.all, all the tasks run at the same time, and the block finishes when the last one completes.

-- Fetch three APIs at the same time
parallel:
  users is fetch("https://api.example.com/users")
  posts is fetch("https://api.example.com/posts")
  comments is fetch("https://api.example.com/comments")

-- All three are done now -- use the results
say "Got {length(users)} users, {length(posts)} posts, {length(comments)} comments"
-- Process multiple files at the same time
parallel:
  image is resizeImage("photo.jpg", 800, 600)
  thumbnail is resizeImage("photo.jpg", 150, 150)
  metadata is readExif("photo.jpg")

saveFile("photo-resized.jpg", image)
saveFile("photo-thumb.jpg", thumbnail)
say "Photo taken on: {metadata.date}"

race:, First one wins

Sometimes you want to start several tasks but only care about whichever finishes first. The race: block does exactly that, like starting a race between runners, the first one to cross the finish line wins, and the rest are ignored.

This is useful for timeouts, trying multiple servers, or getting the fastest response.

-- Try two mirrors, use whichever responds first
result is race:
  fetch("https://mirror1.example.com/data")
  fetch("https://mirror2.example.com/data")

say "Got data from the fastest mirror!"
-- Implement a timeout: if the API doesn't respond in 5 seconds, give up
result is race:
  fetch("https://slow-api.example.com/data")
  delay(5000) then give back {error: "Timed out"}

if result.error:
  say "The request timed out."
otherwise:
  say "Got the data!"

Channels, Sending messages between tasks

Channels are like mailboxes that tasks use to send messages to each other. One task puts a message in the mailbox (send), and another task picks it up (receive). This is the safest way for tasks to communicate, instead of sharing variables directly, they pass messages through a channel.

The buffer number controls how many messages can sit in the mailbox before the sender has to wait. Think of it like a physical mailbox, if it can hold 5 letters, the mail carrier can drop off 5 without waiting. But if it is full, they have to wait until someone picks up a letter.

-- Create a channel that can hold 3 messages
channel messages with buffer 3

-- Task 1: produce messages
spawn task producer:
  send "Hello" to messages
  send "World" to messages
  send "Done" to messages

-- Task 2: consume messages
spawn task consumer:
  msg1 is receive from messages
  msg2 is receive from messages
  msg3 is receive from messages
  say "{msg1} {msg2} -- {msg3}"
  -- Output: Hello World -- Done
-- A worker pool pattern: multiple workers sharing a channel
channel jobs with buffer 10
channel results with buffer 10

-- Start 3 workers
repeat 3 times with i:
  spawn task worker:
    job is receive from jobs
    result is processJob(job)
    send result to results

-- Send jobs to the workers
for each item in workItems:
  send item to jobs

select:, Waiting on multiple channels

Sometimes a task needs to listen to more than one channel at a time, or give up after a certain amount of time. The select: block lets you do this, it waits and reacts to whichever channel has a message first. Think of it like standing in a room with multiple doors, you wait until any door opens, then go through that one.

channel orders with buffer 5
channel cancellations with buffer 5

-- Wait for either an order or a cancellation
select:
  when receive order from orders:
    say "New order: {order.item}"
    processOrder(order)
  when receive cancel from cancellations:
    say "Cancellation: {cancel.id}"
    refundOrder(cancel)
  after 30000:
    say "No activity for 30 seconds, checking again..."

The after clause is a timeout, if no channel has a message within that many milliseconds, the timeout code runs instead. This prevents your program from waiting forever.

-- A chat-like system with select
channel userMessages with buffer 10
channel systemAlerts with buffer 5

repeat forever:
  select:
    when receive msg from userMessages:
      say "[User] {msg.author}: {msg.text}"
    when receive alert from systemAlerts:
      say "[System] {alert.message}"
    after 60000:
      say "No messages for a minute. Quiet in here!"

Traits & Interfaces

What are traits? Traits are like contracts. They define a set of methods that a class must implement. If a class says it implements a trait, the compiler checks that all the required methods are present. This is useful for writing code that works with any type that has certain capabilities.

Think of traits like job requirements. A job posting says "must be able to write reports and give presentations." Any candidate who meets those requirements can fill the role, it does not matter who they are specifically.

Defining a trait

describe trait Printable:
  to display -> text

describe trait Serializable:
  to toJSON -> text
  to fromJSON data as text

Implementing a trait

describe User implements Printable, Serializable:
  name is ""

  to display -> text:
    give back my.name

  to toJSON -> text:
    give back "{\"name\": \"{my.name}\"}"

  to fromJSON data as text:
    say "parsing..."
If you forget to implement a method required by a trait, quill check will tell you exactly which method is missing and where.

Generic Constraints

Generics become even more powerful when you add constraints. The where keyword lets you say "this type parameter must implement a specific trait." This way, you can write generic functions that still know what operations are available.

Basic constraint

to sort items as list of T where T is Comparable:
  give back items | sort

to printAll items as list of T where T is Printable:
  for each item in items:
    say item.display()

Multiple constraints

to process items as list of T where T is Printable, T is Serializable:
  for each item in items:
    say item.display()
    say item.toJSON()

Destructuring

What is destructuring? Destructuring lets you pull values out of objects and lists in a single step, instead of accessing them one by one. It is like opening a package and labeling each item as you take it out.

Object destructuring

person is {name: "Alice", age: 30, city: "Portland"}

-- Pull out name and age
{name, age} is person
say name   -- "Alice"
say age    -- 30

-- Rest: grab some fields and collect the rest
{name, ...rest} is person
say rest   -- {age: 30, city: "Portland"}

List destructuring

items are [1, 2, 3, 4, 5]

-- Pull out the first two
[first, second] are items
say first    -- 1

-- Head and tail pattern
[head, ...tail] are items
say head     -- 1
say tail     -- [2, 3, 4, 5]

Nested destructuring

user is {name: "Bob", address: {city: "Seattle", zip: "98101"}}

{address: {city, zip}} is user
say city   -- "Seattle"
say zip    -- "98101"

In function parameters

to greet {name, age}:
  say "{name} is {age} years old"

greet({name: "Alice", age: 30})

Iterators & Generators

What are generators? A generator is a special function that can pause and produce values one at a time using yield. Instead of computing everything up front and returning a big list, generators produce values lazily, only when you ask for the next one.

Think of a generator like a card dealer. They do not throw the whole deck at you. They deal one card at a time, whenever you ask.

Basic generator

to countUp start:
  n is start
  loop:
    yield n
    n is n + 1

-- Produces 1, 2, 3, 4, 5, ... forever
counter is countUp(1)

Fibonacci generator

to fibonacci:
  a is 0
  b is 1
  loop:
    yield a
    temp is a
    a is b
    b is temp + b

-- Get the first 10 Fibonacci numbers
fibs is fibonacci() | take 10 | collect
say fibs   -- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Functions that use yield compile to JavaScript function* generators. The loop: keyword creates an infinite loop, it runs forever until something breaks out of it.

Lazy Evaluation

Lazy evaluation means values are only computed when they are actually needed. Combined with generators and the pipe operator, you can build powerful data processing pipelines that handle huge (even infinite) data sets efficiently.

Lazy chains

-- Process a million numbers without using a million items of memory
result is range(1, 1000000)
  | filter with n: n % 2 is 0
  | map_list with n: n * n
  | take 10
  | collect

say result   -- [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

Available lazy operations

These operations are lazy, they do not process items until you call collect, reduce, first, or last:

Terminal operations (these consume the iterator and produce a result):

Combining with generators

-- Find the first 5 prime Fibonacci numbers
result is fibonacci()
  | filter with n: isPrime(n)
  | take 5
  | collect

Type-Based Pattern Matching

Quill's match expression supports matching on the type of a value, not just its content. Combined with guard clauses, this gives you very expressive branching logic.

Matching by type

match value:
  when text t:
    say "Got text: {t}"
  when number n:
    say "Got number: {n}"
  when list l:
    say "Got a list with {length(l)} items"
  when nothing:
    say "Got nothing"

Guard clauses

Add an if condition after a when clause to refine the match:

match age:
  when n if n is less than 18:
    say "Minor"
  when n if n is less than 65:
    say "Adult"
  otherwise:
    say "Senior"

Exhaustive checking

When matching on algebraic data types, the compiler checks that you cover every variant. If you miss one, you get a clear error:

define Shape:
  Circle of radius
  Rectangle of width, height
  Triangle of base, height

match shape:
  when Circle r:
    say "Circle with radius {r}"
  when Rectangle w, h:
    say "Rectangle {w}x{h}"
  -- compiler error: non-exhaustive match, missing Triangle variant
Exhaustive checking only applies to algebraic types (created with define). For other values, use otherwise: as a catch-all.

Full-Stack in One File

Quill lets you write an entire application, server, database, and UI, in a single file. The compiler separates server-side and client-side code automatically. No configuration needed.

Server block

server:
    port is 3000
    route get "/api/users":
        users is await DB.find({})
        respond with users

Database block

database:
    connect "sqlite://app.db"
    model User:
        name as text
        email as text

UI + Mount

component App:
    state:
        users is []
    to render:
        h1 "My App"
        for each user in users:
            p "{user.name}"

mount App to "#app"

Running it

Just one command:

quill run app.quill

That gives you a running server, a connected database, and a rendered UI. No package.json, no node_modules, no webpack, no config files.

When to use full-stack mode: It is perfect for prototypes, internal tools, and small apps. For large projects, you can split into separate files and use Quill's import system.

Async Improvements

Quill's async system goes beyond basic await. You can cancel running tasks, iterate over async streams, and handle parallel operations where some may fail without crashing the whole group.

Cancellation

Sometimes you start a background task and then decide you no longer need it — maybe the user navigated away, or a timeout was reached. Use cancel to stop a running task.

spawn task fetcher:
  data is await fetchJSON("/slow-endpoint")

-- Changed our mind, stop the task
cancel fetcher

Async iteration

When data arrives in chunks (like reading a large file or receiving messages from a server), use for await each to process each piece as it arrives.

for await each chunk in stream:
  say "Got: {chunk}"

Parallel settled

A regular parallel: block stops everything if one task fails. With parallel settled:, every task runs to completion. Each result is either a Success() or an Error(), so you can handle failures individually.

parallel settled:
  a is await fetchJSON("/a")
  b is await fetchJSON("/b")

-- a and b are each Success() or Error(), never throws
if a.ok:
  say a.value

Error Propagation

Quill has a Result type that represents either a successful value or an error. This lets you handle errors explicitly without exceptions, similar to Rust's Result type but with Quill's English-like syntax.

Success and Error

Functions return Success(value) when things go well and Error(message) when something goes wrong.

to loadUser id as number -> Result of User:
  if id is 0:
    give back Error("not found")
  give back Success({name: "Alice", id: id})

The ? operator

Writing error checks for every function call gets repetitive. The ? operator automatically propagates errors — if the result is an Error, the current function returns that error immediately. If it is a Success, the inner value is unwrapped.

to loadProfile id as number -> Result of Profile:
  user is loadUser(id)?          -- returns Error early if loadUser fails
  give back Success(user.profile)

The try expression

Use try as an expression to provide a fallback value when something fails.

name is try loadUser(42) otherwise "anonymous"

Advanced Destructuring

You already know how to destructure variables ({name, age} is person). Quill also lets you destructure directly inside loops and match expressions, which keeps your code short and readable.

In loops

Instead of pulling out fields inside the loop body, destructure right in the for each line.

for each {name, age} in users:
  say "{name} is {age} years old"

In match expressions

Match on the shape of an object by destructuring inside when clauses.

match response:
  when {status: 200, body}:
    say body
  when {status: 404}:
    say "Not found"
  otherwise:
    say "Unexpected status"

Computed Properties

Sometimes you do not know the name of an object key until your program is running. Computed properties let you use a variable or expression as the key by wrapping it in square brackets.

key is "color"
obj is {[key]: "blue", size: 10}
say obj.color   -- "blue"

This is useful when building objects dynamically — for example, collecting form fields into an object where the field names come from user input.

Tagged Templates

Tagged templates let you prefix a string with a function name. The function receives the string parts and interpolated values separately, so it can process them — for example, escaping SQL parameters or sanitizing HTML.

-- SQL with automatic parameter escaping
result is query`SELECT * FROM users WHERE age > {minAge}`

-- HTML with sanitization
page is html`<h1>{title}</h1><p>{body}</p>`

-- CSS with scoping
styles is css`
  .card:
    padding is {spacing}px
    color is {theme.text}
`

The built-in tags query, html, and css are part of the standard library. You can also write your own tag functions.

Visibility

By default, all fields and methods in a class are accessible from anywhere. Use private and public to control what outside code can see and use.

describe Account:
  private balance is 0
  public name is ""

  public to deposit amount:
    my.balance is my.balance + amount

  private to audit:
    say "Balance: {my.balance}"

Private fields compile to JavaScript # private fields, so they are truly private at runtime — not just a naming convention.

Enums with Methods

Algebraic data types can have methods attached to them. This is useful when your enum variants carry values and you want to define behavior that works across all variants.

define HttpStatus:
  OK is 200
  NotFound is 404
  ServerError is 500

  to isSuccess:
    give back my.value is less than 400

  to toMessage:
    match my:
      when OK: give back "OK"
      when NotFound: give back "Not Found"
      when ServerError: give back "Internal Server Error"

Call methods on an enum value just like on an object: HttpStatus.OK.isSuccess() returns yes.

Type Utilities

Type utilities let you create new types by transforming existing ones. Instead of redefining a type from scratch, you can take an existing type and make all fields optional, pick a subset of fields, or mark everything as read-only.

type PartialUser is Partial of User          -- all fields optional
type UserName is Pick of User, "name" | "email"  -- only name and email
type SafeUser is Omit of User, "password"     -- everything except password
type ScoreMap is Record of text, number       -- string keys, number values
type Frozen is Readonly of Config             -- no field can be reassigned
type Complete is Required of PartialUser      -- all fields required

These work the same way as TypeScript utility types, but with Quill's English-like syntax.

Decorators

Decorators let you attach extra behavior to a function or class without changing its code. Write the decorator name with an @ symbol on the line above. Think of them as labels that tell Quill to wrap the function with additional logic.

-- This route requires authentication and is rate-limited
@authenticated
@rateLimit(100)
to getUsers request:
  give back DB.find({})

-- This function logs every call
@log
to processOrder order:
  say "Processing {order.id}"

Decorators compile to standard JavaScript decorator syntax. You can use built-in decorators like @log, @authenticated, and @rateLimit, or write your own.

WebSockets

WebSockets allow real-time, two-way communication between a server and its clients. Quill provides a dedicated websocket block that handles connection lifecycle and broadcasting.

websocket "/chat":
  on connect client:
    say "{client.id} joined"

  on message client, data:
    broadcast data

  on disconnect client:
    say "{client.id} left"

The broadcast function sends a message to every connected client. You can also send to a specific client with client.send(data).

Testing Mocks

Mocks let you replace a real function with a fake one during tests. This is useful when you do not want your tests to make real HTTP requests or talk to a real database.

mock fetchJSON with url:
  give back {name: "Mock User"}

test "fetches user":
  user is await getUser(1)
  expect user.name is "Mock User"
  expect fetchJSON was called 1 times

After the test finishes, the original function is automatically restored. You can also check how many times a mocked function was called and with what arguments.

Variable Scoping

What is scoping? Scoping is about where a variable can be seen and used in your program. A variable created inside a block (like a function, loop, or if statement) only exists inside that block. Once the block ends, the variable disappears.

Think of it like rooms in a house. A lamp in the bedroom can only light up the bedroom. Code in other rooms cannot see it.

Function scope

Variables created inside a function (including its parameters) are local to that function. Code outside the function cannot see them:

to greet name:
  message is "Hello, {name}!"
  say message

greet("Alice")   -- Hello, Alice!
-- say message     -- ERROR: message is not defined here
-- say name        -- ERROR: name is not defined here

Block scope (if / for / while)

Variables defined inside if, for each, or while blocks are scoped to that block:

score is 85

if score is greater than 80:
  grade is "A"
  say grade   -- A (works fine inside the block)

-- grade is not accessible here outside the if block

Outer variables are visible inside blocks

Code inside a block can see and modify variables from the enclosing scope:

total is 0

for each n in [10, 20, 30]:
  total is total + n   -- modifying the outer variable

say total   -- 60
Rule of thumb: Define variables in the smallest scope you need them. If a variable is only used inside a loop, create it inside the loop. This keeps your code clean and avoids accidental name collisions.

User Input

How do I get input from the user? Since Quill compiles to JavaScript, you can read input from the terminal using Node.js built-in modules. Quill provides a convenient way to do this.

Reading a line of input

Use the ask function to prompt the user and get their response:

name is await ask("What is your name? ")
say "Hello, {name}!"

Using input in a loop

say "Type 'quit' to exit."
running is yes

while running:
  answer is await ask("Enter a command: ")
  if answer is "quit":
    say "Goodbye!"
    running is no
  otherwise:
    say "You typed: {answer}"

Using readline directly

If you need more control, you can use the Node.js readline module directly since Quill compiles to JavaScript:

use "readline" as rl

reader is rl.createInterface({
  input: process.stdin,
  output: process.stdout
})

reader.question("How old are you? ", with answer:
  say "You are {answer} years old!"
  reader.close()
)
Note: The ask function requires await because reading user input is an asynchronous operation. The program pauses until the user types something and presses Enter.

Built-in Template Engine

Quill ships with a built-in template engine for generating HTML programmatically. Instead of concatenating strings or using a separate templating library, you can build type-safe HTML with plain Quill functions.

Core functions

Shorthand tag builders

For convenience, Quill provides shorthand functions for common HTML elements. Each works exactly like tag() but with the element name pre-filled:

-- All of these shorthand builders are available:
div()  span()  h1()  h2()  h3()  p()  a()  img()
ul()  ol()  li()  form()  input()  button()  label()  textarea()
table()  tr()  th()  td()
nav()  header()  footer()  section()  article()  main()

Example

items is ["Apple", "Banana", "Cherry"]
html is page({
  title: "My Page",
  body: tag("div", {class: "container"}, [
    tag("h1", "Hello from Quill!"),
    tag("ul", eachItem(items, with item: tag("li", item)))
  ])
})
say html
Tip: Combine the template engine with reactive components for server-rendered pages that hydrate on the client, or use it standalone for static HTML generation.

Utility CSS Framework

Quill includes a built-in Tailwind-style utility CSS framework. When you use components, the framework is auto-injected — no extra setup required. Apply utility classes via the className attribute.

Available categories

Example

component Dashboard:
  to render:
    div className "container mx-auto p-8":
      h1 className "text-3xl font-bold mb-6": "Dashboard"
      div className "grid grid-cols-3 gap-4":
        div className "card shadow-md hover:shadow-lg transition":
          h3 className "font-semibold mb-2": "Users"
          p className "text-2xl text-blue-600": "1,234"
      button className "btn btn-primary mt-4": "View All"
Note: The utility CSS is automatically included when your file contains components. No import or configuration is needed. The classes follow Tailwind CSS naming conventions, so existing Tailwind knowledge transfers directly.

Next Steps

Congratulations! You have learned the fundamentals of Quill. Here is where to go from here:

The best way to learn is to build something. Start small, a calculator, a to-do list, a guessing game, and gradually tackle bigger projects. Every expert was once a beginner. Happy coding!