Guide

Structuring a program

A Quillo program is a main .qwe file and the files it uses. There is no package file and no build step, and each file says what it takes from the others. As a program grows, a few rules decide how well your editor and quillo check keep up with it. They are all on this page, and each one was tried before it was written down.

One main file, and the files it uses

A small shop, split the way larger programs are:

shop/  main.qwe            the program: its database, its parts, its routes  lib/    money.qwe         pure helpers: pages and the server both use them    orders.qwe        server fns and queries  ui/    kit.qwe           views every page uses: the frame, buttons, cards    cart.qwe          components  migrations/         001_orders.sql, ...  data/               the database: make the folder, db.open does not  public/             files served as they are
// main.qwedb.open("data/shop.db") | unwrap      // stops here if data/ is missingdb.migrate("migrations")use "lib/money.qwe"use "lib/orders.qwe"use "ui/kit.qwe"use "ui/cart.qwe"route GET / = <Frame title="Shop"><Cart/></Frame>
// lib/money.qwe: pure, so pages and the server both use it.fn price(cents: int) -> str = `€${floor(cents / 100)}.${slice(str(100 + cents % 100), 1)}`// lib/orders.qweserver fn load_orders() -> list with db = db.query("select * from orders order by id")// ui/kit.qweview Frame(title: str, children) =    <html><head><meta charset="utf-8"/><title>{title}</title></head><body>{children}</body></html>// ui/cart.qweimport lib/money.qwe use priceimport lib/orders.qwe use load_orderscomponent Cart() {    state orders = load_orders()    let total = orders | map((o) => o.cents) | sum()    <section>        <ul>{for o in orders => <li key={o.id}>{o.item}: {price(o.cents)}</li>}</ul>        <p>Total {price(total)}</p>    </section>}

And the table the queries read, migrations/001_orders.sql, with two rows to see:

create table orders (id integer primary key, item text not null, cents integer not null);insert into orders (item, cents) values ('Mug', 1250), ('Tea', 499);

quillo run shop/main.qwe then serves / with Mug: €12.50, Tea: €4.99 and Total €17.49. Without | unwrap, a missing data/ folder would be one line in the log (db: unable to open database file) and the program would go on with SQLite in memory, losing its orders at each restart.

What the order decides

lib/early.qwe:1: error: `LATER` is used before it is defined (line 1)

The order that works: pure helpers and constants first, then data (server fns, queries, rules), then views and pages, as in main.qwe above.

What a file sees

A file sees its own names, the builtins, and what it takes:

use lib/money.qwe                             // every public name of the fileimport lib/money.qwe use price, tax           // only theseimport lib/money.qwe use price as shop_price  // this one, under another name here

A type taken as another, import lib/geo.qwe use Point as Pt, is Pt in annotations, patterns and type definitions too. as cannot take the name of one of the program's types, and a name no type can be written as (P, one letter) is warned of with one that can (Pt).

The file comes first, and the first word says what kind of thing it is. There are three reasons for that order:

  1. Mental model. The line reads in the order things happen: what kind of code this is, where it is, then which of its names you want. import is Quillo or Rust on the server; client, preload and preconnect are the browser. A glance at the start of a line tells you which side of the program you are on.
  2. Editor. Once the file is written, the editor knows its names and offers them as you type, leaving out those you took. With the names first, there is nothing to offer until the path is there.
  3. Readability. Lines line up by where things come from, so a file's sources read down the left, and names no longer sit after a long address at the far right.

0.6's use price from lib/money.qwe still works, with a warning, and quillo fix turns it around. A later release removes it.

What a file takes is for that file, and is not passed on: when ui/cart.qwe takes from lib/money.qwe, a file that uses ui/cart.qwe does not get price with it.

For now a name a file did not take still works, with a warning, so a program can move a file at a time. A later release makes it an error.

A return at the top level of a file ends the file there: nothing after it is defined, and quillo check says so at the first line after it (this never runs: the `return` at line 2 leaves the file first, so nothing below it is defined).

ui/cart.qwe:7: warning: `price` comes from lib/money.qwe, which this file does not take: add `import lib/money.qwe use price` (`quillo fix` writes it)

quillo fix writes those lines, in every file of the program, after a byte-order mark and with the file's own line ends (CRLF in a CRLF file). Run it once for each program, a service's or a mounted one's too:

quillo fix main.qwe

Two files may define one name. A total in lib/cart.qwe and a total in lib/invoice.qwe are two functions, and a file's own name comes first. A file that uses both whole and calls total is told to choose:

main.qwe:3: error: `total` comes from lib/cart.qwe and lib/invoice.qwe: take the one you mean: import lib/cart.qwe use total
Defined twiceWhat happens
Function, view or component, in one fileAn error
Function, view, component or top-level let, in two filesAllowed: two names, and each file takes the one it means
server fn, type, service, in one file or twoAn error, naming both places: a server fn's name is its address, and types and services are the program's
let x twice in one fileAllowed: the second gives the variable again
Route (same method, path)Not reported: the first one loaded answers

A type and a service are seen in every file without a use. There are no prefixes yet: a file writes import lib/auth.qwe use login and calls login(), not auth.login().

A file's own helpers need no use at all: mark them private, and only that file sees them.

// lib/cart.qweprivate let TAX = 0.12private fn total(lines: list) = sum(lines | map((l) => l.price * l.qty))public fn cart_total(lines: list) = total(lines) * (1 + TAX)

private goes before a fn, view, component or top-level let, at a file's top level; public, which says the default, is optional. Another file that calls total is told so, and editors offer it only in its own file:

