Status: Design phase. Bootstrap compiler in progress (Zig). Items marked [TBD] are accepted in principle but not yet formally specified or implemented.


Introduction

Violet is a systems programming language that combines:

  • Zig’s manual allocation philosophy, comptime metaprogramming, fast compile times, and simplicity
  • Rust’s interfaces (traits), generics, affine type system, and operator overloading
  • Pascal’s readability, keyword conventions, and syntactic clarity

Violet targets embedded systems, operating systems, games, servers, and any domain where control over memory and performance is essential. It provides memory safety without a borrow checker through a combination of linear types, scope-tagged pointers, and explicit allocator discipline.

  -- Hello, World
const process = import("std/process").Context;
const debug   = import("std/debug");

fn main(ctx: process.Context): !void {
    debug.print("Hello, World!\n");
}
  

Design Philosophy

Explicit over implicit. Every allocation is visible. Every suspension point is visible. No hidden control flow.

Manual allocation, compiler-verified discipline. You choose when and where to allocate. The compiler ensures you clean up.

No borrow checker, meaningful safety. Linear types, scope tags, and unsafe boundaries catch the most dangerous bugs without the cognitive overhead of lifetime annotations.

One way to do things. One async runtime, one allocator interface, one derive mechanism. Ecosystem cohesion over infinite flexibility.

Pascal heritage, modern execution. Keywords like and, or, not, case, repeat...until and nil alongside {} blocks, generics, and a Zig-style build system.


Toolchain

The Violet toolchain is a single binary: violet.

  violet build                          -- debug build (Cranelift backend) [TBD]
violet build --mode=ReleaseSafe       -- release build (LLVM backend) [TBD]
violet build --mode=ReleaseUnsafe     -- maximum performance, reduced safety [TBD]
violet run                            -- build and run
violet test                           -- run @test functions
violet fmt                            -- format source files
violet expand src/main.vi             -- show @expand macro output [TBD]
violet expand --type Foo src/main.vi  -- expand only Foo's derives [TBD]
violet pkg add arena-alloc            -- add a palette dependency
violet pkg remove arena-alloc
violet pkg update
violet pkg build --mode=ReleaseSafe
violet pkg publish                    -- publish to palette registry [TBD]
  

Backend Split

ModeBackendPurpose
DebugCranelift [TBD] / C backendFast iteration, sub-second rebuilds
ReleaseSafeLLVMOptimized, all checks active
ReleaseUnsafeLLVMMaximum performance, reduced checks

Linkers: mold (default on Linux), wild for incremental linking [TBD], lld on other platforms.

File Extensions

  • .vi — Violet source file
  • palette.kdl — package manifest
  • palette.lock.kdl — lockfile (generated, commit to VCS)
  • build.vi — build script (a normal Violet program)

Build Instructions

Violet’s bootstrap compiler is written in Zig. You need Zig 0.14+ to build it.

Prerequisites

  # Install Zig
# Linux / macOS: https://ziglang.org/download/
# Windows: winget install zig.zig
zig version  # should print 0.14.x or later
  

Clone and Build

  git clone https://codeberg.org/violet-lang/violet
cd violet
zig build
  

Linux

  zig build -Doptimize=ReleaseSafe
./zig-out/bin/violet --version
  

macOS

  zig build -Doptimize=ReleaseSafe
./zig-out/bin/violet --version
  

Windows

  zig build -Doptimize=ReleaseSafe
.\zig-out\bin\violet.exe --version
  

Running Tests

  zig build test
  

Syntax Reference

Comments

  -- line comment (Pascal style)
(* block comment
   can span multiple lines *)

/// doc comment for the following declaration
fn add(a: i32, b: i32): i32 {
    return a + b;
}
  

Variables and Constants

  var x: i32 = 5;        -- mutable binding
const max: usize = 1024; -- immutable binding

x := 10;               -- reassignment uses :=
x += 1;                -- compound assignment (shorthand for x := x + 1)

if x = 10 {            -- = in expression position is equality comparison
    do_thing();
}
  

Rationale: := for reassignment eliminates the == vs = confusion. = is used consistently for initialization and comparison.

Functions

  -- basic function
fn add(a: i32, b: i32): i32 {
    return a + b;
}

-- void function (no return type)
fn greet(name: []const u8) {
    debug.print("hello {}", .{name});
}

-- failable function
fn parse(s: []const u8): ParseError!u32 {
    if s.len = 0 { return error.UnexpectedEof; }
    -- ...
}

-- static method (associated function, no receiver)
impl Point {
    static fn new(x: f32, y: f32): Self {
        return .{ .x = x, .y = y };
    }
}

-- instance method with mutable receiver
impl Point {
    fn translate(*self, dx: f32, dy: f32) {
        self.x += dx;
        self.y += dy;
    }
}

-- instance method with immutable receiver
impl Point {
    fn len(*const self): f32 {
        return #sqrt(self.x * self.x + self.y * self.y);
    }
}

-- consuming method (moves self)
impl String {
    fn intoSlice(self): []u8 {
        -- self is consumed
    }
}
  

Self parameter forms:

  • *self — mutable receiver (*Self)
  • *const self — immutable receiver (*const Self)
  • self — consuming receiver, moves the value

Rule: Inside impl blocks, every fn must either be static or have a self parameter. A non-static fn without self is a compile error.

Types

  -- struct
type Point = struct {
    x: f32,
    y: f32,
};

-- packed struct (no padding, C-compatible layout)
type Flags = packed struct {
    active:   u1,
    mode:     u3,
    priority: u4,
};

-- C-compatible layout
type CVec3 = extern struct {
    x: f32,
    y: f32,
    z: f32,
};

-- enum (fieldless variants only, always Copy)
type Color = enum {
    Red,
    Green,
    Blue,
};

-- union (tagged variants with optional data)
type Shape = union {
    Circle { radius: f32 },
    Rect   { w: f32, h: f32 },
    Point,              -- unit variant
};

-- error type with payload
type ParseError = error {
    InvalidChar  { ch: u8, pos: usize },
    UnexpectedEof,
    TooLong      { max: usize, got: usize },
};

-- type alias
type Meters = f32;
  

Struct Initialization

  var p = Point { .x = 1.0, .y = 2.0 };
