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.
#init functions to allocate resources, #destroy functions to clear them at scope exitmove instances to a new owner, or share instances by refview instances are non-owning, zero-copy slices that can't outlive their source.nm files as well as the usual LSP nicetiesInstall 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.
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)
import System
pub func main = () {
Console.write_line("Hello, World!")
}
The nomen CLI has several commands: init, run, build, check, format, test and docs. Top-level options:
| Command | Description |
|---|---|
init <name> | creates a new project folder for a program with the supplied name |
run | runs the program that is in the current folder, or the .nm file specified with --in |
build | builds the program into an executable file |
check | parses and compiles the program and reports warnings and errors |
format | formats the code (defaults to tabs, 100 chars wide, sorted imports, implicit types, trailing commas) |
test | runs any .test.nm files in the current folder |
docs | generates a folder of documentation from code comments |
| Option | Alias | Description |
|---|---|---|
--in <path> | -i | Input .nm file or folder (auto-discovered if omitted). |
--out <path> | -o | Declared but currently unused; output goes to <root>/build/. |
--config <path> | -c | Path to a JSON build config file. |
--watch | -w | Re-run the pipeline on file changes. |
--arch <a> | -a | Backend: aarch64 (default) or c. |
--platform <p> | -p | Target: macos, ios, linux, android, windows, web (host-derived default). |
--lib <path> | -l | Path to the System library directory. |
--audit | Enable memory auditing of the generated program. | |
--audit-runtime | Path to audit_runtime.c (used with --audit). | |
--check | format dry-run: report changes without writing. |
See CLI.md for the full reference (input resolution, build output, config files, and examples).
See SPEC.md for the full language specification.
bool
int // and int8, int16, int32 and int64
uint // and uint8, uint16, uint32 and uint64
float // and float32 and float64
string
char
null
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
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"
}
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) //
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 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 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 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)
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}"
}
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 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()
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)
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
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)
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 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)
}
}
const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")
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 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)
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.
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
The standard System library is imported with import System.
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()
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.
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 resourcesSee MEMORY.md for the full model.
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.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 oneThe 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.
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.
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.
Adapted from Programming Language Benchmarks. Run on my laptop. Any errors in adaptation are my fault.
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| helloworld | 3ms | 3ms | 4ms | 4ms | 4ms | 0.8-0.8x |
| knucleotide | 8ms | 5ms | 18ms | 7ms | 8ms | 0.4-1.1x |
| regex-redux | 24ms | 13ms | 16ms | 18ms | 4ms | 1.3-6.0x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 29ms | 19ms | 11ms | 33ms | 20ms | 0.9-2.6x |
| fannkuch-redux | 168ms | 108ms | 39ms | 206ms | 127ms | 0.8-4.3x |
| binarytrees | 146ms | 124ms | 146ms | 129ms | 103ms | 1.0-1.4x |
| merkletrees | 157ms | 141ms | 278ms | 125ms | 136ms | 0.6-1.3x |
| nsieve | 44ms | 35ms | FAIL | 41ms | 43ms | 1.0-1.1x |
| lru | 7ms | 4ms | 7ms | 5ms | 6ms | 1.0-1.4x |
| json-serde | 16ms | 11ms | 4ms | 7ms | 9ms | 1.8-4.0x |
| nbody | 24ms | 24ms | 37ms | 21ms | 15ms | 0.6-1.6x |
| spectral-norm | 22ms | 11ms | 12ms | 12ms | 9ms | 1.8-2.4x |
| mandelbrot | 54ms | 51ms | 99ms | 15ms | 14ms | 0.5-3.9x |
| edigits | 4ms | 3ms | 5ms | 4ms | 3ms | 0.8-1.3x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 502ms | 336ms | 133ms | 573ms | 313ms | 0.9-3.8x |
| fannkuch-redux | 2046ms | 1267ms | 390ms | 2476ms | 1485ms | 0.8-5.2x |
| binarytrees | 1576ms | 1322ms | 1841ms | 1369ms | 1103ms | 0.9-1.4x |
| merkletrees | 732ms | 647ms | 1399ms | 584ms | 644ms | 0.5-1.3x |
| nsieve | 168ms | 134ms | FAIL | 279ms | 303ms | 0.6-0.6x |
| lru | 21ms | 10ms | 18ms | 7ms | 13ms | 1.2-3.0x |
| json-serde | 64ms | 35ms | 4ms | 23ms | 35ms | 1.8-16.0x |
| nbody | 218ms | 206ms | 335ms | 182ms | 128ms | 0.7-1.7x |
| spectral-norm | 181ms | 83ms | 84ms | 84ms | 61ms | 2.2-3.0x |
| mandelbrot | 205ms | 195ms | 381ms | 50ms | 48ms | 0.5-4.3x |
| edigits | 5ms | 5ms | 4ms | 4ms | 4ms | 1.2-1.2x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 1387ms | 672ms | 189ms | 5641ms | 3079ms | 0.2-7.3x |
| helloworld | 418ms | 392ms | 94ms | 4980ms | 2086ms | 0.1-4.4x |
| fannkuch-redux | 512ms | 426ms | 97ms | 5061ms | 2124ms | 0.1-5.3x |
| binarytrees | 512ms | 441ms | 102ms | 5148ms | 2017ms | 0.1-5.0x |
| merkletrees | 463ms | 406ms | 94ms | 4958ms | 1992ms | 0.1-4.9x |
| nsieve | 458ms | 403ms | FAIL | 4819ms | 2057ms | 0.1-0.2x |
| lru | 610ms | 461ms | 136ms | 5055ms | 2190ms | 0.1-4.5x |
| knucleotide | 610ms | 475ms | 90ms | 5521ms | 2444ms | 0.1-6.8x |
| json-serde | 719ms | 539ms | 98ms | 6203ms | 2995ms | 0.1-7.3x |
| regex-redux | 787ms | 534ms | 167ms | 5693ms | 7159ms | 0.1-4.7x |
| nbody | 706ms | 522ms | 170ms | 5093ms | 2198ms | 0.1-4.2x |
| spectral-norm | 486ms | 424ms | 94ms | 4983ms | 2364ms | 0.1-5.2x |
| mandelbrot | 465ms | 400ms | 98ms | 5165ms | 2139ms | 0.1-4.7x |
| edigits | 723ms | 582ms | 96ms | 5339ms | 2551ms | 0.1-7.5x |
903 commits
TypeScript
95.9%
Zig
2.4%
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.
#init functions to allocate resources, #destroy functions to clear them at scope exitmove instances to a new owner, or share instances by refview instances are non-owning, zero-copy slices that can't outlive their source.nm files as well as the usual LSP nicetiesInstall 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.
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)
import System
pub func main = () {
Console.write_line("Hello, World!")
}
The nomen CLI has several commands: init, run, build, check, format, test and docs. Top-level options:
| Command | Description |
|---|---|
init <name> | creates a new project folder for a program with the supplied name |
run | runs the program that is in the current folder, or the .nm file specified with --in |
build | builds the program into an executable file |
check | parses and compiles the program and reports warnings and errors |
format | formats the code (defaults to tabs, 100 chars wide, sorted imports, implicit types, trailing commas) |
test | runs any .test.nm files in the current folder |
docs | generates a folder of documentation from code comments |
| Option | Alias | Description |
|---|---|---|
--in <path> | -i | Input .nm file or folder (auto-discovered if omitted). |
--out <path> | -o | Declared but currently unused; output goes to <root>/build/. |
--config <path> | -c | Path to a JSON build config file. |
--watch | -w | Re-run the pipeline on file changes. |
--arch <a> | -a | Backend: aarch64 (default) or c. |
--platform <p> | -p | Target: macos, ios, linux, android, windows, web (host-derived default). |
--lib <path> | -l | Path to the System library directory. |
--audit | Enable memory auditing of the generated program. | |
--audit-runtime | Path to audit_runtime.c (used with --audit). | |
--check | format dry-run: report changes without writing. |
See CLI.md for the full reference (input resolution, build output, config files, and examples).
See SPEC.md for the full language specification.
bool
int // and int8, int16, int32 and int64
uint // and uint8, uint16, uint32 and uint64
float // and float32 and float64
string
char
null
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
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"
}
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) //
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 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 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 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)
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}"
}
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 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()
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)
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
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)
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 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)
}
}
const greeting = "Hello, " + name
const dashes = "-" * 10
Console.write("You are \{age} years old.")
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 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)
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.
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
The standard System library is imported with import System.
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()
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.
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 resourcesSee MEMORY.md for the full model.
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.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 oneThe 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.
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.
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.
Adapted from Programming Language Benchmarks. Run on my laptop. Any errors in adaptation are my fault.
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| helloworld | 3ms | 3ms | 4ms | 4ms | 4ms | 0.8-0.8x |
| knucleotide | 8ms | 5ms | 18ms | 7ms | 8ms | 0.4-1.1x |
| regex-redux | 24ms | 13ms | 16ms | 18ms | 4ms | 1.3-6.0x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 29ms | 19ms | 11ms | 33ms | 20ms | 0.9-2.6x |
| fannkuch-redux | 168ms | 108ms | 39ms | 206ms | 127ms | 0.8-4.3x |
| binarytrees | 146ms | 124ms | 146ms | 129ms | 103ms | 1.0-1.4x |
| merkletrees | 157ms | 141ms | 278ms | 125ms | 136ms | 0.6-1.3x |
| nsieve | 44ms | 35ms | FAIL | 41ms | 43ms | 1.0-1.1x |
| lru | 7ms | 4ms | 7ms | 5ms | 6ms | 1.0-1.4x |
| json-serde | 16ms | 11ms | 4ms | 7ms | 9ms | 1.8-4.0x |
| nbody | 24ms | 24ms | 37ms | 21ms | 15ms | 0.6-1.6x |
| spectral-norm | 22ms | 11ms | 12ms | 12ms | 9ms | 1.8-2.4x |
| mandelbrot | 54ms | 51ms | 99ms | 15ms | 14ms | 0.5-3.9x |
| edigits | 4ms | 3ms | 5ms | 4ms | 3ms | 0.8-1.3x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 502ms | 336ms | 133ms | 573ms | 313ms | 0.9-3.8x |
| fannkuch-redux | 2046ms | 1267ms | 390ms | 2476ms | 1485ms | 0.8-5.2x |
| binarytrees | 1576ms | 1322ms | 1841ms | 1369ms | 1103ms | 0.9-1.4x |
| merkletrees | 732ms | 647ms | 1399ms | 584ms | 644ms | 0.5-1.3x |
| nsieve | 168ms | 134ms | FAIL | 279ms | 303ms | 0.6-0.6x |
| lru | 21ms | 10ms | 18ms | 7ms | 13ms | 1.2-3.0x |
| json-serde | 64ms | 35ms | 4ms | 23ms | 35ms | 1.8-16.0x |
| nbody | 218ms | 206ms | 335ms | 182ms | 128ms | 0.7-1.7x |
| spectral-norm | 181ms | 83ms | 84ms | 84ms | 61ms | 2.2-3.0x |
| mandelbrot | 205ms | 195ms | 381ms | 50ms | 48ms | 0.5-4.3x |
| edigits | 5ms | 5ms | 4ms | 4ms | 4ms | 1.2-1.2x |
| Benchmark | Nomen/A | Nomen/C | Go | Zig | Rust | Compare |
|---|---|---|---|---|---|---|
| pidigits | 1387ms | 672ms | 189ms | 5641ms | 3079ms | 0.2-7.3x |
| helloworld | 418ms | 392ms | 94ms | 4980ms | 2086ms | 0.1-4.4x |
| fannkuch-redux | 512ms | 426ms | 97ms | 5061ms | 2124ms | 0.1-5.3x |
| binarytrees | 512ms | 441ms | 102ms | 5148ms | 2017ms | 0.1-5.0x |
| merkletrees | 463ms | 406ms | 94ms | 4958ms | 1992ms | 0.1-4.9x |
| nsieve | 458ms | 403ms | FAIL | 4819ms | 2057ms | 0.1-0.2x |
| lru | 610ms | 461ms | 136ms | 5055ms | 2190ms | 0.1-4.5x |
| knucleotide | 610ms | 475ms | 90ms | 5521ms | 2444ms | 0.1-6.8x |
| json-serde | 719ms | 539ms | 98ms | 6203ms | 2995ms | 0.1-7.3x |
| regex-redux | 787ms | 534ms | 167ms | 5693ms | 7159ms | 0.1-4.7x |
| nbody | 706ms | 522ms | 170ms | 5093ms | 2198ms | 0.1-4.2x |
| spectral-norm | 486ms | 424ms | 94ms | 4983ms | 2364ms | 0.1-5.2x |
| mandelbrot | 465ms | 400ms | 98ms | 5165ms | 2139ms | 0.1-4.7x |
| edigits | 723ms | 582ms | 96ms | 5339ms | 2551ms | 0.1-7.5x |
903 commits
TypeScript
95.9%
Zig
2.4%