Hello, World

Every application declares def new(args string[]) int - its return value is the process exit code.

main.adm
application Hello {
	def new(args string[]) int {
		println("Hello, 🌍")
		return 0
	}
}

String Interpolation

Any expression goes inside {}. A value whose type implements str() formats itself - no explicit conversion needed.

interpolation.adm
module greet {
	interface IStringer {
		def str() string
	}

	type Thing {
		def str() string {
			return "Thing"
		}
	}
}

application Interpolation {
	use greet

	def new(args string[]) int {
		let n = 3
		let t = greet.Thing{}
		let si greet.IStringer = t

		println("n={n}")              // n=3
		println("t={t}")              // t=Thing
		println("sum={n + n * 2}")     // sum=9
		return 0
	}
}

Arrays & Maps

Arrays support set-style operators and whole-array arithmetic; slices are views into the same storage. Maps iterate as key/value pairs.

collections.adm
application Collections {
	def new(args string[]) int {
		let a = int[1, 2, 3, 4]
		let b = int[3, 4, 5, 6]

		println(a & b)        // [3, 4]     intersection
		println(a | b)        // [1..6]     union
		println(a - b)        // [1, 2]     difference
		println(a[-2:])       // [3, 4]     last two

		a[:] *= 10            // scale every element in place
		println(a)            // [10, 20, 30, 40]

		let stock = map<string, int>{
			"widget": 12,
			"gasket": 3,
		}

		stock["bolt"] = 40
		for let (name, count) in stock {
			println("{name}: {count}")
		}
		return 0
	}
}

Regex

Regexes are a primitive with their own literal syntax, not a library type - flags go after the closing slash, and captures are referenced as $1, $2 in replacements.

regex.adm
application Regex {
	def new(args string[]) int {
		let r = /[0-9]+/

		r.test("a1b")                // true
		r.matchAll("a1b22c")          // ["1", "22"]
		r.split("a1b22c")             // ["a", "b", "c"]
		r.replace("a1b22c", "X", -1)  // "aXbXc"

		// the g flag replaces every match without a count
		let rg = /[0-9]+/g
		rg.replace("a1b22c", "X")      // "aXbXc"

		// capture groups, reordered in the replacement
		let cap = /(a)([0-9]+)/
		cap.replace("a12 b a3", "$2-$1", -1)  // "12-a b 3-a"

		// strings accept a regex directly
		"hello123".contains(/[0-9]+/)     // true
		return 0
	}
}

Big Numbers

bigint is arbitrary-precision and uses the same operators as any other number - no special call syntax for arithmetic that would overflow a machine integer.

bignum.adm
application BigNumbers {
	def new(args string[]) int {
		let x bigint = bigint(0x1234567890abcdef)
		let y bigint = bigint(7)

		// exponentiation on a value far past 64 bits
		let z bigint = (x**3) - (3*x) + y

		if (x < z) && (z != x) {
			println(z)
		}
		return 0
	}
}

A literal too large for int infers bigint on its own - no cast needed.

Pattern Matching

match destructures tuples and struct shapes, and each case can carry a guard after a ;.

matching.adm
module shapes {
	struct User {
		id    int
		email string
	}

	def classify(x int) string {
		match x {
			case 0:
				return "zero"
			case (n); n > 0:
				return "positive: {n}"
			default:
				return "negative"
		}
	}

	def sum(t (int, int)) int {
		match t {
			case (a, b):
				return a + b
			default:
				return 0
		}
	}

	def userId(u User) int {
		match u {
			case {id int, email string}; id > 0:
				return id
			default:
				return 0
		}
	}
}

Error Handling

A function that can fail returns !T. try propagates the error to the caller; onerror recover handles it locally.

errors.adm
module inventory {
	struct Item {
		name  string
		stock int
	}

	def restock(item Item, qty int) !Item {
		if qty <= 0 {
			fail "quantity must be positive"
		}
		return Item{name: item.name, stock: item.stock + qty}
	}
}

