Skip to main content

Language Traits

Files: quilt/src/lang.rs, quilt/src/meta.rs

Two trait families are the extension points for adding language support to Quilt.

Language / LanguagePost — parsing

pub trait Language {
type Post: LanguagePost;

fn parse_pre(&mut self, ikind: Option<InnerKind>, code: &[FlatNode]) -> Result<Self::Post>;
// Convenience wrappers:
fn parse(&mut self, code: &[FlatNode]) -> Result<Arc<QTerm>>;
fn parse_expr/stmt/file/auto(&mut self, code: &[FlatNode]) -> Result<Arc<QTerm>>;
fn arity(&self, tag: &str) -> Arity;
fn typ(&self, tag: &str) -> InnerKind;
fn hashbang(&self) -> Option<&'static str>;
}

pub trait LanguagePost: Debug {
fn holes(&self) -> &[Hole];
fn parse_post(&self, plugs: &[Arc<QTerm>]) -> Result<Arc<QTerm>>;
}

FlatNode

pub enum FlatNode<'a> {
Hole, // a placeholder for a child term
Str(&'a str), // ordinary source text
NewLine,
}

The multi-language engine collects the Node list for a given fragment and builds a Vec<FlatNode> where each Node::Quote/Node::Unquote becomes a FlatNode::Hole. This flat list is passed to parse_pre.

Hole

pub struct Hole {
pub otag: Box<str>, // the tree-sitter tag of the hole in the outer language
pub prefix: Box<[Box<str>]>, // accumulated indentation prefixes
}

parse_pre must return one Hole per FlatNode::Hole in its input (in order). The Multi engine uses hole.otag to label the Quote/Unquote node in the output tree, and hole.prefix to strip indentation from nested content.

InnerKind

pub enum InnerKind { Expr, Stmt, File }

Passed as a hint to parse_pre. Some parsers use it to try specific grammar entry points instead of guessing. None means auto-detect.

Arity

pub enum Arity { Unknown, Const(u8), Variadic }

Returned by Language::arity(tag). Variadic tags (e.g. Rust's "block" and "source_file") tell the expander to use expand_tuple in variadic mode, generating an imperative builder block rather than a single tb(..).c(..)..b() call.

hashbang

Returns the shebang line used to run the expanded file as a script, e.g.:

  • Rust: "#!/usr/bin/env rust-script"
  • Python: "#!/usr/bin/env python3"

quilt uses this to determine which runner to invoke.


MetaLanguage — expansion

pub trait MetaLanguage {
fn expand_quote(
&self, lang1: &str, tag: &str, i: Index,
lang2: &str, qterm: &Arc<QTerm>, cmds: &[CmdOrHole],
) -> Result<Arc<QTerm>>;

fn expand_unquote(
&self, lang1: &str, tag: &str, i: Index,
lang2: &str, qterm: &Arc<QTerm>, cmds: &[CmdOrHole],
) -> Result<Arc<QTerm>>;

fn expand_tuple(
&self, lang1: &str, tag: &str,
qterms: &[Arc<QTerm>], cmds: &[CmdOrHole], arity: Arity,
) -> Result<Arc<QTerm>>;

fn wrap_child(&self, qterm: Arc<QTerm>, okind: OuterKind) -> Result<Arc<QTerm>>;

// Operator spellings — the string that ↑ / ↓ / ← / ⟨T⟩ / ⟨N⟩ expand to:
fn lift_str(&self) -> &'static str { LIFT }
fn reduce_str(&self) -> &'static str { REDUCE }
fn emit_str(&self) -> &'static str { EMIT }
fn type_str(&self) -> &'static str { TYPE }
fn name_str(&self) -> &'static str { NAME }
}

How expansion works

When the expander encounters a Quote { lang2, … } at Sky depth, it calls:

meta.expand_quote(lang1, tag, index, lang2, expanded_term, cmds)

This should return a QTerm whose code, when executed, constructs the quoted term at runtime. For the Rust meta-language this means returning code like:

quote("expression_statement", 1, "rs", <term>, &[...cmds...])

Similarly for expand_unquote and expand_tuple.

OuterKind

pub enum OuterKind { None, Emit, Splice }

Passed to wrap_child:

  • None — no wrapping needed.
  • Emit — the child is a quote inside a variadic context; wrap as .emit(&mut b_) (Rust) or .e(child) (Python).
  • Splice — the child is a statement-valued unquote that should be inlined; wrap as a bare statement (Rust: add ;).

Operator spelling constants

The five special glyphs in .quilt source are translated to language-specific strings:

GlyphDefault constantRust override
"__LIFT__""qlift()"
"__REDUCE__""reduce()"
"__EMIT__"(same)"
⟨T⟩"__TYPE__""Arc<QTerm>" (via bootstrap)
⟨N⟩"__NAME__""name()"

The Bootstrap meta-language has its own spellings for bootstrapping meta.rs.


TSLanguage — the tree-sitter helper

File: quilt/src/treesitter.rs

Most concrete language implementations use TSLanguage<P: TSProvider> rather than implementing Language directly. A TSProvider supplies:

pub trait TSProvider {
fn parser(&mut self) -> &mut tree_sitter::Parser;
fn hole_str(&self) -> &'static str; // placeholder: "{}" or "__HOLE__"
fn unwrap(&self, qterm: QTerm, ikind: Option<InnerKind>) -> (QTerm, InnerKind);
fn arity(&self, tag: &str) -> Arity { Arity::Unknown }
fn typ(&self, tag: &str) -> InnerKind { InnerKind::File }
fn hashbang(&self) -> Option<&'static str> { None }
}

TSLanguage::parse_pre substitutes each FlatNode::Hole with hole_str(), feeds the resulting string to the tree-sitter parser, finds the placeholder tokens by their text content, and records the hole positions. parse_post then replaces those positions with real Arc<QTerm> children.

unwrap is called on the top-level tree-sitter node to strip the outer source_file wrapper and infer whether the fragment is an expression or statement.

DynTSLanguage<P> is a newtype that wraps TSLanguage<P> and boxes its Post type so it can be stored as Box<dyn Language<Post = Box<dyn LanguagePost>>> in a DictMulti.