Language Reference

This page is the user-oriented reference for the current seed Language Specification. The complete grammar and operational semantic chapters live under compiler/llvm. For current limits and closure claims, see Current Status.

Source encoding and names

.sd files are UTF-8. Comments and string contents may contain Unicode. string owns valid UTF-8 and str is a borrowed read-only UTF-8 pointer-plus-byte-length view. Text length is measured in bytes; Unicode scalar, grapheme, normalization, and locale operations belong in libraries.

Source identifiers intentionally use ASCII:

Kind Rule Example
variable, function, field, module [a-z][a-z0-9_]* my_document
struct, ADT, union, trait, type parameter, variant same lower snake case parse_result
constant [A-Z][A-Z0-9_]* MAX_DOCUMENT_SIZE

The formal language name is seed. Types are not capitalized. _ discards a binding; a leading underscore may mark an intentionally unused private binding.

Line comments start with //. Block comments /* ... */ nest. The clean grammar does not use # comments.

The source loader validates the whole file as UTF-8 before lexing and rejects embedded NUL; decoded string literals are validated as well.

Bindings, constants, and mutation

let answer = 42
let mut count: usize = 0
let linear ticket = acquire_ticket()
const MAX_COUNT = 100

The current compiler also accepts typed scalar expressions, private fixed u32 constant tables such as const TABLE: [4]u32 = [1 as u32, 2 as u32, 3 as u32, 4 as u32], static text, tuples, and structs. Indexed and field reads use immutable static storage with ordinary bounds checks. Public aggregate values retain their recursive representation in schema-14 source-elided builds.

Bindings are immutable unless declared mut. Assignment to a binding, field, or index through a binding requires that binding to be mutable. linear creates an exactly-once obligation.

To discard a return value, bind it to a unique name starting with _: let _unused = expression. Named discards avoid the "duplicate active binding" error that plain let _ = expression produces when used multiple times in the same scope. A leading underscore in any identifier marks the binding as intentionally unused.

Functions, methods, and returns

fn add(left: i64, right: i64) -> i64 {
    left + right
}

fn document.title_size() -> usize {
    len(self.title)
}

unsafe fn read_register(address: *mmio mut i64) -> i64 {
    raw_volatile_read<i64>(address, 0)
}

A block's final expression is its value. ret expression returns early; the clean compiler does not use return. A method declaration includes the nominal type before the dot and receives implicit self; do not write self in its parameter list. fn type_name.drop() is a consuming destructor. @borrow on a method or trait method gives self a shared lexical loan. @mut instead requires a mutable addressable receiver, holds an exclusive loan for the call, and makes mutations to self visible to the caller. The two attributes are mutually exclusive.

Public declarations use pub. Foreign declarations use explicit extern calling convention/link metadata and are unsafe to call until wrapped by an audited safe function. Platform-C argument classification is target-specific: Windows x86-64 passes aggregate parameters larger than eight bytes indirectly, so str, slices, and equivalent C structs interoperate with MinGW/MSVC-style callees.

Core types

Category Types
control void, never, bool
integer i8, i16, i32, i64, u8, u16, u32, u64, isize, usize; byte aliases u8
float f32, f64
text/buffer str, string, bytes, shared_bytes
formatting format_args (ephemeral)
aggregate/view [n]t, []t, mut []t, tuples, nominal structs/ADTs/unions
failure ?t, result<t,e>, alloc_error
callable non-owning fn(t) -> u, escaping box fn(t) -> u
low-level raw pointers plus user, kernel, physical, mmio, and dma address spaces
exact-once linear t

alloc_result<t> abbreviates result<t, alloc_error> without introducing a new type identity, representation, cleanup rule, or ABI.

Hosted main may return result<i64, e>. ok(status) exits with that status; alloc_error uses the fixed allocation diagnostic. Another e must be Copy, have no destructor, and implement @borrow fn e.display() -> str; its view is written once before status 1, after ordinary cleanup.

@static_view extern fn ... is the audited FFI declaration for a returned str/slice whose storage is process/static lifetime. The attribute is rejected on ordinary seed functions and does not make the foreign call safe.

Effect attributes may annotate fn declarations: @no_alloc, @allocates, @blocking, @panics, @io, @interrupt_safe. Qualified callback types use @no_runtime_effects fn(...) -> t to declare a complete empty effect summary. An unsafe struct may declare @unsafe_send to assert the send capability, or @repr("null_niche") to enable null-pointer optimization on a single raw-pointer field for ?wrapper.

