Skip to content

Types

Grayscale is statically typed. Every variable and expression has a type known at check time.

Grayscale’s integer types are named by signedness and width:

Type Width Range
i8 8-bit -128 to 127
i16 16-bit -32,768 to 32,767
i32 32-bit -2^31 to 2^31-1
i64 64-bit -2^63 to 2^63-1
u8 8-bit 0 to 255
u16 16-bit 0 to 65,535
u32 32-bit 0 to 2^32-1
u64 64-bit 0 to 2^64-1

An integer literal with no other type to take is an i64. Arithmetic is overflow-checked: overflow or underflow produces a runtime panic rather than silent wrapping.

do main() {
mut small i64 = 42
mut large i64 = 9223372036854775807 // max i64
mut count u64 = 18446744073709551615 // max u64
mut flags u8 = 0xFF
}

A literal outside the declared type’s range, or a negative literal assigned to an unsigned type, is a check-time error. Byte data is u8: a [u8] stores one byte per element, and the stdlib’s byte-oriented functions (io.read_bytes, encoding, binary, uuid.to_bytes) take and return [u8].

Use cast to convert between integer types: cast(value, i32), cast(value, u16). A narrowing cast is range-checked at runtime.

The names int, uint, float, and byte are not built in. Code that prefers them declares them as aliases:

alias int = i64
alias uint = u64
alias float = f64
alias byte = u8
do main() {}
Type Width C Type Precision
f32 32-bit float ~7 decimal digits (IEEE 754 single-precision)
f64 64-bit double ~15 decimal digits (IEEE 754 double-precision)

A float literal with no other type to take is an f64. Use f32 when interfacing with C APIs that expect single-precision or when memory is constrained.

Division by zero with floating-point operands produces a runtime panic. However, special IEEE 754 values (NaN, Infinity, -Infinity) can appear through stdlib math functions (e.g., edge cases in trigonometric or logarithmic functions). Use math.is_nan(), math.is_infinite(), and math.is_finite() to check for these values.

do main() {
mut pi f64 = 3.14159
mut ratio f32 = 0.5
}

Integer values are implicitly promoted to a floating-point type when the target type is f32 or f64. This applies to variable declarations, assignments, function arguments, map literal values, and return statements:

do main() {
mut x f64 = 5 // 5.0
mut y f32 = 1 // 1.0
x = 42 // 42.0
}

No explicit cast is needed. The promotion is lossless for values within the floating-point range.

The string type represents a UTF-8 encoded byte sequence. String indexing (str[i]) returns the byte at byte position i, not a Unicode codepoint. len() returns the byte length, not the character count.

For ASCII strings, one byte equals one character, so indexing works as expected:

do main() {
mut greeting string = "Hello, World!"
mut first_char char = greeting[0] // 'H'
}

For multi-byte UTF-8 strings, individual bytes may not form complete characters:

do main() {
mut s string = "日本語"
println(len(s)) // 9 (byte length, not 3 characters)
println(char_count(s)) // 3 (Unicode character count)
println(to_char(s, 0)) // 日 (the character at codepoint index 0)
println(cast(to_char(s, 0), i64)) // 26085 (its Unicode codepoint value)
}

Use to_char() to access characters by codepoint index and char_count() to get the true character count. to_char() returns a char; apply cast(c, i64) to it for the numeric codepoint.

The bool type has exactly two values: true and false.

do main() {
mut flag bool = true
mut result bool = 10 > 5 // true
}

The char type is a 32-bit integer holding a single Unicode codepoint in the range U+0000–U+10FFFF. It is distinct from u8 (8-bit, 0–255) and from the other integer types. At the C boundary a char is int32_t.

do main() {
mut letter char = 'A' // U+0041
mut newline char = '\n' // U+000A
mut eacute char = '\u{E9}' // U+00E9 é
mut cjk char = char(26085) // U+65E5 日
}
  • cast(c, i64) yields the numeric codepoint; char(n) converts an integer codepoint to a char. A value of n outside U+0000–U+10FFFF is a compile-time error for a constant argument and a runtime panic otherwise.
  • Byte-indexing a string (s[i]) yields the raw byte as a char in 0–255. Use to_char(s, i) for the codepoint at codepoint index i.
  • char values compare and order by codepoint. Arithmetic on char is evaluated as i64.
  • char is a primitive, hashable type: valid as a map key, in when, and for mut array/map literal inference.

