Skip to main content

Expressions

Expressions are things that can be evaluated to obtain a value. Combinations of values, functions, and operations form expressions. Additionally, control flow mechanisms such as when and match are expressions.

let x: Int = 2 * 4
let y: String = when {
x < 0 => "negative"
_ => "non-negative"
}
assert_eq(y, "non-negative")

Blocks

A block is a group of expressions. The value of the final expression in the block is the value of the block.

let message = {
let a = "have a byte"
let b = "of cake"
a + " " + b
}
assert_eq(message, "have a byte of cake")

Expression terminals

There are no explicit end of expression terminators, and expressions can easily span multiple lines. The compiler will automatically determine where an expression ends. An expression ends at the end of a line where the last token is any of the following:

  • An identifier.
  • A basic literal (such as an integer or a string).
  • The return keyword.
  • One of the puntuation ), ], or }.

Precedence

The following table shows all operator and expression types sorted by decreasing precedence (i.e., expressions at the top of the table have higher precedence and are evaluated first).

Operator/ExpressionDescriptionAssociativity
()
[]
{}
Grouping
a::b
a.b
a.0
a[b]
f(a)(using b)
Path
Property
Tuple element
Index
Function application
Left to right
a ** bExponentiationRight to left
!a
-a
..a
Logical not
Negation
Spread
Right to left
a * b
a / b
a % b
Multiplication
Division
Modulo
Left to right
a + b
a - b
Addition
Subtraction
Left to right
a | fPipeLeft to right
a == b
a != b
a < b
a <= b
a >= b
a > b
Equality
Inequality
Lesser
Lesser or equal
Greater or equal
Greater
Left to right
a && bLogical andLeft to right
a || bLogical orLeft to right
a..b
a..=b
Exclusive range
Inclusive range
Right to left
when { ... }When expression
match x { ... }Match expression
fn(x) { ... }Anonymous function
,SeparatorLeft to right
returnReturn

Bindings

A value is anything that can be bound to a name and is often referred to as a variable. In Fuyu, the use of 'variable' is avoided: instead, a name is said to be bound to a value in a particular context.

let message = "hello"
// ^ ^
// name value

Most types can be determined by inference, but types can be specified with :.

let n = 123 // Int is inferred.
let m: Int = 123
assert_eq(n, m)

Names created in different ways have strict naming rules. These are not simply convention - the compiler will reject programs that do not follow these rules.

Name ofCreated withExampleRegular Expression
Valuelet, using, match, function argumentx, some_value_*[a-z][a-z0-9_]*
Functiondeff, read_all_*[a-z][a-z0-9_]*
TypetypeList, SomeType[A-Z][A-Za-z0-9]*
Type parametertype, let, def, providea, type_param_*[a-z][a-z0-9_]*
ModuleName of the module filestd, some_module_*[a-z][a-z0-9_]*

When an name with the same name as a reserved word is required, a raw name starting with r# can be used. For example, when is a keyword, but r#when is a name.

A namespace is created when a module is imported. Namespaces are not the same as names; thus, a namespace and a name can be identical, and there is no ambiguity.

// value.fuyu
export let value: Int = 100

// main.fuyu
import value // value namespace.

def main() {
let value = "hello" // Local value name.
assert_eq(value, "hello") // Use the local value name.
assert_eq(value::value, 100) // Use the value namespace.
}

Scope

The scope of binding is the context in which a specific name is bound to a specific value. Names (e.g., those created by let or provide) can only be accessed within their scope. The following create new scopes:

  • Bodies of let, def, provide, and fn,
  • Within (...), [...], and {...}.
import std/fmt
import std/io

let x = 5
// x added to scope

when {
x < 0 => {
io::println(fmt::debug(x))
let y = 2 * x // y added to scope
// y goes out of scope
}
_ => {
io::println(fmt::debug(x))
let y = x + 1 // y added to scope
// y goes out of scope
}
}

// Uncommenting the following line is an error because y is not in scope.
// io::println(fmt::debug(y))

A binding can be overwritten with a different value and type. This binding is effective for the rest of the scope where the binding occurs.

let x = 1
assert_eq(x, 1)
let x = "hello"
assert_eq(x, "hello")

A new scope can be created anywhere with a pair of {...}. This can be useful for locally scoping intermediate values.

{
let a = 1
}
// Uncommenting the following line is an error because a is not in scope.
// io::println(fmt::debug(a))