application Warehouse {
	use inventory

	def new(args string[]) int {
		let item = inventory.Item{name: "widget", stock: 12}

		// recover with a fallback value
		let a = inventory.restock(item, -1) onerror recover item

		// inspect the error before deciding
		let b = inventory.restock(item, 8) onerror (err error) {
			println("restock failed: {err.message}")
			recover item
		}

		println(b.stock)
		return 0
	}
}

Contracts

expects runs before the body, provides runs after - on every return path. A failed assertion becomes an error, so the function returns !T.

contracts.adm
module accounts {
	def withdraw(balance int, amount int) !int {
		expects {
			assert amount > 0 onerror fail "amount must be positive"
			assert amount <= balance onerror fail "insufficient funds"
		}

		let remaining = balance - amount
		return remaining

		provides {
			assert remaining >= 0
			assert remaining < balance
		}
	}
}

Transactions

A function declares how to save and undo its own state with begin and rollback. Those blocks lie dormant until the function is called from inside a transaction - then any fail unwinds every rollback automatically.

transactions.adm
application Ledger {
	let state int = 0

	def apply(x int) {
		begin {
			state = state + x
		}

		rollback {
			state = state - x
		}

		state = state + 1
	}

	def boom() !int {
		fail "boom"
	}

	def run() !none {
		transaction {
			apply(5)
			let v = try boom()   // fails - every rollback runs
			println(v)
		} onerror (err error) {
			println("rolled back: {err.message}")
			recover none
		}
		return none
	}

	def new(args string[]) int {
		try run()
		return 0
	}
}

Generics

A where block adds members that only exist when the type argument satisfies the constraint - User<Admin> has permissions, User<string> doesn't.

generics.adm
module demo {
	struct Pair<A, B> {
		first  A
		second B
	}

	type Box<T> {
		value T

		def new(v T) { self.value = v }
		def get() T { return self.value }
	}

	type Admin {}
	type Account {}

	type User<T> {
		name string

		where T is Admin | Account {
			permissions string[]

			def tag() string { return "privileged" }
		}
	}

	def run() int {
		let p Pair<int, string> = Pair<int, string>{first: 1, second: "hi"}

		let b = new Box<int>(10)
		let c = new Box<string>("x")

		let u User<Admin> = new(name="bob")
		return b.get() + c.get().len() + u.permissions.len()
	}
}

Operator Overloading

An infix method binds a type to an operator symbol, so instances of it work with ordinary operator syntax - including shifts and rotations.

infix.adm
module bits {
	type Flags {
		value int

		infix def | (other Flags) Flags {
			return Flags{value: value | other.value}
		}

		infix def & (other Flags) Flags {
			return Flags{value: value & other.value}
		}

		infix def <<< (count int) Flags {
			return Flags{value: value <<< count}
		}
	}
}

application Bits {
	use bits

	def new(args string[]) int {
		let read  = bits.Flags{value: 1}
		let write = bits.Flags{value: 2}

		let rw = read | write     // calls infix def |
		let hi = rw <<< 4          // calls infix def <<<

		// the standard library does this too - strings repeat with *
		println("ha" * 3)          // hahaha
		return 0
	}
}

Lambdas

param => expr is shorthand for an anonymous function. Parameter types are inferred when the callback's shape is already known.

lambdas.adm
application Lambdas {
	def new(args string[]) int {
		let a = int[1, 2, 3]

		let doubled = a.mapElem(v => v * 2)
		let labels  = a.mapElem(v => sprint(v))

		// an anonymous function that names itself can recurse
		let fib = def fib(n int) int {
			return n when n < 2
			return fib(n-1) + fib(n-2)
		}

		println(doubled.len())
		println(fib(10))
		return 0
	}
}

Parallel Loops

forall spreads iterations across every available CPU core. Order isn't guaranteed; finish runs once all workers are done.

parallel.adm
application Parallel {
	def new(args string[]) int {
		let values = [10, 20, 30, 40]

		forall let (idx, v) in values {
			println("worker got {v} at {idx}")
		} finish {
			println("all workers done")
		}

		return 0
	}
}