Wide Integer Types (i128, u128, i256, u256)

Section titled “Wide Integer Types (i128, u128, i256, u256)”

Grayscale provides portable wide integer types backed by struct-based arithmetic (no compiler extensions required):

Type Width size_of
i128 128-bit 16
u128 128-bit 16
i256 256-bit 32
u256 256-bit 32

Wide integers support all standard arithmetic (+, -, *, /, %) and comparison operators (==, !=, <, >, <=, >=). Values are constructed using the type name as a function:

do main() {
mut a i128 = i128(42)
mut b i128 = i128(100)
mut c i128 = a + b // i128 addition
println(c) // prints "142"
println(type_of(c)) // "i128"
println(size_of(i128)) // 16
mut x i64 = cast(c, i64) // cast back to i64
mut s string = string(c) // convert to string
}

Wide integers use the same overflow-checked arithmetic as i64 and u64; overflow produces a runtime panic.

A negative literal assigned to u128 or u256 is rejected with E3036, the same as for u64.

The pointer type ^Type represents a memory address pointing to a value of Type.

Syntax Meaning
^i64 Pointer to an i64
^MyStruct Pointer to a MyStruct
addr(x) Get the address of x
p^ Dereference pointer p
do main() {
mut x i64 = 42
mut p ^i64 = addr(x)
println(p) // 0x16d1ab9f8, prints the address as hex
println(p^) // 42, explicit dereference reads the pointee
p^ = 100
println(x) // 100
}

Printing a pointer value (println(p), print(p), etc.) outputs the address in hex (0x...) when non-nil and nil when null. Use p^ to access the pointee.

Dereferencing a nil pointer causes a runtime panic.

nil is assignable anywhere a pointer type is expected — variable initialization, assignment, struct fields, and function call arguments (including struct function calls and default parameter values):

const Node struct {
val i64
next ^Node
}
do make_node(parent ^Node) -> ^Node {
n = new(Node)
return n
}
do main() {
root = make_node(nil) // OK: nil satisfies the ^Node parameter
println(root)
}

Const-sourced pointers: addr() can be called on a const-declared variable. The resulting pointer allows reading the value, but the compiler rejects any attempt to write through it (p^ = ..., p^.field = ..., p.field = ..., p^ += ...). This protection follows the pointer wherever it goes — if q = p and p points to a const-declared variable, q inherits the restriction, and so does a pointer read back out of a function’s return value, a struct field, an array element, or a map value. Passing such a pointer to a function that writes through that parameter is rejected at the call. This matches the behavior of ref() on const sources — the address is safe to take, the mutation is not.

do main() {
const x i64 = 42
mut p = addr(x)
println(p^) // 42 — reading is allowed
// p^ = 99 // ERROR — writing through a const-sourced pointer
mut q = p
// q^ = 99 // ERROR — const origin propagates through assignment
println(q^)
}

Pointer aliasing: Calling addr() more than once on the same variable produces pointers that all refer to the same memory. Changing the value through one pointer changes it for all of them. In multithreaded code, protect shared variables with sync.lock() to avoid data races.

do main() {
mut x i64 = 10
mut p1 = addr(x)
mut p2 = addr(x)
p1^ = 99
println(p2^) // 99 — p1 and p2 point to the same variable
}

Raw pointers with raw(): raw() takes the address of a variable just like addr(), but returns a raw pointer — an unsafe pointer with no safety guards. Dereferences skip the nil-check panic, and the compiler does not enforce const-source write protection. The same argument rules apply — raw() requires a variable, field, or index expression (not a literal or call result), and cannot take the address of a map index or a dynamic [T] array element.

// q^ dereference has no nil-check — if q were nil, behavior is undefined
do main() {
const x i64 = 42
mut p = raw(x)
p^ = 99 // allowed — raw() bypasses const-source protection
println(p^) // 99
mut q ^i64 = raw(x)
}

raw() is considered unsafe and is intended for performance-critical code where nil checks are a measurable overhead and the programmer guarantees pointer validity. Prefer addr() in all other cases.