var p: Point = .{ .x = 1.0, .y = 2.0 };  -- anonymous, type inferred

-- with field defaults
type Config = struct {
    host: []const u8 = "localhost",
    port: u16        = 8080,
};

var cfg = Config { .host = "example.com" };  -- port uses default
  

Control Flow

  -- if statement
if x > 0 {
    positive();
} else if x < 0 {
    negative();
} else {
    zero();
}

-- if expression (then...else)
var abs = if x >= 0 then x else -x;

-- case (pattern matching)
case shape {
    .Circle { radius } => debug.print("r={}", .{radius}),
    .Rect   { w, h }   => debug.print("{}x{}", .{w, h}),
    _                  => debug.print("unknown"),
}

-- case as expression
var n = case opt {
    .Some { value } => value,
    .None           => 0,
};

-- for loop
for item in collection {
    process(item);
}

-- for with index
for item, i in collection {
    debug.print("[{}] {}", .{i, item});
}

-- multi-sequence for
for x, y in xs, ys {
    debug.print("{} {}", .{x, y});
}

-- while
while condition {
    step();
}

-- infinite loop with break-value
var result = loop {
    var x = try next();
    if x.valid { break x; }
};

-- post-condition loop (Pascal heritage)
repeat {
    input := try readLine();
} until input != "";
  

Logical and Bitwise Operators

  -- logical (word operators, Pascal style)
if x > 0 and y > 0 { }
if not valid { }
if a or b { }
if a xor b { }

-- bitwise (symbol operators)
var flags = a & b;   -- AND
var mask  = a | b;   -- OR
var inv   = ~a;      -- NOT
var bits  = a ^ b;   -- XOR
var shift = a << 2;
  

Rationale: Word operators for logic, symbol operators for bitwise. No ambiguity between & (bitwise AND) and and (logical AND).

Type Operators

  -- type check
if shape is *Circle {
    shape.draw();
}

-- safe downcast (panics on failure)
var circle = shape as *Circle;

-- safe optional cast (returns ?*Circle)
var circle = shape as? *Circle;

-- numeric cast
var y: i64 = x as i64;
var z: ?u8 = x as? u8;    -- checked narrowing, .None if doesn't fit
  

Nil

  var p: *i32 = nil;         -- raw pointers only
