Vela Future Directions

A Kimi-edition design notebook: bolder memory models, zero-cost abstractions, and ideas that push Vela's power without losing its smallness.

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
Pros
  • 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
Cons
  • 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
Why it's cool: it is Rust's ownership model distilled to three words. Vela could implement it as an opt-in layer: keep today's region pointers (^T@r, already checked and never null), and add own/read/write on top of them for new code.
With regions: capabilities would sit on top of region pointers rather than replace them. A 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
Pros
  • Bulk deallocation is extremely fast
  • Region names make lifetimes explicit but not cryptic
  • Combines beautifully with capability ownership
Cons
  • Region subtyping ("this region outlives that one") adds complexity
  • Some long-lived data needs escape mechanisms
Why it's cool: regions give Vela a memory model that is safer than C's malloc/free, faster than reference counting, and simpler than a full borrow checker. Named regions make the safety visible in source code.
Implemented. Vela now works this way, spelled for the language as it is (it has no 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
Pros
  • 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
Cons
  • Order of multiple defers matters and must be intuitive (LIFO is standard)
  • A deferred expression must not itself fail, or failure needs a story
Why it's cool: 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.
With regions: memory no longer needs it, since a region is already released on every way out of its block. 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.
Why it's cool: most languages pay for 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.)

Why it's cool: this is the safety benefit of Rust's 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.

Why it's cool: metaprogramming and zero-cost abstractions become possible without a separate macro language (types a declaration takes are already there, see Types a declaration takes). Vela stays one language from runtime to compile time.

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.

Why it's cool: Vela could become a serious language for numeric and game code without abandoning its minimalism. Vector types are small syntax with huge upside.

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.

Why it's cool: systems programming sometimes needs to talk directly to hardware or squeeze cache lines. Vela can expose this power without making it the default.

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.

Why it's cool: Vela could be used for kernel code, cryptography, and micro-optimization without dropping to a separate assembler or losing type safety at the interface.

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.

Why it's cool: generic code often hits a tension between speed and size. Letting the programmer decide, rather than the compiler guessing, is a rare power feature.

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.)

Why it's cool: it removes the need for helper tasks that only exist to compute a value from a kind. Less boilerplate, same safety.

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 when on args.len is today. It costs nothing when running, and a case that can't match is never checked against that value, so a case may use n + 1 freely.
  • 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 P of 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] xs gives a read-only view of the items, of any count: an array [int#4], or a slice [int]@r. It can be gone through with each, read with xs[#i] and xs.len, but not grown or changed, which answers the mutation concern without copying anything (a pointer and a count). case [int#4] xs would 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's case (int, float) a, b, which reads like fixed a, b = t.
  • Shapes and kinds by name: case Point p, and case maybe int m for a maybe. A kind's variants are a value match, which match already does, so a type case only names the kind: case Shape s.
  • The type's needs come with it: in case int n, n is an int with everything an int allows, whatever P... 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), which match 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.type with type cases (case int n, told apart from text cases by having no quotes), or when v is int n.
  • Matching a type that takes types, case Box<T> b with T left 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. A show(v) in else needs P...: shown, as now.
  • Regions: case text t matches text@r too, and t keeps the value's region.
Why it fits: it costs nothing when running, uses the per-copy pruning Vela already has, and turns a group from something that can only be passed on and shown into something a task can compute with.

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 args and t.#k would 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 R alone, what squares sends is in the region the wrapper's body gives next, 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 to R by its place (the k-th value of the group is the region R lives in, checked in each copy), or a wrapper of such a task still writes -> [T]@a and 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.
Why it waits: naming a region beside the group covers what a wrapper of such a task needs, sends in that region included, with no per-use rules about which reads a group allows.

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 Shaped would 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 Shaped can't hold one in place the way an array of Circle does: the value lives in a region and the Shaped points to it, so the type names that region, Shaped@r, and everything the region rules say about pointers applies (it never outlives r, it is never null).
  • What a method may say. A method taking other: this can't be called through a Shaped, 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, bind and flex would need a rule for a value behind a table: alters methods only through a flex or bind one, as for any value, and a copy copies the pointer, so it is twofold.
  • Getting the shape back. A match on a Shaped by its shape (case Circle c) would read which table it holds, a type test as t.#k is โ€ฆ is.
Why it waits: the static form covers what a task over many shapes needs, at no cost when the program runs, and a kind holding each shape as a variant already holds different shapes in one slice. The dynamic form adds a value of a new sort, a cost on each call, and rules about which methods can be called; it is worth its own Brief once a program wants it.

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
Why it's bold: algebraic effects are powerful but can make control flow hard to follow. They fit Vela only if kept simple and explicit โ€” no implicit effect inference, and handlers must be lexically visible.

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.

Why it's cool: typestates turn runtime bugs into compile-time errors for resources, protocols, and UI flows. They are a natural fit for a systems language with explicit lifetimes.

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
Partly there without a word: every value can already be shown (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.

Why it's cool: it gives Vela the convenience of Rust's derives without adding traits or macros to the core language. Derive is just a compiler service.

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.

Why it's cool: it gives Vela most of the ergonomics of method chaining without adding methods, receivers, or namespaces.
Built as call pipes: |>> 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.

Why it's cool: it brings mathematical clarity to complex numeric types like 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, not for bitmask manipulation.
    • Power & roots: pow(exp), fast modular exponentiation pow_mod(exp, modulus) for cryptographic key exchange, and integer square root isqrt().
    • Number theory utilities: greatest common divisor gcd(a, b) and modular inverse.
  • 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 across Int128, Int256, Int512, and Int1024 write cleanly with full checked-arithmetic safety.
Why it's cool: fixed-width stack-allocated big integers offer zero heap overhead, zero garbage collection pauses, and guaranteed cache locality while covering all modern cryptographic and numeric needs.

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, so xs.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, rev for sources that can go backwards, and peekable.
  • More consumers: count, sum, min/max for 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.
Why it's cool: the same words (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().

Why it's cool: this is Go's channels plus Rust's ownership guarantees. It keeps the friendly syntax of channels while eliminating an entire class of concurrency bugs.

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.

Why it's cool: actors give Vela a concurrency model that is both simple and safe, with zero locks needed inside user actor code and no risk of deadlocks or data races on actor state.

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

Minor improvements that landed while writing this document:
  • --version flag: prints the compiler name and version and exits successfully.
  • --check flag: 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_000 and 1_000.000_001 are accepted, with underscores ignored for readability. Malformed forms such as 1__000 or 1_ 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.