andrewjk/nomen

0

stars

903

commits

TypeScript

primary language

Sep 14, 2026

updated

README

The Nomen Programming Language

Nomen is a statically-typed, memory managed language that compiles to C and AArch64 assembly (macOS only for now).

It's intended to make memory safe programming easier than it has traditionally been while maintaining a relatively small surface area.

Most types of memory corruption (use before initialization, use after free, double free) should be caught at compile time and there's a simple constraints system to ensure that array bounds are checked before their items are accessed.

Features

  • Typed Values - bools, ints, strings and so forth
  • Structs & Classes — value-type structs and reference-type classes
  • Automatic Memory Management#init functions to allocate resources, #destroy functions to clear them at scope exit
  • Ownershipmove instances to a new owner, or share instances by ref
  • Borrowed Slicesview instances are non-owning, zero-copy slices that can't outlive their source
  • Traits — interface-based polymorphism
  • Enums & Bitsets — sum types with associated data, plus composable bit flags
  • Operator Overloading — custom behavior for arithmetic operators
  • Generics — type-safe generic structs and classes with compile-time checking
  • Constraints — compile-time assertions on parameters, fields, and variables
  • Higher-Order Functions — first-class functions and lambdas (no closures)
  • Structured Concurrency — run concurrent tasks via OS threads
  • Core System Library - small but growing, with data structures that remove the need for you to fight with the borrow checker
  • GUI System - a top-down/bottom-up layout system and a few different native controls (WIP)
  • VS Code Extension - with syntax highlighting for .nm files as well as the usual LSP niceties

Quick Start

Installation

Install the Nomen CLI using npm (or your preferred package manager):

npm i -g nomen-lang

There is also a VS Code extension that you can install by searching for Nomen. It requires that you have the CLI installed first.

Run a Program

cd my_program
nomen run

# or
nomen --in path/to/program.nm

Target a specific backend:

nomen run --arch c         # emit C
nomen run --arch aarch64   # emit AArch64 assembly (default)

Hello, World!

import System

pub func main = () {
    Console.write_line("Hello, World!")
}

Command-Line Interface

The nomen CLI has several commands: init, run, build, check, format, test and docs. Top-level options:

CommandDescription
init <name>creates a new project folder for a program with the supplied name
runruns the program that is in the current folder, or the .nm file specified with --in
buildbuilds the program into an executable file
checkparses and compiles the program and reports warnings and errors
formatformats the code (defaults to tabs, 100 chars wide, sorted imports, implicit types, trailing commas)
testruns any .test.nm files in the current folder
docsgenerates a folder of documentation from code comments
OptionAliasDescription
--in <path>-iInput .nm file or folder (auto-discovered if omitted).
--out <path>-oDeclared but currently unused; output goes to <root>/build/.
--config <path>-cPath to a JSON build config file.
--watch-wRe-run the pipeline on file changes.
--arch <a>-aBackend: aarch64 (default) or c.
--platform <p>-pTarget: macos, ios, linux, android, windows, web (host-derived default).
--lib <path>-lPath to the System library directory.
--auditEnable memory auditing of the generated program.
--audit-runtimePath to audit_runtime.c (used with --audit).
--checkformat dry-run: report changes without writing.

See CLI.md for the full reference (input resolution, build output, config files, and examples).

Language Overview

See SPEC.md for the full language specification.

Value Types

bool
int     // and int8, int16, int32 and int64
uint    // and uint8, uint16, uint32 and uint64
float   // and float32 and float64
string
char
null

Variables

Variables can be const or var, and variable types can be nullable:

const name = "Alice"
var age = 30
var uint count = 10
var int? maybe = null

Strings

const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")

const multiline =
    "Multiline strings start
    "with a double quote
    "on each line

// slice(start, end) returns a non-owning view string over [start, end)
if greeting.length >= 5 {
    view hi = greeting.slice(0, 5)
    Console.write(hi.to_string())   // "Hello"
}

Functions

Note the out syntax for return types, which can be omitted when using the arrow form:

func add = (int a, int b, out int) {
    return a + b
}

pub func double = (int x) => x * 2

Default parameters are supported:

func greet = (string name = "world") {
    Console.write("Hello, \{name}!")
}
greet()         // "Hello, world!"
greet("Alice")  // "Hello, Alice!"

As are variadic parameters:

func sum = (...int numbers, out int) {
    var total = 0
    while i < numbers.length; i += 1 {
        total = total + numbers.at(i)
    }
    return total
}
sum(1, 2, 3)    //

Control Flow

if and else:

var x = 5
if x > 0 {
    Console.write("positive")
} else {
    Console.write("zero")
}

There is no else if, use a switch instead:

const x = 5
switch {
    case x > 100 {
        Console.write("big")
    }
    case x > 10 {
        Console.write("medium")
    }
    else {
        Console.write("small")
    }
}

while loops run while a condition is true and can take a post-condition that is run at the end of each loop:

var x = 0
while x < 10 {
    Console.write("\{x}")
}

var y = 0
while y < 10; y += 1 {
    Console.write("\{y}")
}

