Skip to Content
i documentation
The language standardChapters

1. Words and symbols

1.1 Source files

A source file is UTF-8 text, and its name ends in .i (the normal surface) or .is (the Lisp surface, §14). \r\n and \r are the same as \n.

1.2 Comments

A comment starts with // and goes to the end of the line. A comment that starts with /// is the documentation of the item after it. A comment that starts with //// is the documentation of the module. Comments have no effect on the meaning of a program.

1.3 Names

name    = [ "_" ] lower { lower | digit | "_" } .
discard = "_" | "__" { lower | digit | "_" } .
UpName  = upper { upper | lower | digit } .
lower   = "a" … "z" .   upper = "A" … "Z" .   digit = "0" … "9" .
hex     = digit | "a" … "f" | "A" … "F" .   oct = "0" … "7" .   bin = "0" | "1" .
char    = (* any Unicode scalar value but '"' and "\" *) .
  • A name is a value, a parameter, a label, or a part of a module path. A name may start with one _, as in _count. It is a normal name in every way, but a compiler does not warn when it is not used.
  • A discard is _ alone, or a word that starts with two _, as in __old_value. It is a place in a pattern that binds no name; the word after __ only says what is left out.
  • An UpName is a type, a type variable (§4.7), or a constructor.

1.4 Keywords

i has no keywords. Every word is a name, an UpName, or a discard (§1.3), and any name may be used as a name. The forms that other languages make with keywords are made with brackets and symbols: imports and exports are groups at the start of a file (§2.2), matching is a call of a chain of functions (§6.12), and a guard is written with ? inside a parameter group.

Some forms are macro calls (§14): an attribute that is not built in, such as @m or @m(args) before an item (§2.2); a top-level item that is a call, m(args) or a.m(args) (§2.2); a declaration with neither @external nor @js, which goes with a typed macro binding of the same name (§14.8); and, in a type, a lowercase name or a.m followed by ( (§3). The parser reads each of them in full without knowing which names are macros. Each of them is a compile error when its name is not a macro there.

1.5 Symbols

=   :   ,   .   ..   ->   <-   ?   |   |>   @
(   )   [   ]   {   }   <<   >>
+   -   *   /   %   <   <=   >   >=   ==   !=   &&   ||   <>   &
+.  -.  *.  /.      <.  <=.  >.  >=.

Source text is cut into tokens from left to right. At every point, the longest token that matches is taken: <=. is one token, and << is one token. << is matched before <-, so <<-1 is << and -1; x<-1 is x, <- and 1, and a comparison with a negative number is written x < -1. ! alone and ^ are not tokens. < and > also enclose type parameters (§4.7). Spaces (U+0020 and tab), line ends, and comments come between tokens, and some line ends are breaks (§1.7). After ., digits are read as a decimal, never as a float: t.0.1 is (t.0).1. A token may not be followed directly by a letter, a digit, or _ that would continue it as a word or a number: 12px, 0xG, 1.0E5, and fooBar are compile errors.

1.6 Literals

int_lit    = decimal | "0x" hex { hex | "_" } | "0o" oct { oct | "_" } | "0b" bin { bin | "_" } .
decimal    = digit { digit | "_" } .
float_lit  = decimal "." [ decimal ] [ "e" [ "-" ] decimal ] .
string_lit = cooked_string | multiline_string | raw_string .
cooked_string = '"' { char | escape | interpolation } '"' .
interpolation = "\(" Expr ")" .
multiline_string = '"""' line_end { char | escape | interpolation } line_end indent '"""' .
raw_string = raw_single(h) | raw_multi(h) .
raw_single(h) = hashes(h) '"' { raw_char(h) | raw_escape(h) | raw_interpolation(h) } '"' hashes(h) .
raw_multi(h) = hashes(h) '"""' line_end { raw_char(h) | raw_escape(h) | raw_interpolation(h) }
               line_end indent '"""' hashes(h) .
raw_escape(h) = "\" hashes(h) ( "n" | "r" | "t" | "f" | '"' | "\" | "u{" hex { hex } "}" | line_end ) .
raw_interpolation(h) = "\" hashes(h) "(" Expr ")" .
escape     = "\" ( "n" | "r" | "t" | "f" | '"' | "\" ) | "\u{" hex { hex } "}" .

In the parameterized raw productions, h is the positive number of opening hashes and hashes(h) is exactly that many # characters. raw_char(h) is any character that starts neither a closing delimiter for that raw form nor a backslash followed by exactly h hashes, not more. Such a backslash must begin a valid raw escape or interpolation; an unknown active escape is a compile error. Other backslashes and quotes are ordinary content. After the opening hashes, triple quotes immediately followed by a line break select the multi-line form. Raw multi-line strings also obey the closing-line and indentation rules below.

backslash = #"\q"#
quote = #"a"b"#
main = () { (backslash, quote) }
  • A literal is never negative. A negative number is made with the operator - (Int) or -. (Float) (§6.9). In a pattern, a negative literal is written in the same way (§7.1).

  • _ only puts digits into groups. It may not be first, last, or next to another _.

  • The value of a float literal must be a finite IEEE 754 binary64 number. If not, it is a compile error.

  • \u{…} has 1 to 6 hex digits. Its value must be a Unicode scalar value: 0 to 10FFFF, but not D800 to DFFF. If not, it is a compile error.

  • An integer literal may be of any size.

  • A string may go over more than one line. Line ends in it are changed as in §1.1.

  • In an expression, "a\(e)b" interpolates an expression e, including calls, blocks and nested string literals. Every interpolated expression must have type String; there is no implicit conversion. Before the core tree, interpolation becomes <> of its parts from the left, leaving out empty text parts. "a\(x)b" is "a" <> x <> "b"; a sole expression keeps an empty part: "\(x)" is "" <> x. Interpolation is forbidden in labels, attribute arguments, import paths, literal types and patterns. The normal formatter retains interpolation as written; the Lisp surface has no interpolation and uses <>.

  • A multi-line string begins with """ immediately followed by a line break. Its closing """ is on a line preceded only by spaces or tabs. That exact indentation prefix is removed from every nonblank content line; a line with less or different indentation is a compile error. Blank lines with less indentation are accepted and made empty; indentation beyond the closing prefix remains part of a blank line. The opening line break and the line break before the closing line are not part of the value. Escapes and interpolation work as in ordinary strings. A backslash immediately before a physical line break joins that line to the next after indentation is removed.

  • A raw string surrounds its quotes with one or more matching #, as in #"text"#, ##"text"## or #""" … """#. Backslashes are literal unless followed by exactly the opening number of hashes. With one hash, \#n is a line feed and \#(e) interpolates, while \n and \(e) remain literal text. Raw multi-line strings use the same indentation and delimiter-line rules; \# at line end joins lines in a one-hash raw string. Closing quotes consume the matching number of hashes; too few do not close the string.

1.7 Lines

A line end is a break when both sides of it allow one:

  • the last token before it can end an expression: a name, an UpName, a discard, a literal, ), ], }, or >>; and
  • the first token after it can begin a statement: a name, an UpName, a discard, a literal, (, [, {, <<, -, -., .., or @, or < when it begins type parameters (below).

Comments are not tokens, so the tokens on the two sides of a comment are what count. Every line end that is not a break is only a space.

A break ends something. What it ends depends on the brackets it is directly inside:

  • Directly inside the { } of a block, and at the top level of a file, a break ends a statement or an item. Attributes are part of the item after them, so a break after an attribute does not end the item.
  • Directly inside ( ) (data, data types, data patterns, parameter groups, service patterns and needs), [ ], or << >>, a break is the same as a ,. A , at the end of a line may be left out.
  • Directly inside a { } that groups a type (§3), a break is a compile error.

With no break, as on one line, a statement is the longest expression that may be read from its start, and a , is needed between parts. So statements may share a line: b |> (True) { 1 } | (False) { 0 }.

But a statement that starts with (, -, or -. begins a new line, because the expression before it on the same line goes on with it. In { a = f (b, c) = g … }, f(b, c) is read as a call, so the statement is the expression f(b, c) followed by a stray =, a compile error. With a constructor, the one binding may be correct and mean something else: { mk = Pair (b, c) = g … } is the binding mk = Pair(b, c) = g, which takes g apart, and it does not bind mk to Pair.

From this rule:

  • When ( is on the same line as the expression before it, it is a call (§6.6). When ( is the first thing on a line, it begins a new statement or part.
  • The { of a function body (§6.8) is on the same line as the ) or the type before it.
  • A line that ends in an operator, ,, =, :, ->, <-, (, [, {, or << goes on in the next line. A line that begins with ., |, ?, ->, <-, or a binary operator other than - and -. goes on from the line before. So a branch of a chain that begins a line begins it with | (§6.12), and a provide may begin a line with <-.
  • a and then - b on the next line are two statements. To write a - b on two lines, put - at the end of the first line.
  • A line whose first tokens are <, one or more UpNames with , between them, > and ( begins a new statement: a function with type parameters (§6.8). To go on with a comparison in that form on the next line, put the operator at the end of the line before.

One rule does not depend on lines: in the -> part of a function, after the ) of its parameters, a { right after ->, |, &, or - is part of the type (§3). The first other { that is not inside brackets begins the body.

i fmt formats .i and .is source files in their own surface, including both extensions by default in src/, test/ and given directories; --check reports files that would change. Both surfaces use 80 columns. The normal surface indents with one tab per level. A : that introduces a type uses exactly the same spacing and layout rule as =: one space on each side, and the same continuation indentation and line-breaking rule for a long right side. Thus inspect : <A>(A) -> String, x : Int = e, and (deadline) <- (clock : Clock) { … }. This applies to declarations, pattern annotations and service items. Bit-array segments keep their compact <<value:Format>> spelling (§8.1): that colon introduces a format, and has no = counterpart. In the normal surface, the call depth of an expression is the number of nested function calls on the deepest path through call arguments: a call whose callee is an UpName constructor and data literals do not count, field access and pipes do not add depth, and a function literal argument starts again at 0 for its body. A call whose call depth is greater than 2 is never laid out on one line: its arguments go one per line, indented, with the closing ) on its own line (the layout of a call that does not fit the width), and the rule applies again to each argument; a block whose one statement is broken so is over several lines. The Lisp surface is laid out by Lisp convention (cljfmt indentation): a datum that fits stays on one line; closing brackets never begin a line and are gathered at the end of the form’s last line; indentation is with spaces. A broken block-like form (=, ->, |, ., @, :, !) keeps its head and distinguished first parts on the head line and indents its body two spaces; another call either keeps its first argument on the head line and aligns the others under it, or puts its arguments under the head, one space in; {, [ and << align their parts under the first. Reader prefixes stay attached, strings are not split, comments keep their place (a line comment before closing brackets moves them to the next line), and at most one existing blank line separates top-level items. A single indivisible atom, string or comment may exceed the width. i fmt --to is uses that same layout.

Formatting is idempotent: formatting its output again gives identical text. It preserves the same code as defined by §14.1, including definition kind. A successful conversion between surfaces preserves that same code; conversion there and back may change layout and literal spelling, but not the parsed program. If the destination surface cannot represent the program, conversion reports an error and leaves the source unchanged.