Async & Channels

await composes with &&/|| over completion state: || resumes on the first to finish, && waits for both.

concurrency.adm
module work {
	async def slow() int {
		let acc = 0
		for let i in 0..2000000 {
			acc = i
		}
		return 7
	}

	async def fast() string {
		return "ok"
	}
}

application Concurrency {
	use work

	def new(args string[]) int {
		let a = work.slow()
		let b = work.fast()

		// resume as soon as either finishes
		let first = await (a || b)
		println("string won") when first is string

		// futures can be cancelled or given a deadline
		let stuck = work.slow().timeout(50ms)
		let dropped = work.slow().cancel()

		// a channel between workers
		let ch channel<int> = new(size=4)
		ch <- 42
		ch.close()

		select {
			case ch:
				println(<-ch)
			default:
				println("nothing ready")
		}

		// or drain a channel in parallel across cores
		forall let v in ch {
			println(v)
		} empty {
			println("channel was empty")
		}
		return 0
	}
}

Value Tags

Tags travel with a value's identity - through calls, array slots and channels - while copies start clean. A gate function refuses anything that was not marked as signed.

main.adm
module demo.tags {
	type Request {
		id int
		def new(v int) {
			id = v
		}
	}

	def deliver(message string) !none {
		fail "unsigned message" unless message.tags().has("signed")
		println("delivered: {message}")
		return none
	}
}

application TagsDemo {
	use demo.tags::(*)

	def new(args string[]) int {
		let message = "hello"
		message.tags().set("+signed")
		deliver(message) onerror (err error) {
			println("rejected: {err.message}")
			recover none
		}

		let copy = message + "!"          // a new value: no tags
		deliver(copy) onerror (err error) {
			println("rejected: {err.message}")
			recover none
		}

		let request = new Request(7)
		request.tags().add("Network/primary").add("pii")
		let queue channel<Request> = new(size=1)
		queue <- request
		let got = <-queue
		if got is Request {
			for let tag in got.tags().list() {
				println(tag)                 // Network/primary, then pii
			}
		}
		return 0
	}
}

Services

@commit() mutates service state and emits a typed event; @on subscribes to it. @query() reads a snapshot.

service.adm
module demo {
	use std.services::(*)

	service Storage {
		opened int

		@query()
		def openCount() int {
			return opened
		}

		@commit()
		def open(id int) int {
			opened += 1
			return id
		}
	}

	@on(Storage.open)
	def logOpen(ev Event<int>) {
		println("opened {ev.result}, total {Storage.openCount()}")
	}
}

Custom Annotations

meta def defines a new annotation. The first parameter's context type decides what it can decorate - and a parameter annotation can rewrite the argument before the body ever runs:

param_meta.adm
module demo {
	use std.reflection as reflection

	meta def setTo(ctx reflection.ParameterMetaContext, value int) {
		ctx.set(value)
	}

	def id(@setTo(42) x int) int {
		return x
	}

	def run() bool {
		return id(1) == 42   // the argument was rewritten on the way in
	}
}

Annotations are also discoverable at runtime, so a registry can find everything tagged with a given annotation without any string lookups or manual registration:

discovery.adm
module demo {
	use std.reflection

	meta def mark(target reflection.DeclarationMetaContext, name string) {}

	@mark("t")
	type Thing {}

	@mark("f")
	def handler(x int) int {
		return x
	}

	def run() int {
		let hits = reflection.attrs(mark)
		return hits.len()   // 2 - the type and the function
	}
}

And metadata attached by an annotation stays readable at runtime, which is how the JSON encoder finds its field names:

field_meta.adm
module demo {
	use std.reflection as reflection
	use std.data.encoding as encoding

	type T {
		@encoding.json(name="a", omitempty=true)
		x int
	}

	def run() bool {
		let t = T{x: 1}
		let md = reflection.fieldMetadataOf(t, "x")
		return md.load("encoder.json.name") == "a"
	}
}

SIMD