Integer add/subtract/multiply wraps deterministically at the type width. Division by zero, signed minimum divided by -1, invalid shifts, invalid safe casts, and failed safe indexing trap. Release profiles preserve these rules. Explicit integer widening uses source signedness; narrowing keeps low bits and equal-width signedness changes preserve bits. Integer/float and float-width casts follow IEEE-754 conversion. Float-to-integer requires unsafe and a finite destination-representable input.

Byte literals use b'A' and have type u8. One raw ASCII byte or one \\n/\\r/\\t/\\0/\\\\/\\'/\\xNN escape is accepted. Use b'\\xFF' for a non-ASCII byte; seed deliberately has no locale-dependent character literal type.

Structs, ADTs, and unions

struct document {
    title: string
}

type parse_result {
    parsed(document)
    invalid(str)
}

union scalar_bits {
    integer: u64
    decimal: f64
}

A struct literal is document.{ title: owned_title }. An ADT constructor is qualified, for example parse_result.parsed(value), and its payload is inspected through exhaustive match. A payload-free ADT replaces the legacy enum keyword:

type color { red green blue }

Raw union construction/access is unsafe. Safe tagged alternatives use ADTs.

Options, results, and propagation

fn find(enabled: bool) -> ?i64 {
    if enabled { some(42) } else { none }
}

fn allocate_name(view: str) -> result<string, alloc_error> {
    view.to_string()
}

some(value) and expected-type none construct an option. result.ok(value) and result.error(problem) construct a result. ? consumes the container, yields its success/present payload, or returns the corresponding failure from the current function. Result propagation may change the success type but must preserve the error type. It performs no implicit allocation or clone.

is_some/is_none and is_ok/is_error inspect by shared borrow. unwrap_or(fallback) consumes the container and cleans the unselected owned value exactly once. or_abort(), expect(message), and main-only or_exit(status) are explicit application boundaries; they never hide inside ordinary ? propagation.

? cannot cross a spawn boundary while the outlined task ABI returns void.

Control flow and patterns

The implemented control forms are expression-oriented if/elif/else, while, for, exhaustive match, blocks, break, continue, and ret. elif is the required spelling; else if is not accepted. Match arms have no commas. Patterns cover bindings/discards, literals, tuples, structs, and qualified ADT variants; guards preserve a move-only scrutinee until the selected arm actually transfers it.

for over ranges, fixed arrays, and slices lowers to an allocation-free index/length loop. for index, value in enumerate(items) adds a usize index without a heap iterator. Move-only elements require explicit indexing/ownership handling.

Text, views, and conversions

Typed phase-1/2 interpolation is allocation-free unless explicitly materialized:

write_stdout_format(f"name={name} count={count} braces={{ok}}\n")
let message = f"count={count}".to_string()?

Expressions evaluate once from left to right. Supported values are str, borrowed string, bool, and builtin integers. The ephemeral format_args result must be passed directly or consumed by .to_string()?; it cannot be assigned with let, returned, captured, or stored. Integer/text/bool width, fill, alignment and integer base specifications are implemented. Custom types use trait display { @borrow fn display() -> str } with direct static dispatch. Floats support default general output plus f/F, e/E, and g/G with precision through 18. Custom :? formatting uses the analogous borrowed debug.debug() -> str trait.

Generics and traits

Generic parameters use lower snake case. Definitions are checked against their bounds, then concrete reachable instances are monomorphized. Trait dispatch is static and direct: there are no implicit vtables, boxing, allocation, or clone. Impl completeness, signatures, coherence, and orphan rules are checked. Public generic templates, common instances, ephemeral format_args parameters, null-niche nominal representation, exclusive mutable receivers, qualified callbacks, and boxed callable signatures persist through current schema-16 .sdi. Materialized method instances are exported only by an interface that also owns the generic method definition. A wrapper around an imported generic type therefore records its public contract without an orphan specialization; a source-elided consumer obtains the method template from the wrapper's dependency interface and materializes the concrete instance there.

inline fn requests bounded inlining. A pub inline fn publishes its verified body and deterministic body hash in schema-13 .sdi while retaining an exported fallback symbol. Inline declarations have a 4096-byte source budget; a body change invalidates dependent artifacts. Mutable slice partitions use view.split_at_mut(index) and tuple patterns may declare mutable bindings as (mut left, mut right). The split checks index <= len, allocates nothing, and keeps the common owner exclusively loaned until both siblings die.

Closures and callbacks