for loops run over a set of items and can also take a post-condition:

const numbers = [1, 2, 3]

for num of numbers {
    Console.write("\{num}")
}

var i = 1
for num of numbers; i += 1 {
    Console.write("\{i}: \{num}")
}

Inside a loop, break can be used to stop the loop and continue can be used to move to the next loop iteration.

You can use let or -> to return a value from any control flow statement (analogous to return or =>):

const x = 12

const y = if x > 100 -> "big"
          else -> "small"

const z = switch {
    case x > 100 -> "big"
    case x > 10 -> "medium"
    else -> "small"
}

There is also a match statement that we'll see a bit further down.

Structs

Structs are value types — assignment copies the fields. Construction calls a struct's auto-generated #init:

pub struct Point {
    pub var int x
    pub var int y

    pub func translate = (ref self, int dx, int dy) {
        self.x = self.x + dx
        self.y = self.y + dy
    }

    pub func distance_from_origin = (self, out int) {
        return self.x * self.x + self.y * self.y
    }
}

const p = Point(3, 4)
p.translate(1, 1)
const d = p.distance_from_origin()

Classes

Classes are reference types — always heap-allocated and shared on assignment. Methods use ref self for mutable access:

class Counter {
    var int count = 0

    func increment = (ref self) {
        self.count = self.count + 1
    }
}

var c = Counter()
c.increment()

Enums

Enums are sum types. Cases can carry associated data, and shorthand .case syntax works where the type is known:

pub enum Direction {
    case north
    case south
    case east
    case west
}

var Direction dir = .east

pub enum Shape {
    case circle(int radius)
    case rect(int width, int height)
}

const shape = Shape.rect(10, 20)

Pattern Matching

match compares a value against cases and can bind the data a case carries. A match on an enum is checked for exhaustiveness — cover every case, or add an else:

const message = match shape {
    case .circle(r) -> "radius \{r}"
    case .rect(w, h) -> "\{w}x\{h}"
}

Bitsets

A bitset defines flags meant to be combined with bitwise operators:

pub bitset Permissions {
    case read
    case write
    case execute
}

var flags = Permissions.read | Permissions.write
const can_write = (flags & Permissions.write) == Permissions.write
flags = flags ^ Permissions.execute

Traits

Traits declare method signatures that implementing structs provide. A concrete struct assigned to a trait-typed variable is callable through the trait's interface:

pub trait Printable {
    func to_string = (self, out string)
}

pub struct Point: Printable {
    pub var int x
    pub var int y

    pub func to_string = (self, out string) {
        return "Point(\{self.x}, \{self.y})"
    }
}

const Printable p = Point(1, 2)
const s = p.to_string()

Extension Methods

Add methods to an existing struct or class with an extend block. The keyword must match the type (extend struct for a struct, extend class for a class), and only methods may be added:

struct Point {
    var int x
    var int y
}

extend struct Point {
    pub func manhattan = (self, out int) {
        return self.x + self.y
    }
}

const p = Point(3, 4)
const m = p.manhattan()

Extended methods behave exactly like methods declared in the body — same dispatch, visibility, and overloading. extend blocks may sit before or after the type, and may target types from another module.

An extend may also make an existing type conform to one or more traits out of line by listing them after :. The required trait methods can live in the same extend, another extend, or the body:

trait Stringable {
    func to_string = (out string)
}

struct Circle {
    var int radius
}

extend struct Circle: Stringable {
    func to_string = (out string) {
        return "Circle"
    }
}

const Stringable s = Circle(5)

Auto-Derived Methods

Conforming to Stringable, Equatable, or Hashable auto-generates the matching method — the same way a struct gets an auto-generated #init. A hand-written method always wins, and the derivation only fires when every field is itself derivable:

pub struct Point: Equatable, Stringable {
    pub var int x
    pub var int y
}

const a = Point(1, 2)
const b = Point(1, 2)
const bool same = a == b            // true  — derived #op_eq
const bool diff = a != b            // false — derived from #op_eq
const string s = a.to_string()      // "Point(x=1, y=2)" — derived to_string

Operator Overloading

Structs define custom operator behavior with #-prefixed function names:

struct Vec2 {
    var int x
    var int y

    func #op_add = (self, Vec2 other, out Vec2) {
        return Vec2(self.x + other.x, self.y + other.y)
    }
}

const sum = Vec2(1, 2) + Vec2(3, 4)

Generics

Structs, classes, and free functions can declare type parameters. Instantiate a generic by passing concrete type arguments in angle brackets:

struct Box<T> {
    var T value
}

var Box<int> b = Box<int>(42)

Type parameters may carry trait bounds (<T: Named>, multiple with +); each concrete type argument must conform to its bound:

trait Named {
    func id = (self, out int)
}

struct Holder<T: Named> {
    var T item
}

Generic free functions infer their type arguments from the call site, so T is never written explicitly:

func unwrap<T> = (Box<T> box, out T) {
    return box.value
}

var Box<int> b = Box<int>(42)
var int v = unwrap(b)   // T inferred as int