A call that returns a pointer can be dereferenced directly, without storing the pointer first. new(Foo)^ allocates a Foo and immediately gives you the value, and the same applies to any function that returns ^Type: return new(Foo)^ or mut val = make_thing()^.

The dot operator (.) automatically dereferences pointers to structs. If p is a ^MyStruct, writing p.field is equivalent to p^.field. This auto-dereference applies to field access and struct function calls but does not apply in other contexts: println(p) prints the address, and return p returns the pointer itself. Use explicit p^ when you need the pointee value rather than field access.

These rules decide the type of every number value and where it may go. They are the same in every position a value can be written.

Literals take their type from context. A number literal — or an expression built only from number literals, such as 200 + 100 - 50 or 1 bit_shift_left 9 — has no type of its own until something gives it one:

  • the slot it is stored into (see below): mut b u8 = 200 makes 200 a u8;
  • the other operand of a binary operator: in x == 0.1 with x f32, 0.1 is an f32;
  • the target of a cast it is the operand of: in cast(1.0e300, f32), the literal is an f32.

Otherwise it becomes an i64, or an f64 if it contains a decimal literal. An expression made only of literals is computed exactly (up to 256 bits) before it takes its type, so mut a u8 = 200 + 100 - 50 stores 250. A literal whose value does not fit the type it takes is E3036: mut b u8 = 300, mut n u8 = -1, mut f f32 = 1.0e300, mut a u8 = 1 bit_shift_left 9. An integer literal no integer type can hold (at or above 2^256, or below -2^255) is E3046. An array, map or struct literal passes its element, key, value and field types down to each entry, so mut xs [i8] = {a, 7} makes 7 an i8.

Every slot converts a value the same way. A slot is any place a value is stored: a variable declaration, a reassignment, a compound assignment x op= v (v is checked as x’s type), an element, map key or map value, a pointer’s target, a field, a struct literal field, a field or parameter default, an enum payload, a when arm (checked as the subject’s type), each position of a multi-value declaration, an array or map literal entry, a call argument (user, struct and func-typed functions, builtins and stdlib functions), a return value, an array or string index (an i64), a map index (the key type), and a range() bound or step (an i64, or the widest wide integer type among its bounds). A value of type S stored into a slot of type T:

S to T Example Result
the same type i32 to i32 allowed
wider, same signedness i32 to i64, u8 to u16, f32 to f64 allowed
unsigned to strictly wider signed u32 to i64 allowed
integer to float i64 to f64 allowed
narrower i64 to i32, u64 to u8, i64 to u8, f64 to f32, i128 to i64 E3155
other signedness, same or greater width i64 to u64, u8 to i8, i8 to u64 E3019
float to integer f64 to i64 the position’s type mismatch error

An array or map never converts its elements: a [i64] value is not a [u8]. A narrowing or signedness-crossing conversion is written with cast.

Binary operators keep their operands’ type. For + - * / %, the bitwise operators and the comparisons:

  • two operands of the same type compute (or compare) at that type: f32 + f32 is f32;
  • operands of one signedness and different widths compute at the wider type: i8 + i32 is i32, i128 + i256 is i256;
  • a literal operand takes the other operand’s type: x == 0.1 with x f32 compares as f32;
  • two literals give a literal (computed exactly, as above);
  • bit_shift_left and bit_shift_right take a count of any integer type, and the result has the left operand’s type; a count outside [0, width - 1] of that type panics (P0092);
  • any other pair of typed operands — i64 + u8, i64 + f64, u64 == i64 — is an error (E3002, or E3156 for a comparison); convert one side with cast.

Arithmetic is overflow-checked at the result type, whatever expression or compound assignment it appears in: xs[0] += 10 on a [u8] holding 250 panics, whether xs is a variable, a field, a map value, or reached through a pointer.

Arrays are ordered collections of elements of the same type.

Dynamic arrays have variable length:

do main() {
mut numbers [i64] = {1, 2, 3, 4, 5}
mut empty [string] = {}
}

Fixed-size arrays have a length specified at declaration:

do main() {
const fixed [i64, 3] = {10, 20, 30}
}

