Adding a Language
This guide walks through adding a new language to Quilt. It uses the pattern established by the existing concrete languages (rust, python, html, wgsl, zsh, bash, nix, text).
1. Decide the role
A language can be:
- Host language — the ground language in a
.quiltfile. Requires both aLanguageand aMetaLanguageimplementation. Example: Rust, Python. - Target language — only appears inside
lang↖…↗quotes. Requires only aLanguage. Example: HTML, WGSL.
2. Grammar
If the language needs tree-sitter parsing (recommended):
- Fork or adapt an existing tree-sitter grammar for the language.
- Add a hole node to the grammar. Rust uses
{}and Python uses__HOLE__as hole tokens; your grammar needs a token that is syntactically valid in expression/statement position and uniquely recognizable. - Host the grammar as its own repo under the QuiltLang GitHub org, following the same structure as the existing forks (Cargo bindings in
bindings/rust/). - Add it to
[workspace.dependencies]in the rootCargo.tomlas a git dependency, like the existingtree-sitter-*forks.
If the language doesn't need tree-sitter, implement Language directly (see the bootstrap/lang.rs approach).
3. Create the language module
Create quilt/src/langs/<lang>/:
langs/<lang>/
├── mod.rs
├── lang.rs # Language implementation
└── meta.rs # MetaLanguage (only for host languages)
lang.rs
For a tree-sitter-backed language, use TSLanguage<YourProvider>:
pub struct YourProvider(tree_sitter::Parser);
impl Default for YourProvider {
fn default() -> Self {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_your_lang::LANGUAGE.into()).unwrap();
Self(parser)
}
}
impl TSProvider for YourProvider {
fn parser(&mut self) -> &mut tree_sitter::Parser { &mut self.0 }
fn hole_str(&self) -> &'static str {
"__HOLE__" // must match your grammar's hole token
}
fn unwrap(&self, qterm: QTerm, ikind: Option<InnerKind>) -> (QTerm, InnerKind) {
// Strip the root "source_file" wrapper and infer whether the
// content is an expression, statement, or file.
// See rust/lang.rs or python/lang.rs for reference.
todo!()
}
fn arity(&self, tag: &str) -> Arity {
// Return Variadic for nodes that accept arbitrarily many children
// (e.g. block-like constructs). Default: Unknown.
Arity::Unknown
}
fn hashbang(&self) -> Option<&'static str> {
// Only needed for host languages that are runnable via `quilt`.
None
}
}
pub type YourLanguage = TSLanguage<YourProvider>;
pub type DynYourLanguage = DynTSLanguage<YourProvider>;
meta.rs (host languages only)
Implement MetaLanguage. The three required methods build code that reconstructs the term at runtime:
#[derive(Default)]
pub struct YourMetaLanguage;
impl MetaLanguage for YourMetaLanguage {
fn expand_quote(&self, lang1, tag, i, lang2, qterm, cmds) -> Result<Arc<QTerm>> {
// Build code: quote(tag, i, lang2, <term>, [..cmds..])
Ok(build_quote_code(tag, i, lang2, qterm, cmds))
}
fn expand_unquote(&self, lang1, tag, i, lang2, qterm, cmds) -> Result<Arc<QTerm>> {
Ok(build_unquote_code(tag, i, lang2, qterm, cmds))
}
fn expand_tuple(&self, lang1, tag, qterms, cmds, arity) -> Result<Arc<QTerm>> {
Ok(if arity == Arity::Variadic {
build_variadic_block(tag, cmds, qterms)
} else {
build_tuple_code(tag, cmds, qterms)
})
}
// Override operator spellings if needed:
fn lift_str(&self) -> &'static str { "your_lift()" }
fn reduce_str(&self) -> &'static str { "your_reduce()" }
}
You can reuse langs::rust::ops or langs::python::ops helpers if your meta-language emits Rust or Python constructor code.
4. Add a Cargo feature
In the root Cargo.toml:
[workspace.dependencies]
tree-sitter-your-lang = { git = "https://github.com/QuiltLang/tree-sitter-your-lang.git" }
In quilt/Cargo.toml (tree-sitter languages must also enable parse):
[features]
your_lang = ["dep:tree-sitter-your-lang", "parse"]
default = [..., "your_lang"]
[dependencies]
tree-sitter-your-lang = { workspace = true, optional = true }
5. Register in langs/mod.rs
#[cfg(feature = "your_lang")]
pub mod your_lang;
6. Add to Omni
Edit langs/omni.rs:
In OmniLanguages:
pub struct OmniLanguages {
// …existing fields…
#[cfg(feature = "your_lang")]
your_lang: OmniLanguage,
}
impl Default for OmniLanguages {
fn default() -> Self {
Self {
// …
#[cfg(feature = "your_lang")]
your_lang: OmniLanguage::YourLang(YourLanguage::default()),
}
}
}
In Languages for OmniLanguages: add a "your_lang" match arm to get and get_mut.
If it's a host language, in OmniMetaLanguages: add a field and a match arm for the MetaLanguage.
In OmniLanguage enum: add YourLang(YourLanguage) variant and implement the Language delegation methods.
In dict_omni_language(): add ret.add_lang("your_lang", …) (and ret.add_meta for host languages).
7. Update DynOmniLanguages (optional)
If you want the language accessible via dict_omni_language() (needed for tests and the LSP), also add it to DynOmniLanguages and DynOmniMetaLanguages in omni.rs.
8. Add to the LSP adapters (for host languages)
If the language will be a host ground language in the LSP:
- Add a
LanguageAdapterimpl inquilt-lsp/src/adapters.rsdefining:comment_syntax()— how to write placeholder comments.splice_block()— the placeholder for a quote in the projected document.wrap_fragment(body)— how to wrap a quoted fragment so the downstream server tokenizes it.
- Add a
MetaLanguageAdapterimpl for the projection logic. - Register the new adapter in
language_adapter()andmeta_adapter().
9. Write tests
Add tests alongside your implementation:
cargo test -p quiltlang your_lang
At minimum test round-tripping: parse a fragment, serialize it back, and check it matches the input.