The .vel language
An indentation-sensitive DSL that compiles to idiomatic modern C++.
velc is a single-pass compiler. It parses .vel files, type-checks them against a primitive registry, and emits one .vel.cpp plus .vel.h per source file. The output is plain modern C++ that consumes the framework — there is no runtime interpreter, no JIT, no garbage collector.
Source-token compression
The same UI in JSX vs. Vel:
<View style={{padding: 24, gap: 12, backgroundColor: theme.bg0, borderRadius: 12}}>
<Text style={{color: theme.fg, fontWeight: '700'}}>Hello</Text>
</View>
V p=lg g=md bg=bg0 r=md
T "Hello" font=bold
That’s a 4–6× compression on real components without losing the declarative shape. For LLMs writing UI, this is often the difference between fitting a feature in context and not. Vel was designed from day one with agent-authored code as a primary user.
Components
Every .vel file declares one or more #Component blocks. Each component lowers to a C++ class that subclasses vel::Widget.
#Counter
@count = 0
@doubled = $count * 2 // detected as derived → Computed<int>
V g=md
T "Count: {$count}, doubled: {$doubled}" font=bold
Btn "++" -> click => $count = $count + 1
What velc emits, semantically:
Signal<int> count_{0}— plain reactive cell.Computed<int> doubled_{ [this]{ return count_.get() * 2; } }— the codegen walks the init expression’s AST, finds$countas a dependency, and binds the computed to it.- The component constructor
.listen()s every signal it owns. The nexttick()callsrebuild(), re-measures, and re-places before the frame paints.
Reactive state
Four signal types, all triggered by syntax in @state declarations:
| Declaration | Inferred type |
|---|---|
@count = 0 | Signal<int> |
@doubled = $count * 2 | Computed<int> (depends on $count) |
@users: std::vector<User> = await fetchUsers() | AsyncSignal<std::vector<User>> |
@theme = persist("theme", "dark") | PersistentSignal<std::string> |
See Framework › Signals for the full reactive contract.
C/C++ FFI
The single most important constraint in Vel: you must be able to drop any existing C++ codebase in with one line.
use "myorg/db.h" // raw C++ include, verbatim
use "ui/card.vel" // cross-file vel import, pulls in components
#UserList
@users = await db::fetchUsers() // qualified C++ calls work everywhere
...
Once a header is in scope, qualified names work in every expression. The lexer understands ::, so db::fetchUsers(), std::vector<myorg::User>, and await myapp::loadAsync() all parse and codegen straight through to the underlying calls. The framework knows nothing about your backend — velc just emits the include and the call site. Your existing C++ links into the binary like any other translation unit.
Effects
~ declares an effect — a callable that re-runs whenever any of its dependencies change.
#Logger
@count = 0
~ $count =>
std::cout << "count is now " << $count << std::endl
Effects compile to per-dep listeners on the signals they reference. They are declarative side effects, not event handlers.
Conditionals & loops
if $users.loading
Spinner
else if $users.error != ""
Alert text=$users.error
else
for u in $users.value
UserCard user=u
if/else if/else/for are first-class language constructs. They compile to if/else/range-based for in C++ and re-execute on every rebuild().
Coming soon
This page is a high-level tour. Deeper docs in progress:
- Lexer & parser specification
- Type checker semantics
- Code generation walkthrough
- Errors & diagnostics