Type parameters are type-erased at the storage level — all values are 8 bytes on aarch64, so T exists only for compile-time checking. The compiler emits one specialized copy per concrete instantiation (Box<int>Box_int). See GENERICS.md for the full design.

Constraints

Constraints are compile-time assertions on parameters, fields, and variables. They are checked whenever the value is a compile-time constant:

func restricted = (int x: x > 5) {
    Console.write("\{x}")
}

restricted(10)   // OK
restricted(2)    // Error: Parameter constraint not satisfied

Indexes within a range are considered safe:

func sum = (...int nums) {
    // not ok, because we don't know how many items are in nums:
    const first = nums.at(0)

    // ok, because we make sure we are in bounds:
    const second = if nums.length > 2 -> nums.at(1)
                   else -> -1

    // ok, because we know we are in bounds in each iteration:
    var result = 0
    for i in 0 .. nums.length {
        result += nums.at(i)
    }
}

Strings

const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")

Arrays

var numbers = [1, 2, 3, 4, 5]
const first = numbers.at(0)
numbers.set(1, 99)

const combined = [1, 2] + [3, 4]
const repeated = [1, 2] * 3

Tuples

Tuples are anonymous structs with positional fields _0, _1, etc and that support destructuring:

var things = [1, "first"]
Console.write("\{things._0} \{things._1}")

func get_person = (int id, out [string, int]) {
    return ["Andrew", id + 100]
}

var [name2, age2] = get_person(12)

Anonymous Structs

An inline [ field = value, ... ] literal is an anonymous struct — a temporary collection of named values. Used as a value it is inferred as a struct, so its fields can be read and destructured:

const p = [ name = "C", x = 25, y = 70 ]
Console.write("\{p.name} \{p.x} \{p.y}")
var [name, x, y] = p

To build a named struct, call its constructor. Seed a literal from it with .. to override fields that have declared defaults, applied after construction:

struct Circle {
    var string name
    var int center_x
    var int center_y
    var int radius
}

func print_circle = (Circle c) {
    Console.write("\{c.center_x},\{c.center_y},\{c.radius}")
}

print_circle(Circle("C", 25, 70, 15))

struct Layout {
    var int grow = 0
    var int shrink = 0
}

const Layout big = [ .. Layout(), grow = 2, shrink = 3 ]

The anonymous-struct type is inferred and has no source-level name, so it can only be used where its type can be inferred. Overrides may only target fields with a declared default; required fields are set positionally by the constructor call.

Destructuring

The var [ ... ] = expr form binds names by pulling values out of the right-hand side. Tuples, arrays, structs, and classes are all supported — the kind of value determines how the brackets are read:

// Tuples — bind positionally
func get_person = (int id, out [string, int]) {
    return ["Andrew", id + 100]
}
var [pname, page] = get_person(12)
var [a, b] = [11, "hello"]

// Arrays — bind positionally by index
const nums = [1, 2, 3]
var [first, second, third] = nums

// Structs and classes — bind by field name (bare name or `field = name`)
struct Point {
    var int x
    var int y
}
const p = Point(3, 4)
var [x, y] = p
var [x = px, y = py] = p

Standard Library

The standard System library is imported with import System.

Console

Console.write("no newline")
Console.write_line("with newline")

const string line = Console.read_line()
const char c = Console.read_char()
const string p = Console.platform()

Concurrency

Nomen uses structured concurrency via nurseries: every concurrent split rejoins before its lexical scope exits.

func fetch = (uint64 id) {
    Console.write_line("ok")
}

pub func main = () {
    async nursery {
        nursery.spawn(fetch(1))
        nursery.spawn(fetch(2))
        nursery.spawn(fetch(3))
        // block does not exit until all three fetches finish
    }
}

A Task handle lets you wait on or cancel a spawned call:

func compute = (uint64 n) => n + 1

pub func main = () {
    async nursery {
        var t = nursery.spawn(compute(41))
        t.wait()
        var r = t.result_uint64()
    }
}

See ASYNC.md for the full design.

Memory Management

Nomen cleans up automatically at scope exit — no garbage collector, no reference counting. The compiler inserts the frees for you. Two hooks let types participate:

struct Transaction {
    var int handle

    func #init = (self, int handle) {
        self.handle = handle
    }

    func #destroy = () {
        // runs automatically when a Transaction goes out of scope
    }
}
  • #init customizes construction (an auto-generated one exists otherwise)
  • #destroy runs at scope exit for structs and classes that own resources
  • Heap strings and class instances are freed automatically

See MEMORY.md for the full model.

Ownership & Borrows

Class instances are heap-allocated and, by default, shared on assignment. To express single ownership, Nomen borrows a few ideas from move semantics:

class Box {
    var int value
}

// an owning field — only classes can hold classes, and only via move
class Holder {
    move Box content
}

// an owning parameter — the caller gives up access with `move`
func take = (move Box b) {
    Console.write("\{b.value}")
}

