Skip to main content

Basics

Comments

Comments are used to annotate and document code. There are three kinds of comments.

  1. Line comments begin with // and extend to the end of the line.
  2. Documentation comments begin with /// and extend to the end of the line. Documentation comments are used to automatically create documentation from the source code. Documentation comments can use Markdown.
  3. The shebang comment is a special comment that starts with #! and may only appear as the very first text in a source file.
#!/usr/bin/env fuyu

import std/io

/// This function is called when the program is started.
main: () -> ()
main () = io.println "Hello" // Comments can appear after code.

Booleans

Booleans are represented by the Bool type, which can take on the values True and False.

let is_fuyu_cool = True

Integers

Integers are represented by the Int type, which is an arbitrary precision signed integer (commonly referred to as a bignum). By default, integers are specified in decimal (base 10). Case-sensitive prefixes of 0b for binary (base 2), 0o for octal (base 8), and 0x for hexadecimal (base 16) may be used. Underscores (_) may appear anywhere in the literal except before the first digit, after the last digit, and before, between, or after the characters of the 0b, 0o, and 0x prefixes. Multiple underscores may appear consecutively.

let integers = [
0, 50, 1_000_000, // Decimal
0b10101010, 0b1111_0110, // Binary
0o12345670, 0o123_005_774, // Octal
0x1234567890abcdefABCDEF, 0xfe_23_06, // Hexadecimal
]

// Multiple underscores may appear consecutively.
let special_number = 1_2__3___4

Floats

A Float is an IEEE 754 floating-point number with at least double precision (i.e., 64 bits). A Float literal must include either a . or e (or both) to distinguish it from an integer literal. Underscores (_) can be used to separate digits and may appear anywhere in the literal except before the first digit, after the last digit, immediately after the . (radix point), and before, between, or after the characters of the e, e+, and e-.

let floats = [
0.0, 23.45, 1_057.1, 3.141_593, // Regular notation.
1e9, 2.5e-4, 2_712.349_753e+10, // Scientific notation.
]

// Excessive underscores for some reason.
let funky_float = 6___.7_8__9e4__5
info

Since Fuyu runs on the BEAM, it uses the same rules as the BEAM for floats. In particular, a Float on the BEAM does not exactly match IEEE 754, and an operation that produces a NaN, +Inf, or -Inf will instead result in a panic.

This means that NaN, +Inf, or -Inf are not representable by a Float and do not appear in Fuyu.

Strings

A String represents textual data encoded in UTF-8. String literals use double quotes ("...") and may contain Unicode characters. Escapes are used to insert values that may not be easy or possible to type:

  • \n: Newline (U+000A).
  • \r: Carriage return (U+000D).
  • \t: Tab (U+0009).
  • \\: Backslash (\).
  • \": Double quote (").
  • \u{XXXXXX}: Unicode escape of up to 24 bits, where each X is a hex digit representing a Unicode code point. There can be 1 to 6 hex digits in the curly braces.
// These strings are equal.
let string_one = "🍉 is a watermelon"
let string_two = "\u{1F349} is a watermelon"

When a string spans multiple lines, all lines are joined by a space. Leading whitespace on continuing lines, trailing whitespace on non-terminal lines, and empty lines are ignored. Escapes such as \n and \u{...} are preserved.

// These strings are equal.
let string_one = "apples
bananas cherries"
let string_two = "apples bananas cherries"

Block strings use three double quotes to delimit the start and end. The following whitespaces are stripped: leading whitespace before the first non-whitespace line of text, leading whitespace common to the start of each line, trailing whitespace on each line, trailing whitespace after the last non-whitespace line of text, and carriage returns (U+000D) are stripped. Carriage returns included by an escape, such as \r, are respected.

// These strings are equal.
let string_one = """
text that
is spread
across
several lines
"""
let string_two = " text that\n is spread\nacross\n several lines"

Raw strings are delimited with r"..." and do not process any escapes. There can be 0 or more # characters between the r and the opening ", and the same number of # characters must appear after the closing ". Carriage returns (U+000D) are removed. The same whitespace processing rules as Strings enclosed in "..." are applied.

// These strings are equal.
let raw_string_one = r"no \n escapes"
let string_one = "no \\n escapes"

// These strings are equal.
let raw_string_two = r#"nested r"raw" string "#
let string_two = "nested r\"raw\" string"

Tuples

