Syntax
Newlines terminate statements. Blocks are expressions; the last expression is the value.
Bindings
let name = "lowlang"
var count = 0
count += 1
letis immutable,varis mutable.- Types are inferred and may be written as
let x: i64 = 1.
Functions
fn add(a: i64, b: i64) -> i64 {
a + b
}
fn counter() -> fn() -> i64 {
var n = 0
fn() -> i64 {
n += 1
n
}
}
let twice = fn(x: i64) -> i64 { x * 2 }
let ops: [str: fn(i64) -> i64] = ["twice": twice, "negate": negate]
let parse: fn(str) -> i64 ! ParseError = parse_int
let square: fn(i64) -> i64 = fn(x) { x * x }
fn first(s: str) -> u8 {
s[0]
}
"simon".first()
items.push(4)
let shouted = "hi"
|> text.repeat(3)
|> first
let n = input |> parse()?
- A function type is written
fn(i64, mut str) -> bool, with! Efor a result that can fail. Two function types are the same when their parameters,mutmarkers and results are. - A named function is a value of its function type.
fn(params) -> R { body }is a closure. Parameter and result types may be omitted when the expected function type supplies them; a closure with no written result and no expected type returns nothing. - Values of function type can be stored in variables, fields, slices and maps, passed and returned, and called like functions. They cannot be compared or printed.
- A closure captures the variables it names by reference, as Go does: it reads and assigns the variable itself, which lives as long as any closure holding it.
- Each loop iteration binds fresh variables, so closures made in different iterations see different ones.
- A
mutparameter cannot be captured. - A variable that is never assigned after its binding, never passed as
mutand never modified bypush,pop,truncateordeletecannot be told apart from a copy, so the closure holds its value directly.
x.f(args)callsf(x, args), so calls chain left to right:"ab".repeat(3).first().fis any function callable by its plain name: a function in the current module, a function value in scope, a generic function or a builtin.- When no such name exists and
xis a struct or enum, or a pointer to one, from another module,fis that module's public function:url.authority()callshttp.authority(url). - A struct field of function type named
ftakes precedence, soop.apply(v)calls the stored function. - When the first parameter is
mut,xis passed asmutand must be a mutable place.
x |> f(args)callsf(x, args), andx |> fcallsf(x).- The right side is a call, a function name, a module function such as
text.repeat, a closure, or a struct field holding a function; any other expression is an error.x |> ops.scalecalls the stored function, never a function namedscale. |>has the lowest precedence of the binary operators and groups left to right:a + b |> f()isf(a + b).- A trailing
?applies to the call:x |> parse()?isparse(x)?. - A
mutfirst parameter receivesxasmut, as withx.f(args).
- The right side is a call, a function name, a module function such as
- A line that starts with
|>or.continues the expression on the line before it.
Control flow
if n > 0 { print(n) } else { print(0) }
for i in 0..10 { print(i) }
for x in xs { print(x) }
for i, x in xs { print(i) }
for n > 0 { n -= 1 }
for { break }
rows: for row, cells in grid {
for col, cell in cells {
if cell == want { break rows }
}
}
for i in a..bcounts fromaup to but not includingb; the bounds are integers of one type.for x in xsvisits each element of a slice;for i, x in xsalso binds the index asi64.- Loop variables are immutable.
break,continue,returnandfailare expressions of type never, so they can stand wherever a value is expected: after??, in an arm, or as a branch ofif.name:beforeforlabels a loop.break nameandcontinue nameact on that loop from inside nested loops and run the deferred statements and drops of every block they leave. A label must be used, cannot repeat the label of an enclosing loop, and is not visible inside closures.
Numbers
let mask: u32 = 0xFF00
var flags: u8 = 0b0000_0101
let mode = 0o640
flags |= 1 << 4
let shifted = mask >> 8
let ratio: f32 = 0.5
let length = sqrt(3.0 * 3.0 + 4.0 * 4.0)
let whole = i64(floor(length))
let byte = u8(300)
- Integer types are
i64,i32,u64,u32,u16andu8; float types aref64andf32. - Integer literals are decimal, or hexadecimal, binary and octal with
0x,0band0o;_may separate digits. Float literals may have an exponent. - An integer literal takes the type the context expects, including the other operand of a binary operator, and must fit it; otherwise it is
i64. A literal above thei64range is accepted only as au64. In anifexpression with no expected type, an integer literal branch takes the type of the other branch. - A float literal is
f64unless anf32is expected. - Integer arithmetic wraps at the type's width. Division by zero panics.
- Operands of a binary operator have the same type. Conversions are explicit calls such as
u8(x),f64(n)andi64(f): integer conversions truncate or extend, float to integer truncates toward zero. &,|,^,~,<<and>>work on integers.>>is arithmetic on signed types and logical on unsigned ones. The shift count may be any integer type; a count of 64 or more, or a negative one, gives 0, or the sign fill for>>on a negative signed value.- Binary operators bind as in Go, from tightest to loosest:
*,/,%,<<,>>,&; then+,-,|,^; then??; then comparisons; then&&; then||. Soa - b >> 1isa - (b >> 1)andx | y - zis(x | y) - z. %is integer only. Floats follow IEEE 754: division by zero gives an infinity, and comparisons with NaN are false except!=.sqrt,floorandexptake and returnf32orf64.printaccepts any integer type.
Vectors
import std/simd
fn find(data: []u8, c: u8) -> i64 {
let needle: u8x32 = simd.splat(c)
var i = 0
for i + 32 <= data.len {
let chunk: u8x32 = simd.read(data, i)
let hits = simd.mask(chunk == needle)
if hits != 0 {
return i + simd.trailing_zeros(hits)
}
i += 32
}
-1
}
- Vector types are 128 or 256 bits of one lane type:
u8x16,u8x32,u16x8,u16x16,i32x4,i32x8,u32x4,u32x8,i64x2,i64x4,u64x2,u64x4,f32x4,f32x8,f64x2andf64x4. +,-,*,&,|,^,~and unary-work lane by lane./works on float lanes.<<and>>work on integer lanes by a scalar count, arithmetic on signed lanes. Compound assignments work the same way.- Comparisons work lane by lane and give a vector of the same type whose lanes are all ones or all zeros; unsigned lanes compare unsigned.
- A scalar operand of the lane type is broadcast, so
chunk == '"'compares every lane. u8x32(v)reinterprets a vector of the same width.std/simdprovidesreadandwritebetween a vector and a slice at an element index,read_str,splat,mask(one bit per lane from its top bit),any,blend,min,max,lane,with_lane,shuffleandshuffle16(a byte table lookup within each 16-byte half, zero where the index has its top bit set),trailing_zeros,count_onesandfind_byte. Reads and writes are bounds-checked.- Vectors are values: they can be passed, returned, stored in fields and slices, and compared lane by lane. They cannot be compared with
==as a whole or printed.
Slices
let xs = [3, 1, 4]
var squares: []i64 = []
push(mut squares, 9)
let middle = xs[1..3]
xs[0] = 10
print(xs.len)
[]Tis a pointer, a length and a capacity over garbage-collected memory. Assigning or passing a slice shares its elements.[a, b]builds a slice; an empty[]needs an expected type.xs[i]reads or writes an element and panics wheniis outside0..xs.len. Any integer type indexes.xs.lenis the length asi64.xs[a..b],xs[..b]andxs[a..]share the elements in that range. The result's capacity equals its length, sopushonto it always copies and never overwrites the parent.push(mut xs, v)appends, growing the capacity to twice plus four when full.pop(mut xs)removes the last element and returns it asT?, ornonewhenxsis empty.truncate(mut xs, n)sets the length tonand panics unless0 <= n <= xs.len.popandtruncatekeep the capacity, so a laterpushreuses the freed space.- They change only the binding they are given. A slice that shares its elements with another binding sees a later
pushoverwrite the freed elements, as after anypushthat finds spare capacity. A reslice such asxs = xs[..n]is the form that copies on the nextpush. - Elements past the length stay in the array, and keep what they point to alive, until a
pushoverwrites them.
- They change only the binding they are given. A slice that shares its elements with another binding sees a later
- A function in the program named
poportruncatetakes precedence over the builtin. - A slice literal or map literal whose elements are all constants is built once per program, at its first evaluation, and reused.
- A literal is used in place when it is bound with
letand the binding is only indexed, iterated or asked for.len, or when it is indexed, iterated or measured directly. - Any other use gets a copy, because
push, a slice of it, an argument, a return value, a field or an assignment through it would otherwise mutate the shared value. - A slice literal under 1024 bytes and a map literal under 4 entries are rebuilt for those uses, since copying them costs more than building them.
- A literal is used in place when it is bound with
- Elements of
u8,u16,u32,i32andf32are packed at their natural width; other elements take a word each.
Maps
let ages = ["ada": 36, "alan": 41]
ages["grace"] = 85
print(ages["bob"] ?? -1)
var counts: [str: i64] = [:]
counts["a"] += 1
delete(mut counts, "a")
for name, age in ages { print("{name} {age}") }
for name in ages { print(name) }
[K: V]maps keys of an integer type,bool,stror a pointer type to values of any type.- A map is a pointer to a table on the garbage-collected heap, so assigning or passing it shares the table, and
m[k] = vworks through any binding. [k: v, ...]builds a map;[:]is an empty one and needs an expected type.m[k]returnsV?.m[k] = vinserts or replaces. A compound assignment such asm[k] += 1starts from a zero value whenkis missing.delete(mut m, k)removes a key; removing a missing key does nothing.m.lenis the number of entries.for k, v in mandfor k in mvisit entries in insertion order. An overwritten value keeps its key's position; a deleted and re-inserted key moves to the end. Inserting new keys while iterating may skip or repeat entries.- A map literal of constants is built once per program in a release build. A use that only indexes, iterates or measures it shares the built table; any other use gets a copy from the prelude's
__map_clone, which copies the entry array and the index. A literal of fewer than 4 entries is rebuilt for those uses, because the copy costs as much as the build. - A map is an array of entries in insertion order plus an open-addressed index of one word per slot.
- A slot is 0 when empty, -1 when its entry was deleted, and otherwise holds the upper 32 bits of the key's 63-bit hash above the entry number plus one. A probe skips empty slots and slots of other keys without reading the entries.
- Iteration reads the entries, never the index, so the hash function never shows in the order. No program output depends on a hash value.
- The map code reads the low bits of the hash as the position and the upper 32 bits as the tag.
- Hashes are seeded per process, so no key set can be built to collide without the seed. Start-up fills
rt_hash_seedwith 128 random bits and forces the second word odd.- On Linux the bits are the 16 bytes at
AT_RANDOM, orgetrandomwhen the auxiliary vector has none, or the fallback when that fails. On Windows they come fromProcessPrng, with the same fallback. - The fallback derives each word through its own SplitMix64 finalizer:
s0from one RDTSC read, the stack address and one RDRAND word, ands1from a second RDTSC read taken after acpuid, a rotated stack address and a second RDRAND word. RDRAND contributes only when CPUID reports it and the instruction succeeds within 10 tries.
- On Linux the bits are the 16 bytes at
- Hash function, with
s0ands1the two seed words andmum(a, b)the high 64 bits of the 128-bit product ofaandbxor its low 64 bits:- An integer,
boolor pointer keykhashes tofold(mum(k ^ s0, s1)). - A
strof 16 bytes or fewer reads two wordsaandb. 8 to 16 bytes give the first and last eight. 4 to 7 give the first and last four packed intoawithbzero. 1 to 3 give the first, middle and last byte. The empty string gives zero. - Every
strstarts fromacc = mum(s1 ^ length, s0 | 1), so the length reaches the hash only through a product with seed words. - A longer string folds every 16-byte block while more than 16 bytes remain, computing
acc = mum(w0 ^ s0, w1 ^ acc), and then reads its last 16 bytes as the two words. - A
strhashes tofold(mum(a ^ s0, b ^ acc)).fold(h)isx = h ^ h >> 32followed by(x ^ x >> 16) >> 1, which keeps 63 bits.
- An integer,
- Two strings of different lengths whose input words differ by a chosen xor do not collide under every seed, because cancelling the length term needs
mum(s1 ^ n, s0 | 1) ^ mum(s1 ^ n', s0 | 1), which depends on the seed. Collisions between distinct strings remain possible with probability about 2^-63 per pair, and the hash gives no protection onces0ands1are known.
Strings
let name = "Ada"
let line = "{name} is {age}, ratio {score / 2.0}"
let full = name + " Lovelace"
for c in "héllo" { print(c) }
let first: u8 = name[0]
let copy = str(bytes(name))
stris an immutable pointer and length over UTF-8 bytes.s.lenis the length in bytes,s[i]reads a byte asu8, ands[a..b]shares a byte range.for c in svisits code points asu32;for i, c in salso binds each code point's byte offset. Invalid UTF-8 yields U+FFFD for one byte."{expr}"interpolates a number,boolorstr;{{and}}are literal braces.a + bconcatenates. A chain such asa + b + c + d, with any interpolations inside it, is one operation that allocates the result once.- Floats are formatted as the shortest decimal that reads back to the same value: positional for exponents from -6 to 20, otherwise
1.5e-7or1e+21. Infinities print asinfand-inf, NaN asNaN. 'a'is a code point of typeu32, oru8where one is expected.bytes(s)copies a string into a[]u8;str(b)copies one back.==and!=compare contents.<,<=,>and>=compare bytes in order, which for UTF-8 is code point order.- Equality checks the lengths, then the pointers, then compares eight bytes at a time with an overlapping final load.
Structs
struct Point {
x: f64
y: f64
}
fn len(p: Point) -> f64 {
sqrt(p.x * p.x + p.y * p.y)
}
fn shift(mut p: Point, dx: f64) {
p.x += dx
}
var p = Point { x: 3.0, y: 4.0 }
p.x += 1.0
shift(mut p, 2.0)
let x = 1.0
let q = Point { x, y: 2.0 }
print(len(p) + len(q))
struct Div {
padding: f64 = 0.0
align: Align = Start
on_click: fn() = fn() {}
children: []Element = []
}
let page = Div { padding: 8.0 } {
Label(Text { value: "title" })
Box(Div { align: Center } {
button("ok")
button("cancel")
})
}
- Literals name their fields, separated by commas or newlines;
xalone meansx: x. A field declared with= exprmay be left out; every other field must be given. - A default is any expression. It is checked once against the field's type, in the declaring module's scope, with no access to locals or other fields. It is evaluated at every literal that leaves the field out, after the written fields. A field with a default can be left out of a literal in another module even when it is private.
- A brace block on the same line after a struct literal holds its children: the entries, one per line or separated by commas, become a slice literal for the struct's
childrenfield. - A brace block after a call becomes a slice literal passed as the last argument, so
fn button(label: str, children: []Element) -> Elementtakes children the same way. - A trailing block is not allowed where a struct literal is not.
- A struct literal cannot appear directly in an
if,forormatchcondition; wrap it in parentheses. - Structs are values: assignment and argument passing copy them.
==and!=compare structs field by field when every field can be compared: numbers,bool,str, pointers, channels, and structs and enums made of them. Each field uses its own==, so strings compare by content, floats by value and pointers by identity.- Fields are assigned through a
varbinding or amutparameter, at any depth:r.max.x = 1. Other parameters are immutable. - A struct cannot contain itself by value; it can hold a
*pointer to its own type. - Fields are laid out in declaration order, one word per scalar, with no padding.
References
fn swap(mut a: i64, mut b: i64) {
let t = a
a = b
b = t
}
var x = 1
var y = 2
swap(mut x, mut y)
- A
mutparameter refers to the caller's variable; reads and writes go through to it. The call site writesmutbefore the argument. - A
mutargument must be avarbinding, amutparameter, a field or element of either, or a place through a dereferenced pointer. - One call cannot pass overlapping places as
mut:swap(mut p.x, mut p.x)is rejected,swap(mut p.x, mut p.y)is allowed. Twomutparameters therefore never alias. - References exist only as parameters. They cannot be stored, returned or bound with
let, so they never outlive the variable they refer to. - A
mutparameter occupies one word holding the address of the place. - A place narrower than a word, such as an element of a packed slice like
[]u8or a narrowextern structfield, is copied into a word-sized frame slot before the call and copied back at its own width after it, so the callee's word-sized reads and writes never touch the neighbouring bytes. Code in the callee that reads the same element through another route, such as a pointer to the slice, sees the value from before the call until the callee returns.
Pointers
enum List {
Cons(i64, *List)
Nil
}
var list = List.Nil
list = Cons(1, new(list))
let c = new(Counter { hits: 0 })
c.hits += 1
bump(mut *c)
let copy = *c
new(value)copies a value onto the garbage-collected heap and returns a*T. Pointers are never null.p.fieldreads and writes through the pointer;*pis the whole value, readable, assignable and passable asmut.- The object behind a pointer is mutable through any pointer to it, including one held by
let. ==and!=on pointers compare identity.mutarguments through pointers are not checked for overlap, since two pointers may refer to the same object.
Enums
enum Shape {
Circle(radius: f64)
Rect(w: f64, h: f64)
Empty
}
fn area(s: Shape) -> f64 {
match s {
Circle(r) => 3.14159 * r * r
Rect(w, h) => w * h
Empty => 0.0
}
}
matchmust cover every variant, or end with_or a binding arm. Arms after_or a binding arm, and duplicate arms, are errors.- A binding arm is a name starting with a lowercase letter that is not a variant, such as
other => describe(other). It matches anything and binds the whole subject. - A capitalized name that is not a variant is an error, so a mistyped variant is never taken for a binding.
- A binding arm is a name starting with a lowercase letter that is not a variant, such as
- An arm that starts with a literal (
4,-1,'x',"a",true), a parenthesised expression,!or~, or a name followed by.(Mode.Fast,limits.max) is a test arm: an expression compared with==.4 => ...means== 4 => ...and(base + 1) => ...means== base + 1 => .... Amatchwith a test arm must end with_. A bare name is a variant or a binding, so write(limit) => ...to compare against a variable. - An arm that starts with a binary operator tests the subject:
% 3 == 0 => ...meanssubject % 3 == 0, with the subject on the left and ordinary precedence after it.- The subject is evaluated once and arms are tried in order.
- Such tests work on a subject of any type, including enums (
== Empty) and the error of acatch. - A
matchthat uses one, or whose subject is not an enum, must end with_. - Arms are separated by newlines or commas.
fn classify(n: i64) -> str {
match n {
% 15 == 0 => "FizzBuzz",
% 3 == 0 => "Fizz",
% 5 == 0 => "Buzz",
_ => "number"
}
}
- Patterns bind fields by position;
_ignores a field, and a bare variant name ignores all of them. - An arm body is an expression, or
break,continue,returnorfail. - Variants are constructed as
Shape.Circle(2). Where the expected type is known (an annotatedlet, an argument, an assignment, a return,fail), the bare name works:let s: Shape = Empty. - Code generation for a
matchdepends on its arms. It applies when the arms before the last are all distinct variants of one enum, or all distinct integer or character literals of one type:- With at least 12 such arms whose values span at most 4096 and at most three times the arm count, it compiles to a
switchblock: the backend emits a bounds check and an indirect jump through a table of 32-bit offsets. - Without that density, 48 or more arms compile to a binary search over the values.
- Fewer arms keep the chain of comparisons.
- Duplicate values, test arms and arms with other expressions always keep the chain.
- A
switchon a constant folds to its target.
- With at least 12 such arms whose values span at most 4096 and at most three times the arm count, it compiles to a
- An enum whose variants all have no payload is a single word. Otherwise an enum is a tag word followed by the largest variant's fields.
- An enum cannot contain itself by value; it can hold a
*pointer to its own type. ==and!=compare enums whose variant fields can all be compared: equal values have the same variant and equal fields. Optionals compare the same way, sox == noneandx == 5work on ani64?.
Optionals
fn find(xs: []i64, target: i64) -> i64? {
for i, x in xs {
if x == target {
return i
}
}
none
}
let index = find(xs, 7) ?? -1
match find(xs, 4) {
some(i) => print("at {i}")
none => print("missing")
}
struct Node {
value: i64
next: *Node?
}
T?holds eithersome(value)ornone.- A
Tis accepted wherever aT?is expected: an annotatedlet, an assignment, an argument, a field, a return and either branch of aniformatch.noneandsome(v)take their type from that expectation. x ?? fallbackgives the value insidex, or evaluatesfallbackwhenxisnone.- The fallback may itself be a
T?, making the result aT?. - The fallback may leave instead, as in
let line = read_line() ?? break. ??binds tighter than comparisons and looser than arithmetic, and groups to the right:a ?? b ?? 0triesa, thenb.
- The fallback may itself be a
matchunwraps an optional withsome(v)andnonearms and must cover both.?after a type binds to everything before it:*Node?is an optional pointer and[]i64?an optional slice.- An optional is an enum with
noneas tag 0 andsomeas tag 1, so it takes one word more than its value.
Errors
An error type is an enum. A function that can fail declares -> T ! E.
enum ParseError {
Empty
BadDigit(at: i64)
}
fn parse(s: str) -> i64 ! ParseError {
if s.len == 0 { fail Empty }
...
}
fn total(a: str, b: str) -> i64 ! ParseError {
parse(a)? + parse(b)?
}
fn validate(n: i64) ! ParseError {
if n < 0 { fail BadDigit(n) }
}
fn main() ! ParseError {
let n = parse("42") catch {
Empty => 0
BadDigit(at) => at
}
let m = parse("") catch -1
validate(n)?
}
fail vreturns the errorv;vis checked against the function's error type, so bare variant names work.- Postfix
?returns the error to the caller, whose error type must be the same enum. catch { arms }matches the error exhaustively; a binding arm such ase => report(e)receives the whole error.catch valuesubstitutes a fallback.fn f() ! Efails or returns nothing.- A result cannot be discarded, bound or passed on without
?orcatch, except by returning it from a function with the same result type. - When
maindeclares! Eand returns an error, the program printserror: <Variant>to stderr and exits with status 1. Panics exit with status 2. - A result is a tag word (0 ok, 1 error) followed by the larger of the ok value and the error enum.
Embedded data
let shader = embed("shaders/quad.spv")
let font = embed("fonts/fallback.ttf")
print(shader.len)
embed("path")is the contents of a file as astr, read at compile time. The path must be a string literal and is relative to the directory of the file that callsembed, so a module carries its own assets.- The bytes live in the binary's read-only data, aligned to 16 bytes; nothing is copied at run time. Indexing, slicing,
.lenandbytes(...)work as on any string. - A missing file is a compile error.
low devwatches embedded files, and a change to one reloads the functions that embed it.
C interop
extern "c" {
fn malloc(n: i64) -> ptr
fn free(p: ptr)
fn strtol(s: ptr[u8], mut end: ptr, base: i32) -> i64
fn snprintf(buf: ptr[u8], n: i64, format: ptr[u8], ...) -> i32
pub fn div(a: i32, b: i32) -> DivT
}
extern struct DivT {
quot: i32
rem: i32
}
using block = Block[u8] { data: malloc(64) }
let buf = block.data
snprintf(buf, 64, cstr("%d-%.2f"), 42, 3.14159)
print(str(buf))
var end: ptr = null
let n = strtol(cstr("123abc"), mut end, 10)
let pixels = view(buf, 64)
extern "name" { ... }at the top level of a file names a C library and declares its functions without bodies."c"is the C library itself.- A trailing
...makes a function variadic. fn errno_location() -> ptr[i32] = "__errno_location"gives a function a Low name different from its C symbol, for symbols that start with__or are Low keywords.- Declarations are private to their module unless marked
pub. - A library is linked when code reachable from
maincalls or takes the address of one of its declared functions, so importing a module that declares C functions costs nothing until one is used. - A program that uses no library is a static executable with no libc. A program that uses any is a dynamic executable linked against libc and each used library.
- C signatures use C types: integers, floats,
bool,ptr,ptr[T], andextern structs by value. - A
mutparameter passes the address of a Low place for the duration of the call, as C# passesref. C may read and write through it. ptr[T]is a nullable pointer to C memory.ptrisvoid*and converts to and from anyptr[T].nulltakes its type from context.==and!=compare addresses.view(p, n)is a[]Tovernelements atp, bounds-checked like any slice.load(p)andstore(p, v)read and write oneT.cstr(s)is a NUL-terminated copy of a string.str(p)copies a NUL-terminatedptr[u8]back.extern structhas C layout: fields in order at their natural alignment, size rounded to the largest alignment. Its fields must have C types. It is used like any struct, and its fields are read and written at their own width.- Memory may be passed to C for the duration of a call. Memory C keeps after the call must be pinned:
pin(x)returns the address of a pointer, slice, map or string's data and keeps it alive untilunpin(p). - Memory C allocates is never touched by the collector.
extern fn(ptr[i64], ptr[i64]) -> i32is a C function pointer, one word, like C#'sdelegate* unmanaged. Its parameters and result must have C types. It is a C type, so it can be a parameter, a result or anextern structfield.- A top-level Low function becomes a C callback where an
extern fnis expected, if it takes and returns only numbers,bool,ptrorextern fn, with nomutparameters. - An
externfunction used as a value is its C address. Calling anextern fnvalue makes a C call. - A closure reaches C through a
ptruser-data argument: pin a pointer to the state it needs and pass a top-level callback that loads it.
extern "c" {
fn qsort(base: ptr, n: i64, size: i64, compare: extern fn(ptr[i64], ptr[i64]) -> i32)
}
fn ascending(a: ptr[i64], b: ptr[i64]) -> i32 {
i32(load(a) - load(b))
}
qsort(data, n, 8, ascending)
Defer
let fd = open(path)?
defer close(fd)
defer {
let summary = "closing {path}"
log(summary)
}
deferruns a statement when its enclosing block exits: at the end of the block, onreturn,failor a propagated?, and whenbreakorcontinueleaves it.- Deferred statements of a block run last first.
- The statement sees variables as they are at exit.
- It cannot
return,fail, use?,break,continueor contain anotherdefer.
Using
struct File {
fd: i32
drop close(fd)
}
struct Conn {
sock: i32
buf: Block[u8]
drop {
flush(sock)
close(sock)
}
}
struct Block[T] {
data: ptr[T]
drop free(data)
}
using f = open_file("config.low")
using c = connect(addr)?
using var current = open_file("a")
current = open_file("b")
drop <stmt>inside a struct body declares its cleanup, with the struct's fields in scope as borrowed values. Likedefer, it takes one statement or a{ }block.- A struct has at most one
drop, andusingcannot appear in it. - A struct with a
dropis droppable, and so is a struct with a droppable field, even without its owndrop. Generic instances are droppable per instance:Pair[File, i64]is,Pair[i64, i64]is not. - Releasing a value runs its own
dropfirst, then releases its droppable fields in declaration order. using x = exprbindsxand releases it when the enclosing block exits by any path: the end of the block,return,fail,?,breakorcontinue. Releases anddefers run together, last first.using var x = exprcan be reassigned. Assignment releases the old value, and exit releases the value held then.usingcannot appear insidedefer. Binding a non-droppable value withusingis an error.- A droppable value cannot be bound with
letorvar: "File has drop; bind it with using". Results of droppable types are unwrapped with?orcatchstraight into ausingbinding. - Droppable values are move-only:
return x, or a function body ending inx, moves ausingbinding out. The caller owns the value and must bind it withusing.- A struct literal field or an assignment to a field of a droppable value moves the value into it, and the outer value releases it. A field is assigned through a
using varbinding or amutparameter, and the field's old value is released first. using y = xmovesxintoy.- Passing a value as an argument borrows it: the callee cannot release it, store it or return it. A freshly produced value, such as a call result or a struct literal, must be bound with
usingbefore it is passed. - Any other copy is an error: binding it with
let, storing it in a slice, map, channel, optional, pointer or enum variant, moving a field out of its struct, discarding a call result, or capturing it in a closure. - A struct with a droppable field is itself droppable, so it cannot be built with
newor held by a pointer, andspawncannot take one as an argument: "spawn cannot take File; it has drop". Code that shares a resource across closures, pointers or green threads passes a copyable view of it and binds the owner where the resource has one home (see Resource ownership). - Using a binding after it was moved is an error, as is using one that may have been moved on one branch or by an earlier loop iteration.
- Generic code is checked under these rules for each instance whose type arguments are droppable, so
fn same[T](x: T) -> T { x }cannot be instantiated withFile. - Each
usingbinding has a hidden flag that a move clears. The release at exit runs only while the flag is set, so a value moved on one branch is released exactly once.
Modules
import geometry
import t = ui/text
pub struct Circle {
pub radius: f64
cache: f64
}
pub fn area(c: Circle) -> f64 {
3.14159 * c.radius * c.radius
}
fn main() {
let c = geometry.circle(2.0)
print(geometry.area(c))
let s: geometry.Shape = Square(3)
print(t.shout("hi"))
}
- A module is a directory. Every
.lowfile in it shares one namespace, so files of a module need no imports between them. - The project root is the nearest directory, starting at the entry, that holds a
low.modfile. Without one it is the entry's directory. import ui/textloads<root>/ui/text/. A path starting withstd/names a module of the standard library, which is compiled intolow, so a project directory namedstdcannot be imported.low build dirbuilds the directory as the entry module.low build file.lowbuilds that one file as the entry module, without the other files beside it.low lint file.lowtype-checks the program, prints warnings for the program's own modules (not the standard library), and exits with status 1 if there were any.low build,low run,low asmandlow irprint the same warnings to stderr and continue. The language server reports them as warnings.prefer-matchflags anif/else if/elsechain with at least two conditions in which every condition is==between one side-effect-free subject (a variable, field, index or dereference) and a non-negative integer, character or string literal or an enum variant. It suggestsmatch subject { 4 => ..., _ => ... }.- Imports come before declarations and apply to their file.
- A module is used through its last path segment,
text.shout(...), or through a name given withimport t = ui/text. - Types are written
geometry.Shapeand built withgeometry.Point { ... }. A variant can be namedgeometry.Shape.Dotor, where the type is expected,Dot. - Declarations and struct fields are private to their module unless marked
pub. Apubenum's variants are public. - A struct with private fields without defaults cannot be built with a literal outside its module.
- An import cycle is an error, as is importing two modules under one name.
Generics
struct Stack[T] {
items: []T
}
enum Tree[T: Ord] {
Node(*Tree[T], T, *Tree[T])
Leaf
}
fn max[T: Ord](a: T, b: T) -> T {
if a > b { a } else { b }
}
fn sum[T: Number](xs: []T) -> T {
var total: T = 0
for x in xs {
total += x
}
total
}
fn sort[T](mut xs: []T, less: fn(T, T) -> bool) { ... }
print(max(3, 9))
print(max[u8](200, 100))
var s = Stack[str] { items: [] }
let p = Pair { first: 1, second: "x" }
let t: Tree[i64] = Leaf
let pick: fn(i64, i64) -> i64 = max
- Functions, structs and enums take type parameters in brackets after their name. Generic types are written with arguments:
Stack[i64],geometry.Labelled[str]. - A constraint lists what a type parameter supports, joined with
+:Eqallows==and!=. It is met by numbers,bool,str, pointers, channels, and structs and enums whose fields are allEq.Ordadds<,<=,>and>=. It is met by numbers andstr.Hashadds use as a map key. It is met by integers,bool,strand pointers.Numberadds arithmetic, numeric literals, conversion withT(x)andprint.Integeradds bitwise operators and shifts, and impliesHash.Floatis met byf64andf32.- An unconstrained parameter can only be moved, stored and passed. Anything else a generic function needs, such as an ordering of its elements, is passed in as a function value.
- A generic body is checked once against its constraints, so a missing constraint is reported at the definition, not at a use.
- Each generic function's type parameters are its own. Two functions may both name a parameter
T, and a function type mentioning one is distinct from the same text mentioning the other. - Type arguments are inferred from the arguments, then from the expected type. They can be written explicitly:
max[u8](a, b),Stack[str] { ... }. - A generic struct literal or enum variant takes its arguments from the expected type or from its field values. A generic function used as a value takes them from the expected function type.
- Each distinct set of type arguments compiles to its own copy,
max[i64]andmax[f64], so generic code runs as fast as code written for one type. - Values of different kinds that share behaviour, such as the elements of a UI tree, are structs of function values whose closures capture each kind's state.
Concurrency
fn worker(jobs: chan[i64], results: chan[i64]) {
for j in jobs {
results <- j * 2
}
}
let jobs = chan[i64](16)
let results = chan[i64](16)
spawn worker(jobs, results)
jobs <- 21
close(jobs)
print((<-results) ?? 0)
let next = select {
v = <-results => v ?? -1
jobs <- 5 => 0
after(100) => -2
else => -3
}
sleep(10)
wait_readable(fd)
spawn f(args)starts a green thread running the call. The function value and the arguments are evaluated before it starts.mutarguments are not allowed. A function that can fail must have its error handled inside the spawned call.chan[T](n)makes a channel holding up tonvalues.chan[T]()is unbuffered, so every send waits for a receiver. A channel is a reference: assigning or passing it shares it.c <- vsends, waiting while the buffer is full.<-creceives aT?, waiting while the channel is empty. It givesnoneonce the channel is closed and drained.close(c)wakes every waiter. Sending on or closing a closed channel panics.for v in creceives until the channel is closed and drained.selectwaits on several operations at once and runs the arm of the first that can proceed. It is an expression likematch.- Arms are
name = <-c,<-c,c <- v,after(ms)andelse. - Channel and value expressions are evaluated first, in order.
- Ready arms are taken in order.
elseruns when nothing is ready. Withoutelsethe green thread waits.after(ms)fires oncemsmilliseconds pass.
- Arms are
sleep(ms)pauses the green thread.wait_readable(fd)andwait_writable(fd)wait until a file descriptor is ready, using epoll. One green thread may wait on a descriptor at a time.- A program ends when
mainreturns, whatever other green threads are doing. - When every green thread is waiting and nothing can wake one, the program panics with
every green thread is blocked forever. A green thread that exhausts its stack panics withstack overflow.