var h = Holder(move Box(7))
var b = Box(42)
take(move b)   // b is invalid after this
  • move marks a class-typed field or parameter as owned (moved in).
  • ref passes a value by reference so the callee can mutate it; the caller must write ref at the call site, and a const value can't be borrowed mutably.
  • Plain parameters are read-only — take them by value and make a local var copy if you need a mutable scratch value the caller never sees.
  • swap atomically moves a value out and replaces it with a fresh one

The same borrow machinery backs non-owning slices. string.slice(start, end) returns a view string — an O(1) (ptr, len) borrow of the source's buffer. The checker guarantees a view can't outlive its source and is invalidated once the source is reassigned (which frees the buffer it points into):

var string s = "hello world"
view v = s.slice(0, 5)               // borrows from s
Console.write(v.to_string())         // "hello" — materializes an owned copy
s = "changed"                        // frees s's old buffer → v dangles
Console.write("\{v.length}")         // Error: borrow invalidated

See BORROW.md for the rules and the borrow-invalidation checks.

GUI

Nomen ships a native UI layer in System::Controls: windows, text, buttons, checkboxes, and a layout engine + compositor. The example app in app/ is a small todo-list GUI built with it.

import System
import System::Controls

pub func main = () {
    var Window win = Window("Nomen", 400, 300)
    var Text title = Text(win)
    title.set_text("Hello")
    win.show()
}

The layout engine is constraints-down, sizes-up (like Flutter/SwiftUI): parents hand each child a size range, children report their intrinsic size, and the engine resolves it into pixel frames. See GUI.md for the full layout and compositor design.

Questions

Why create a new language? I'm hoping to find the sweet spot between the ease of use of garbage collected languages and the power of manual memory allocated languages, which I don't think anyone has found yet.

Was AI used in the development of this programming language? Yes, at the start of 2026 this was a much smaller hand-developed language with a half implemented C backend. Since then it has gained a fully implemented C backend, fully implemented AArch64 backend, and many features, all produced by AI under human guidance.

Benchmarks

Adapted from Programming Language Benchmarks. Run on my laptop. Any errors in adaptation are my fault.

Run times (single-size)

BenchmarkNomen/ANomen/CGoZigRustCompare
helloworld3ms3ms4ms4ms4ms0.8-0.8x
knucleotide8ms5ms18ms7ms8ms0.4-1.1x
regex-redux24ms13ms16ms18ms4ms1.3-6.0x

Run times (small)

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits29ms19ms11ms33ms20ms0.9-2.6x
fannkuch-redux168ms108ms39ms206ms127ms0.8-4.3x
binarytrees146ms124ms146ms129ms103ms1.0-1.4x
merkletrees157ms141ms278ms125ms136ms0.6-1.3x
nsieve44ms35msFAIL41ms43ms1.0-1.1x
lru7ms4ms7ms5ms6ms1.0-1.4x
json-serde16ms11ms4ms7ms9ms1.8-4.0x
nbody24ms24ms37ms21ms15ms0.6-1.6x
spectral-norm22ms11ms12ms12ms9ms1.8-2.4x
mandelbrot54ms51ms99ms15ms14ms0.5-3.9x
edigits4ms3ms5ms4ms3ms0.8-1.3x

Run times (large)

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits502ms336ms133ms573ms313ms0.9-3.8x
fannkuch-redux2046ms1267ms390ms2476ms1485ms0.8-5.2x
binarytrees1576ms1322ms1841ms1369ms1103ms0.9-1.4x
merkletrees732ms647ms1399ms584ms644ms0.5-1.3x
nsieve168ms134msFAIL279ms303ms0.6-0.6x
lru21ms10ms18ms7ms13ms1.2-3.0x
json-serde64ms35ms4ms23ms35ms1.8-16.0x
nbody218ms206ms335ms182ms128ms0.7-1.7x
spectral-norm181ms83ms84ms84ms61ms2.2-3.0x
mandelbrot205ms195ms381ms50ms48ms0.5-4.3x
edigits5ms5ms4ms4ms4ms1.2-1.2x

Compile times

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits1387ms672ms189ms5641ms3079ms0.2-7.3x
helloworld418ms392ms94ms4980ms2086ms0.1-4.4x
fannkuch-redux512ms426ms97ms5061ms2124ms0.1-5.3x
binarytrees512ms441ms102ms5148ms2017ms0.1-5.0x
merkletrees463ms406ms94ms4958ms1992ms0.1-4.9x
nsieve458ms403msFAIL4819ms2057ms0.1-0.2x
lru610ms461ms136ms5055ms2190ms0.1-4.5x
knucleotide610ms475ms90ms5521ms2444ms0.1-6.8x
json-serde719ms539ms98ms6203ms2995ms0.1-7.3x
regex-redux787ms534ms167ms5693ms7159ms0.1-4.7x
nbody706ms522ms170ms5093ms2198ms0.1-4.2x
spectral-norm486ms424ms94ms4983ms2364ms0.1-5.2x
mandelbrot465ms400ms98ms5165ms2139ms0.1-4.7x
edigits723ms582ms96ms5339ms2551ms0.1-7.5x

Contributors

andrewjk

903 commits

andrewjk/nomen

0

stars

903

commits

TypeScript