Fixed-size arrays must be declared with const. Providing fewer values than the declared size is permitted; providing more values than the declared size is an error.

do main() {
const a [i64, 5] = {1, 2, 3} // OK (3 of 5 slots used, remaining zero-initialized)
// const b [i64, 5] = {1, 2, 3, 4, 5, 6} // Error: 6 values exceeds size of 5
println(a)
}

The size specifier N may also be a compile-time integer constant of any integer type (i8–i64, u8–u64). The constant must be declared before the array and must resolve to a value greater than zero.

do main() {
const SIZE i64 = 4
const buf [u8, SIZE] = {0x01, 0x02, 0x03, 0x04}
}

Multi-dimensional arrays:

do main() {
mut matrix [[i64]] = {{1, 2}, {3, 4}}
mut cube [[[i64]]] = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}
}

Array indexing is zero-based. Accessing an index outside the valid range produces a runtime error.

Maps are unordered collections of key-value pairs. The map keyword is optional — [K:V] and map[K:V] are identical. An empty map is written {:}; a bare {} is an empty array:

// Long form is also valid:
do main() {
mut ages [string:i64] = {
"alice": 30,
"bob": 25
}
mut empty [string:i64] = {:} // Empty map
mut scores map[string:i64] = {"math": 95}
}
do main() {
mut arr [i64] = {} // Empty array
mut m [string:i64] = {:} // Empty map
}

Bracket disambiguation:

Syntax Meaning Example
[T] Dynamic array of T [i64]
[T,N] Fixed-size array of T [i64,3]
[K:V] Map from K to V [string:i64]

Maps must be declared with mut. Declaring a map with const is a compile-time error. If the keys are known at compile time, use a struct instead.

Keys must be of a hashable type: an integer or float type, string, bool, or char.

Accessing a key that does not exist produces a runtime error.

Structs are user-defined composite types with named fields.

const Point struct {
x i64
y i64
}
const Person struct {
name string
age i64
active bool
}
do main() {
}

Struct and enum declarations must be at the top level of a file, never inside a function or block. Fields go on separate lines or, on one line, are separated by ;. Unlike functions and control flow, structs and enums define types, not logic, and types belong where they are visible, nameable, and reusable. Burying a type inside a function makes it invisible to the rest of your program and harder to find when reading code.

A field may be a fixed-size array ([T,N]), the same spelling used for a local const f [T,N]. Its length never changes: arrays.append, prepend, insert_at, remove, remove_at, remove_first, remove_last, clear, and deduplicate are all rejected on it, whether called directly or through a member-expression chain like o.inner.items. Reading and writing individual elements works as long as the containing instance is mut. A struct literal that under-initializes the field zero-fills the rest (W3003); over-initializing it is an error (E3052) — the same rules as a local fixed-size array.

const Buffer struct {
data [u8, 256]
}
do main() {
}

A struct may reference itself through a pointer field. Value-type self-reference is rejected at compile time.

const Node struct {
val i64
next ^Node // OK: pointer field
}
// Value-type self-reference is an error:
// const Bad struct {
// val i64
// next Bad // error: struct 'Bad' cannot contain itself by value; use a pointer field '^Bad'
// }
do main() {}

To traverse a recursive struct, dot notation automatically dereferences pointer fields with no explicit ^ required. The ^ suffix is also accepted if preferred:

const Node struct {
val i64
next ^Node
}
do main() {
mut a = new(Node)
mut b = new(Node)
a.val = 1
b.val = 2
a.next = b
println(a.val) // 1
println(a.next.val) // 2, implicit dereference
println(a.next^.val) // 2, explicit dereference (also valid)
}

Mutual recursion through pointer fields is supported. Both structs must use pointer fields (^Type) to reference each other; value-type mutual reference is rejected at compile time.

Struct instantiation uses named field syntax:

const Point struct {
x i64
y i64
}
do main() {
mut origin Point = Point{x: 0, y: 0}
mut p Point = Point{} // Zero-initialized: x=0, y=0
println(origin)
println(p)
}

Struct fields may specify a default value using = expr after the type. When a struct is created with new() or a struct literal that omits a field, the default value is used instead of zero-initialization.

