Skip to content

expressions

Expressions in ghūl are constructs that evaluate to a value. They can be used to perform calculations, make comparisons, and combine values in various ways.

literals

integers

Integer literals consist of an optional radix (base), followed by a sequence of digits with optional underscores for readability, followed by a dot and a decimal fraction and/or exponent (for floating point numbers) and finally a type suffix.

ghul
let i = 12_345_678; // int
let hex = 0x1234_ABCD; // int
let long = 1_000_000_000_000_000L; // long
let hex_unsigned_long = 0x1234_5678__9ABC_DEF0_UL; // ulong
let b = 99b; // byte

char

ghul
let c = 'c';
let u_macron = 'ū'

floating point

ghul
let s = 123.456; // single
let t = 123.456E5; // single
let d = 123.456D; // double
let e = 123_456_789_000.0D // double

string

ghul
let hello_world = "Hello World!";
let unicode = "ghūl programming language"

array

Array literals are constructed from a comma separated list of element values enclosed in [ and ]. The array element type is inferred as the most specific type compatible with all elements (which may be object if no more specific ancestor type exists). The resulting array type is E[] where E is the inferred element type.

ghul
let animals = ["frog", "bat", "elephant"]; // string[]
let things = ["frog", 1234, 12.5]; // object[]
let lists = [[1, 2], [3, 4], [5, 6], [7, 7]]; // int[][]

tuple

Tuple literals are constructed from a comma separated list of elements enclosed in ( and ). Each element can be a bare value or a named value, and each element can optionally specify a type. Where explicit types are omitted, element types will be inferred.

ghul
let path_with_id = (path = "/tmp/my-file.txt", id = 1234);
let path = path_with_id.path;
let id = path_with_id.id;

If tuple elements are not explicitly named, they are assigned names consisting of a back-tick followed by an index

ghul
let things = ("thing", 12.34);
let name = things.`0;
let weight = things.`1;

function

Function literals are constructed from an parenthesized argument list, a return type, and a return expression or a function body. If there is only one argument, no parentheses are needed.

expression body function literal

ghul
let simple_add = (x: int, y: int) -> int => x + y;

block body function literal

ghul
let complex_add = (x: int, y: int) -> int is
let result = x + y;
return result;
si;

type inference

Return type can usually be omitted provided it can be inferred from the type of the expression body or any values returned from the block body

ghul
let simple_add = (x: int, y: int) => x + y;
let complex_add = (x: int, y: int) is
let result = x + y;
return result;
si;

Argument types usually can be inferred if the function literal is being passed into a function.

ghul
let list = [1, 2, 3, 4, 5];
list | .filter(element => element < 3);

capturing and closure

A function literal can refer to identifiers from its surrounding lexical scope; those references form its closure. What the closure sees through each identifier depends on whether the local variable it refers to is mutable.

An immutable local (let) is captured by value at the point the function literal is constructed. The closure holds a snapshot of that value:

ghul
let g = () => i;

Inside a loop, each iteration produces its own local, so each closure captures its own value:

ghul
// Define a list to hold the closures:
let closure_list = LIST();
// Iterate over an integer range:
for i in 1::10 do
// Create a closure capturing i's current value
let closure = () => i;
// Add the closure to the list:
closure_list.add(closure);
od
// Each closure captured the value of i at the
// time of its creation:
for closure in closure_list do
write_line("Closure captured value: {closure()}");
od

A mutable local (let followed by mut) is captured by reference: the closure shares one live variable with the outer scope and with any other closures over the same name. Assignments are visible in both directions.

ghul
let counter mut = 0;
let bump = (n: int) is
counter = counter + n;
si;
let peek = () -> int => counter;
bump(10);
bump(5);
write_line("counter = {counter}, peek() = {peek()}");
counter = 15, peek() = 15

Mutability of the captured variable is independent of mutability of the value it holds. When the captured value is a reference to an object, the closure cannot reassign the variable to point at a different object, but it can still call methods or assign to properties on the object it currently refers to:

ghul
let object_reference = SOME_OBJECT();
let closure = () => object_reference.some_property;

Here closure cannot make object_reference point somewhere else, but it can read and modify the state of the SOME_OBJECT it currently refers to.

arithmetic

Arithmetic expressions allow you to perform mathematical calculations using operators such as +, -, *, /, and %.

ghul
let sum = 10 + 5; // Addition
let difference = 10 - 5; // Subtraction
let product = 10 * 5; // Multiplication
let quotient = 10 / 5; // Division
let remainder = 10 % 3; // Modulo (remainder)

comparison

Comparison expressions allow you to compare values using operators such as ==, !=, <, >, <=, and >=.

ghul
let equal = 5 == 5; // Equality
let not_equal = 5 != 10; // Inequality
let less_than = 5 < 10; // Less than
let greater_than = 10 > 5; // Greater than
let less_than_or_equal = 5 <= 5; // Less or equal
let greater_than_or_equal = 10 >= 10; // Greater or equal

short circuit logical

Logical expressions allow you to combine or negate boolean values using the && (logical AND), || (logical OR), and ! (logical NOT) operators.

ghul
let logical_and = true /\ false; // Logical AND
let logical_or = true \/ false; // Logical OR
let logical_not = !true; // Logical NOT

Evaluation stops as soon as the result is known

conditional

Conditional expressions allow you to evaluate different expressions based on a condition using the if-then-else construct.

ghul
let max = if a > b then a else b fi;

function call

Function call expressions allow you to invoke functions and methods by providing the necessary arguments.

ghul
let result = sum(10, 5);

property access

Property access expressions allow you to access the properties of an object using the dot notation.

ghul
let length = "Hello".length;

indexer

Indexer expressions allow you to access elements of an array or collection using square brackets.

ghul
let first_element = [1, 2, 3][0];

constructor

Constructor expressions allow you to create new instances of classes or structs by invoking their constructors.

ghul
let point = POINT(10, 20);

type cast

Type cast expressions allow you to explicitly convert a value from one type to another using the cast keyword.

ghul
let integer_value = cast int(3.14);

default value

A default expression evaluates to the default value of a type: null for reference types, the zero value for numeric and other value types.

default[T] pins the type explicitly. A bare default takes its type from the surrounding context — a typed let, an assignment, or a return:

ghul
let a = default[int]; // 0
let b: string = default; // null
zero[T]() -> T => default;

let a = default initialises a local to its type's default value, where the type is inferred from how the local is later used.

These are the main types of expressions in ghūl. They can be combined and nested to form more complex expressions and statements:

ghul
entry() is
let x = 10;
let y = 5;
let sum = x + y;
let product = x * y;
let is_greater = x > y;
if is_greater then
IO.Std.write_line("x is greater than y");
else
IO.Std.write_line("x is not greater than y");
fi
let numbers = [1, 2, 3, 4, 5];
let first_number = numbers[0];
IO.Std.write_line(
"The first number is: {first_number}"
);
si
x is greater than y
The first number is: 1