primary language

Sep 14, 2026

updated

README

The Nomen Programming Language

Nomen is a statically-typed, memory managed language that compiles to C and AArch64 assembly (macOS only for now).

It's intended to make memory safe programming easier than it has traditionally been while maintaining a relatively small surface area.

Most types of memory corruption (use before initialization, use after free, double free) should be caught at compile time and there's a simple constraints system to ensure that array bounds are checked before their items are accessed.

Features

  • Typed Values - bools, ints, strings and so forth
  • Structs & Classes — value-type structs and reference-type classes
  • Automatic Memory Management#init functions to allocate resources, #destroy functions to clear them at scope exit
  • Ownershipmove instances to a new owner, or share instances by ref
  • Borrowed Slicesview instances are non-owning, zero-copy slices that can't outlive their source
  • Traits — interface-based polymorphism
  • Enums & Bitsets — sum types with associated data, plus composable bit flags
  • Operator Overloading — custom behavior for arithmetic operators
  • Generics — type-safe generic structs and classes with compile-time checking
  • Constraints — compile-time assertions on parameters, fields, and variables
  • Higher-Order Functions — first-class functions and lambdas (no closures)
  • Structured Concurrency — run concurrent tasks via OS threads
  • Core System Library - small but growing, with data structures that remove the need for you to fight with the borrow checker
  • GUI System - a top-down/bottom-up layout system and a few different native controls (WIP)
  • VS Code Extension - with syntax highlighting for .nm files as well as the usual LSP niceties

Quick Start

Installation

Install the Nomen CLI using npm (or your preferred package manager):

npm i -g nomen-lang

There is also a VS Code extension that you can install by searching for Nomen. It requires that you have the CLI installed first.

Run a Program

cd my_program
nomen run

# or
nomen --in path/to/program.nm

Target a specific backend:

nomen run --arch c         # emit C
nomen run --arch aarch64   # emit AArch64 assembly (default)

Hello, World!

import System

pub func main = () {
    Console.write_line("Hello, World!")
}

Command-Line Interface

The nomen CLI has several commands: init, run, build, check, format, test and docs. Top-level options:

CommandDescription
init <name>creates a new project folder for a program with the supplied name
runruns the program that is in the current folder, or the .nm file specified with --in
buildbuilds the program into an executable file
checkparses and compiles the program and reports warnings and errors
formatformats the code (defaults to tabs, 100 chars wide, sorted imports, implicit types, trailing commas)
testruns any .test.nm files in the current folder
docsgenerates a folder of documentation from code comments
OptionAliasDescription
--in <path>-iInput .nm file or folder (auto-discovered if omitted).
--out <path>-oDeclared but currently unused; output goes to <root>/build/.
--config <path>-cPath to a JSON build config file.
--watch-wRe-run the pipeline on file changes.
--arch <a>-aBackend: aarch64 (default) or c.
--platform <p>-pTarget: macos, ios, linux, android, windows, web (host-derived default).
--lib <path>-lPath to the System library directory.
--auditEnable memory auditing of the generated program.
--audit-runtimePath to audit_runtime.c (used with --audit).
--checkformat dry-run: report changes without writing.

See CLI.md for the full reference (input resolution, build output, config files, and examples).

Language Overview

See SPEC.md for the full language specification.

Value Types

bool
int     // and int8, int16, int32 and int64
uint    // and uint8, uint16, uint32 and uint64
float   // and float32 and float64
string
char
null

Variables

Variables can be const or var, and variable types can be nullable:

const name = "Alice"
var age = 30
var uint count = 10
var int? maybe = null

Strings

const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")

const multiline =
    "Multiline strings start
    "with a double quote
    "on each line

// slice(start, end) returns a non-owning view string over [start, end)
if greeting.length >= 5 {
    view hi = greeting.slice(0, 5)
    Console.write(hi.to_string())   // "Hello"
}

Functions

Note the out syntax for return types, which can be omitted when using the arrow form:

func add = (int a, int b, out int) {
    return a + b
}

pub func double = (int x) => x * 2

Default parameters are supported:

func greet = (string name = "world") {
    Console.write("Hello, \{name}!")
}
greet()         // "Hello, world!"
greet("Alice")  // "Hello, Alice!"

As are variadic parameters:

func sum = (...int numbers, out int) {
    var total = 0
    while i < numbers.length; i += 1 {
        total = total + numbers.at(i)
    }
    return total
}
sum(1, 2, 3)    //

Control Flow

if and else:

var x = 5
if x > 0 {
    Console.write("positive")
} else {
    Console.write("zero")
}

There is no else if, use a switch instead:

const x = 5
switch {
    case x > 100 {
        Console.write("big")
    }
    case x > 10 {
        Console.write("medium")
    }
    else {
        Console.write("small")
    }
}

while loops run while a condition is true and can take a post-condition that is run at the end of each loop:

var x = 0
while x < 10 {
    Console.write("\{x}")
}

var y = 0
while y < 10; y += 1 {
    Console.write("\{y}")
}

for loops run over a set of items and can also take a post-condition:

const numbers = [1, 2, 3]

for num of numbers {
    Console.write("\{num}")
}

var i = 1
for num of numbers; i += 1 {
    Console.write("\{i}: \{num}")
}

Inside a loop, break can be used to stop the loop and continue can be used to move to the next loop iteration.

You can use let or -> to return a value from any control flow statement (analogous to return or =>):

const x = 12

const y = if x > 100 -> "big"
          else -> "small"

const z = switch {
    case x > 100 -> "big"
    case x > 10 -> "medium"
    else -> "small"
}

There is also a match statement that we'll see a bit further down.

Structs

Structs are value types — assignment copies the fields. Construction calls a struct's auto-generated #init:

pub struct Point {
    pub var int x
    pub var int y

    pub func translate = (ref self, int dx, int dy) {
        self.x = self.x + dx
        self.y = self.y + dy
    }

    pub func distance_from_origin = (self, out int) {
        return self.x * self.x + self.y * self.y
    }
}

const p = Point(3, 4)
p.translate(1, 1)
const d = p.distance_from_origin()

Classes

Classes are reference types — always heap-allocated and shared on assignment. Methods use ref self for mutable access:

class Counter {
    var int count = 0

    func increment = (ref self) {
        self.count = self.count + 1
    }
}

var c = Counter()
c.increment()

Enums

Enums are sum types. Cases can carry associated data, and shorthand .case syntax works where the type is known:

pub enum Direction {
    case north
    case south
    case east
    case west
}

var Direction dir = .east

pub enum Shape {
    case circle(int radius)
    case rect(int width, int height)
}

const shape = Shape.rect(10, 20)

Pattern Matching

match compares a value against cases and can bind the data a case carries. A match on an enum is checked for exhaustiveness — cover every case, or add an else:

const message = match shape {
    case .circle(r) -> "radius \{r}"
    case .rect(w, h) -> "\{w}x\{h}"
}

Bitsets

A bitset defines flags meant to be combined with bitwise operators:

pub bitset Permissions {
    case read
    case write
    case execute
}

var flags = Permissions.read | Permissions.write
const can_write = (flags & Permissions.write) == Permissions.write
flags = flags ^ Permissions.execute

Traits

Traits declare method signatures that implementing structs provide. A concrete struct assigned to a trait-typed variable is callable through the trait's interface:

pub trait Printable {
    func to_string = (self, out string)
}

pub struct Point: Printable {
    pub var int x
    pub var int y

    pub func to_string = (self, out string) {
        return "Point(\{self.x}, \{self.y})"
    }
}

const Printable p = Point(1, 2)
const s = p.to_string()

Extension Methods

Add methods to an existing struct or class with an extend block. The keyword must match the type (extend struct for a struct, extend class for a class), and only methods may be added:

struct Point {
    var int x
    var int y
}

extend struct Point {
    pub func manhattan = (self, out int) {
        return self.x + self.y
    }
}

const p = Point(3, 4)
const m = p.manhattan()

Extended methods behave exactly like methods declared in the body — same dispatch, visibility, and overloading. extend blocks may sit before or after the type, and may target types from another module.

An extend may also make an existing type conform to one or more traits out of line by listing them after :. The required trait methods can live in the same extend, another extend, or the body:

trait Stringable {
    func to_string = (out string)
}

struct Circle {
    var int radius
}

extend struct Circle: Stringable {
    func to_string = (out string) {
        return "Circle"
    }
}

const Stringable s = Circle(5)

Auto-Derived Methods

Conforming to Stringable, Equatable, or Hashable auto-generates the matching method — the same way a struct gets an auto-generated #init. A hand-written method always wins, and the derivation only fires when every field is itself derivable:

pub struct Point: Equatable, Stringable {
    pub var int x
    pub var int y
}

const a = Point(1, 2)
const b = Point(1, 2)
const bool same = a == b            // true  — derived #op_eq
const bool diff = a != b            // false — derived from #op_eq
const string s = a.to_string()      // "Point(x=1, y=2)" — derived to_string

Operator Overloading

Structs define custom operator behavior with #-prefixed function names:

struct Vec2 {
    var int x
    var int y

    func #op_add = (self, Vec2 other, out Vec2) {
        return Vec2(self.x + other.x, self.y + other.y)
    }
}

const sum = Vec2(1, 2) + Vec2(3, 4)

Generics

Structs, classes, and free functions can declare type parameters. Instantiate a generic by passing concrete type arguments in angle brackets:

struct Box<T> {
    var T value
}

var Box<int> b = Box<int>(42)

Type parameters may carry trait bounds (<T: Named>, multiple with +); each concrete type argument must conform to its bound:

trait Named {
    func id = (self, out int)
}

struct Holder<T: Named> {
    var T item
}

Generic free functions infer their type arguments from the call site, so T is never written explicitly:

func unwrap<T> = (Box<T> box, out T) {
    return box.value
}

var Box<int> b = Box<int>(42)
var int v = unwrap(b)   // T inferred as int