if p = nil { #panic("null"); }

var x: ?i32 = .None;       -- optionals use .None, never nil
  

Defer and Errdefer

  fn example(alloc: Allocator): !void {
    var file = try fs.open("data.txt");
    defer file.deinit();           -- runs on any exit

    var list = List[u8]::new();
    errdefer list.deinit();        -- runs only on error exit

    try file.readAll(alloc, &list);
    return list.intoSlice();       -- success: errdefer does NOT run
}
  

Visibility

  pub type ExportedType = struct { ... };    -- public
pkg type PackageType  = struct { ... };    -- package-visible
type PrivateType      = struct { ... };    -- file-private (default)

pub type Config = struct {
    pub host:      []const u8,             -- public field
    pub readonly port: u16,                -- publicly readable, privately writable
    timeout:       Duration,               -- private field
};
  

Unsafe

  unsafe {
    var p = raw_ptr as *MyStruct;
    p.*.doThing();
}

unsafe fn rawAccess(ptr: *u8): u8 {
    return ptr.*;
}
  

What requires unsafe:

  • Raw pointer dereference from unsafe source
  • Calling extern functions
  • Pointer casts via as between pointer types
  • nil dereference
  • #asm inline assembly
  • #ptrAdd, #uncheckedIndex, #bitCast
  • Manually implementing Send/Sync via unsafe provide

What unsafe does NOT disable:

  • Linear type checking
  • Scope tag checking
  • Bounds checking (use #uncheckedIndex explicitly to opt out)
  • Nil checks (compiler still inserts them unless in ReleaseUnsafe mode)

Type System

Primitive Types

TypeDescription
i8, i16, i32, i64, i128Signed integers
u8, u16, u32, u64, u128Unsigned integers
isize, usizePointer-sized signed/unsigned
f32, f64IEEE 754 floats
boolBoolean
voidNo value
noreturnDiverging (never returns)
anyopaqueType-erased pointer target
charUnicode scalar value (restricted u32)

Arbitrary-width integers [TBD]: u1, u3, u7 etc. (Zig-style)

Integer Arithmetic

  -- always panics on overflow (Option A, settled)
x + y
x - y
x * y

-- wrapping (always defined)
x +% y
x -% y
x *% y

-- saturating (always defined)
x +| y
x -| y
x *| y

-- integer division and modulo [TBD: div/mod keywords vs / %]
x / y    -- or: x div y (Pascal style, under debate)
x % y    -- or: x mod y
  

Pointers and Slices

  *T           -- mutable pointer
*const T     -- immutable pointer
[*c]T        -- C pointer (opaque provenance)
*anyopaque   -- type-erased pointer
*s T         -- scope-tagged pointer [TBD: full rules]
[]T          -- mutable slice (pointer + length)
[]const T    -- immutable slice
[]s T        -- scope-tagged slice [TBD]

-- dereference
p.*          -- explicit postfix dereference (never prefix *)
  

Optionals

  ?T           -- optional, sugar for Option[T]

var x: ?i32 = 5;
var y: ?i32 = .None;

-- unwrap patterns
var n = x orelse 0;               -- default value
var n = x orelse return error.Missing;  -- early return
var n = x?;                        -- propagate None upward
if x |val| { use(val); }           -- conditional unwrap

-- adaptor methods
x.isSome()
x.isNone()
x.unwrapOr(default)
x.andThen(|v| someOp(v))
x.map(|v| transform(v))
  

Error Unions

  !T           -- inferred error set
E!T          -- named error union (E is an error type)

-- example
fn parse(s: []const u8): ParseError!u32 { ... }

-- propagation
var n = try parse(input);          -- propagates if error types match
var n = parse(input) catch wrap;   -- auto-wraps into caller's error type

-- handling
var n = parse(input) catch |e| {
    case e {
        .InvalidChar { ch, pos } => { ... },
        .UnexpectedEof => { ... },
    }
};
  

wrap in error types:

  type AppError = error {
    wrap ParseError,      -- generates: ParseError { inner: ParseError }
    wrap ConnectError,
    Custom { code: u32 },
};
  

Generics

  -- generic function with bounds
fn sum[T: Add[T] + Default](items: []T): T {
    var result = T::default();
    for item in items { result += item; }
    return result;
}

-- generic type
type List[T] = struct {
    ptr: [*]T,
    len: usize,
    cap: usize,
};

-- generic impl block
impl[T] List[T] {
    static fn new(): Self { ... }
    fn push(*self, alloc: Allocator, val: T): !void { ... }
}

-- requires clause for complex bounds
fn process[T, U](a: T, b: U): U
requires T: Serialize + Clone,
         U: Deserialize,
{
    ...
}
  

Comptime Value Parameters

  type FixedList[T, const N: usize] = struct {
    data: [N]T,
    len:  usize,
};

var arr: FixedList[i32, 16] = FixedList[i32, 16]::new();
  

Ownership and Move Semantics

  interface Copy {}    -- marker: trivially copyable (derive for POD types)
interface Clone {
    fn clone(*const self, alloc: Allocator): !Self;
}
interface NoCopy {}  -- opt-out of auto-derived Copy
interface NoClone {} -- opt-out of Clone, implies NoCopy
  

Rules:

  • Non-Copy types move on assignment — use it once, compiler tracks it
  • Copy types are bitwise-copied silently
  • Clone is always explicit and always shows an allocator
  • Using a moved value is a compile error
  var f = File::open("x.txt");
consume(f);     -- f moved
consume(f);     -- compile error: f was moved
  

Linear Types

Types that implement Destroy are linear — they must be explicitly consumed or the compiler errors.

  interface Destroy {
    type Args;
    fn deinit(*self, args: Self::Args): void;
}

provide Destroy for List[T] {
    type Args = Allocator;
    fn deinit(*self, alloc: Allocator) {
        alloc.free(self.ptr[0..self.cap]);
    }
}

-- usage
fn example(alloc: Allocator): !void {
    var list = List[i32]::new();
    defer list.deinit(alloc);       -- must consume or compiler errors
    try list.push(alloc, 42);
}
  

Two-tier cleanup:

  interface AsyncClose {
    type Args;
    fn close(*self, io: *Io, args: Self::Args): !void;
}

-- deinit: sync, no scheduler, always available (force-close)
-- close:  async, graceful shutdown, scheduler required
  

Smart Pointers and Deref

  interface Deref[T] {
    fn deref(*const self): *const T;
    fn derefMut(*self): *T;
}

-- .* desugars to .deref() or .derefMut() based on context
var rc: Rc[List[i32]] = ...;
rc.*.push(alloc, 5);    -- explicit dereference, one layer
  

Standard smart pointers:

  • Box[T] — unique ownership, heap allocated, implements Destroy
  • Rc[T] — single-threaded reference counting
  • Arc[T] — atomic reference counting, thread-safe
  • Cell[T] — interior mutability for Copy types [TBD]
  • RefCell[T] — runtime borrow checking, single-threaded [TBD]

dyn — Dynamic Dispatch

  dyn interface Drawable {
    fn draw(*self, ctx: *Context): void;
    fn bounds(*const self): Rect;
}

var d: *dyn Drawable = &my_circle;
d.*.draw(ctx);

-- heap allocated dyn
var d = alloc.create(Drawable, my_circle);
defer alloc.destroy(d);
  

Object safety rules (dyn interface):

  • No static fn
  • No generic methods
  • No methods returning Self by value
  • Associated types must be pinned at use site

Memory Model

Explicit Allocators

Every allocation takes an explicit Allocator parameter. No hidden allocation ever.

  fn makeList(alloc: Allocator): !List[i32] {
    var l = List[i32]::new();
    errdefer l.deinit(alloc);
    try l.push(alloc, 1);
    try l.push(alloc, 2);
    return l;
}
  

using — Implicit Allocator Threading

  fn process(alloc: Allocator, data: []const u8): !Report {
    using alloc;    -- alloc is implicit for this scope

    var tokens = try tokenize(data);    -- alloc threaded implicitly
    errdefer tokens.deinit();           -- alloc implicit in deinit

    var tree = try parse(tokens.asSlice());
    errdefer tree.deinit();

    return try analyze(tree);
}
  

ctx parameter modifier:

  -- declaration: marks parameter as implicitly suppliable
fn tokenize(input: []const u8, ctx alloc: Allocator): ![]Token { ... }

-- call site: alloc comes from `using alloc` in scope
using gpa;
var tokens = try tokenize(source);

-- explicit override
var tokens = try tokenize(source, .alloc = arena);
  

Rules:

  • using is lexical — applies to the rest of the current block
  • Two using values of the same type in the same scope → compile error
  • Inner scope using shadows outer
  • ctx parameters are NOT allowed on extern("C") functions

Blessed types for ctx: Allocator. *Scheduler is explicitly excluded — schedulers must always be explicit because suspension affects correctness (lock-safety, ordering).

Scratch Allocations

  scratch(alloc, 4096) {
    -- temporary arena, freed at block exit
    -- using established implicitly
    var parsed = try parseHeaders(req.raw);
    var validated = try validate(parsed);
    -- all scratch data freed here
}
  

Scope-Tagged Pointers [TBD]

  scope s {
    var x: i32 = 5;
    var p: *s i32 = &x;    -- pointer tagged with scope s
    use(p.*);
    -- p cannot escape this block
}
-- p is dead, cannot be used

-- coercion: outer scope tag can be used in inner scope
scope s {
    var x: i32 = 5;
    var p_outer: *s i32 = &x;
    scope t {
        var p_temp: *t i32 = p_outer;   -- valid: s outlives t
        -- p_outer := &y;               -- invalid: y is scoped to t
    }
}
  

Rules:

  • *s T cannot escape scope s
  • Outer scope pointers can coerce to inner scope (safe)
  • Inner scope pointers cannot escape to outer (compile error)
  • Tags are inferred locally, required at API boundaries

Escaping a scope:

  • Copy T out by value (for Copy types)
  • Clone using an outer allocator (for Clone types)
  • Move an owned value out if it has no scope-tagged fields

UB Policy

Outside unsafe:

OperationBehavior
Integer overflow (plain +)Always panic
Nil dereferenceAlways panic
Out-of-bounds accessAlways panic
Uninitialized read (#undefined)Compile error if statically detectable; panic in debug (poisoned memory)
Data racesPrevented by type system
Pointer arithmeticNot available

Inside unsafe:

OperationBehavior
Integer overflowSame (panics) unless +% or `+
Nil dereferenceUB — compiler assumes non-null
Out-of-boundsPanics by default; #uncheckedIndex for UB opt-in
Uninitialized readUB in ReleaseUnsafe, poisoned in Debug
Data racesUB — programmer asserts synchronization
Pointer arithmetic#ptrAdd available, UB if out of allocation

Build Modes:

ModeNil checkBounds checkOverflow checkUninitialized
DebugPanicPanicPanicPoisoned (0xAA)
ReleaseSafePanicPanicPanicUndefined
ReleaseUnsafeUB inside unsafe#uncheckedIndex UBSameUB

Error Handling

  -- define error type with payload
type FileError = error {
    NotFound { path: []const u8 },
    PermissionDenied,
    Io { code: i32 },
};

-- return error
fn openFile(path: []const u8): FileError!File {
    if not pathExists(path) {
        return error.NotFound { .path = path };
    }
    -- ...
}

-- propagate (exact type match)
fn readConfig(path: []const u8): FileError!Config {
    var f = try openFile(path);
    defer f.deinit();
    return try parseConfig(f);
}

-- wrap into outer error type
type AppError = error {
    wrap FileError,
    wrap ParseError,
    Custom { msg: []const u8 },
};

fn run(): AppError!void {
    var cfg = openFile("config.toml") catch wrap;   -- auto-wraps FileError
    -- ...
}

-- handle explicitly
var result = openFile("x.txt") catch |e| {
    case e {
        .NotFound { path } => {
            debug.print("not found: {}", .{path});
            return error.Custom { .msg = "file missing" };
        },
        _ => return e as AppError,
    }
};
  

errdefer — cleanup only on error:

  fn process(alloc: Allocator): ![]u8 {
    var buf = try alloc.alloc(u8, 1024);
    errdefer alloc.free(buf);    -- frees buf only if we return an error

    try doWork(buf);
    return buf;                  -- success: errdefer does NOT run
}
  

Generics and Comptime

Comptime Parameters

  fn printFields(comptime T: type, val: *const T): void {
    inline for (#typeInfo(T).fields) |field| {
        debug.print("{}: {}", .{field.name, #fieldGet(val.*, field.name)});
    }
}
  

inline if and inline for

  fn serialize[T](val: *const T): []u8 {
    inline if #typeInfo(T).repr = .Int {
        return serializeInt(val.*);
    } else inline if #typeInfo(T).repr = .Struct {
        return serializeStruct(T, val);
    } else {
        #compileError("cannot serialize {}", .{#typeName(T)});
    }
}
  

inline for generates code at compile time:

  inline for (fields) |field| {
    -- each iteration generates separate code
}

inline for (field, i) in fields {
    inline if i > 0 { writer.writeStr(", "); }
    writer.writeStr(field.name);
}
  

Comptime Type Functions

  fn Pair(comptime A: type, comptime B: type): type {
    return struct {
        first:  A,
        second: B,

        fn swap(*const self): Pair(B, A) {
            return .{ .first = self.second, .second = self.first };
        }
    };
}

var p = Pair(i32, f32) { .first = 1, .second = 2.0 };
  

TypeInfo

  type TypeInfo = struct {
    ident:  []const u8,
    size:   usize,
    align:  usize,
    repr:   TypeRepr,
};

type TypeRepr = union {
    Struct  { fields: []FieldInfo, packed: bool, extern_: bool },
    Enum    { variants: []VariantInfo, tag_type: *TypeInfo },
    Union   { fields: []FieldInfo },
    Error   { variants: []ErrorVariantInfo },
    Int     { bits: u8, signed: bool },
    Float   { bits: u8 },
    Pointer { inner: *TypeInfo, mutable: bool },
    Slice   { inner: *TypeInfo },
    Bool,
    Void,
    NoReturn,
};
  

Comptime Sandbox

Comptime code is sandboxed — it can only:

  • Read type information via #typeInfo
  • Read palette-local files via #embedFile
  • Read build-time env vars via #readEnv (root palette only)
  • Perform arithmetic and string manipulation

Comptime code cannot:

  • Write files
  • Make network requests
  • Execute processes
  • Read files outside the palette root

Interfaces

Declaration

  interface Drawable {
    fn draw(*self, ctx: *Context): void;
    fn bounds(*const self): Rect;
}

-- interface with requirements
interface Car requires Vehicle, Mechanical {
    fn drive(*self): void;
}

-- dyn-safe interface
dyn interface Runnable {
    fn run(*self): void;
}
  

Implementation

  provide Drawable for Circle {
    fn draw(*self, ctx: *Context): void {
        fill_circle(ctx, self.center, self.radius);
    }
    fn bounds(*const self): Rect {
        return Rect::fromCircle(self.*);
    }
}
  

Generic Implementations

  -- specific generic type
provide[T] Clone for List[T] {
    fn clone(*const self, alloc: Allocator): !Self { ... }
}

-- blanket implementation
provide[T: LuaType] Destroy for T {
    fn deinit(*self, _: void) { ... }
}

-- specialize to override blanket
specialize provide Destroy for LuaFunction {
    fn deinit(*self, _: void) { ... }
}
  

Operator Overloading

  operator (+) = Add;
operator (-) = Sub;
operator (*) = Mul;
operator (/) = Div;
operator ([]) = Index;
operator (=)  = Eq;

interface Add[T] {
    fn add(a: T, b: T): T;
}

provide Add[Vec2] for Vec2 {
    fn add(a: Vec2, b: Vec2): Vec2 {
        return .{ .x = a.x + b.x, .y = a.y + b.y };
    }
}

var c = a + b;   -- desugars to Add[Vec2].add(a, b)
  

Index Interface

  interface Index {
    type Value;
    type Idx;

    fn getIndex(*const self, Self::Idx): !*const Self::Value;
    fn getIndexMut(*self, Self::Idx): !*Self::Value;
    unsafe fn getIndexUnchecked(*const self, Self::Idx): *const Self::Value;
    unsafe fn getIndexUncheckedMut(*self, Self::Idx): *Self::Value;
    fn setIndex(*self, Self::Idx, Self::Value): !void;
    unsafe fn setIndexUnchecked(*self, Self::Idx, Self::Value): void;
}

-- arr[i] desugars to arr.getIndex(i)?
-- unsafe { arr.getIndexUnchecked(i) } for unchecked access
  

HashMap does not implement Index — key absence is a normal case, not an error. Use .get(), .getMut(), .set(), .remove(), .entry().

Naming: @expand(Interface) calls Interface::generate(T). Conflicts: manual provide wins over expanded, duplicate expands are a compile error.


Closures

  -- [capture_list](params): ReturnType { body }

var offset: i32 = 5;
var add = [offset](x: i32): i32 { return x + offset; };

-- move capture
var data = loadData(alloc);
var f = [move data](_: i32): void { process(data); };

-- inferred types from context
io.scope([](s: *IoScope) {
    try s.spawn([move data](_: *IoScope) {
        process(data);
    });
});

-- named functions coerce to closure type
fn myTask(s: *IoScope): !void { ... }
try io.scope(myTask);

-- _ for unused params (but not ALL params)
[](s: *IoScope, _: Event) { use(s); }
  

Capture rules:

  • Capture list is always explicit
  • move x — moves x into the closure
  • x (no move) — copies if Copy, else moves
  • Reference captures are not supported (would require lifetimes)

Thread safety: Closures passed to io.spawn must be Send — all captured values must be Send. Non-Send captures (e.g., Rc[T]) are a compile error at the spawn site.


Async and Concurrency

The Io Interface

Violet’s async model uses an explicit *Io parameter rather than async/await keywords. Functions that take *Io may suspend; functions without it are provably synchronous.

  interface Io {
    fn waitFd(*self, fd: Fd, event: IoEvent): !void;
    fn sleep(*self, dur: Duration): !void;
    fn yield_(*self): void;
    fn spawnBlocking(*self, alloc: Allocator, work: BoxedBlockingTask): !void;
    fn scope(*self, alloc: Allocator, body: BoxedScopeTask): !void;
    fn spawn(*self, alloc: Allocator, task: BoxedTask): !TaskHandle;
    fn spawnDetached(*self, alloc: Allocator, task: BoxedTask): !void;
    fn waker(*self): Waker;
    fn handle(*self, alloc: Allocator): IoHandle;
    fn run(*self): !void;
    fn shutdown(*self): void;
}

interface IoScope: Io {
    fn spawn(*self, alloc: Allocator, task: BoxedTask): !void;
}
  

Why explicit *Io instead of async fn:

  • Suspension is visible at every call site — critical for lock safety
  • No function coloring at the syntax level
  • Multiple schedulers in one program are possible
  • Library code is scheduler-agnostic
  fn serveRequests(io: *Io, alloc: Allocator): !void {
    using alloc;

    try io.scope([](s: *IoScope) {
        while true {
            var conn = try acceptConnection(io);
            try s.spawn([move conn](_: *IoScope) {
                try handleConnection(io, conn);
            });
        }
    });
    -- all connection tasks joined before returning
}
  

Inside io.scope, use s for everything: IoScope extends Io, so s.waitFd, s.sleep, s.spawn all work through the scope.

Manual Task Management

  -- spawn with handle (must join or detach)
var handle = try io.spawn(alloc, [](s: *IoScope) {
    try doWork(io);
});
-- ... do other work ...
try handle.join();        -- wait for completion
-- or:
handle.detach();          -- convert to daemon task (cancelled on shutdown)

-- fire and forget daemon
try io.spawnDetached(alloc, [](s: *IoScope) {
    while true {
        try emitMetrics(io);
        try s.sleep(Duration::seconds(30));
    }
});
  

TaskHandle is linear — you must join, detach, or awaitCancelled. Dropping without one is a compile error (asserts in debug).

Cancellation

Tasks are cancelled cooperatively — error.Cancelled is returned at suspension points. errdefer handles cleanup:

  fn worker(io: *Io, alloc: Allocator): !void {
    var list = List[u8]::new();
    errdefer list.deinit();    -- runs if cancelled or any other error

    try io.waitFd(fd, .Read);  -- returns error.Cancelled if cancelled
    try process(&list);
    list.deinit();
}
  

CPU-Bound Work

  fn heavyWork(io: *Io, alloc: Allocator): !u64 {
    return try io.spawnBlocking(alloc, [](_: void): u64 {
        -- runs on thread pool, Violet task suspends
        return computeResult();
    });
}
  

Waker and Cross-Scheduler Communication

  type Waker = struct {
    ptr:    *anyopaque,
    vtable: *const WakerVTable,
};

-- Waker holds Weak reference to scheduler
-- wake() is a no-op if scheduler has been destroyed
  

#currentScheduler [TBD]

  fn downloadEasy(url: []u8): ![]u8 {
    var io = #currentScheduler();    -- thread-local escape hatch
    var conn = try tcp.connect(io, url);
    return try conn.readAll(alloc);
}
  

Use #currentScheduler only in application code, not library code. Libraries should take explicit *Io.

Thread Safety: Send and Sync [TBD]

  interface Send {}    -- safe to transfer to another thread
interface Sync {}    -- safe to share reference across threads

-- auto-derived by compiler from field types
-- unsafe provide for manual assertion
unsafe provide Send for MyRawHandle {}
  
TypeSendSync
Primitives
*T, *const T
Rc[T]
Arc[T: Send]
Mutex[T: Send]
Cell[T: Send]

C FFI

Importing C Headers

  const c = #cImport({
    #cInclude("stdio.h");
    #cInclude("mylib.h");
    #cDefine("MY_FEATURE", "1");
});

unsafe {
    c.printf("hello %d\n", 42);
}
  

Extern Functions

  extern fn malloc(size: usize): [*c]u8;
extern fn free(ptr: [*c]u8): void;
extern("stdcall") fn winFunc(hwnd: *anyopaque): i32;
  

All extern calls require unsafe.

Exporting to C

  export fn violetAdd(a: i32, b: i32): i32 {
    return a + b;
}

export("my_lib_add") fn add(a: i32, b: i32): i32 {
    return a + b;
}
  

Generate C header: violet build --emit-header mylib.h

C-Compatible Types

  type WireHeader = extern packed struct {
    magic:    u16,
    version:  u8,
    flags:    u8,
    length:   u32,
};

type Color = extern enum(u32) {
    Red   = 0,
    Green = 1,
    Blue  = 2,
};
  

FFI Callbacks

The user_data: *anyopaque pattern:

  const ffi = import("std/ffi");

type AppCtx = struct {
    alloc: Allocator,
    io:    *Io,
};

extern("C") fn myCallback(data: *u8, user_data: *anyopaque): void {
    unsafe {
        var ctx = ffi.restore(*AppCtx, user_data);
        using ctx.alloc;
        doWork(ctx.io, data);
    }
}

-- registration
var ctx = AppCtx { .alloc = gpa, .io = io };
unsafe {
    c.register_callback(myCallback, &ctx as *anyopaque);
}
  

ctx parameters are not allowed on extern("C") functions — compile error.

std.c — C Standard Library Bindings

  const c = import("std/c");

c.int, c.uint, c.size_t, c.char
c.malloc, c.free, c.memset, c.strlen
c.ptr = *anyopaque   -- void*
  

Disable std.c in build.vi to avoid linking libc:

  -- build.vi
exe.libc(.none);    -- no libc, embedded targets
exe.libc(.musl);    -- bundle musl (self-contained binary)
exe.libc(.system);  -- link system libc (default)
  

Inline Assembly

  unsafe {
    asm("add {a}, {b}")
        .input("a", x)
        .input("b", y)
        .output("out", &result)
        .clobber("cc")
        .execute();
}

-- architecture-specific
const template = inline if #targetArch() = "x86_64" {
    "add %[a], %[b]"
} else if #targetArch() = "aarch64" {
    "add %[out], %[a], %[b]"
} else {
    #compileError("unsupported architecture");
};
  

Standard Library

Violet’s standard library is split between core (always available, no OS) and std (requires OS).

core — Always Available

ModuleContents
core/allocatorAllocator fat-pointer type, AllocatorVTable
core/optionOption[T], ?T sugar
core/resultError union support
core/sliceSlice operations
core/memmemcpy, memset, basic memory
core/typesCopy, Clone, NoCopy, NoClone, Send, Sync, Destroy, AsyncClose
core/fmtBasic formatting

std Modules

ModuleContents
std/allocGPA, ArenaAllocator, FixedBufferAllocator
std/collectionsList[T], HashMap[K,V], BTreeMap[K,V], RingBuffer[T]
std/stringString, StringView, char utilities
std/ioIo interface, file I/O, buffering
std/fsFilesystem operations
std/netTCP/UDP sockets, DNS
std/asyncIoUringScheduler, EpollScheduler, KqueueScheduler
std/syncMutex[T], Arc[T], Channel[T], atomics
std/processContext, args, env, signals
std/fmtString formatting, printing
std/mathNumeric utilities, PRNG
std/serialJSON, KDL, MessagePack
std/compressDeflate, zlib
std/cryptoSHA-256, Blake3, ChaCha20 (primitives only)
std/testTest framework
std/logStructured logging
std/timeClocks, Duration, timers
std/unicodeGrapheme clusters, normalization
std/cC standard library bindings (disablable)
std/ffiffi.restore, FFI utilities
std/buildBuild system API

String Types

  type char = u32;       -- Unicode scalar value (restricted)
type String = struct { buf: List[u8] };      -- owned, UTF-8 validated
type StringView = []const u8;                -- borrowed, UTF-8 by convention

-- construction
var s = try String::from(alloc, "hello");
var s = try String::fromUtf8(alloc, bytes);  -- validates UTF-8

-- iteration
for b in s.bytes()      { -- b: u8    }
for c in s.codepoints() { -- c: char  }
-- for x in s { }  -- compile error: ambiguous, use .bytes() or .codepoints()

-- grapheme clusters (library)
const unicode = import("std/unicode");
for g in unicode.graphemes(s.view()) { -- g: StringView }
  

Collections

List[T] — dynamic array

  var list = List[i32]::new();
defer list.deinit(alloc);

try list.push(alloc, 42);
var x = try list.get(0);
var n = list.len();
var slice = list.asSlice();
  

HashMap[K, V] — open-addressing hash map (does NOT implement Index)

  var map = HashMap[String, i32]::new();
defer map.deinit(alloc);

try map.set(alloc, "key", 42);
var val = map.get("key");           -- ?*const i32
var val = map.getOrInsert(alloc, "key", 0);

-- entry API (avoids double-lookup)
var e = try map.entry(alloc, "key");
case e {
    .Occupied { val } => val.* += 1,
    .Vacant   { slot } => try slot.insert(alloc, 1),
}
  

Process Context

  fn main(ctx: process.Context): !void {
    using ctx.alloc;

    for arg in ctx.args {
        debug.print("{}", .{arg});
    }

    var home = ctx.env.get("HOME") orelse "/tmp";
    ctx.exit(0);
}

-- simplified (no context needed)
fn main(): void {
    debug.print("hello\n");
}
  

Package System

Manifest — palette.kdl

  package {
    name "myapp"
    version "0.1.0"
    authors "Developer <dev@example.com>"
    license "MIT"
}

deps {
    dep "std-io" version="1.2.0"
    dep "arena-alloc" version=">=0.4.0"
    dep "mylib" git="https://codeberg.org/user/mylib" rev="abc123"
    dep "locallib" path="../locallib"
}

targets {
    bin "myapp" root="src/main.vi"
    lib "mylib" root="src/lib.vi"
}
  

“Palette” — Violet’s name for a package. A palette is a collection of colors; your dependencies are the colors in your project’s palette.

Build Script — build.vi

  const build = import("std/build");

fn main(b: *build.Builder) {
    var opts = b.options();
    var mode = opts.enum("mode", build.BuildMode, .Debug);

    var exe = b.addExecutable(.{
        .name = "myapp",
        .root = "src/main.vi",
        .mode = mode,
    });

    exe.linkSystemLib("z");
    exe.libc(.musl);
    exe.addIncludePath("vendor/mylib/include");

    b.install(exe);
}
  

CLI Commands

  violet pkg add arena-alloc           # add dependency
violet pkg add arena-alloc@0.4.0    # pinned version
violet pkg remove arena-alloc
violet pkg update
violet pkg publish                   # publish to registry [TBD]
  

Tutorials

Tutorial 1: Basic Operations

  const debug = import("std/debug");
const fmt   = import("std/fmt");

fn main(): void {
    -- integers
    var x: i32 = 42;
    var y: i32 = x + 8;
    debug.print("y = {}\n", .{y});

    -- strings
    const greeting: []const u8 = "Hello, Violet!";
    debug.print("{}\n", .{greeting});

    -- optionals
    var maybe: ?i32 = 5;
    if maybe |val| {
        debug.print("got {}\n", .{val});
    }
    var n = maybe orelse 0;

    -- pattern matching
    var result = compute(x);
    case result {
        .Ok { value } => debug.print("ok: {}\n", .{value}),
        .Err { msg }  => debug.print("err: {}\n", .{msg}),
    }
}

type Result = union {
    Ok  { value: i32 },
    Err { msg: []const u8 },
};

fn compute(x: i32): Result {
    if x > 0 {
        return .Ok { .value = x * 2 };
    }
    return .Err { .msg = "negative input" };
}
  

Tutorial 2: Working With Allocators

  const mem   = import("std/mem");
const alloc = import("std/alloc");

fn main(ctx: process.Context): !void {
    -- create a general-purpose allocator
    var gpa = alloc.GPA::new();
    defer gpa.deinit();
    var allocator = gpa.allocator();

    -- allocate a list
    var numbers = List[i32]::new();
    defer numbers.deinit(allocator);

    try numbers.push(allocator, 1);
    try numbers.push(allocator, 2);
    try numbers.push(allocator, 3);

    for n in numbers.asSlice() {
        debug.print("{}\n", .{n.*});
    }

    -- scratch allocation (temporary arena)
    scratch(allocator, 4096) {
        var temp = try buildTempData();
        process(temp);
        -- temp freed here
    }
}
  

Tutorial 3: Error Handling

  type AppError = error {
    wrap IoError,
    wrap ParseError,
    Config { msg: []const u8 },
};

fn loadConfig(alloc: Allocator, path: []const u8): AppError!Config {
    -- open file, errors auto-wrapped
    var f = openFile(path) catch wrap;
    defer f.deinit();

    var contents = try f.readAll(alloc);
    defer alloc.free(contents);

    -- parse, errors auto-wrapped
    return parseConfig(contents) catch wrap;
}

fn main(ctx: process.Context): !void {
    using ctx.alloc;

    var cfg = loadConfig("config.toml") catch |e| {
        case e {
            .IoError { inner } =>
                debug.print("IO error: {}\n", .{inner}),
            .ParseError { inner } =>
                debug.print("Parse error: {}\n", .{inner}),
            .Config { msg } =>
                debug.print("Config error: {}\n", .{msg}),
        }
        return error.Config { .msg = "failed to load" };
    };

    run(cfg);
}
  

Tutorial 4: Interfaces and Polymorphism

  -- define an interface
dyn interface Animal {
    fn speak(*const self): []const u8;
    fn name(*const self): []const u8;
}

type Dog = struct { name_: []const u8 };
type Cat = struct { name_: []const u8 };

provide Animal for Dog {
    fn speak(*const self): []const u8 { return "woof"; }
    fn name(*const self):  []const u8 { return self.name_; }
}

provide Animal for Cat {
    fn speak(*const self): []const u8 { return "meow"; }
    fn name(*const self):  []const u8 { return self.name_; }
}

-- static dispatch (generic)
fn makeNoise[A: Animal](animal: *const A) {
    debug.print("{} says {}\n", .{animal.name(), animal.speak()});
}

-- dynamic dispatch (runtime polymorphism)
fn makeNoiseAll(animals: [](*dyn Animal)) {
    for a in animals {
        debug.print("{} says {}\n", .{a.*.name(), a.*.speak()});
    }
}

fn main(): void {
    var dog = Dog { .name_ = "Rex" };
    var cat = Cat { .name_ = "Whiskers" };

    makeNoise(&dog);   -- static, no overhead
    makeNoise(&cat);

    var animals = []*dyn Animal{ &dog, &cat };
    makeNoiseAll(&animals);
}
  

Tutorial 5: Asynchronous Programming

  const io_mod = import("std/async");
const net    = import("std/net");

fn main(ctx: process.Context): !void {
    using ctx.alloc;

    var io = try io_mod.IoUringScheduler::new(ctx.alloc, 256);
    defer io.deinit(ctx.alloc);

    try io.run();
}

fn serverMain(io: *Io, alloc: Allocator): !void {
    using alloc;

    var listener = try net.TcpListener::bind(io, "0.0.0.0:8080");
    defer listener.deinit();

    debug.print("Listening on :8080\n");

    -- structured concurrency: all connections joined on exit
    try io.scope([](s: *IoScope) {
        while true {
            var conn = try listener.accept(io);
            try s.spawn([move conn](_: *IoScope) {
                handleConnection(io, conn) catch |e| {
                    debug.print("connection error: {}\n", .{e});
                };
            });
        }
    });
}

fn handleConnection(io: *Io, conn: net.TcpConn): !void {
    defer conn.deinit();

    var buf: [4096]u8 = #undefined;
    var n = try conn.read(io, &buf);

    try conn.write(io, buf[0..n]);
}
  

Tutorial 6: Concurrency With Channels

  const sync = import("std/sync");

fn main(ctx: process.Context): !void {
    using ctx.alloc;

    var io = try io_mod.IoUringScheduler::new(alloc, 256);
    defer io.deinit(alloc);

    var ch = try sync.Channel[i32]::new(alloc, 32);
    defer ch.deinit(alloc);

    try io.scope([](s: *IoScope) {
        -- producer
        try s.spawn([move ch.sender()](_: *IoScope) {
            for i in 0..10 {
                try ch.send(i);
            }
            ch.close();
        });

        -- consumer
        try s.spawn([move ch.receiver()](_: *IoScope) {
            while ch.recv() |val| {
                debug.print("got {}\n", .{val});
            }
        });
    });
}
  

Tutorial 7: C FFI

  const c   = import("std/c");
const ffi = import("std/ffi");

-- calling C functions
fn countBytes(data: []const u8): usize {
    unsafe {
        return c.strlen(data.ptr);
    }
}

-- C callback with context
type SqliteCtx = struct {
    alloc:   Allocator,
    results: *List[[]const u8],
};

extern("C") fn sqliteCallback(
    user_data: *anyopaque,
    col_count: c.int,
    col_values: [*c][*c]const u8,
    _: [*c][*c]const u8,
): c.int {
    unsafe {
        var ctx = ffi.restore(*SqliteCtx, user_data);
        var i: usize = 0;
        while i < col_count as usize {
            var val: []const u8 = c.ptrToSlice(col_values[i]);
            ctx.results.push(ctx.alloc, val) catch return 1;
            i += 1;
        }
    }
    return 0;
}

-- importing C headers
const sqlite = #cImport({
    #cInclude("sqlite3.h");
});

fn query(db: *sqlite.sqlite3, sql: []const u8, alloc: Allocator): !List[[]const u8] {
    var results = List[[]const u8]::new();
    errdefer results.deinit(alloc);

    var ctx = SqliteCtx { .alloc = alloc, .results = &results };

    unsafe {
        var rc = sqlite.sqlite3_exec(
            db,
            sql.ptr,
            sqliteCallback,
            &ctx as *anyopaque,
            nil,
        );
        if rc != sqlite.SQLITE_OK {
            return error.SqliteError { .code = rc };
        }
    }

    return results;
}
  

Tutorial 8: HTTP Server [TBD]

  -- Note: std/http is a companion palette, not in std
-- violet pkg add violet-http

const http = import("violet-http");

fn main(ctx: process.Context): !void {
    using ctx.alloc;

    var io = try io_mod.IoUringScheduler::new(alloc, 1024);
    defer io.deinit(alloc);

    var server = try http.Server::new(io, alloc, .{
        .addr = "0.0.0.0",
        .port = 8080,
    });
    defer server.deinit(alloc);

    try server.get("/", handleRoot);
    try server.get("/health", handleHealth);
    try server.post("/echo", handleEcho);

    debug.print("Serving on :8080\n");
    try server.run(io);
}

fn handleRoot(req: *http.Request, res: *http.Response): !void {
    try res.status(200);
    try res.body("Hello from Violet!");
}

fn handleHealth(_: *http.Request, res: *http.Response): !void {
    try res.json(.{ .status = "ok" });
}

fn handleEcho(req: *http.Request, res: *http.Response): !void {
    var body = try req.readBody(req.alloc);
    defer req.alloc.free(body);
    try res.status(200);
    try res.body(body);
}
  

Incomplete / TBD

The following are accepted in principle but not yet formally specified or implemented.

Grammar

  • Full EBNF incorporating all accepted constructs
  • Scope-tagged pointer syntax (*s T, []s T, []_)
  • ctx parameter modifier
  • unsafe fn in interface declarations
  • requires T: not Interface in expand fn
  • Blanket and specialize provide syntax
  • Loop labels and break label
  • inline for multi-sequence with index variable
  • Anonymous scope elision []_
  • Multi-dimensional array syntax and indexing

Semantics

  • Scope tag propagation rules through generics
  • using resolution formal rules
  • ctx C ABI ordering rules
  • Linear type checker + errdefer interaction
  • Definite assignment analysis for #undefined
  • Blanket impl conflict detection
  • dyn interface object safety formal enumeration
  • error.Cancelled propagation rules

Features

  • Send and Sync marker interfaces (direction settled, not implemented)
  • Cell[T] and RefCell[T]
  • Box[T]
  • div and mod integer operators (vs / and %)
  • char as restricted u32 (settled, not in grammar)
  • Block comment syntax (* *) (settled, not in grammar)
  • Cranelift debug backend
  • Wild incremental linker integration
  • ARMv7 via Cwerg backend
  • WASM target (low priority)
  • violet expand tool
  • violet pkg publish and registry

Standard Library

  • std/http (companion palette)
  • std/tls (companion palette)
  • std/postgres, std/sqlite (companion palettes)
  • std/regex (companion palette)
  • Full std/unicode (grapheme clusters, normalization)
  • Full std/crypto (full suite, primitives done)
  • std/compress (deflate done, zstd companion palette)

Tooling

  • Formatter rules
  • LSP implementation
  • Test framework details
  • Doc comment markup and generation
  • Error message philosophy and templates

Appendix: Intrinsics Reference

IntrinsicDescription
#panic(msg, .{args})Unconditional panic, noreturn
#assert(cond)Panic if false, stripped in release
#assert(cond, msg)With message
#unreachable()UB in release, panic in debug
#trap()Hardware trap instruction
#setPanicHandler(fn)Compile-time panic handler override
#typeInfo(T)Comptime type information
#typeName(T)Comptime type name string
#typeHash(T)Comptime type identity hash
#sizeOf(T)Byte size of T
#alignOf(T)Alignment of T
#field(val, name)Access field by comptime name
#fieldGet(val, name)Get field value by name
#fieldSet(val, name, v)Set field value by name
#implements(T, I)Comptime interface check
#ident(a, b)Concatenate strings into identifier
#undefinedExplicit uninitialized value
#ptrAdd(ptr, n)Unsafe pointer arithmetic
#bitCast(val)Bit-level reinterpretation
#uncheckedIndex(s, i)Unchecked index, unsafe
#memcpy(dst, src, n)Raw memory copy
#embedFile("path")Embed file as []const u8
#embedDir("path")Embed directory listing
#readEnv("VAR")Build-time env var (root palette)
#compileTime()Build timestamp string
#gitRevision()Git revision string
#paletteVersion()Version from palette.kdl
#currentScheduler()Thread-local scheduler escape hatch
#structFrom(T, f, m)Generate struct from fields
#matchUnion(v, vs, h)Generate case over union variants
#targetArch()Comptime target architecture string
#compileError(msg, .{})Emit compile error
#asm(...)Inline assembly (see FFI tutorial)

Violet is designed and documented in this specification. The language is in active design and bootstrap compiler development. Contributions and feedback are welcome at https://gitlab.com/violet-lang