Ranged Type Aliases
Every use of int, float, and byte in type positions must go through a typealias with mandatory range constraints. This creates a stronger type system where every numeric value has a documented domain. bool and cstring are exempt from this requirement — bool is unranged by nature, and cstring is a pointer type (a NUL-terminated UTF-8 byte pointer used to interoperate with __Builtins.* runtime intrinsics).
Restriction in with clauses: Bare primitive types (int, float, byte) cannot be used as type arguments in with clauses on typealias or type declarations. You must create a ranged typealias first. bool, String, and other struct types are not affected.
// INVALID — bare primitives in with clausestypealias IntArray = Array with int // ERRORtype IntBox implements Container with int // ERROR
// VALID — use a ranged typealiastypealias Integer = int(i64.min to i64.max)typealias IntArray = Array with Integer // OKtype IntBox implements Container with Integer // OKDeclaration:
typealias Port = int(0 to 65535)typealias Percentage = float(0.0 to 100.0)typealias Pixel = int(0 to u8.max)typealias Temperature = int(-273 to 1000)The to keyword makes the upper bound inclusive. The upto keyword makes it exclusive:
typealias Score = int(0 upto 100) // 0 to 99Type-qualified bounds:
Use type.min and type.max to reference bounds of specific numeric types:
typealias FileHandle = int(0 to u32.max)typealias SmallSigned = int(i8.min to i8.max)Supported types: u8, u16, u32, u64, i8, i16, i32, i64, f32, f64.
When both bounds use type qualifiers, they must reference the same type (e.g., i64.min to i64.max, not i8.min to i32.max). A type-qualified bound paired with a literal must form a natural range — 0 to u32.max is valid, but 0 to i64.max is an error (use i64.min to i64.max or 0 to u64.max instead). A negative-literal lower paired with u64.max upper (e.g., int(-1 to u64.max)) is also rejected — no single 64-bit type can represent both ends; use i64.min to i64.max or 0 to u64.max. Byte ranges must have bounds within 0 to u8.max.
Range identifiers as expressions:
type.min and type.max can also be used as expressions anywhere an integer literal is valid — in variable assignments, comparisons, arithmetic, function arguments, etc.:
var x = u16.max // 65535if value == i32.max 'check' // ...end 'check'var y = u8.max + 1 // 256Construction:
Cast a value into a ranged type with as:
typealias Port = int(0 to 65535)var p = 8080 as PortIn most cases the cast is unnecessary — when a literal flows into a slot whose type is already a ranged alias (a parameter, a struct field, a function return), the literal is checked against that target type directly. Use as when the target type needs to be visible at the use site, or when narrowing a wider value to a smaller range.
Compile-time range checks:
Literal values are checked at compile time. This is a compile error:
typealias SmallInt = int(0 to 10)var x = 15 as SmallInt // error: Value 15 is outside the range of 'SmallInt'Runtime range checks:
When the value is a computed expression, a runtime range check is emitted that panics on violation:
typealias Port = int(0 to 65535)typealias RawValue = int(i64.min to i64.max)function makePort(n RawValue) returns RawValue var p = n as Port // runtime check: panics if n < 0 or n > 65535 return pend 'makePort'Return value range checks:
Functions with a ranged return type have their return values checked:
- Returning a literal outside the range is a compile error
- Returning a computed expression emits a runtime range check
- Types whose range covers the full representation (e.g.,
ExitCode) are exempt
typealias Score = int(0 to 100)
function half(s Score) returns Score return s / 2 // runtime range check on return valueend 'half'Arithmetic:
Ranged types support standard arithmetic. The result of arithmetic between ranged values is the underlying primitive type:
typealias Score = int(0 to 100)var a = 30 as Scorevar b = 12 as Scorevar sum = a + b // result is intAll arithmetic on ranged integer types uses 64-bit operations regardless of storage type.
Storage:
The compiler automatically selects the smallest x86-optimal integer width that can represent the declared range for storage in arrays and global variables. All arithmetic still uses 64-bit operations.
| Range fits in | Storage used |
|---|---|
| 0 to u8.max | u8 (1 byte) |
| -128 to 127 | i8 (1 byte) |
| 0 to 65535 | u16 (2 bytes) |
| -32768 to 32767 | i16 (2 bytes) |
| 0 to 4294967295 | u32 (4 bytes) |
| -2147483648 to 2147483647 | i32 (4 bytes) |
| anything wider | i64 (8 bytes) |
typealias Pixel = int(0 to 65535) // stored as u16 in arrays and globalstypealias Delta = int(-32768 to 32767) // stored as i16 in arrays and globalstypealias Percent = int(0 to 100) // stored as u8 in arrays and globalsLocal variables always use 64-bit registers regardless of the ranged type’s storage class.
Standard library aliases:
The standard library exports a small set of cross-cutting aliases that don’t belong to any one domain:
| Alias | Definition | Purpose |
|---|---|---|
ExitCode | platform-dependent | Process exit codes |
HashValue | u32 | Hash function results |
Codepoint | int(0 to 1114111) | Unicode codepoints |
Domain-specific quantities (counts, indices, byte offsets, math values) are declared as typealiases inside the module they belong to — for example String exports ByteCount and GraphemeCount, Math exports Real, and Array keeps ElementCount/ElementIndex private. Application code should follow the same pattern: declare a typealias that names the purpose (e.g. Tally, BytePos, Coord) rather than reaching for a generic Count/Index.
Assignment and rebinding:
Assigning one ranged integer variable to another initially creates an alias — both variables refer to the same underlying value. However, reassigning with arithmetic produces a new value and rebinds the variable without affecting the original:
typealias Pos = int(0 to i64.max)var startPos = Pos{10}var pos = startPos // pos and startPos initially share the same value
pos = pos + 1 // rebinds pos to a new value (11) -- startPos is unaffectedprint("{startPos}") // 10 -- startPos is unchangedprint("{pos}") // 11This behavior means that using a ranged integer as a loop cursor is safe — advancing pos never mutates startPos:
function skipSpaces(src ByteArray, startPos Pos) returns Pos var pos = startPos // pos starts at the same value as startPos while pos < src.length 'loop' if src[pos] != b' ' 'notSpace' break 'loop' end 'notSpace' pos = pos + 1 // advances pos; startPos is unaffected end 'loop' return posend 'skipSpaces'This is in contrast to struct assignment, where field mutations through an alias affect the original. See Reference-by-Default Assignment.