Type parameters are type-erased at the storage level — all values are 8 bytes on aarch64, so T exists only for compile-time checking. The compiler emits one specialized copy per concrete instantiation (Box<int>Box_int). See GENERICS.md for the full design.

Constraints

Constraints are compile-time assertions on parameters, fields, and variables. They are checked whenever the value is a compile-time constant:

func restricted = (int x: x > 5) {
    Console.write("\{x}")
}

restricted(10)   // OK
restricted(2)    // Error: Parameter constraint not satisfied

Indexes within a range are considered safe:

func sum = (...int nums) {
    // not ok, because we don't know how many items are in nums:
    const first = nums.at(0)

    // ok, because we make sure we are in bounds:
    const second = if nums.length > 2 -> nums.at(1)
                   else -> -1

    // ok, because we know we are in bounds in each iteration:
    var result = 0
    for i in 0 .. nums.length {
        result += nums.at(i)
    }
}

Strings

const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")

Arrays

var numbers = [1, 2, 3, 4, 5]
const first = numbers.at(0)
numbers.set(1, 99)

const combined = [1, 2] + [3, 4]
const repeated = [1, 2] * 3

Tuples

Tuples are anonymous structs with positional fields _0, _1, etc and that support destructuring:

var things = [1, "first"]
Console.write("\{things._0} \{things._1}")

func get_person = (int id, out [string, int]) {
    return ["Andrew", id + 100]
}

var [name2, age2] = get_person(12)

Anonymous Structs

An inline [ field = value, ... ] literal is an anonymous struct — a temporary collection of named values. Used as a value it is inferred as a struct, so its fields can be read and destructured:

const p = [ name = "C", x = 25, y = 70 ]
Console.write("\{p.name} \{p.x} \{p.y}")
var [name, x, y] = p

To build a named struct, call its constructor. Seed a literal from it with .. to override fields that have declared defaults, applied after construction:

struct Circle {
    var string name
    var int center_x
    var int center_y
    var int radius
}

func print_circle = (Circle c) {
    Console.write("\{c.center_x},\{c.center_y},\{c.radius}")
}

print_circle(Circle("C", 25, 70, 15))

struct Layout {
    var int grow = 0
    var int shrink = 0
}

const Layout big = [ .. Layout(), grow = 2, shrink = 3 ]

The anonymous-struct type is inferred and has no source-level name, so it can only be used where its type can be inferred. Overrides may only target fields with a declared default; required fields are set positionally by the constructor call.

Destructuring

The var [ ... ] = expr form binds names by pulling values out of the right-hand side. Tuples, arrays, structs, and classes are all supported — the kind of value determines how the brackets are read:

// Tuples — bind positionally
func get_person = (int id, out [string, int]) {
    return ["Andrew", id + 100]
}
var [pname, page] = get_person(12)
var [a, b] = [11, "hello"]

// Arrays — bind positionally by index
const nums = [1, 2, 3]
var [first, second, third] = nums

// Structs and classes — bind by field name (bare name or `field = name`)
struct Point {
    var int x
    var int y
}
const p = Point(3, 4)
var [x, y] = p
var [x = px, y = py] = p

Standard Library

The standard System library is imported with import System.

Console

Console.write("no newline")
Console.write_line("with newline")

const string line = Console.read_line()
const char c = Console.read_char()
const string p = Console.platform()

Concurrency

Nomen uses structured concurrency via nurseries: every concurrent split rejoins before its lexical scope exits.

func fetch = (uint64 id) {
    Console.write_line("ok")
}

pub func main = () {
    async nursery {
        nursery.spawn(fetch(1))
        nursery.spawn(fetch(2))
        nursery.spawn(fetch(3))
        // block does not exit until all three fetches finish
    }
}

A Task handle lets you wait on or cancel a spawned call:

func compute = (uint64 n) => n + 1

pub func main = () {
    async nursery {
        var t = nursery.spawn(compute(41))
        t.wait()
        var r = t.result_uint64()
    }
}

See ASYNC.md for the full design.

Memory Management

Nomen cleans up automatically at scope exit — no garbage collector, no reference counting. The compiler inserts the frees for you. Two hooks let types participate:

struct Transaction {
    var int handle

    func #init = (self, int handle) {
        self.handle = handle
    }

    func #destroy = () {
        // runs automatically when a Transaction goes out of scope
    }
}
  • #init customizes construction (an auto-generated one exists otherwise)
  • #destroy runs at scope exit for structs and classes that own resources
  • Heap strings and class instances are freed automatically

See MEMORY.md for the full model.

Ownership & Borrows

Class instances are heap-allocated and, by default, shared on assignment. To express single ownership, Nomen borrows a few ideas from move semantics:

class Box {
    var int value
}

// an owning field — only classes can hold classes, and only via move
class Holder {
    move Box content
}

// an owning parameter — the caller gives up access with `move`
func take = (move Box b) {
    Console.write("\{b.value}")
}

var h = Holder(move Box(7))
var b = Box(42)
take(move b)   // b is invalid after this
  • move marks a class-typed field or parameter as owned (moved in).
  • ref passes a value by reference so the callee can mutate it; the caller must write ref at the call site, and a const value can't be borrowed mutably.
  • Plain parameters are read-only — take them by value and make a local var copy if you need a mutable scratch value the caller never sees.
  • swap atomically moves a value out and replaces it with a fresh one

