Docs
Language reference
The language itself, section by section. The other docs pages cover the memory model, the annotations shipped with the compiler and the standard library, the application home folder and migrations, the policy and permission model, and plugins.
Introduction
ADM is a statically-typed, multi-paradigm language for applications, services, and UI. Every program is one of four things - an application, a library, a plugin, or a module - and all four follow the same syntax and rules.
Three ideas carry through the rest of the language:
- Structured. Structs, enums, unions, interfaces, and datatypes describe data. Functions and transactions describe behavior.
- Expressive. Pattern matching, string interpolation, async/await, channels,
and error handling (
fail,onerror,expect/provide) are part of the core syntax. - Native. Applications, libraries, plugins, and modules all go through the same compilation pipeline.
Compiler
Everything goes through one CLI, adm.
ADM compiles for every platform and architecture its LLVM backend targets, not just the host
machine. adm build --os currently accepts linux, windows,
darwin, android, ios, and wasm; --arch
recognizes amd64, 386, arm64, arm,
riscv64, ppc64le, ppc64, s390x, and
mips64 (defaulting to the host's own when left unset). Cross-compiling to one of
these just picks the matching LLVM target triple.
--os wasm builds for the web: a page, a small JavaScript loader and a WebAssembly
module that any web server serves, with the C library and the fonts it needs carried by the
compiler. adm run --os wasm opens it in the browser and adm test --os wasm
runs the suites in a headless one. Tasks run concurrently on the page's one thread; files live in
the browser's storage; HTTP goes through fetch and WebSockets through the browser's;
a UI program draws in the page's canvas through WebGL 2. The Browser service
reaches what only a page has: the address and history, metadata, cookies, storage. Raw sockets,
processes, plugins and devices are not there.
Every command and subcommand, with its flags, is on the Command line
page; adm <command> --help prints the same for the installed version. In
short: build, check, run, test,
fmt and doc work on the source tree; app,
audit, lib and list on the project and its manifest;
get, install, publish, search and
vendor on packages; doctor, targets,
update and version on the installation itself.
Syntax Basics
Whitespace is insignificant except inside literals - no semicolons at the end of statements, no significant indentation. Source files are UTF-8; a byte order mark at the start of a file, as Windows editors write one, is ignored.
Comments
Line comments run to the end of the line; block comments nest arbitrarily, so commenting out a region that already contains a comment works:
// single line comment
/*
* block comment
* spanning lines
*/
/* outer /* inner */ still commented */
A run of // lines directly above a declaration is its doc comment. Start it with the name and say what the thing does, what it takes, what it returns and when it fails. adm doc, the hover card and the IDE's Documentation tab show it, and the IDE renders it in place with the gutter pencil to switch back to the raw lines.
A fenced block, three backticks on their own comment line before and after, is a code block shown in a grey box with highlighting. Tags at the end of the comment, one per line, are shown as rows: every @author joined under Author, each @see a link to that symbol under See Also, @since, @deprecated, and any other @word under its own name. A tag's text runs on until the next tag; tags come after the description, never inside it.
// Groups the digits in threes: `1,234,567`; `-1,000`; `999` unchanged.
// `separator` replaces the comma.
//
// Example usage:
// ```
// comma(1234567) == "1,234,567"
// comma(-1234567, " ") == "-1 234 567"
// ```
// @author Ada Lovelace
// @see words
// @since 1.0
def comma(n int64, separator string = ",") string {
A block whose language is example is lifted out of the description instead, titled by the line above it, and listed under Examples by adm doc and the Documentation tab. A plain block is the better default.
Operators
| Operator | Purpose |
|---|---|
+ - * / % |
Arithmetic. + also concatenates strings and arrays, and merges maps (always into a new value). |
** | Exponentiation - a ** b raises a to the power b. |
.* | Element-wise matrix multiply (see Arrays & Slicing). |
+= -= *= /= %= |
Compound assignment - lhs = lhs op rhs. |
& | ^ ~ ! |
Bitwise AND / OR / XOR / NOT. Arrays reuse these as set operations. |
<< >> | Logical shifts; signedness is respected. |
<<< >>> |
Bit rotations, with compound forms <<<= and >>>=. |
&& || ! | Short-circuiting logical operators. |
< <= > >= == != |
Comparisons - work across numeric primitives, strings, chars, and enums. |
cond ?? (a, b) |
Conditional choice; only the taken branch is evaluated (see Conditionals). |
:= | Conditional junction - reuses the left operand across a chain (see Conditionals). |
is | Type test, used for narrowing (see Type Narrowing). |
A type can overload most of these for its own instances with an infix method -
see Types โ Methods.
Assignment
Several names can be assigned at once, which makes swapping values a one-liner - the right side is fully evaluated before anything is written:
let a = 1
let b = 2
a, b = b, a // a is 2, b is 1
++ and -- increment and decrement in place, and the usual unary operators apply to values:
let x int = 3
let y = -x // negate
let w = ^x // bitwise NOT
let ok bool = true
let nope = !ok
let counter = 0
counter++
counter--
Assignment is an expression, so it yields the value that was just written - handy inside a call or a condition. Compound assignment and slice assignment behave the same way:
let x int = 0
println((x = 7)) // 7
println((x += 1)) // 8
let a = [1, 2]
println((a[0] = 9)) // 9
println((a[:] += 1)) // every element incremented
Program Structure
Every ADM source file uses the .adm extension. A single file can hold any number
of applications, libraries, plugins, and modules - there's no one-file-one-thing rule.
Any of those four can be split across multiple files with the partial modifier,
so one large application or module doesn't have to live in one large file. All the fragments
for the same partial thing have to sit in the same directory; the compiler stitches
them back together before type checking runs.
The directory a file lives in has nothing to do with its namespace, though. A module's name
comes from its own declared identifier - module std.net.http is always
std.net.http, regardless of which folder or filename it's written in. Folders are
just how you organize files on disk.
Applications
An application block defines an executable target. Applications live at file
scope, support annotations (@manifest, @allowDowngrade, etc.), and may be split
with partial application as long as all fragments share the same identifier and
directory.
@annotation()
application MainApp {
def new(args string[]) int {
print("Hello, World")
return 0
}
def dispose() {
cleanup()
}
}
- The constructor
def new(args string[]) intis the entry point; the integer return value becomes the process exit code. def dispose()is optional and runs when the runtime shuts the application down.- Application bodies may declare properties and methods, like a regular
type- but applications cannot have generics,whereclauses, orinfixmethods, because they are never instantiated by the user. - Members cannot be marked
internal; applications are always public compilation units. - Multiple
applicationdeclarations in one workspace generate multiple binaries. An unnamed application inherits the filename and must be compiled explicitly - it cannot be spread viapartial. - Application names must be unique across the workspace, and are a single identifier - no dots, unlike a module's name.
A project can define any number of applications. adm build with no arguments
builds every application it finds; pass one or more names to build only those. A monorepo
with a CLI, a daemon, and a worker can build any one of them, several, or all of them, in a
single invocation.
An application's name is also the build output's filename, sanitized down to letters and
digits (with .exe appended on Windows).
adm.toml Manifest
An application can carry an optional adm.toml file - a TOML manifest at the
project root, keyed by the ADM application name. Run adm app init
to generate one: it writes the manifest and also creates an Ed25519 signing keypair, putting
the public key in the manifest and the private key in your config directory (or a path you
choose with --private-key-out). The private key should never be committed.
None of it is required. Without a manifest the application still has an identity:
__app_name__ and __app_id__ are the declared application name (the
file name for an anonymous application { }) and __app_version__ is
0.1; @manifest(name = "myapp", version = "1.2.0") on the
application block sets both without a file. The update constants resolve to
empty strings. A adm.toml wins over all of that; when present, it sets:
- Identity - the publisher's public key and a repo salt, combined with each
app's slug (or its application name) to derive stable
publisher_id/app_idvalues that survive renames. - Update metadata - a feed URL, channel, and artifact name, set globally or
per app. These only reach your program as compiler constants (
__update_feed__,__update_channel__,__update_artifact__) - nothing in the toolchain fetches or applies updates on its own. - Packaging - a version plus a list of files to bundle, consumed by
adm package buildto produce a release archive.
A family of commands works with the manifest once it exists, all on the Command line page:
adm app info/adm app id- print what the manifest configures and the identity it resolves to.adm app set/adm app unset- write or remove one value, keeping the rest of the file;adm app bump major|minor|patchbumps a version.adm app rekey- a new key pair and salt in place.adm app sign/adm app verify- sign a built artifact with the publisher key, then verify it later.adm app policy init|set|unset|get|log- the application's policy file and decision log.adm package build/adm package install- build a release archive from the packaging rules and install it locally.
Every value the manifest can set has a matching compiler constant: __app_name__,
__app_id__, __app_version__, __publisher_id__,
__publisher_pubkey__,
__plugin_policy__ (the [plugins] table: whose plugins load and what they may be given),
plus the three update fields above. There's also an __build_id__ constant that's
always set and changes on every build - don't use it as a stable identity.
Libraries & Plugins
Libraries and plugins share code without an entry point of their own - no def new,
nothing runs them directly. The difference is how they reach a program:
libraryis shared as source:adm buildpacks its exported modules into a signed.admlib, published through the registry and compiled together with the program thatuses it. See Libraries.pluginis packaged:adm buildproduces one.admpluginfile holding the plugin's metadata, its export index and a native shared object per platform, which a host loads at runtime through the plugin manager. See Plugins.
Both live at file scope, both can be partial, and neither can be marked
internal - like applications, an unnamed one inherits its filename. The block
name is the unit's identity and the module prefix it owns; what a package ships, what a
consumer may use, and how shared code is split are covered under
Libraries โ What ships.
library Geometry {
use std.math
def area(radius float) float {
return math.Pi * radius * radius
}
}
A plugin states its public surface with an export list, using the same selectors
as use; everything else in it stays private. The compiler checks that every
exported symbol exists, is not internal, and that its signature only mentions
exported types. @manifest(version = ...) sets the plugin's version:
@manifest(version = "1.2.0")
plugin ImageTools {
use (
acme.imaging
acme.imaging.filters
)
export (
acme.imaging.filters // every non-internal symbol of the module
acme.imaging::(Image, decode) // a subset
)
}
adm build --platforms linux-amd64,linux-arm64,darwin-arm64,windows-amd64 builds
the plugin for each platform listed and bundles them all into the one package; without the
flag only the host platform is built. The Plugins page covers the
package layout, adm doc --plugin and the shared runtime.
adm build --libs or --plugins narrows a build to just those targets,
the same way naming an application does; adm lib manages libraries and
dependencies once you have some.
Modules & Namespaces
Modules are source containers that group symbols under a qualified name. They never produce a binary or library on their own - their contents are pulled into whichever application, library, or plugin imports them.
module my.project.name {
// module code
}
- Module names can be simple identifiers (
module utils) or dotted namespaces (module std.net.http). A file may host multiple modules. internal modulehides the module outside its package;partial modulelets you split it across files in the same directory.- Module bodies may contain
usestatements, constants, functions, types, structs, enums, unions, interfaces, datatypes, services, components, views, and styles. - Modules cannot declare other top-level units -
application,library,plugin, or nestedmoduledeclarations are forbidden inside a module block.
Modules are also how ADM talks to other languages. Annotate a module with an FFI attribute and every function declared without a body becomes an external symbol pulled in from that library instead of compiled ADM code. The standard library's OpenGL bindings do exactly this:
@link("GL")
module std.ui.renderer.opengl.gl {
const GL_TRIANGLES uint32 = 0x00000004
def glClear(mask uint32);
def glViewport(x, y, width, height int);
}
Everything in this module is a real GL symbol - glClear and glViewport
come straight from the system OpenGL library, not from ADM code. More on this under
Interoperability.
use Imports
use std.net.http
use my.tools as mod
use some.lib::(foo, bar as Baz)
use some.lib::*
use (
std.io,
utils.math,
graphics.engine::(Renderer as Ren)
)
- A plain
use std.net.httpimports the module itself; you reference its exports through the module name -http.Response, not the full dotted path. use my.tools as modaliases the whole module to a different name.use my.tools as _links the module without a name. Nothing in the file can refer to it; it is there for what it registers, such as a renderer thatreflection.attrsdiscovers by its annotation. The linter leaves these alone.::(foo, bar as Baz)imports specific symbols by name, optionally renaming any of them. A module imported this way can't itself be aliased - there's nothing to alias, only the symbols it exposes are in scope.::*(or::(*)) imports every exported symbol from a module, including every overload of an imported function name.- Several imports can share one
use ( ... )block.
Module names aren't implicitly in scope - even after use std.net.http, you still
write http.Response, never std.net.http.Response.
use is legal almost anywhere a statement is: inside modules, inside type bodies
(so a method can pull in a helper without polluting the rest of the type), and inside function
bodies when scoping needs to stay tight. Wherever it's written, the import is scoped to that
block alone.
Data Types
Primitives
Values are declared with let, or const for ones that can't be
reassigned. A type annotation is optional - leave it off and ADM infers it from the value:
let x int = 2
let y = 2 // inferred as int
Strings (string) are UTF-8 and support interpolation and multi-line literals:
let a string = "Hello, ๐"
let b = "Hello {user}" // interpolation
let c = """
This is a
multi-line string
"""
let d = `{"name": "Ada", "path": "C:\data"}` // raw: no escapes, no interpolation
A backtick string is raw: its text is exactly what stands between the backticks, over any
number of lines, with no escapes and no {expr} interpolation. JSON, regexes and
Windows paths go in as written. It cannot hold a backtick; a carriage return in it is dropped.
Inside a string, {expr} interpolates any expression: a variable, a call, an
index. A lone { followed by an expression start (a letter, a digit, a quote, a
bracket) opens interpolation, so a literal brace is written \{ and
\}:
let name = "Ada"
let n = 3
println("{name} has {n} items ({n * 2} halves)") // Ada has 3 items (6 halves)
println("template: \{name}") // template: {name}
println("json: \{\"ok\": true\}") // json: {"ok": true}
Chars (char) are single-quoted and hold one Unicode code point:
let c char = 'a'
Booleans (bool) are true/false; none represents the absence of a value.
Integers come in a signed/unsigned family - int,
int8/16/32/64/128, uint, uint8/16/32/64/128 - and can be
written in decimal, hex, octal, or binary. Spaces (or underscores) can split up long digit runs
for readability; the compiler ignores them:
let x = 1000000
let x = 1 000 000
let a = 0xDEAD BEEF
let a = 0o142
let a = 0b1010 1010
A number too large for int, like 6277101735386680762814942322444851025767571854389858533375,
is simply written as a literal - ADM infers bigint.
Floats (float, float16/32/64/128) accept the same
space separators, a leading-dot shorthand, and exponents:
let y = 1000.0000001
let y = 1 000.000 0001
let y = .3
let x = 1e-3
complex32/64/128 use an i suffix for the imaginary part, e.g. 123i.
Durations (duration) are a number plus an SI-style suffix:
let a = 1ns
let a = 1us
let a = 1ยตs
let a = 1ms
let a = 1s
let a = 1m
let a = 1h
Regexes (regex) are slash-delimited, with an optional run of flags after the closing slash:
let a = /./
let a = /[a-z]{2,}\d*/mg
Patterns use RE2 syntax: \d \s \w, Unicode classes such as \p{L} and
\p{Greek}, named groups (?P<name>...), non-capturing groups
(?:...), lazy repeats *? +? ??, counted repeats {n,m} and inline
flags (?i). The flags are i (case-insensitive), m (^
and $ at line breaks), s (. matches \n),
U (lazy and greedy swapped) and g (replace replaces every match).
Matching is leftmost-first and runs in linear time, with no backtracking; backreferences and
lookaround are not RE2 and make the pattern invalid. Two regexes are equal when their patterns
and flags are. test also takes a byte[] of UTF-8 text, which, unlike a
string, can hold U+0000.
A pattern that is only known at run time, such as one read from a file, becomes a regex with new regex(pattern, flags); one that does not compile matches nothing:
let rule = new regex("^.+@example\\.com$", "i")
rule.valid() // true: the pattern compiles
rule.test("Ana@Example.com") // true
new regex("(a", "").valid() // false: it matches nothing
SIMD's vec2/4/8/16/32/64/128 and mask2/4/8/16/32/64/128 types get their
own section below, under Vectors (SIMD).
Converting between types uses the target type as a call - there are no implicit numeric conversions, so a narrowing or lossy conversion has to be written out:
let bytes = byte[]("test") // string -> byte[]
let str = string(bytes) // byte[] -> string
let i = int32(1.5) // numeric -> numeric
let letter = string('Z') // char -> string
let held any = 7
let seven = int(held) // any -> the type it holds; another type panics
A type name is not reserved: a let or parameter named byte shadows
it, and byte(x) then calls that value.
Primitives aren't bare values either - they carry methods, so common operations read as ordinary method calls:
let s = " hello "
s.trim() // "hello"
let x = 3
x.max(5) // 5
let xs = [1, 2, 3]
xs.indexOf(2) // 1
xs.len() // 3
Strings index and slice like arrays do, including negative indexes - and because strings are UTF-8, indexing works on characters, not bytes:
let s = "abcd"
s[0] // "a"
s[:1] // "a"
s[1:3] // "bc"
s[-2:] // "cd"
Slicing mechanics are covered in full under Arrays & Slicing.
- Unit. Every index and length is a Unicode code point:
s.len()counts code points,s[i]is the code point ati,s[a:b]selects code points,for let (i, ch) in syields code-point indexes. Bytes are reached throughs.bytes(); grapheme clusters are not a unit the language knows. - Bounds. Negative indexes count from the end. An out-of-range
s[i]yields'\0'rather than panicking; slice bounds are clamped andb <= agives"". A string slice always copies. - Comparison.
==and ordering are byte-wise (code-point order for valid UTF-8). No normalization, no case folding;upper/lowermap ASCII only. - Ill-formed bytes. A string built from bytes decodes each ill-formed byte, overlong form, surrogate or out-of-range value as U+FFFD, counting one code point. A string cannot contain U+0000: bytes after an embedded zero are dropped.
- Cost.
len()is O(1) after the first call;s[i]is O(1) on ASCII strings and walks with a cursor otherwise, so sequential access stays linear.
Arrays & Slicing
Arrays are T[], dynamically sized, with a literal form - [1, 2, 3],
or Type[1, 2, 3] with an explicit element type.
== and != on arrays compare identity: [1, 2] == [1, 2]
is false, since the two literals are two arrays. a.equals(b)
compares contents.
An untyped literal takes its element type from the items, or from where it is used: a
declared any[], an assignment target or an any[] parameter push
their element type into the literal, and items of unrelated types make it an
any[] on their own, each item boxed:
let ints = [1, 2, 3] // int[]
let mixed = [4, "z", 5.5] // any[]
let row any[] = [1, "two"] // any[] by declaration
def log(fields any[]) { ... }
log([now(), "started", 200]) // the parameter type reaches the literal
The element type is part of a container's type and is never converted: an
int[] is not an any[], a Dog[] is not an
Animal[], a map<string, int> is not a
map<string, any>, and passing one for the other is a compile error.
Literals are the exception, since they have no type yet: a map literal argument takes the
parameter's type the same way an array literal does.
def save(fields map<string, any>) { ... }
save({"retries": 3}) // built as map<string, any>
let counts = {"retries": 3} // map<string, int>
save(counts) // error: not a map<string, any>
let fields map<string, any> = {"retries": 3}
save(fields)
Indexing supports negative indexes, which count from the end:
let a = int[1, 2, 3, 4]
let first = a[0] // 1
let last = a[-1] // 4
a[-2] = 10 // a is now [1, 2, 10, 4]
Slicing produces a view into the same backing storage - no copying:
let whole = a[:] // full view
let prefix = a[:2] // first 2 elements
let suffix = a[-2:] // last 2 elements
let trimmed = a[:-1] // drops the last element
- Writes through a slice are visible through the original array (and any other slice) as long as they still share the same backing storage.
- A slice has its own
len, and its capacity is its length: pushing onto a slice copies it first, so it never writes over the elements of the array past its end. - If growing an array or slice needs more capacity, it detaches - allocates a new backing buffer and copies. Existing slices keep working but stop sharing storage with the grown one.
- Use
.clone()to force a copy instead of a view:let copy = a.clone().
Bulk operations treat arrays like sets, and apply arithmetic across every element at once:
let a = int[1, 2, 3, 4]
let b = int[3, 4, 5, 6]
let intersection = a & b // [3, 4]
let joined = a | b // [1, 2, 3, 4, 5, 6]
let concat = a + b // [1, 2, 3, 4, 3, 4, 5, 6]
let diff = a - b // [1, 2]
let xor = a ^ b // [1, 2, 5, 6]
a[:] = 0 // mutate every element in place
a[:] /= 10 // divide every element in place
a += [7] // append; type-checked at compile time
a = a + ["oops"] on an int[] is a
compile-time error.
In an array literal ; separates rows: int[1, 2, 3; 4, 5, 6] is an
int[][]. Arrays have no matrix arithmetic. Matrices are their own
type, matrix<T>: a rectangular grid of numbers with the matrix product,
element-wise operators and scalar multiply and divide. matrix<T, R, C> fixes
the shape in the type, so a shape mismatch is a compile-time error:
let m = matrix<float, 2, 3>[1, 2, 3; 4, 5, 6] // two rows of three
let n matrix<float, 3, 2> = [7, 8; 9, 10; 11, 12] // shape from the annotation
let product = m * n // matrix product: matrix<float, 2, 2>
let hadamard = m .* m // element-wise
let x = m[1][2] // 6.0: row 1, column 2
Maps
Maps are map<K, V>, with a literal syntax that mirrors structs. A key is a number,
bool, char, string or enum (equal values are one key), a
struct (equal when every field is, so its fields must be key types too), or a
type. Two instances of a type are different keys even with equal fields, unless the
type declares both def hash() uint64 and def equals(other T) bool: the
map then hashes and compares through them, and instances that are equals must
return the same hash (new KeyHasher().add(a).add(b).sum() builds one from the
compared values). Declaring only one of the two is a compile error where the type is used as a
key; functions, arrays, maps, tuples, optionals and interfaces cannot be keys. A key must not
change while it is in a map. a + b on two maps merges b into a copy of
a and returns the copy, like the array operators, neither operand changes;
merge(other) is the in-place form.
let m = map<string, int>{
"a": 1,
"b": 2,
}
Reading and writing use the same bracket syntax as arrays:
m["c"] = 3
let a int = m["a"]
let missing = m.get("missing") // ?int - none if the key is absent
Other lookups and mutations are plain methods:
m.hasKey("a") // true
m.delete("b")
m.len() // 1
Iterating a map yields key/value pairs; keys() and
values() return them as arrays on their own:
for let (k, v) in m {
println(k)
println(v)
}
let ks = m.keys() // string[]
let vs = m.values() // int[]
Tuples
Tuples group a fixed set of values without declaring a type: (T1, T2, ...) is
the type, and a parenthesized, comma-separated expression is the literal.
let ingredients = ("Sugar", 25, true)
let t (int, int) = (1, 2)
A single parenthesized expression stays just that expression - it takes at least one comma to make a tuple.
Elements are read by position, counted from zero, with .0,
.1 and so on. Each has its own type, positions chain through nested tuples, and
an element takes whatever its type takes, a method call or a call of a function it holds:
let name = ingredients.0 // "Sugar"
let grams = ingredients.1 * 2 // 50
let nested = ((1, 2), ("a", [10, 20, 30]))
let last = nested.1.1[2] // 30
let fns = (twice, 5)
let ten = fns.0(fns.1) // calls twice(5)
The position is written right after the value, with no space before the dot; anywhere else
.5 is a float. A position the tuple does not have is a compile error, and so is
assigning to an element: a tuple keeps its values, make a new one to change them.
Destructuring pulls a tuple's values out positionally with let:
let (name, amount, active) = ingredients
// name = "Sugar", amount = 25, active = true
Use _ to skip a position you don't need:
let (_, amount, _) = ingredients
The same let ( ... ) syntax also destructures a struct - but there it matches by
field name instead of position, and any field can be renamed with as:
let (id as userId, address) = user
Tuples can also be unpacked with a match pattern - case (a, b): return a + b -
covered under Match.
Vectors (SIMD)
vec2/4/8/16/32/64/128<T> are fixed-width SIMD vectors over a numeric lane
type T - the number is the lane count, and operations run across all lanes at
once when hardware SIMD is available. mask4/8/16/32/64/128<T> hold the
per-lane result of a comparison.
A vector is built from a zero-valued instance, then loaded or splatted:
let data = float[1.0, 2.0, 3.0, 4.0]
let v vec4<float>
let a = v.load(data, 0) // 4 lanes starting at index 0
let w vec4<float>
let b = w.splat(2.0) // 2.0 broadcast to every lane
Arithmetic works lane-wise through the usual operators:
let sum = a + b // [3.0, 4.0, 5.0, 6.0]
let prod = a * b // [2.0, 4.0, 6.0, 8.0]
sum.store(data, 0) // writes the 4 lanes back into data
Comparisons return a mask instead of a bool, and a mask can pick lanes from either vector:
let hi = a.gt(b) // mask4<float>: true where a's lane > b's lane
let picked = hi.blend(a, b) // a's lane where hi is true, else b's
Structs
Structs are record-like: mutable fields, no methods or properties. Declare one with
struct; annotations work both on the struct itself and on individual fields:
@annotation()
struct User {
@json(omitempty=true)
name string
id int
age, height int // grouped: share one type
userType = "user" // default value, type inferred
a, b = 0, true // grouped defaults, types inferred
address struct { // anonymous inline struct
street string
city string
}
}
Instantiating a struct uses a literal; the type name can be dropped once it's already known from context:
let u User = {id: 1, name: "MyName", address: {street: "street"}}
An anonymous struct is written where it is used, with no declaration:
struct { x: 2, y: 3 } as a value, each field taking the type of its value, and
struct { x int, y int } as a type. Two anonymous structs of a module with the
same field names and types, in the same order, are one type:
let p = struct { x: 2, y: 3 }
p.x = 10
p = struct { x: 30, y: 40 } // same fields, same type
let rows = [struct { name: "a", n: 1 }, struct { name: "b", n: 2 }]
def sum(p struct { x int, y int }) int {
return p.x + p.y
}
sum(p)
sum({x: 1, y: 2}) // a literal without a type takes the parameter's
A field of the type form is a plain name type; defaults, annotations and
modifiers need a declared struct. A value with no type of its own (none, an
untyped {...}) cannot give a field its type. A different field order is a
different type, a declared struct is never an anonymous one, and two modules that write
the same fields have two types. The inline address struct { ... } field above
is such a type: it is built with {street: "Main", city: "Cluj"} or with an
anonymous literal of the same fields.
Embedding a struct - writing its type name as a bare field - copies that struct's fields onto the outer one:
struct Account {
User // embedding: id, name, age... become Account's own fields
someModule.Properties // embedded types can be qualified too
balance float
}
Enums
Enums declare a fixed set of named constants. Members auto-increment from 0
unless given an explicit value, and a trailing comma is fine:
enum Direction {
North, South, West, East,
}
Give members explicit values - including expressions that reference earlier members in the same enum:
internal enum Color {
Red = 0xFF0000,
Green = 0x00FF00,
Blue = 0x0000FF,
}
enum Flags {
None,
Read,
Write,
ReadWrite = Read | Write,
}
internal enum keeps it visible only inside the declaring module, just like an
internal module.
Access a member through the enum's name, and compare with == like any other value:
let f = Flags.ReadWrite
let same = f == Flags.ReadWrite // true
Enums are declaration-only: they can't host methods, can't be partial, and can't
declare a constructor or destructor. Only plain identifiers are allowed as member names.
Unions
A union is a raw memory type: every member shares the same storage. Writing to
one member changes the bits for all of them - reading a different member reinterprets those
same bytes as its type. Unions don't track which variant was last written.
A union whose members are numbers and bool can be a parameter, a result or a
struct field of a C declaration: it is the C union of
the same members.
union Size {
A int
B float
}
Variants are constructed like calls, and read like fields:
let s Size = Size.A(10)
let a = s.B // 1.401298e-44 - the bits of 10 (int), reinterpreted as float
That reinterpretation is what a union is for. Since nothing tracks the active variant, reading the "wrong" member is legal and hands you back whatever bytes happen to be there.
Unions can be marked internal to be scope it to the declaring module. Unions can't be partial:
Interfaces & Datatypes
An interface declares behavior - a set of method signatures. Any type that has
all of them automatically satisfies the interface, no implements clause needed -
the same implicit satisfaction Go uses for its interfaces.
interface Animal {
Walk() !int
Eat<W>() ?bool
}
internal interface Dog {
Animal
someModule.SomeInterface
Bark()
}
Interfaces support embedding - Dog pulls in every method Animal
requires, plus its own Bark() - and generics in method signatures
(Eat<W>).
== on two interface values compares what they hold: the same object held
twice is equal, two different objects are not.
A method named on an interface value without a call (host.greet, with
host Host) is a function value bound to that value, as a method named on an
object is: apply(host.greet, "x") calls greet on the object the
interface holds. Methods from embedded interfaces work the same way.
A datatype is the same idea applied to data instead of behavior: it describes a
required shape - which fields must exist, and their types - rather than which methods must
exist. Any struct or type exposing those fields satisfies it, implicitly, the same way:
datatype UserLike {
id int
email string
}
internal datatype Account {
UserLike
someModule.SomeDatatype
name string
}
Datatypes embed the same way interfaces do, and can be used anywhere a type constraint is
expected - including generic bounds. & combines constraints (a type must
satisfy all of them); | accepts any one of them:
def save<T: UserLike & Serializable>(value T) {
// UserLike ensures value exposes the right fields,
// Serializable ensures it has the methods needed to persist it.
}
any
any holds a value of any type at all. It's the escape hatch for cases where the
type genuinely isn't known ahead of time - and like a union, ADM won't let you use it as a
concrete type until you narrow it with is or match:
let x any = 123
x is int // true
let p = Person{first: "Ada"}
let a any = p
a is Named // true - interfaces work too
The value keeps its real type underneath, so std.reflection.typeName(x) reports
what it actually is. Reach for a union type instead
whenever the set of possibilities is known - it's checked at compile time, where
any pushes the check to a narrowing site.
Units of measure
A number literal can carry a unit in parentheses: 100(km),
9.81(m/sยฒ), 36(km/h). The value's type is its dimension
(Length, Speed, Force), declared with
@dimension or derived with *, / and **.
+, - and comparisons need the same dimension, and *
and / combine dimensions, so adding a Length to a Time
is a compile error. At run time a quantity is a float in SI base units; the
dimension exists only in the type.
use std.units::*
let distance = 420(km)
let t = distance / 110(km/h) // Time
println(t.str(h, digits = 1)) // 3.8 h
println(t.to(min)) // 229.090909, a float
let wrong = distance + t // error: cannot add Length and Time
v.to(unit) gives the number in another unit, v.str(unit) the text,
and quantity(x, unit) turns a variable into a quantity. When a duration
is used with a quantity, it counts as a Time. Units of measure
covers the unit notation, conversions, offset and logarithmic units, declaring your own
units, and the full std.units catalogue.
Types
A type is ADM's stateful, instantiable building block - the closest thing to a
class. Unlike a struct, it can have methods, properties, generics, and a
constructor/destructor pair:
Declaring a Type
@controller()
partial type User<T> {
use std.crypto.hash
internal atomic id T = defaultValue()
name string = "unknown"
balance float
where T is Account | Admin {
permissions string[]
}
def new(val string) {
name = val
}
def dispose() {
audit("disposing {name}")
}
infix def + (delta float) User<T> {
balance += delta
return self
}
}
Fields work like struct fields, but can take a default value (name string = "unknown";
it is evaluated before the instance exists, so it can't name another member or self),
and the type itself can carry annotations and modifiers the same way a function can - this one
is partial, so the rest of it can live in another file, as long as every fragment
repeats the <T> generic parameter list. use inside a type is
scoped to that type only, and a type body can also nest its own struct,
enum, or union declarations. A const in a type body
belongs to the type, not to an instance: its methods read it by name, other code through
the type, Limits.Most.
internal type Cache { ... } hides the whole type outside its module - the usual
way to keep a helper type from leaking into your public API.
A field marked weak (weak parent ?Node) refers to another object
without owning it: the field must be optional, its target a struct, type, service or component,
and it reads none once the target has been released. Use it for back references so
parent and child form no reference cycle - see
Memory model โ Weak references.
A field marked @persist(key) is kept between runs of the program:
@persist("editor.fontSize") fontSize int = 12. An object that is made starts with
the value kept under the key, before new() runs, and an assignment that changes
the field keeps the new value. It works on a field of a type, a service or a component, not of
a struct; the rules are under
@persist.
A constructor with one parameter marked @implicit makes the type accept that
parameter's type directly: def send(to Secret<string>) is called as
send("hunter2") and the compiler inserts the constructor, the way a value
becomes a ?T. Inward only, one hop, never through an interface, and the
constructor cannot fail; wrappers that validate use a factory returning !T
instead. The language server shows such a parameter as
Secret<string>/string.
A new may leave out what the code around it says. The type arguments of a
generic type come from where the value goes when that is declared, otherwise from the
constructor's arguments: padding Edges<Extent> = new Edges(12(px)),
let free = new Edges(2.5). Where the whole type is declared,
new(...) is enough: card.padding = new(9.0),
return new(7.0).
A where T is Constraint { ... } block adds members that only exist when the
generic parameter satisfies that constraint. Instantiate the type with a type argument that
doesn't match, and those members simply aren't there:
type Admin {}
type Account {}
type User<T> {
name string
where T is Admin | Account {
permissions string[]
def tag() string { return "privileged" }
}
}
let u1 User<Admin> = new(name="bob")
u1.tag() // fine - T is Admin, satisfies the where clause
let u2 User<string> = new(name="alice")
// u2.tag() would not compile - string doesn't satisfy Admin | Account
Constructors & Destructors
def new(...) runs right after allocation; def dispose() runs when
the runtime disposes the instance. Neither one declares a return type, and
dispose takes no parameters: it is the destructor, so a method that takes
arguments has another name. Construction accepts named arguments the same way any call does:
type Pair {
x int
y int
def new(x int, y int) {
self.x = x
self.y = y
}
def sum() int {
return self.x + self.y
}
}
let p = new Pair(y=2, x=3)
dispose is also a statement, for cleaning up before the runtime otherwise would:
type Payload {
id int
def dispose() {
// release whatever id owns
}
}
let a = Payload{id: 1}
dispose a
Properties
A property bundles storage with its accessor logic in one block. Every property gets an
implicit value storage slot, private to the block, that get/set
read and write:
type Box {
count int {
def get() int { return value }
def set(v int) { value = v }
}
}
let b = Box{}
b.count = 2
b.count += 1
print(b.count) // 3
Both accessors are optional: drop get for a write-only property, drop
set and it becomes computed (read-only) instead. Accessors are ordinary
defs and can be internal, atomic,
cuda, or sql - but never async or infix. A
property itself can't be partial and can't declare its own where
block; specialize the surrounding type instead.
Methods
A method is just a def inside a type, with an implicit self
receiver. It can be generic on its own, even when the type isn't, and a call can pin the type
argument explicitly if it can't be inferred:
type Holder {
def mid<T>(x T) T { return x }
}
let h Holder = new()
h.mid(1) // T inferred as int
h.mid<string>("ok") // T pinned explicitly
infix turns a method into an operator overload - infix def + is what
makes a + b work for a type's own instances. Only type methods can be
infix; it can't be combined with async or internal:
type Bits {
value int
infix def <<< (count int) Bits {
return self
}
infix def & (other Bits) Bits {
return self
}
}
let a = Bits{value: 1}
let b = Bits{value: 2}
a <<< 1
a & b
Everything from Params & Returns applies to methods too -
modifiers, overloading, generics, annotations - plus infix, which only makes
sense here.
Control Flow
Branching, looping, and pattern matching, and the same three work on primitives, structs, tuples and every other type.
Conditionals
if/else work as usual, and can carry an
initializer before the condition, separated by ; - scoped to the whole chain:
if let x = test(); x == 1 {
x = 2
} else {
x = 3
}
when and unless turn a single statement into its own guarded
conditional - often more readable than a whole if block for a one-liner:
x = 1 when t
x = 3 unless t
callFunc() when ready
fail unless valid
The ?? choice operator picks between two expressions; only the taken branch is evaluated:
let cond bool = true
let x int = cond ?? (1, 2) // 1
Conditional junctions let := reuse the left operand across a
whole &&/|| chain, instead of repeating it:
let color = "white"
if color := "white" || "black" || "blue" {
// ...
}
// same as: if color == "white" || color == "black" || color == "blue" {}
planned The same trick works with regexes and && - each regex tests the same
subject, which reads nicely for validating a password against several patterns at once:
let password = "secret123!"
if password := /:alpha/ && /:digit/ && /:punct/ {
// ...
}
A junction beside another condition is written in parentheses:
a == b || (c := d || f). Without them it is a compile error.
Loops
for takes a few different headers. The classic three-part form:
for let i = 0; i < 100; i++ {
work(i)
}
Init and post can each carry more than one variable, sharing the clause:
for let i, j = 0, 0; i < b.len(); i, j = i + 4, j + 3 {
b[i] = b[j]
}
Drop the init and post and only a condition is left:
for count < limit {
count++
}
Drop the condition too and it loops forever, until something breaks it:
for {
if done() {
break
}
}
Four suffix blocks attach extra behavior to any for, in any combination:
every expr { ... }- runs after iterations matchingexpr(or after every iteration ifexpris omitted).break { ... }- runs if the loop exits viabreak.empty { ... }- runs if the body never executes at all.finish { ... }- runs after the loop completes normally.
for let i = 0; i < 3; i++ {
sum += 1
break when i == 1010
} every i % 2 == 0 {
sum += 10
} break {
sum += 100
} empty {
sum += 1000
} finish {
sum += 10000
}
A range produces every value from the start up to (but not including) the end:
for let i in 0..10 {
sum += i
}
The loop runs i from the first bound up to, not including, the second. a..b is only a loop header, of for and forall: a range is not a value, and writing one anywhere else is a compile-time error.
The same in form iterates an array - with the index if you ask for it - and a map, exactly like Maps showed earlier:
let values = [10, 20, 30]
for let (idx, v) in values {
println(idx)
println(v)
}
A string iterates the same way, yielding each character with its index:
let s = "ab"
for let (i, ch) in s {
println(i)
println(ch)
}
break exits a loop immediately; continue skips to the next
iteration. Both work in every loop form, and break is what triggers the
break { ... } suffix block:
for let i = 0; i < 3; i++ {
if i == 1 {
continue // skip the rest of this iteration
}
sum = sum + 1
}
for let j = 0; j < 3; j++ {
if j == 1 {
break // leave the loop entirely
}
sum = sum + 10
}
forall runs a range or array loop in parallel, spreading the work across as
many CPU cores as are available. Order isn't guaranteed - each worker just picks up the next
item as it finishes the last one:
forall let val in myArray {
compute(val)
}
The suffix blocks still work under forall: every still runs, just
out of order, and finish only runs once every worker has finished.
Any type can be iterated with for let v in value by implementing the builtin
Iterator<T> interface, a single method def iterate() ?T. The
loop calls it until it returns none; the type keeps its own cursor and resets it
after exhaustion so the value can be walked again. An iterator, like a channel, has no
index or key: with two names the first is _ (for let (_, v) in value),
and two names never take an element apart, so an iterator of tuples is read with one name.
An Iterator<T> is a
real interface value, so it can be stored and passed around:
type Countdown {
from int
next int
def new(from int) {
self.from = from
next = from
}
def iterate() ?int {
if next == 0 {
next = from
return none
}
next -= 1
return next + 1
}
}
for let n in new Countdown(3) {
println(n) // 3, 2, 1
}
let it Iterator<int> = new Countdown(2)
for let n in it { ... }
A generator writes the same thing as a function. A function or method that returns
Iterator<T> and contains yield produces its values one at a
time: calling it returns the iterator without running the body, and each
iterate() runs the body up to the next yield. Locals, parameters
and self keep their values in between. A bare return or the end
of the body ends the sequence, and the iterator then starts again from the arguments of the
call:
def fibonacci() Iterator<int> {
let (a, b) = (0, 1)
for {
yield a
(a, b) = (b, a + b)
}
}
for let n in fibonacci() {
break when n > 100
println(n) // 0, 1, 1, 2, 3, 5, ...
}
def countdown(from int) Iterator<int> {
for from > 0 {
yield from
from--
}
}
yield is a statement and takes when / unless guards.
A generator is a plain function (not async, atomic,
sql, cuda or meta); yield cannot stand
inside forall or transaction. An anonymous function that returns
Iterator<T> and yields is a generator too, keeping what it captures for
as long as the iterator lives. defer runs once per sequence: when it ends, or
when the iterator is dropped before that. A generator of Iterator<!T> may
use try and fail: the error is the sequence's last value, and the
next iterate() answers none. It costs one object.
Switch / Match
switch compares one value against several, with an optional initializer just
like if. Cases can list more than one value, and fall through explicitly with
continue instead of implicitly:
switch let x = classify(); x {
case 1, 2:
continue
default:
return 0
}
A condition-only switch, with no value to compare against, reads like a chain of if/else if:
switch {
case a == 1:
// ...
case a > 1:
// ...
default:
// ...
}
match goes further: it can destructure the value right in the case,
and each case can carry its own guard after a ;:
def classify(x int) int {
match x {
case 0:
return 0
case (n); n > 0:
return n
default:
return -1
}
}
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
}
}
classify matches a literal and binds a guarded name; sum
destructures a tuple positionally; userId destructures a struct's shape
(with a guard) the same way destructuring let does.
A case can also be a bare type instead of a pattern. On a union-typed value, that narrows
it: inside case string: the value really is a string; fall
through to default and it's narrowed to whatever's left of the union.
Multiple types can share one case, narrowing to that subset instead of just one.
A match without default need not name every member: a value no
case takes skips the match, and adm lint warns
(non-exhaustive-match) naming the members left out:
def run(v string | int) int {
match v {
case string:
return 0
default:
return takesInt(v) // v is narrowed to int here
}
}
The same bare-type case works against an interface or datatype - including a generic parameter, which is how a function can behave differently depending on what constraint its type argument happens to satisfy:
interface Stringer {
def str() string
}
def useMatch<T>(x T) string {
match T {
case Stringer:
return x.str()
default:
return ""
}
}
Matching a value against none or error works the same way, and is
covered in more depth under Type Narrowing and
Error Handling.
Type Narrowing
Some values can legitimately be more than one shape: a union type is one of several types, an
optional is either a value or none. ADM won't let you use either as a single
concrete type until you narrow it - with is, match, or a
when/unless guard.
Union & Intersection Types
A union type, T1 | T2, accepts either shape, and is valid anywhere a type
annotation can appear. is checks which one you have; match does
the same thing case by case:
let width string | int = 100
let pixels int
if width is string {
pixels = parsePixels(width)
} else {
pixels = width
}
// or by pattern matching
match width {
case string:
pixels = parsePixels(width)
case int:
pixels = width
}
Nothing lets you read width as a plain value without picking a branch first -
unlike structurally typed languages such as TypeScript, there's no "anything goes" path
around it.
A value of a union goes where another union is expected when that union takes each of its
members. What is left of an A | B | C once is A was ruled out is a
B | C, and it is still returned, assigned and passed as the union it came from.
An optional or an errorable is unwrapped first.
== on two union values compares what they hold: 13(px) stored in
two Extents is one value, two strings compare by their text, two objects only
when they are the same object, and members of different types are never equal.
A member that is errorable or optional is written in parentheses,
(!string) | User, since !string | User is the errorable of
string | User. The wrapper does not stay in the value: a failed
!string is its error, an empty optional is none,
so match has case error or case none beside the
member types.
An intersection type, T1 & T2, requires both at once. It shows up mostly in
generic bounds, combining an interface and a datatype constraint the way
UserLike & Serializable did earlier.
A type alias can name any type expression, including a union, and reads exactly like the type it points to:
type Numeric = int | float
let score Numeric = 9.5
Aliases are always a single expression - they can't be partial.
Optional Types
?T marks a value that might be none instead of holding a
T. The same narrowing tools apply - is, match, and the
when/unless guards from Conditionals:
struct Box { value int }
def read(b ?Box) int {
return 0 when b is none
return b.value
}
Converted to any, an optional does not stay one: an empty ?T becomes
none and a set one becomes its T, so v is none and
case T work on the any as they do on the optional.
An optional of an optional is that optional, since there is one none: where a type
parameter T stands for ?string, ?T is ?string.
The same value narrows just as well with match:
let o ?int = none
match o {
case none:
// nothing to read
case int:
println(o)
}
Functions
A function declaration can carry modifiers, generics, parameter defaults and annotations, and - for a few special cases - a foreign-language body instead of an ADM one:
@annotation()
internal atomic async def funcName<A, B: Serializable>(a int, b string = "default") !?Result {
let x, y = 1
return try process(a, b) onerror recover fallback()
}
Params & Returns
A function can carry any of these modifiers:
internal- visible only inside the declaring module, same as everywhere else.atomic- wraps the call in a per-instance lock, so only one caller runs it at a time; everyone else blocks until it's done. More under Concurrency.async- the function returns a future instead of running immediately. Covered in depth under Async / Await / Futures.infix- turns a method into an operator overload (a + b). Only type methods can beinfix, so it's covered under Types โ Methods.meta- turns the function itself into a reusable annotation, the way@trim(below) is defined. Covered in depth under Attributes & Modifiers.cuda,sql- the body is a foreign-language DSL instead of ADM statements (below).
Parameters can share a type across grouped names, take a default value, and a call can name any argument regardless of position - arguments after a named one just can't go back to being positional:
def add(a int, b int = 5) int {
return a + b
}
let s = add(a=2, b=3)
let t = add(a=2) // b defaults to 5
A trailing ...T parameter collects any number of extra arguments into an array, and a matching array can be spread back out at the call site with the same ...:
def sum(nums ...int) int {
let total = 0
for let (_, n) in nums {
total = total + n
}
return total
}
let xs = [1, 2, 3]
let a = sum(...xs)
let b = sum(0, ...xs)
Parameters take annotations too, and an annotation can rewrite the value before the body
ever sees it. @trim() does exactly that - it reads the argument, trims it, and
writes the trimmed value back:
def isAlpha(@trim() txt string) bool {
// txt has already been trimmed by the time the body runs
// ...
}
Annotation arguments can be named too, the same way call arguments can -
@validate(min=-10, max=10) is the general shape. Annotations aren't limited to
compile time either: they're inspectable at runtime through reflection, the same mechanism
behind @flag, @json, and @controller.
Functions and methods can share a name as long as their full signatures - parameter types
and count, plus the receiver type for methods - differ. Overload resolution happens entirely
at compile time; a call must match exactly one candidate or it's a compile error, never a
runtime guess. Used as a value, an overloaded name takes the overload the expected function
type asks for (let f def (int) string = show); a let without a
declared type cannot take one.
cuda and sql functions carry a foreign-language
body instead of ADM statements. {name} inside a
sql body names a parameter or a field of the type; it reaches the database as a
bound parameter, never as text pasted into the statement:
cuda def vectorAdd(a, b, out float[]) !none {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < out_len) out[i] = a[i] + b[i];
}
type Users {
@connection()
conn db.Queryable
sql def find(email string) !?User {
SELECT * FROM users WHERE email = {email}
}
sql def rename(id int, name string) !none {
UPDATE users SET name = {name} WHERE id = {id}
}
sql def emails() !string[] {
SELECT email FROM users
}
}
A sql def is a method of a type that holds its connection: one property marked
@connection (from std.data.db) holding a db.Queryable, a
connection or a transaction. The result type says how the rows are read, and is always errorable:
!noneruns the statement;!db.Resultreturns what it changed;!Userreads the first row into the struct or type, column by field name, and fails when there is none;!?Userreturnsnonethen;!User[]reads every row;!int,!string,!bool,!float,!byte[], sized numbers, dates and times read the first column;?turns a missing row or a NULL intonone,[]reads the column of every row;- a tuple such as
!(int, ?string)reads one column per element.
An alias reads as the type it stands for: type Pair = (int, ?string) makes
!Pair a tuple, type MaybeUser = ?User makes !MaybeUser an
optional row. The statement is the body's text without the indentation its lines share, so
adm fmt never changes it. Assigning the @connection property waits until
the queries running on that instance finish.
A sql def outside a type, or in a type that marks no property, takes its connection
from the Database service: the one @db("name") names, else the default
connection. A missing connection fails the call, not the build.
The statement is the same text for every database: the compiler hands it to the driver with the
driver's own placeholders (?, $1, :name). The connection
comes from std.data.db (db.connect(url), db.pool(url))
with one of the driver libraries linked: adm.database.sqlite,
adm.database.postgres, adm.database.mysql. Rows map onto a struct's
fields by column name; @column on a field names another column or the form the
value is stored in. The Databases example shows both.
A sql def can also be written where it is used, as an anonymous function:
let count = sql def (least int) !int { SELECT COUNT(*) FROM stock WHERE quantity >= {least} }.
It runs on the connection of the type whose method holds it, else on the service's, and its
placeholders read its parameters and the variables around it.
Inside a transaction { } block the sql def calls run in a database
transaction, with nothing to write at the call sites: the first call on a connection begins
it, later calls on that connection reuse it (nested blocks and called functions included),
the end of the block commits it and a failure in the block rolls it back. A block that
touches two databases holds one transaction on each; their commits are separate, and when
one fails after another committed the error is a TransactionCommitError whose
committed names the databases that stay committed. A task started inside the
block runs its queries outside the block's transactions.
try db.isolate(db.Isolation.Serializable, readOnly = true) inside the block sets the
isolation level and read-only mode of the transactions it begins after that call.
A body of several statements is a script: the statements run one by one, each with its own
placeholders, and a failure names the statement (statement 4 of 9: ...). A
; inside quotes, comments or a trigger's BEGIN ... END does not end
a statement. A script returns !none and does not begin or commit a transaction
itself: it runs inside the caller's transaction { } block.
sql def createSchema(firstTeam string) !none {
CREATE TABLE teams (id bigint PRIMARY KEY, name text NOT NULL);
ALTER TABLE users ADD COLUMN team bigint;
INSERT INTO teams (id, name) VALUES (1, {firstTeam});
}
A body written BEGIN ... END (or DECLARE ... BEGIN ... END) is a stored
routine in the database's own language. It is created in the database under the function's
name when its connection is added to the Database service, and again only when
it changed; calling the function calls it. The result picks the kind: !none a
procedure, a single value a function, rows a function returning a table (PostgreSQL) or a
procedure that selects them (MySQL). SQLite has no stored routines.
@db("reports")
@db.routine(name = "top_users", schema = "analytics") // optional: another name, a schema
sql def topUsers(n int) !User[] {
BEGIN
RETURN QUERY SELECT * FROM users ORDER BY score DESC LIMIT n;
END
}
@db.sqlschema("schema.sql") on a type (or on one sql def) checks its
statements against the schema's DDL when the program is built: a table or column the schema
does not have, a placeholder whose value does not fit its column, or a result whose columns do
not read as the declared type stops the build at the statement. The file is what
pg_dump --schema-only or mysqldump --no-data writes.
The database's schema is set up by the application's own migration steps:
@migration(from = "4.32", to = "4.33", db = "main") on a sql def (often
a script) or on a function that calls several. Database.add runs the steps the
database has not seen, each in a transaction, under a lock so instances starting together
migrate once; a database with no version yet runs them all. The version is kept in the
application's home folder, or with versions = "ops.adm_versions" in a table of the
database, which several servers sharing it need.
Everything outside a {name} placeholder is plain text in the target language -
the compiler doesn't try to understand it, just records the identifiers it needs to
substitute before handing the body to the target toolchain.
A cuda def body is one CUDA kernel. Its ADM parameters are the kernel's
parameters: a numeric scalar arrives as the matching C type, and an array T[]
arrives as T* name plus int64_t name_len, copied to the device
before the launch and back after it. The result is always !none: a launch can
fail (no driver, rejected kernel), and values come back through the arrays. The grid is one
thread per element of the first array or buffer parameter, in blocks of @launch(block = N)
threads (default 256), so a body guards with i < name_len;
@launch(x = "w", y = "h") names integer parameters holding the thread count per
axis instead.
use std.gpu
cuda def scale(values float32[], factor float32) !none {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < values_len) values[i] *= factor;
}
def new(args string[]) int {
let samples = float32[1, 2, 3, 4]
scale(samples, 2.5) onerror return 1
println(samples) // [2.5, 5, 7.5, 10]
println(gpu.available()) // true when a driver loaded
return 0
}
The kernel is compiled at build time for every GPU architecture the toolkit knows (PTX plus
SASS, one fatbin) and also as plain C, so the same program runs without a GPU, or with a driver
that cannot run the images: the CPU path runs the blocks in parallel. ADM_CUDA=gpu or
=cpu forces either, ADM_CUDA_DEVICE picks the device, and
gpu.available() / gpu.deviceName() report the path in use. A body
that needs device-only features (__shared__, __syncthreads, warp
intrinsics) declares @launch(cpu = false) and fails without a GPU. A kernel
that uses newer instructions names the lowest compute capability it needs,
@launch(arch = 80) for TF32 tensor cores: its image targets that architecture and newer,
and on an older device it takes the CPU path (or fails, when GPU-only).
gpu.capability() gives the device's capability times ten, so a caller picks
the kernel: if gpu.capability() >= 80 { tf32Matmul(...) } else { matmul(...) }.
Data that many launches share stays on the device in a std.gpu.DeviceBuffer<T>:
gpu.upload(data) copies once, the kernel declares the parameter as a
gpu.DeviceBuffer<T> and sees it like a T[], download and write move data explicitly, and
dispose frees it. download waits for every queued launch; a buffer.readback()
(start(), then wait(into)) waits only for those queued before its start;
readback(overlap = true) also lets the launches queued after start run during the copy, provided they do not write the buffer. To send data without waiting, keep it in a gpu.hostBuffer<T>(count) (page-locked memory, filled with write(from, at)): buffer.write(from, start, count, at) queues the copy of a range behind the launches queued so far and returns at once; with overlap = true the copy runs beside the launches queued after it, which must leave the range alone until buffer.join(). async cuda def returns a future, so the scheduler
keeps other tasks running while the driver works:
use std.gpu
async cuda def step(cells gpu.DeviceBuffer<byte>, width int, height int) !none { ... }
def simulate(initial byte[], width int, height int, generations int) !byte[] {
let cells = try gpu.upload(initial)
for let g in 0..generations {
try await step(cells, width, height) // no copies between launches
}
let out = byte[]
out.len(cells.len())
try cells.download(out)
cells.dispose()
return out
}
Closures
A def block can be declared inline as a value - with the same parameter and
return syntax as a top-level function - and called immediately, or stored and called later:
let value = def () int {
let start = now()
defer log("elapsed", now() - start)
return compute()
}()
It closes over surrounding variables by reference, the same way a deferred call does - give it a name and it can even call itself, which is otherwise the only reason to name one:
let f = def fib(n int) int {
return n when n < 2
return fib(n-1) + fib(n-2)
}
An anonymous function can still have a sql or cuda
body - but it can't be meta or infix, and doesn't accept
internal or atomic; those only make sense on a named declaration.
A lambda is sugar over the same thing: param => expr expands to an anonymous
function that returns expr. The parameter type can be dropped when it's already
known from context, like a callback argument:
let a = int[1, 2, 3]
let b = a.mapElem(v => v + 1)
let c = a.mapElem((v int) => v + 1)
param => { ... } takes a statement body. Its result is what the place asks
for, with the parameter types written or not: handed to a def (int) int it
returns an int, handed to a def (int) or kept in an untyped
let it returns nothing, and a return with a value there is an error.
A call of a function that returns nothing gives no value either: let x int = log("a")
is warned of (W-TYPE-0002) and x holds its type's zero value, an untyped
let y = log("a") holds none, and _ = log("a") discards the call
without a warning.
Defer
defer schedules a call to run at the end of the current function's scope -
typically cleanup, like closing a resource opened earlier in the same function:
def run() {
let f File = new()
defer f.close()
defer print("second")
print("body")
}
Multiple deferred calls run in reverse order - last deferred, first run - so the code above
prints body, then second, then finally closes f.
Error Handling
Errors are values, not exceptions. A function that can fail says so in its return type, and a failure propagates up the call stack on its own until something tests for it, recovers it, or the program crashes.
Errorable Types
!T marks a value that might be a T or might be an error. If a
function's body contains fail, its return type has to be errorable - the
compiler checks this, not just a convention:
// compile-time error: fail requires an errorable return type
def name(a int) int {
if a == 0 {
fail "invalid"
}
return a**a
}
// compiles: the ! marks the return type errorable
def name(a int) !int {
if a == 0 {
fail "invalid"
}
return a**a
}
An errorable value has to be dealt with before you can use it as a plain value - either test
it, or use try to propagate the error to whoever called you instead of handling
it here:
// open returns !File - f might be a File, or an error
let f !File = open("file")
// try unwraps it to a plain File, or bails out of this function
// with the same error if open failed
let f File = try open("file")
fail / onerror
fail mirrors return, but returns an error instead of a result.
Failing with a string is shorthand for building the builtin Error struct;
failing with any struct that has the same shape works too, so you can attach extra fields.
A handler tells them apart with is: err is MyCustomStruct is true
for an error raised with that struct and reads its fields in the branch;
err is Error is true for the plain error a string or an
Error literal raises.
A return without a value ends a function that has no result, one declared
none, and one declared !none (it returns without a failure). In a
function with any other result it is an error.
fail "reason"
// same as
fail Error{message: "reason"}
// a custom error struct, as long as it has the fields of error
// (message, code, cause, stack)
fail MyCustomStruct{message: "reason", traceid: id}
onerror attaches a handler to a call. recover stops the error there
and supplies a fallback - a value, another call, or a whole block if you need more logic -
instead of letting the error keep propagating:
def mightFail(flag bool) !int {
if flag {
fail "boom"
}
return 7
}
// recover with a plain value
let a = mightFail(false) onerror recover 99
// recover with access to the error itself
let b = mightFail(true) onerror (err error) recover 99
// bail out of the surrounding function instead of recovering a value here
let c = mightFail(false) onerror return 0
A block form works when recovering takes more than one expression:
let h = open("file") onerror (err error) {
log("Error opening file: {err}")
recover try open("default") unless error.critical
}
Contracts
expects and provides wrap a function with assertions - preconditions
checked before the body runs, postconditions checked after, no matter which return
path was taken. Because a failed assertion is itself a failure, a function using either block
has to return an errorable type:
def guarded(x int) !int {
expects {
assert x > 0
}
let y int = x + 1
return y
provides {
assert y > x
}
}
assert also works on its own, outside expects/provides,
anywhere a boolean check makes sense. Either way, a failed assertion can have its error message
overridden with onerror:
def createUser(name, email string) !User {
expects {
assert name != "" onerror fail "Name is required"
assert email != "" onerror fail "Email is required"
} onerror (err error) fail ExpectationErrors{message: "Invalid input", cause: err}
// save user to db and create a User instance
return user
provides {
assert user != none
assert user.name == name
}
}
Transactions
begin and rollback blocks declare how a function saves and restores
state, but they only actually run when that function is called from inside a
transaction { ... } region - outside one, they're skipped entirely:
let state int = 0
def someFunction(x int) {
begin {
state = state + x
}
rollback {
state = state - x
}
state = state + 1
}
Inside a transaction block, any fail triggers every available
rollback automatically; rollback can also be called directly to trigger it by
hand. onerror on the transaction itself handles whatever error comes out the
other side:
def run() !none {
transaction {
someFunction(5)
rollback
} onerror (err error) {
recover none
}
return none
}
A resource can also end with the block: anything with commitTx() !none and
rollbackTx() !none, such as a database transaction. A library joins it to
the running block on first use; when the body is done the block commits its resources
in the order they joined, and a commit that fails rolls back the ones after it and
arrives in onerror. When the body fails they are rolled back, newest
first. A nested block joins the outermost one.
Concurrency
Three primitives cover concurrency: futures
for a single background result, channels for a pipe between workers, and select
for waiting on several channels at once.
Async / Await / Futures
An async function returns a future - async T - instead of a
T directly. await unwraps it; using an async T without
awaiting it (or propagating it onward as async) is a type error:
async def fetch() int { return 41 }
def new(args string[]) int {
let a async int = fetch()
let b int = await a
print(b)
return 0
}
Any call - async function or not - can run in the background just by prefixing it with async:
def compute(x int) int {
return x + 1
}
let f async int = async compute(41)
let v int = await f
await composes with &&/||, using the same
short-circuit logic as booleans but over completion state instead. await (a || b)
resumes on whichever finishes first and cancels the other (unless it called
.keepAlive()); await (a && b) waits for both:
async def slowInt() int {
// ... slow work ...
return 7
}
async def fastString() string {
return "ok"
}
let a = slowInt()
let b = fastString()
let v = await (a || b)
print("string") when v is string
print("int") when v is int
A future exposes .running()/.done()/.canceled() to
check its state, .cancel(?reason) to request cancellation, and
.timeout(duration) to set a deadline:
let homepage async string = load("https://adm-lang.dev")
homepage.timeout(3s)
Cancelling is cooperative. A cancelled or timed-out future is finished at once, so
await returns without waiting for the body and never panics:
an async T gives the value an uninitialised let of
T holds (0, "", an empty array, a default
instance, nothing for any or a union), an async ?T gives none, and an
async !T gives an error with code canceled or
timeout whose message is the reason passed to cancel.
.canceled() tells a cancelled result from a computed one, and so does
the tag canceled: on the value itself when it is an object, on the
variable an await initialises when it is a number, a bool or
another scalar (let n = await job, then n.tags().has("canceled")).
Cancelling an a || b or a && b cancels both operands,
except those kept alive.
The task running the body is told, not stopped: its time.sleep returns
early, an await inside it cancels the future it waits for (unless that
future is .keepAlive()), and a socket operation it waits in fails with code
canceled. A channel it waits on behaves, for that task only, as if it were
closed and empty: a receive yields the zero value, a for over the channel
ends, a select runs no case, and a send with no room does not send; the
channel stays open for everyone else. Code that computes without waiting asks
canceled(), which is true in a cancelled task:
async def index(files string[]) int {
let count = 0
for let file in files {
return count when canceled()
count += indexOne(file)
}
return count
}
A future cancelled before its task first runs never runs its body. A task waiting for a lock or inside foreign code is not interrupted.
An AsyncScope handles futures as a group. async<group> call()
starts a call as a member (the scope is a name, a field or a call), and a future that
already exists joins with .scope(group); await group returns
when no member is running and fails with the first member's error. A member that fails cancels the others
(new AsyncScope(failFast=false) lets them finish), and a scope that is
dropped cancels the members still running:
def load() !Page {
let group = new AsyncScope()
let profile = async<group> fetchProfile()
let teams = async<group> fetchTeams()
try await group
return new Page(await profile, await teams)
}
group.cancel(?reason) cancels the members still running and those that
join later, group.timeout(duration) gives the group one deadline,
group.limit(count) lets at most that many members started with
async<group> run at once,
group.running(), group.done() and group.len()
report its state, and group.errors() lists the errors of the members
that failed. group.wait() is the method form of await group.
try and await compose in either order on an errorable future -
try await f() and await try f() both work.
An onerror handler may follow an awaited expression, as it may follow a call:
let page = await task onerror recover emptyPage(). It takes the failure of the
awaited !T and leaves the T, for a future in a variable, an awaited
call (await fetch(id) onerror ...) and an AsyncScope
(await group onerror (err error) { ... recover none }).
A TaskLocal<T> holds one value per running task. set(value)
stores it for the task that calls it (none clears) and get() reads
it later in the same task as a ?T, however deep the calls between them. Declare
it once, as a module let. A task started with async begins with the
values of the task that started it, as they were at that moment; what either one sets
afterwards the other does not see, and a task's values are dropped when it ends.
let currentRequest = new TaskLocal<Request>()
def handle(request Request) {
currentRequest.set(request)
route()
}
def audit(action string) {
let request = currentRequest.get()
if request is Request {
record(request.id, action)
}
}
Channels
A channel is a typed pipe between workers - chan <- value sends,
<-chan receives. An unbuffered channel (the default) blocks the sender until
a receiver is ready; a buffered one has room for values to queue up:
let ch channel<int> = new(1) // buffered, room for 1
println(ch.len())
ch <- 7
let v = <-ch
println(v)
new channel<T>(size=?, mode=?) is the full constructor - size
defaults to 0 (unbuffered), and mode (ChanMode.FIFO or
ChanMode.LIFO) defaults to FIFO. A channel exposes a closed property, and the range form of
for drains it until the producer closes it: for let value in channel { ... }.
ch.offer(value) sends only when that needs no waiting and returns whether it sent:
a buffered channel must have room, a channel of size 0 a receiver waiting. It never waits and
returns false on a closed channel. A wake-up signal several tasks may raise is
signal.offer(none) on a channel<none>(1).
Select
select waits on several channels at once, running whichever case is ready first,
or default if none are:
let ch channel<int> = new(1)
ch <- 42
select {
case ch:
let v = <-ch
println(v)
default:
println(0)
}
There's no fall-through, so continue isn't allowed inside a select
case - every case either handles its event or control leaves the select entirely.
Value Tags
Every reference value carries an optional set of string tags, reached through
.tags(). Tags are metadata for tracing, ownership tracking and policy checks: a
request marked as coming from the primary network, a string that has been signed, a record
holding personal data. They are not part of the value's type.
Tagging Values
value.tags() returns the value's TagSet, creating an empty one on
first use. The set offers add, remove, toggle,
has, list, len, clear and
set(spec), where the spec is whitespace-separated: +tag adds,
-tag removes, a bare tag adds, and the empty string clears
everything. Mutators return the set, so calls chain.
let request = new Request(7)
request.tags().add("Network/primary").add("pii")
let message = "hello"
message.tags().set("+signed")
if request.tags().has("pii") {
redact(request)
}
request.tags().set("-pii +audited") // remove one, add another
request.tags().set("") // clear
Strings, arrays, maps, channels, errors, any, closures and instances of declared
types carry their tags on the object itself - struct instances included, since
they are heap-backed. Numbers, bool, char and enums have no
identity, so their tags belong to the
variable holding them: let x = 5; x.tags().add("trusted") tags
x's storage, and the compiler moves the tags with the value through bindings,
arguments and returns. A copy shares the set until its first mutation, then gets its own -
tagging a copy never changes the original. Only named storage can be tagged:
5.tags() and (a + b).tags() are compile errors. Tags do not cross
indirect calls, and scalar array elements or struct fields do not carry tags yet.
Propagation Rules
A tag set belongs to the value's identity. It follows the value wherever the value itself goes: assignments, function arguments and returns, container slots, channel sends, futures and errors. A callee can therefore check what it was handed:
def send(message string) !none {
fail "refusing an unsigned message" unless message.tags().has("signed")
// ...
return none
}
let items Request[]
items.push(request)
items[-1].tags().has("pii") // true: the same object sits in the slot
let ch channel<Request> = new(size=1)
ch <- request
let got = <-ch // got.tags() is request.tags()
A copy is a new value and starts with an empty set: a + b on strings,
clone(), slicing, map.values() and a freshly created error inherit
nothing from their inputs. The set lives exactly as long as the value and is released with it.
Services
A service behaves like an OS service: the ADM service manager starts it before
application.new runs and stops it on shutdown, and it owns exclusive access to
one slice of the platform - networking, storage, the UI surface, whatever it's responsible for.
A service method is one of three kinds: @query() reads a snapshot of its state,
@commit() mutates it and emits a typed Event<T> that other code
can react to, and @emit() only exists to describe that event's shape - it can't be
called directly. An event emitted inside a commit reaches its handlers after the outermost commit completes; @emit(immediate = true) runs them at the call.
Those annotated methods are the whole public surface. Fields are always private to the
service, and a def without an annotation is internal: callable from the service's
own methods, refused from anywhere else (method "helper" is internal to service
Counter). The manager's members are the exception: the ServiceDescriptor
fields (description, dependencies, lifecycle,
permissions, status) and the ServiceHooks methods (config,
start, stop, restart) stay reachable. @commit() may be async; @query() and
@emit() may not.
use std.services::(Dependencies, Lifecycle, RestartMode, StartMode)
service Network {
description = "Outgoing connections"
dependencies = Dependencies{hard: [], soft: []}
lifecycle = Lifecycle{start: StartMode.Manual, restart: RestartMode.Never}
permissions = []
connections Connection[]
def config<T>(val T) !none {
return none
}
async def start() !none {
return none
}
async def stop() !none {
return none
}
async def restart() !none {
return none
}
@query()
def connectionCount() int {
return connections.len()
}
@commit()
async def open(address string) !Connection {
let con = try dial(address)
connected(address)
connections += [con]
return connections[-1]
}
@emit()
def connected(address string) string {
return address
}
}
@on(Network.connected)
def log(evt Event<string>) {
println("Connected to {evt.result}")
}
Network.open("...") calls straight into the service like any other function. The
handler subscribed with @on(Service.method, ?filter) takes exactly one argument -
Event<T>, where T is that method's return type - and an optional
filter to only react to some events. What the event carries, when handlers run,
and how the manager starts, restarts and reports services is covered under
Services โ Events and observers.
When the program ends, the started services stop, each before the services it names as hard
dependencies, so a stop() can still use them; services with no such relation stop
in the reverse of the order they were registered in. A service's
@persist(key) fields take their kept values when the
program starts, before any service's start() runs.
Services can't be constructed with new and can't declare their own
new/dispose - the manager owns that lifecycle entirely.
@commit() methods are implicitly atomic; @query() and
@emit() can never be async; and infix is rejected outright,
since nothing ever operates on a service like a value. A service can still be internal
or partial like anything else.
ADM ships a set of builtin services that cover the platform surface every application
needs, each in its own module std.services.<name>
(use std.services.cache::(Cache)). Every method signature and every annotation they provide is listed in the
services reference.
| Service | Description |
|---|---|
| Application | Identity of the running application (PID, executable path, version, uptime), its home folder, shutdown and exit. |
| Runtime | The ADM runtime: system and memory information, environment variables, the service list, starting programs. |
| Storage | Files and directories: open, read, write, list, memory maps. |
| Network | Sockets, addressing and name resolution; std.net is the usual way in. |
| Cache | In-memory key-value cache with TTL and idle expiry. |
| Scheduler | Cron-style scheduling of functions at intervals or times. |
| Policy | Permissions and policy decisions. |
| Diagnostics | Metric registry: counters, gauges, histograms. |
| Notifications | Desktop notifications. |
| Clipboard | Text copy and paste through the system clipboard. |
| Devices | Cameras, microphones, GPUs, USB and other devices. |
| Debugger | CPU and memory profiles, call traces. |
| UI | Windows, the renderer, UI events (std.ui). |
| Config planned | Layered configuration with profiles. |
| Logging | Leveled logging with structured fields, to console, file and network writers. |
| Messaging | In-memory queues by topic: consumer groups, priorities, retries and dead letters. |
| I18n planned | Locale selection and message catalogs. |
| IPC | Remote interfaces between processes: @remote<I> serves an interface, IPC.connect<I> calls it, over TCP, TLS or Unix sockets. |
| Telemetry | Sends metrics, logs, errors, usage events and spans to a configured server (OpenTelemetry), with the user's consent. |
| Vault | Encrypted key/value store for secrets, values and keys. |
UI
ADM's UI is built from three pieces bound together into a component: a view (structure), a style (appearance), and a controller (behavior) - all written in plain ADM, not a separate template language.
The standard component set is std.ui.components:
Button, Text, Row, TextField and the rest. A name in the
samples below that is not in it (MessageCard) stands for a component you declare
yourself.
UI components goes through the same system as a guide, with the windows and a picture of every component std ships.
Views & Styles
A view body is a regular ADM block. Components are instantiated like function calls
with arguments passed by name (Button(label = "Logout", onClick = doLogout)), children go in a trailing
{ ... } block, and ordinary control flow - if/else,
for, match, the when guard - works right inside it:
view MyView {
Button(label = "Logout", onClick = doLogout) when user.isLoggedIn
if user.isLoggedIn {
Text(text = "Welcome back, {user.name}")
for let (_, item) in messages {
MessageCard(item)
} empty {
Text(text = "You have no messages")
}
} else {
Column(gap = 8(px)) {
Heading(text = "Welcome")
TextField(email, placeholder = "Email", kind = TextKind.Email)
TextField(password, placeholder = "Password", kind = TextKind.Password, onSubmit = submit)
Button(label = "Login", onClick = submit)
}
}
}
A style body is the same idea applied to appearance: a regular block, run in a scope that
predeclares context variables - centered, box, on,
children, plus layout references like root/parent/prev/next.
Assigning a struct literal to one overrides the base style; let introduces a
reusable snippet:
style MyStyle {
let width = 100
centered = {
align: root.center,
size: width,
}
box = {
padding: width / 10,
backgroundColor: prev.backgroundColor,
right: min(next.left - 10, root.right - 30),
}
if parent.width >= 100 {
centered.box.backgroundColor = Colors.alert
}
// per-state overrides
on = {
hover: { cursor: "pointer" },
pressed: { transform: scale(0.97) },
}
// style child components by name
children = {
Button: { ...loginButton },
VStack: { spacing: 20 },
}
}
Views and styles can't be partial or annotated - they're pure templates. Declare
one standalone with a name, like MyView above, and a component can reference it
by that name; or write it anonymously, inline inside the component itself.
Components
A component binds a view, a style, and a controller type together.
The UI service asks for a window or surface and renders the component tree - the same
component source targets a desktop window or a WASM/web canvas; the backend decides which.
Reference named view/style/type declarations, or write every piece anonymously inline - both are the same construct:
component myComponent {
use lib
view lib.MyView
style lib.MyStyle
type lib.MyType
}
component myComponent {
view { // anonymous view
}
style { // anonymous style
}
type { // anonymous controller
}
}
The controller type is where state and event handling live - a regular
type, following every rule from Types (imports, annotations,
internal members, infix methods, all of it):
use std.ui
use std.ui::(UI)
component SubmitButton {
view {
}
style {
}
type {
label string = "Submit"
loading bool = false
@on(UI.click)
def onClick(evt Event<ui.ClickEvent>) {
loading = true
// do some work here
}
}
}
A component can be internal: it stays inside its module, a part that the
module's own components place and that nothing outside can name, so a control split into
parts exposes only the one that assembles them.
A component can be partial, but its embedded view and style can't be - only the
component wrapper splits across files. UI events are delivered through the
UI service; a controller method opts in by annotating itself, the way
onClick does above.
children() as a statement of a view is the place of what the caller wrote in the
block after the call: view { Text(title); children(); Divider() } puts the block's
components between the view's own two parts. It may stand under an if and in the
block of a call of the view (Box() { children() }), which hands them on to that
child. Their arguments are evaluated in the caller's view, and their parent() is the
component whose view placed them. A block that reaches a component, on a call of a component
whose view has no children(), is a compile error.
A slot is an input typed view. The caller fills it inside the
block of the call with an assignment, and the component's view places it by calling it:
component Card {
type {
@input
title string = ""
// A slot with a fallback, and an optional one.
@input
header view = view { Text(title) }
@input
footer ?view = none
}
view {
header()
children()
if !(footer is none) {
footer()
}
}
style Plain
}
Card(title = "Profile") {
header = view { Icon("user"); Text(user.name) }
if canEdit {
footer = view { Button("Edit") }
}
Text(user.bio)
}
view { ... } is an expression: its statements are a view's, written in the scope
of whoever writes it, and it has no box of its own. Where it is placed its components become
children of the component around the call; placed twice, it is mounted twice. A slot has a
fallback, shown while nothing filled it, or is optional and placed under
if !(name is none). Only slots are assigned in a block, and a slot whose name is
also in scope where the block is written is a compile error: one of the two is renamed. A slot
that receives something is a function returning a view
(row ?(def(Item) view) = none, placed with row(item)), filled with
row = (it Item) => Row(it): a component call written where a view is expected
is the view of that call.
ref = name on a component call of a view names the component the call places
and declares name as a member of the component, usable in its type, style and
view. The member is a weak ?TextField for TextField(ref = named):
the view sets it when the child is built, and it reads none while the child is
not in the view (TextField(ref = named); Label(target = named)). No field is
written for it; a field or method of that name is a duplicate identifier. A ref is given
once per view and not inside a loop.
A component takes type parameters after its name, as a type does:
component List<T> { ... }. The parameter is in scope in its
type { } and its view (items T[], def first() ?T),
and List<File> is a type (weak list ?List<File>,
c is List<File>). A call writes the type arguments,
List<File>(files), or leaves them to its arguments,
List(files); a parameter no argument settles takes its default or is a compile
error. Each instance is compiled for its own type arguments. A ref on such a
call declares its member with the instance the call makes
(List(files, ref = list) gives a ?List<File>). The
component's inline style { } reads self, the refs and the type
parameters in its values; only a let of that style cannot be typed with a
type parameter.
A component may name its controller as a type declared apart and marked
@controller(): component Counter { ...; type Counting }. The
component has that type's fields, inputs and methods as if they stood in its own
type { }, and inside the controller self is the component.
Several components may name one controller, each with its own state; the controller may be
a partial type split over files, and for a generic component it declares the
component's type parameters (type Listed<T>). @emit
and ref need the inline type { }.
A method of a component's type { } marked @emit() is an event of
the component. A listener is a function or method marked @on that takes
ComponentEvent<T> (source, name,
result; T is none for an event without a result):
@on(Button.clicked) hears every button,
@on(save.clicked) the one the ref save holds, and
@on(Button.clicked, ref("save")) those a selector matches. A listening method
runs for each live instance of its type.
The unnamed argument of a component call (Text("hi")) sets the controller field
marked @input(value = true); every other argument is passed by name. A field
declared @input(bind = true) is an input its component may assign: the assignment
reaches what the caller passed when that is a field marked @bindable (itself, or
its whole struct or type) or a property with a setter, named alone or as the last member of a
path such as model.email. Anything else the caller passes is a copy.
A field of a component's type { } marked @state is a state of the
component: an assignment that changes its value emits UI.stateChange (in
std.ui) with StateChange { source; state; value; previous }, the
component, the field's name, the new value and the one it replaced. Listeners
(@on(UI.stateChange), taking Event<StateChange>) run at the
assignment, whoever makes it: a method, other code, the parent's view passing the input
another value, a bound input assigned by the child. Any task may assign a state: a statement
that reads or assigns one holds the program's state lock, which the UI loop holds for each
pass and a task gives up while it waits. Numbers, strings and enums compare by
value, objects, arrays and maps by identity; an equal value emits nothing, nor do the values a
component is built with. A change in place of what the field holds counts as a change too
(counts[key] = 1, items[i].done = true, files.push(f):
an element or member stored through it, or a @mutates method of a value reached
from it). A selector picks the changes
a listener hears: @on(UI.stateChange, ref("mainTabs").state("selected")), or
own().state("file") in a method of the component itself.
Every component has the members of the prelude's Component<T>: inputs such as
enabled, the tree (childNodes, parent()) and the lifecycle
(attached(), detached(), render(ctx), rerun() which runs its view again, unmount() which lets go of the children its view placed once it has left the tree, setChildren(given) which makes the components of given what the view places with children(), for a component made in code that holds others). A controller method
with the same name and parameters replaces one of them for that component, except rerun() and unmount(), which the compiler writes from the component's view: declaring either is an error that asks for another name. The UI draws a component through render(ctx RenderContext) only, so a controller whose render methods all take something else (render(ctx DrawContext), render()) is an error too: none of them would ever run. A value typed
component holds any component, and a call such as c.render(ctx) runs the
method of the component it holds. c is Scroller tests whether that component's
controller implements an interface and adds the interface's methods in the branch;
c is ScrollView narrows to the component itself.
A view names its controller's fields, properties and methods directly in the arguments of the
components it places (Text(text = title)). The view runs again in the next frame
once its component changed: one of its fields (an input, a state or any other) took another
value, it called redraw(), a component it names with ref changed, or
the window, the theme or a state such as hover did. An argument whose value
differs from what was passed the last time is then assigned to the child's input, which
changes the child in turn; so is one that is, or comes from, a @state or
@input field changed in place since the last run, though it is the same object. What a view reads through any other reference (a model object
changed in place, a service, a global) shows once the component, or one above it, calls
redraw(). An if, match or when in a view is
evaluated again too, so components enter and leave with their branch, and a loop is iterated
again, keeping its children by position. A view body must therefore be cheap and without
effects. Literals and handlers (onClick = f) are bound when the component is built; a handler names a function, a method of the controller, or is a function literal closing over the controller and the view's locals (each child of a loop gets the values of its own pass). Optional and tuple arguments are compared member by member. new Name(...) on a component is the call Name(...): the view is mounted.
c.style of a value typed component is typed style, the kind
of every style, with the members every style has (variant, geom).
is with a datatype tests whether the style has the datatype's fields and narrows it
in that branch, where the fields are read and written on the style itself:
datatype Sized {
width, height Sizing
visible bool
}
def hide(c component) {
let look = c.style
if look is Sized {
look.visible = false
}
}
look is Card, with a style declaration, tests that the value is that style
or a style that embeds it, and narrows it to a view of Card: its properties
are read and written on the style itself. style.clone() returns a copy of a style with the same
property values, which no component has as its style.
Enter and exit
A style body may assign enter and exit: the look a component
appears from and the look it leaves to. Each lists properties of the style; a nested
literal changes the fields it names of the value the property holds. A component whose
style has Motion travels between that look and its own over its
transition (style.transition = Transition{duration: 150ms}
in the controller).
style Fade {
Box
Compositing
Transforms
Motion
enter = {opacity: 0.0, transform: {translate: {y: 8(px)}}}
exit = {opacity: 0.0}
}
style.enterandstyle.exitare functions,?def(look style, owner component), none in a style that writes none: calling one assigns the listed properties on the style given (aclone(), when the component's own look must stay) for the component given.- A value may name what the module sees and the style's own
lets. Embedding a style does not bring itsenterandexit. A style body may also assignon, the looks of states:on = {hover: {opacity: 0.8}, [hover, selected]: {...}, disabled: {opacity: 0.4}}. A state ishover,pressed,focused,focusWithin,focusVisible,disabled,draggingordropTarget, a@statebool field of the component, or a case of one of its@stateenum fields; a list of states and anoninside an entry hold when all of them do. Entries that hold are applied in the order written to a copy of the style the component is drawn with; a name that is no state of the component is an error. A component'sstyleis its resolved style: a property read through it (style.opacity,$ref.opacity) comes from that copy while a state holds, and an assignment is to the style itself. A style that embeds styles with anonhas their entries, applied before its own. A style embedded twice, or reached through two styles that both embed it, is one copy of its properties; two different declarations of one property name are an error. - A child a view no longer places, whose style has an
exit, leaveschildrenfor its parent'sleavingand stays drawn until the exit has played;parent.finishLeaving(child)lets go of it (the UI runtime calls it). Itsrefmember reads none meanwhile. - A view that reaches the call again before that (the branch taken again, the same
keyin a loop, the same position in a loop without keys) places the same component, which travels back. - A component's own field may not have the name of a field every component has
(
children,leaving,enabled).
Attributes & Modifiers
Attributes (@name(...), or just @name when no arguments are
passed) attach metadata to a declaration. Modifiers
(internal, partial, ...) change how a declaration behaves. Both have
appeared in earlier sections; this one is the reference for each.
Every attribute the compiler and the standard library ship is catalogued on the
Annotations page.
Attributes
An attribute's arguments can be positional or named, same as a regular call, and every attribute is preserved in compiled metadata, where tooling and runtime reflection can read it:
@env("APP_REGION", defaultValue = "eu-west")
internal const region string
meta def is how an attribute like this gets defined in the first place - it's
what @trim from Params & Returns actually is:
meta def trim<string>(target reflection.ParameterMetaContext) {
let v = string(target.get())
target.set(v.trim())
}
The context parameter's type is what limits where an attribute can be used - one that takes
a ParameterMetaContext only makes sense on a parameter, one built around a
service-specific context only makes sense on a service. Overload the same
meta def with a different context type to let it target more than one kind of
declaration.
A handful of attributes are built into the compiler itself. @cfg(...)
conditionally compiles a declaration; an excluded copy is not even required to parse:
@os("linux", "darwin")
const SEPARATOR = "/"
@os("windows")
const SEPARATOR = "\\"
@os, @arch, @build, @feature, and
@version are sugar over the full @cfg(os=, arch=, build=, features=, version=)
form; @os and @arch take several values, any one of which keeps the
declaration. Keys within one @cfg all have to match (AND); stacking more than one
@cfg on the same declaration means any one of them matching is enough (OR).
os/arch come from ADM_OS/ADM_ARCH (or the
host by default); build/features come from adm build
flags like --release --features simd,fastmath.
A file can say the same with its name: platform_linux.adm belongs to Linux
alone, as if every declaration in it carried @os("linux"). The names are the
targets of adm build --os (linux, darwin,
windows, android, ios, wasm), tests
included (keys_wasm_test.adm). A build for another system never reads such a
file, so web code costs a Linux build nothing.
@embed(path) is the other built-in: it bakes the file at path
(relative to the source file) into a byte[] constant at compile time;
@embed(path, gzip = true) stores it gzip-compressed, to inflate with
std.data.compress.gzip.Reader.
Two more fill in a declaration's value from the outside world. @env(name, defaultValue)
reads an environment variable, and @flag, from std.cli, fills a variable from the command line when the
program first uses it; the variable's type says what the flag takes, its name is the flag's name, its
declared value the default and its doc comment the help text --help prints:
module demo.globals {
use (
std.cli::(flag)
std.os
)
@env("ADM_TEST", "fallback")
const tmp string
// Enable debug mode.
@flag
let debug bool = false // --debug
// Port to listen on.
@flag(short = "p")
let port int = 8080 // --port 9000, -p 9000
// How long a request may take.
@flag
let wait duration = 30s // --wait 1m30s
// File read instead of standard input.
@flag
let input ?os.File // --input ~/notes.txt
}
Modifiers
Every modifier in the language, and the section that covers it:
| Modifier | Applies to | Meaning |
|---|---|---|
internal | modules, members, services, unions, enums | Limits visibility to the current package. |
partial | application/library/plugin/module/type/service/component | Splits a declaration across multiple files in the same directory. See Program Structure. |
atomic | fields, functions, properties | Locks per-instance around the call. See Params & Returns. |
weak | fields | A non-owning optional reference (weak parent ?Node): never keeps its target alive, reads none once the target is gone. For back references, so parent and child form no cycle. See Memory โ Weak references. |
async | functions, types | Returns a future instead of running immediately. See Async / Await / Futures. |
cuda, sql, meta | functions | Body is a foreign DSL, or (for meta) defines a new attribute. See Params & Returns. |
infix | methods | Operator-style invocation (a + b). Only on type methods - see Types โ Methods. |
Interoperability
ADM calls into any language that exposes a C-compatible ABI directly, builds Zig, Rust and
Go sources into the program, and reaches JavaScript, Lua, Python, Java, .NET and WebAssembly
through libraries. Only a module body
can host a foreign declaration - a function signature with no block body, ending in
;:
module ffi.math {
// a shared library
@native("libc.so.6")
def getpid() int32;
// a C header
@c("math.h")
def cbrt(x float64) float64;
// a C symbol under another name
@c("math.h", symbol = "fabs")
def absolute(x float64) float64;
}
A few rules: only module bodies can have a body-less
def - applications, types, and services always need a real one. They can't be
sql, cuda, meta, or infix,
and every parameter/return type of a C declaration has to be representable in C - no channels,
optionals, errorables, or ADM-only composites. atomic/async still work
as wrappers around the call even though the body itself is foreign.
For binding a whole library at once instead of one function at a time, annotate the module
itself - this is the same @link("GL") mechanism Modules & Namespaces
already showed for the OpenGL bindings:
@c("libm.so")
module Math {
def sin(x float64) float64;
def cos(x float64) float64;
}
Every def inside becomes an implicit FFI declaration, without repeating the annotation on each one.
Zig, Rust and Go sources
A module can name a source file of another compiled language. The build compiles it with
that language's own compiler and links it into the program, so there is no library to build
by hand and no @link line:
@zig("numbers.zig", prefix = "numbers_")
module numbers {
def hash(data byte[], n uint) uint;
def primes(out uint32[], capacity uint) uint;
// Zig calls the ADM function it is given.
def collatz(start uint, visit def(uint)) int;
}
@zig("file.zig")builds a Zig file: each declaration is one of itsexport fns.@rust("file.rs")builds a Rust file, and@rust("folder")a crate (the folder withCargo.toml) through cargo: each declaration is a#[no_mangle] pub extern "C" fn. A single file takes no crate attributes (#![...]).@go("folder")builds a Go package: each declaration is a function the package marks//export. The package is an ordinary one inside a Go module, notpackage main.- The path is relative to the source file.
prefixis what the exported names start with (hashabove isnumbers_hash); the same annotation on a function withsymbol = "..."names an export that does not follow it. - Types cross as they do for
@c: numbers,bool, strings as C strings, arrays of numbers as a pointer to their elements, structs and unions by value, functions as function pointers. - The build needs the language's compiler on
PATH(or named byADM_ZIG,ADM_RUSTC,ADM_CARGO,ADM_GO) and downloads nothing. A panic in Rust or Go ends the program. - After a Zig or Rust source is built, each declaration is compared with the function it exports: a parameter or result of another width, sign or kind, another number of parameters, or a function the source does not export stops the build with both sides named. Go declarations are checked for existing only.
Scripting languages
A scripting language comes as a library that embeds its engine: the engine's C sources ship
with the library and are compiled into the program, so nothing else has to be installed.
Add one to a project with adm get admlang:adm.interop.js (or
adm.interop.lua, adm.interop.python). Write the script as an ordinary module of its language, then
declare its functions in ADM without a body, under the library's annotation:
export function total(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
module billing {
use adm.interop.js::*
struct Item {
price float
quantity int
}
@js("billing.js")
def total(items Item[]) !float;
}
let due = try billing.total(cart)
- The file is named relative to the source file and is built into the program; nothing is read at run time, and changing the script rebuilds the module. Each declaration is the script's function of the same name.
- Arguments and results convert by their declared types: numbers, bools, strings,
byte[], arrays, structs (field by field, nested ones included),map<string, V>and optionals. A number that does not fit the declared type fails the call. - A script that throws fails an errorable declaration (
!T) with aScriptError; a declaration that cannot fail panics instead. - The library's
Runtimetype is the engine itself, for scripts that arrive at run time:eval, values, calls in both directions, and limits on memory and time for code the program does not trust.
Lua works the same way, with a file that returns a table of functions:
module shop {
use adm.interop.lua::*
struct Customer {
name string
years int
orders int
}
@lua("rules.lua")
def discount(customer Customer) !float;
}
Python works the same way through adm.interop.python, which embeds
MicroPython: @python("rules.py") binds a declaration to the function of the
same name in the file.
These annotations are ordinary meta defs, so a library can bind any engine the
same way: a body-less declaration under a function meta is answered by the meta, which reads
the arguments, takes the script as a file built into the program and supplies the result.
See Writing your own.
Java and .NET
adm.interop.java and adm.interop.dotnet load the Java VM or the
.NET runtime installed on the machine when the program first uses it; building the program
needs neither. A declaration without a body is a static method of the class or type the
annotation names:
module pricing {
use adm.interop.java::*
@java("Pricing")
def total(prices float[], quantities int32[], customer string) !float;
// Java's null arrives as none.
@java("Pricing")
def coupon(code string) !?string;
}
try java.runtime().start(classPath = ["classes"])
let due = try pricing.total(prices, quantities, "ana@partner.example")
- Numbers, bools, strings and their arrays cross each way, copied.
@dotnet("Acme.Pricing")declares a .NET method the same way; a declarationtotalalso bindsTotal. - An exception fails the call with a
ScriptErrorthat carries the stack trace and the exception itself as a value. runtime()reaches everything else:loada class or type,constructobjects,invokemethods,getandsetfields and properties. An ADM function goes where Java takes an interface and .NET a delegate or an interface.- Running the program needs Java 8 or later (
JAVA_HOME, orjavaonPATH), or .NET 8 or later (DOTNET_ROOT, ordotnetonPATH).
WebAssembly
adm.interop.wasm runs WebAssembly 2.0 modules, the 128-bit vector instructions
included. It is written in ADM, so it adds no foreign code to the program:
use adm.interop.wasm
let loaded = try wasm.decode(bytes)
let store = new wasm.Store()
store.fuel(1000000)
let instance = try store.instantiate(loaded)
let results = try instance.invoke("add", wasm.Value[
wasm.Value{kind: wasm.ValueType.I32, bits: 2},
wasm.Value{kind: wasm.ValueType.I32, bits: 3}
])
decodereads a binary into aModule;validatechecks it without running anything.- A
Storeholds what instances share. Itsfunction,global,tableandmemorymake things a module can import,providenames them. fuellimits what the code may run. A trap, running out of fuel included, is an ADM error of typeTrap.- Typed calls, WASI and the text format are not there yet.
| Layer | Languages |
|---|---|
| Native ABI | C, C++, Rust, Zig, Go, Swift, Fortran, Nim, D, Pascal, Assembly |
| Sources built with the program | Zig (@zig), Rust (@rust), Go (@go) |
| Embedded engines | JavaScript (adm.interop.js, QuickJS-ng), Lua (adm.interop.lua, Lua 5.4), Python (adm.interop.python, MicroPython 1.29), WebAssembly (adm.interop.wasm, written in ADM) |
| Installed runtimes | Java (adm.interop.java, Java 8 or later), .NET (adm.interop.dotnet, .NET 8 or later) |
| Embedded engines planned | CPython, Ruby, PHP, Perl, R, Scheme/Lisp variants, TCL |
| Bridging layers planned | Kotlin, Scala, Clojure, Erlang/Elixir |
Testing
check blocks keep tests next to the code they verify, with no separate test
framework. They're parsed and type-checked with the production code, so a broken test
surfaces immediately, but the compiler drops them from the final binary
unless adm test is what's asking for them.
Unit Tests
A check "name" { ... } block is legal at file scope and inside modules, types,
services, components, and applications - just not inside a function body. If it has no
explicit @test() function, its top-level statements run as one synthetic test:
check "luhnCheck" {
use std.testing
for let (_, t) in string["79927398713", "49927398716"] {
assert luhnCheck(byte[](t))
}
for let (_, t) in string["123", ""] {
assert !luhnCheck(byte[](t))
}
}
Name individual cases with @test() instead, and each one runs - and reports - separately:
check "Path" {
@test()
def TestExtension() {
assert extension("/path/to/file.txt") == "txt"
assert extension("file") == ""
}
@test()
def TestBase() {
assert base("/path/to/file.txt") == "file.txt"
}
}
A suite can also declare use imports, constants, structs, and helper functions -
all scoped to that suite, never exported - and can nest other check blocks to
group related tests. Lifecycle hooks from std.testing are calls in the suite's
body that take a function: beforeAll(handler) runs it once before the suite's
first test, afterAll once after its last, beforeEach and
afterEach around every test, those of nested suites included. A hook that
fails fails the test it ran for. Files named
_test.adm or .test.adm are discovered automatically; run everything
with adm test.
Benchmarks
@benchmark() marks a suite function as a benchmark instead of a test - same
check block, same scoping rules:
check "UUID" {
use std.testing
@benchmark()
def newUUID() {
let uuid = new UUID()
}
}
Run benchmarks with adm test --bench . (the text selects benchmarks by name;
. matches all). @skip() leaves a test out and @only()
runs just the tests carrying it. @timeout(5s) fails a test that runs longer
than the limit; on a check suite it is the time the suite's tests may take
together. All three also go on a suite, and adm test --timeout 30s limits
every test without its own. @fuzz() on a function with parameters makes it a
fuzz target: adm test runs it on the empty input and on the files of
testdata/fuzz/<target>/, and adm test --fuzz . generates
inputs and saves one that fails there.
@parallel() tests of a suite run at the same time, after its other tests.
@golden("testdata/report.txt") compares the string or
byte[] a test returns with that file, beside the test's source;
adm test --update-golden writes it.
Magic Constants
A set of double-underscore constants is filled in by the compiler and folded into the binary at build time. They're usable anywhere an expression is, which makes them handy for logging, diagnostics, and build metadata:
def whereAmI() string {
return "{__file_name__}:{__line__} in {__module__}"
}
| Constant | Description |
|---|---|
__compiler_version__ | The compiler's semantic version string. |
__target__ | Full target triple: CPU-Vendor-OS (e.g. "x86_64-unknown-linux-gnu"). |
__os__ | OS of the active compilation target ("linux", "windows", "darwin", "android", "wasm"). |
__arch__ | Architecture of the active target ("x86_64", "arm64", "wasm32"). |
__build_timestamp__ | ISO-8601 timestamp of when the build ran. |
__app_name__ | The application's name: from adm.toml, else @manifest, else the declared application name. |
__app_id__ | Stable identity derived from the manifest's publisher key and slug; the application name when there is no manifest. |
__app_version__ | The application's version: from adm.toml, else @manifest, else "0.1". Drives the home folder migrations. |
__endianness__ | "little" or "big", per the target architecture. |
__ptr_width__ | Pointer width in bytes for the target. |
__int_width__ | Size of the default integer type, in bytes. |
__word_size__ | Native register width, in bytes. |
__stack_alignment__ | Required stack alignment for the target. |
__abi__ | Active ABI (e.g. "sysv", "msvc"). |
__debug__ | True when compiling in debug mode. |
__release__ | True when compiling in release/optimized mode. |
__in_test__ | True when compiling in test mode. |
__labs__ | True in a build that has service labs: a test build (adm test) and a build the IDE runs. |
__git_commit__ | Git commit hash of the build (empty outside a Git repository). |
__git_branch__ | Git branch name of the build (empty outside a Git repository). |
__git_tag__ | Git tag name of the build (empty outside a Git repository). |
__file_name__ | Basename of the current source file. |
__file_path__ | Absolute path to the current source file. |
__dir__ | Absolute directory path of the current file. |
__owner__ | Qualified name of the type, service, component or application the code stands in (shop.Till); empty in a function of a module. |
__caller__ | The instance the current method runs on, as ?any: none in a function of a module. Inside a @trackCaller function, the instance of the caller. |
__file_hash__ | Compiler-generated hash of the file contents (used for caching). |
__line__ | Current line number, 1-based. |
__module__ | Fully-qualified module of the current file. |
__name__ | Current symbol name - function, type, method, and so on. |
__build_id__ also exists and changes on every build - see
adm.toml Manifest, which covers it alongside the
identity constants a manifest fills in.