Skip to content

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.

Listed from highest to lowest. Rows at the same level have the same precedence; within a level, Assoc records the associativity.

OperatorsAssocNotes
a[i], f(...), ., ->, postfix ++, postfix --leftprimary
prefix + -, !, ~, prefix ++, prefix --, * (deref), & (addr-of), (type) cast, sizeof(), < > >> >>> (byte extract, asm)rightunary
*, /, %leftmultiplicative
+, -leftadditive
<:, :>leftrotate (ROL / ROR)
<<, >>leftshift (ASL / ASR)
<, >, <=, =>leftrelational
==, !=leftequality
&leftbitwise AND
^leftbitwise XOR
|leftbitwise OR
&&leftlogical AND
||leftlogical OR
? :rightternary
=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <:=, :>=rightassignment

<: 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-in
u8 c = a << 1; // arithmetic left shift; bit 7 lost

* 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.

Inside the (type) cast, two forms have non-C semantics:

  • (Dog*) animal — runtime-checked downcast. Traps on mismatch.
  • (Dog* ?) animal — failable downcast. Yields (Dog*)0 on mismatch.

These are class-pointer-only; (u16 ?)x is a compile-time error. Details on Inheritance & protocols.

sizeof(T) evaluates to a compile-time u16 byte count. Works on any type, including struct and class.

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:

PrefixMeaning
<xlow 8 bits of x
>xbits 8..15 of x
>>xbits 16..23 of x
>>>xbits 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.

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 + 1
flags &= ~MASK; // flags = flags & ~MASK
acc <:= 1; // acc = acc <: 1

When 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.

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=88
wide 200+100=300
-17/5=-3 -17%5=-2
$81 << 1 = $0002 $81 >> 1 = $0040
$81 <: 1 = $0003 $81 :> 1 = $00C0
and=$00A0 or=$00FA xor=$005A not=$000F
short-circuit ok: 0
max=11 eq=0 ne=1
compound=31
pre=6 post=6 i=7
sizeof u8=1 u16=2 u32=4 u64=8 float=4 double=8
through pointer: 43

Note 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.