This page is generated from docs/language_definition.md on every deploy, and every Iron example in it is compiled and run in CI. A plain Markdown copy for tools and language models is at /llms/reference.md; see /llms.txt for the full index.
Iron Reference Manual
This manual describes the Iron programming language as implemented by the
iron compiler that ships with this repository (version 5.0). It is a
reference, not a tutorial: each section states what the compiler accepts and
what the program does, with a small complete example. Every iron code block
in this document is compiled and executed by scripts/test_doc_examples.sh,
and blocks that show a compile error are checked to fail with the stated
diagnostic code, so the manual cannot drift from the compiler without the test
failing.
Iron is a general-purpose native programming language. It is statically typed
and compiles to C, then to a native binary. It has no garbage collector, no
exceptions, no operator overloading and no implicit numeric conversions.
Values live on the stack by default; explicit heap, rc and arena
allocation cover the other cases.
Contents:
- Lexical conventions
- Types
- Expressions
- Statements
- Declarations
- Memory
- Concurrency
- Compile-time evaluation
- The standard library
- Programs, projects and the command line
- Diagnostics
- Not yet implemented
- Complete syntax of Iron
1. Lexical conventions
1.1 Source text
Iron source files use the .iron extension and are UTF-8 text. Spaces,
tabs and carriage returns separate tokens. Newlines are tokens for the
lexer, but the parser skips them wherever they appear, so a newline never
ends a statement by itself: statements are delimited by the grammar (section
1.6). Semicolons are not statement terminators; ; appears only inside list
types and list literals ([Int; 4]).
1.2 Comments
Iron has one comment form: // starts a comment that runs to the end of
the line. There are no block comments, and # is not a comment. A //
inside a string literal is string text ("http://example.com").
Documentation. A run of // comment lines directly above a
declaration, an object field, an enum variant or an interface method, with
no blank line between the lines or before the declaration, is its
documentation. The language server shows it on hover, in completion and in
signature help, with one leading space removed from each line. A blank line
breaks the association, and a comment at the end of a line of code is never
documentation. The compiler otherwise ignores comments.
A comment never changes what a program means. One comment has a meaning to
the compiler's own tests: a comment whose text starts with @file: (a
file marker) tells the test harness's multi-file mode that the source
after it belongs to another file. Only that mode
(IRON_MULTI_FILE_TEST=1) honors it; in any other compile it is an
ordinary comment, and diagnostics after it still name the file it is
written in.
// Doubles its argument. // Attached to `twice` because no blank line separates them. func twice(x: Int) -> Int { return x * 2 // a comment at the end of a line } // A comment followed by a blank line documents nothing. func main() { println("{twice(21)}") }
42
Directives. A comment written //key:value, with no space after //,
a key of lowercase letters, digits and _ starting with a letter, and a
: right after the key, is a directive. A directive belongs to the
declaration or field it sits directly above, as part of its comment run. It
may instead trail an object field on that field's line. Directive lines are
not part of the documentation. The value is the rest of the line.
// json:id, with a space, is an ordinary comment.
object Item { // The catalog number. //db:primary_key pub val item_id: Int //json:id pub val name: String //json:name } func main() { val it = Item(7, "lamp") println("{it.item_id} {it.name}") }
7 lamp
The compiler keeps this metadata on each relevant AST node as its
doc_comment text and a source-ordered directives list. Each directive is
(key, value, line, trailing); line is one-based and trailing can be true
only for an object field. This is the representation available to the later
compile-time reflection work in issue #455. Directives have no meaning of
their own, except the iron key: //iron:<hint> is a compiler hint. A hint
changes only how the compiler represents or optimizes a declaration (a
function, object or field), never what the program does;
anything that is part of a type or changes what type checks, such as
[T, layout: soa] or [I, unordered], is language syntax instead. The
hints are:
| Hint | On | Meaning |
|---|---|---|
//iron:fusible |
a function or method, on the line above func |
the function can run inside a fused list loop: no effects, no panics (sections 5.2 and 9.3.2) |
An //iron: hint the compiler does not know, or one written above a
declaration it does not apply to, is warning W0617, so a misspelling such
as //iron:fusable does not pass silently. A //iron:fusible function
that cannot keep the claim is warning W0618.
Transition. Earlier versions wrote line comments with --,
documentation lines with /// and the fusion hint as @fusible. These
are still accepted, with the same meaning as //, // and
//iron:fusible, while code moves over; iron fmt --migrate-comments
[paths] rewrites them (section 10) and a later version will reject them.
1.3 Identifiers and keywords
An identifier is a letter or _ followed by letters, digits and _
(ASCII only). A lone _ is the wildcard, usable as a binding name in
val, var, tuple destructuring and match patterns; it is not an
expression. Identifiers are case sensitive. By convention types and enum
variants are CapitalCase; functions, variables and fields are
snake_case. An identifier is not restricted by the keywords, macros, or
runtime symbols of the language targeted by a compiler backend. For example,
default, register, and int are valid Iron identifiers. Capitalization is
also significant in one place: in X.Y(args) and X.Y, an uppercase X
makes the expression a possible enum variant construction; the resolver
distinguishes it from a method call or field access (section 3.6).
The following words are keywords and cannot be used as identifiers:
and await break comptime continue copy defer drop elif else enum extends extern false for free func heap if impl import in init interface is leak match mut nocopy not null object or parallel patch pool private pub pure rc readonly return self spawn static super true unchecked val var weak while
extends, super, mut, private and pool are reserved but have no
valid use: the compiler rejects them with an explanation (extends and
super because Iron has no inheritance, mut and private because var
and default privacy replaced them, pool because thread pools are not
implemented). init, copy, drop, null and free may additionally
appear as method names after a . or after func inside an object body,
which is how Box.null() and b.free() are spelled.
The words as, layout, soa, aos, unordered, allow_drop_skip
and Self are ordinary identifiers with a special meaning in specific
positions (import aliases, list attributes, heap options and the
receiver type). fusible is an ordinary identifier too; it is special only
after @ in the transitional @fusible (section 1.2).
1.4 Literals
Integers are decimal (42), hexadecimal (0xFF, 0Xff) or binary
(0b1010). Digits may not be separated by _, and a letter directly after a
number is an error. An integer literal has type Int (64-bit signed) unless
it initializes a binding or field of another integer type, or is an operand
whose partner has another integer type (x + 1 with x: Int8 is Int8
arithmetic), in which case it takes that type. A constant expression
(literals combined with -, ~ and the arithmetic, bitwise and shift
operators) behaves like the literal it folds to: x + (2 + 3) is
x + 5. A literal or constant expression whose value does not fit the type
it takes is an error (E0292). Hexadecimal and binary literals must fit in
64 bits.
Floats are a digit sequence, a . and a digit sequence: 3.25,
0.5. There is no exponent form and no trailing-dot form (1. and .5
are not float literals). A float literal has type Float (64-bit) unless
the context asks for Float32.
Booleans are true and false. null is the value of every
nullable type (section 2.2).
Strings are written between double quotes on one line, or between
""" and """ when they span lines (the newlines are part of the value).
A string literal is at most 4095 bytes. The escape sequences are:
| Escape | Meaning |
|---|---|
\n |
newline (byte 10) |
\r |
carriage return (byte 13) |
\t |
tab (byte 9) |
\0 |
the zero byte |
\xHH |
the byte HH: exactly two hex digits, \x00 to \xFF |
\\ |
backslash |
\" |
double quote |
\{ and \} |
literal braces (not interpolation) |
\u{H} |
the Unicode scalar H (1 to 6 hex digits), encoded as UTF-8 |
Any other character after a backslash is kept together with the backslash.
\0 is only the zero byte: a digit after it is an ordinary character
("\012" is the three bytes 0, 1, 2; there are no octal escapes). A
\x that is not followed by two hex digits, and a \u that does not
name a Unicode scalar, are errors (E0002).
\xHH writes one byte, not a character. A string holds bytes (binary
data is a String, section 9.12), so every value is allowed, but a byte
from \x80 to \xFF on its own is not UTF-8 text: write characters as
\u{...} ("\u{e9}" is the two bytes \xC3\xA9) and keep \x80 and
above for binary data. A literal may contain zero bytes anywhere; they are
part of its value and its byte_len():
func main() { val s = "a\0b\x41\r\n" println("{s.byte_len()} {s.byte_at(1)} {s.byte_at(3)} {s.byte_at(4)}") }
6 0 65 13
A string literal that contains an unescaped { is an interpolated
string: each {expression} is evaluated and its text spliced in (section
3.3). Interpolated expressions may themselves contain string literals.
func main() { val h = 0xFF val b = 0b1010 val f = 3.25 val s = "tab\there \"quoted\" braces \{x\} unicode \u{48}\u{49}" val m = """first line second line""" val name = "iron" println("{h} {b} {f}") println(s) println(m) println("nested {name.upper()} and {"lit".len()} and {1 + 2 * 3}") }
255 10 3.25
tab here "quoted" braces {x} unicode HI
first line
second line
nested IRON and 3 and 71.5 Operators and punctuation
+ - * / % == != < > <= >=
and or not & | ^ ~ << >> is
= += -= *= /= &= |= ^= <<= >>=
. .. , : ; -> ? @ &(address-of)
( ) [ ] { }! on its own is not a token (not is the logical negation), and
neither are && and || (and, or); each is an error (E0002) that
names the word to write.
Section 3.1 gives the precedence of the operators.
1.6 Newlines and statement boundaries
Because the parser skips newlines everywhere, an expression continues onto
the next line whenever that line starts with something that can extend it: a
binary operator, (, [, ., or is. This is what allows long calls and
list literals to be split across lines, but it also means that a line that
begins with -, ( or [ continues the previous statement. A bare
return must be the last statement of its block, since an expression on the
following line would be taken as its value.
func f(x: Int) -> Int { return x } func main() { val a = 10 val b = a - 3 -- continues the previous line: b is 7 val c = f( 1, ) val d = 1 + 2 println("{b} {c} {d}") }
7 1 3
2. Types
Iron is statically typed. Every binding, field and parameter has a type that is either written after a colon or inferred from its initializer. There are no implicit conversions between numeric types, and none between numbers and strings.
2.1 Primitive types
| Type | Description |
|---|---|
Int |
64-bit signed integer (the default integer type) |
Int8, Int16, Int32, Int64 |
signed integers of the given width |
UInt, UInt8, UInt16, UInt32, UInt64 |
unsigned integers (UInt is 64-bit) |
Float |
64-bit IEEE double (the default float type) |
Float32, Float64 |
floats of the given width |
Bool |
true or false |
String |
an immutable sequence of Unicode characters, stored as UTF-8 (it may also hold arbitrary bytes, section 1.4) |
Void |
the result type of a function without -> T (never written) |
Arithmetic on the same integer type wraps on overflow (two's complement).
Integer division truncates toward zero and division by zero panics at run
time. Mixing Int with Float, or two different integer widths, in one
expression is an error (E0222, E0202); convert explicitly (section 2.9).
Strings are values: copying a string copies a reference to shared,
reference-counted characters and never mutates them. len(s) and
s.len() count characters (code points); s.byte_len() counts bytes.
Iterating a string with for yields one-character strings.
2.2 Nullable types
T? is a type whose values are either a T or null. A nullable type can
be written for any non-pointer type (Int?, String?, Node?,
Result[Int, String]?, [Int]? for a list that may be absent). A plain T converts to T? implicitly, so a
function declared -> Int? may return i or return null. Reading a
field or calling a method through a nullable value without checking it
first is an error (E0204); compare with null first. x != null
narrows x to T inside an if or while body (and after an early
return in the other branch) until x is assigned a value that may be
null: while cur != null { total += cur.value; cur = cur.next } walks a
linked list. The right operand of and is narrowed by a true left
operand and the right operand of or by a false one, so
x != null and x > 0 and s == null or s.len() == 0 need no if.
Comparing a T? that was not checked with a value (x == 3, s < "a")
is an error (E0204): check it first, as in x != null and x == 3. A
binding assigned inside a loop or a closure is not
narrowed there by an outer check, since the assignment may already have
run. Comparing a value whose type is not nullable with null warns
(W0616): the answer is known before the program runs.
func find(xs: [Int], target: Int) -> Int? { var i = 0 while i < len(xs) { if xs[i] == target { return i } i += 1 } return null } func main() { val idx = find([4, 5, 6], 6) if idx != null { println("found at {idx}") } val missing = find([4, 5, 6], 9) if missing == null { println("missing") } var s: String? = null s = "now" println("{s}") println("{idx != null and idx > 1} {s != null and s == "now"} {missing == null or missing > 1}") }
found at 2 missing now true true true
A nullable list [T]? (or a nullable map, set, or object holding a list)
follows the list ownership rules of section 2.3: it has one owner, so
binding it to a second name, storing it in a field or returning a
parameter is an error (E0328); check it for null, then write
xs.copy() or xs.take(). A function returning [T]? returns a fresh
list or null:
func evens(n: Int) -> [Int]? { if n < 0 { return null } var out: [Int] = [] for i in range(n) { if i % 2 == 0 { out.push(i) } } return out } func main() { val xs = evens(5) if xs != null { println("{xs.len()} {xs[2]}") } println("{evens(-1) == null}") }
3 4 true
A nullable type may also be the element type of a list, fixed array or
bounded vector ([String?], [Int?; 3]), the value type of a map
(Map[String, Int?]) and the element type of a channel
(Channel[Int?]). null and plain T values are stored as elements
([1, null, 3], xs.push(null), m.put(k, null), ch.send(null)), an
element read out of the collection is a T? that is checked with
!= null before use like any other, and xs.contains(null) finds a
null element. A map key mapped to null is present: m.has(k) is
true and m.get(k) returns null, while get on an absent key still
panics. Map keys and set items must be Hashable, which a nullable type
is not (E0206). In expression position the type argument is written
the same way: Map[String, Int?](), Map[String, [Int?]]().
func first_present(xs: [Int?]) -> Int? { for x in xs { if x != null { return x } } return null } func main() { var xs: [Int?] = [null, 2, null] xs.push(4) println("{xs.len()} {xs[0]} {first_present(xs)} {xs.contains(null)}") val names: [String?] = ["ann", null] for n in names { if n != null { println("{n.upper()}") } else { println("nobody") } } var ages = Map[String, Int?]() ages.put("ann", 30) ages.put("bob", null) println("{ages.has("bob")} {ages.get("bob")} {ages.has("cy")}") }
4 null 2 true ANN nobody true null false
Nullable pointers are written ?*T (section 6.4), and a nullable weak
reference weak rc T? (section 6.3).
2.3 Lists, fixed arrays and bounded vectors
[T] is a growable list of T. [T; N] is a fixed-size array of exactly
N elements, where N is an integer constant: a literal or constant
expression ([Int; 2 * 8]), a comptime expression (section 8), or the
name of a global val holding one of these (val N = comptime size(3),
then [Int; N]); anything else is an error (E0202). [T; <=N] is a bounded
vector: a list that can hold at most N elements and whose storage is
inline. All three are indexed with xs[i] from 0, checked at run time
(an out-of-range index panics; a constant out-of-range index on a fixed
array is a compile error, E0312), and iterated with for.
A list literal [a, b, c] has the type of its elements; an empty literal
[] needs a type annotation on the binding (E0229). fill(n, v) builds a
list of n copies of v.
A list has exactly one owner. Assigning a list binding to another binding,
storing it in a field or another list, or returning a parameter list is an
error (E0328): write xs.copy() for an independent copy or xs.take()
to move the contents out and leave xs empty. Passing a list to a
function lends it for the duration of the call. Growing or shrinking a
list and writing an element require a var binding (E0235).
rc [T] is a shared list (section 6.3). The list methods are listed in
section 9.3.
A fixed array and a bounded vector are values, like objects: their
elements live inline, and assigning one, passing it, returning it or
storing it in a field or a list copies the elements, so a write through
one binding is never seen through another. filter on either returns a
list [T]; copy() and take() exist only on lists.
func main() { var xs: [Int] = [] xs.push(3) xs.push(1) xs.insert(0, 9) xs[1] = 30 println("{xs.len()} {xs[0]} {xs[1]} {xs.contains(1)}") val fixed: [Int; 3] = [7, 8, 9] var bounded: [Int; <=4] = [1, 2] bounded.push(3) val zeros = fill(4, 0) println("{fixed[1]} {len(fixed)} {bounded.len()} {zeros.len()}") var other = fixed other[1] = 80 println("{fixed[1]} {other[1]}") val dup = xs.copy() val moved = xs.take() println("{dup.len()} {moved.len()} {xs.len()}") val grid: [[Int]] = [[1, 2], [3]] println("{grid[0][1]} {grid.len()}") }
3 9 30 true 8 3 3 4 8 80 3 3 0 2 2
A list type may carry attributes after the element type:
[T, layout: soa] stores an object list field by field (structure of
arrays) and [T, layout: aos] element by element (the default); either
is used the same way. [I, unordered], for an interface type I, keeps
the elements grouped by concrete type instead of in insertion order and
cannot be indexed (E0329); iteration visits every element.
A layout: soa list of an object type keeps one column per field, all in
one allocation: the values of the first field of every element, then those
of the next field, and so on. Every list operation gives the same result,
and the same panic at the same line, as for the default layout; what
changes is where the bytes are:
ps[i].xreads or writes one value of thexcolumn and touches no other field, so a loop over one or two fields of a long list reads contiguous memory and can be vectorized.&ps[i].xpassed to a pointer parameter points into the column.- A whole element (
ps[i],for p in ps,pop,remove) is gathered from the columns into a value;push,insertandps[i] = vscatter one.insertandremovemove the tail of every column. - A whole element has no address. A method called on
ps[i],&ps[i]passed to a pointer parameter andps[i]passed to avarparameter lend a gathered copy for the duration of the call; when the call returns, what it may have changed is stored back (for a method, the fields its body can write; nothing for areadonlymethod or a*Tparameter). During such a call the element read again through the list itself still has its value from before the call. The pointer rules of section 6.4 are those of the default layout: the pointer lives for the call (a copy the callee keeps is stale afterwards), the list is pinned while the call runs, and the address cannot be kept (E0330). - The layout belongs to the list value and is chosen where the list is
made. A list literal,
[]included, takes the layout of the type it is checked against: the annotation of the binding, field, parameter or return type it initializes.copy(),take(), a slice andfiltergive a list of the same layout as their source; any other new list (map, a literal with no annotated type) has the default layout. - A list keeps its layout wherever it goes.
[T]and[T, layout: soa]are the same type: a parameter, binding or field of either accepts a list of either layout, and passing, returning or storing a list never converts it. - Copy and drop hooks run exactly as for the default layout, once per element copied or destroyed.
For an element type that is not an object (numbers, String, enums) the
attribute is accepted and changes nothing. Fixed arrays and bounded
vectors take no attributes by definition, although the compiler currently
accepts and ignores them there (#841). Traversing every field of every element is
not faster in a layout: soa list; use it for lists whose hot loops read
a few fields of many elements. The debugger formatters show the elements
of either layout.
object Particle { var x: Float var y: Float init(x: Float, y: Float) { self.x = x self.y = y } } interface Drawable { readonly func draw() -> String } object Dot impl Drawable { val id: Int readonly func draw() -> String { return "dot{self.id}" } } object Square impl Drawable { val id: Int readonly func draw() -> String { return "square{self.id}" } } func main() { var ps: [Particle, layout: soa] = [] ps.push(Particle(1.0, 2.0)) ps.push(Particle(3.0, 4.0)) var sum = 0.0 for p in ps { sum += p.x } println("{sum} {ps.len()}") val shapes: [Drawable, unordered] = [Dot(1), Square(2), Dot(3)] for s in shapes { print("{s.draw()} ") } println("") }
4 2 dot1 dot3 square2
2.3.1 Borrowed views
View[T] is a read-only borrowed window over contiguous elements and
ViewMut[T] is writable. Bytes is View[UInt8]. A view is a two-word
value containing a pointer and a length; making or slicing one does not
copy its elements.
Use s.bytes() for a String, xs.view() for a list, fixed array or bounded
vector, and xs.view_mut() for a writable view of a var collection.
v.slice(start, end) checks 0 <= start <= end <= v.len() once. Indexing
and iteration use the same checked rules as lists. There is deliberately no
unchecked view access. The optimizer removes proven checks from ordinary
loops, and --report-bounds-checks lists those that remain.
Views have these methods:
| Method | Description |
|---|---|
len() -> Int, slice(a, b) -> Self |
length and checked subview |
index_of(x) -> Int |
first index, or -1 |
starts_with(v), ends_with(v), equals(v) |
comparison with a read-only view |
to_list() -> [T] |
copy the elements |
fill(x), copy_from(v) |
write a ViewMut; the source must fit |
as_view() -> View[T] |
read-only form of a ViewMut |
Bytes also has to_string() (a lossy UTF-8 copy) and
load_u64_le_or(i, fill), which reads up to eight little-endian bytes and
uses fill past the end. String.from_utf8(bytes) -> String? copies valid
UTF-8 and returns null for invalid bytes. A String, list, fixed array or
bounded vector is lent automatically to a compatible view parameter.
func middle(input: Bytes) -> Bytes { return input.slice(1, input.len() - 1) } func main() { val bytes = "hello".bytes() println("{middle(bytes).to_string()} {bytes[0]}") var xs = [1, 2, 3] val writable = xs.view_mut() writable[1] = 9 println("{xs[1]}") }
ell 104 9
A view cannot be stored in a field, global, list, map, nullable, tuple,
closure or detached task (E0369). A function may return a view only when
it has exactly one view, String or list parameter, and the result comes from
that parameter. While a view is live, its source cannot be reassigned,
resized, taken, passed to a var parameter or mutated directly (E0370);
writes go through ViewMut. Liveness ends at the view's last lexical use.
A temporary and a list or String reached through a heap, rc or pointer
handle cannot be borrowed.
2.4 Tuples
(T1, T2, ...) with at least two element types is a tuple type. A tuple
value is written (a, b). Tuples are used to return several values; they
are taken apart with a destructuring val (section 4.2) and have no other
operations (there is no positional access). Where a tuple type is expected (a
return, an argument, an annotated binding), each element of a tuple literal
converts to its component as it would to a binding of that type: a plain
T or null fills a T? component (return (null, "none") from a
function returning (Int?, String)). A tuple that holds a list, map or set
(also inside an object or an optional component) is taken apart only where
it is made, val (a, b) = f(): destructuring a binding, parameter, field or
element holding one would give the list a second owner, and is an error
like any implicit duplication (E0328, section 6.8).
func divmod(a: Int, b: Int) -> (Int, Int) { return (a / b, a % b) } func main() { val (q, r) = divmod(17, 5) val (_, only_r) = divmod(9, 4) println("{q} {r} {only_r}") }
3 2 1
2.5 Function types
func(T1, T2) -> R is the type of functions and lambdas taking T1 and
T2 and returning R; omit -> R for a function that returns nothing.
Function values are created from lambda expressions (section 3.7); they
can be stored in bindings, fields and lists and passed as arguments, and a
function type can be a type argument (Channel[func(Int) -> Int](4),
Map[String, func(Int) -> Int](), section 13). The
name of a top-level function is a function value too (val f = twice,
apply(twice, 4), xs.map(twice)).
2.6 Objects, enums and interfaces
Named types are declared with object (section 5.3), enum (5.6) and
interface (5.5). An object is a value type: assigning it or passing it by
value copies its fields. An interface type holds any object that implements
the interface and dispatches method calls to that object.
2.7 Pointer and lifecycle types
These types are part of the memory model and are described in section 6:
| Type | Meaning |
|---|---|
*T, *var T |
a checked pointer to a T (writable through *var T) |
?*T |
a nullable checked pointer |
*unchecked T, *var unchecked T |
an unchecked (raw) pointer for FFI and Box |
rc T |
a strong reference-counted handle to a shared T |
weak rc T, weak rc T? |
a weak handle that does not keep the T alive |
rc [T] |
a shared, reference-counted list |
Box[T] |
an owned heap cell that hands out unchecked pointers |
heap is not a type: a heap T(...) expression produces a value of type
T whose storage is on the heap and whose binding must be freed
(section 6.2). Writing heap in a type annotation is an error (E0273).
2.8 Generics
Functions, objects and enums may take type parameters in square brackets:
func largest[T](xs: [T]) -> T, object Pair[A, B],
enum Result[T, E]. A parameter may carry an interface constraint,
[T: Hashable], which every type argument must satisfy (E0206). Type
arguments are inferred from the arguments of a call or construction
(Pair(1, "one")) or written on the type (Stack[Int](),
val r: Result[Int, String]). A type argument written in an expression
may be any type: a function type, a tuple, an rc, weak rc or pointer
type (id[func(Int) -> Int](twice), Box[rc Node](n),
Stack[(Int, String)](), p.first[*Node](a, b)). Generic code is
instantiated per set of type arguments at compile time. A type parameter's
constraint also provides its static functions, called on the parameter
itself: T.parse(text) in func read[T: Parse](text: String) -> T?
(section 5.5).
func largest[T](xs: [T], less: func(T, T) -> Bool) -> T { var best = xs[0] for x in xs { if less(best, x) { best = x } } return best } object Pair[A, B] { val first: A val second: B } object Stack[T] { var items: [T] init() { self.items = [] } func push(x: T) { self.items.push(x) } func pop() -> T { return self.items.pop() } readonly func size() -> Int { return len(self.items) } } func main() { val big = largest([3, 9, 4], func(a: Int, b: Int) -> Bool { return a < b }) val p = Pair(1, "one") var st = Stack[Int]() st.push(1) st.push(2) println("{big} {p.first} {p.second} {st.pop()} {st.size()}") }
9 1 one 2 1
A generic enum value takes the type arguments its payload does not fix from
where it is used: return Maybe.Nothing in a function returning
Maybe[Int], an argument, an assignment or an annotated binding.
The same holds for a generic construction or call: type arguments that
neither are written nor follow from the arguments come from the expected
type, the type the value is checked against. That is the annotation of a
val or var, the target of an assignment, the parameter at a call
(functions, methods, constructors), the return type of the enclosing
function, the field in an object construction, or the element of a list
or tuple whose type is known; an rc or T? expected type gives its
inner type. This covers the library containers (Channel, Mutex,
RWLock, Box, Map, Set), generic objects and generic functions
whose type parameters appear in the return type. Written type arguments
and those the arguments fix are never replaced: when they disagree with
the expected type the program is rejected with a type mismatch naming
both (E0202).
object Stack[T] { var items: [T] init() { self.items = [] } } func empty[T]() -> [T] { return [] } func counts() -> Map[String, Int] { return Map() } func size(s: Set[Int]) -> Int { return s.len() } func main() { val ch: Channel[Int] = Channel(4) var seen: Set[String] = Set() val st: Stack[Int] = Stack() val xs: [Int] = empty() val shared: rc Map[String, Int] = rc Map() ch.send(len(xs) + len(st.items)) println("{ch.recv()} {seen.add("a")} {counts().len()} {size(Set())} {shared.len()}") }
0 true 0 0 0
2.9 Type conversions
Calling a primitive type name like a function converts a value:
Int(x), Float(x), Int32(x), UInt8(x) and so on. The source must be
a numeric type or Bool (E0310; Bool itself is not a conversion
target); float to integer truncates toward zero, and Int(true) is 1. A
float outside the target's range converts to the type's largest or
smallest value and NaN converts to 0 (Int8(x) with x = 300.5 is 127).
A conversion to a narrower integer type warns (W0601) and a constant that
does not fit, integer or float, is an error (E0311). Numbers are converted to strings with
n.to_string() or by interpolation; strings are parsed with s.to_int()
and s.to_float(). There is no String(x).
func main() { val f: Float = 3.99 val n = Int(f) val small: Int8 = 100 val u: UInt8 = 250 val total = Int(small) + Int(u) println("{n} {Int(-3.99)} {Int(true)} {Float(n) / 2.0} {total}") println("{"42".to_int() + 1} {"2.5".to_float() * 2.0} {n.to_string()}") }
3 -3 1 1.5 350 43 5 3
3. Expressions
3.1 Operators and precedence
Binary operators are left associative. From lowest to highest precedence:
| Level | Operators |
|---|---|
| 1 | is |
| 2 | or |
| 3 | and |
| 4 | \| |
| 5 | ^ |
| 6 | & |
| 7 | == != |
| 8 | < > <= >= |
| 9 | << >> |
| 10 | + - |
| 11 | * / % |
| 12 | unary - not ~ & heap rc weak rc comptime await |
| 13 | postfix .name [i] [a..b] (args) |
is binds loosest of all: not s is Circle parses as (not s) is Circle
and is a type error; write not (s is Circle). Comparison binds tighter
than equality, so 1 <= 2 == true is (1 <= 2) == true. and and or
short-circuit.
func main() { println("{2 + 3 * 4 - 8 / 2 % 3}") println("{1 << 4 | 1} {6 & 3 ^ 1} {~0} {-7 / 2} {-7 % 3}") println("{1 < 2 and 2 < 3 or false} {not true} {1 <= 2 == true}") }
13 17 3 -1 -3 -1 true false true
3.2 Arithmetic, comparison, logical and bitwise operators
+ - * / % apply to two operands of the same numeric type. + also
concatenates two strings. == != compare numbers, booleans, strings (by
content), enum values and null. Two enum values are equal when they are
the same variant with equal payloads; an enum with an object or list in a
payload compares only against a unit variant (shape == Shape.Empty).
Objects and lists have no ==: compare their fields or elements.
< > <= >= compare numbers, and strings in code point order. and,
or, not take Bool operands. & | ^ ~ << >> take integer operands
(E0233) of any width; & | ^ need both of one type, and a shift has the
type of its left operand. A shift by the operand's width or more gives 0
for << and the sign (0 or -1) for >>; a shift by a negative count is a
panic. Compound assignments += -= *= /= &= |= ^= <<= >>= are
statements (section 4.3).
3.3 String interpolation and concatenation
Inside a string literal, {e} evaluates e and inserts its text. Any
value that has a text form may be interpolated: numbers, booleans,
strings, enums (the variant name), objects with a
readonly func to_string() -> String method (through that method, also
behind rc), the nullable forms of all of these (null when there is no
value; for an object T? or rc T?, to_string() runs only when there is
a value), and the results of calls and field accesses. Interpolating a
value with no text form, such as an object without to_string(), is an
error (E0334). Floats print the shortest
decimal that reads back to the same value, without a trailing .0
(3.0 prints as 3). Two strings are joined with +, and s += t
appends to a var string.
func main() { val f = 3.0 val name = "Iron" val s = "a" + "b" + "c" println("{name}: {f} {f / 4.0} {s} {s == "abc"} {true}") }
Iron: 3 0.75 abc true true
3.4 Calls, methods, fields and indexing
f(a, b) calls a function or a function value; arguments are positional
(E0101 names named arguments as unsupported) and a trailing comma is
allowed. x.field reads a field, x.method(args) calls a method,
Type.method(args) calls a named init, a static function (section 5.3) or
a standard library function, and
Type(args) constructs an object (section 5.3). xs[i] indexes a list,
array or string (a string index yields a one-character string), and
xs[a..b] takes the characters a (inclusive) to b (exclusive) of a
string; xs[a..] runs to the end. On a list, xs[a..b] and xs[a..]
return a new list of those elements.
A pointer, rc handle, heap value or Box cell is accessed with the
same . syntax as the value it refers to (auto-dereference); there is no
prefix * operator.
3.5 List and tuple literals
[a, b, c] is a list literal (a trailing comma is allowed and elements may
be spread over lines); [T; n] is a typed array literal used to declare
fixed arrays; (a, b) with two or more elements is a tuple literal.
3.6 Enum values
Color.Red names a unit variant and Shape.Circle(1.0) constructs a
variant with a payload. Enum values are compared with == and taken apart
with match (section 4.7).
3.7 Lambdas and closures
func(params) [-> T] { body } in expression position is a lambda. When
the lambda is passed directly to a parameter of function type, its
parameter and return types are taken from that parameter and may be
omitted; everywhere else the parameter types must be written (E0324) and
a lambda without -> T returns nothing, whatever its body does. A lambda
may refer to bindings of the enclosing function. A
val binding is captured by value. A var binding that a lambda
captures moves into a reference counted cell shared by the enclosing
function and every closure that captured it: mutations inside the lambda
are visible outside, and the closure may outlive the function that
declared the var (a counter closure keeps its count). A lambda may
capture a list only when it is passed directly as a call argument
(E0328 otherwise); to share a list with a stored closure use rc [T].
A nocopy value follows the same borrow rule (6.7): a lambda may capture
one only when it is passed directly as an argument to a callee that just
calls it during the call; it then reads the caller's binding, and the
value is dropped once, by that binding (E0286 otherwise). A callee
keeps the closure, and the capture is rejected, when it stores the
parameter, returns it, hands it to a callee that keeps it, or is a
constructor, push, insert or a function without an Iron body; the
list methods map, filter, reduce and forEach only call it.
The environment of a closure is reference counted, so a closure may be
returned, stored in a field or list, and copied. A recursive lambda is
written by assigning it to the var it calls through
(fact = func(n: Int) -> Int { ... fact(n - 1) }); the cell and the
closure then refer to each other, and the runtime frees the pair once
nothing else reaches either. Two closures that call each other through
two such vars are not reclaimed.
func make_adder(n: Int) -> func(Int) -> Int { return func(x: Int) -> Int { return x + n } } func apply_twice(f: func(Int) -> Int, x: Int) -> Int { return f(f(x)) } object Button { val on_click: func(Int) -> String } func main() { val add5 = make_adder(5) println("{add5(1)} {apply_twice(add5, 0)}") println("{apply_twice(func(x) { return x * 3 }, 2)}") val handlers: [func(Int) -> Int] = [add5, add5] println("{handlers[0](4)}") val b = Button(func(n: Int) -> String { return "clicked {n}" }) println(b.on_click(3)) var local = 1 val bump = func() { local += 10 } bump() println("{local}") val xs = [1, 2, 3] val ys = [2] val common = xs.filter(func(x: Int) -> Bool { return ys.contains(x) }) println("{common.len()}") }
6 10 18 9 clicked 3 11 1
3.8 Allocation expressions
heap T(args), heap(in: arena) T(args), rc T(args), rc [a, b],
weak rc null and x.downgrade() create values with an explicit
lifecycle; they are described in section 6. heap and rc may only
appear in front of an allocation expression, not in a type annotation, a
parameter or a binding keyword position (E0273, E0297); the reserved
pool keyword is rejected with E0298. heap requires an object or
collection allocation; heap of a number or Bool is a type mismatch
(E0202).
3.9 Type tests
x is T is true when the interface value x currently holds an object
of type T. Inside the if block guarded by x is T (and after an early
return in the other branch), x is narrowed and the fields and methods of
T are available. The operand must be an interface or object value
(E0322).
interface Shape { readonly func area() -> Float } object Circle impl Shape { val r: Float readonly func area() -> Float { return 3.0 * self.r * self.r } } object Square impl Shape { val side: Float readonly func area() -> Float { return self.side * self.side } } func describe(s: Shape) -> String { if s is Circle { return "circle of radius {s.r}" } if not (s is Square) { return "unknown" } return "square with area {s.area()}" } func main() { println(describe(Circle(1.0))) println(describe(Square(2.0))) }
circle of radius 1 square with area 4
3.10 comptime, spawn and await
comptime e evaluates e at compile time (section 8). spawn("name") {
body } starts a thread and yields a handle; await h waits for it and
yields the value the body returned (section 7).
4. Statements
4.1 Blocks and scope
A block is { statement* }. Blocks are the bodies of functions, methods,
control statements and lambdas, and a bare block may appear as a statement.
A binding is visible from its declaration to the end of the enclosing block.
Stack values declared in a block are destroyed when the block exits, in
reverse declaration order (section 6.1). A binding may shadow a binding of
an enclosing scope, except that a match pattern binding may not shadow an
existing name (E0227).
4.2 Bindings
val name = expr declares an immutable binding and var name = expr a
mutable one. Either may carry a type (val speed: Float = 2.5), which is
required when the initializer does not determine it (val xs: [Int] = [],
val h = Channel[Int](4)) and lets a literal take another
numeric type (val small: Int8 = 100). A var binding may be declared
without an initializer and assigned later; reading it before every path has
assigned it is an error (E0314).
A val cannot be reassigned (E0203), have a field written (E0234) or
have a mutating method called on it (E0235); lists follow the same rule
for their mutating methods and index writes. _ as a binding name
evaluates the initializer and discards it.
val (a, b) = expr destructures a tuple; each position is a name or _,
at least two positions are required, an initializer is mandatory and a type
may be written after the pattern (val (a, b): (Int, Int) = pair()).
The initializer of a val or var may be a spawn expression (section
7.1); spawn cannot appear anywhere else inside an expression.
4.3 Assignment
target = expr stores into a var binding, a field of a var object or
of a var parameter, an element xs[i] of a var list, or a field
reached through a pointer, rc handle or heap binding. The compound
forms += -= *= /= &= |= ^= <<= >>= compute target op expr and store
it; they are statements, not expressions, and are only defined for numeric
operands. Assignment to a val parameter is an error (E0266); declare
the parameter var to mutate it (section 5.2).
4.4 if
if cond { ... } elif cond { ... } else { ... }Conditions must be Bool; the braces are mandatory; elif and else
are optional. if x is T narrows x inside the block (section 3.9).
4.5 while
while cond { ... } repeats its block while the condition holds.
break leaves the innermost while or for, and continue starts its next
iteration (for a for over range(n), with the next number). Both first run
the defers and drop the values of every scope they leave inside the loop
body. They apply to the loop of the same function body: one inside a
lambda, a defer or a spawn block within the loop, or outside any loop, is
an error, and so is one in a parallel loop, whose iterations run
independently (E0335). A match arm may be a break or continue.
func main() { var found = -1 for x in [4, 7, -2, 9, -5] { if x > 0 { continue } found = x break } var n = 0 while true { n += 1 if n == 3 { break } } println("{found} {n}") }
-2 3
4.6 for
for name in iterable { ... } binds name to each element in turn. The
iterable may be a list, fixed array, bounded vector or rc [T] (elements),
a string (one-character strings), a Set[T] (items) or range(n) (the
integers 0 to n - 1); a Map[K, V] is iterated with two names, for
(key, value) in m (section 9.10). The loop variable is immutable inside
the body. range takes exactly one argument and is written only as the
iterable of a for loop; a loop does not iterate over an integer (for i
in 10 is an error: write for i in range(10)), nor over any other value
(E0202). Appending parallel after the iterable runs the
iterations on several threads (section 7.4).
A loop over a growable list reads the elements in place, so the list must
keep them where they are until the loop is done. The body may assign to
elements (xs[i] = v) and change other lists, but adding to the iterated
list, removing from it or replacing it in the body is an error (E0346):
collect the changes and apply them after the loop, loop over xs.copy(),
or use a while loop over indices. Where the compiler cannot see the
change (a function or closure the body calls, another rc handle, a list
reached through a field), the list is pinned while the loop runs: growing
it past its capacity, removing, inserting, popping or clearing elements,
or freeing it aborts with list changed while a pointer to one of its
elements is in use. The pin ends on every way out of the loop.
func main() { var total = 0 for n in [1, 2, 3] { total += n } for i in range(3) { total += i } var k = 0 while k < 3 { k += 1 } for c in "héy" { print("[{c}]") } println("") if total > 100 { println("big") } elif total > 5 { println("medium: {total} {k}") } else { println("small") } }
[h][é][y] medium: 9 3
4.7 match
match subject {
pattern -> statement
pattern -> { statements }
else -> statement
}The subject must be an enum value, an integer, or an interface value
(E0323; String, Bool and Float subjects are rejected). Each arm is
a pattern, ->, and either a single statement or a block. The optional
else arm must come last. match is a statement; arms usually return or
print.
Patterns:
Enum.Variantmatches a unit variant;Enum.Variant(a, b)matches a payload variant and binds its fields to new immutable names._skips a field. TheEnum.qualifier may be omitted when the variant name is unambiguous. A pattern may not bind a name that already exists in scope (E0227), and the number of bindings must equal the payload arity (E0225).- An integer literal (or constant expression) matches that value of an
integer subject; an
elsearm is then required (E0224). - For an interface subject,
Type(name)matches when the value holds aTypeand binds it asname, giving access to the object's fields.
A match on an enum or interface must cover every variant or implementor,
or have an else arm (E0224); an arm that can never match is an error
(E0226). A nested payload pattern such as Outer.Some(Inner.Circle(r)) is
rejected (E0332) when the inner enum has more than one variant, since the
inner pattern is not tested at run time: bind the payload and match it in a
second match.
enum Shape { Circle(Float), Rect(Float, Float), Empty, } interface Animal { readonly func sound() -> String } object Dog impl Animal { val name: String readonly func sound() -> String { return "woof" } } object Cat impl Animal { val lives: Int readonly func sound() -> String { return "meow" } } func area(s: Shape) -> Float { match s { Shape.Circle(r) -> return 3.0 * r * r Shape.Rect(w, h) -> return w * h Shape.Empty -> return 0.0 } return 0.0 } func size(n: Int) -> String { match n { 0 -> return "zero" 1 -> return "one" else -> return "many" } return "unreachable" } func main() { println("{area(Shape.Rect(2.0, 3.0))} {area(Shape.Empty)} {area(Shape.Circle(1.0))}") println("{size(1)} {size(7)}") val pets: [Animal] = [Dog("Rex"), Cat(9)] for a in pets { match a { Dog(d) -> println("{d.name} says {d.sound()}") Cat(c) -> { val lives = c.lives println("cat with {lives} lives says {c.sound()}") } } } }
6 0 3 one many Rex says woof cat with 9 lives says meow
Generic enums work the same way, with the two extra steps noted in section 2.8:
enum Result[T, E] { Ok(T), Err(E), } func parse_positive(n: Int) -> Result[Int, String] { if n > 0 { val ok: Result[Int, String] = Result.Ok(n) return ok } val err: Result[Int, String] = Result.Err("not positive") return err } func main() { val r: Result[Int, String] = parse_positive(-1) match r { Result.Ok(v) -> { val n: Int = v println("ok {n}") } Result.Err(e) -> { val msg: String = e println("error: {msg}") } } }
error: not positive
4.8 return
return expr leaves the function with a value; return alone leaves a
function without a result type. Every path of a function with a result type
must return (E0293), and the value must have the declared type (E0215).
A return inside an init may not carry a value (E0252) and may not
leave fields unassigned (E0250).
4.9 defer
defer statement and defer { block } schedule the statement to run when
the enclosing block exits, whether by falling off its end or by return.
Deferred statements run in reverse order of registration, after the
block's own statements and before its stack values are destroyed. Inside a
loop body, deferred statements run at the end of each iteration. The most
common form is defer free x for a heap value (section 6.2). A deferred
body runs while its block exits and cannot leave it: return inside it is
an error (E0336), as are break and continue (E0335). A lambda
inside a deferred body may return from itself.
func main() { { defer { println("cleanup 2") } defer println("cleanup 1") println("body") } var i = 0 while i < 2 { defer println("end of iteration {i}") i += 1 } println("done") }
body cleanup 1 cleanup 2 end of iteration 1 end of iteration 2 done
4.10 free and leak
free x releases the heap value bound to x and leak x marks it as
intentionally never freed; both are described in section 6.2.
4.11 in arena { ... }
in a { ... } makes a (an Arena) the default allocator for every
heap expression inside the block (section 6.6).
4.12 Expression statements
A call, method call or spawn expression may stand as a statement. Any
other expression on its own is accepted by the parser but has no effect.
5. Declarations
A program is a sequence of top-level declarations: imports, functions,
objects, patches, interfaces, enums and global bindings. Declarations may
appear in any order and are visible throughout the file. Most become visible
throughout the package when pub; section 5.10 records the current exceptions.
5.1 The entry point
A binary program has exactly one main, with no result type, written
either func main() or func main(args: [String]) to receive the
command-line arguments after the program name (UTF-8, iron run file.iron
-- a b passes a and b); any other parameter list is an error
(E0202).
Execution starts there and the process exits with status 0 when main
returns. A run-time failure (index out of range, division by
zero, failed assert, stale pointer) prints a message to standard error and
aborts.
func main() { val samples = [10, 20, 30, 40, 50] var total: Int = 0 for sample in samples { total += sample } println("processed={len(samples)} total={total}") }
processed=5 total=150
5.2 Functions
func name[T, U](p1: T1, var p2: T2) -> R { body }Parameters are val by default: a parameter cannot be reassigned or
mutated (E0266, E0234). A parameter declared var is a copy that the
body may assign to and mutate; when the function returns, the final value
is written back to the caller's var binding, so var parameters behave
like in-out arguments; the same holds for a var parameter of a method or
an init. The argument is a var binding, or a mutable field or list
element of one (E0267 for anything read-only). The original place stays
where it is for the call and receives the final value: the list it is in is
pinned and the heap, arena or pointed-to value it is in is kept alive, so
changing that list, or freeing or resetting that value, aborts while the
call runs (section 6.4). A list parameter is lent (never copied); a var
list parameter lets the body grow or shrink the caller's list. An object
parameter is lent for the call; it is not copied and its copy or drop
block does not run in the callee. The result type follows
->; omit it for a function that
returns nothing. Argument count and types must match exactly (E0216,
E0217). Functions cannot be overloaded (E0201) and there are no default
or named arguments. Type parameters are written after the name (section
2.8).
object Point { var x: Int init(x: Int) { self.x = x } func bump() { self.x += 1 } } func by_value(p: Point) -> Int { return p.x + 100 } func by_var(var p: Point) { p.x = 99 p.bump() } func set_seven(var n: Int) { n = 7 } func grow(var xs: [Int]) { xs.push(4) } func main() { var a = Point(1) println("{by_value(a)} {a.x}") by_var(a) var n = 1 set_seven(n) var xs = [1, 2, 3] grow(xs) println("{a.x} {n} {xs.len()}") }
101 1 100 7 4
A top-level function name is a function value: val f = twice,
apply(twice, 4) and [twice] work like the equivalent lambdas.
Compiler built-ins such as print, println, len, range and fill
are calls, not values: naming one without calling it is E0202.
Compiler hints on the line directly above a function are
//iron:fusible, //iron:inline, //iron:noinline and //iron:cold.
They respectively allow list-loop fusion, require inlining, prevent
inlining, and put an unlikely slow function on the backend's cold path.
inline and noinline together are an error; recursive inline warns
with W0631 because it cannot be honored everywhere. The older @fusible
means //iron:fusible and is accepted while code migrates (section 1.2).
The compiler hint //iron:fusible on the line directly above a func or
a method is a checked claim: the function can run inside a fused list loop
(section 9.3.2), which means that it has no observable effect and cannot
panic. The compiler verifies the claim when it builds the program and
reports warning W0618, with the reason, for a function that does not keep
it. The hint changes nothing else: fusion is decided by the same proof for
every callback, hinted or not, and a program prints the same with its hints
removed. The older @fusible before func means the same and is accepted
while code migrates (section 1.2).
5.3 Objects
object Name[T] impl Interface1, Interface2 {
val immutable_field: Type
var mutable_field: Type
pub var exported_field: Type
init(params) { self.field = ... }
init named(params) { ... }
func mutating(...) -> R { ... }
readonly func observer(...) -> R { ... }
pure func computation(...) -> R { ... }
copy { ... }
drop { ... }
}An object is a value type made of named fields. Every field is declared
with val or var and a type (E0176); a field cannot have an inline
default value (E0262). An object is constructed by calling its name with
the arguments of its anonymous init: Name(args). When an object declares
no init, it must have only val fields (E0264) and is constructed
positionally, one argument per field in declaration order.
Initializers. init(params) { ... } is the anonymous initializer and
init name(params) { ... } a named one, called as Name.name(args). The
body assigns every field through self exactly once for val fields
(E0248) and at least once for var fields on every path (E0247); it
may not read a field before assigning it (E0246), call a method on the
partially built self (E0249), return early (E0250), delegate to
another init (E0251) or return a value (E0252). An object has at
most one anonymous init (E0201). Name.init(args) is an explicit
spelling of Name(args).
//iron:align=N directly above an object, field, local val or local
var raises its alignment to a power of two from 1 through 4096.
//iron:align=cacheline means 64 bytes, or 128 on Apple arm64. An alignment
below the natural alignment is E0368. Stack values, fields, fixed arrays
and list element storage honor over-alignment. Over-aligned heap, rc, arena,
interface, closure and spawned-task storage is not implemented yet and is
also E0368; it is deferred until the generation-table allocator work is
merged.
Methods are declared inside the object body with func, take an
implicit receiver self of the object type, and are called as
value.method(args). A method has one of three tiers:
| Tier | May write self |
May call | I/O |
|---|---|---|---|
func (default, mutating) |
yes | anything | yes |
readonly func |
no (E0238) |
readonly and pure methods (E0239) |
no (E0278) |
pure func |
no (E0244) |
pure methods only (E0242) |
no (E0240) |
readonly and pure methods may be called on val bindings; a mutating
method needs a var binding (E0235). A pure method may also not write
its parameters (E0243) or read a global var (E0241). The modifiers
are only valid inside object, patch and interface bodies (E0245), and
init takes none.
Static functions. static func name(params) -> R { body } declares a
function of the type rather than of a value: it has no receiver (self in
its body is E0210), takes no tier modifier (E0245), and is called on the
type, Name.name(args). Calling it on a value is E0350; calling a method
through the type name, Name.method(args), is E0351, because there is no
value for self. A static function may be pub, may take type parameters,
and may be added by a patch object. A generic object cannot declare one
(E0245): no value fixes its type arguments. Two members of one name,
methods or static functions, are E0201.
self and Self. Inside a method self is the receiver;
outside a method it is an error (E0210). The identifier Self names the
enclosing object type: it may be used as a type anywhere in a method or
static function (Self, Self?, [Self]), and Self(args) and
Self.f(args) construct and call through the enclosing type.
Visibility. Fields and methods are private to the file that declares
the object by default. pub on a field synthesizes a getter named after the
field and, for pub var, a setter set_<field>; call sites keep using
obj.field and obj.field = v, and a user method with one of those names
is an error (E0237). pub on a method or init exports it; pub init
is only allowed inside a pub object. See section 5.10 for cross-file
rules.
copy and drop blocks run when a value is copied or destroyed
(section 6.7); nocopy object forbids copying (section 6.7).
import math object Vec2 { val x: Float val y: Float init(x: Float, y: Float) { self.x = x self.y = y } init zero() { self.x = 0.0 self.y = 0.0 } static func unit_x() -> Self { return Vec2(1.0, 0.0) } readonly func length() -> Float { return Math.sqrt(self.x * self.x + self.y * self.y) } readonly func scaled(k: Float) -> Self { return Vec2(self.x * k, self.y * k) } } object Account { pub var balance: Int pub val id: Int var history: Int init(id: Int) { self.id = id self.balance = 0 self.history = 0 } func deposit(amount: Int) { self.balance += amount self.history += 1 } pure func fee(amount: Int) -> Int { return amount / 100 } } object Point { val x: Int val y: Int } func main() { val v = Vec2(3.0, 4.0) val origin = Vec2.zero() val p = Point(1, 2) println("{v.length()} {v.scaled(2.0).x} {origin.y} {Vec2.unit_x().x} {p.x},{p.y}") var acc = Account(7) acc.deposit(50) acc.balance = 60 acc.set_balance(acc.balance + 5) println("{acc.id} {acc.balance} {acc.fee(250)}") var copy_of_acc = acc copy_of_acc.deposit(1) println("{acc.balance} {copy_of_acc.balance}") }
5 6 0 1 1,2 7 65 2 65 66
5.4 patch object
patch object Name { ... } adds methods and initializers to an existing
object declared in the same package or in the standard library. A patch may
declare impl Interface to make the type conform; it may not add fields
(E0253), patch an unknown type (E0254), redefine an existing method
(E0255) or take type parameters. Methods added by a patch have the same
tiers and visibility rules as methods declared in the object body.
object Celsius { val degrees: Float } interface Describable { readonly func describe() -> String } patch object Celsius impl Describable { readonly func describe() -> String { return "{self.degrees} C" } init freezing() { self.degrees = 0.0 } } patch object String { readonly func shout() -> String { return self.upper() + "!" } } func main() { val c = Celsius.freezing() println("{c.describe()} {"hey".shout()}") }
0 C HEY!
5.5 Interfaces
interface Name {
readonly func required() -> R
pure func also_required(x: T) -> R
func with_default() -> R { body }
static func make(x: T) -> Self?
static func with_default_too() -> R { body }
}An interface lists method signatures, each with a tier modifier. An object
conforms by naming the interface after impl and defining every listed
method without a default body (E0205) with a tier at least as strict as
the interface's (E0257; a pure method satisfies a readonly
signature). A signature with a body is a default: an implementor that does
not define the method inherits it, and may override it. Interfaces may not
declare init (E0256), and the keyword is impl, not implements. An
interface may have any number of implementors; a value of interface type
can be built from any of them and dispatches calls at run time. Interfaces
may be the element type of lists and the type of fields and parameters,
and are the subjects of is (section 3.9) and type match (section 4.7).
A var binding of interface type mutates the object it holds in place.
The implementing function must have the signature's shape: the same
parameters (types and var) and result (E0353), and static exactly
where the interface says static (E0352). In a signature Self stands
for the implementing type, anywhere a type can be written:
readonly func same(o: Self) -> Bool is implemented as
readonly func same(o: Point) -> Bool (or with Self again). A default
body may use Self the same way (Self(args), Self.make(args),
var out: [Self] = []); it is copied into each implementor with Self
meaning that implementor.
Static functions in an interface belong to the implementing types:
static func parse(text: String) -> Self? is implemented by a
static func parse in each implementor (or by the default body). A static
function is called on a type: on an implementor (Point.parse(t)), or in a
generic function on a type parameter constrained by the interface,
T.parse(t), which each instance turns into a direct call. Calling a name
on a type parameter that its constraint does not declare as a static
function is E0355.
A value of interface type dispatches at run time on the object it holds, so
only methods it can dispatch are callable through it: a static function, a
method with a parameter of type Self (the value may hold a different
implementor than the argument) and a method whose result is built from
Self other than a plain -> Self are E0354 there. They are callable on
the concrete type or through a constrained type parameter, which is how
such interfaces are meant to be used:
interface Parse { static func parse(text: String) -> Self? readonly func same(o: Self) -> Bool static func label() -> String { return "parsed" } } object Pt impl Parse { val x: Int val y: Int static func parse(text: String) -> Self? { val parts = text.split(",") if parts.len() != 2 { return null } return Pt(parts[0].len(), parts[1].len()) } readonly func same(o: Pt) -> Bool { return o.x == self.x and o.y == self.y } } func parse_all[T: Parse](texts: [String]) -> [T] { var out: [T] = [] for t in texts { val v = T.parse(t) if v != null { out.push(v) } } return out } func main() { val pts = parse_all[Pt](["a,bb", "nope", "ccc,d"]) println("{pts.len()} {pts[0].same(Pt(1, 2))} {Pt.label()}") }
2 true parsed
interface Shape { readonly func area() -> Float readonly func name() -> String { return "shape" } } object Square impl Shape { val side: Float readonly func area() -> Float { return self.side * self.side } } object Circle impl Shape { val r: Float readonly func area() -> Float { return 3.0 * self.r * self.r } readonly func name() -> String { return "circle" } } func total(shapes: [Shape]) -> Float { var t = 0.0 for s in shapes { t += s.area() } return t } func main() { val shapes: [Shape] = [Square(2.0), Circle(1.0)] println("{total(shapes)}") for s in shapes { println("{s.name()} {s.area()}") } }
7 shape 4 circle 3
Hashable is the one interface the standard library defines
(pure func hash() -> Int and pure func equals(other: Hashable) -> Bool);
the integer types, Bool and String satisfy it without declaring impl.
5.6 Enums
enum Name[T] {
Unit,
WithValue = 5,
Payload(T, Int),
}An enum lists variants separated by commas (a trailing comma is allowed).
A variant may carry a payload of one or more types, or an explicit integer
value (= 5; a variant without one takes the previous variant's value
plus one, the first 0). The value is what a variant of an enum without
payloads is at run time, and what C receives through a binding
(KeyboardKey.SPACE is 32). Unit variants are written Name.Variant, payload variants
are constructed with Name.Variant(args). Enum values are compared with
== and inspected with match. Enums have no methods and cannot be
converted to integers; put behavior in functions that take the enum. Enums
may be generic, and a payload may be the enum type itself (recursive
enums).
enum Color { Red, Green = 5, Blue, } enum Token { Number(Int), Word(String), End, } func show(t: Token) -> String { match t { Token.Number(n) -> return "number {n}" Token.Word(w) -> return "word {w}" Token.End -> return "end" } return "" } func main() { val c = Color.Green if c == Color.Green and c != Color.Blue { println("green") } println("{show(Token.Number(4))} {show(Token.Word("hi"))} {show(Token.End)}") }
green number 4 word hi end
5.7 Global bindings
val and var declarations may appear at the top level. A global val
is a constant initialized before main runs (its initializer may call
functions); a global var is a mutable variable shared by the whole
program (and by all threads, without synchronization). Like every other
top-level declaration a global is private to its file; pub val and
pub var share it with the package's other files (E0320 otherwise). A
pure method may not read a global var (E0241).
Like every declaration, a global is visible in the whole file, also above
its declaration. Globals are initialized before main, every one of them
whether or not anything reads it, and each after the globals its
initializer reads: directly, or through the functions, methods and
initializers it calls. Globals that do not depend on each other are
initialized in declaration order; in a package that is the order iron
compiles its files (section 10.2). A global whose initializer depends on
the global itself, such as val a = b + 1 and val b = a, or val a = f()
with f reading a, is an initialization cycle (E0345).
val AREA = WIDTH * HEIGHT val WIDTH = 4 val HEIGHT = WIDTH + 1 func main() { println("{AREA} {WIDTH} {HEIGHT}") }
20 4 5
val LIMIT = 10 val ANSWER = compute() var counter = 0 func compute() -> Int { return 6 * 7 } func bump() { counter += 1 } func main() { bump() bump() println("{LIMIT} {ANSWER} {counter}") }
10 42 2
5.8 extern func
extern func name(params) -> R declares a C function without a body. The
call compiles to a direct C call; the C name is the Iron name with each
snake_case segment capitalized and the underscores removed
(init_window becomes InitWindow), and a name without underscores is
used unchanged. Only functions whose declarations the generated C already
includes (the C standard library, and raylib when it is imported) can be
called; there is no way to name a header. Int maps to int64_t,
Float to double, Bool to bool, String to the runtime string
struct and *unchecked T to a raw pointer. Declaring an extern with the
name of an Iron built-in is a duplicate declaration (E0201).
extern func labs(x: Int) -> Int extern func srand(seed: Int) extern func rand() -> Int func main() { srand(1) val r = rand() println("{labs(-9)} {r >= 0}") }
9 true
5.9 Imports
import name makes a standard library module available (section 9), or
documents a dependency on a source file of the package: import util
matches src/util.iron or any file under a directory named util, and a
dotted path import a.b matches consecutive directory components. Importing
an unknown module is an error (E0209); an unused import warns (W0611).
import x as y is rejected: Iron has no module namespaces, so imported
declarations are used by their own names.
The modules that require an import are math, io, time, log,
hint, os, atomic, net, http, websocket, url, json, crypto,
encoding, mysql and raylib; their functions are
called on the capitalized module object (import math, then
Math.sqrt(x)). Everything else in section 9 (strings, lists, Box,
Arena, Channel, Mutex, RWLock, FileHandle, Hashable, RawPtr)
is available without an import.
5.10 Visibility
Functions, objects, patches, enums and global bindings are private to their
file unless marked pub; global bindings use pub val and pub var.
Interfaces are currently visible package-wide even without pub, while an
extern function stays file-private even when written pub extern func
(#842). Using any other
private declaration from another file of the package is an error (E0320). Object members are private to the declaring
file unless marked pub (section 5.3). The word private is not accepted
(E0101). Visibility does not make names local: all files of a package
share one namespace, so two files may not declare the same top-level name,
whether or not either is pub (E0201).
6. Memory
Iron has no garbage collector. Every value has one of five lifecycles,
chosen at the point where the value is created: stack (the default),
heap, rc, weak rc, and arena. Pointers (*T) and Box[T] refer to
values without owning them. The compiler and runtime check the common
mistakes: freeing a non-heap value, forgetting to free, escaping a heap
value, dereferencing a stale checked pointer.
6.1 Stack values
A binding initialized with a plain expression holds its value directly.
Objects are copied on assignment and when stored in lists or fields; each
copy is destroyed independently. Passing an object to an ordinary parameter
lends it for the call, while a var parameter writes back to its original
place (section 5.2). A value is destroyed
when its block exits (in reverse declaration order) or, for a temporary,
as soon as the expression that used it is done: a field read off a
temporary (make().name) copies the field out and then drops the
temporary. Destruction runs the object's drop block, if any (section
6.7), then the drops of its fields in reverse declaration order.
object Res { val id: Int drop { println("drop {self.id}") } } func main() { val a = Res(1) val b = Res(2) { val c = Res(3) println("inner") } println("end of main") }
inner drop 3 end of main drop 2 drop 1
6.2 heap, free and leak
heap T(args) allocates the object on the heap and yields a binding that
is used exactly like a stack value (fields through ., mutation only when
the binding is var). The program must release it with free x, usually
scheduled with defer free x, or declare that it is never released with
leak x. Heap values are not freed automatically: a heap binding that is
neither freed nor leaked warns (W0606) and the memory and the drop
block are lost. A heap value cannot leave the function that allocated it:
returning it or storing it where it outlives the scope is an error
(E0207); to share ownership use rc. free and leak apply only to
heap bindings (E0212, E0213, E0274, E0275), and an rc handle
cannot be leaked (E0214). Freeing runs drop. Using a value after it was
freed, including a second free, is caught at run time by a generation
check and aborts the program with a "stale pointer dereference" message.
object World { var seed: Int init(seed: Int) { self.seed = seed } drop { println("drop world {self.seed}") } } func main() { var world = heap World(42) defer free world world.seed = 43 println("seed {world.seed}") val forever = heap World(7) leak forever println("leaked {forever.seed}") }
seed 43 leaked 7 drop world 43
object Node { val value: Int } func make() -> Node { val n = heap Node(1) return n } func main() { val m = make() }
Inside in arena { ... } (section 6.6) heap allocates from the arena
instead and needs no free.
6.3 rc and weak rc
rc T(args) allocates a shared, reference-counted object and yields a
handle of type rc T. Copying the handle (assignment, passing, storing in
a field or list, capturing in a closure) increments the count; destroying a
copy decrements it, and when the last handle goes away the object's drop
runs and the memory is released. All handles see the same object, and a
field may be written, or a method that changes the object called, through
any handle, including a val one. Handles
are not nullable (?rc T is rejected, E0297) and cannot be leaked
(E0214). The type rc T may be used for fields, parameters and results.
weak rc T is a handle that does not keep the object alive. It is obtained
with handle.downgrade() on an rc value or written as the constant
weak rc null, and turned back into a usable handle with w.upgrade(),
which yields a nullable strong handle, written rc T?, that is null when
the object has already been destroyed. A weak handle cannot be
dereferenced directly (E0299). Weak handles break reference cycles, for
example a child that points back to its parent.
rc [a, b, c] is a shared list: every copy of the handle reaches the same
list, it can be grown and indexed through any copy, and it is freed with the
last copy. It is the way to share a list with a closure or between fields.
object Player { val name: String drop { println("drop {self.name}") } } object Owner { val name: String var pet: weak rc Pet init(name: String) { self.name = name self.pet = weak rc null } } object Pet { val owner: rc Owner val nick: String } func make_counter() -> func() -> Int { val seen: rc [Int] = rc [1, 2, 3] return func() -> Int { return len(seen) } } func main() { val strong = rc Player("Alice") val weak_ref = strong.downgrade() { val another = strong val maybe = weak_ref.upgrade() if maybe != null { println("alive: {maybe.name}") } } val owner = rc Owner("Ann") val pet = rc Pet(owner, "Rex") owner.pet = pet.downgrade() val link: weak rc Pet = owner.pet val back = link.upgrade() if back != null { println("{back.owner.name} owns {back.nick}") } val shared: rc [Int] = rc [1, 2] val alias = shared alias.push(3) println("{shared.len()} {shared[2]} {make_counter()()}") }
alive: Alice Ann owns Rex 3 3 3 drop Alice
6.4 Checked pointers
&x takes the address of a binding, field or fixed array element and
yields a checked pointer: *T when x is a val and *var T when x
is a var. ?*T is a nullable pointer that may hold null. Fields are
read and, through *var T, written with p.field, and methods are called
with p.m(), also a String or number method on a pointer to one and a
method of an interface value through a pointer to it; a
pointer to a primitive prints its pointee when interpolated into a string,
but there is no prefix * operator, so it cannot be assigned through. A
pointer to a list, a fixed array, a Map or a Set can be stored and
passed on, but the collection's methods, indexing and iteration are not
available through it (E0220, E0202): reach the collection through a
field of the value a pointer refers to (p.items.push(x)) or pass it to a
var parameter. Inside a mutating method self may be passed to a
pointer parameter, and &self is a pointer to the receiver.
Passing a binding to a parameter of type *T takes its address
automatically, and a generic parameter written *T infers T from the
argument.
Lifetimes. Every pointer refers to storage with a lifetime, and every
dereference (a field read or write, a method called through the pointer,
a value read out of it, &p.field) checks that the storage is still
alive, whatever produced the pointer: an & in the same function, a
parameter, a field, a list element, a call result, a capture. A
dereference of dead storage aborts the program with a message naming the
kind of storage:
| The pointer was taken from | It stays valid while | Message after that |
|---|---|---|
a local (val, var, a parameter, a pattern binding) |
the local has not been destroyed: its block has not ended, see below | dangling stack pointer to frame |
a var that a closure captured (section 3.7) |
the variable's cell exists: the declaring block has not ended, or a closure that captured it is alive | stale pointer dereference |
| a global | always | |
a heap value or a field of one |
the value has not been freed | stale pointer dereference |
| an arena value or a field of one | the arena was not reset, restored to a save point taken before the value, or destroyed (section 6.6) | stale pointer dereference (arena) |
a field of an rc object |
some strong handle to the object exists | stale pointer dereference |
a field of self in a method |
the receiver is valid (the rules above, for the value the method was called on) | as for the receiver |
| a list element (only as a call argument, below) | the call has not returned | stale pointer dereference |
through another pointer (&p.field) |
p's storage is valid |
as for p |
The block of a local is its innermost { ... }: a function body, the body
of an if or else, a loop body (each iteration has its own locals), a
match arm, or a plain block. A pointer kept past the end of its block,
in an object, a list, a closure, an enum payload or an interface value
that outlives it, dangles even though the function is still running. The
same holds for a pointer read on another thread: a task that reads a
pointer to a local of a function that has returned panics, and one that
is awaited inside the block that owns the local reads it normally. The
check is exact for every program without data races; a program that frees
or ends storage on one thread while another reads it races, and the race
is not guaranteed to be detected.
At the end of a block its defer statements run first, while every local
is still alive, and then the locals are destroyed in reverse order of
declaration (section 6.7). A drop block that runs then may dereference a
pointer to a local declared before its own value, which is still alive (a
guard holding a pointer to what it guards), and panics on a pointer to a
local declared after it, which is already destroyed. A panic raised while
a destructor runs is reported as destructor panicked, followed by the
type, the cause (one of the messages above) and the site.
The variable of a for loop is a local of the iteration. A var narrowed
from T? by a null check, or to one implementor of an interface by is,
holds a value that the next assignment destroys or replaces: its address
(explicit, or taken automatically for a *T parameter) can only be a call
argument (E0270 otherwise), the pointer lives for that call, and so does
a pointer a method called on the narrowed var takes from self. A var
that is never assigned again and a narrowed val have no such limit.
Inside a lambda, a captured val is the closure's own copy: a pointer to
it taken in the lambda ends when the lambda returns. A captured var is
the variable itself (the table above).
List elements. The address of an element of a growable list, or of
anything stored inside one (&xs[i], &xs[i].field,
&xs[i].inner.field, &xs[i].fixed[0]), is only allowed directly as a
call argument (E0330), in either list layout (section 2.3), and so is
passing an element to a *T parameter. The pointer lives for that call: a copy the
callee keeps is stale once the call returns. While the call runs the list
is pinned: growing it past its capacity, removing, inserting, popping or
clearing elements, or freeing it, through any handle and on any thread,
aborts with list changed while a pointer to one of its elements is in
use. A for loop over a list pins it in the same way (section 4.6). A method called on a list element (xs[i].m()) pins the list in
the same way when it might change a list.
Receivers. A method called on an rc object read out of a field,
a list or a global keeps the object alive until the method returns, even
if the method drops the last other handle.
Compile-time rules. Checked pointers support no arithmetic (E0268,
E0295); & cannot be applied to a temporary or to a place inside one,
such as &make().field (E0270); a pointer to a local cannot be returned
directly (E0271; returning the address of a global or of a place reached
through a pointer is fine); & on an rc handle is rejected (E0296;
&r.field is allowed and checked as above); and &list[i] is restricted
as described above (E0330). A checked pointer that is not ?*T is never
null: comparing it with null is a constant (W0616).
Limits. A program can have 2^30 heap values, rc objects with a
pointer into them and arena segments alive at once, 4095 running threads
that have taken the address of a local, and scopes with addressed locals
nested 1,048,575 deep; past any of them it aborts with a
message naming the limit. No count of allocations, scope entries or
threads over the life of the program makes a dead pointer valid again.
object Player { var hp: Int init(hp: Int) { self.hp = hp } } func observe(p: *Player) -> Int { return p.hp } func heal(p: *var Player) { p.hp += 10 } func maybe(p: ?*Player) -> Bool { return p != null } func main() { var player = Player(80) val view: *Player = &player heal(&player) println("{observe(player)} {view.hp} {maybe(&player)}") }
90 90 true
A pointer into the receiver taken by a mutating method, an element borrow for a call, and a pointer to a local of an enclosing block are all valid while their storage is:
object Counter { var n: Int init(n: Int) { self.n = n } func handle() -> *var Int { return &self.n } } object Slot { var p: ?*Int init() { self.p = null } } func show(p: *Int) -> String { return "{p}" } func main() { var c = Counter(1) val h = c.handle() c.n = 5 println("{h}") var xs = [10, 20, 30] println(show(&xs[1])) xs.push(40) var slot = Slot() val outer = 7 { val inner = 8 slot.p = &outer println("inner {inner}") } val q = slot.p if q != null { println("outer {q}") } }
5 20 inner 8 outer 7
object Res { val id: Int } func escape() -> *Res { val local = Res(1) return &local } func main() { val p = escape() }
6.5 Unchecked pointers, Box and RawPtr
*unchecked T and *var unchecked T are plain C pointers with no
generation check; they exist for the FFI boundary and for Box. Iron code
obtains them from Box[T]: Box(value) moves a value into an owned
heap cell (Box is nocopy), b.unwrap() returns a
*var unchecked T to the contents, b.is_null() tests the cell,
b.free() releases it, and Box.null() is an empty cell. Fields are
accessed through the pointer with . as usual. Checked and unchecked
pointers are distinct types (E0289, E0294) and & never produces an
unchecked pointer. RawPtr.of(x) produces a type-erased RawPtr and
Ptr.cast[T](raw) turns it back into a *unchecked T.
A pointer to a collection does not expose that collection's methods or
indexing. Calling a list, map, set, channel or lock operation through a
pointer is E0220; pass or retain the collection value itself instead.
object Config { var width: Int var title: String init(width: Int, title: String) { self.width = width self.title = title } } func main() { val boxed = Box(Config(640, "Iron App")) val cfg: *var unchecked Config = boxed.unwrap() cfg.width = 800 println("{cfg.width} {cfg.title} {boxed.is_null()}") boxed.free() val empty: Box[Config] = Box.null() println("{boxed.is_null()} {empty.is_null()}") var n: Int = 7 val raw: RawPtr = RawPtr.of(n) val p: *unchecked Int = Ptr.cast[Int](raw) println("{p}") }
800 Iron App false true true 7
6.6 Arenas
Arena(bytes) creates a bump
allocator. heap(in: a) T(args) allocates from arena a, and inside
in a { ... } every plain heap T(args) does. Arena values are never
freed individually: a.reset() releases everything at once, and
a.save() / a.restore(point) roll the arena back to an earlier mark.
Because the arena frees in bulk, the drop block of an object allocated
in it does not run, and neither does the cleanup of a field that owns
memory outside the arena (a list, a string, a map or set, an rc handle,
a closure), which then leaks; the compiler warns about it (W0605) unless the
allocation says heap(in: a, allow_drop_skip: true) T(args). Accessing an
arena value after reset or restore is a stale pointer error at run
time, also through a pointer kept in another object or read in a callee
(section 6.4). A save point is only valid while everything allocated
before it is: restoring to one taken before a reset, or before an
earlier restore to an older save point, or to a save point of another
arena, aborts with arena restored to a stale save point. A value allocated after a restore never reuses the
validity of one the restore ended. rc allocation inside an in arena
block is an error (E0301).
a.used() and a.capacity() report the arena's byte counts. An arena
value releases its memory when its binding ends; a heap Arena(bytes) is
freed with free, like any heap value.
object Particle { val x: Int val y: Int } func main() { val frame = Arena(65536) in frame { val p1 = heap Particle(10, 20) val p2 = heap Particle(30, 40) println("p1 ({p1.x},{p1.y}) p2 ({p2.x},{p2.y})") } val mark = frame.save() val p3 = heap(in: frame) Particle(1, 2) println("{p3.x} {frame.used() > 0}") frame.restore(mark) frame.reset() println("{frame.used()}") }
p1 (10,20) p2 (30,40) 1 true 0
6.7 drop, copy and nocopy
An object body may contain a drop { ... } block, run once when each
instance is destroyed (stack scope exit, free, last rc handle released,
list cleared), with self bound to the dying value. It may contain a
copy { ... } block, run after each implicit or explicit copy of the value
(x.copy() copies an object explicitly, retaining its rc fields and
cloning its list fields). An object declared nocopy object cannot be
copied at all: assigning it to another binding, passing it by value or
storing it is an error (E0286), which is how Box, Channel, Mutex,
RWLock and FileHandle guarantee a single owner. A list of nocopy
objects moves its elements (take(), pop(), remove()) and lends them
(field reads, for, methods), but anything that would duplicate one is
E0286 too: copy(), a slice, filter, and binding an element or a
field (val p = ps[i]).
A closure or a thread can only borrow a nocopy value, as it borrows a
list (6.8): a lambda passed directly as an argument to a callee that only
calls it (3.7), or a spawn surely awaited in the block that starts it
(7.1), reads the value through its binding and never copies or drops it.
Any other capture, by a lambda bound to a val or var, returned, stored
in a field, list or map, passed to a callee that keeps it, or by a spawn
that is not surely awaited, is E0286. The rule covers a val binding or
parameter whose type is a nocopy object or holds one by value (in a
field, a fixed array, an optional, a tuple or an enum payload). It does not
cover a var binding, which a closure shares through its counted cell
(3.7), a heap binding, rc values and pointers, or Mutex, Channel,
RWLock and Arena: these are reference counted handles, and a closure or
thread holds its own reference to them (7.1). To share a nocopy value
with a closure that is stored or returned, or with a thread that may
outlive the block, make it reference counted, val h = rc FileHandle.open(p):
each closure then holds its own reference and the value is dropped once,
when the last holder is gone. Two threads that both use it guard the
reference with a lock, Mutex(rc Hasher.new(alg)). Each object has at most
one drop and one copy block (E0284, E0285); drop may not be
readonly (E0287) and may not return early (E0288).
A closure may capture a nocopy value only as a borrow: the lambda must be
passed directly as a call argument and the callee must not keep it. A spawn
may borrow one only when it is awaited in the same block. Binding, returning
or storing such a closure, passing it to a callee that keeps it, or letting
the spawn escape is E0286; the diagnostic names rc as the ownership fix.
object Point { val x: Int copy { println("copied {self.x}") } drop { println("dropped {self.x}") } } nocopy object Token { val id: Int } func main() { val p = Point(3) val q = p val t = Token(5) println("{q.x} {t.id}") }
copied 3 3 5 dropped 3 dropped 3
nocopy object Token { val id: Int } func main() { val t = Token(1) val u = t }
nocopy object Res { val id: Int drop { println("drop {self.id}") } } func twice(f: func() -> Int) -> Int { return f() + f() } func reader(r: rc Res) -> func() -> Int { return func() -> Int { return r.id } } func main() { val a = Res(1) println("{twice(func() -> Int { return a.id })}") val t = spawn("t") { return a.id * 10 } println("{await t}") val b = rc Res(2) val get = reader(b) println("{get()}") }
2 10 2 drop 2 drop 1
nocopy object Res { val id: Int } func main() { val a = Res(1) val get = func() -> Int { return a.id } println("{get()}") }
6.8 List ownership
Lists are the one kind of value that is never copied implicitly. The
rules, all enforced at compile time with E0328:
val b = awhereais a list, storing a list binding in a field or in another list, and returning a list parameter are rejected; writea.copy()ora.take().- A function or method receives a list argument by reference for the
duration of the call; it may read it, and mutate it only through a
varparameter. A pointer to one of its elements may only be passed to a call, which pins the list until it returns (section 6.4). A list passed to avarparameter cannot be passed again in the same call (the callee may move its elements while the other parameter refers to them); passxs.copy()for the other one. - A lambda may capture a list only when the lambda is passed directly as a
call argument to a callee that only calls it (3.7); a lambda that is
bound, returned or stored, or given to a callee that keeps it, cannot
capture a list. A
spawnbody that captures a list must be awaited in the block that started it, by a later statement that always runs (noreturn,breakorcontinuebefore it).nocopyvalues follow the same rules (6.7). - To share a list, use
rc [T].
func main() { val a = [1, 2, 3] val b = a }
7. Concurrency
7.1 spawn and await
spawn("name") { body } runs the body concurrently and evaluates to a
handle. The body is a block that must return a value, and
await handle blocks until the thread finishes and yields that value. A
handle can be awaited once (E0325). A spawn expression may only appear as
the initializer of a val or var, or as a statement (its result is then
discarded and the task is never awaited). A bound spawn (val h = spawn)
runs on a new operating system thread. A spawn statement is a task for the
runtime's worker pool, a fixed set of one thread per CPU core but one (the
environment variable IRON_WORKERS sets another size): a
task that blocks (on a socket, a sleep or a database call) holds a worker
until it returns, and tasks beyond the pool's size wait their turn. Run
long-lived loops as bound spawns, and see Http.poller (networking guide)
for servers that keep many idle connections. The body may read val bindings of the enclosing
function and use Mutex, Channel and rc values; a list captured by a
spawn must be awaited in the same block (E0328), and so must a nocopy
value (or a value holding one), which the thread borrows from its binding
without copying or dropping it (E0286, 6.7). For a nocopy value the
await must be sure: a later statement of the same block that always runs,
with no return, break or continue before it that could leave the
block; to hand one to a thread that may outlive the block, make it rc
(and guard it with a Mutex when both threads use it). Writing a captured
var from a thread is a data race and warns (W0604). A thread keeps what
it captured alive until it finishes, even when the block that declared it
ends first (always possible for a spawn that is never awaited): it holds its
own reference to each captured Mutex, Channel, RWLock, Arena, rc
value and var, borrows a captured list or nocopy value (which is why
such a spawn must be awaited), and keeps its own copy of the other values. A
checked pointer a thread reads is checked like on the thread that took it
(section 6.4): a pointer to a local of a function that has returned
dangles. A thread that is still running when the program ends is stopped
with it. Named thread pools
are not implemented: spawn("name", pool) is rejected (E0326).
func sum_to(n: Int) -> Int { var total = 0 for i in range(n) { total += i } return total } func main() { val a = spawn("a") { return sum_to(10) } val b = spawn("b") { return sum_to(100) } val ra = await a val rb = await b println("{ra} {rb}") }
45 4950
7.2 Channel[T]
Channel[T](capacity) creates a bounded queue of T; the element type
is written as the type argument (val ch = Channel[Int](4)), since no
argument carries it. T may be any type, a function type included, so a
channel can carry closures between tasks (Channel[func(Int) -> Int](4))
and rc handles (Channel[rc Job](8)).
ch.send(v) blocks while the channel is full and ch.recv() blocks while
it is empty. Channels are nocopy and are closed when their
owner goes out of scope; there is no explicit close and no non-blocking
receive. A channel owns the items it holds as a map owns its values
(section 9.10): send takes its argument like put (a list, map or set
binding, or an object holding one, is E0328: send xs.copy() or
xs.take()), recv moves the item out, and items still queued when the
channel goes away are dropped.
7.3 Mutex[T] and RWLock[T]
Mutex(value) wraps a value in a lock; m.lock() returns a
MutexGuard[T] that holds the lock until the guard's scope ends, with
g.get() and g.set(v) to read and write the protected value. RWLock(value) is the
reader-writer variant: l.read() returns an RWReadGuard[T] with get(),
and l.write() an RWWriteGuard[T] with get() and set(v). All of
these are nocopy. A lock owns its value: Mutex(v), RWLock(v) and
set(v) take their argument as Map.put does (a list, map or set binding,
or an object holding one, is E0328), get() hands out a copy the caller
owns, set drops the value it replaces, and the value is dropped with the
lock.
func main() { val ch = Channel[Int](4) val producer = spawn("producer") { for i in range(3) { ch.send(i * 10) } return 0 } var got = 0 for i in range(3) { val x: Int = ch.recv() got += x } await producer println("{got}") val counter = Mutex(0) val worker = spawn("worker") { for i in range(100) { val g = counter.lock() val cur: Int = g.get() g.set(cur + 1) } return 0 } for i in range(100) { val g = counter.lock() val cur: Int = g.get() g.set(cur + 1) } await worker val final_guard = counter.lock() val total: Int = final_guard.get() println("{total}") val settings = RWLock(5) val reader = settings.read() val seen: Int = reader.get() println("{seen}") }
30 200 5
A channel of closures hands work to another task; each closure keeps what it captured alive until it has run:
func main() { val jobs = Channel[func(Int) -> Int](4) val base = 100 val producer = spawn("producer") { for i in range(3) { val k = i * 10 jobs.send(func(x: Int) -> Int { return x + k + base }) } return 0 } var total = 0 for i in range(3) { val job: func(Int) -> Int = jobs.recv() total += job(i) } await producer println("{total}") }
333
7.4 Parallel for
for x in xs parallel { ... } runs the iterations of the loop on several
threads and waits for all of them. The body must not write bindings of the
enclosing scope (E0208); use a Mutex to accumulate. range(n) and lists
may be iterated in parallel; a list is iterated through a binding
(for x in xs parallel), not a list expression. A parallel body cannot
capture a nocopy value (E0286): share it as rc and guard it with a
Mutex, since several worker threads may use it. The parallel(pool) form
parses but pools are not implemented (E0326).
func main() { val total = Mutex(0) for i in range(8) parallel { val g = total.lock() val cur: Int = g.get() g.set(cur + i) } val g = total.lock() val sum: Int = g.get() println("{sum}") }
28
8. Compile-time evaluation
comptime expr evaluates expr while compiling and replaces it with the
resulting constant. The evaluator handles integer, float and boolean
literals and arithmetic (+ - * / %, comparisons, == and !=, unary
- and not), the bitwise operators and shifts (& | ^ ~ << >>), the
numeric conversions (UInt64(x), Int8(x), Float(n), ...), string
literals, list literals and indexing, object
construction, the built-ins len, range, fill and read_file(path)
(which reads a file relative to the source file and yields its contents),
and calls to top-level functions whose bodies use only val, var,
assignment, if, while, for and return over those values. The
evaluation computes what the same call computes at run time: integer
arithmetic wraps at the width of its type, unsigned values compare and
divide as unsigned, a shift by the width or more gives 0 (or the sign for
>> of a signed value), a float converted to an integer saturates, and
fill makes independent copies. An object is
built at compile time only when it has no init (positional
construction). Method calls, field access, string concatenation,
interpolation, reading a global, break and continue, lambdas, match,
enum values, and heap or rc allocation are not available at compile
time (E0231); an error while evaluating, such as an integer division by
zero, a shift by a negative count or an index out of range, is E0232 (a float divided by zero is an
infinity or NaN, as at run time, and % on floats is the exact remainder
fmod with the sign of the dividend: 5.5 % 2.0 is 1.5, -5.5 % 2.0
is -1.5, and a zero divisor gives NaN). Evaluation is limited to one million
steps and to calls nested 256 deep (E0230). The result is typically
bound to a global val. A comptime expression that sizes a fixed array
or bounded vector ([Int; N] with val N = comptime f(3), or [Int;
comptime f(3)]) is evaluated while that type is checked, before the other
compile-time evaluations; it must produce an Int, and does not read
files.
val TABLE_SIZE = comptime (64 * 4) val FACTORIAL_5 = comptime fact(5) val SQUARES = comptime squares(4) func fact(n: Int) -> Int { var acc = 1 for i in range(n) { acc = acc * (i + 1) } return acc } func squares(n: Int) -> [Int] { var out = fill(n, 0) var i = 0 while i < n { out[i] = i * i i += 1 } return out } func main() { println("{TABLE_SIZE} {FACTORIAL_5} {SQUARES[3]} {len(SQUARES)}") }
256 120 9 4
8.1 Compile-time field reflection
Field reflection is a source expansion facility. It runs after a generic function is materialized or an interface default is copied to an implementing object, while the reflected object type is concrete. The resulting program contains ordinary field accesses and literals. There is no run-time type or field descriptor.
comptime for f in fields(T) visits the fields of object T in declaration
order. T may be Self, a type parameter that has become concrete, or a
concrete object type. Inside the loop, the field descriptor has these
compile-time properties:
| Form | Compile-time value |
|---|---|
f.name |
declared field name as a String |
f.index |
zero-based declaration index as an Int |
f.type |
declared type, usable in a type position or comptime match |
f.is_var |
whether the field was declared var |
f.is_nullable |
whether the declared type is nullable |
f.is_pub |
whether the field is public |
f.visibility |
"public" or "private" |
f.doc |
the field's documentation (section 1.2) as a String, or null |
f.tag(name) |
the value of the field's //name:value directive as a String, or null |
name must be a string literal. A directive belongs to the field when it is
on its own line in the comment run directly above the field, or at the end of
the field's line; //json: gives "", and when a key is written more than
once the first value is used. The compiler gives directive keys no meaning of
its own: each library reads and validates its own. The object-level forms are
typeinfo(T).doc and typeinfo(T).tag(name), read from the comments above
the object declaration.
interface Columns { readonly func columns() -> String { var out = "" comptime for f in fields(Self) { comptime if f.tag("db") != "skip" { comptime if f.tag("db") == null { out = out + f.name + ";" } else { out = out + f.tag("db") + ";" } } } return out } } object Row impl Columns { // The primary key. val id: Int //db:row_id val name: String val secret: String //db:skip } func main() { println(Row(1, "a", "s").columns()) }
row_id;name;
value.(f) becomes a direct access to that field. It may appear on either
side of assignment when the field and value are writable. A default method
in an interface may read private fields after an object opts in with impl.
interface Describe { readonly func describe() -> String { var out = "" comptime for f in fields(Self) { out = out + f.name + "=" comptime match f.type { Int -> { out = out + self.(f).to_string() } String -> { out = out + self.(f) } E? -> { out = out + "nullable" } else -> comptime error("Describe does not support this field") } out = out + ";" } return out } } object User impl Describe { val id: Int val name: String }
comptime if selects a branch from compile-time field metadata. An unselected
branch is removed before name resolution and type checking. comptime match
does the same for a type. It accepts exact named types, nullable patterns such
as E?, list patterns such as [E], constructed type patterns such as
Map[K, V], and an interface name implemented by the subject type. Pattern
identifiers bind types for use in the selected body. If no arm applies and
there is no else, the compiler reports E0360. comptime error(message)
reports E0232 only when its containing branch is selected.
Fields[T]() supports construction by reflected assignment in an opted-in
interface default. If T has a zero-argument init, construction calls it so
its defaults are preserved. Otherwise the compiler constructs the object
field by field. b.(f) = value marks a field present, b.missing() returns
the first missing non-nullable field name as String?, and b.build() returns
the completed T. Nullable fields are not required.
Reflection forms are compile-time-only. Using one where no concrete reflected
type is available is E0357; reflecting a non-object is E0358; a condition
that is not a reflection constant is E0359; and a type that cannot be safely
initialized for Fields[T] is E0361. Enums and reflection over generic
object declarations are not part of this stage.
9. The standard library
The standard library is a set of Iron declarations (in src/stdlib/)
whose bodies are provided by the C runtime. Modules marked import must be
imported by name (section 5.9); the rest is always available. Functions on a
module object are called as Module.function(args); methods are called on
values. A module function may be generic, like a top-level function
(section 5.2): its type arguments are inferred from the arguments, as in
Json.encode_list(points) (func Json.encode_list[T: JsonEncodable](xs:
[T]) -> String).
9.1 Built-in functions
These are available everywhere without an import.
| Function | Description |
|---|---|
println(s: String) |
writes s and a newline to standard output |
print(s: String) |
writes s without a newline |
len(x) -> Int |
number of elements of a list, array or vector, or characters of a string |
range(n: Int) |
the sequence 0 .. n-1, only valid as the iterable of for |
fill(n: Int, v: T) -> [T] |
a list of n copies of v |
min(a: Int, b: Int) -> Int, max(a: Int, b: Int) -> Int |
smaller or larger of two integers |
clamp(x: Int, lo: Int, hi: Int) -> Int |
x limited to [lo, hi] |
abs(x: Int) -> Int |
absolute value |
assert(cond: Bool), assert(cond: Bool, msg: String) |
abort with msg (or the source location) when cond is false |
assert_eq(actual, expected), assert_ne(a, b) |
abort, naming the source line and both values, unless actual == expected (a != b); a statement, for values that compare with == and have a text form |
read_file(path: String) -> String |
file contents, only inside comptime |
print and println take exactly one String; interpolate other values.
min, max, clamp and abs are Int only (use Math for floats).
func main() { println("{min(3, 9)} {max(3, 9)} {clamp(15, 0, 10)} {abs(-4)}") print("no newline, ") println("then one") assert(len("abc") == 3, "len counts characters") }
3 9 10 4 no newline, then one
9.2 String methods
All string methods are readonly; indexes count characters from 0 and a
missing substring gives -1. Unlike list indexing, string positions never
panic: substring(start, end) clamps both ends to the string
("hello".substring(3, 10) is "lo", an empty range gives ""),
char_at(i) out of range is "" and byte_at(i) out of range is -1.
to_int() and to_float() accept whitespace around the number
(" 42\n".to_int() is 42) and give 0 for anything else that is not
entirely a number ("4 2", "3x", ""). to_int reads an optional sign
and decimal digits, and gives 0 for a value outside Int; to_float
reads an optional sign, digits with an optional fraction or a fraction
alone (".5", "5.") and an optional exponent ("1e3"), and nothing
else (no hexadecimal, inf or nan); a value past the Float range is
an infinity. upper() and lower() apply the simple Unicode case mapping
(one character to one character); a character whose case form is several
characters (ß) keeps its case. With an empty pattern, split("") gives
the characters, replace("", x) returns the string unchanged and
count("") is 0. pad_left and pad_right pad with the first
character of ch, or with spaces when ch is empty.
String.from_byte(b) is the one-byte string for b in 0..255 and ""
for any other value.
| Method | Description |
|---|---|
len() -> Int, byte_len() -> Int |
characters, bytes |
upper() -> String, lower() -> String, trim() -> String |
case and whitespace |
contains(sub) -> Bool, starts_with(p) -> Bool, ends_with(s) -> Bool |
tests |
index_of(sub) -> Int, rindex_of(sub) -> Int, count(sub) -> Int |
search |
split(sep: String) -> [String], chars() -> [String] |
split into parts or characters |
join(parts: [String]) -> String |
join parts with the receiver as separator |
replace(old, new) -> String, repeat(n) -> String |
rewriting |
substring(start, end) -> String, char_at(i) -> String |
slices (end exclusive) |
pad_left(width, ch) -> String, pad_right(width, ch) -> String |
padding |
to_int() -> Int, to_float() -> Float |
parsing (0 when not a number) |
byte_at(i) -> Int, String.from_byte(b: Int) -> String |
byte access |
byte_slice(start, end) -> String |
bytes [start, end), clamped like substring (for binary data) |
release() |
does nothing (kept for source compatibility) |
Int, Int32 and Float have a to_string() -> String method. s[i]
is s.char_at(i) and s[a..b] is s.substring(a, b).
9.2.1 Scalar bit and overflow operations
Every integer type has leading_zeros(), trailing_zeros() (both return
the bit width for zero), count_ones(), clear_lowest_bit(),
lowest_bit(), rotate_left(k), rotate_right(k), byte_swap() and
prefix_xor(). Rotations reduce k modulo the width, including negative
counts. Arithmetic methods return wrapped results together with a flag:
a.add_overflow(b) -> (T, Bool) a.sub_overflow(b) -> (T, Bool) a.mul_overflow(b) -> (T, Bool) a.add_carry(b, carry: Bool) -> (T, Bool)
mul_wide returns the full product as (high: T, low: U), where U is
the unsigned type of the same width; mul_high returns only the high half.
They are available on all integer widths. Signed integers have
to_bits() -> U and T.from_bits(U). Float and Float64 use UInt64,
and Float32 uses UInt32, with the same to_bits and from_bits names.
These are bit casts, not numeric conversions.
func main() { val s = "Hello, Wörld" println("{s.len()} {s.byte_len()} {s.upper()} {s.index_of("o")} {s.rindex_of("o")}") println("{s.substring(7, 12)} {s.char_at(8)} {s.count("l")} {s.replace("l", "L")}") val parts = "a,b,,c".split(",") println("{parts.len()} {parts[2].len()} {",".join(["x", "y"])}") println("[{" x ".trim()}] {"ab".repeat(3)} {"7".pad_left(3, "0")} {"7".pad_right(3, "-")}") println("{s.byte_at(0)} {String.from_byte(65)} {"abc".chars()[1]} {s[0]} {s[0..5]}") }
12 13 HELLO, WÖRLD 4 4 Wörld ö 3 HeLLo, WörLd 4 0 x,y [x] ababab 007 7-- 72 A b H Hello
9.3 List methods
Methods on [T] (and, where noted, on fixed arrays, bounded vectors and
rc [T]). Mutating methods need a var list.
| Method | Description |
|---|---|
len() -> Int |
number of elements (also len(xs)) |
push(v: T), pop() -> T |
append, remove and return the last element |
insert(i: Int, v: T), remove(i: Int) -> T |
insert before / remove at index |
get(i: Int) -> T, set(i: Int, v: T) |
checked element access (same as xs[i]) |
get_unchecked(i: Int) -> T, set_unchecked(i: Int, v: T) |
access without the bounds check |
clear(), reverse(), sort() |
in place; sort orders Int, Float and String ascending (NaN after every number, strings in code point order) |
contains(v: T) -> Bool |
membership |
copy() -> [T], take() -> [T] |
independent copy; move the contents out |
map(f: func(T) -> U) -> [U] |
transform |
filter(f: func(T) -> Bool) -> [T] |
keep matching elements |
reduce(init: U, f: func(U, T) -> U) -> U |
fold |
forEach(f: func(T)) |
call f on each element |
sum() -> T |
sum of an [Int] or [Float] |
view() -> View[T], view_mut() -> ViewMut[T] |
borrow the elements; the mutable form needs a var list |
spare(n) -> ViewMut[T], commit(k) |
reserve and zero n scalar elements, then append the first k written elements (k <= n) |
A slice xs[a..b] must lie within the list with a <= b; anything else
is an index out of bounds panic, like xs[i]. contains compares with
==, so a list never contains NaN.
pop() on an empty list panics ("pop from an empty list"), and get,
set, remove, insert and xs[i] with an index out of range panic
("index out of bounds"), naming the Iron line of the call; this holds for
every element type, lists of interface values included.
The lambdas passed to map, filter, reduce and forEach must write
their parameter types. map cannot produce tuples (a list cannot hold
them, E0202). A chain of these calls may be fused into one loop (section
9.3.2); that never changes what the program does.
func main() { val ys = [1, 2, 3, 4, 5] val squares = ys.map(func(x: Int) -> Int { return x * x }) val evens = ys.filter(func(x: Int) -> Bool { return x % 2 == 0 }) val total = ys.reduce(0, func(acc: Int, x: Int) -> Int { return acc + x }) val labels = ys.map(func(x: Int) -> String { return "n{x}" }) println("{squares[4]} {evens.len()} {total} {ys.sum()} {labels[0]}") ys.forEach(func(x: Int) { print("{x},") }) println("") var xs = [5, 6, 7, 8] xs.set(1, 60) val removed = xs.remove(0) xs.reverse() val popped = xs.pop() println("{removed} {popped} {xs.get(0)} {xs.len()}") var words = ["b", "c", "a"] words.sort() println("{words[0]}{words[1]}{words[2]}") }
25 2 15 15 n1 1,2,3,4,5, 5 60 8 2 abc
9.3.1 StringBuilder
StringBuilder is a nocopy byte builder. new() starts empty and
with_capacity(n) reserves bytes. push_byte, push_bytes, push_str,
push_int and push_uint append without temporary Strings. reserve(n)
makes room for the next n bytes. spare(n) -> ViewMut[UInt8] returns
zeroed writable bytes after the current end; after writing them,
commit(k) appends the first k (k <= n). bytes() borrows the built
prefix, len() reports its byte length, clear() keeps the allocation,
and finish() moves the buffer into a String and leaves the builder empty.
func main() { var out = StringBuilder.with_capacity(32) out.push_str("count=") out.push_int(12) val tail = out.spare(1) tail[0] = UInt8(33) out.commit(1) println(out.finish()) }
count=12!
9.3.2 Loop fusion
map, filter, reduce, forEach and sum each run one loop over their
list, and map and filter allocate the list they return. Written as a
chain, xs.map(f).filter(g).sum() is three loops and two intermediate
lists. The optimizer may fuse such a chain into one loop that takes one
element of xs through every stage before it reads the next, and builds
no intermediate list.
The fused loop calls the callbacks in a different order (f, g, f,
g, ... instead of every f and then every g). Fusion is therefore done
only where no program can tell the difference. All of the following must
hold; where one does not, the calls run as written.
The stages. A chain is two or more of the five methods, each called on
the result of the one before. map and filter may be anywhere; reduce,
sum and forEach return no list and can only be last. Any other method
(copy, len, a slice, an index, ...) is not a stage: the chain ends
before it and a new one may start after it. A chain is split wherever a
rule below fails, and each part of two or more stages is fused on its own.
The callbacks. Each callback must be a named function or a lambda the
compiler can see at the call: written there, or a val bound to one. A
callback received as a parameter, read from a field or stored in a var
is unknown. A lambda may capture val bindings (it gets a copy); one that
captures a var shares it with the enclosing function and is not fused.
The body, and the body of every function it calls, must be proven free of
effects and of anything that can panic. What is allowed is:
- arithmetic, comparison, logic and bit operations on numbers and
Bool, and conversions between numeric types; - integer
/and%by a constant other than zero, and shifts by a constant count (float division never panics); if,while,match, localvalandvar,return;- reading a field of an object, tuple or enum value held directly, and constructing an object or tuple;
- reading a global;
- calling a function or method of the program that itself passes, generic or not. A recursive function does not pass.
Everything else keeps the stage out of a fused loop: any I/O, writing a
global, a field, a list element or through a pointer, a var parameter,
indexing a list, integer division or modulo by a value that is not a
constant, a shift by a count that is not a constant, anything that builds
a String or a list, allocation, reading through an rc, heap or pointer
value, optionals, closures, tasks, and every function implemented outside
Iron (the String and list methods among them).
The values between stages. The values one stage hands to the next must
be numbers, Bool, or objects made only of those with no copy or drop
hook and not nocopy. So must the accumulator of a reduce. A map that
produces Strings, lists, rc handles, optionals, enums or objects that own
any of them really builds its list, and every such value is created,
copied and dropped exactly as written. The stage that reads that list may
start a new chain. The source list of a chain may hold anything; it is
read in place.
The source. A list of an interface type is never read by a fused loop
(it keeps its elements per implementing type, and a loop over it must
still visit them in list order). Every other list can be: a binding, a
parameter, a global, a literal or call result (kept alive until the loop
is done), an rc [T], a fixed-size array, a bounded vector, a slice, a
list of either layout. A map or filter at the end of a chain makes a
list of the layout its type asks for.
Bindings. The stages need not be in one expression. val a = xs.map(f)
followed by a.filter(g).sum() is one chain when a has no other reader
and nothing but argument setup stands between the two calls: constants,
arithmetic, building the callbacks, constructing an object. Any call or
other statement in between could change what the earlier stage reads (the
source list, a global), so it ends the chain there, as does a branch or a
loop between the two calls. The arguments of a later stage count as "in
between": in xs.map(f).reduce(next_id(), g) the call of next_id runs
after map has finished, as written.
What a fused loop does. It visits the source in list order. For each
element it applies the stages in order: map replaces the value, filter
drops the element when its callback returns false, reduce updates the
accumulator (starting from its first argument), sum adds to a total that
starts at zero, forEach calls its callback. A map or filter in last
position appends to the new list. An empty source calls nothing: the
result is zero, the initial value of reduce, or an empty list. The loop
runs where the last stage is written, once each time that point is
reached.
Fusion is local to one function body and to the stages that pass; a
pipeline inside a callback is a list method call like any other (the outer
stage is not fused, the inner pipeline is, on its own). It adds and removes
no synchronization: a pipeline in a spawn or parallel body is fused by
the same rules. Only an optimizing build fuses: with --no-optimize or
--debug every call runs as written, so the two builds of a program must
print the same, and that is how the test suite checks fusion.
iron build --warn-fusion-break prints, for every list method call that
was left out of a fused loop, the rule that stopped it:
note: --warn-fusion-break: main.iron:12: .map() is not fused: its callback calls 'println', whose effects the compiler cannot rule out note: --warn-fusion-break: main.iron:20: .sum() is not fused with the .map() before it: the intermediate list has another reader
var calls = 0 //iron:fusible func double(x: Int) -> Int { return x * 2 } func counted(x: Int) -> Int { calls += 1 return x + 1 } func main() { val xs = [1, 2, 3, 4] // one loop, no intermediate list println("{xs.map(double).filter(func(x: Int) -> Bool { return x > 2 }).sum()}") // counted writes a global: map runs first, then filter and sum in one loop println("{xs.map(counted).filter(func(x: Int) -> Bool { return x > calls }).sum()}") }
18 5
9.4 math (import)
Math has the constants Math.PI, Math.TAU and Math.E and the
functions sin, cos, tan, asin, acos, sqrt, floor, ceil,
round, log, log2, exp (all (x: Float) -> Float), atan2(y, x),
pow(base, exp), hypot(a, b), lerp(a, b, t) (Float arguments and
results), sign(x: Float) -> Int, random() -> Float in [0, 1),
random_float(min, max) -> Float, random_int(min: Int, max: Int) -> Int
(inclusive at both ends, over any range including all of Int; min when
max < min) and seed(n: Int). The functions are the platform C
library's: round rounds halfway cases away from zero, a result outside a
function's domain is NaN (sqrt(-1.0)), and sign(NaN) is 0. After
seed(n) the sequence is the same on every run.
import math func main() { println("{Math.PI > 3.14} {Math.floor(2.7)} {Math.pow(2.0, 10.0)} {Math.sign(-2.5)}") Math.seed(42) val r = Math.random() println("{r >= 0.0 and r < 1.0} {Math.random_int(3, 3)} {Math.hypot(3.0, 4.0)}") }
true 2 1024 -1 true 3 5
9.5 io (import)
Simple functions on IO:
| Function | Description |
|---|---|
read_file(path) -> String, write_file(path, content), append_file(path, content) |
whole-file text I/O (an unreadable file reads as "") |
read_lines(path) -> [String] |
the lines of a file |
read_line() -> String |
one line from standard input |
write_stdout(s) -> Int, write_stderr(s) -> Int |
write all of s at once without buffering (lines from several threads do not interleave; println's buffer is flushed first); the bytes written, or -1 |
file_exists(path) -> Bool, is_dir(path) -> Bool |
tests |
create_dir(path), delete_file(path) |
directories and deletion |
list_files(dir) -> String |
the entries of a directory, one per line |
basename(path), dirname(path), extension(path), join_path(a, b) |
path helpers (extension has no leading dot) |
Result-returning functions, which report errors in the value instead of failing silently:
| Function | Result |
|---|---|
read_text(path, max_bytes), read_bytes(path, max_bytes) |
FileReadResult { data: String, error: Int, error_message: String } |
write_text(path, content), write_bytes(path, content), append_text(path, content), append_bytes(path, content) |
FileWriteResult { bytes: Int, error: Int, error_message: String } |
copy_file(src, dst, overwrite: Bool), move_file(src, dst, overwrite: Bool) |
FileWriteResult |
file_info(path) |
FileInfo { exists, is_file, is_dir: Bool, size, modified_unix, error: Int, error_message: String } |
error is 0 on success; otherwise it is one of the codes of
src/runtime/iron_errors.h and error_message says which:
| Code | Meaning |
|---|---|
| 8000 | not found (the file or a directory on its path does not exist) |
| 8001 | permission denied |
| 8002 | invalid argument: an empty path, a path holding a zero byte, a name too long, a negative limit, or copy / move onto the source itself |
| 8003 | the file is larger than max_bytes |
| 8004, 8005 | read failed, write failed |
| 8007 | the destination exists (copy / move without overwrite) |
| 8008 | a component of the path is a file, not a directory |
| 8009 | the path is a directory (reading or writing it as a file) |
A missing file is not an error for file_info (exists is false). A path
that is empty or holds a zero byte names no file for every function: the
simple ones read "", write nothing and report false (the prefix before
the zero byte is never used). The path helpers work on the bytes of the
path with / as the separator; join_path("", b) is b. Files whose
size the system does not report (/proc files, pipes) are read to their
end, still within max_bytes.
FileHandle.open(path) -> FileHandle opens the file for reading and
writing, creating it when it does not exist (an existing file keeps its
contents); h.fd is the descriptor, -1 when the file could not be opened
(an empty path, a directory, a missing directory). The handle is nocopy
and closes itself when dropped; h.close() closes it at once and sets
fd to -1, and a second close does nothing.
import io func main() { val path = "/tmp/iron_manual_io.txt" val w = IO.write_text(path, "one\ntwo\n") val r = IO.read_text(path, 1024) val lines = IO.read_lines(path) val info = IO.file_info(path) println("{w.error} {w.bytes} {r.error} {r.data.len()} {lines[1]} {info.size}") val missing = IO.read_text("/tmp/iron_manual_missing_file", 16) println("{missing.error != 0} {IO.extension("a/b.iron")} {IO.basename("a/b.iron")} {IO.join_path("a", "b")}") IO.delete_file(path) println("{IO.file_exists(path)}") }
0 8 0 8 two 8 true iron b.iron a/b false
9.6 time (import)
Time.now() -> Float (seconds since the Unix epoch), Time.unix_ms() -> Int
(milliseconds since the Unix epoch), Time.now_ms() -> Int
and Time.now_ns() -> Int (milliseconds and nanoseconds of a monotonic clock
with an arbitrary origin, for measuring durations), Time.sleep(ms:
Int), Time.since(start: Float) -> Float (seconds elapsed since a
Time.now() value), and Time.Timer(seconds: Float) -> Timer with the
fields elapsed_ms and duration_ms and the methods done() -> Bool,
update(dt: Float) and reset() (the last two need a var timer).
A timer counts whole milliseconds: Time.Timer(seconds) and
update(dt) convert like a Float to Int conversion (truncating,
saturating at the Int limits, NaN to 0, section 2.9) and the elapsed time
saturates instead of wrapping. Time.now_ms() and Time.now_ns() are
monotonic; Time.now() is the wall clock. Time.sleep of zero or a
negative time returns at once.
Duration is a millisecond value type built with Duration.millis(n),
Duration.seconds(n), Duration.minutes(n) or Duration.from_ms(n),
read with d.ms or d.to_ms().
import time func main() { val started = Time.now_ms() Time.sleep(5) println("{Time.now_ms() - started >= 5} {Duration.seconds(2).to_ms()} {Time.Timer(0.5).done()}") }
true 2000 false
9.6.1 os (import)
OS.env(name) -> String? reads an environment variable (null when it is
not set), OS.env_or(name, fallback) -> String returns fallback instead,
and OS.has_env(name) -> Bool tests for it (an empty value counts as set).
Names and values are UTF-8 on every platform. A name that is empty or
holds = or a zero byte is never set.
For long-running programs: OS.trap_shutdown_signals() makes SIGINT and
SIGTERM (Ctrl+C, Ctrl+Break and closing the console on Windows) set a flag
instead of ending the process, and OS.shutdown_signal() -> Int returns
the first such signal received (2 or 15), or 0, for the program to poll and
shut down cleanly; a second signal ends the process at once.
OS.exit(code) ends the process with an exit status (after flushing
standard output; drops, defers and other tasks do not run),
OS.pid() -> Int is the process id and OS.cpu_count() -> Int the
number of CPUs the process may run on (its affinity mask on Linux).
import os func main(args: [String]) { val home = OS.env("IRON_MANUAL_UNSET_VAR") if home == null { println("unset") } println("{OS.env_or("IRON_MANUAL_UNSET_VAR", "default")} {args.len()}") }
unset default 0
9.6.2 atomic (import)
AtomicInt.new(v) and AtomicBool.new(b) make a value that tasks share
without a lock. Copies of the handle (a spawn's capture, a field, an
argument) refer to the same value, freed with the last copy. get(),
set(v), swap(v) (returns the previous value) and
compare_and_set(expected, desired) (whether it stored) work on both;
AtomicInt.add(d) adds and returns the new value. Every operation is
sequentially consistent.
import atomic func main() { val hits = AtomicInt.new(0) val t = spawn("worker") { for i in range(1000) { hits.add(1) } return 0 } for i in range(1000) { hits.add(1) } await t println("{hits.get()} {hits.compare_and_set(2000, 1)} {hits.get()}") }
2000 true 1
9.7 log (import)
Log.debug(msg), Log.info(msg), Log.warn(msg) and Log.error(msg)
write a line [YYYY-MM-DD HH:MM:SS] [LEVEL] msg (local time; colored
when standard error is a terminal) to standard error; Log.set_level(level)
with Log.DEBUG (0), Log.INFO (1, the default), Log.WARN (2) or
Log.ERROR (3) hides messages below the level. Any Int is a level: one
below Log.DEBUG shows everything, one above Log.ERROR hides everything.
The message is written as given, every byte of it (a newline in it starts
a new line); a line from one task is never mixed with another's.
9.8 hint (import)
Hint.black_box(x: Int) -> Int returns its argument while preventing the
C optimizer from reasoning about it; it exists for benchmarks.
Hint.likely(c) and Hint.unlikely(c) return the Bool unchanged and tell
the optimizer which outcome is expected.
9.9 Memory and concurrency types
These are always available and are described in sections 6 and 7. Each
one is constructed like any object, Type(args) or Type[T](args) when
no argument carries the element type and no expected type gives it
(section 2.8); there is no .new method (E0333):
| Type | API |
|---|---|
Box[T] (nocopy) |
Box(v) or Box[T](v) -> Box[T], Box.null() -> Box[T], b.unwrap() -> *var unchecked T, b.is_null() -> Bool, b.free() |
Arena |
Arena(bytes), Arena.threadsafe(bytes), a.save() -> ArenaSave, a.restore(p: ArenaSave), a.reset(), a.used() -> Int, a.capacity() -> Int |
RawPtr |
RawPtr.of(x) -> RawPtr, Ptr.cast[T](raw) -> *unchecked T |
Channel[T] (nocopy) |
Channel[T](capacity: Int), ch.send(v: T), ch.recv() -> T |
Mutex[T], MutexGuard[T] (nocopy) |
Mutex(v) or Mutex[T](v), m.lock() -> MutexGuard[T], g.get() -> T, g.set(v: T) |
RWLock[T], RWReadGuard[T], RWWriteGuard[T] (nocopy) |
RWLock(v) or RWLock[T](v), l.read(), l.write(), g.get() -> T, g.set(v: T) (write guard only) |
FileHandle (nocopy) |
FileHandle.open(path) -> FileHandle, h.close(), field fd: Int |
Map[K, V], Set[T] |
section 9.10 |
9.10 Hashable, Map and Set
Map[K, V] is a hash table from keys of type K to values of type V;
Set[T] is a hash set of T. Both are constructed empty, with their type
arguments written out (Map[String, Int](), Set[Int]()) or taken from
the expected type (var m: Map[String, Int] = Map(), return Set() in
a function returning Set[Int], section 2.8), and both are library objects: there is no literal and no
m[k] indexing. The key type must satisfy Hashable (E0206): the
integer types, Bool and String do, and an object does when it declares
impl Hashable with pure func hash() -> Int and pure func
equals(other: Hashable) -> Bool (section 5.5); two keys are the same
entry when their hashes agree and equals is true. The value type may be
anything that can be stored in a list, including lists, rc handles,
closures and other maps; function, rc and pointer types are written as
type arguments like any other (Map[String, func(Int) -> Int](),
Map[Int, rc Node]()). A function type, an rc handle or a pointer is
not Hashable, so it cannot be a key or a set item.
| Method | Meaning |
|---|---|
m.put(k, v) |
insert or overwrite; the old value is dropped |
m.get(k) -> V |
the value; panics when the key is absent |
m.get_or(k, d) -> V |
the value, or d when the key is absent |
m.has(k) -> Bool |
whether the key is present |
m.remove(k) -> Bool |
drop the entry; whether it was present |
m.len() -> Int, m.clear() |
entry count; drop every entry |
m.keys() -> [K], m.values() -> [V] |
fresh lists of copies, in no particular order |
m.copy(), m.take() |
an independent copy; move the contents out, leaving m empty |
s.add(x) -> Bool |
insert; whether x was new |
s.has(x), s.remove(x) -> Bool, s.len(), s.clear() |
as for a map |
s.values() -> [T], s.copy(), s.take() |
as for a map |
A map or set owns its keys and values exactly as a list owns its elements
(section 6.8): put and add take ownership of their arguments (a value
copied out of a binding is retained or cloned first), get and get_or
hand out a copy that the caller owns, remove, clear and scope exit
drop what they remove, and a map binding is never duplicated implicitly
(E0328): write copy() or take(), or share it as rc Map[K, V].
put, remove, clear, add and take need a var binding (E0235).
for (k, v) in m visits every entry with read-only copies of the key and
the value; for x in s visits every item. Iteration order is unspecified,
and the loop body must not add or remove entries of the map it iterates.
get on a missing key is a panic ("key not found in map"), so test with
has or use get_or when the key may be absent.
Maps and sets can be elements of a list ([Map[String, Int]],
[Set[String]]): the list owns its tables, copy() clones each one, and
ms[i].put(k, v) changes the table in place. A list of channels
([Channel[T]]) owns its channels and destroys them with the list.
object Pt impl Hashable { val x: Int val y: Int pure func hash() -> Int { return self.x * 31 + self.y } pure func equals(other: Hashable) -> Bool { if other is Pt { return self.x == other.x and self.y == other.y } return false } } func main() { var counts = Map[String, Int]() for w in ["a", "b", "a"] { counts.put(w, counts.get_or(w, 0) + 1) } var keys = counts.keys() keys.sort() println("{counts.len()} {keys[0]} {counts.get("a")} {counts.has("z")}") var total = 0 for (k, v) in counts { total += v } println("{total} {counts.remove("b")} {counts.remove("b")}") var seen = Set[Pt]() println("{seen.add(Pt(1, 2))} {seen.add(Pt(1, 2))} {seen.has(Pt(1, 2))} {seen.len()}") var groups = Map[String, [Int]]() groups.put("even", [2, 4]) var even = groups.get("even") even.push(6) groups.put("even", even.take()) println("{groups.get("even").len()}") }
2 a 2 false 3 true false true false true 1 3
9.11 net, http, websocket and url (import)
The networking modules are documented in docs/networking.md.
In summary: net provides Net.tcp_dial(host, port, timeout),
Net.tcp_listen(host, port), TcpListener.accept, TcpSocket.read,
TcpSocket.write, close, UDP (Net.udp_bind, Net.udp_sendto_v4,
Net.udp_sendto_v6, UdpSocket.recvfrom), IPv4Addr / IPv6Addr
parsing and formatting and Net.lookup_host; every fallible call returns a
tuple whose second element is a NetError { code: Int } (0 on success).
http provides a server (Http.listen, HttpServer.accept,
HttpConnection.read_request, send_response, Http.listen_tls), a
client (Http.get, Http.post_json, Http.request, HttpClient.open)
and response builders (Http.response, json_response, html_response,
text_response, file_response, Http.header); requests and responses
are objects with status, headers, body, error and error_message
fields. websocket provides WebSocket.connect (and the _with_ca,
_insecure and _with_protocols variants), send_text, send_bytes,
ping, receive, close, abort, is_open and the server side
HttpConnection.upgrade_websocket. url provides Url.parse(s) ->
(Url, UrlError), Url.build(u), Url.resolve(base, ref),
Url.percent_encode, Url.percent_decode and a Url.builder().
All timeouts are integer milliseconds.
Url.parse splits RFC 3986 references without decoding or case folding:
the text before the first : is the scheme when it is a letter followed
by letters, digits, +, - or . and no /, ? or # comes first
(otherwise the input is a relative reference); the user info runs to the
last @ of the authority; a host in brackets ends at the first ], which
must be followed by nothing or : and a port (2004); a host without
brackets cannot hold :, [ or ] (2003); the port is decimal digits up
to 65535 (2005), and an empty port is the scheme default (port is -1).
Errors: 2000 for an empty input, 2009 past 8192 bytes, 2007 for a %
without two hex digits in percent_decode. Url.build writes the
reference back so that Url.parse of the result gives the same fields.
Url.resolve follows RFC 3986 section 5.2; an empty query or authority
counts as absent (a Url cannot tell them apart).
A port outside 0..65535 given to net is an invalid argument (1024).
TcpSocket.write writes the whole buffer within its deadline and returns
the byte count (on an error, what was written before it); TcpSocket.read
returns what one receive gives, at most max_bytes. A WebSocket value
names its connection: after close or abort every copy of it is stale
(is_open is false, the other calls return 6005); after a receive fails
(6002 framing, 6003 too large, 6004 invalid UTF-8, or the stream ending
inside a frame) the connection is failed and later receives and sends
return 6005; is_open is false once a close frame was received.
9.11.1 json (import)
The json module is documented in docs/json.md. Json is a
JSON value: match on v.node (JsonNode.Null, Bool, Int, Float,
Str, Arr, Obj) or use the typed accessors (as_int() -> Int?,
get(key), at(i), get_string(key), pointer("/a/0"), path("a.0")).
Arrays and objects are shared between copies of a Json (clone() makes
a deep copy) and keep their insertion order; integers that fit in 64 bits
stay Int. Json.parse(text) is a strict RFC 8259 parser that returns a
JsonResult with value, error (0 on success, 4000..4099 otherwise),
error_message, line and column; Json.parse_with(text,
JsonLimits(max_depth, max_bytes)) bounds untrusted input.
to_string(), pretty() and format(indent, sort_keys) serialize with
shortest round-trip floats. JsonWriter streams JSON into a native buffer
and JsonReader walks a document without building a tree.
import json func main() { val r = Json.parse("\{\"id\": 7, \"tags\": [\"a\"], \"price\": 9.5}") val doc = r.value doc.get("tags").push(Json.str("b")) println("{r.error} {doc.get_int("id")} {doc.get("price").as_float()} {doc}") val w = JsonWriter.compact() w.begin_object() w.field_string("name", "Ann") w.field_int("visits", 3) w.end_object() println(w.finish()) }
0 7 9.5 {"id":7,"tags":["a","b"],"price":9.5}
{"name":"Ann","visits":3}9.11.2 mysql (import)
import mysql is a MySQL and MariaDB client that speaks the client/server
protocol itself (TLS, the caching_sha2_password,
mysql_native_password, sha256_password and mysql_clear_password
plugins, the text and binary protocols); it is documented in
docs/mysql.md. Mysql.connect(MysqlConfig) and
Mysql.connect_url(url) return a nocopy MysqlConn with query(sql),
exec(sql, params: [MysqlValue]) (a prepared statement, cached per
connection), run(sql), prepare(sql), begin, commit, rollback,
ping, reset, select_db, escape, quote and close. A query
returns a nocopy MysqlResult holding every row: row_count(),
column_count(), column(i), col(name), affected_rows(),
last_insert_id(), next_result() and typed readers by row and column
(int, uint, float, text, bytes, bool, datetime, time
returning T?, get_int and the like returning (T, MysqlError), and
value returning a MysqlValue). Mysql.open_pool(config,
MysqlPoolOptions(min, max)) returns an rc MysqlPool that any number of
tasks share (query, exec, run, acquire). Every operation reports a
MysqlError { code, sqlstate, message, server } (code 0 on success) and
none panics on a server or network failure.
9.12 crypto and encoding (import)
The cryptography modules are documented in docs/crypto.md.
Binary data is a String (strings hold arbitrary bytes; measure and cut it
with byte_len and byte_slice). encoding provides Hex.encode,
Hex.decode, Base64.encode, Base64.decode, Base64.url_encode and
Base64.url_decode; decoders return DecodeResult { data: String, error:
Int, error_message: String }. crypto provides digests
(Crypto.hash(HashAlgorithm.Sha256, data), Crypto.sha256(data), ... and
the streaming Hasher.new(alg) / Hasher.hmac(alg, key) with update and
finish), Crypto.hmac, Crypto.hkdf, Crypto.pbkdf2, Crypto.scrypt,
password hashing (hash_password, verify_password,
password_needs_rehash), constant_time_equal, xor, random_bytes,
random_int, random_token, authenticated encryption (seal, open,
encrypt, decrypt with AeadAlgorithm.Aes256Gcm or ChaCha20Poly1305)
and RSA-OAEP. Fallible calls return CryptoResult { data: String, error:
Int, error_message: String }; a forged ciphertext is error 9004, not a
panic. The module is OpenSSL 3's libcrypto and links it like http does.
import crypto import encoding func main() { println(Hex.encode(Crypto.sha256("abc"))) val key = Crypto.random_bytes(32) val box = Crypto.encrypt(AeadAlgorithm.ChaCha20Poly1305, key, "secret", "") val back = Crypto.decrypt(AeadAlgorithm.ChaCha20Poly1305, key, box.data, "") println("{back.error} {back.data} {Base64.url_encode("hi?")}") }
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 0 secret aGk_
9.13 raylib (import)
import raylib makes the raylib bindings available: the objects
Vector2, Vector3, Color, Rectangle, Texture, Font and so on,
the enums KeyboardKey, MouseButton, ConfigFlags and others, and the
functions as snake_case methods on namespace objects (Window.init,
Window.should_close, Draw.begin, Draw.text, Input.is_key_down, ...).
The full binding list is src/stdlib/raylib.iron, and the graphics guide at
ironlang.dev/raylib shows complete programs.
10. Programs, projects and the command line
10.1 Single files
ironc build file.iron compiles one file to a binary next to it (-o
picks the path), ironc run file.iron compiles and runs it, and
ironc check file.iron type-checks it. iron build file.iron, iron run
file.iron and iron check file.iron do the same. A single file may
import only standard library modules.
10.2 Packages
iron init name (or iron init --lib name) creates a package: a
directory with an iron.toml manifest, src/main.iron (or src/lib.iron
for a library) and a .gitignore. Inside a package, iron build compiles
every .iron file under src/ and vendor/ into target/, iron run
builds and runs it, iron check type-checks the same sources, iron test
runs the package's test "name" { ... } blocks (section 10.2.1) and every
tests/test_*.iron program, and iron fmt file.iron reformats a file
(--check only reports, and exits with 0 when the file is formatted, 1
when it would change, 2 when it does not parse and 3 when it cannot be
read). iron fmt --migrate-comments [paths] rewrites
the transitional -- comments, /// documentation lines and @fusible
in every .iron file under the given files or directories (the current
directory by default) to // and //iron:fusible (section 1.2); it
changes only comments as the lexer reads them, checks that every rewritten
file still lexes, parses and documents the same, and with --check only
lists the files it would change. In a package, iron build, iron run and iron
check take no file argument (program arguments follow -- in iron
run), and iron test takes one, a name filter (iron test -- -name for
a filter that starts with -); any other argument is an error, as is a
second file given to ironc. Builds and checks of the same package may run
at the same time. All files of a package share one namespace: a
pub declaration in one file is visible in every other file, and a
private one only in its own (E0320), subject to the interface and extern
exceptions in section 5.10. import of a package file is
optional and documents the dependency. The files are compiled in this
order, which is the order of their globals' initialization where those do
not depend on each other (section 5.7): the vendored files, src/lib.iron,
the other files under src/ sorted by path, src/main.iron.
[package] name = "demo" version = "0.1.0" type = "bin" # or "lib" description = "optional" iron = ">= 4.4.0" # optional compiler version constraint
Values are TOML basic strings (the escapes \", \\, \t, \n and
\r); a malformed string stops the build with its line. The name is the
name of the output file (target/<name>, target/lib<name>.a), so it may
not be empty, . or .., or contain / \ : " < > | ? * or control
characters; iron init without a name derives it from the directory, with
such characters replaced by _. type is "bin" or "lib".
The iron constraint uses full X.Y.Z versions with the operators >=,
>, <=, <, = (or no operator for an exact version), ^ (same
major, or same minor before 1.0) and ~ (same minor), and comma-separated
clauses are combined with AND (">= 4.0.0, < 5.0.0"). A pre-release such
as 4.4.0-alpha sorts before 4.4.0. A mismatch stops the build with the
version to install. There is no [dependencies] table: a manifest that
declares one fails with a vendoring hint.
10.2.1 Test blocks
A test "name" { ... } declaration, at the top level of any file, is a
test: a body checked like a function's, run by iron test (or ironc test
file.iron) and left out of every other build. test is a keyword only in
this position. The tests of a package are compiled with its sources, so a
test in src/ can call that file's private functions; each runs in its own
process, and a failed assert, assert_eq, assert_ne or a panic fails
it. The name is a string literal without interpolation (escapes allowed).
Each selected test prints test <name> ... ok or test <name> ... FAILED
followed by its output, indented; a failed assert(cond) reports its
file:line (the path as the build names the file, src/a/b.iron in a
package), assert_eq also the expected and actual values. The run ends
with P passed, F failed (and , K filtered out when a filter, a
substring of the names, left tests out) and fails when a test failed.
func parse_digit(c: String) -> Int { return "0123456789".index_of(c) } test "digits parse" { assert_eq(parse_digit("7"), 7) assert_eq(parse_digit("x"), -1) }
10.3 Third-party code
Iron has no package manager, registry or lockfile. To use third-party
Iron code, copy its source into vendor/<name>/ and commit it. iron
build, iron run and iron check compile every .iron file under
vendor/ together with src/: a vendored directory that has its own
iron.toml and src/ contributes only its src/, other directories
contribute every .iron file recursively, and tests/, examples/,
target/ and hidden directories are skipped. Vendored code shares the
package namespace, so its pub declarations are used directly; two
vendored libraries that declare the same name are a duplicate declaration
(E0201).
10.4 Build flags
iron build and iron run accept -o path / --output path,
--release (optimized C compilation), --no-optimize (skip Iron's own
IR optimizations), --debug (debug information on the .iron lines,
without optimization or inlining, for gdb, lldb or the Visual Studio
debugger), --debug-build (keep the generated C under
.iron-build/), --emit-c (build only: write the C file and stop
before the C compiler), --dump-ir-passes, --report-compression,
--report-bounds-checks,
--warn-fusion-break, --force-comptime (ignore the comptime cache),
--target=web (section 10.5) and --target=<os>-<arch> (cross
compilation, section 10.6). --no-strict-v3 accepts a few removed syntax forms for
debugging old code. --verbose prints the generated C and the link line
and --version prints the compiler version and the toolchain it compiles
with. On Linux and macOS the environment variable IRON_CFLAGS adds C
compiler flags (separated by spaces, at most 16) to the compilation of the
program and of the runtime built into it, for example
IRON_CFLAGS="-g -fsanitize=address,undefined" iron build for a sanitizer
build.
--report-bounds-checks prints the source line, function and access kind
for each checked list, array or view access left after Iron optimization,
then a total. It does not weaken a check and omits checks in the prepended
standard library.
10.5 The web target
iron build --target=web compiles a package to WebAssembly with the
Emscripten toolchain pinned in .emsdk-version, producing
dist/web/index.html together with its .js loader and .wasm module.
It runs on every host Iron supports; emcc (or emcc.bat on Windows)
must be on PATH, as emsdk_env leaves it. The [web] table of the
manifest configures it:
[web] title = "My App" # page title shell = "custom_shell.html" # optional HTML template initial_memory = 67108864 # bytes stack_size = 5242880 # bytes pthread_pool_size = 4 assets = ["assets/sprites.png"] # files preloaded into the virtual FS
A web program cannot await (E0501) and must drive its frame loop from
main with a single canonical while loop (E0700 to E0703).
10.6 The backend
Iron compiles to C and hands the C to a compiler it ships. The generated
C is C17 with the GNU extensions clang accepts under -std=gnu17
(statement expressions and __builtin_* intrinsics, __attribute__
annotations, _Static_assert), compiled with -fwrapv (integer overflow
wraps), -fno-strict-aliasing and -ffp-contract=off (every float
operation is rounded on its own; a * b + c is never fused into one
multiply-add, so a float result is the same on every target). It includes only the runtime header,
which itself depends only on the freestanding C headers (stdint.h,
stddef.h, stdbool.h, stdarg.h, stdatomic.h); every fixture in the
test suite also compiles with -ffreestanding -nostdinc.
The C compiler is a pinned toolchain, the same on every machine: clang,
lld, llvm-ar, llvm-dlltool and compiler-rt from LLVM 23.1.2, built
for the X86, AArch64 and WebAssembly targets and published per host
(macOS arm64 and x86_64, Linux x86_64 and arm64, Windows x86_64) as the
toolchain-23.1.2-1 release. ironc looks for it at $IRON_TOOLCHAIN,
then next to itself in <prefix>/lib/iron/toolchain/, then in
~/.iron/toolchain/23.1.2-1/, where it downloads it on first use and
verifies its SHA-256. It never uses a compiler from PATH, refuses a
bundle built from another LLVM release and reports the one it uses in
iron --version; iron toolchain info, iron toolchain path and
iron toolchain install expose the same lookup. IRON_TOOLCHAIN is a
developer override: a mismatched version there only warns.
A native build targets the host: aarch64-apple-darwin and
x86_64-apple-darwin against libSystem, x86_64-linux-gnu and
aarch64-linux-gnu against glibc, and x86_64-pc-windows-msvc against
the Universal C Runtime. On Windows and macOS a native build links
against the host's runtime bundle (below) when one is installed; the
Windows release archive ships one next to ironc, so no Visual Studio
Build Tools are needed there. Otherwise, and on Linux, the runtime and
standard library C sources are compiled together with the program, so a
native build also needs the platform's C library headers and link inputs:
the Visual Studio Build Tools on Windows, the Xcode command line tools on
macOS, the C library development package on Linux.
When a platform prerequisite is missing ironc names it and offers to run
the installer; iron toolchain check probes for them explicitly.
iron build --target=<os>-<arch> (and ironc build --target=...) cross
compiles instead. The targets are linux-x86_64, linux-arm64,
macos-arm64, macos-x86_64 and windows-x86_64. A cross build needs no
SDK: the generated C
is freestanding, and the program is linked against a runtime bundle,
the Iron runtime and standard library and the compiler builtins
precompiled for the target by the pinned toolchain, plus the target's C
library. On Linux that is a static musl, linked with ld.lld, and the
result is a static executable that runs on any distribution. On macOS
the program is linked with ld64.lld against
libSystem through a libSystem.tbd text stub that lists the symbols
the runtime uses; dyld binds them to the real library at run time,
the minimum system version is macOS 11, and arm64 executables are ad-hoc
signed by the linker. On Windows
the program is linked with lld-link against the Universal C Runtime
and the other system DLLs it calls (ucrtbase, kernel32, ws2_32,
bcrypt, all part of Windows 10 and later) through import libraries in
the bundle, with the bundle's own entry point in place of the MSVC
startup code; the executable needs neither the Build Tools nor the Visual
C++ redistributable, and a debug build carries CodeView information in a
PDB. Each release publishes
iron-rt-<version>-<target>.tar.gz with a .sha256 sidecar; ironc
looks for the bundle at $IRON_RT_DIR/<target>, then in
<prefix>/lib/iron/rt/<target>, then in ~/.iron/rt/<version>/<target>,
where it downloads and verifies it on first use. A bundle must come from
the same compiler version and commit as ironc; IRON_RT_DIR is the
developer override that only warns. In a package iron build --target=
writes the binary to target/<target>/<name>; iron run and ironc run
refuse a cross target because the binary cannot execute on the host, and
--target is not accepted for library packages or together with
--target=web.
Every bundle carries TLS: OpenSSL 3.5, built statically for the target,
is linked into programs that import http or websocket. Servers are
verified against the system's trust: the CA bundle of the Linux
distribution or macOS (SSL_CERT_FILE and SSL_CERT_DIR override it),
and on Windows the chain is checked by the system (CryptoAPI), which also
fetches roots Windows has not stored yet. The Windows and macOS bundles
also carry raylib; on macOS it is linked against .tbd stubs for the
frameworks it uses (AppKit, Foundation, CoreFoundation, CoreGraphics,
IOKit, libobjc). A Linux cross build cannot use raylib, because a
static executable cannot load the system's OpenGL; such a program is
built on the target, natively.
11. Diagnostics
The compiler reports errors as error[E0nnn] and warnings as
warning[W0nnn], each with the source location and usually a hint. The
codes cited in this manual:
| Code | Meaning |
|---|---|
| E0001 to E0005 | lexical errors: unterminated string, invalid character, invalid number, out of memory, string too long |
| E0101, E0102 | unexpected token, expected expression |
| E0175, E0176 | keyword used as a binding name; field without val or var |
| E0200, E0201 | undefined identifier; duplicate declaration |
| E0202, E0215, E0216, E0217, E0218 | type mismatch; return type; argument count; argument type; not callable |
| E0203, E0234, E0235, E0266 | reassigning a val; writing a field of a val; mutating call on a val; writing a parameter |
| E0204 | using a nullable value without a null check |
| E0205, E0206 | missing interface method; unsatisfied generic constraint |
| E0207, E0212, E0213, E0214, E0274, E0275 | heap value escapes; free/leak of a non-heap value; leak of an rc; free/leak of a non-binding |
| E0208 | write to an outer binding inside parallel |
| E0209 | module not found |
| E0210 | self outside a method |
| E0219, E0220 | no such field; no such method |
| E0223 | circular by-value type (an object that contains itself, also through T?) |
| E0222 | mixing Int and Float |
| E0224, E0225, E0226, E0227, E0228 | non-exhaustive match; pattern arity; unreachable arm; pattern binding shadows a name; unknown variant |
| E0229 | empty list literal without a type |
| E0230, E0231, E0232 | comptime step limit; unsupported comptime construct; comptime error |
| E0233 | bitwise operator on a non-integer |
| E0237 | method name reserved by a pub field accessor, or a method named drop (the destructor is drop { ... }) |
| E0238 to E0245 | method tier violations (readonly writes, pure I/O and calls, modifier placement) |
| E0246 to E0252 | init rules (read before assign, unassigned field, double assign, method on partial self, early return, delegation, return value) |
| E0253, E0254, E0255 | patch adds a field; patch target not found; patch redefines a method |
| E0256, E0257 | init in an interface; tier mismatch with the interface |
| E0262, E0264, E0265 | inline field default; var fields without an init; assigning a val field after its init |
| E0268, E0270, E0271, E0294, E0295, E0296 | pointer arithmetic; address of a temporary; escaping stack reference; regime errors; & on an rc |
| E0273, E0274, E0297, E0298 | heap/rc/pool in an invalid position |
| E0278 | I/O in a readonly method |
| E0284 to E0288 | duplicate drop/copy; copy of a nocopy value (also a capture that is not a borrow, 6.7); readonly drop; early return in drop |
| E0289 | checked/unchecked pointer mismatch |
| E0293 | missing return |
| E0299, E0301 | dereferencing a weak rc; rc inside an arena block |
| E0310, E0311, E0312 | invalid cast; constant does not fit; constant index out of range |
| E0314 | possibly uninitialized var |
| E0320, E0321 | private declaration used from another file; standalone func Type.method form |
| E0322, E0323, E0324, E0325, E0326 | unsupported is; unsupported match subject; lambda parameter type; awaited twice; thread pools |
| E0328, E0329, E0330 | implicit list copy or capture; indexing an unordered list; address of a growable list element or of a field inside one |
| E0368 | invalid //iron:align, or over-aligned storage not supported yet |
| E0369 | a view escapes its source or a returned view has no unique source parameter |
| E0370 | a view source is not borrowable or is changed while the view is live |
| E0335 | break / continue outside a loop, in a lambda, defer or spawn body inside one, or in a parallel loop |
| E0336 | return inside a defer body |
| E0346 | a for loop body adds to, removes from or replaces the list it iterates |
| E0350, E0351 | static function called on a value; method called through the type name |
| E0352, E0353 | implementor's function static where the interface's is not (or the reverse); implementor's parameters or result differ from the interface signature |
| E0354 | static function, or method built from Self, called through an interface value |
| E0355 | T.f() on a type parameter whose constraint declares no static function f |
| E0356 | __fields, reserved for code the compiler generates |
| E0357 to E0361 | reflection outside expansion; non-object reflection target; non-constant comptime if; unmatched comptime match; field builder cannot initialize a type |
| E0331, E0332, E0333, E0334 | list extension with a body; refutable nested pattern; Channel.new(4) and the other .new constructor spellings; interpolating a value with no text form |
| E0345 | initialization cycle between globals |
| E0501 | await on the web target |
| E0700 to E0703 | web main loop rules |
| W0601, W0604, W0605, W0606 | narrowing cast; spawn data race; arena skips drop; heap value never freed |
| W0611, W0613, W0614 | unused import; var never reassigned; var parameter never reassigned |
| W0617 | unknown //iron: compiler hint, or one on a declaration it does not apply to |
| W0618 | //iron:fusible on a function that has an effect or can panic (reported by build and run) |
| W0631 | //iron:inline cannot be honored because the function is recursive |
docs/dev/diagnostic-codes.md lists every code with its message.
12. Not yet implemented
Settled design decisions that the compiler does not implement yet, listed so that older material is not mistaken for the current language:
- Thread pools: the
poolkeyword,spawn("name", pool)andfor ... parallel(pool). - Reading and writing a primitive through a pointer (
*p). - Lambda parameter inference outside a function-typed parameter position.
String,BoolandFloatsubjects inmatch.- A
nocopy objectcannot currently also bepub(#840). - raylib in Linux cross builds: it needs the system's OpenGL, which a static musl executable cannot load.
- over-aligned heap, rc, arena, interface, closure and spawned-task storage;
stack, field, fixed-array and list storage already honor
//iron:align.
13. Complete syntax of Iron
This section gives the syntax of Iron in extended BNF, one production per
construct, as implemented by the parser in src/parser/parser.c.
{ x } means zero or more repetitions of x, [ x ] means optional,
( x | y ) groups alternatives and 'x' is a literal token. IDENT,
INT, FLOAT and STRING are the lexical tokens of section 1; STRING
covers plain, multi-line and interpolated strings. NAME is an identifier
or one of the keywords allowed in name position. Newlines and comments are
not part of the grammar: the parser skips them between any two tokens (see
section 1.6). The grammar is checked against the fixture corpus by
scripts/grammar_check.py.
Comments are lexical (section 1.2):
comment ::= '//' { any character except newline }
directive ::= '//' KEY ':' { any character except newline }
KEY ::= 'a'..'z' { 'a'..'z' | '0'..'9' | '_' }
doc_run ::= { own-line comment, one per line, no blank line between }
(documents the declaration, field, enum variant or
interface method on the next line)The iron directive values implemented here are fusible, inline,
noinline, cold, and align=N|cacheline. Function directives attach to
the following declaration. align may also precede an object, field or
local binding. Directives are lexical comments, so they add no grammar
production.
-- and /// are transitional spellings of //, and @fusible of the
directive //iron:fusible (section 1.2).
program ::= { decl }
decl ::= import_decl
| val_decl
| var_decl
| [ ( 'nocopy' | 'pub' ) ] object_decl
| [ 'pub' ] ( func_decl | extern_decl | patch_decl | interface_decl | enum_decl | array_ext_decl
| type_method_decl )
| test_decl
import_decl ::= 'import' IDENT { '.' IDENT }
func_decl ::= [ '@' 'fusible' ] 'func' IDENT [ generic_params ] param_list [ '->' type ] block
test_decl ::= 'test' STRING block
extern_decl ::= 'extern' 'func' IDENT param_list [ '->' type ]
array_ext_decl ::= [ '@' 'fusible' ] 'func' '[' IDENT ']' '.' NAME [ generic_params ] param_list [ '->' type ] block
type_method_decl ::= [ '@' 'fusible' ] 'func' IDENT '.' NAME [ generic_params ] param_list [ '->' type ] block
generic_params ::= '[' generic_param { ',' generic_param } [ ',' ] ']'
generic_param ::= IDENT [ ':' IDENT ]
param_list ::= '(' [ param { ',' param } [ ',' ] ] ')'
param ::= [ 'val' | 'var' ] NAME [ ':' type ]
object_decl ::= 'object' IDENT [ generic_params ] [ impl_clause ] '{' { member } '}'
impl_clause ::= 'impl' IDENT { ',' IDENT }
member ::= [ 'pub' ] [ 'readonly' | 'pure' ] ( field | method | init_decl | 'copy' block | 'drop' block )
| [ 'pub' ] 'static' method
field ::= ( 'val' | 'var' ) IDENT ':' type
method ::= 'func' NAME [ generic_params ] param_list [ '->' type ] block
init_decl ::= 'init' [ IDENT ] param_list block
patch_decl ::= 'patch' 'object' IDENT [ impl_clause ] '{' { patch_member } '}'
patch_member ::= [ 'pub' ] [ 'readonly' | 'pure' ] ( method | init_decl | 'copy' block | 'drop' block )
| [ 'pub' ] 'static' method
interface_decl ::= 'interface' IDENT '{' { iface_method } '}'
iface_method ::= [ 'static' | 'readonly' | 'pure' ] 'func' IDENT param_list [ '->' type ] [ block ]
enum_decl ::= 'enum' IDENT [ generic_params ] '{' [ variant { ',' variant } [ ',' ] ] '}'
variant ::= IDENT [ '(' type { ',' type } [ ',' ] ')' | '=' INT ]
val_decl ::= 'val' ( binding [ ':' type ] [ '=' init_expr ]
| '(' binding { ',' binding } [ ',' ] ')' [ ':' type ] '=' expr )
var_decl ::= 'var' binding [ ':' type ] [ '=' init_expr ]
binding ::= IDENT | '_'
init_expr ::= spawn_expr | expr
block ::= '{' { stmt } '}'
stmt ::= val_decl | var_decl | return_stmt | if_stmt | while_stmt | for_stmt
| match_stmt | defer_stmt | free_stmt | leak_stmt | in_arena_stmt
| comptime_for_stmt | comptime_if_stmt | comptime_match_stmt
| spawn_expr | block | expr_stmt
return_stmt ::= 'return' [ expr ]
if_stmt ::= 'if' expr block { 'elif' expr block } [ 'else' block ]
while_stmt ::= 'while' expr block
for_stmt ::= 'for' ( IDENT | '(' IDENT ',' IDENT ')' ) 'in' expr
[ 'parallel' [ '(' expr ')' ] ] block
match_stmt ::= 'match' expr '{' { match_arm } [ 'else' '->' arm_body ] '}'
match_arm ::= pattern '->' arm_body
arm_body ::= block | stmt
comptime_for_stmt ::= 'comptime' 'for' IDENT 'in' 'fields' '(' type ')' block
comptime_if_stmt ::= 'comptime' 'if' expr block
{ 'elif' expr block } [ 'else' block ]
comptime_match_stmt ::= 'comptime' 'match' type '{'
{ type '->' arm_body }
[ 'else' '->' arm_body ] '}'
pattern ::= [ IDENT '.' ] IDENT [ '(' [ sub_pattern { ',' sub_pattern } [ ',' ] ] ')' ]
| expr
sub_pattern ::= '_' | IDENT '.' IDENT [ '(' [ sub_pattern { ',' sub_pattern } [ ',' ] ] ')' ] | IDENT
defer_stmt ::= 'defer' ( 'free' expr | block | stmt )
free_stmt ::= 'free' expr
leak_stmt ::= 'leak' expr
in_arena_stmt ::= 'in' expr block
spawn_expr ::= 'spawn' '(' STRING [ ',' expr ] ')' block
expr_stmt ::= expr [ assign_op expr ]
assign_op ::= '=' | '+=' | '-=' | '*=' | '/=' | '<<=' | '>>=' | '&=' | '|=' | '^='
expr ::= unary { binary_op unary | 'is' IDENT }
binary_op ::= 'or' | 'and' | '|' | '^' | '&' | '==' | '!=' | '<' | '>' | '<=' | '>='
| '<<' | '>>' | '+' | '-' | '*' | '/' | '%'
unary ::= '-' unary | 'not' unary | '~' unary | '&' unary
| 'heap' [ heap_opts ] unary | 'rc' unary | 'weak' 'rc' ( 'null' | unary )
| 'comptime' unary | 'await' unary | postfix
heap_opts ::= '(' heap_opt { ',' heap_opt } ')'
heap_opt ::= 'in' ':' expr | 'allow_drop_skip' ':' ( 'true' | 'false' )
postfix ::= primary { '.' NAME [ type_args ] [ call_args ]
| '.' '(' IDENT ')'
| '[' expr [ '..' [ expr ] ] ']'
| '[' elem { ',' elem } ']'
| call_args }
elem ::= expr | type
call_args ::= '(' [ expr { ',' expr } [ ',' ] ] ')'
type_args ::= '[' type { ',' type } ']'
primary ::= INT | FLOAT | STRING | 'true' | 'false' | 'null' | IDENT | 'self'
| '(' expr ')'
| '(' elem ',' elem { ',' elem } [ ',' ] ')'
| '[' type ';' expr ']'
| '[' [ elem { ',' elem } [ ',' ] ] ']'
| lambda
lambda ::= 'func' param_list [ '->' type ] block
type ::= 'weak' 'rc' type
| 'rc' type
| [ '?' ] ptr_type
| tuple_type
| list_type
| func_type
| reflected_type
| named_type
reflected_type ::= IDENT '.' 'type'
ptr_type ::= '*' [ 'var' ] [ 'unchecked' ] type
tuple_type ::= '(' type ',' type { ',' type } ')'
list_type ::= '[' elem_type { ',' list_attr } [ ';' [ '<=' ] expr ] ']' [ '?' ]
elem_type ::= func_type | 'rc' type | 'weak' 'rc' type | list_type | ptr_type | tuple_type | named_type
list_attr ::= 'layout' ':' ( 'soa' | 'aos' ) | 'unordered'
func_type ::= 'func' [ '(' [ type { ',' type } ] ')' ] [ '->' type ]
named_type ::= IDENT '?' [ type_args ] | IDENT [ type_args ] [ '?' ]
NAME ::= IDENT | 'init' | 'copy' | 'drop' | 'null' | 'free'Operator precedence is given in section 3.1: expr is parsed by
precedence climbing over the flat sequence of unary operands and
operators, with every binary operator left associative and is binding
loosest of all. In postfix, X.Y(args) and X.Y where X and Y both
start with an uppercase letter denote an enum variant construction, and
x.m[T](args) is only read as a generic method call when the token after
[ is a type name starting with an uppercase letter, [, or a token that
starts a type by the rule below, and a ( follows the ]. Between the
brackets of postfix and of a list primary, and between the parentheses
of a tuple primary, an element is read as a type when
- it starts with
?or*: no expression does (there is no prefix*operator), soSet[*Node]()andChannel[?*Node](1)hold types; - it starts with
funcand its signature is not followed by{: alambdaalways has a block, soChannel[func(Int) -> Int](4)andMap[String, func(func(Int) -> Int) -> Int]()hold function types while[func(x: Int) -> Int { return x }],[func(x) { return x }]and[func() { }]are lists of lambdas; - it starts with
rcorweak rc(other than the expressionweak rc null) between the brackets ofpostfix, where nothing is indexed by a handle (Box[rc Node](n),Channel[weak rc Node](8)). In a list or a tuplerc xis an element ([rc p, rc Node(1)]), so there such an element is atypeonly when what follows thercis one by this rule ([rc func(Int) -> Int],[rc *Node],[rc Node?]);[rc Node]stays anrcexpression and reads as the list type where a type is expected (Map[String, [rc Node]]()); - it contains
?outside parentheses (Map[String, Int?](),Map[String, [Int?]]()).
Every other element is an expr; one that names a type (Int,
Map[String, Int], [Int], (Int, String)) is taken as that type where
a type argument is expected. In pattern,
the first alternative is used when the arm starts with IDENT '.' or with
an uppercase identifier followed by (, and expr otherwise. The
standalone form func (r: T) method() is recognized only to report
E0260. type_method_decl (func Type.method()) and array_ext_decl
(func [T].method()) declare the standard library's methods; in a program
they are errors (E0321, and E0331 for a list extension with a body),
and patch object is how a program adds methods to a type.