const Config struct {
host string = "localhost"
port i64 = 8080
verbose bool = false
}
do main() {
mut c = new(Config) // c.host = "localhost", c.port = 8080, c.verbose = false
mut c2 = Config{port: 3000} // c2.host = "localhost", c2.port = 3000, c2.verbose = false
}

Grouped fields share the same default:

const Point struct {
x, y i64 = 0
z i64 = 1
}
do main() {
}

Fields without a default value remain zero-initialized when omitted.

#json structs cannot have default field values. They are data-only and always zero-initialized from JSON input.

Fields are accessed using dot notation:

const Point struct {
x i64
y i64
}
do main() {
mut origin Point = Point{x: 0, y: 0}
mut x_value i64 = origin.x
origin.x = 10 // Modification (if variable is mut)
println(x_value)
println(origin)
}

Enums define a type with a fixed set of named values.

Integer enums (default, auto-incrementing from 0):

const Direction enum {
NORTH // 0
EAST // 1
SOUTH // 2
WEST // 3
}
do main() {
}

Explicit values can be assigned to enum variants. Subsequent variants without an explicit value auto-increment from the last:

const Foobar enum {
BAZ = 10 // 10
QUX // 11
QUUX = 50 // 50
CORGE // 51
}
do main() {
}

Enum variants go on separate lines or, on one line, are separated by ;, as in const Color enum { RED; GREEN; BLUE }.

Enums are not integers. Even though integer enums are backed by numeric values under the hood, you cannot compare an enum variable with an integer (d == 0), assign an integer to an enum variable (d = 2), or perform arithmetic on enum values. Enums can only be compared with values of the same enum type using == and !=: use Direction.NORTH, .NORTH, or another Direction variable, never a raw number. Assigning an enum value to an i64 variable is allowed, and the enum is implicitly widened to its underlying integer value: mut status i64 = Direction.NORTH assigns 0.

To compare an enum value against an integer, use cast() to bridge the gap: if cast(Direction.NORTH, i64) == 0 { ... }. You can also cast the other way: cast(0, Direction).

Printing enum values depends on the enum’s backing:

Enum kind println(value) prints
Plain integer-backed (default) The underlying integer (e.g. 0 for the first variant)
String-backed The variant’s string value (e.g. "todo")
#error_code-tagged (and ErrorCode itself) The variant’s name (e.g. "PAYMENT_DECLINED") — see ErrorCode
const Color enum {
RED
GREEN
BLUE
}
const Status enum {
TODO = "todo"
DONE = "done"
}
#error_code
const PaymentErrors enum {
PAYMENT_DECLINED
PAYMENT_CANCELED
}
do main() {
println(Color.RED) // "0" — plain enums print their integer
println(Status.TODO) // "todo" — string enums print their string
println(PaymentErrors.PAYMENT_DECLINED) // "PAYMENT_DECLINED" — error-code enums print their name
}

A plain enum has no name table generated for it, so printing one falls back to its widened integer value — consistent with a plain enum’s underlying value being a real integer under the hood. String-backed and #error_code-tagged enums each carry an obvious human-readable form already (the string literal, or the compiler-owned error-code name table), so those print that instead.