vec4<T> and friends map to hardware SIMD where available - arithmetic runs across all lanes at once, and comparisons return a mask for branchless selection.

simd.adm
application Simd {
	def new(args string[]) int {
		let data = float[1.0, 2.0, 3.0, 4.0]

		let v vec4<float>
		let a = v.load(data, 0)

		let w vec4<float>
		let b = w.splat(2.0)

		let scaled = a * b          // all four lanes at once
		let big    = a.gt(b)        // mask4<float>
		let picked = big.blend(a, b) // branchless select

		scaled.store(data, 0)
		return 0
	}
}

C Interop

Annotate a module and every body-less def inside binds to that library - this is how the standard library's OpenGL bindings work.

ffi.adm
@link("GL")
module gl {
	const GL_TRIANGLES uint32 = 0x00000004

	def glClear(mask uint32);
	def glViewport(x, y, width, height int);
}

@c("libm.so")
module libm {
	def sin(x float64) float64;
	def cos(x float64) float64;
}

GPU Kernels

A cuda def is a CUDA kernel with ADM parameters; arrays are copied to the device and back around the launch, and the same program runs on the CPU when there is no GPU.

blur.adm
application Brighten {
	use std.gpu

	cuda def brighten(pixels byte[], amount int) !none {
		int i = blockIdx.x * blockDim.x + threadIdx.x;
		if (i < pixels_len) {
			int v = pixels[i] + amount;
			pixels[i] = v > 255 ? 255 : v;
		}
	}

	def new(args string[]) int {
		let image = byte[10, 120, 250]
		brighten(image, 40) onerror recover none
		println(image[0], image[1], image[2])   // 50 160 255
		println("ran on {gpu.deviceName()}") when gpu.available()
		return 0
	}
}

Data reused across launches goes into a DeviceBuffer with gpu.upload; see GPU kernels in the reference.

Collections

std.data.collections has the usual structures as generic types; every one iterates with for.

words.adm
application Words {
	use std.data.collections::*

	def new(args string[]) int {
		let counts = new HashMap<string, int>()
		for let word in "the cat and the hat".split(" ") {
			counts.set(word, counts.get(word, 0) + 1)
		}

		let repeated = new Deque<string>()
		for let entry in counts {
			repeated.pushBack(entry.key) when entry.value > 1
		}
		println(repeated.popFront())        // the

		let seen = new BloomFilter(1024)
		seen.add("cat")
		println(seen.mightContain("cat"))   // true
		return 0
	}
}

Semantic Versions

std.data.semver parses versions, compares them by precedence and checks ranges in the usual ^, ~ and || notation.

semver.adm
application Versions {
	use std.data.semver::*

	def new(args string[]) int {
		let v = parse("1.4.0-rc.1")
		return 1 when v is error

		println(v.major)                          // 1
		println(v < "1.4.0")                      // true: a prerelease precedes the release
		println(v.satisfies("^1.2.0"))            // false: prereleases need an opt-in
		println(v.satisfies(">=1.4.0-alpha"))     // true
		println(v.canonical())                    // 1.4.0-rc.1
		return 0
	}
}

Plugins

A plugin exports part of what it uses and builds into a single .admplugin package; adm doc --plugin shows what is inside.

imagetools.adm
@manifest(version = "1.2.0")
plugin ImageTools {
	use (
		acme.imaging
		acme.imaging.filters
	)

	export (
		acme.imaging.filters
		acme.imaging::(Image, decode)
	)
}

adm build writes ImageTools.admplugin; the Plugins page covers the package layout and the shared runtime.

Tests

check suites live next to the code they verify. They're type-checked with the rest of the program and dropped from release builds.

path_test.adm
module std.os {
	check "Path" {
		@test()
		def TestExtension() {
			assert extension("/path/to/file.txt") == "txt"
			assert extension("file") == ""
		}

		@benchmark()
		def BenchBase() {
			base("/path/to/file.txt")
		}
	}
}

Run them with adm test, or benchmarks with adm test --bench.