4 min read 834 words Updated Sep 04, 2026 Created Sep 04, 2026

Plan: extract blr::quote module; slim df to its function API

Status: complete (2026-07-09; deviations noted inline below)
Date: 2026-07-09

Context / current state

Today the quote "node" type family lives in three places, all under df:

LocationContent
stdlib/wit/df.witbinaryoperator, binary, function, node + fromcsv/filter/show
stdlib/src/df.rsbindgen for world df; Host impl; build_node_expr (node → datafusion Expr)
stdlib/src/df.blrpub extern type decls for all four + the three extern fns

The compiler side already has the quote concept separated:
lang/src/runtime/quote.rs registers std::quote::to_nodes (a compile-time
rewrite — see lang/src/compiler/mantle/quote.rs), and its node_type() doc
comment literally says it "mirrors stdlib/wit/df.wit". The guest never imports
quote at runtime: to_nodes(…) is rewritten into an inline node-list literal,
so only std::df appears as a component import (std::Xblr:X/X-interface,
lang/src/compiler/mod.rs:371).

Goal: move the four node types into a new blr:quote WIT package / stdlib
module, leaving df with only data-frame + fromcsv/filter/show.

Phase 1 — WIT (stdlib/wit/)

  1. New quote.wit:

    package blr:quote {
        interface quote-interface {
            variant binaryoperator {
                addition,
                concat,
                division,
                equal,
                greater-than,
                less-than,
                multiplication,
                subtraction,
            }
            record binary {
                left: s64,
                op: binaryoperator,
                right: s64,
            }
            record function {
                body: s64,
                param: string,
            }
            variant node {
                binary(binary),
                function(function),
                integer(s64),
                select(string),
            }
        }
    
        world quote {
            import blr:quote/quote-interface;
            export blr:quote/quote-interface;
        }
    }
    

    The interface must be named quote-interface so std::quote
    blr:quote/quote-interface matches blr_path_to_component_path.

  2. Edit df.wit: delete the four type defs; add
    use blr:quote/quote-interface.{binaryoperator, binary, function, node};
    inside df-interface (needed only because filter's signature mentions
    node). Keep data-frame, fromcsv, filter, show unchanged.

  3. Verify bindgen tolerates cross-package use from files in the shared
    wit/ dir (multiple packages already coexist there — df/fmt/math/sums/std
    — so this should be fine; cargo build -p blr-stdlib is the check).
    Fallback if it fails: nest quote.wit under a deps/ layout and pass
    path:/deps: to the bindgen! invocations.

Phase 2 — stdlib Rust (stdlib/src/)

  1. New quote.rs:
    • wasmtime::component::bindgen!({ world: "quote" }) — types only, no host
      functions (confirm the generated code compiles with zero fns; likely no
      Host trait / add_to_linker is emitted).
    • Move build_node_expr + its #[cfg(test)] module from df.rs here
      (import Node/Binaryoperator from the generated blr::quote path).
      The df module keeps only its function API.
  2. lib.rs: add pub mod quote;.
  3. df.rs: replace
    use crate::df::blr::df::df_interface::{Binaryoperator, Node}; with the
    blr::quote path (or a crate::quote re-export); filter delegates to
    crate::quote::build_node_expr. Add a with: clause to the df bindgen only
    if the cross-package types land somewhere inconvenient — inspect generated
    paths first.
  4. lang/src/exec.rs: no change expected — the guest imports no
    blr:quote functions (nodes are compile-time), so nothing new to link.
    Confirm via the integration tests.

Phase 3 — blr-language module (stdlib/src/*.blr)

  1. New quote.blr: the four pub extern type decls moved from df.blr
    (this also makes source(db, "std::quote") resolve if a program imports
    the module).
  2. df.blr: keep only the three pub extern fn decls.
    ⚠️ filter(frame: DataFrame, predicate: list<node>) now needs a
    cross-module type reference, and the grammar only supports unqualified
    TypeExpr::Alias (lang/src/compiler/air/parser.lalrpop:279):
    • Subtask: add a qualified type form, e.g.
      <Identifier> "::" <Identifier> => TypeExpr::Qualified { module, alias },
      then resolve it in crust::lower by looking up type aliases registered
      under the imported module's path (imported extern type aliases already
      flow into item sources via item_source_for_mod,
      lang/src/compiler/mod.rs:242) and through the external_type
      conversion.
    • Fallback (if that is too invasive for v1): leave a temporary
      pub extern type node = ... in df.blr with a TODO — ABI-safe since the
      component model matches structurally — and land the grammar work
      separately.
  3. lang/src/runtime/quote.rs: no code change (type shape is identical);
    update the node_type() doc comment to "mirrors stdlib/wit/quote.wit".
    QUOTE_OPS = "std::quote" already maps to the right component path.

Phase 4 — Tests & verification

  1. Existing integration tests must pass unchanged: df.blr, df_filter.blr
    (they only import std::df and call unqualified to_nodes).
  2. New integration test: import std::quote + import std::df using
    filter(fromcsv(…), to_nodes(:(…))) (with a .out file) — exercises the
    quote module as an importable, zero-function component module (validates
    nucleus emission for a types-only module).
  3. Move the build_predicate_logical_expr unit test to stdlib/src/quote.rs.
  4. Gate: cargo fmt, cargo clippy -- -D warnings, cargo test (workspace).

Suggested order

WIT (1) → stdlib Rust (2) → build+test stdlib → grammar subtask + .blr files
(3) → full test suite (4). Each phase leaves the tree compilable; the only
phase with genuine new language work is 3's qualified-type-name subtask, which
has a documented fallback so the rest can land first.

Risks

  • WIT cross-package use in one wit/ dir — low risk (multi-package dir
    already used); quick build check in Phase 1.
  • world: "quote" with no functions — bindgen output shape unverified;
    Phase 2 step 1 may end up as a bindgen against the interface or a types-only
    re-export instead.
  • Qualified type names — the one substantive compiler change (parser +
    crust alias resolution + external_type mapping); the fallback keeps the
    refactor shippable without it.