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 $count as a dependency, and binds the computed to it.
  • The component constructor .listen()s every signal it owns. The next tick() calls rebuild(), re-measures, and re-places before the frame paints.

Reactive state

Four signal types, all triggered by syntax in @state declarations:

DeclarationInferred type
@count = 0Signal<int>
@doubled = $count * 2Computed<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