Vela is already a clean, opinionated little language. This document is a sketch of where it could go if it dared to be more ambitious โ especially in memory safety, performance, and expressive power. The ideas here are meant to be inspiring rather than a rigid roadmap; some are experiments, some are long-term bets, and a few are small enough to land today.
Memory Safety: Beyond
Manual new and free
Vela's memory model was explicit but unsafe: a missing
free leaked, a double free corrupted, and a
freed pointer could still be read. Named regions, below, have since
replaced new/free (see
Memory and Regions, and the guide
Vela Regions). The other ideas here stay:
several of them can still make regions more flexible, or cover
resources other than memory. (Null is already gone: a
^T always points to something, and a pointer that can be
missing is a maybe ^T.) The ideas below aim to keep the explicit feel while making common
mistakes impossible โ or at least caught at compile time.
Capability Ownership:
own, read, write
Instead of a single ^T pointer, Vela could track
capabilities at the type level. A capability says what you are
allowed to do with a value, and the compiler rejects operations that
don't match.
task copy_name(own src: text) -> text
; `own` means this task now owns src; the caller cannot use it after.
<- src
fi
task print_name(read name: text)
; `read` means immutable, temporary access. The owner keeps the value.
show(name)
fi
task reset(flex write counter: ^int@r)
; `write` means mutable access to memory owned by someone else.
counter^ = 0
fi
- Eliminates use-after-free and double-free for owned values
- No hidden reference counting or garbage collector
- Capabilities are visible in types, so APIs are self-documenting
- Requires a borrow/capability checker in the compiler
- More concepts for beginners than today's single pointer
- Needs careful design to keep Vela's syntax minimal
^T@r, already
checked and never null), and add
own/read/write on top of them
for new code.
read ^Node@r parameter would promise the task doesn't
change the list, and read-only pointers are what would let younger
data point into an older region safely; an
own ^T@r could be dropped from its region early. See
Ideas for regions.
Named Regions:
T@arena Done
Make regions first-class and named. A value's type can include the region it lives in, and references cannot escape that region. This is stronger than arenas alone: it lets the compiler prove a reference is still valid.
task process_files() -> int
region arena
fixed buffer: [byte]@arena = allocate(arena, 4096)
work(buffer)
; `buffer` and everything in `arena` are freed here automatically.
<- 0
fi
A function can accept a region parameter so that callers decide where data lives:
task load_config(arena: region) -> text@arena
fixed bytes: text@arena = read_file(arena, "config.ini")
<- bytes
fi
- Bulk deallocation is extremely fast
- Region names make lifetimes explicit but not cryptic
- Combines beautifully with capability ownership
- Region subtyping ("this region outlives that one") adds complexity
- Some long-lived data needs escape mechanisms
allocate):
region arena declares a region freed when its block
closes; new T(value) at arena (or
new@arena T(value)) allocates in it, giving a
^T@arena; a task allocates in a parameter
arena: region and may return
-> ^T@arena; a task that only reads names the region
in its types (list: maybe ^Node@r) without taking it;
and a shape holding pointers lives in one region,
Node@arena. There is no free, and there is
no region subtyping yet: a pointer is never stored in another
region. Details:
Memory and Regions; the tutorial,
the limitations with their workarounds, and ideas such as shapes
with several regions are in Vela Regions.
Scope Guards: defer
A small, powerful statement that schedules cleanup when the current block ends. It works with any resource, not just memory: files, locks, network sockets.
task read_lines(path: text)
fixed f = open(path)
defer close(f)
loop not eof(f)
show(read_line(f))
fi
; close(f) runs here, even if a `<-` above had left the task early.
fi
- Cleanup is written next to acquisition, not at the bottom of the block
- Deterministic, no GC
- Cheap to implement: the compiler emits cleanup code at block exits
- Order of multiple defers matters and must be intuitive (LIFO is standard)
- A deferred expression must not itself fail, or failure needs a story
defer is the missing
half of manual memory management. It lets Vela keep explicit
ownership while removing the tedium and bug surface of matching
every acquisition with a release.
defer is still the right tool for everything else a
block acquires, such as locks and sockets (a file is closed by the
region it was opened in, see
Vela Library), and the generated
C would run deferred code at the same exits where regions are freed.
Niche-Optimized Nullability
Vela's maybe ^T already uses the null pointer as
none, so it costs nothing extra over ^T.
But Vela could go further: track which types have spare niches and
use them automatically, so other maybe values need no
separate flag.
kind Option
Some(value: int)
None
fi
; The compiler sees that `Some` always has a non-zero tag, and stores the
; whole kind in a single int64_t: the payload when the tag is non-zero,
; and zero for `None`. No extra memory, no indirection.
Option<T> with extra memory or branches. Vela
could make nullable pointers, booleans-with-missing, and small
tagged unions as cheap as their raw counterparts.
Static Leak Checking
The compiler could require every resource-acquiring expression to have a matching release on every control-flow path. This is like a borrow checker but only for resources, and only forward-propagating.
task leaky(path: text) -> int
fixed f = open(path)
show(read_line(f))
; Error: `f` is owned but never closed.
<- 0
fi
With defer, this becomes trivial to satisfy; without
it, the checker demands an explicit close on every
path. The rule is simple:
every own value must be consumed exactly once by the
end of its scope.
(This example allocated memory with new before regions
existed. Regions already rule out memory leaks past a block, so the
idea now matters for other resources, and for owned pointers dropped
from a region early.)
Drop without the complexity of lifetimes. It catches
the most common resource bug โ forgetting to release something โ at
compile time.
Performance: Zero-Cost Power Features
Vela compiles to C, so it already inherits C's speed. The ideas below add features that let programmers express intent the C compiler cannot see on its own.
Compile-Time Execution:
comptime
Let Vela run code while compiling. Use it for constants, lookup tables, parser generators, and type-driven code generation.
comptime fib(n: int) -> int
when n < 2
<- n
else
<- fib(n - 1) + fib(n - 2)
fi
fi
fixed table: [int#10] = comptime build_lookup_table()
comptime tasks run in the compiler using the same
interpreter (or the same code generator targeting a small VM). They
have no I/O and no mutable global state. The result must be a
constant value.
A first step is built: a macro,
#SIZE = 4, is a value the compiler computes from
literals, other macros, and operators, and
macros that take values are
planned. A comptime task would be the step after,
running whole tasks.
SIMD Vector Types
Explicit vector types that map directly to CPU SIMD registers. No auto-vectorization magic, just predictable performance.
fixed a: vec4f = [1.0, 2.0, 3.0, 4.0] fixed b: vec4f = [5.0, 6.0, 7.0, 8.0] fixed c: vec4f = a + b ; four additions in one instruction
Vectors support lane-wise arithmetic, shuffles, reductions, and masked operations. The compiler generates the right intrinsics for the target; if SIMD is unavailable, it scalarizes cleanly.
Controlled Shape Layout
Let programmers control how shapes are laid out in memory for cache, ABI, or hardware reasons.
shape Vec3 packed
x: float
y: float
z: float
fi
shape Header aligned(64)
magic: int
version: int
fi
Vela could also support field reordering hints:
shape Foo hot tells the compiler to group hot fields
together and pad cold fields to the end. This is information a C
compiler usually ignores.
Inline Assembly with Vela Types
An asm block where inputs and outputs are ordinary Vela
values, type-checked by the compiler.
task rdtsc() -> int
flex low = 0 ; Vela has no uninitialized variables
flex high = 0
asm("rdtsc" : "=a"(low), "=d"(high))
<- (high << 32) | low
fi
The compiler checks that the constraints match the Vela types, inserts the right clobbers, and forbids outputs that don't fit in a register. It is as unsafe as assembly always is, but the boundaries are typed.
Monomorphization Hints
Vela already makes one task or shape for every set of types a program uses (Types a declaration takes), which is the fast choice and the only one. This would let a declaration ask for the other: share one body when binary size matters more than speed.
task max<T: ordered>(a: T, b: T) -> T inline
when a > b
<- a
fi
<- b
fi
inline names what Vela does today: a separate copy for
every set of types. shared would mean one body reached
through type-erased pointers, paying a call and losing the copy per
type. Sharing only works where the body never needs a value's size,
so the words a type is asked to allow would have to say which of
them a shared body may use.
Expressiveness: More Language in Less Syntax
Vela's syntax is one of its best features. These ideas extend what a small syntax can express.
Match Expressions Done
Built on 2026-09-25, for when as well,
and with both spellings a case can have:
case p = value on the case's own line, or a block whose
last line gives the value, the rule a plan B's block already had.
-> stayed out of it, as
Future Directions argued.
The language guide describes it.
Let match produce a value, so it can be used on the
right side of a binding.
fixed grade = match score
case A _ -> "excellent"
case B _ -> "good"
case C _ -> "pass"
case F _ -> "fail"
fi
Each branch ends with -> expr instead of
<-, distinguishing match values from task returns.
The compiler still checks exhaustiveness. (Future Directions
argues against -> here, since it already names a
task's result type, and suggests a plan B's rule instead: the last
line of each case is its value.)
Working With a Group's Values Idea
A type group gives its values
one at a time with each v in args, and
P alone is the type of v. Two things are
still missing: a loop over the group inside a loop over it (refused
since 2026-10-02, because P would name two types at
once), and a way to do something with a value of a type the
body can't name, such as adding it when it is an int.
These ideas are the user's (2026-10-03), with notes on how they fit.
1. Loops inside loops over one group
Each loop gets a type of its own, so the inner loop's
P and the outer loop's are told apart. A way to write
it: the loop names its turn's type, each v: A in args,
and A is that value's type inside it, as
P is today; each v in args alone keeps
meaning P.
task pairs<P...: shown>(args: P...)
each v: A in args
each w: B in args
show(v, " with ", w) ; A and B are each copy's own types
fi
fi
fi
It costs nothing when running: the body is written out once for each
pair, as it is for each value now (n values give n ร n blocks, which
a size limit should keep in check). Today's way around it, which
works: give the value and the group on to a task of its own,
with(v, args...).
2. Matching a value by its type
v.type gives the type as text, which each copy knows
when compiling, but it doesn't help a match:
case "int" compares text, and inside the case
v is still a P, so v + 1 is
still refused. What is wanted is a match that names the
type and gives the value under that type. The user's syntax:
match v?, where the ? asks for the type
itself, not its text.
task total<P...: shown>(args: P...) -> float
flex sum = 0.0
each v in args
match v?
case int n
sum += float(n)
case float x
sum += x
case [int] xs ; an array of any count, or a slice
each n in xs
sum += float(n)
fi
case (int, float) a, b ; a tuple, taken apart
sum += float(a) + b
else
show("skipped ", v)
fi
fi
<- sum
fi
; and as a condition:
when v? is [int] xs
show(xs.len)
fi
How it would work.
-
Each copy of the task knows the type of each value, so only the
matching case is kept and the others are left out, as a
whenonargs.lenis today. It costs nothing when running, and a case that can't match is never checked against that value, so a case may usen + 1freely. -
Since a group can hold any type, a type match needs an
else, or each use whose value no case takes is an error at that use, as a tuple item past a call's values is now. -
Outside a task that takes types the type is known already, so
match v?there is a warning: only one case can ever match. -
The same works on
Pof a plain generic task,<T>, not only groups.
Improvements on the idea.
-
[int]as a view, not an array. The idea asks that a slice be given in its array form. An array's count is part of its type and known when compiling ([int#4]), and a slice's is known only when running, so a slice can't become an array. Instead,case [int] xsgives a read-only view of the items, of any count: an array[int#4], or a slice[int]@r. It can be gone through witheach, read withxs[#i]andxs.len, but not grown or changed, which answers the mutation concern without copying anything (a pointer and a count).case [int#4] xswould match only that count, and give the array itself. -
Taking a tuple apart could use the case form Vela
already has,
case (int, float) (a, b), or the idea'scase (int, float) a, b, which reads likefixed a, b = t. -
Shapes and kinds by name:
case Point p, andcase maybe int mfor amaybe. A kind's variants are a value match, whichmatchalready does, so a type case only names the kind:case Shape s. -
The type's needs come with it: in
case int n,nis anintwith everything anintallows, whateverP...asked of its types.
Limits and open questions.
-
?after a tuple's item already asks for its type,t.#2? is maybe int(built 2026-10-03, see Tuples), whichmatch v?would extend to any value.?also marks a startup decorator whose failure may pass ($d?), and many languages use a?after a value to send its failure on. If Vela ever wants that,v?would mean two things. Other spellings to weigh:match type v,match v.typewith type cases (case int n, told apart from text cases by having no quotes), orwhen v is int n. -
Matching a type that takes types,
case Box<T> bwithTleft open, would need a type to be named by the case, which is a feature of its own; only whole types (Box<int>) at first. -
A case can't ask "any type that can be shown" (
case shown s): needs are asked of a whole group, not matched. Ashow(v)inelseneedsP...: shown, as now. -
Regions:
case text tmatchestext@rtoo, andtkeeps the value's region.
Regions in a Group Idea
Since 2026-10-05 a
group holds places but never a
region: a region is always named beside the group,
next: (@a, P...) -> [T]@a, r: region, args: P...,
which is the user's rule for now. The harder way, kept here in case
it is wanted later, lets a group hold a task's region too, so
traced<P..., R>(next: (P...) -> R, args: P...)
alone fits squares(r: region, n: int) -> [int]@r.
What it would take.
-
Reading the values. A region has no text and no
place in a tuple, so
show(args...),(args...),each v in argsandt.#kwould be refused for the uses that give a region. A wrapper that compiles for one task would then fail on another, with the error at the$( โฆ )line rather than in the wrapper. Each read would need a rule: skip a region, or refuse it with a message naming the task that gives it. -
What the wrapper sends. With
Ralone, whatsquaressends is in the region the wrapper's body givesnext, which no type says (the reason a generic task never finds a type in a task type's own region, see places and regions). Either the group's region is tied toRby its place (the k-th value of the group is the regionRlives in, checked in each copy), or a wrapper of such a task still writes-> [T]@aand names the region, which is what naming it beside the group already does. - Cost. Nothing when running, as for any group: each use is a copy of its own.
Patterns as the Types of Values Idea
Since 2026-10-16 a pattern is a
requirement on types: a task taking
<T: Shaped> is copied for each type it is given,
and each copy calls that type's own methods directly. The author
asked for the other half to be written down for later: a pattern as
the type of a value, so that one slice holds a
Circle and a Square side by side, and a
call goes to the method of whichever shape each value is when the
program runs (Rust's dyn Trait, Go's interfaces, Java's
and C#'s interfaces, Swift's existential any Shaped).
fixed shapes: [Shaped]@r = [Circle { โฆ }, Square { โฆ }, ...] at r ; the idea, not Vela
each s in shapes
show(s.area()) ; Circle.area or Square.area, as each value is
fi
What it would take.
-
A value that knows its methods. Each value of
type
Shapedwould be two words: where the shape's value is, and a table of its methods, one table per shape and pattern, made by the compiler. A call reads the method from the table and calls it, one indirect call, which C can't inline. -
Where the value is. Shapes differ in size, so a
Shapedcan't hold one in place the way an array ofCircledoes: the value lives in a region and theShapedpoints to it, so the type names that region,Shaped@r, and everything the region rules say about pointers applies (it never outlivesr, it is never null). -
What a method may say. A method taking
other: thiscan't be called through aShaped, since the other value might be another shape; Rust calls such traits not object-safe. Each pattern would be checked for whether all its methods can be called so, or each method marked. -
The three words.
fixed,bindandflexwould need a rule for a value behind a table:altersmethods only through aflexorbindone, as for any value, and a copy copies the pointer, so it is twofold. -
Getting the shape back. A
matchon aShapedby its shape (case Circle c) would read which table it holds, a type test ast.#k is โฆis.
Algebraic Effects
A lightweight effect system where a task can perform an
effect and a caller can handle it. This unifies errors, generators,
async, and dependency injection in one concept.
effect Log
case Message(text: text)
fi
task do_work()
perform Log.Message("starting")
; ...
fi
task main()
handle do_work()
case Log.Message msg
show(msg)
resume ; continue the caller
fi
fi
Typestate / Session Types
Encode state machines in the type system. A file handle can be
File<Open> or File<Closed>;
calling read on a closed file is a compile error. A
shape takes types already (Shapes that take types), so what this needs on top is types that stand for a state and
hold no values, and a rule that a value of one is used once.
shape File<State>
fd: int
fi
task open(path: text) -> File<Open>
; ...
fi
task close(f: File<Open>) -> File<Closed>
; ...
fi
task read(f: File<Open>) -> text
; ...
fi
This generalizes to session types for protocols: a socket type
tracks whether the next allowed action is send or
receive, preventing protocol bugs at compile time.
Derive for Shapes and Kinds
Ask the compiler to generate common tasks for a shape or kind: equality, hashing, cloning, debug printing.
shape Point
x: int
y: int
derive Eq, Hash, Show
fi
fixed a = Point { x: 1, y: 2 }
fixed b = Point { x: 1, y: 2 }
show(a == b) ; true, generated automatically
show(a) writes
Point { x: 1, y: 2 }, see
values as text) and compared item by
item with ~= (a ~= b is true).
a == b compares where they are, so it is false here
(see Comparing). What derive
would still add is hashing and copies.
Derive is implemented with compile-time reflection (or comptime code generation), so it costs nothing at runtime and adds no hidden vtables.
Pipeline Operator
A syntax for chaining tasks left-to-right, making nested calls read more naturally.
fixed result = fetch(url)
|> parse_json()
|> extract_user()
|> validate()
value |> task(args) is rewritten to
task(value, args). It is pure sugar, but it makes
data-flow pipelines readable without methods.
|>> gives the value to the second argument (one
more > per argument), a line starting with
|> goes on with the chain, and a step that fails
stops it, so the chain fails with that failure. It is not quite a
rewrite: the value is worked out before the call's own arguments.
Operator Overloading Idea
Allow custom shapes to implement standard arithmetic, comparison,
and unary operators (+, -, *,
/, %, -x, <,
<=, >, >=).
; With operator overloading, mathematical expressions write naturally:
fixed a = int512("123456789012345678901234567890") ?
fixed b = int512("987654321098765432109876543210") ?
fixed sum = a + b ; Rewritten or dispatched to a.plus(b)
fixed product = a * b ; Rewritten or dispatched to a.times(b)
fixed is_less = a < b ; Rewritten or dispatched to a.cmp(b) < 0
Without operator overloading, big integers and numeric shapes must
rely entirely on chained method calls like
a.plus(b).times(c), which obscures algorithm logic.
Int512 could (and should) use this feature: mapping
+ to plus, - to
minus, * to times,
/ to div, % to
rem, and comparisons to
cmp/less.
Operator overloading preserves Vela's strict safety model:
overloaded arithmetic operations on Int512 and other
shapes must still trap immediately on overflow or division by zero,
guaranteeing that custom numeric shapes maintain the exact same
safety guarantees as primitive sized integers.
Int512, vectors, matrices,
and fixed-point math without sacrificing type safety or runtime
speed.
Built 2026-11-08, in the pattern-named form (see
the language reference): a type
following a pattern whose methods are named plus,
minus, times, div,
rem, neg, or cmp answers
the matching operator (a + b as
a.plus(b); comparisons via cmp beside
0), with both operands of the one type.
std/int512 declares
pattern Numeric<T> and
Int512 follows it.
Deferred, each for a later decision: mixed-type operands
(Int512 + int); overloading ==/!=
by value; **; the bitwise operators. Each needs rules
of its own (conversions for mixing, equality semantics for shapes,
method names for powers and bit work).
Big Integer Family & Future Improvements Idea
Vela introduces Int512 (signed two's complement 512-bit
integer) with limb-level native operations and shape methods. Future
directions expand this foundation into a complete big integer
ecosystem:
-
Unsigned
UInt512: An unsigned 512-bit integer holding non-negative values from 0 up to 2512 − 1. Essential for cryptography, bitfield manipulation, and hashing where negative signs and two's complement sign-extension are unwanted. -
Extended Operations:
-
Bitwise shifts & logic:
shl,shr(arithmetic vs logical),and,or,xor,notfor bitmask manipulation. -
Power & roots:
pow(exp), fast modular exponentiationpow_mod(exp, modulus)for cryptographic key exchange, and integer square rootisqrt(). -
Number theory utilities: greatest common
divisor
gcd(a, b)and modular inverse.
-
Bitwise shifts & logic:
-
Sized Big Integer Family: Expanding beyond 512
bits to standardized widths sharing the same limb-arithmetic
engine:
-
Int128/UInt128: Ideal for 128-bit UUIDs, high-precision IPv6 addresses, and nanosecond timestamps. -
Int256/UInt256: The standard integer width for blockchain smart contracts (EVM), 256-bit hash digests, and elliptic curve cryptography (secp256k1, Ed25519). -
Int1024/UInt1024: For RSA key math and ultra-high-precision scientific calculations.
-
-
Operator Overloading Synergy: Unifying all big
integer types with Vela's operator overloading (
+,-,*,/,%,==,<), so expressions acrossInt128,Int256,Int512, andInt1024write cleanly with full checked-arithmetic safety.
Iterators & Collection Pipelines: After the First Version Idea
The plan builds pipelines in two steps: eager tasks first (numbers |> filter(n -> n % 2 == 0) at r, each step making a new slice), then lazy ones, once a pattern can
take types: pattern Iterator<T> in the library,
adapter shapes following it, and chains that start at the
collection, xs.map(โฆ).filter(โฆ).collect(r), allocating
nothing until they are collected. The first version stays small on
purpose. What may come after it:
- Chains kept as values: holding a lazy chain in a variable, sending it from a task, or giving it to one, with its type written short rather than as nested adapter shapes.
- One fused loop: the compiler turning a whole chain into one C loop with each step's lambda written inline, rather than one call through a task value per step and item.
-
Lambdas that keep what they use: adapters holding
[@r]lambdas, soxs.map(x -> x + offset)works, which needs a shape's field to hold one (refused today). -
More sources: arrays, ranges
(
(0..n).map(โฆ)), maps' keys and values, text's characters and lines, and files read line by line. -
More adapters:
take_while,skip_while,chain,flat_map,windows,chunks,step_by,revfor sources that can go backwards, andpeekable. -
More consumers:
count,sum,min/maxfor ordered items,position, and collecting into a map. - Generators: a task that yields values one at a time as an iterator (see Lazy streams below), once the C backend can keep a task's place between calls.
- Arrays in the eager tasks: today an array never becomes a slice, so the eager tasks take slices; a view of an array as a slice would let them take both.
map,
filter, fold) read the same in a pipe and
in a chain, so a program moves from the simple eager form to the
lazy one by changing where the chain starts, not how it is written.
Concurrency Without Shared Mutation
Vela's minimalism is a good foundation for a concurrency model that is easy to reason about. The theme is: share by communicating, and ownership travels with the message.
Ownership-Transferring Channels
A channel can only carry own values. After sending, the
sender can no longer use the value; the receiver becomes the sole
owner.
task worker(input: Chan[own Job], output: Chan[own Result])
endless
fixed job = input.wait_and_receive()
fixed result = process(job)
output.dispatch(result)
fi
fi
Because ownership moves, there can be no data races on the carried values. The runtime only needs to synchronize the channel itself, not the data.
With regions, a message could also carry a whole region: every
pointer into it names it, so once it is sent, the sender has nothing
left that points there. Channels support
dispatch(c, v) / c.dispatch(v),
wait_and_receive(c) /
c.wait_and_receive(), non-blocking
receive(c) / c.receive(), and
close(c) / c.close().
Actor-Style Tasks & M:N Scheduler
A task can be spawned as an actor with its own mailbox. The actor owns its state; messages are handled sequentially. No locks, no shared mutable state. Lightweight actors run on an M:N green thread scheduler pool in the runtime.
actor Counter(count: int = 0)
on Increment(step: int)
count += step
fi
on Get(reply: Chan[int])
reply.dispatch(count)
fi
fi
task main()
fixed c = spawn Counter(0)
c.dispatch(Counter.Increment(5))
fixed reply = chan_new<int>(1)
c.dispatch(Counter.Get(reply))
fixed res = reply.wait_and_receive()
show("count: ", res)
c.close()
fi
The actor/fi syntax is just a task with a
built-in message loop. The compiler guarantees that the actor's
state is private and only touched inside its own
on handlers. The M:N scheduler automatically
multiplexes thousands of actors across background worker threads.
Smaller but Worthwhile Ideas
Refinement types Idea
fixed n: int where n > 0 = 5 โ the compiler checks
predicates where it can, and runtime checks where it must.
Tagged pointers Idea
Small variants and booleans stored inside a pointer's unused low bits, cutting allocation for common cases.
Lazy streams Idea
each n in fib_stream() where the stream is computed
on demand, backed by a generator task.
First-class assertions Idea
assert(x > 0) as a statement, compiled out in release
builds or turned into traps.
Static array slices Done
[]T as a fat pointer (ptr, len) with
bounds checking and no ownership. Implemented as
[T]@r, a pointer and a length into a region, with
every index checked; see Slices.
Named arguments Idea
resize(width: 2.0, height: 3.0) for readability,
mirroring shape literals.
Source maps in generated C Idea
#line directives so C compiler errors and debuggers
point at Vela source lines.
Decorators on generic tasks Minor idea
A startup decorator or a wrapper on a task that takes types,
$(timed) task first<T>(โฆ), which Vela refuses:
such a task is no value until it is given its types, so a
decorator would need one copy per set of types the program uses,
run where each copy is made. Kept aside on purpose, since it adds
much uncertainty for little gain; a wrapper on each use's own task
does the same today.
Compile-time benchmarks Idea
comptime bench fib(35) runs a benchmark during build
and warns if it regresses.
Already Applied in This Fork
-
--versionflag: prints the compiler name and version and exits successfully. -
--checkflag: parses and type-checks a file without generating C or invoking the C compiler. Useful for editors and CI. -
Integer literal underscores: numbers like
1_000_000and1_000.000_001are accepted, with underscores ignored for readability. Malformed forms such as1__000or1_are rejected.
These are non-breaking additions: existing programs compile
unchanged, and the CLI flags follow the same style as
--emit-c.
Since then, a larger idea from this notebook has landed too, as a
breaking change: named regions.
region r, ^T@r, and
new T(v) at r replaced
new/free; see
Vela Regions.
Recommended Priority Order
| Phase | Focus | Why first? |
|---|---|---|
| 1. Safety foundation |
defer, static leak checking for resources (null
pointers are already gone: maybe ^T)
|
Catches the most common bugs with the smallest compiler change |
| 2. Borrowed views |
[]T slices (done, in regions:
[T]@r), read capabilities
|
Adds power without changing ownership semantics |
| 3. Regions (done) | Named arenas and T@region |
Bulk allocation + safety, a big win for real programs |
| 4. Compile time | comptime tasks |
Unlocks generics, derives, and metaprogramming |
| 5. Concurrency | Ownership channels or actors | Only after memory is solid |
| 6. Big bets | Algebraic effects, refinement types, SIMD | Experiment once the core is stable |
๐ก The Kimi Take
Vela's biggest opportunity is to become a
safer C that still feels like C โ not a smaller
Rust. That means keeping explicit memory and explicit control, but
adding just enough compile-time proof to eliminate leaks,
use-after-free, and data races. Named regions are now in place;
defer, static leak checking for resources, borrowed
slices, and the region ideas in
Vela Regions would complete a
memory story that is both innovative and practical.
For performance, comptime and SIMD are the
high-leverage bets: they make Vela competitive with C++ and Zig
without adding complexity to the everyday syntax. And for
expressiveness, match expressions (now
built) and derive keep Vela small
while removing the boilerplate that currently forces helper tasks
and hand-written equality.