fn apply(values: [3]i64, callback: fn(i64) -> i64) -> i64 {
    let mut total = 0
    for value in values {
        total = total + callback(value)
    }
    total
}

let base = 40
let total = apply([1, 2, 3], |value: i64| base + value)
let retained = (box |value: i64| base + value).or_abort()

|...| and prefix-position || create anonymous callables. Parameter types may be omitted when an expected fn(...) -> t supplies them. A plain closure uses stack capture storage, allocates nothing, and cannot escape its lexical owner. Copy-capable captures copy; owned captures move; captures are immutable. Borrowed, mutable, region-bound, already-moved, and other unsafe-to-store captures are rejected.

box |...| is the explicit escaping form and returns result<box fn(...) -> t, alloc_error>. The owner may be stored or returned, but is move-only, non-cloneable, and non-sendable. Drop destroys its captures exactly once and frees its allocation. Region allocation remains lexical. There is no implicit conversion between stack and boxed closures.

Seed fn(...) -> t callbacks use a one-pointer code/environment descriptor. C/platform function pointers stay thin and accept named functions only. Closure effects are inferred from the body and checked against qualified callback contracts such as @no_runtime_effects fn() -> void. Recursive closures are unsupported.

Ownership summary

See Ownership for the operational model.

Regions, tasks, and libraries

region(capacity) { body } installs a lexical checked bump allocator. Values, views, or pointers tied to the region cannot escape. Structured tasks captured inside a region join before its backing storage is released.

spawn { body } is structured: every edge leaving the owning lexical scope waits for its task group before cleanup. Capture is copy or move by capability. Channels, typed joins, events, cancellation, and synchronization are provided by the ordinary compiler/llvm/libs/task package. The clean grammar does not have built-in chan<t>, send, recv, or detached spawn.

Unsafe substrate

raw_try_alloc<t>, raw_null<t>, raw_is_null<t>, raw_read<t>, raw_write<t>, raw_free<t>, raw_slice<t>, atomics, volatile access, fences, address-space casts, raw assembly, FFI, syscalls, and raw unions require unsafe. Normal applications should consume safe wrappers from libraries.

Repository launcher

./seed/seed check [--lib] [path|file.sd]
./seed/seed build [path] [-o output] [compiler flags]
./seed/seed run [path] [-- program arguments]
./seed/seed test [path] [compiler flags]
./seed/seed format [--check] [file.sd]
./seed/seed new path [--name package_name]
./seed/seed pkg resolve|add|update|prune ...
./seed/seed install [path|package] [--profile release] [--system] [--prefix path] [--dry-run]
./seed/seed uninstall package [--system] [--prefix path] [--dry-run]
./seed/seed doc [path] [--output file]
./seed/seed lint [--json] [path]
./seed/seed lsp
./seed/seed publish --check [path]
./seed/seed mobile android <setup|build|stage|check|package|run|doctor>

check is a narrow direct source check. It resolves relative source/interface imports and local manifest path dependencies; it does not first compile a missing bare repository package. Use build or run when validating package integration that depends on automatic core-library staging.

build performs incremental compilation with automatic dependency resolution. Bare package imports (e.g. use "vec") are staged into an isolated target/profile store. A second build reuses matching .sdi/native artifacts. test forwards trailing compiler flags, including repeated --link-object and --link-library inputs, to every test binary. This is required for library tests whose safe Seed surface is backed by an external native ABI.

install compiles the package in release mode and installs it system-wide: executables to ~/.local/bin (or /usr/local/bin with --system), dependency generations to ~/.local/lib/seed/generations/, and interfaces to ~/.local/share/seed/interfaces/. Use --profile debug for debug builds and --prefix /custom/path for custom install locations.

uninstall removes a previously installed package and its dependency generations. Both install and uninstall support --dry-run for preview. The transactional installed-package record includes target, profile, dependency-generation identity, compiler checksum, installed-artifact checksum, source checksum and lock checksum so a deployed application can be traced to its exact build inputs.

Remote publication is separate; seed publish --check validates package and lock integrity locally without running unrestricted scripts. Compiled packages use target-qualified .sdi plus --install-library/--install-interface. Local monorepos can keep multiple independently versioned packages connected by relative path dependencies. Their tree checksums drive rebuilds; ordinary local development does not require a Git repository, tag, or publication for each library.

