Skip to content


Repository files navigation


Void is a high performance WebAssembly language with an emphasis on full stack web development.

fn fib(n: i32) -> i32
  if n < 2 then:
    fib(n - 1) + fib(n - 2)

fn main() -> void
  for n in range(15)
    print fib(n)
fn app() -> JSX::Element
  let todo_items = ["wake up", "eat", "code", "sleep"]
      ${ i => <li>${i}</li>}


Void is in it's very early stages of development. Void is not ready for public announcement or use. Some core syntax and semantics are subject to change. Expect frequent breaking changes. In addition, many documented features are not yet implemented.


  • Functional
  • Hybrid Nominal & Structural type system
  • Algebraic effects
  • First class wasm support
  • Macros and language extensions
  • Uniform function call syntax
  • Homoiconic
  • Pythonesque syntax that de-sugars into a lisp like dialect
    • Parenthesis can be elided in most cases
    • Infix notation and standard function call notation support

Guiding Principles:

  • Fun to write and read.
  • Predictability
  • Hackability
  • Balance a great developer experience with performance
  • Play nice with others

Getting Started


npm i -g voidc


voidc path/to/code.void


Currently requires node v22 nightly

fnm --node-dist-mirror install v22



// Comments are single line and are marked with a c style slash slash

Primitive Types

true // Boolean
false // Boolean
1 // i32 by default (32 bit integer)
1.0 // f32 by default (32 bit float)
"Hello!" // String, can be multiline, supports interpolation via ${}
[1, 2, 3] // Tuple literal
{x: 2, y: 4} // Object literal
#[1, 2, 3] // Array
#{x: 3, y: 4} // Dictionary


// Immutable variable
let my_immutable_var = 7

// Mutable variable
var my_var = 7


A Basic function:

fn add(a: i32, b: i32) -> i32
  a + b

In most cases the return type can be inferred

fn add(a:i32, b:i32) = a + b // The equal sign is used when the function is written on one line

To call a function, use the function name followed by the arguments in parenthesis

add(1, 2)

Void also supports uniform function call syntax (UFCS), allowing functions to be called on a type as if they were methods of that type.


Labeled arguments

Labeled arguments can be defined by wrapping parameters you wish to be labeled on call in curly braces.

fn add(a: i32, {to: i32}) = a + to

add(1, to: 2)

By default, the argument label is the same as the parameter name. You can override this by specifying the label before the argument name.

fn add(a: i32, {to:b: i32}) = a + b

add(1, to: 2)

Labeled arguments can be thought of as syntactic sugar for defining a object type parameter and destructuring it in the function body[1]:

fn move({ x: i32 y: i32 z: i32 }) -> void
  // ...

// Semantically equivalent to:
fn move(vec: { x: i32 y: i32 z: i32 }) -> void
  let { x, y, z } = vec
  // ...

move(x: 1, y: 2, z: 3)

// Equivalent to:
move({ x: 1, y: 2, z: 3 })

This allows you to still use object literal syntax for labeled arguments when it might be cleaner to do so. For example, when the variable names match the argument labels:

let [x, y, z] = [1, 2, 3]

// Object field shorthand allows for this:
move({ x, y, z })

// Which is better than
move(x: x, y: y, z: z)

[1] The compiler will typically optimize this away, so there is no performance penalty for using labeled arguments.

Control flow

If statements

if 3 < val then:
  "hello" // true case
  "bye" // false case (optional)

Ifs are expressions that return a value

let x = if 3 < val then: "hello" else: "bye"


Basic loops repeat until returned from

var a = 0
  if a > 10
    return a
  a += 1

Loops can be labeled

var a = 0
loop name: "increment"
  if a > 10
    return_from "increment" a
  a += 1

Useful constructs from looping through iterables

for item in iterable
  print item

Match Statements

let x = 3
match x
  1 => print "One"
  2 => print "Two"
  3 => print "Three"
  _ =>
    // Match statements are exhaustive, they must cover every possible
    // case. When not every case is covered, a default handler must be
    // provided.
    write "A number"


let double = n => n * 2 n => n * 2

Dot Notation

The dot is a simple form of syntactic sugar

let x = 4


// Translates to


fn add<T>(a: T, b: T) -> T
  a + b

With trait constraints

fn add<T impls Numeric>(a: T, b: T) -> T
  a + b