Types
Primitive types
Section titled “Primitive types”| Type | Size | Meaning |
|---|---|---|
u8, i8 | 1 byte | 8-bit unsigned / signed integer |
u16, i16 | 2 bytes | 16-bit unsigned / signed integer |
u32, i32 | 4 bytes | 32-bit unsigned / signed integer |
u64, i64 | 8 bytes | 64-bit unsigned / signed integer |
float | 4 bytes | IEEE-754 binary32 |
double | 8 bytes | IEEE-754 binary64 |
bool | 1 byte | alias of u8; values are true and false |
void | — | absence of value (function returns, parameter list (void)) |
string | pointer | alias of u8* (pointer to null-terminated bytes) |
pointer | target-defined | typeless pointer |
Every width is the same on every target — a u32 is four bytes on the 6502 as
well as on arm64. Only pointers vary: 3 bytes on the banked xt6502 ({lo, hi, bank}), 8 bytes on the 64-bit hosts, 4 on arm9/m68k. Code that needs the number
should use sizeof(T*) rather than a baked-in constant.
Floating point is IEEE-754 on every target, including the 6502, where the arithmetic is done by a software runtime or hardware on the FPGA.
i64 / u64 on every target
Section titled “i64 / u64 on every target”64-bit arithmetic works on all six targets, not just the 64-bit hosts. The
difference is only how: arm64, x86_64 and win64 do it in registers, while
the narrow targets do it out of line — m68k through line-A HLE selectors, arm9
through inline adds/adc plus libgcc, and xt6502 through hand-written
routines in support/xt6502/asm/{i64,u64}/.
The answers are identical everywhere, including 64-bit literals. Add, multiply, divide, shift, unsigned wraparound and comparison of a 2^40 value all agree byte-for-byte between a 6502 and an arm64.
sizeof(i64) is 8 on every target, because width is a layout contract: a struct
containing an i64 lays out identically everywhere.
Conversions
Section titled “Conversions”- Assigning a wider integer to a narrower one truncates, with no sign extension.
- Assigning
float/doubleto an integer takes the integral part, truncated toward zero:(i32)3.7is3,(i32)-3.7is-3. Magnitudes that overflow the destination saturate to0. - Same-width arithmetic stays at that width. There is no C-style promotion to
int, sou8 + u8wraps at 8 bits. Only genuinely mixed-width operands widen (u8 + u16→u16). To get a wider result, widen the operands:
u8 a = (u8)200, b = (u8)100;u8 narrow = a + b; // 44 — 300 & 0xFFu16 wide = a + b; // 44 — STILL a u8 addu16 real = (u16)a + (u16)b; // 300 — widen the OPERANDSThe destination cannot change how the operator computes, which is the point: an
expression means the same thing wherever its result goes. This differs from C,
which promotes both operands to int and would give 300 for the second line.
Structs
Section titled “Structs”Structs gather related data into a value type with copy semantics — passed and returned by value. Field alignment is target-defined: the 6502 packs them byte-by-byte (padding would waste bytes on a byte-oriented CPU), while the register machines insert padding so each field lands on its natural boundary. Declaration order is preserved regardless.
typedef struct { u16 x; u8 y;} CursorPos;
CursorPos topRight = {319, 0};CursorPos middle = {159, 100};Initialisers use { … }, as C does. Members are listed in declaration order;
omitted trailing members are zero-filled, and supplying more elements than the
struct holds is a compile-time error.
([ … ] was accepted here too, for the same reason (( )) was accepted as a
block — an Atari 8-bit keyboard has no brace keys. It went when (( )) did, so
there is one spelling to learn rather than two. An enum body still takes
either.)
A struct can be returned by value:
CursorPos centre(void) { CursorPos c = {160, 96}; return c;}But you may not return a pointer to a stack-resident struct — the storage goes away when the scope ends:
CursorPos* bad(void) { CursorPos c = {1, 2}; return &c; // illegal — c dies at scope exit}Passing &struct as an argument is fine: the callee only holds the pointer for
the duration of the call.
Enumerations
Section titled “Enumerations”enum suits = {hearts, clubs, diamonds, spades};enum directions = {N = 4, S, E, W}; // 4, 5, 6, 7Enumerations start at 0 unless given an explicit value; subsequent entries increment by 1. The compiler picks the smallest unsigned type that holds every value.
Arrays
Section titled “Arrays”u8 cakes[3];u8 spaces[] = {' ', '\t', '\n'}; // size inferred from the initialiseru16 scores[8] = {100, 87}; // remaining 6 slots zero-filledArray size is part of the type; with an initialiser present the size in [ ] may
be omitted.
Range initialiser
Section titled “Range initialiser”Fixed-size arrays with an integer element type also accept a range:
u8 buf[10] = 0..10; // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9u8 b2[5] = 1...5; // 1, 2, 3, 4, 5 (inclusive)u16 b3[4] = 100..104; // 100, 101, 102, 103i8 b4[3] = -2..1; // -2, -1, 0Both bounds must constant-fold, and the resulting count must match the declared
element count; mismatches and non-literal bounds are rejected at compile time.
Float, struct and class arrays still need the { … } form.
.length
Section titled “.length”Fixed-size arrays and heap-allocated pointers both expose a .length
pseudo-property:
u16 local[8];u16 n1 = local.length; // compile-time constant: 8
u16* heap = new u16[64];u16 n2 = heap.length; // 64.length on a pointer the compiler did not record a count for — one that
crossed a function boundary, or came from anywhere but new T[N] — is a compile
error, not a wrong number: there is no map entry to answer from.
Pointers
Section titled “Pointers”Pointer syntax uses *, as C does. & takes an address, and * dereferences:
u16 value = (u16)1234;u16* p = &value;u16 v = *p; // load*p = (u16)4321; // store-> is sugar for “dereference and reach a member”: p->x is (*p).x. Unlike C,
. on a pointer-to-struct or pointer-to-class also works — the compiler
auto-dereferences. Class receivers conventionally use ., because a class
instance is nearly always reached through a pointer, and sprite.draw() reads
better than sprite->draw().
A hardware register is a pointer to a fixed address, reached by casting:
volatile u8* COLBK = (u8*)$D01A;*COLBK = *COLBK + (u8)1; // both accesses happen even at -O3 (volatile)Casting
Section titled “Casting”Casting uses C’s (type) syntax:
u16 n = (u16)x;Two extensions handle class-pointer traffic:
(Dog*) animal— runtime-checked downcast. On a mismatch the program traps.(Dog* ?) animal— failable downcast. On a mismatch it yields(Dog*)0; on success, the retyped pointer. Pair it with anif (d != 0)guard.
Upcasts, same-class casts and non-class-pointer casts are unaffected. The full story is on Inheritance & protocols.
Type inference: auto
Section titled “Type inference: auto”auto infers a variable’s type from its initialiser:
auto x = 3; // u8auto x = -3; // i8auto x = 257; // u16auto x = -259; // i16auto x = 65589; // u32auto x = -555_555; // i32auto x = 4.5; // floatauto x = "hi"; // string (u8*)auto x = true; // boolInteger literals pick the smallest type that holds them; positive values become unsigned, negative values signed.
Inference follows expressions and function returns too, widening where a genuinely mixed-width expression requires it:
u8 a = 4, b = 5;auto c = a + b; // c is u8
u8 a = 4; u16 d = 500;auto e = a + d; // e is u16 (widened)
// given: u8 fn(void) { … }auto v = fn(); // v is u8Explicit types are still preferred — auto is for cases where the expression
makes the type obvious and restating it would be noise.
Type aliases: typedef
Section titled “Type aliases: typedef”Any type can be aliased:
typedef u16 Tick;typedef u8* bytes;typedef RGB[] palette;Aliases are transparent: Tick and u16 are interchangeable everywhere.
A typedef of a function signature is also how a bound-method type is spelled —
typedef void Handler(i32 v); gives you Handler^. See
Bound methods & callbacks.
Protocols
Section titled “Protocols”A protocol is a named interface — method signatures with no bodies. The
type-system view is simply that a protocol name in a type position (usually as a
pointer, e.g. Drawable*) accepts any conforming class instance. Conformance and
optional methods are covered on
Inheritance & protocols.
Worked example
Section titled “Worked example”// types.xc — the scalar types, integer width rules, and pointers.#import "Foundation.xc"#import "Stdio.xc"
i32 main(void){ u8 small = (u8)200; u16 mid = (u16)60000; i32 wide = (i32)-100000; u64 huge = (u64)1 << (u64)40;
// printf's width contract: %d is 16-BIT and %ld is 32-bit, and both are // signed — which is why 60000 in a u16 prints as -5536. Stdio.printf("u8=%d u16=%d (as i32 %ld) i32=%ld\n", small, mid, (i32)mid, wide); Stdio.printf("2^40 = %ld:%ld (hi:lo)\n", (u32)(huge >> (u64)32), (u32)huge);
// Same-width arithmetic stays at that width. u8 a = (u8)200, b = (u8)100; u8 wrapped = a + b; // 300 & 0xFF = 44 // Widening the DESTINATION does not help — `u16 w = a + b;` is still a u8 // add, and still 44. To get the true sum, widen the OPERANDS. u16 widened = (u16)a + (u16)b; // 300 Stdio.printf("u8 200+100 -> %d widened -> %d\n", (u16)wrapped, widened);
// Literal prefixes: $ hex, % binary, _ ignored anywhere in a literal. u16 hex = $BEEF; u8 bin = %1010_0101; u32 big = 1_000_000; Stdio.printf("hex=%ld bin=%d big=%ld\n", (i32)hex, (u16)bin, big);
// Pointers use *, & takes an address. u16 value = (u16)1234; u16* p = &value; Stdio.printf("*p = %d\n", *p); *p = (u16)4321; Stdio.printf("value now %d\n", value);
// The sigil binds to the TYPE, so this declares TWO pointers. u16* x, y; x = &value; y = &value; Stdio.printf("both pointers: %d %d\n", *x, *y);
bool ok = true; float f = 1.5; double d = 3.1d; Stdio.printf("bool=%d float=%f double=%lf\n", ok ? (u16)1 : (u16)0, f, d); return 0;}u8=200 u16=-5536 (as i32 60000) i32=-1000002^40 = 256:0 (hi:lo)u8 200+100 -> 44 widened -> 300hex=48879 bin=165 big=1000000*p = 1234value now 4321both pointers: 4321 4321bool=1 float=1.500000 double=3.1000000000