Syntax
Inko uses a familiar syntax using curly braces, and is easy to understand by both humans and computers.
Modules
Each Inko source file is a module and may contain the following:
- Imports
- Constants
- Classes
- Methods
- Trait implementations
- Classes that are reopened
- Comments
Unlike languages such as Ruby and Python, it's not valid to include expressions directly in a module, i.e. this is invalid:
import std.stdio (Stdout)
Stdout.new.print('hello')
class async Main {
fn async main {}
}
Imports
The import
statement is used to import a module or symbols from a module. The
syntax is as follows:
import mod1.mod2 # This imports `mod1.mod2` and exposes it as `mod2`
import mod1.mod2 (A) # This imports the symbol `A`
import mod1.mod2 (A, B) # This imports the symbols `A` and `B`
import mod1.mod2 (self) # This imports `mod2` from module `mod1`
You can also alias symbols when importing them:
import mod1.mod2 (A as B) # `A` is now exposed as `B`
You can also import module methods:
import std.process (sleep)
Imports may specify one or more build tags, resulting in the compiler only
processing the import
if all the build tags match:
import foo if linux and amd64
The syntax for the tags is import FOO if TAG1 and TAG2 and ...
.
External imports
The syntax import extern "NAME"
is used to link against C libraries. The
string "NAME"
specifies the library name, not the file name or path of the
library. For example, to import libm:
import extern "m"
Constants
Constants are defined using let
at the module top-level. Constants are limited
to integers, floats, strings, arrays of constants, and binary expressions of
constants:
let A = 10
let B = 10.5
let C = 'foo'
let D = A + 5
let E = [A, 10]
let F = 10 + A
Constants are made public using let pub
:
let pub A = 10
Strings used as constant values don't support string interpolation.
Methods
Methods are defined using the fn
keyword. At the top-level of a module only
instance methods can be defined (i.e. static methods aren't valid directly in a
module). Methods are private by default.
Methods use the following syntax:
# A private immutable instance method:
fn method_name {}
# A public immutable instance method:
fn pub method_name {}
# A private mutable instance method:
fn mut method_name {}
# A private moving method:
fn move method_name {}
# A public mutable instance method:
fn pub mut method_name {}
# A public mutable async instance method (only available in async classes):
fn pub async mut method_name {}
# Method names may end with a ?, this is used for predicate methods:
fn valid? {}
# Method names may also end with a =, this is used for setters:
fn value= {}
# Static and instance methods can use the `inline` keyword to ensure they're
# always inlined:
fn inline foo {}
fn pub inline foo {}
fn pub inline mut foo {}
Methods with arguments are defined like so, optional arguments aren't supported:
fn method_name(arg1: ArgType, arg2: ArgType) {}
A return type is specified using ->
:
fn method_name -> ReturnType {}
Type parameters are specified before regular arguments:
fn method_name[A, B](arg1, A, arg2: B) {}
Like classes, you can specify a list of required traits:
fn method_name[A: ToFoo + ToBar, B](arg1, A, arg2: B) {}
The method's body is contained in the curly braces:
fn method_name {
[10, 20]
}
External functions
Signatures for C functions are defined using the fn extern
syntax. These
functions can't define any generic type parameters, can only be defined at the
top-level of a module (i.e. not in a class), and can't specify the mut
keyword. For example:
import extern "m"
fn extern ceil(value: Float64) -> Float64
Variadic functions are defined using ...
as the last argument:
fn extern printf(format: Pointer[UInt8], ...) -> Int32
If a body is given, the method is instead defined instead of the compiler expecting it to be defined elsewhere:
fn extern example -> Int {
42
}
In this case, variadic arguments are not supported.
Classes
Classes are defined using the class
keyword:
class Person {}
A class can define one or more fields using let
:
class Person {
let @name: String # `@name` is the field name, and `String` its type
let @age: Int
}
To make a class public, use class pub
like so:
class pub Person {
let @name: String # `@name` is the field name, and `String` its type
let @age: Int
}
Instances of classes are created using the same syntax as method calls:
Person(name: 'Alice', age: 42)
Positional arguments are also supported:
Person('Alice', 42)
If no fields are given, the parentheses are required:
class Example {}
Example()
Enums
Enums are defined as follows:
class pub enum Result {}
Enum classes allow defining of constructors using the case
keyword:
class enum Result {
case Ok
case Error
}
Enum classes can't define regular fields.
Generic classes
Generic classes are defined like so:
class enum Result[T, E] {
case Ok(T)
case Error(E)
}
Here T
and E
are type parameters. Type parameters can also list one or more
traits that must be implemented before a type can be assigned to the parameter:
class enum Result[T, E: ToString + ToFoo + ToBar] {
case Ok(T)
case Error(E)
}
Type parameters can also specify the mut
requirement, restricting the types to
those that allow mutations:
class MyBox[T: mut] {}
Processes
Processes are defined as async classes like so:
class async Counter {
}
class pub async Counter {
}
C structures
C structures are defined using class extern
. When used, the class can't define
any methods or use generic type parameters:
class extern Timespec {
let @tv_sec: Int64
let @tv_nsec: Int64
}
Methods
Classes can define static methods, instance methods, and async methods (in case the class is an async class):
class Person {
let @name: String
# This is a static method, available as `Person.new`.
fn static new(name: String) -> Person {
# ...
}
# This is an instance method, which is only available to instances of
# `Person`.
fn name -> String {
# ...
}
}
class async Counter {
fn async increment {
# ...
}
}
Reopening classes
A class can be reopened using the impl
keyword like so:
impl String {}
Within the body, only methods are allowed; fields can only be defined when the class is defined for the first time.
Traits
Traits are defined using the trait
keyword, and like classes default to being
private.
Traits are defined like so:
trait ToString {}
trait pub ToString {}
Required traits
Traits can specify a list of other traits that must be implemented before the trait itself can be implemented:
trait Debug: ToString + ToFoo {}
Methods
Traits can define both default and required methods:
trait ToString {
# A required instance method:
fn to_string -> String
# A default instance method:
fn to_foo -> String {
'test'
}
}
Traits can't specify static or async methods.
Traits can also define type parameters:
trait ToArray[T] {}
And like classes and methods, these can define required traits:
trait ToArray[T: ToFoo + ToBar] {}
Type parameters can also specify the mut
requirement, restricting the types to
those that allow mutations:
trait ToArray[T: ToFoo + mut] {}
Implementing traits
Traits are implemented using the impl
keyword:
impl ToString for String {}
The syntax is impl TraitName for ClassName { body }
. Within the body only
instance methods are allowed.
Comments
Comments start with a #
and continue until the end of the line:
# This is a comment that spans a single line
Multiple uninterrupted comment lines are treated as a single comment:
# This is
# a single
# comment.
# But this is a separate comment due to the empty line above.
Identifiers
Identifiers are referred to by just using their name:
this_is_an_identifier
Identifiers can contain the following characters: a-z
, A-Z
, 0-9
, _
, $
,
and may end with a ?
.
While identifiers can contain dollar signs (e.g. foo$bar
), this is only
supported to allow use of C functions using a $
in the name, such as
readdir$INODE64
on macOS. Do not use dollar signs in regular Inko method
or variable names.
The self
keyword is used to refer to the receiver of a method. This keyword is
available in all methods, including module and static methods.
Field references
Fields are referred to using @NAME
where NAME
is the name of the field:
@address
Constant references
Constants are referred to as follows:
let NUMBER = 42
fn example {
NUMBER
}
It's also possible to refer to a constant in a module like so:
import foo
fn example {
foo.SOME_CONSTANT
}
Scopes
Scopes are created using curly braces:
'foo'
{ # <- This is the start of a new scope
'bar'
} # <- The scope ends here
Strings
Inko has two types of strings: single quoted and double quoted strings. Both types of strings are the same semantically. Strings support the following escape sequences:
\"
\'
\0
\\
\e
\n
\r
\t
\#
For example:
'hello\nbar'
"hello\nbar"
Strings can span multiple lines:
"this string spans
multiple
lines"
Strings support Unicode escape sequences using the syntax \u{XXXXX}
:
'foo\u{AC}bar'
Strings support string interpolation:
let name = 'Alice'
"hello ${name}" # => 'hello Alice'
'hello ${name}' # => 'hello Alice'
Here ${
marks the start of the embedded expression, and }
the end. To use a
literal ${
, escape the $
sign:
let name = 'Alice'
'hello \${name}' # => 'hello \${name}'
Integers
The syntax for integers is as follows:
10
0x123
Underscores in integer literals are ignored, and are useful to make large numbers more readable:
123_456_789
Floats
Floats are created using the usual floating point syntax:
10.5
-10.5
10e+2
10E+2
Arrays
Arrays are created using flat brackets:
[]
[10]
[10, 20]
Booleans
Booleans are created using true
and false
.
Inko doesn't have a dedicated negation operator such as !expression
or not expression
.
Instead, you'd use the predicate methods Bool.true?
and Bool.false?
.
Bool.true?
returns true
if its receiver is also true
,
while Bool.false?
returns true
if the receiver is false.
Here's a simple example:
if volume_is_too_loud.false? {
turn_volume_to(11)
}
Nil
The nil
keyword is used to create an instance of Nil
.
Conditionals
if
expressions use the if
keyword:
if foo {
one
} else if bar {
two
} else {
three
}
Loops
Infinite loops are created using the loop
keyword:
loop {
# ...
}
Conditional loops are created using the while
keyword:
while condition {
# ...
}
break
and next
can be used to break out of a loop or jump to the next
iteration respectively. Breaking a loop with a value (e.g. break 42
) isn't
supported.
Pattern matching
Pattern matching is performed using the match
keyword:
match value {
case PAT -> BODY
}
Each case starts with the case
keyword, specifies one or more patterns, an
optional guard, and a body to run if the pattern matches.
The following patterns are supported:
- Integer literals:
case 10 -> BODY
- String literals:
case 'foo' -> BODY
- Constants:
case FOO -> BODY
- Bindings:
case v -> BODY
,case mut v -> BODY
(allows reassigning ofv
) - Wildcards:
case _ -> BODY
- Enum constructors:
case Some(v) -> BODY
- Classes:
case { @name = name } -> BODY
- Tuples:
case (a, b, c) -> BODY
- OR patterns:
case 10 or 20 -> BODY
Guards are also supported:
match foo {
case Some(num) if num > 10 -> foo
case _ -> bar
}
String literals used as patterns don't support string interpolation.
Closures
Closures are created using the fn
keyword. Unlike methods, the argument types
are optional.
fn {}
fn (a, b) {}
fn (a: Int, b: Int) {}
fn (a, b) -> ReturnType {}
Tuples and grouping
Expressions are grouped using parentheses:
(10 + 5) / 2 # => 7
Tuples are also created using parentheses, but must contain at least a single comma:
(10) # => Int
(10,) # => Tuple1[Int]
(10, 20) # => Tuple2[Int, Int]
Borrows
Borrows are created using ref
and mut
:
ref foo
mut bar
Recover expressions
Recovering is done using the recover
keyword:
recover foo
recover { foo }
Returning values
The last value in a body is its return value. Explicitly returning values is
done using return
:
return 42
Throwing values is done using throw
:
throw 42
This is syntax sugar for the following:
return Result.Error(42)
Error handling
try
is available as syntax sugar for a match
, and supports values of type
Result
and Option
.
try option
(where option
is an Option
) is syntax sugar for the following:
match option {
case Some(val) -> val
case None -> return Option.None
}
try result
(where result
is a Result
) is syntax sugar for the following:
match result {
case Ok(val) -> val
case Error(error) -> return Result.Error(error)
}
Defining variables
Variables are defined using let
:
let number = 42
let number: Int = 42
let _ = 42
By default a variable can't be assigned a new value. To allow this, use let
mut
:
let mut number = 42
number = 50
Pattern matching in a let
isn't supported.
Assigning variables
Variables are assigned a new value using the syntax VAR = VAL
. Inko also
supports swapping of values using the syntax VAR := VAL
:
let mut a = 42
a := 50 # => 42
a # => 50
Method calls
Methods without arguments can be called without parentheses. If arguments are given, parentheses are required:
self.foo # calls foo() on self
self.foo() # Same thing
foo(10, 20)
self.foo(10, 20)
Named arguments are also supported:
foo(name: 10, bar: 20)
When using named arguments, they must come after positional arguments:
foo(10, bar: 20)
Binary expressions
Binary operator expressions use the following syntax:
left OP right
For example:
10 + 5
Operators are left-associative. This means 5 + 10 * 2
evaluates to 30
, not
25
. The following binary operators are supported:
+
, -
, /
, *
, **
, %
, <
, >
, <=
, >=
, <<
, >>
, |
, &
, ^
, ==
, !=
, >>>
Inko also supports two logical operators: and
and or
. These operators have a
higher precedence than the regular binary operators. This means
1 + 2 and 3 + 4
is parsed as (1 + 2) and (3 + 4)
. and
and or
have the
same precedence as each other.
Indexing
Inko doesn't have dedicated indexing syntax such as array[index]
or
array[index] = value
, instead you'd use the methods get
, get_mut
, and
set
like so:
let numbers = [10, 20]
numbers.get(1) # => 20
numbers.set(1, 30)
numbers.get(1) # => 30
Type casts
Type casting is done using as
like so:
expression as TypeName
The as
keyword has the same precedence as binary operators. This means that
this:
10 + 5 as ToString
Is parsed as this:
(10 + 5) as ToString
And this:
foo as Int + 5 as Foo
Is parsed as this:
(foo as Int + 5) as Foo