Flags enums (powers of 2, annotated with #flags):

#flags
const Permissions enum {
READ // 1
WRITE // 2
EXECUTE // 4
DELETE // 8
}
do main() {
}

A #flags enum may have at most 63 variants — one per usable bit of int64 (bit 63 is the sign bit). More is rejected (E3143).

Enum values are accessed using dot notation:

const Direction enum {
NORTH
SOUTH
}
const Status enum {
TODO = "todo"
DONE = "done"
}
do main() {
mut dir = Direction.NORTH
mut status = Status.TODO
println(dir)
println(status)
}

Implicit enum selector (.VARIANT):

When the expected enum type is known from context, you can use the shorthand .VARIANT instead of the full EnumName.VARIANT. The compiler resolves the enum type automatically.

const Direction enum {
NORTH
EAST
SOUTH
WEST
}
do move(d Direction) -> i64 { return 0 }
do get_dir() -> Direction { return .NORTH }
// Struct literal fields
const Config struct {
dir Direction
}
do main() {
// Variable declaration with type annotation
mut dir Direction = .NORTH
// Assignment
dir = .SOUTH
// Function arguments
mut moved i64 = move(.EAST)
// When/is branches
when dir {
is .NORTH { println("north") }
is .SOUTH { println("south") }
default { println("other") }
}
// Comparisons
if dir == .WEST { println("west") }
// Return statements
mut d Direction = get_dir()
mut c = Config{ dir: .EAST }
// Array literals
mut dirs [Direction] = {.NORTH, .SOUTH}
println(c)
println(dirs)
println(d)
}

The full EnumName.VARIANT form is always valid and is required when no type context is available.

Tagged Enums (Variants with Data):

Enum variants can carry associated data (payloads), making the enum a tagged union. A variant’s payload is declared with positional types in parentheses:

const Shape enum {
Circle(f64)
Rect(f64, f64)
Point
}
do main() {
}

An enum becomes a tagged union if ANY variant has a payload. Variants without payloads (like Point above) are plain tag-only variants. Payloads and explicit values (= 5) are mutually exclusive per variant. String enums and #flags enums cannot have payloads.

Construction:

Tagged enum values are constructed by calling the variant with arguments:

const Shape enum {
Circle(f64)
Rect(f64, f64)
Point
}
do main() {
mut s Shape = Shape.Circle(3.14)
mut r Shape = Shape.Rect(10.0, 20.0)
mut p Shape = Shape.Point
}

The implicit selector syntax also works with tagged constructors:

const Shape enum {
Circle(f64)
Rect(f64, f64)
Point
}
do main() {
mut s Shape = .Circle(3.14)
}

Destructuring with when/is:

Use pattern destructuring in when/is to extract payload values. Use the fully-qualified form:

const Shape enum {
Circle(f64)
Rect(f64, f64)
Point
}
do main() {
mut shape Shape = Shape.Circle(3.14)
when shape {
is Shape.Circle(radius) {
println("Circle with radius ${radius}")
}
is Shape.Rect(w, h) {
println("Rectangle ${w} x ${h}")
}
is Shape.Point {
println("Just a point")
}
}
}

The implicit selector form (dot-prefix) also works:

const Shape enum {
Circle(f64)
Rect(f64, f64)
Point
}
do main() {
mut shape Shape = Shape.Rect(1.0, 2.0)
when shape {
is .Circle(r) { println("radius: ${r}") }
is .Rect(w, h) { println("${w}x${h}") }
is .Point { println("point") }
}
}

The number of bindings in a pattern must match the variant’s payload count. #strict exhaustiveness checking works with tagged enums.

func is a type keyword that represents a reference to a named function. Function references are created with ()name or ref(name) and are always const. The func type is used in parameter declarations, struct fields, arrays, and maps to accept or store callable references.

do double(n i64) -> i64 { return n * 2 }
do main() {
const f = ()double
println(f(5)) // 10
}

A typed func signature specifies parameter and return types:

do apply(x i64, f func(i64) -> i64) -> i64 {
return f(x)
}
do main() {
}

See Function References for full documentation including calling conventions, parameter usage, and struct field storage.

Grayscale is a statically-typed language with type inference. The type of every variable is known at compile time, and explicit type annotations are optional in most contexts when the compiler can determine the type from the initializer.

Type inference works with:

  1. Standalone literals - The type is inferred from the literal value
  2. Function return values - The variable’s type is inferred from the function’s return type
  3. Struct literals - The type is known from the struct name
  4. Built-in constructors - new(Type) (returns ^Type) and copy(value)
  5. Multiple return values - Each variable’s type is inferred from the corresponding return type
  6. mut array and map literals of primitives - [T] is inferred from the element type, map[K:V] from the first pair

Array and map literal inference applies only to mut declarations whose elements are all primitives (integer and float types, string, bool, char) — for maps, both keys and values must be primitive. Empty literals ({}, {:}), const declarations, and literals containing structs, enums, pointers, or nested containers still require an explicit annotation (e.g. mut arr [Point] = {Point{x: 1, y: 2}}).

File-scope const declarations of primitive types and arrays require explicit type annotations. Type inference for const is only supported inside function bodies. For example, const MAX_SIZE i64 = 100 is required at file scope, while const x = 42 is valid inside a function.

const Point struct {
x i64
y i64
}
const Person struct {
name string
}
do sum(a i64, b i64) -> i64 {
return a + b
}
do divide(a, b i64) -> (i64, i64) {
return a / b, a % b
}
do main() {
// Inferred from literals
mut x = 42 // Inferred: i64
mut name = "Alice" // Inferred: string
mut pi = 3.14 // Inferred: f64
mut flag = true // Inferred: bool
// Explicit annotations are always accepted
mut y i64 = 42 // Explicit: i64
// Inferred from function return type
mut result = sum(1, 2) // Inferred: i64
println(type_of(result)) // Output: i64
// Inferred from struct literal
const p = Point{x: 1, y: 2} // Inferred: Point
// Inferred from built-in constructors
mut val = new(Person) // Inferred: ^Person (pointer)
mut dup = copy(val^) // Inferred: Person (copy() needs a value, not a pointer)
// mut array/map literals of primitives are inferred
mut arr = {1, 2, 3} // Inferred: [i64]
mut letters = {'a', 'b', 'c'} // Inferred: [char]
mut scores = {"alice": 10, "bob": 7} // Inferred: map[string:i64]
mut fixed [Point] = {Point{x: 1, y: 2}} // Explicit: non-primitive elements
// Multiple return values
mut quotient, remainder = divide(10, 3) // Both inferred: i64
}

Explicit type annotations are generally optional but can be used for clarity or documentation. The exceptions are file-scope const declarations of primitive types and arrays, and any const or non-primitive array/map literal, which always require explicit type annotations.

Explicit type conversions are performed with cast, string(), and char():

do main() {
mut i i64 = cast('A', i64) // 65 - char to i64 (code point)
mut f f64 = cast(42, f64) // 42.0 - i64 to f64
mut s string = string(123) // "123" - i64 to string
mut c char = char(65) // 'A' - i64 to char
}

Conversions that would lose information or are invalid produce check-time or runtime errors.

The cast keyword provides explicit type conversion for values and arrays:

do main() {
mut small u8 = cast(42, u8)
mut truncated i64 = cast(3.7, i64) // 3
mut text string = cast(123, string) // "123"
mut parsed i64 = cast("42", i64) // 42
}

A string cast to an integer or float type is parsed at runtime; a string that is not a number panics (P0084 for an integer target, P0085 for a float target).

For array conversions, cast converts each element to the target element type:

do main() {
mut ints [i64] = {1, 2, 3}
mut bytes [u8] = cast(ints, [u8])
}

Range constraints are enforced at runtime (e.g., u8 values must be 0-255).

cast truncates floats to integers; it does not round. cast(3.9, i64) gives 3, not 4.

The alias keyword creates an interchangeable name for an existing type:

const Point struct {
x i64
y i64
}
alias Meters = f64
alias Vec2 = Point
alias Names = [string]
alias Lookup = map[string:i64]
do main() {}

Rules:

  • File-scope only — aliases cannot be declared inside functions.
  • Public by default — prefix with private to restrict to the declaring file.
  • Erased at compile time — aliases produce no runtime overhead. type_of() returns the underlying type name.
  • Transitive — aliases can chain: alias A = i64 then alias B = A resolves B to i64.
  • Can alias: primitives, structs, enums, arrays ([T]), maps (map[K:V]), and pointers (^T).
  • Cannot alias: module-qualified types (mod.Type) or the wildcard type (?).
  • The alias name may not be a reserved type name or a builtin function name — alias i64 = f64 and alias println = i64 are both rejected, the same way a struct or enum by those names is.

Aliases are fully interchangeable with the underlying type:

alias Meters = f64
do main() {
mut d Meters = 10.5
println(type_of(d)) // "f64"
println(d + 1.0) // 11.5
}

Struct and enum aliases work with constructors and member access:

const Point struct {
x i64
y i64
}
alias Vec2 = Point
const Color enum {
RED
GREEN
BLUE
}
alias Hue = Color
do main() {
mut p Vec2 = Point{x: 1, y: 2}
mut c Hue = Hue.RED
}

Private aliases restrict access to the declaring file:

private alias InternalID = i64
do main() {}