5. Types
Base is statically and nominally typed. Every expression has one type. Inference is local: from an initialiser to its binding, from arguments to generic parameters, from a declared result into return, from context into literals and none. It never crosses a public signature, which always spells its types. There is no subtyping. The implicit conversions are exactly: a pointer to a more-qualified pointer of the same pointee, any object pointer to void*, a mutable array to a span, a mutable span to a read-only span, a string literal to cstr, T to T?, T to a successful T!, and a conforming pointer to an interface view. Everything else is written.
5.1 Scalars#
| Type | Meaning |
|---|---|
bool | true or false; one byte holding 0 or 1; no numeric conversion |
u8, u16, u32, u64 | unsigned integers |
i8, i16, i32, i64 | two's-complement signed integers |
usize, isize | unsigned and signed integers the width of a pointer on the target |
f16, f32, f64 | IEEE 754 binary floats |
char | one Unicode scalar value |
unit | the single value () of a function that returns nothing |
never | the type of an expression that cannot complete: error(...), trap(...), a function that never returns |
usize and isize are the types of sizeof, alignof, offsetof, span lengths, array indices, and pointer differences. Their width is fixed by the backend: 64 bits on the native targets, 32 on WebAssembly. Because of that, a usize cannot appear where the compiler needs a constant before it has chosen a target, which is a top-level let or an array length. Use a fixed-width type there.
never coerces to any type because the value never exists. Operands before a never operand are still evaluated, left to right.
Why usize is not a constant. The compiler keeps every target fact, pointer width included, out of its intermediate representation until the backend, so that one verified program feeds every backend. sizeof(T) is therefore an instruction the backend folds, not a number the front end knows. Making it a compile-time constant would have quietly moved the target into the front end.
5.2 The c module#
C's types live in the standard c module as distinct nominal types with explicit conversion. They exist so that a C signature can be written exactly and so that luce bind (§17.5) never has to guess a width.
c type | C type | Width and signedness |
|---|---|---|
c.char, c.schar, c.uchar | char, signed char, unsigned char | 8 bits; c.char is unsigned on AArch64 Linux and signed on the other supported targets |
c.short, c.ushort | short, unsigned short | 16 bits |
c.int, c.uint | int, unsigned int | 32 bits |
c.long, c.ulong | long, unsigned long | 64 bits on SysV and AAPCS64 targets, 32 on Windows x64 |
c.longlong, c.ulonglong | long long, unsigned long long | 64 bits |
c.size, c.ssize, c.ptrdiff, c.intptr, c.uintptr | size_t, ssize_t, ptrdiff_t, intptr_t, uintptr_t | pointer width; same representation as usize/isize |
c.bool | _Bool | 1 byte, 0 or 1; same representation as bool |
c.float, c.double | float, double | 32 and 64 bits |
c.wchar | wchar_t | 32 bits except on Windows, where it is 16 |
c.va_list | va_list | opaque; may only be passed through to C |
long double and _Complex have no Base type; luce bind refuses them and names the shim recipe. usize(x) converts c.size exactly; c.int(x) from i64 is a checked conversion.
5.3 Pointers#
T* # pointer to mutable T; never null
const T* # pointer to read-only T; never null
volatile T* # pointer whose loads and stores are observable effects
T*? # nullable pointer; `none` is C's null; one word
void* # untyped pointer; C's void *
const void* # untyped read-only pointer
const (T*)* # C's T *const *: a pointer to a read-only pointer to T
- A pointer type is a complete type followed by
*.constandvolatileprefix the pointee and may combine. A qualifier applies to the innermost type; to qualify a pointer itself, parenthesise it. voidmay appear only immediately before*, optionally afterconst.- A bare pointer is never null. Every operation that could produce a null pointer produces
T*?instead: an integer-to-pointer cast (§7.5), anexternslot declared?(§17.1), a nullable field read.T*?is an ordinary optional whose representation is the null niche:noneis address zero and the type is one word. It is unwrapped like any optional (§11.1). There is noNULLliteral. T*converts implicitly toconst T*,volatile T*, andconst volatile T*. A qualifier is never removed implicitly. Any object pointer converts implicitly tovoid*, and anyconstobject pointer toconst void*. The reverse directions are casts.==and!=compare addresses between any two object pointers, across qualifiers and withvoid*.<,<=,>,>=between two object pointers are the total order of addresses, and Base guarantees that order across objects, which C does not. Pointers are notHashable.
Why never null. In C, every pointer is nullable and almost none of them should be; the check-before-every-dereference habit is where a large share of C's crashes and a larger share of its noise come from. A bare T* that cannot be null lets a function's signature say which pointers may be absent, and lets the compiler put the single check where the ? is. Hare, Zig, and Cyclone reached the same design.
Why a niche. A nullable pointer must be one word so that a struct holding one has the layout its C definition has. Full Luce represents optional foreign handles as a token plus a flag, because a C library may legitimately traffic in a zero handle and the two must remain distinguishable. Base's pointers are not handles: zero is null, so the niche is correct, and the compiler's intermediate representation gains a nullable-pointer type for Base alone (§19.2).
5.4 Arrays and spans#
T[N] # fixed-size value array, N a positive compile-time integer
T[] # span: pointer plus length, elements mutable, non-owning
const T[] # span with read-only elements
[N]or[]after a complete type is an array or span. A bracket after a type name that contains types is a generic argument list (§13.1); a bracket that contains only an integer, or nothing, is always an array or span suffix.Pair[i64, str][4]is four pairs;u8[4][4]is an array of four arrays of four bytes, read inside-out as in C;Node*[]is a span of pointers.Nis positive. C forbids zero-length arrays and Base does not declare what C cannot.- An array is a value: it copies element by element, may live inline in a struct or a local, and has C's layout.
- A span is two words, a pointer and a
usizelength. It does not own its elements and does not keep them alive. It is made from an array, from a pointer and a count, or by slicing:
var buffer: u8[4096]
let all: u8[] = buffer # an array lends itself as a span
let head = all[0..<16] # half-open slicing, checked
let tail = all[16..]
let view = u8[](pointer, count) # from a pointer and a length
- Indexing and slicing are bounds-checked and trap on violation, in every build.
span.lengthisusize;span.datais the pointer;span.first()andspan.last()areT?. - An empty span's
datais a non-null, correctly aligned, dangling pointer that must not be dereferenced. This keeps the empty span distinct fromnone, soT[]?uses the ordinary tagged optional representation, not a niche. A C caller that passes(NULL, 0)to an exported span parameter receives the empty span; the export wrapper normalises it (§17.6). T[]converts implicitly toconst T[]. AT[N]converts implicitly toT[]when it is mutable and toconst T[]otherwise.- A span of a local array is a pointer into the frame; §6.6 states the escape rule.
Why spans are the workhorse. C's second-most-common shape after text is "a pointer to N of these, beside N", and every bug in that shape comes from the two travelling separately. A span carries both, checks the index, and costs the same two registers.
5.5 Text#
str is an immutable UTF-8 view: a const u8* and a usize byte count, with the invariant that the bytes are valid UTF-8. Equality and ordering compare bytes. text.byte_count is O(1). text.bytes() is a const u8[]. for character in text iterates Unicode scalars as char. str does not support integer indexing, because byte and scalar boundaries differ; slice text.bytes() and validate, or iterate. Nothing about str allocates, and + on two strings does not exist; use a builder or format_into.
cstr is const c.char* pointing at NUL-terminated text of unknown encoding. It exists for the C boundary.
str.from_bytes(bytes: const u8[]) -> str!validates.str.from_bytes_uncheckeddoes not, and is for code that has already validated.text.to_cstr(allocator) -> c.char*!copies with a terminating NUL into memory the caller owns and can free.value.to_str() -> str!on acstrscans to the NUL and validates.- A formatted string
f"..."has no value. It is consumed immediately byprint, by aWriter.write(§14.4), or byformat_into(buffer: u8[], f"...") -> str!, which writes into the caller's buffer and returns a view of it. Interpolation lowers to appends on that sink; no intermediate string exists. This is theprintfreplacement.
Why a view. Full Luce's str is owned and reference-counted, which is what makes concatenation safe there. Base has no reference counting, so a string is a view of storage that something else owns: static data, a buffer, an arena. Keeping the name str in both tiers means "text" reads the same in both; the difference is who owns the bytes, and Chapter 18 states how the two cross. The formatted-string rule follows: there is no owner for a fresh string, so the interpolation goes to whoever asked for it.
5.6 Function types#
func(A, B) -> R is a C function pointer: one word, never null; func(...)? is nullable with the null niche. A named function, a method through its type (Point.distance), and a capture-free lambda convert to it. Base has no closures (§9.6), so a function type is exactly a C function pointer, and an extern declaration's function-pointer parameters are written with func. A function type has no zero value (§6.1). Conversion between a function pointer and void* is an explicit cast (§7.5).
5.7 Tuples#
(i64, str) is a fixed-size anonymous value used for local grouping and multiple results (§9.3). Tuples have no field names, no methods, and no one-element form; () is the value of unit. A tuple has C's layout as a struct of its components.
5.8 Optionals#
T? is either .some(value) or .none. It is one layer deep; applying ? to an optional is rejected. Its representation is the null niche for pointer, function, and interface-view types, and a tag beside the value otherwise. Chapter 11 states how it is produced and consumed.
5.9 Atomic types#
@T is an atomic variant of T, permitted when T is an integer, bool, usize/isize, a pointer, or a nullable pointer, of at most pointer width. Chapter 15 specifies its operations. @ is part of the type: &x on an @u32 yields @u32*, and no cast removes @.
5.10 Type aliases#
pub type Pixel = f32[4]
type Callback = func(void*, i32) -> unit
An alias is another spelling for the same type. It creates no distinction, has no methods, and is not generic. For a domain distinction, declare a one-field struct.
5.11 Layout#
Every Base aggregate has the target C ABI's layout: declaration order, the ABI's alignment and padding, no reordering. A struct has at least one field, as in C. packed struct Name: removes padding; align(N) struct Name: raises alignment to the power of two N. These are the only two layout controls. Taking the address of a field of a packed struct is a compile error unless the field's natural alignment is one, because C makes the resulting dereference undefined and Base does not produce the pointer.
sizeof(T), sizeof(expression), alignof(T), and offsetof(T, field) are calls yielding usize. alignof takes a type only, as in C.
Base has no type-based aliasing rule. Any pointer may alias any object of compatible size and alignment, and a backend may never assume otherwise.
Why. A Base struct is a C struct; that is what lets Base be the language C libraries are bound in and the runtime is written in. The aliasing guarantee exists because unions and (T*) casts are how C reinterprets memory, and a future backend adding type-based alias analysis would silently break every program that uses them. Stating the guarantee now costs nothing on the current backend and forbids the future mistake.