The same borrow machinery backs non-owning slices. string.slice(start, end) returns a view string — an O(1) (ptr, len) borrow of the source's buffer. The checker guarantees a view can't outlive its source and is invalidated once the source is reassigned (which frees the buffer it points into):

var string s = "hello world"
view v = s.slice(0, 5)               // borrows from s
Console.write(v.to_string())         // "hello" — materializes an owned copy
s = "changed"                        // frees s's old buffer → v dangles
Console.write("\{v.length}")         // Error: borrow invalidated

See BORROW.md for the rules and the borrow-invalidation checks.

GUI

Nomen ships a native UI layer in System::Controls: windows, text, buttons, checkboxes, and a layout engine + compositor. The example app in app/ is a small todo-list GUI built with it.

import System
import System::Controls

pub func main = () {
    var Window win = Window("Nomen", 400, 300)
    var Text title = Text(win)
    title.set_text("Hello")
    win.show()
}

The layout engine is constraints-down, sizes-up (like Flutter/SwiftUI): parents hand each child a size range, children report their intrinsic size, and the engine resolves it into pixel frames. See GUI.md for the full layout and compositor design.

Questions

Why create a new language? I'm hoping to find the sweet spot between the ease of use of garbage collected languages and the power of manual memory allocated languages, which I don't think anyone has found yet.

Was AI used in the development of this programming language? Yes, at the start of 2026 this was a much smaller hand-developed language with a half implemented C backend. Since then it has gained a fully implemented C backend, fully implemented AArch64 backend, and many features, all produced by AI under human guidance.

Benchmarks

Adapted from Programming Language Benchmarks. Run on my laptop. Any errors in adaptation are my fault.

Run times (single-size)

BenchmarkNomen/ANomen/CGoZigRustCompare
helloworld3ms3ms4ms4ms4ms0.8-0.8x
knucleotide8ms5ms18ms7ms8ms0.4-1.1x
regex-redux24ms13ms16ms18ms4ms1.3-6.0x

Run times (small)

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits29ms19ms11ms33ms20ms0.9-2.6x
fannkuch-redux168ms108ms39ms206ms127ms0.8-4.3x
binarytrees146ms124ms146ms129ms103ms1.0-1.4x
merkletrees157ms141ms278ms125ms136ms0.6-1.3x
nsieve44ms35msFAIL41ms43ms1.0-1.1x
lru7ms4ms7ms5ms6ms1.0-1.4x
json-serde16ms11ms4ms7ms9ms1.8-4.0x
nbody24ms24ms37ms21ms15ms0.6-1.6x
spectral-norm22ms11ms12ms12ms9ms1.8-2.4x
mandelbrot54ms51ms99ms15ms14ms0.5-3.9x
edigits4ms3ms5ms4ms3ms0.8-1.3x

Run times (large)

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits502ms336ms133ms573ms313ms0.9-3.8x
fannkuch-redux2046ms1267ms390ms2476ms1485ms0.8-5.2x
binarytrees1576ms1322ms1841ms1369ms1103ms0.9-1.4x
merkletrees732ms647ms1399ms584ms644ms0.5-1.3x
nsieve168ms134msFAIL279ms303ms0.6-0.6x
lru21ms10ms18ms7ms13ms1.2-3.0x
json-serde64ms35ms4ms23ms35ms1.8-16.0x
nbody218ms206ms335ms182ms128ms0.7-1.7x
spectral-norm181ms83ms84ms84ms61ms2.2-3.0x
mandelbrot205ms195ms381ms50ms48ms0.5-4.3x
edigits5ms5ms4ms4ms4ms1.2-1.2x

Compile times

BenchmarkNomen/ANomen/CGoZigRustCompare
pidigits1387ms672ms189ms5641ms3079ms0.2-7.3x
helloworld418ms392ms94ms4980ms2086ms0.1-4.4x
fannkuch-redux512ms426ms97ms5061ms2124ms0.1-5.3x
binarytrees512ms441ms102ms5148ms2017ms0.1-5.0x
merkletrees463ms406ms94ms4958ms1992ms0.1-4.9x
nsieve458ms403msFAIL4819ms2057ms0.1-0.2x
lru610ms461ms136ms5055ms2190ms0.1-4.5x
knucleotide610ms475ms90ms5521ms2444ms0.1-6.8x
json-serde719ms539ms98ms6203ms2995ms0.1-7.3x
regex-redux787ms534ms167ms5693ms7159ms0.1-4.7x
nbody706ms522ms170ms5093ms2198ms0.1-4.2x
spectral-norm486ms424ms94ms4983ms2364ms0.1-5.2x
mandelbrot465ms400ms98ms5165ms2139ms0.1-4.7x
edigits723ms582ms96ms5339ms2551ms0.1-7.5x

Contributors

andrewjk

903 commits

Languages

TypeScript

95.9%

Zig

2.4%