Build a construction DSL with @ctx
C+ has one piece of DSL syntax, the builder block
@ctx { ... }, and it is not special-cased: any package can become a
construction DSL by shipping a small protocol. This example does both halves —
defines a tiny DSL package, then uses it — and the whole thing compiles and runs.
The DSL here builds a weighted-sum tree (the same shape a UI package like
@view { text(...) } uses; the mechanism is identical, only the item type
differs).
Create the DSL
A context package needs three things: an item type, constructor
functions that return it, and a Builder with new / add / finish.
That is the entire contract.
src/group.cplus:
pub struct Item { pub value: i32, pub weight: i32 }
// A constructor function: the bare `leaf(...)` you write inside @group.
pub fn leaf(v: i32) -> Item { return Item { value: v, weight: 1 }; }
impl Item {
// A method modifier: `.boost(1)` on an item line.
pub fn boost(mut self, by: i32) { self.weight = self.weight + by; return; }
}
pub struct Builder { sum: i32 }
impl Builder {
pub fn new() -> Builder { return Builder { sum: 0 }; }
pub fn add(mut self, item: Item) { self.sum = self.sum + item.value * item.weight; return; }
pub fn finish(move self) -> Item { return Item { value: self.sum, weight: 1 }; }
}
// A container element: takes a *filled* Builder and folds its children
// into one Item. `nest { ... }` inside @group calls this.
pub fn nest(b: Builder) -> Item { return Item { value: b.sum, weight: 1 }; }
That is a complete DSL. No macros, no compiler hooks — just a struct, some functions, and the three Builder methods.
Use it
src/main.cplus:
import "./group" as group;
fn main() -> i32 {
let zero = @group { }; // empty block: new() then finish()
let base = 4;
let tree = @group {
let doubled = base * 2; // a setup `let`, spliced through
group::leaf(doubled)
.weight = 2 // field-assignment modifier on the item above
group::leaf(3)
.boost(1) // method modifier
nest { // a same-context container element
group::leaf(5)
}
};
return tree.value + zero.value;
}
How it lowers
The block is rewritten to ordinary calls — this is exactly what the compiler emits, and it is what you could have written by hand:
let tree = {
let mut __b = group::Builder::new();
let doubled = base * 2;
let mut __i0 = group::leaf(doubled);
__i0.weight = 2;
__b.add(__i0);
let mut __i1 = group::leaf(3);
__i1.boost(1);
__b.add(__i1);
// nest { ... } builds its own filled Builder, then group::nest(that)
let mut __c = group::Builder::new();
__c.add(group::leaf(5));
__b.add(group::nest(__c));
__b.finish()
};
Because the output never names a collection type (no Vec), DSL packages work
even on embedded targets where the heap is gated.
Results
cpc build
./target/debug/bb ; echo $?
It exits 27. The arithmetic, value * weight summed per item:
leaf(8).weight = 2→ 8 × 2 = 16leaf(3).boost(1)(weight 1 + 1 = 2) → 3 × 2 = 6nest { leaf(5) }folds to an Item of value 5, added at weight 1 → 5- the empty
@group { }contributes 0
16 + 6 + 5 + 0 = 27.
Reproduce
A two-file project:
# Cplus.toml
[package]
name = "bb"
[[bin]]
name = "bb"
path = "src/main.cplus"
Put group.cplus and main.cplus from above in src/, then:
cpc build
./target/debug/bb ; echo $? # 27
For the full feature — contextual name lookup, if/for collection control
flow, and the parse-time rules — see Builder blocks.
‹ Back to all examples