BPDL Specification

BMUX Plugin Definition Language (BPDL) — Specification

BPDL is the typed interface description language BMUX plugins use to declare their public contracts. It is a small, purpose-built DSL designed to be easy to write simple plugins with while scaling to robust, typed, multi-plugin ecosystems.
This document is the normative grammar and semantics.

File structure

A BPDL source file represents one plugin. Each file has a header declaring the plugin identity, optional import directives referencing other plugins’ schemas, followed by one or more interfaces.
plugin bmux.windows version 1; interface windows-state { // items... } interface windows-events { // items... }

Grammar (informal)

schema := plugin_header import* interface* plugin_header := "plugin" dotted_ident "version" integer ";" import := "import" ident "=" dotted_ident ";" interface := "interface" ident "{" interface_item* "}" interface_item := record | variant | enum | query | command | events record := "record" ident "{" field_list? "}" variant := "variant" ident "{" variant_case_list? "}" enum := "enum" ident "{" enum_case_list? "}" query := "query" ident "(" param_list? ")" "->" type ";" command := "command" ident "(" param_list? ")" "->" type ";" events := "events" type ";" field := ident ":" type variant_case := ( "@default" )? ident ( "{" field_list "}" )? enum_case := ( "@default" )? ident type := primitive | named | qualified | "list" "<" type ">" | "map" "<" type "," type ">" | "result" "<" type "," type ">" | "unit" | type "?" qualified := ident "." ident // alias.type-name primitive := "bool" | "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f32" | "f64" | "string" | "bytes" | "uuid" ident := [a-zA-Z_] [a-zA-Z0-9_\-]* dotted_ident := ident ( "." ident )* integer := [0-9]+

Naming conventions

Identifiers use kebab-case (pane-state) or snake_case (pane_state). The Rust codegen normalizes kebab-case → snake_case for field/module names and to PascalCase for type/trait names.

Types

Primitives

BPDLRust (generated)
boolbool
u8 … u64u8 … u64
i8 … i64i8 … i64
f32, f64f32, f64
stringString
bytesVec<u8>
uuid::uuid::Uuid
unit()

Containers

BPDLRust (generated)
T?Option<T>
list<T>Vec<T>
map<K, V>::std::collections::BTreeMap<K, V>
result<T, E>::std::result::Result<T, E>
map<K, V> lowers to BTreeMap for deterministic JSON key order in RPC payloads. Keys must be one of string, uuid, or an integer primitive (u8…u64, i8…i64); other key types are rejected at validation time.

Qualified references (imports)

A type from another plugin’s schema is referenced via <alias>.<type-name>, where the alias is declared at the top of the file with import:
plugin bmux.decoration version 1; import windows = bmux.windows; interface decoration-state { command focus-imported-pane(source: windows.pane-state) -> result<unit, string>; }
At validation time the alias must be declared; at codegen time the caller supplies a mapping from alias to Rust crate path (see the schema! macro section below) and the qualified reference is emitted as ::<crate>::<interface>::<Type>.

Items

record

Structs with named fields.
record pane-state { id: uuid, focused: bool, name: string?, }

variant

Tagged unions. Cases may be unit (running) or carry a struct-like payload (exited { code: i32 }). Serde-serialized with a "kind" tag in snake_case. A single unit case may be marked @default to emit an impl Default.
variant pane-status { @default running, exited { exit-code: i32 }, }

enum

Pure unit-only tag sets. No payloads. Use variant if payloads are needed. At most one case may be marked @default.
enum border-style { none, @default ascii, single, double, }
The generated code includes impl Default for BorderStyle { fn default() -> Self { Self::Ascii } }.

query

Read-only operations. Synchronous-semantics but returned as async from the generated service trait.
query pane-state(id: uuid) -> pane-state?;

command

Operations that may mutate state. May be fallible via result<_, _>.
command focus-pane(id: uuid) -> result<unit, focus-error>;

events

Declares the event type emitted by this interface’s event stream. An interface has at most one events declaration. Subscribers receive typed events via PluginEvent::decode_typed(...) on the plugin host.
events pane-event;

Code generation

The bmux_plugin_schema_macros::schema! proc macro takes a braced argument block:
// Simple schema with no imports. bmux_plugin_schema_macros::schema! { source: "bpdl/windows-plugin.bpdl", } // Schema that imports types from another plugin. bmux_plugin_schema_macros::schema! { source: "bpdl/decoration-plugin.bpdl", imports: { windows: { source: "../windows-plugin-api/bpdl/windows-plugin.bpdl", crate_path: ::bmux_windows_plugin_api, }, }, }
The macro expands to a module containing:
  • One submodule per interface (its name normalized to snake_case).
  • Each submodule contains:
    • Rust structs for each record (serde-derived).
    • Rust enums for each variant (tagged, snake_case serde rename).
    • Rust enums for each enum (tagged, snake_case serde rename).
    • impl Default for any enum or variant with a @default case.
    • A pub const INTERFACE_ID: &str = "<interface-name>" matching the BPDL name verbatim.
    • A pub trait <PascalName>Service that bundles all queries and commands as async methods returning Pin<Box<dyn Future<...> + Send>>. Consumers call through &dyn Service; providers impl the trait directly.
Qualified type references are resolved against the imports table. For the example above, windows.pane-state emits as ::bmux_windows_plugin_api::windows_state::PaneState.

Inline schemas

For small self-contained schemas and proc-macro tests, a sibling schema_inline! macro takes the BPDL source as a string literal without touching the filesystem:
bmux_plugin_schema_macros::schema_inline!(r#" plugin my.plugin version 1; interface iface { record r { id: uuid } } "#);
Only the single-schema form is supported; imports require schema!.

Semantic rules (validated at compile time)

  • Plugin id must be non-empty.
  • Import aliases within a single schema must be unique.
  • Type names are unique within an interface.
  • All Named type references must resolve to a declared type in the same interface.
  • Qualified type references (alias.type-name) must reference a declared import alias. When the schema! macro supplies the imported schema, the type must also exist in that schema.
  • Variant case names are unique within their variant.
  • Enum case names are unique within their enum.
  • At most one case per enum or variant may be annotated @default.
  • @default on a variant case is legal only when the case is unit (no payload).
  • Operation names (queries + commands) share a namespace and must be unique within an interface.
  • An interface declares at most one events item.
  • map<K, _> keys must be one of string, uuid, or an integer primitive (u8…u64, i8…i64).
  • record/variant types must be acyclic in their required fields. Cycles through T?, list<T>, or map<_, T> value position are allowed because the generated Rust compiles; direct T fields (including via result<T, _> or result<_, T>) form cycle edges and are rejected.
Validation is performed by bmux_plugin_schema::validate (or validate_with_imports for full cross-schema resolution) and produces Error::Validate with a descriptive message on failure.

Runtime registry

bmux_plugin_schema::registry::SchemaRegistry is the in-memory store the plugin host uses at load time:
let mut reg = SchemaRegistry::new(); reg.register(windows_schema_source)?; reg.register(decoration_schema_source)?; reg.check_compatibility("bmux.windows", "bmux.decoration", "windows-events")?;
check_compatibility returns Err(Vec<CompatError>) listing every facet that doesn’t match — version mismatches, missing interfaces, missing operations, and operation signature disagreements.

Future extensions (not yet supported)

The grammar is intentionally small. Planned additions, each additive:
  • Resources (long-lived handles with methods).
  • Stream types beyond the single per-interface events.
  • Default values on record fields (with serde #[serde(default)]).
  • Inline documentation that survives into generated rustdoc.

Example — windows plugin

See plugins/windows-plugin-api/bpdl/windows-plugin.bpdl for the production schema defining the windows plugin’s complete public API (three interfaces: state queries, commands, and a pane-event stream).