Python Bindings
Crate: quilt-python/ — quilt_python (Cargo), quilt (Python import name)
The quilt_python crate exposes the core Quilt IR to Python via PyO3. It is the runtime that expanded .py.quilt files import:
from quilt import *
Building
build-py # maturin build --release, then installs the module into the package dir
This builds a wheel with maturin and extracts the native extension module into quilt-python/quilt/ as _quilt.abi3.so. The module name is quilt._quilt; the quilt/__init__.py re-exports everything for from quilt import *.
The crate targets ABI3 (abi3-py38) so one build works for CPython ≥ 3.8.
quilt automatically sets PYTHONPATH to include the quilt-python/ directory.
API
The Python API mirrors the Rust QTermBuilder fluent interface.
Term constructors
leaf(tag, code) # childless node with Write(code) command
sym(s) # leaf where tag == code
tb(tag) # begin a Tuple builder
qb(tag, i, lang) # begin a Quote builder
ub(tag, i, lang) # begin an Unquote builder
quote(tag, i, lang, term, cmds) # construct a Quote QTerm directly
unquote(tag, i, lang, term, cmds) # construct an Unquote QTerm directly
Builder fluent methods
Called on a builder object returned by tb/qb/ub:
builder.w("text") # Write
builder.n() # NewLine
builder.p(" ") # Push indent prefix
builder.x() # Pop prefix
builder.c(child) # insert child (no & — Python doesn't have borrows)
builder.e(child) # emit (like .c but semantically "emit into variadic")
builder.b() # build → QTerm
Command constructors
write("text") # StrCmd::Write
push(" ") # StrCmd::Push
NL # StrCmd::NewLine constant
POP # StrCmd::Pop constant
HOLE # CmdOrHole::Hole constant
cmd(strcmd) # CmdOrHole::Cmd(strcmd)
These three are constants here and functions (
NL(),POP(),HOLE()) in thequilt-wasmruntime that.ts.quiltfiles target — the one shape change to watch for when porting a metaprogram between the two hosts. See Relation to the wasm runtime.
QTerm methods
term.coparse() # serialize to a string
str(term) # the same text — `__str__` is coparse
term.qlift() # lift to builder code (like Rust's QLift trait)
Other functions
name("ident") # identifier node (⟨N⟩ operator)
qlift(value) # lift int/str/QTerm to a Python term (↑ into a py quote)
qlift_html(value) # lift int/str/QTerm to HTML text, entity-escaped (↑ into an html quote)
Serialization
term.postcard_bytes() # -> bytes
from_postcard_bytes(data) # -> QTerm
The wire format of the py↓ / rs↓ protocol: a Rust meta-program runs a
generated Python stage, and the stage returns its QTerm as these bytes for the
Rust side to decode. You rarely call them by hand — quilt generates the script
that does (langs/rust/ops.rs).
postcard is a positional format, so the encoding is a contract between two
separately-built runtimes rather than an implementation detail: a reordered
QTerm field or a serde(skip) on span moves it, and both ends move with it,
so an encode-then-decode round-trip cannot notice. The shared corpus therefore
pins the bytes for a handful of terms (postcard on a case in
conformance/runtime/cases.json) and checks them from both runtimes. If you
change the QTerm layout deliberately, re-pin every one of them in the same
commit.
Running generated code
Helpers (in quilt/__init__.py) for evaluating a term's coparse() output:
reduce(term) # the `↓` operator: run the term's code and return the value.
# Glyph-aware (expands a still-Quilt fragment via `quilt` first)
# and block-aware (runs leading statements, returns the trailing
# expression — the block value).
run(term) # run a generated stage and return its whole namespace (dict),
# e.g. when you want several bindings, not one value.
expand(src) # expand Quilt source text to plain Python by shelling out to
# `quilt expand` (no compilation). run() is expand() + exec.
reduce_rs(term) # the `rs↓` operator: evaluate a term as Rust via rust-script.
reduce/.↓ evaluates a term to a value, and does so across stages:
- Glyph-aware — if the source is still Quilt (it contains glyphs, e.g. a
generated fragment that itself quotes) it is expanded via the prebuilt
quiltbinary first (found via$QUILT, set automatically when launched byquilt, elsePATH). - Block-aware — a generated stage is usually a statement sequence, not a
bare expression.
↓runs the leading statements and returns the value of the trailing expression — None if it ends in a statement — the block-value semantics of Rust ({ …; expr }), Lispbegin, Ruby, etc. So a stage ending in its result expression reduces straight to that result.examples/staged_pow.py.quiltends Stage 2 with itsmake_scaledgenerator and reduces it withstage2.↓.
run() remains for when you want the stage's whole namespace rather than a
single value.
How expanded .py.quilt code looks
When the Quilt engine expands a Python .quilt file, each ↖…↗ quote becomes a call that constructs a QTerm:
# Source:
expr = ↖1 + 2↗
# Expanded (approximately):
expr = (tb("binary_expression")
.c(leaf("integer_literal", "1"))
.w(" + ")
.c(leaf("integer_literal", "2"))
.b())
And an unquote ↙x↘ becomes a .c(x) splice at the corresponding hole position.
Example: hello.py.quilt
#!/usr/bin/env quilt
from quilt import *
expr = ↖1 + 2↗
print("expr =", expr.coparse())
ten = ↖10↗
scaled = ↖↙ten↘ * 100↗
print("scaled =", scaled.coparse())
Run with quilt examples/hello.py.quilt.
Relation to Rust bindings
The Python builder API is intentionally parallel to the Rust QTermBuilder API. The main differences are:
| Rust | Python |
|---|---|
.c(&child) | .c(child) |
Arc<QTerm> | opaque QTerm object |
↑ is postfix: x.↑ → x.qlift() (QLift trait) | ↑ is prefix: ↑(x) → free qlift(x) function |
Variadic block uses imperative b_ | Variadic block uses fluent .e(child) chain |
Relation to the wasm runtime
quilt-wasm is the third published runtime — the one expanded .ts.quilt
files target — and is a closer match to this one: same fluent builder, same
by-value .c(child), same prefix qlift(x), same .e(child) variadic chain.
All three are held together by a shared corpus
(conformance/runtime/cases.json), so a drift between them is a test failure.
One difference remains:
| Python | quilt-wasm |
|---|---|
NL, POP, HOLE are constants | NL(), POP(), HOLE() are functions |
cmd(NL) # quilt-python
[cmd(write("[")), HOLE, cmd(write("]"))]
cmd(NL()) // quilt-wasm
[cmd(write("[")), HOLE(), cmd(write("]"))]
Expanded code is always right for its own target, since each meta-language
emits its own runtime's spelling; the cost falls on a human porting a
metaprogram across hosts. wasm-bindgen cannot export a module-scope constant
at all — #[wasm_bindgen] on a const is a compile error, and only functions,
structs, enums and impls reach JS — and a shared singleton would in any case be
consumed by its first use, because wasm-bindgen moves struct values passed in
arrays. Issue #167 weighed the alternative (a hand-maintained JS entry point
wrapping wasm-pack's output) and kept the divergence; quilt-wasm/README.md
records the full reasoning.