The launcher reads [build].profile = "debug" | "release" | "release-small"; legacy release = true maps to release. An explicit CLI profile overrides the manifest. seed install defaults to release regardless of the manifest unless --profile is explicit. Applications default to src/main.sd; an optional safe relative [package].entry selects another source within the package. Bare package imports resolve to matching installed artifacts first; otherwise they are compiled with the application profile into an isolated target/profile store. Hosted builds link them dynamically by default. A second build reuses matching .sdi/native artifacts; private dynamic-library changes do not rebuild consumers, while public .sdi or ABI changes do.

User installation places executables in ~/.local/bin, immutable dependency generations in ~/.local/lib/seed, and versioned interface mirrors under ~/.local/share/seed/interfaces. --system uses the platform system scope (/usr/local on Unix); --prefix overrides the root. Installation and removal are ownership-recorded and atomic, and no command silently edits shell startup files or loader environment variables.

Operators

Binary precedence from highest to lowest:

  1. multiplicative (*, /, %)
  2. additive (+, -)
  3. shifts (<<, >>)
  4. comparisons (<, <=, >, >=)
  5. equality (==, !=)
  6. bitwise AND (&), XOR (^), OR (|)
  7. logical AND (&&)
  8. logical OR (||)

&& and || short-circuit: a && b evaluates b only when a is true, and a || b evaluates b only when a is false. Both operands must be bool and the result is bool. Short-circuiting also applies to constant folding, so false && expensive() and true || expensive() are valid constant expressions even when expensive() is not constant.

Direct clean compiler

The executable is compiler/llvm/build/seed. Its authoritative flag list is seed --help.

# Source processing
seed --check source.sd
seed --format|--format-check|--format-write source.sd

# Code generation
seed --emit-llvm out.ll source.sd
seed --emit-object out.o source.sd
seed --emit-binary out source.sd
seed --emit-shared library.so source.sd
seed --emit-static-library library.a source.sd
seed --emit-raw out.bin source.sd
seed --run source.sd -- arguments

# Interface inspection
seed --interface-print file.sdi
seed --interface-hash file.sdi
seed --emit-interface out.sdi source.sd

# Library management
seed --install-library artifact --install-interface file.sdi --package name [--seed-home path]

# Debug dumps
seed --lex source.sd
seed --ast-print source.sd
seed --types-print source.sd
seed --symbols-print source.sd
seed --hir-print source.sd
seed --safety-print source.sd
seed --mir-print source.sd
seed --layout-print source.sd
seed --abi-print source.sd
seed --low-mir-print source.sd
seed --llvm-print source.sd

# Build options
seed --release source.sd
seed --release-small source.sd
seed --thin-lto source.sd
seed --pgo-generate dir source.sd
seed --pgo-use profile source.sd
seed --profile-print source.sd
seed --target target source.sd
seed --runtime linux-native|linux-libc source.sd
seed --runtime-profile profile source.sd
seed --runtime-feature feature source.sd
seed --static source.sd
seed --static=all source.sd
seed --sysroot path source.sd
seed --soname name source.sd
seed --entry symbol source.sd
seed --linker-script path source.sd
seed --code-model model source.sd
seed --relocation-model model source.sd
seed --no-red-zone source.sd
seed --link-object file.o source.sd
seed --link-library lib.so|lib.a|package source.sd
seed --cache-dir path source.sd
seed --seed-home path source.sd
seed --package name source.sd

# Diagnostics
seed --diagnostics human|json source.sd
seed --version
seed --help

Debug/inspection modes include token, AST, types, symbols, HIR, safety, MIR, layout, ABI, low-MIR, and LLVM printing. Target/runtime options include --target, --runtime-profile, --runtime-feature, --runtime, --sysroot, entry/linker-script controls, and separate object/library linkage.

--release uses seed-safe-o2-v1; --release-small uses seed-safe-oz-v1. Both are safe. ThinLTO and PGO are explicit release-only options; static linkage is independent. There is no operational --unchecked or --fast-unsafe profile.

Target status

Object support and end-to-end hosted support are separate claims. Linux x86-64/AArch64 are the closed hosted paths. Freestanding x86-64/AArch64/RISC-V64 have boot evidence at their declared levels. Windows x86-64 has PE/runtime/DLL/ task/network evidence under required Wine. macOS x86-64 and arm64 have explicit-SDK executable/dylib/static linking, runtime and pthread scheduling. Native Intel x86-64 execution remains; arm64 is cross-link-only. See compiler/llvm/tests/GATE14_TARGET_MATRIX.tsv.

On an Intel Mac, run SEED_MACOS_RUNNER_POLICY=required make -C seed/compiler/llvm gate14-macos-x86_64-test. The script discovers the SDK with xcrun and uses Apple's ld unless explicitly overridden.