Guide
Rust
A .qwer or .rs (since 0.7) file is Rust, and a program uses it as it uses a .qwe file: its
top-level pub fns are the program's functions. The two extensions are the same to
Quillo: .rs is the one editors, rustfmt and clippy know, and .qwer is the catalog's
and quillo install's. A .rs file is the program's only when a use or import names
it (use "imaging.rs", import imaging.rs use brightness), so a crate's own src/*.rs
beside the program is left alone. Here is imaging.qwer:
//! effects: fs//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["jpeg"] }/// How bright a picture is, from 0 to 255.pub fn brightness(path: &str) -> Result<f64, String> { let img = image::open(path).map_err(|e| e.to_string())?.to_luma8(); Ok(img.pixels().map(|p| p.0[0] as f64).sum::<f64>() / img.len() as f64)}use "imaging.qwer"route GET /photos/:name = { brightness: brightness(`photos/${name}.jpg`)? }- Values cross as Quillo has them:
i64and the other integers up to 64 bits areint(au64orusizeanswer pasti64's range is a runtime error;i128cannot cross);f64andf32arefloat;bool;String,&strandchararestr(acharargument must be one character);Vec<T>,VecDeque<T>and&[T]are lists;HashMap<String, T>andBTreeMap<String, T>are maps, nested too;Option<T>is the value ornull;serde_json::Valueis anything. A function returningResult<T, E>givesT, or an error value withE's message, so?and??work on it.()isnull. References (&String,&Vec<T>) are fine as parameters. Au64pasti64's range inside a returnedserde_json::Valueis a runtime error that names where it is (.id,[1].n), not a float that has lost digits, and one returned alone names its function. NaN and the infinities cross to anf64and back as they are, at any depth, a map of one key too (they arrived asnull); aserde_json::Valueargument that holds one is refused by the argument's name, as JSON has none. An error value given to a Rust function, at the top or inside a list or map, is given back, and the function does not run. - An argument that does not fit is refused with the parameter's name:
`small`: argument `n` does not fit in i32: 5000000000. Values cross at most 126 levels deep: an argument nested deeper is refused (`deep`: argument `v` is nested too deeply: a plugin takes values at most 126 levels deep), and an answer nested deeper is an error that says so (`pr_deep` (w.qwer) gave back a value nested too deeply). - What does not cross: structs (even with
#[derive(Deserialize)]: take aserde_json::Valueandserde_json::from_valueit inside), tuples, sets,Box,Rc,Arc,PathBuf,Cow. A decimal arrives as its text, so anf64parameter refuses one. Bytes are a list of numbers each way (a 10 MB file is about 40 MB of JSON): pass a file's path, or base64 text, instead. - The rest of the file is ordinary Rust: private functions, structs,
impls, modules,use. Apub fnthat is generic (a written lifetime counts:fn f<'a>),async, takes&mut, or has a type that cannot cross, as a fixed-size array cannot, is not exported, andquillo checksays why. So is one with a reference inside another type,Option<&str>,Vec<&str>or&[&str], which the glue cannot build: the message names the owned type to take (Option<String>,Vec<String>).&strand-> &'static strare fine, as are paths (std::string::String,std::collections::HashMap) and comments in a signature. A raw name,pub fn r#type, is the functiontype, and the functions after it are read; a function named like a Quillo keyword (view,match,reply) is not exported. A#[cfg(...)]on apub fnis kept: called where it was left out of the build, it says the function is not in this build. Another crate's type namedValuepasses the check and fails the build. A signature may name what the file's ownuselines bring in: afteruse serde_json::Value;anduse std::collections::HashMap;,pub fn role(auth: Value)andpub fn total(m: HashMap<String, i64>)build as written in full do. - The
//! [dependencies]lines are the file's crates, as inCargo.toml: any crate from crates.io or git. They come first, after other//!lines if any; the first line that is not//!ends them. Only that table is read (no[features]table, no[patch], nobuild.rsof the file's own; a crate's own build scripts do run).serde_jsonis always there. C libraries come through-syscrates. The//!lines may follow//comments, blank lines, block comments and#![...]attributes at the top of the file. //! effects: fs, http, a header line, says what the file's functions do, in the words ofwith(db,http,fs,env,time,random,net;db.analyticsisdb), or//! effects: none. It is what lets awithfunction call them: a function declaredwith fsmay callbrightness, one declaredwith timemay not. A file without the line may do anything: its functions are refused inside anywith, and anywhere under a policy that narrows effects (see Effects). A word that is not an effect is said byquillo checkat its line. Quillo cannot see what Rust does, so the line is the author's claim, read as a promise: write what the code really reaches, and review it in a plugin from elsewhere.- Taking names.
import redis.qwer use cache_get, cache_settakes only those functions;asgives one another name,import redis.qwer use cache_get as redis_get, and it is not there under its own. A name the file does not export is an error that names the nearest it does.import redis.qwer use *, oruse redis.qwer, takes them all. Each file takes what it calls, as with a.qwefile (see Files of a program). - A Rust file's names are the file's that takes them, as a
.qwefile's are: two Rust files exporting one name are two functions, and a file that takes both is told to choose, naming the files (comes from a.rs and b.rs: ... import a.rs use who); one function taken by several files is one function. A name from a Rust file taken whole gives way to the file's own definitions and to names it takes by name. The fix a warning suggests (import x.qwer use len as x_len) parses. - A
pub fnnamed like a builtin (len,get) hides the builtin in the files that take it, andquillo checkwarns of it at the.qwerfile's line, with how to take it under another name:import x.qwer use len as x_len(FEEDBACK27.md, Q-326). quillo checkreads the functions' types from the Rust, without building anything: calls are checked, and editors show them on hover. The language server also runscargo checkon each save of a.qwerfile, and shows Rust's errors in the editor, in a folder the editor says it trusts:cargo checkruns the dependencies' build scripts and macros, so a cloned repository's.qwercould run code as it was opened (since 0.7). The VS Code extension says so from its Workspace Trust; another editor gives the serverinitializationOptions: { "trusted": true }for folders of your own. They run on the server only (in services and mounted programs too); components call them through aserver fn.- A Rust function cannot call back into Quillo: no
db,publishorenv()(it readsstd::envitself). It may start threads, keep a runtime of its own in astaticandblock_onit, and hold a connection pool in astatic.
How it runs:
- The Rust files the program uses that are not built yet are built before its first
statement runs, all in one cargo call over a workspace, so cargo builds them side by
side (
quillo: building ./a.rs, ./b.qwer together); a file that fails there is built again alone by itsuse, which says why. usebuilds the file withcargo($CARGO, or the one on thePATH), in a crate of its own in Quillo's cache:QUILLO_NATIVE_CACHE, else$XDG_CACHE_HOME/quillo/native, else~/.cache/quillo/native, else the system's temporary folder. The crates' builds are shared there, and nothing old is removed (it grows: clear it now and then). The library is kept in.quillo/native/next to the file, named by the file's whole name (sox.rsandx.qwerside by side keep one each), the glue's version, and a hash of the file's text, the glue and the processor: the next start loads it without building. An older Quillo's libraries, and their lock lines, are kept, so going back to it still runs. A build takes seconds the first time, more with large crates, and needs crates.io (or cargo's own offline mode).quillo build app.qwebuilds every Rust file the program and its mounted programs use (not those of its services: build each service's file too), and prints where each library went; it also takes a.qweror.rsfile itself. Deploy.quillo/native/with the program, and the server needs no Rust: it loads what was built for that content, on the same processor and system (there is no cross-compiling: build on Linux for Linux), by the same Quillo glue. A Quillo whose glue changed builds again, and needs cargo then: runquillo buildonce as part of the deploy that brings it..quillo/plugins.lock(since 0.7), beside.quillo/native/, holds each library's file name and SHA-256, a line each, written by every build:quillo build, ausethat built,--watch. Deploy it with.quillo/native/. A library is loaded only when it is the one built: one whose hash differs, or that has no line, is built again where cargo may run (quillo: libhi-687dd0b24f29e5cf.dylib is not the library that was built (its SHA-256 differs from .quillo/plugins.lock's): building it again), and refused where it may not. A folder built before there were locks has none: its libraries load as before, with a warning once (.quillo/plugins.lock is not there, so the libraries in .quillo/native are loaded unchecked, as before; quillo build writes it). A build writes the lock, but a library already there is not built again, so remove.quillo/native/and runquillo buildto get one. Whoever can write both the library and the lock can still run code in the server.--no-buildnever runs cargo, so no crate's build script runs on the server: a library is loaded only as the lock names it, and a file not built is an error (./hi.qwer is not built, and --no-build says not to build it here: build it with quillo build and bring .quillo along); one built only by an older Quillo is said to be (./hi.qwer was built by an older Quillo, whose libraries this one cannot load, and --no-build says not to build it here: run `quillo build` once where Rust is, and bring .quillo along). Under it, a folder with no lock loads nothing.--splitpasses it to its services, and a policy's[plugins] build = falseis the same (see The policy).- A policy's
[plugins] allownames the plugins a program mayuse:"csv"any version,"csv@1"or"csv@1.4.2"by the file's//! version:line (1takes1.4.2, not10.0). A plugin it does not name is refused before anything is built:the plugin `pdf` 2.0.1 is not in the plugins /etc/quillo/policy.toml allows: csv@1, qr. It names plugins by their file's name and header; it does not prove their code. - A call runs off the event loop, without the lock Quillo code holds, on the threads
that also wait for databases (
--workers): a slow function holds up no one, and calls from several requests run on several cores at once; many long calls at once can keep database waits waiting. Inside a SQLite transaction a call holds the lock, and every request waits. Nothing times a call out or cancels it. The values cross as JSON, which suits most calls; a list of millions of numbers takes a while to cross. - A panic is a runtime error in the request that called the function (not an error
value:
??does not catch it), and the server goes on.std::process::exit, an abort, or a stack overflow ends the whole server. Arguments of the wrong type are refused before the function runs. A stop (SIGTERM) while a request is in a call is logged as a stop,quillo: stopped with GET /slow?ms=14000 still running, not as a bug in Quillo. - When the program is started with
--watchor--reload(quillo run main.qwe --watch), a Rust file that changes is built again and the program reloaded with it. A build that fails prints the compiler's errors, and the program goes on as it was. Requests in progress finish with the version they started with: libraries stay loaded, the old ones for the life of the process, with theirstatics (an old pool stays open). - Rust can do anything the server can, so Rust files are the developers', like the
rest of the program: never let a program's users write them (they write sandboxed
Quillo with
run_script).
Plugins from the catalog
quillolang.org/plugins has .qwer files ready to use,
each with a version, an example, and a test run before it is published. quillo install
puts them beside the program (its options are below):
quillo install csv qr jwt # csv.qwer, qr.qwer and jwt.qwer, into this folderquillo install csv --example # and csv.qwe, its example programquillo install --update # the plugins here that the catalog has newerquillo install --list pdf # what the catalog has, matching "pdf"- It reads the catalog (
https://quillolang.org/plugins/index.json;--registryorQUILLO_REGISTRYfor another), downloads each file, and writes it only when its SHA-256 and the//! version:line of its header match the catalog's. A plugin that fails leaves the others going, and the exit status is 1. - A file already there is left alone when it is the same, replaced when the catalog's
version is newer, and kept when it is newer or the same version changed by hand:
--forcereplaces it.--dir DIRinstalls elsewhere.name@1.2.0asks for that version, and fails when the catalog has another (it keeps only the newest). - Then
use csv.qwer, or only what you need,import csv use csv_parse, csv_write: the first run builds it with cargo, as any.qwerfile. - A catalog plugin is called inside a
withfunction only once its header has a//! effects:line, as any Rust file (see Rust). The catalog's files do not have it yet: add it to your copy after reading what the plugin does. installchecks a file against the catalog it came from, which a changed server could change too: the catalog's entries are not signed yet. A host can still list the plugins and versions it takes, in a policy's[plugins] allow.
Other stores
db is for SQL databases. For another kind (Redis, MongoDB, a graph database), use its
crate in a .qwer file: its functions are the driver, and the program calls them like
any other. A count of visits in Redis:
//! effects: db, env//! [dependencies]//! redis = "0.27"use std::sync::OnceLock;/// The client, made on the first call (`REDIS_URL`, or Redis on this machine).fn client() -> Result<&'static redis::Client, String> { static C: OnceLock<redis::Client> = OnceLock::new(); if let Some(c) = C.get() { return Ok(c); } let url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".into()); let c = redis::Client::open(url).map_err(|e| e.to_string())?; Ok(C.get_or_init(|| c))}/// The value at `key`, or null.pub fn cache_get(key: &str) -> Result<Option<String>, String> { let mut con = client()?.get_connection().map_err(|e| e.to_string())?; redis::cmd("GET").arg(key).query(&mut con).map_err(|e| e.to_string())}/// Adds one to the number at `key`, and gives the new number.pub fn cache_incr(key: &str) -> Result<i64, String> { let mut con = client()?.get_connection().map_err(|e| e.to_string())?; redis::cmd("INCR").arg(key).query(&mut con).map_err(|e| e.to_string())}use "redis.qwer"server fn visits(page: str) -> int { watch(`visits:${page}`) // live pages showing this follow the topic int(cache_get(`visits:${page}`)? ?? "0")}server fn visit(page: str) { let n = cache_incr(`visits:${page}`)? publish(`visits:${page}`) // and refresh when it changes n}component Visits(page: str) { state n = live(visits, page) <button onclick={() => visit(page)}>{n} visits</button>}- Values cross as JSON, so a connection cannot be a Quillo value: the Rust keeps it in a
static, and the library stays loaded between calls. (This one connects on each call; a pool, such as the crate'sr2d2feature, keeps connections open.) - Calls run off the event loop, so a slow store holds up no one, and a store that is down gives an error value.
- What
dbdoes by itself is the program's to do here: live pages follow what a loaderwatches and a writepublishes, as above, and writes are not part ofdb.transaction. //! effects: db, envmakescache_getandcache_incrthedbeffect (andenv, forREDIS_URL), so a function declaredwith db, envmay call them and one declaredwith httpmay not.
Thread 0 posts
On Rust: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.
Edit this page on GitHub · made from the user guide · questions on Discord or r/quillo