Operators
xcc operators are largely a subset of C’s, with two notable additions: a pair of rotate operators (<: and :>) that map directly to the 6502’s ROL / ROR instructions, and four byte-extract prefix operators (<, >, >>, >>>) that pull individual bytes out of wider constants and symbols. The byte-extract operators only mean anything inside asm { ... } blocks.
Precedence table
Section titled “Precedence table”Listed from highest to lowest. Rows at the same level have the same precedence; within a level, Assoc records the associativity.
| Operators | Assoc | Notes |
|---|---|---|
a[i], f(...), ., ->, postfix ++, postfix -- | left | primary |
prefix + -, !, ~, prefix ++, prefix --, * (deref), & (addr-of), (type) cast, sizeof(), < > >> >>> (byte extract, asm) | right | unary |
*, /, % | left | multiplicative |
+, - | left | additive |
<:, :> | left | rotate (ROL / ROR) |
<<, >> | left | shift (ASL / ASR) |
<, >, <=, => | left | relational |
==, != | left | equality |
& | left | bitwise AND |
^ | left | bitwise XOR |
| | left | bitwise OR |
&& | left | logical AND |
|| | left | logical OR |
? : | right | ternary |
=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <:=, :>= | right | assignment |
Notes on specific operators
Section titled “Notes on specific operators”Rotate vs shift
Section titled “Rotate vs shift”<: and :> are rotate through carry — they map to the 6502 ROL / ROR instructions, which read and write the carry flag. << and >> are arithmetic shift (ASL / ASR for the right shift). Use rotate when you want to chain bytes through carry; use shift when you want a regular multiply / divide by powers of two.
u8 a = $80;u8 b = a <: 1; // a is rotated left through carry; one-bit shift left + carry-inu8 c = a << 1; // arithmetic left shift; bit 7 lostPointer dereference and address-of
Section titled “Pointer dereference and address-of”* is the unary dereference operator; & takes an address. They’re inverses:
u8 v = 42;u8* p = &v;u8 q = *p; // q == 42*p = 99; // v becomes 99-> is sugar for “dereference then access a member”: p->x is exactly (*p).x. xcc also accepts the dot form p.x directly on a pointer — the compiler auto-dereferences. See Types → Pointers for the rationale.
Cast extensions
Section titled “Cast extensions”Inside the (type) cast, two forms have non-C semantics:
(Dog*) animal— runtime-checked downcast. Traps on mismatch.(Dog* ?) animal— failable downcast. Yields(Dog*)0on mismatch.
These are class-pointer-only; (u16 ?)x is a compile-time error. Details on Inheritance & protocols.
sizeof
Section titled “sizeof”sizeof(T) evaluates to a compile-time u16 byte count. Works on any type, including struct and class.
Byte-extract prefixes (asm context)
Section titled “Byte-extract prefixes (asm context)”These operators have meaning only inside an asm { ... } block, where they let you reach individual bytes of a constant or symbol that the assembler would otherwise treat as a 16- or 32-bit address:
| Prefix | Meaning |
|---|---|
<x | low 8 bits of x |
>x | bits 8..15 of x |
>>x | bits 16..23 of x |
>>>x | bits 24..31 of x |
u16 val = $1234;
asm { lda #<val; // LDA #$34 ldx #>val; // LDX #$12}Outside an asm block these tokens parse as their normal precedences — < and > as relational comparisons, >> as the shift operator. The byte-extract reading is unambiguous in context because the assembler-level grammar accepts only constants and symbols after the prefix.
Compound assignment
Section titled “Compound assignment”Every binary operator that’s also an arithmetic, bitwise, or shift operation has a compound-assignment form: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, plus the rotate forms <:= and :>=. They behave exactly as their expansion suggests:
x += 1; // x = x + 1flags &= ~MASK; // flags = flags & ~MASKacc <:= 1; // acc = acc <: 1When the left-hand side goes through a property setter (see Classes → Properties), the desugaring evaluates the base expression twice — free for identifiers and self, watch-out only if the base has a side effect.
Worked example
Section titled “Worked example”The operators that differ from C, and the ones that don’t:
// operators.xc — the operators that differ from C, and the ones that don't.#import "Foundation.xc"#import "Stdio.xc"
i32 main(void){ // ---- Arithmetic, at the operands' own width ----------------------------- // Same-width arithmetic stays at that width, so these WRAP rather than // promoting to int as C would: 300 & $FF = 44, 600 & $FF = 88. u8 a = (u8)200; u8 b = (u8)100; Stdio.printf("u8 200+100=%d 200*3=%d\n", (u16)(a + b), (u16)(a * (u8)3)); // Widening the OPERANDS is what gets the true sum. Stdio.printf("wide 200+100=%d\n", (u16)a + (u16)b);
// Division and modulo. Integer division truncates toward zero. i16 n = (i16)-17; Stdio.printf("-17/5=%d -17%%5=%d\n", n / (i16)5, n % (i16)5);
// ---- Shift vs rotate ---------------------------------------------------- // `<<` and `>>` shift; `<:` and `:>` ROTATE through the carry flag, which // is what lets you chain bytes together. On the 6502 they are ROL / ROR. u8 hi = $81; Stdio.printf("$81 << 1 = $%x $81 >> 1 = $%x\n", (u16)(hi << (u8)1), (u16)(hi >> (u8)1)); Stdio.printf("$81 <: 1 = $%x $81 :> 1 = $%x\n", (u16)(hi <: (u8)1), (u16)(hi :> (u8)1));
// ---- Bitwise ------------------------------------------------------------ u8 m = $F0; u8 k = $AA; Stdio.printf("and=$%x or=$%x xor=$%x not=$%x\n", (u16)(m & k), (u16)(m | k), (u16)(m ^ k), (u16)(~m));
// ---- Logical, and short-circuit ---------------------------------------- // && and || evaluate left to right and stop as soon as the answer is known. u16 zero = (u16)0; bool safe = (zero != (u16)0) && ((u16)100 / zero > (u16)1); // never divides Stdio.printf("short-circuit ok: %d\n", safe ? (u16)1 : (u16)0);
// ---- Comparison and the ternary ---------------------------------------- u16 x = (u16)7; u16 y = (u16)11; Stdio.printf("max=%d eq=%d ne=%d\n", x > y ? x : y, (u16)(x == y ? 1 : 0), (u16)(x != y ? 1 : 0));
// ---- Compound assignment, including the rotates ------------------------ u8 acc = (u8)1; acc += (u8)4; // 5 acc *= (u8)3; // 15 acc <<= (u8)1; // 30 acc |= (u8)1; // 31 Stdio.printf("compound=%d\n", (u16)acc);
// ---- Increment / decrement --------------------------------------------- // Prefix updates then yields; postfix yields then updates. u16 i = (u16)5; u16 pre = ++i; // i=6, pre=6 u16 post = i++; // post=6, i=7 Stdio.printf("pre=%d post=%d i=%d\n", pre, post, i);
// ---- sizeof ------------------------------------------------------------- // A compile-time constant. Pointer width is the only one that varies by // target; every scalar is the same everywhere. Stdio.printf("sizeof u8=%d u16=%d u32=%d u64=%d float=%d double=%d\n", (u16)sizeof(u8), (u16)sizeof(u16), (u16)sizeof(u32), (u16)sizeof(u64), (u16)sizeof(float), (u16)sizeof(double));
// ---- Address-of and dereference ---------------------------------------- u16 v = (u16)42; u16* p = &v; *p = *p + (u16)1; Stdio.printf("through pointer: %d\n", v); return 0;}u8 200+100=44 200*3=88wide 200+100=300-17/5=-3 -17%5=-2$81 << 1 = $0002 $81 >> 1 = $0040$81 <: 1 = $0003 $81 :> 1 = $00C0and=$00A0 or=$00FA xor=$005A not=$000Fshort-circuit ok: 0max=11 eq=0 ne=1compound=31pre=6 post=6 i=7sizeof u8=1 u16=2 u32=4 u64=8 float=4 double=8through pointer: 43Note the first line: u8 + u8 and u8 * u8 stay at 8 bits and wrap (44 and 88), where C would promote both to int and print 300 and 600. Widening the operands is what gets the true value.