Reference / Types
Types
A type is a set of values and the operations that apply to them. Every value, binding and expression in Sisu has one type, and sisu knows it before the program runs.
fn main() {
let n: i64 = 42
let b: u8 = 255
let x: f64 = 2.5
let ok: bool = true
let c: char = 'ä'
let s: string = "sauna"
print("{n} {b} {x} {ok} {c} {s}") // 42 255 2.5 true ä sauna
}
These are the built-in types:
| Type | Values |
|---|---|
i64 |
The integers from -9223372036854775808 to 9223372036854775807 |
u8 |
The integers from 0 to 255 |
f64 |
64-bit floating-point numbers, as IEEE 754 defines them |
bool |
true and false |
char |
One Unicode scalar value |
string |
Text in UTF-8 |
A program can also use arrays, such as [i64], optionals, such as i64?, and the classes and enums that it declares.
Type names
A type name comes after a : in a let, a parameter or a field, and after -> for the return type of a function.
fn twice(n: i64) -> i64 {
return n * 2
}
fn main() {
let names: [string] = ["Aino", "Eero"]
let found: i64? = None
print(twice(21)) // 42
print(names.len()) // 2
print(found ?? 0) // 0
}
The forms of a type name:
- A built-in type, such as
i64. - The name of a class or an enum that the program declares. A generic class or enum also takes type arguments, such as
Stack<i64>(Generics). [T], an array of elements of the typeT.T?, an optional of the typeT.
A name that is not a type is an error (E0030).
When a let has no type name, the binding gets the type of its value. A None or a [] alone does not tell its type, so it needs a type name (E0050).
fn main() {
let found = None // error[E0050]
}
E0050: what this error means
Integers
An i64 is a signed 64-bit integer. A u8 is an unsigned 8-bit integer, a byte. An integer literal is an i64, unless its context makes it a u8 or an f64 (Literals).
fn main() {
let n = 1000
let b: u8 = 200
print(n * 3) // 3000
print(b + 55) // 255
}
The rules:
- When the result of integer arithmetic is outside the range of its type, the program stops with a panic (Operators). In a constant, this is an error when the program builds (E0088).
- A
u8cannot be negative, so a unary-on au8is an error (E0042). - An
i64and au8do not mix in one operation. Convert one of them first (Conversions).
fn main() {
let b: u8 = 200
let n = 1000
print(n + b) // error[E0032]
}
E0032: what this error means
Floating-point numbers
An f64 is a 64-bit floating-point number. Its arithmetic follows IEEE 754 and never panics. A division by zero gives an infinity or NaN ("not a number"), and a result that is too large gives an infinity.
fn main() {
print(7.0 / 2.0) // 3.5
print(0.1 + 0.2) // 0.30000000000000004
print(1.0 / 0.0) // inf
print(-1.0 / 0.0) // -inf
print(0.0 / 0.0) // nan
print(1e300 * 1e300) // inf
}
The rules:
- NaN is not equal to any value, also not to itself. To test for NaN, use
x.is_nan().x == f64.nanis always false, andx != f64.nanis always true. Both build with a warning (W0005).<,<=,>and>=with NaN are always false. -0.0 == 0.0is true, butprintshows the sign:-0.0.- An
f64and an integer do not mix in one operation. Convert one of them first (Conversions).
fn main() {
let x = f64.nan
print(x == x) // false
print(x.is_nan()) // true
print(-0.0 == 0.0) // true
}
Constants
f64 has these constants. Write them after the type name, such as f64.pi.
| Constant | Value |
|---|---|
f64.pi |
π, 3.141592653589793 |
f64.tau |
2π, 6.283185307179586 |
f64.e |
e, 2.718281828459045 |
f64.infinity |
Positive infinity. -f64.infinity is negative infinity |
f64.nan |
NaN |
f64.max |
The largest finite f64, 1.7976931348623157e308 |
f64.min_positive |
The smallest positive normal f64, 2.2250738585072014e-308 |
f64.epsilon |
The difference between 1.0 and the next larger f64, 2.220446049250313e-16 |
i64 and u8 have no constants.
Methods on numbers
i64 and f64 have these methods. Each parameter has the type of the receiver. u8 has no methods.
| Method | Receivers | Result |
|---|---|---|
x.min(y) |
i64, f64 |
The smaller of x and y |
x.max(y) |
i64, f64 |
The larger of x and y |
x.clamp(lo, hi) |
i64, f64 |
x.max(lo).min(hi): x moved into the range from lo to hi |
x.abs() |
f64 |
The absolute value |
x.sqrt() |
f64 |
The square root |
x.pow(y) |
f64 |
x to the power y |
x.exp() |
f64 |
e to the power x |
x.ln(), x.log10(), x.log2() |
f64 |
The logarithm to the base e, 10 or 2 |
x.floor(), x.ceil() |
f64 |
The nearest whole number below or above x |
x.trunc() |
f64 |
x without its fraction |
x.round() |
f64 |
The nearest whole number. A half goes away from zero |
x.sin(), x.cos(), x.tan() |
f64 |
The sine, cosine or tangent of x radians |
x.asin(), x.acos(), x.atan() |
f64 |
The inverse sine, cosine or tangent, in radians |
y.atan2(x) |
f64 |
The angle in radians of the point (x, y) |
x.hypot(y) |
f64 |
The square root of x * x + y * y |
x.is_nan() |
f64 |
true when x is NaN |
x.is_finite() |
f64 |
true when x is not an infinity and not NaN |
x.is_infinite() |
f64 |
true when x is an infinity |
The f64 methods return an f64, except the three is_ methods, which return a bool. min, max and clamp return the type of the receiver.
fn main() {
print(2.0.sqrt()) // 1.4142135623730951
print(2.5.round()) // 3.0
print((-2.5).round()) // -3.0
print(7.clamp(0, 5)) // 5
print(1.0.atan2(0.0)) // 1.5707963267948966
}
The rules:
- When
lois larger thanhi,clampgiveshi. - On
f64,min,maxandclamporder-0.0below0.0. When one operand ofminormaxis NaN, they give the other operand.
fn main() {
print(7.clamp(5, 0)) // 0
print((-0.0).max(0.0)) // 0.0
print(f64.nan.min(2.0)) // 2.0
}
bool
A bool is true or false. The condition of an if or a while must be a bool: Sisu does not change a number or another value into a bool (E0032).
fn main() {
let n = 3
if n > 0 {
print("positive") // positive
}
}
fn main() {
let n = 3
if n { // error[E0032]
print("positive")
}
}
E0032: what this error means
char
A char is one Unicode scalar value: a code point from 0 to 10FFFF, but not D800 to DFFF. Two chars compare by their code points.
fn main() {
let c = 'ä'
print(c) // ä
print(i64(c)) // 228
print('a' < 'ä') // true
}
Arithmetic does not apply to a char (E0094). To calculate with a code point, convert the char to an i64 and back.
fn main() {
let c = 'a'
let next = char(i64(c) + 1) ?? c
print(next) // b
}
string
A string is text in UTF-8. + joins two strings, and a string literal can interpolate values (Literals). Two strings compare byte by byte, which is the order of their code points.
fn main() {
let name = "Aino"
let greeting = "Hello, " + name
print(greeting) // Hello, Aino
print("Z" < "a") // true
}
A string is not an array. An index on a string is an error (E0068). Strings and formatting tells the methods of a string and its format specs.
Optionals
An optional T? holds a value of the type T, or None. Use ?? to give a value for None, and if let to use the value only when it is there.
fn main() {
let found: i64? = 7
let missing: i64? = None
print(found ?? 0) // 7
print(missing ?? 0) // 0
if let n = found {
print(n * 2) // 14
}
}
The rules:
- Where an optional
T?is expected, aTbecomes aT?that holds it. Some examples are aletwith a type, an assignment, an argument and areturnvalue. Others are a field, an array element, the other side of an==or!=, and the other branches of anifor amatch. Noneneeds an expected optional type. Where the context expects a type that is not optional,Noneis an error (E0032).- An optional is not its value. An arithmetic operator, a comparison, a field or a method of
Tdoes not apply to aT?(E0032, E0062). Use??,?.(Operators) orif let. x == Noneandx != Nonetest whetherxholds a value. Whenxis not optional, the test is an error (E0044).- An optional of an optional, such as
i64??, is a type. A value becomes an optional through one?layer only, and??,if letand?.take off one layer at a time. See Optionals of optionals. printand interpolation do not take an optional (E0059, E0098). The section Option and Result is in development, and it changes this rule.
fn half(n: i64) -> i64? {
if n % 2 == 1 {
return None
}
return n / 2
}
fn main() {
print(half(10) ?? -1) // 5
print(half(7) ?? -1) // -1
let o = if half(4) == 2 { 1 } else { None }
print(o ?? 0) // 1
}
fn main() {
let o: i64? = 5
print(o + 1) // error[E0032]
}
E0032: what this error means
Option and Result
T? is short for Option<T>, and T | E is short for Result<T, E>. Option and Result are enums that a program uses with no declaration. Both spellings of a type are the same type.
fn half(n: i64) -> Option<i64> {
if n % 2 == 1 {
return None
}
Some(n / 2)
}
fn exact_half(n: i64) -> Result<i64, string> {
if n % 2 == 1 {
return Err("{n} is odd")
}
Ok(n / 2)
}
fn main() {
let a: i64? = half(8)
match a {
Some(n) => print(n) // 4
None => print("odd")
}
match exact_half(7) {
Ok(n) => print(n)
Err(reason) => print(reason) // 7 is odd
}
}
The rules:
Option<T>is the enum with the variantsNoneandSome(T).Result<T, E>is the enum with the variantsOk(T)andErr(E). Errors and Result tells how a result is used.Some,None,OkandErrare names in expressions and in patterns, without the name of the enum.Some(p)andNonematch the optional itself. A variant, a literal or a range matches the value inside it, and a name or_matches the whole optional. Only the top of anif letor awhile lettakes off one layer. Patterns on an optional has the rules.- A
print,eprintor interpolation of an optional builds with a warning, because the text can beNone.x.to_string()gives the same text with no warning, for a program that wantsNonein its output.
Optionals of optionals In development
An optional can hold an optional. i64?? is Option<Option<i64>>. So generic code that finds the first element of a [i64?] gives an i64??. None tells that the array is empty. Some(None) tells that the first element is None.
fn first<T>(xs: [T]) -> T? {
if xs.len() > 0 { xs[0] } else { None }
}
fn main() {
let xs: [i64?] = [None, 5]
let a: i64?? = first(xs)
print((a ?? 7) ?? 8) // 8
let b: i64?? = Some(None)
print((b ?? Some(7)) ?? 8) // 8
let c: i64?? = None
print((c ?? Some(7)) ?? 8) // 7
}
The rules:
- A plain value becomes an optional through one layer only. The middle state of an
i64??is writtenSome(None). ??andif letpeel one layer. On ani64??namedx,x ?? 5gives ani64?, and(x ?? None) ?? 0gives ani64.if let y = xbindsywith the typei64?.?.removes one layer, as it does now. Ifa?.bhas abof the typei64?, it gives ani64?, not ani64??.- A type argument can be an optional (Type arguments).
Noneprints asNoneat every depth. AT?that holdsxprints the text ofx. Whenxis itself an optional that holdsNoneat its innermost layer, the text isSome(, the text ofx, and). So every state of a nested optional prints in its own way. Aprintof an optional still builds with the warning;to_stringdoes not.
fn main() {
let a: i64?? = None
let b: i64?? = Some(None)
let c: i64?? = Some(5)
let d: i64??? = Some(Some(None))
print(a.to_string()) // None
print(b.to_string()) // Some(None)
print(c.to_string()) // 5
print(d.to_string()) // Some(Some(None))
}
never
never is the type of an expression that never finishes. These expressions have the type never:
- A call of
panic, andtodo. - A block that reaches a
return,breakorcontinuestatement, or another expression of the typenever. - An
ifor amatchwhose branches are all of the typenever. - A
loopwith nobreakthat ends it, or whosebreaks all have a value of the typenever. - A
whilewhose condition is the literaltrue, with nobreakthat ends it. - A string literal with an interpolation part of the type
never.
A program cannot write never as a type name. A never branch of an if or a match takes the type of the other branches. So does a never break value of a loop. The right side of ?? can be never.
fn parse(text: string) -> i64 {
let n = if text == "one" { 1 } else { return -1 }
return n * 10
}
fn main() {
let o: i64? = 3
print(o ?? panic("no value")) // 3
print(parse("one")) // 10
print(parse("two")) // -1
}
The rules:
- An expression of the type
neverhas no value, so aletcannot bind it (E0043), andprintcannot take it (E0061). - Code after an expression of the type
nevercannot run, and the program builds with a warning (W0001). - A function body that ends in an expression of the type
neverneeds no value at its end (Statements).
fn first_even(xs: [i64]) -> i64 {
for x in xs {
if x % 2 == 0 {
return x
}
}
panic("no even number")
}
fn main() {
print(first_even([3, 8, 5])) // 8
}
unit In development
unit is the type of an expression that gives no value, such as a call of print. It is an ordinary type. A program can write it, for example in -> unit.
fn main() {
// Prints x, then ().
print(print("x"))
// Prints a, then b, then true.
print(print("a") == print("b"))
}
The rules:
unitcan be a type argument, such as[unit],unit?andStack<unit>. This lifts the rule in Type arguments.unithas no literal. Aunitvalue comes from a call, such asprint(...).- A
unitvalue prints as(). It hasEqandOrd, and allunitvalues are equal.
Conversions
A conversion is a call written with the name of a type, such as f64(n). Sisu never converts a value to another type without one, except an integer literal (Literals) and a T that becomes a T?.
fn main() {
let n = 7
let x = 2.5
print(f64(n) * x) // 17.5
print(u8(n) ?? 0) // 7
print(i64(x) ?? 0) // 0
}
| Conversion | From | Result | None when |
|---|---|---|---|
f64(n) |
i64 |
f64 |
Never. Above 2^53, it gives the nearest f64 |
i64(x) |
f64 |
i64? |
x is not a whole number, is NaN or an infinity, or is outside the range of i64 |
i64(b) |
u8 |
i64 |
Never |
i64(c) |
char |
i64 |
Never. The result is the code point |
u8(n) |
i64 |
u8? |
n is outside 0 to 255 |
char(n) |
i64 |
char? |
n is not a Unicode scalar value |
string(cs) |
[char] |
string |
Never |
A conversion that can fail returns an optional. It never panics. To round an f64 to an i64, round it first, for example i64(x.round()).
fn main() {
print(i64(2.7) ?? -1) // -1
print(i64(2.7.round()) ?? -1) // 3
print(u8(300) ?? 0) // 0
print(char(228) ?? '?') // ä
print(string(['o', 'k'])) // ok
}
The rules:
- A conversion takes only the types in its row of the table. A
u8converts to anf64or acharthrough ani64, such asf64(i64(b)). - The argument of
f64,i64,u8orcharhas no expected type. So an integer literal in it is ani64:f64(3)is3.0, andu8(300)isNone. - The argument of
stringexpects a[char], sostring([])is the empty string. i64(-0.0)is0.- A conversion to the type that the value already has is an error (E0054).
- In a constant, a conversion that can fail is an error (E0100).
fn main() {
let n = 5
print(i64(n)) // error[E0054]
}
E0054: what this error means
Parsing a string In development
i64(s) on a string gives an i64?. It follows the rule for a conversion that can fail: the result is None, not a panic.
fn main() {
print(i64("42") ?? -1) // 42
print(i64("-7") ?? -1) // -7
print(i64("4 2") ?? -1) // -1
print(i64("+5") ?? -1) // -1
}
The result is None unless s is an optional - followed by one or more ASCII digits, and the value fits in an i64. A +, a space, an underscore or 0x gives None.
Sisu