Vader
The discipline of static typing, the ergonomics of a script.
A general-purpose language with type inference, compiled to native code by a compiler written in Vader.
An experiment: the compiler compiles itself and runs the examples on both back ends, the VM and native code. The language still changes from one version to the next.
A Shape is a circle or a rectangle, and the match must handle both: forget one and the program does not compile.
module "main"
import "std/io"
import "std/math"
// A sum type: `match` must cover every variant.
Shape :: Circle | Rect
Circle :: struct { radius: f64 }
Rect :: struct { w: f64, h: f64 }
area :: fn(s: Shape) -> f64 = match s {
is Circle -> pi * s.radius * s.radius
is Rect -> s.w * s.h
}
main :: fn() -> i32 {
shapes: Shape[]: [
Circle { .radius = 2.0 },
Rect { .w = 3.0, .h = 4.0 },
]
total := 0.0
for s in shapes {
total += s.area() // UFCS: s.area() is area(s)
}
println("${shapes.len()} shapes, total area ${total}")
return 0
} 2 shapes, total area 24.566370614359172
There are no exceptions. sum_of returns the sum or the error, and its caller cannot use the number before ruling the error out.
module "main"
import "std/io"
import "std/string"
// A failure is a value: `parse_int` returns `i32 | ParseError`,
// and the sum can be used only once the error is ruled out.
sum_of :: fn(text: string) -> i32 | ParseError {
total := 0
for field in text.split(",") {
n :: parse_int(field.trim())
if n is ParseError {
return n
}
total += n
}
return total
}
main :: fn() -> i32 {
for input in ["1, 2, 3", "4, five"] {
result :: sum_of(input)
if result is ParseError {
println("${input}: ${result.msg}")
} else {
println("${input}: ${result}")
}
}
return 0
} 1, 2, 3: 6 4, five: parse_int: invalid digit
A @comptime constant runs ordinary Vader during the build: the program starts with the table already filled in.
module "main"
import "std/io"
squares :: fn(n: i32) -> i32[] {
out: i32[]: []
for i in 0..<n {
out.push(i * i)
}
return out
}
// Computed by the compiler: the program holds the table, not the loop.
@comptime SQUARES :: squares(8)
main :: fn() -> i32 {
println("${SQUARES}")
return 0
} [0, 1, 4, 9, 16, 25, 36, 49]
A function with a yield produces its values lazily, so an endless sequence can be filtered and cut as an iterator.
module "main"
import "std/io"
import "std/iter"
// A function with a `yield` is a generator: it produces its values
// one at a time, as the consumer asks for them.
fibonacci :: fn() -> Iterator<i64> {
a: i64 = 0
b: i64 = 1
for {
yield a
next :: a + b
a = b
b = next
}
}
main :: fn() -> i32 {
evens :: fibonacci().filter(n -> n % 2 == 0).take(6).collect()
println("${evens}")
return 0
} [0, 2, 8, 34, 144, 610]
Why Vader
Types you rarely write
Inference fills in the types of locals, lambdas and generic calls. :: binds a constant, := a variable, and that is the whole ceremony.
No null by accident
A value can be null only when its type says so, T | null. After if user == null { return }, the compiler knows user is a User.
Errors are values
There are no exceptions. A function that can fail returns a union, string | IOError, and the caller handles it like any other value.
Functions, not classes
There are no classes and no inheritance. a.f(b) calls f(a, b), so any function reads like a method of its first argument.
Compile time, not macros
@comptime runs ordinary Vader during the build and keeps what it computes: tables, constants, checks.
Written in Vader
The compiler compiles itself, to the same C byte for byte, and a C compiler is enough to rebuild it from the seed in the repository.
One command for everything
vader run: runs a program on the bytecode VM, with no build step.vader build: compiles a program to a native binary, through C.vader test: runs the@testfunctions of a project.vader fmt: formats the sources; it has no configuration.vader doc: writes the API reference of a project, as a site like the Library reference.vader new: starts a project.vader lspandvader dap: the language server and the debugger that the VS Code and IntelliJ extensions run.
Read on
Specification
The language, chapter by chapter: its types, functions, control flow, modules, decorators and compile-time execution.
Library
The standard library and the packages beside it: I/O, strings, collections, iterators, JSON, the build contract.
Compiler
The self-hosted compiler: its lexer and parser, the type checker, lowering, the back ends, and the tools around it.