Builtin Types
Sun provides several builtin types that are available without explicit definition.
Primitive Types
Sun supports the following primitive types:
| Type | Description | Size |
|---|---|---|
i8, i16, i32, i64 | Signed integers | 1, 2, 4, 8 bytes |
u8, u16, u32, u64 | Unsigned integers | 1, 2, 4, 8 bytes |
f32, f64 | Floating-point numbers | 4, 8 bytes |
bool | Boolean (true or false) | 1 byte |
char | One Unicode scalar value | 4 bytes |
void | No value (for functions with no return) | — |
var a: i32 = 42;
var b: f64 = 3.14159;
var c: bool = true;
var d: u8 = 255;
var e: char = 'k';Integer Arithmetic
Integer /, %, /=, and %= trap if the divisor is zero or a signed
integer's minimum value is divided by -1 (including remainder). The same
checks apply inside unsafe blocks. Statically invalid operations are rejected
at compile time. These operators do not propagate errors or run cleanup on a trap.
Use checked_div(a, b) or checked_rem(a, b) when failure should be handled.
They return _Result<T, ArithmeticError> and work with match and prefix try.
Shifts << and >> mask the count with width - 1, using the bit width
of the operands after integer widening. This includes negative counts:
for i32 operands, a count of 32 becomes 0, 33 becomes 1, and
-1 becomes 31. The same rule applies to <<= and >>=, to constant
evaluation, and at every optimization level. Shifts do not abort.
Valid signed division truncates toward zero, and the remainder has the sign of the dividend. Right shift fills with zeros for unsigned integers and with the sign bit for signed integers. Addition, subtraction, and multiplication wrap at the integer width. Floating-point division is unchanged.
File-scope initializers cannot propagate errors, so potentially failing integer division and remainder are rejected there.
Numeric Literals
Integer literals can use decimal (113), hexadecimal (0x71), or binary
(0b1110001). Hexadecimal digits accept either letter case; the prefixes
are lowercase. All bases follow the same typing and range rules, including
integer suffixes such as 0x71u16 and negative values such as -0x80i8.
Hexadecimal letters are digits, so 0xf32 is the integer 3890.
Use a single _ between digits to group an integer's digits:
var entity: u32 = 0x0001_00c2;
var flags = 0b1000_0001u8;
var count = 1_000;Separators cannot follow a prefix, precede a suffix, end a literal, or appear consecutively. Floating-point literals do not support digit separators.
An untyped integer literal is an i32 (i64 when the value needs the room),
and an untyped float literal is an f64. In a position whose type is already
known — a variable with an annotation, a field, a return — an untyped
integer literal adopts that type when its value fits.
A type suffix names the literal's type directly, which is how a literal
reaches a function or constructor parameter of a narrower type. Every numeric
type is a suffix: i8 through i64, u8 through u64 on integer literals,
and f32/f64 on float literals.
var sa = SockAddrIn(0u32, 7400u16); // without suffixes, each argument
// would need a typed variable
var flag: u8 = 128u8;
var offset = -128i8; // the minus folds into the literal
var ratio = 1.5f32;A value that does not fit its suffix is a compile error, not a silent wrap:
var b = 300u8; // Error: Integer literal 300 cannot be represented as 'u8'A suffixed literal is an ordinary typed value from then on: it widens where
any u8 would (21u8 passes to a u16 parameter) and never narrows —
converting between typed values takes _convert or safe_convert. A char
never accepts an integer literal, suffixed or not.
Characters and Bytes
char holds one Unicode scalar value — any code point from U+0000 to
U+10FFFF except the UTF-16 surrogates. It is a distinct type, not a small
integer: it compares with another char and takes no part in arithmetic.
var letter: char = 'a';
var accented: char = 'é';
var emoji: char = '😀';
if (letter < accented) { ... } // ordered by scalar valueText in Sun is UTF-8 and String is indexed by byte, so code that scans bytes
wants a byte, not a scalar value. That is what a byte literal is for:
var newline: u8 = b'\n';
// String.at() returns a u8, so compare it against a byte literal
while (i < s.length() and s.at(i) != b'\n') { i = i + 1; }The two never mix silently — s.at(i) == 'a' is a compile error, because one
side is a byte and the other a scalar value.
Escapes
| Escape | Meaning |
|---|---|
\n \t \r | Newline, tab, carriage return |
\\ \' \0 | Backslash, single quote, NUL |
\xNN | Hex. In '…' up to \x7F; in b'…' the full \x00–\xFF |
\u{...} | A scalar value by code point, 1–6 hex digits. Character literals only |
Above U+007F a byte and a scalar value stop agreeing, which is why '\xFF'
is rejected and '\u{FF}' is the way to write U+00FF.
String literals ("…") and template strings also support \xNN. Each escape
consumes exactly two hex digits and inserts one byte from \x00 to \xFF,
including NUL. For example, "\x08http/1.1".length() is 9. Hex digits may
use either case; missing or invalid digits are a compile error. These escapes
insert raw bytes, so the result is not necessarily valid UTF-8.
Converting
_convert moves between char and the integer types, and refuses floats and
bool. Not every u32 is a scalar value, so char_of is the checked form:
var n: i64 = _convert<i64>('a'); // 97
var c: char = _convert<char>(b'a'); // 'a'
// None for a surrogate or anything above U+10FFFF
var maybe: Option<char> = char_of(128512);Iterating text
String is byte-indexed, so scalar values come from iterate_chars():
var s = String(alloc, "héllo😀");
s.length(); // 10 bytes
s.count_chars(); // 6 scalar values
var it = s.iterate_chars();
for (var c: char in it) { print(c); }
s.push('!'); // appends the UTF-8 encoding
s.find('😀'); // byte index, or NoneBytes that are not valid UTF-8 decode as U+FFFD and advance one byte, so
iteration always terminates and always covers the whole string.
Arrays
A sized array array<T, N> owns its N elements inline, wherever it lives: a
local, a class field, a global, or an element of another array. Its size is
part of its type, so array<i32, 3> and array<i32, 5> are different types.
Multi-dimensional arrays add one size per dimension: array<f64, 2, 3>.
Declaration and Initialization
// Array literals; the type is array<i32, 5>
var arr = [1, 2, 3, 4, 5];
// With explicit type annotation
var arr: array<i32, 3> = [10, 20, 30];
// Multidimensional arrays
var matrix: array<f64, 2, 3> = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];A size is written as a number or as the name of a constant, which may come
from a module (limits.ROWS), including a module of a precompiled library,
and may be declared further down the file. The
constant must be an integer const at file or module scope whose value the
compiler can work out, and it must not be negative. Its value may come from
calling a function, as in const CELLS = area(2, 3);, when the size is used by
a variable and the function is declared above that variable. A size used by a
class field or a function signature cannot come from a function call yet. A size is not an
expression: give the expression a name first.
const ROWS = 2;
const CELLS = ROWS * 3;
var grid: array<i32, ROWS, 3> = [[1, 2, 3], [4, 5, 6]];
var flat: array<i32, CELLS> = [1, 2, 3, 4, 5, 6];
// var bad: array<i32, ROWS * 3> = ...; // ERROR: a size is a number or a namearray<i32, CELLS> and array<i32, 6> are the same type. A const that only
gets its value when the program starts cannot be a size, and the error says
what made it so:
array size 'SNAP' must be known at compile time, but 'SNAP' is initialized at
startup because it reads 'counter', which is a 'var'Indexing
var arr = [10, 20, 30, 40, 50];
var first = arr[0]; // 10
arr[2] = 33;
// Multidimensional indexing
var matrix = [[1, 2], [3, 4]];
var element = matrix[1, 0]; // 3arr.ndims() is the number of dimensions and arr.dim(i) the size of
dimension i, both as i64. Indices are not bounds-checked at run time.
Ownership
An array is a compound value like a class: passing it by value, assigning it
to a field or binding it to a new variable moves it, and the source may not
be used afterwards. An array whose elements own something (a class with a
deinit) drops each element when the array is dropped. An element of such an
array is borrowed in place with ref or const ref; it cannot be moved out.
var a: array<i32, 3> = [1, 2, 3];
var b = a; // a moves into b
// a[0] // ERROR: use of moved variable 'a'Views: ref array<T>
An unsized array<T> is a view of some sized array with its rank erased.
It only ever exists behind ref: a ref array<T> or const ref array<T>
parameter accepts an array of any size and rank with that element type, and
the view keeps the dimensions so ndims(), dim(i) and multi-dimensional
indexing work through it.
function sum(xs: const ref array<i32>) i32 {
var total = 0;
for (var i: i64 = 0; i < xs.dim(0); i = i + 1) { total = total + xs[i]; }
return total;
}
var a: array<i32, 4> = [1, 2, 3, 4];
sum(a); // a is borrowed, not movedA bare array<T> cannot be a field, local, global, by-value parameter or
return type. A ref array<T> field makes the class a reference holder, and a
ref array<T> return borrows the call's inputs, exactly as with any other
reference (see Memory Safety).
Pointer Types
Sun provides pointer types for working with memory:
ptr<T> - Owning Pointer
An owning pointer with RAII semantics. Memory is automatically freed when the pointer goes out of scope.
var allocator = make_heap_allocator();
var p: ptr<Point> = allocator.create<Point>(3, 4);
// p.x, p.y accessible directly
// Memory freed automatically at scope exitraw_ptr<T> - Raw Pointer
A non-owning raw pointer for low-level memory operations.
var raw: raw_ptr<i32> = allocator.alloc<i32>(10);
// Manual memory management required
unsafe { _free(raw); };static_ptr<T> - Static Pointer
A pointer to immortal/static data, such as string literals. Represented as a fat pointer { ptr data, i64 length }. The data lives for the entire program.
Two methods take the fat pointer apart; they are the supported way to read a static_ptr:
| Method | Returns | Description |
|---|---|---|
length() | i64 | Number of elements (bytes, for a string literal; the NUL terminator is not counted) |
raw() | raw_ptr<T> | The data pointer, for _load<T>, _print_bytes or a C function |
var s: static_ptr<u8> = "hello world";
var n: i64 = s.length(); // 11
var bytes: raw_ptr<u8> = s.raw();
_print_bytes(bytes, n);Passing a static_ptr<T> where a raw_ptr<T> is expected narrows it to raw() automatically, so a string literal can be handed straight to a C function (see C FFI).
Reference Types
A reference borrows a value instead of owning it. Sun never implicitly copies a class, payload enum or interface, so a reference is how one of those is read or modified in place — as a parameter, a local, or a return type.
ref T - Mutable Borrow
Exclusive access to an existing value. Writing through the reference writes the
original. Only one ref to a value may be alive at a time.
function increment(x: ref i32) void {
x = x + 1;
}
function main() i32 {
var count: i32 = 0;
increment(count); // count is now 1
ref r = count; // statement form, no annotation needed
var s: ref i32 = count; // ❌ ERROR: count is already borrowed
return r;
}const ref T - Shared Borrow
Read-only access. Several const refs to one value may be alive at once, and a
mutable ref cannot coexist with any of them. Through a const ref nothing can
be assigned and only const methods can be called.
function total(v: const ref Vec<i32>) i64 {
return v.size(); // ✓ size() is a const method
// v.push(1); // ❌ ERROR: cannot call non-const method 'push'
}const always leads: it is const ref T, never ref const T. A ref T may be
passed or rebound as const ref T, never the other way round.
A reference is a borrow, not a pointer: it always names live storage, it
cannot be null, and it cannot be stored past the lifetime of what it borrows.
Where a by-value T is expected and T is compound, a ref T is rejected —
reading the value out of the borrow would leave two owners of one buffer.
Memory Safety has the full rules.
Builtin Interfaces
Sun provides builtin interfaces for common patterns. These interfaces are always available and cannot be redefined.
IError
The builtin interface for reading error codes and messages. Both methods are
const. With the standard library, message() returns an owned String.
class DivByZero implements IError {
init() {}
const method code() i32 { return 1; }
const method message() String { return String("division by zero"); }
}
/** Owns either a quotient or its concrete failure. */
enum DivideResult<T> { Ok(T), Error(DivByZero) }
/** Checks for a zero divisor before computing the quotient. */
function divide(a: i32, b: i32) DivideResult<i32> {
if (b == 0) { return DivideResult.Error(DivByZero()); }
return DivideResult.Ok(a / b);
}
/** Handles an owned error through a borrowed interface view. */
function main() i32 {
return match divide(10, 0) {
DivideResult.Ok(value) => value,
(e: const ref IError) => -1
};
}Iteration interfaces
IIterator<T, Container> and IIterable<T, Self> are ordinary stdlib
interfaces (stdlib/iterator.sun), not builtins, because their contract names
Option<T>. See Iteration
in the standard library reference and the for-in loop.
Reserved Type Names
The following type names are reserved and cannot be redefined:
IError- Error handling interface
Attempting to define a class or interface with this name will result in a compilation error:
// ❌ ERROR: Cannot redefine builtin interface 'IError'
interface IError {
const method code() i32;
}