A tuple is an ordered collection of values of arbitrary types. Tuples are created with (...) and may have zero or more elements. Tuples are useful for representing a single value (i.e., ()) and for grouping multiple values together. The type of a tuple is simply the tuple of the types of each element.

let a: () = () // 0-tuple, called "unit".
let b: (Int,) = (3.14,) // 1-tuple, trailing comma is required.
let c: (String, String) = ("hello", "world") // 2-tuple.
let d: (Bool, String, List Int) = (True, "text", [5, 6, 7]) // 3-tuple.

Elements of a tuple can be accessed in two ways:

  1. Destructuring uses pattern matching to capture values from a tuple.
  2. Element access is more compact and generally more useful when only a single element from a tuple is required. Elements are accessed with #, where the zero-indexed position of the element in the tuple comes after the #.
let xyz = (7, 15, 3)
// Destructure.
let (_, y, _) = xyz
assert.eq y 15
// Element access.
assert.eq xyz#1 15

Lists

A List is an ordered collection of values belonging to the same type, implemented as a singly linked list. Lists are created with [...] and may have any length (including zero). Lists are type parameterized, which means that the type of a list depends on the type of the values it holds. For example, a list of integers (List Int) and a list of strings (List String) are two different types.

let a: List Bool = [] // Empty.
let b: List Int = [1, 2, 3]
let c: List String = ["one", "two", "three"]

List elements can be accessed with destructuring and make use of the .. rest pattern to capture zero or more items at the end of a list.

let xs = [6, 7, 9]
xs match
[x, ..rs] ->
assert.eq x 6
assert.eq rs [7, 8]
_ -> unreachable

A list can also be created from a different list using a spread. The spread must appear as the very last entry in the list.

let xs = [3, 4]
let ys = [1, 2, ..xs]
assert.eq ys [1, 2, 3, 4]

Expressions

Fuyu is an expression-oriented language, which means almost everything is an expression that evaluates to a value. For example, control flow structures, such as if, are expressions and can be used to obtain a value.

let x = 2 * 4
let y = if x < 0 then "negative" else "non-negative"
assert.eq y "non-negative"

Anything that can appear to the right of an = sign is an expression, which turns out to include many things such as:

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
f &[a] b
Namespace
Record field
Tuple field
Function application
Left-to-right
f @ gFunction compositionRight-to-left
a ** bExponentiationRight-to-left
~a
..a
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 == 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
doDo expression
ifIf expressionRight-to-left
whenWhen expression
x matchMatch expression
\&[x] y -> ...Anonymous function
a | fPipeLeft-to-right
panic
return
todo
try
unimplemented
unreachable
Panic
Return
Todo
Try
Unimplemented
Unreachable
Right-to-left
,SeparatorLeft-to-right
info

Left-to-right associativity, such as +, means that a + b + c is equivalent to (a + b) + c.

Right-to-left associativity, such as **, means that a ** b ** c is equivalent to a ** (b ** c).

Constants

Constants may only appear at the top level of a module. A constant consists of an interface declaration followed by its definition using an =.

small_primes: List Int      // Interface.
small_primes = [2, 3, 5, 7] // Definition.

Only the following are allowed in constants:

A constant is a declaration, not a computation, so constructs such as let, if, match, function calls, and operators are not allowed.

Let 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 the term '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

Let bindings are really a pattern match; however, they must be irrefutable, meaning that the pattern can never fail.

let (x, y) = (3, "hello")
assert.eq x 3
assert.eq y "hello"

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) can only be accessed within their scope. New scopes are created whenever indentation changes or within {...} (refer to the layout rules).

import std/io

main: () -> ()
main () = do
let x = do // `x` added to scope.
let y = 2 // `y` added to scope.
y + 1
io.println x // Works!
// Uncommenting the following line is an error because `y` is not in scope.
// io.println 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.

import std/assert

main: () -> ()
main () = do
let x = 1
assert.eq x 1
let x = "hello"
assert.eq x "hello"

Naming rules

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

Name ofExampleRegular Expression
Valuex, some_value_*[a-z][a-z0-9_]*
Functionf, read_all_*[a-z][a-z0-9_]*
TypeList, SomeType_*[A-Z][A-Za-z0-9_]*
ConstructorSome, None_*[A-Z][A-Za-z0-9_]*
Type parametera, type_param_*[a-z][a-z0-9_]*
Modulestd, some_module_*[a-z][a-z0-9_]*

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