Builder blocks
A builder block is @ctx { ... }, an expression that builds a value
declaratively. It is the one piece of dedicated DSL syntax in C+, and it was the
final language feature: as of v0.0.22 the language surface is frozen, and
everything else grows in packages. @ was previously an invalid character, so no
existing source changes meaning.
A block holds item expressions, leading-dot modifier lines that apply to
the item above them, let setup bindings, and nested blocks:
import "ui/view" as view;
let panel = @view {
let title = "Settings";
view::text(title)
.font = bigger // modifier: assign a field on the item above
.color(blue) // modifier: call a method on it
view::spacer(8)
};
It is not magic — it lowers to a protocol
There are no macros and no compiler-blessed UI types. A builder block lowers to ordinary calls against a fixed protocol the context package provides:
ctx::Builder::new()starts the block.- each item becomes a temporary with its modifiers applied (
__i.font = v,__i.color(args)), thenbuilder.add(__i). builder.finish()is the block's value.
So any package that ships a Builder (new / add / finish), an item type,
and constructor functions becomes a construction DSL. Here is a complete one:
// ui/view.cplus — a context package
pub struct Node { pub value: i32, pub weight: i32 }
pub fn text(v: i32) -> Node { return Node { value: v, weight: 1 }; }
impl Node {
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: Node) { self.sum = self.sum + item.value * item.weight; return; }
pub fn finish(move self) -> Node { return Node { value: self.sum, weight: 1 }; }
}
let tree = @view {
view::text(8)
.weight = 2 // field assignment modifier
view::text(3)
.boost(1) // method modifier
};
// lowers to: new(); t1 = text(8); t1.weight = 2; add(t1);
// t2 = text(3); t2.boost(1); add(t2); finish()
Contextual name lookup
Inside @view { ... }, a bare item name resolves through the context, so
text(...) means view::text(...) with no qualification. Precedence is
locals → same-file top-level → contextual: a let or a same-file function of
the same name shadows the package member. Item field and method names in
modifiers (.font, .boost(...)) are never contextual. Because the rewrite
produces real view::text references before the graph is built,
code-graph and LSP navigation resolve them automatically.
Containers and control flow
A bare name { ... } inside a block is a container element of the same
context: vstack { ... } builds view::vstack, and its children resolve in
view too. A container takes a filled Builder (fn vstack(b: Builder) -> Node),
so the whole feature lowers to new/add plus a finisher — the compiler's output
never names a collection type, which is why DSL packages work even on targets
where Vec is gated.
if/for are collection control flow, Flutter-style: their items add into
the same builder. if needs no else.
let menu = @view {
view::text(header)
vstack {
for item in items {
view::row(item)
}
if logged_in {
view::button(logout)
}
}
};
What the block rejects
Modifier lines are line-oriented: a .name that starts a line attaches to the
current item, while a same-line .name is ordinary postfix access. Inside call
arguments, indexing, grouping parentheses, and nested expression blocks the rule
is off, so wrapped subexpressions are unaffected.
A few shapes are parse-time errors, reported on the offending DSL line: a
modifier with no current item (including right after a let), and return /
break / continue / yield / await / loops / defer / guard inside a
block. A nested different @-DSL is rejected too — use a same-context
container instead. Because the desugar reuses your spans, ordinary sema
diagnostics land where you wrote them: a wrong item type at the item line, an
unknown modifier field at the modifier line, a context without a Builder at the
@ctx line.
cpc fmt keeps @ctx glued to its block and round-trips the whole thing —
containers and if/for included.
See the worked example, Build a construction DSL, for a package and a block you can compile and run.