main.qwe:3: error: `total` is private to lib/cart.qwe: only that file sees it (make it `public` there, or call something public)

A server fn cannot be private (it is an address on the server anyone can call: keep the work in a private fn it calls), and neither can a type yet. A private function given to schedule or every keeps its job under a name of its own, made from the file's path from the main file's folder, so copies of a program started from different folders (quillo run current/main.qwe, a service under --split) share the job. A top-level let named like a builtin (let last = ...) or a module (let json = request.json, let date = ...) hides it in every file of the program: name such values otherwise (body, created).

When a part needs names of its own, give it a boundary:

UseWhenIts names
use lib/x.qweParts of one programSeen by the files that take them
private fn, private letA file's own helpersThat file's only
mount /blog = "blog.qwe"Another program under a pathIts own globals and its own database (Mounting programs)
service billing = "billing.qwe"A part others call, here or on its own serverCalled as billing.invoice(...) (Microservices)

What the browser gets

A component is compiled to JavaScript with the functions it calls, from whichever files they are in: Cart takes price along to the browser. Those functions must only compute. A function that reaches for the database, http, env or files cannot be compiled for the browser, and the program refuses to start, naming it:

quillo: `order_count` (compiled for the browser): `order_count` declares `with db`, which the browser cannot do; make it a `server fn`

quillo check does not catch this yet when a helper is between the component and the database; quillo run does, before it serves anything.

A library that runs in the browser starts with client, whether it comes from a CDN or from your own assets: the path says which. preload or preconnect in its place says how the page gets ready for it, and a long address may leave its use to the next line. Each file that calls it writes the line, and the page loads it once (JavaScript libraries):

preload "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"    use Chartclient /assets/leaflet.js use L

A file taken whole does not pass its browser names on: a name used through it is warned of, with the client line to write. One name from two scripts (or a script and a module's export) is an error, in quillo check and as the program starts, since the page would have only the last: take one as another name.

So keep the helpers components use in files of their own (lib/money.qwe above), apart from the files that hold server fns and queries. It is then clear at a glance what runs where, and rules written once check forms in the browser and saves on the server (Validation).

What the editor needs

Open any file, and the language server finds the main file it belongs to: it looks in the file's folder and up to three folders above it, stopping at the project's root (a folder with .git or Cargo.toml), for a .qwe file that uses it, directly or through other files. It then checks the whole program from there, including what is unsaved in other open files. That is how ui/cart.qwe, opened alone, knows price: there are no false "unknown name" errors, Go to Definition opens lib/money.qwe, and hover shows the signature.

Three things keep that working:

Types where files meet

A function other files call is the program's interface, so give it types. Hover then shows what it takes and gives, and every call to it is checked:

fn price(cents: int) -> str = ...       // hover: fn price(cents: int) -> str
ui/cart.qwe:7: error: argument `cents` of `price` must be int, got str

Without types, fn price(cents) = ... takes anything and the mistake waits for a page to show it. See Records for the shapes of maps that cross between files.

Editors

VS Code. The Quillo extension (quillolang/editors) brings diagnostics as you type, hover, Go to Definition across used files, completion (your names, builtins with their signatures, the members of db., http., channel. and the other modules, effects after with), an outline, snippets, and a debugger (F5, with breakpoints, stepping and the variables). Run this file starts it as quillo run file.qwe --watch, which reloads it when a file changes (an option of quillo run; see the options). It finds quillo in the quillo.path setting, then in the workspace's target/release or target/debug, then on your PATH. Errors on code that runs fine usually mean an old quillo was found: quillo --version names its commit.

cargo install --git https://github.com/quillolang/quillo quillo   # puts quillo on your PATHgit clone https://github.com/quillolang/editors.git && cd editors/vscodenpm install && npm run package    # builds quillo-lang-<version>.vsixcode --install-extension quillo-lang-*.vsix

Other editors run quillo lsp, over stdin and stdout, for *.qwe files. It reads the editor's text, not the file on disk, and gives diagnostics under every URI a file is open by, so a project opened through a link gets them. In Neovim:

vim.filetype.add({ extension = { qwe = "quillo" } })vim.api.nvim_create_autocmd("FileType", {  pattern = "quillo",  callback = function() vim.lsp.start({ name = "quillo", cmd = { "quillo", "lsp" } }) end,})

A .qwer file gets the Rust compiler's errors only when it is saved, in a folder the editor says it trusts (since 0.7), as compiling it runs its crates' build scripts. The VS Code extension says so for a trusted workspace; in Neovim, add init_options = { trusted = true } to vim.lsp.start for folders you trust.

The extension's README has Helix's settings, and quillo dap for debuggers in other editors.

While it runs, quillo run main.qwe --watch reloads the program when it or any file it uses changes, and refreshes the pages you have open. Started, or reloaded, while a file is broken or missing, it watches every file that load read or tried, and the folders of those not there, so fixing or making one loads the program; each error is said once.

Other files in a program

FolderHoldsUsed by
lib/*.qwerRust, used like a .qwe fileRust
migrations/SQL files, run in order, oncedb.migrate("migrations") (Database)
locales/en.json, fr.json, ...translations("locales")
public/Images, fonts, CSSroute GET /assets/*path = files("public", path)

files serves a folder by the visitor's path, but not .env, .git/, the program's own files or a SQLite database, under any name. file("public/" + path) serves whatever file it is given, .env and main.qwe included (this guide's example until 0.7), and quillo check warns about it. See files and file.

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

Thread 0 posts

On Structuring a program: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.