Skip to content

Expression Data Types

Basm supports several data types in expressions.

Integer Types

Integers can be written in multiple formats:

  • Decimal: 42, 255
  • Hexadecimal: $FF, #ABCD, &CAFE, 0x1234, 0X5678
  • Binary: %11001100, 0b10101010, 0B11110000
  • Octal: 0o377, 0O177, @377
  • Character: 'A' (evaluates to ASCII value 65)

All numeric formats are demonstrated in the test file:

    ; Test various numeric base representations
    ; All of these should represent the same values

    org $4000

    ; === Value 255 in different bases ===
decimal_255:
    db 255              ; Decimal
    assert memory(decimal_255) == 255

hex_dollar_255:
    db $FF              ; Hexadecimal with $
    assert memory(hex_dollar_255) == 255

hex_0x_255:
    db 0xFF             ; Hexadecimal with 0x prefix
    assert memory(hex_0x_255) == 255

hex_hash_255:
    db #FF              ; Hexadecimal with # prefix
    assert memory(hex_hash_255) == 255

hex_ampersand_255:
    db &FF              ; Hexadecimal with & prefix
    assert memory(hex_ampersand_255) == 255

binary_255:
    db %11111111        ; Binary with %
    assert memory(binary_255) == 255

binary_0b_255:
    db 0b11111111       ; Binary with 0b prefix
    assert memory(binary_0b_255) == 255

octal_255:
    db 0o377            ; Octal with 0o prefix
    assert memory(octal_255) == 255

octal_at_255:
    db @377             ; Octal with @ prefix
    assert memory(octal_at_255) == 255

    ; Verify all representations are equal
    assert 255 == $FF
    assert 255 == 0xFF
    assert 255 == #FF
    assert 255 == &FF
    assert 255 == %11111111
    assert 255 == 0b11111111
    assert 255 == 0o377
    assert 255 == @377


    ; === Value 42 in different bases ===
decimal_42:
    db 42               ; Decimal
    assert memory(decimal_42) == 42

hex_dollar_42:
    db $2A              ; Hexadecimal
    assert memory(hex_dollar_42) == 42

hex_0x_42:
    db 0x2A             ; Hexadecimal with 0x
    assert memory(hex_0x_42) == 42

binary_42:
    db %00101010        ; Binary
    assert memory(binary_42) == 42

binary_0b_42:
    db 0b101010         ; Binary (without leading zeros)
    assert memory(binary_0b_42) == 42

octal_42:
    db 0o52             ; Octal
    assert memory(octal_42) == 42

    ; Verify all representations are equal
    assert 42 == $2A
    assert 42 == 0x2A
    assert 42 == #2A
    assert 42 == &2A
    assert 42 == %101010
    assert 42 == 0b101010
    assert 42 == 0o52
    assert 42 == @52


    ; === Value 4096 (16-bit) in different bases ===
decimal_4096:
    dw 4096             ; Decimal
    assert memory(decimal_4096) == 0x00  ; LSB
    assert memory(decimal_4096+1) == 0x10  ; MSB

hex_4096:
    dw $1000            ; Hexadecimal
    assert memory(hex_4096) == 0x00
    assert memory(hex_4096+1) == 0x10

binary_4096:
    dw %0001000000000000  ; Binary
    assert memory(binary_4096) == 0x00
    assert memory(binary_4096+1) == 0x10

octal_4096:
    dw 0o10000          ; Octal
    assert memory(octal_4096) == 0x00
    assert memory(octal_4096+1) == 0x10

    ; Verify all representations are equal
    assert 4096 == $1000
    assert 4096 == 0x1000
    assert 4096 == #1000
    assert 4096 == &1000
    assert 4096 == %0001000000000000
    assert 4096 == 0b1000000000000
    assert 4096 == 0o10000
    assert 4096 == @10000


    ; === Edge cases ===

    ; Zero in all bases
    db 0, $0, 0x0, #0, &0, %0, 0b0, 0o0, @0
    assert 0 == $0
    assert 0 == %0
    assert 0 == 0o0

    ; One in all bases
    db 1, $1, 0x1, #1, &1, %1, 0b1, 0o1, @1
    assert 1 == $1
    assert 1 == %1
    assert 1 == 0o1

    ; Powers of 2
    assert 128 == $80
    assert 128 == %10000000
    assert 128 == 0o200

    assert 256 == $100
    assert 256 == %100000000
    assert 256 == 0o400

    ret

Negative integers use the unary minus: -42

Floats

Floating point values support decimal notation and scientific notation:

    ; Test floating point values and operations
    org $4000

start:
    ; Basic floating point values
    pi = 3.14159
    half = 0.5
    negative = -2.5

    ; Assertions for basic floats
    assert pi == 3.14159
    assert half == 0.5
    assert negative == -2.5

    ; Scientific notation
    micro = 1.0e-6
    thousand = 1.5e3

    assert micro == 0.000001
    assert thousand == 1500.0

    ; Float arithmetic
    sum = 1.5 + 2.5
    assert sum == 4.0

    product = 2.0 * 3.5
    assert product == 7.0

    ; Comparison operators with floats
    assert 3.5 > 2.0
    assert 2.0 >= 2.0
    assert 1.5 < 2.5
    assert 2.5 <= 2.5
    assert 2.5 == 2.5

    ; Float functions (if supported)
    abs_neg = abs(-3.5)
    assert abs_neg == 3.5

    min_val = min(1.5, 2.5)
    assert min_val == 1.5

    max_val = max(1.5, 2.5)
    assert max_val == 2.5

    ; Mixed integer and float arithmetic
    mixed = 10 + 0.5
    assert mixed == 10.5

    ret

Examples:

  • 3.14159
  • 2.5
  • -0.5
  • 1.0e-6 (scientific notation)
  • 1.5e3 equals 1500.0

Strings

String literals are enclosed in double quotes and are primarily used with the DB directive:

    ; Test string literals and functions
    org $4000

start:
    ; Basic string literals (used with db directive)
data_string1:
    db "Hello, World!"

data_string2:
    db "CPC forever"

data_string3:
    db ""  ; empty string

    ; String escape sequences
data_escapes:
    db "Line 1\nLine 2"     ; newline
    db "Tab\there"          ; tab
    db "Path\\file"         ; backslash
    db "Say \"hello\""      ; quote

    ; String length function
    len1 = string_len("Hello")
    assert len1 == 5

    len2 = string_len("CPC")
    assert len2 == 3

    len3 = string_len("")
    assert len3 == 0

    ; String concatenation
    greeting = string_concat("Hello", " ", "World")
    assert string_len(greeting) == 11

    ; More complex concatenation
    full_greeting = string_concat("Hello", ", ", "dear ", "friend", "!")
    assert string_len(full_greeting) == 19

    ret

Strings can contain escape sequences:

  • \n - newline
  • \t - tab
  • \\ - backslash
  • \" - quote

String functions:

  • string_len(str) - returns the length of a string
  • string_concat(str1, str2, ...) - concatenates multiple strings (2 or more arguments)

Booleans

Boolean values for conditional expressions:

  • True: true, 1
  • False: false, 0

Booleans are demonstrated in the test file:

    ; Test boolean values and operations
    org $4000

start:
    ; Basic boolean literals
    true_val = true
    false_val = false

    ; Boolean assertions
    assert true_val == true
    assert false_val == false
    assert true == true
    assert false == false
    assert true != false

    ; Boolean in conditional expressions (ternary)
    result1 = true ? 1 : 0
    assert result1 == 1

    result2 = false ? 1 : 0
    assert result2 == 0

    ; Comparison operations return booleans
    is_greater = (10 > 5)
    assert is_greater == true

    is_less = (10 < 5)
    assert is_less == false

    is_equal = (42 == 42)
    assert is_equal == true

    is_not_equal = (42 != 43)
    assert is_not_equal == true

    ; Boolean logic with comparisons
    assert (5 > 3) == true
    assert (5 < 3) == false
    assert (5 >= 5) == true
    assert (5 <= 5) == true
    assert (5 == 5) == true
    assert (5 != 5) == false

    ; Combined logical expressions
    and_result = (true && true)
    assert and_result == true

    and_false = (true && false)
    assert and_false == false

    or_result = (true || false)
    assert or_result == true

    or_false = (false || false)
    assert or_false == false

    ; Negation - using NOT operator
    assert !(true) == false
    assert !(false) == true
    assert NOT(true) == false
    assert NOT(false) == true

    ; Boolean in data generation
data_start:
    db true ? 255 : 0
    assert memory(data_start) == 255

    db false ? 255 : 0
    assert memory(data_start+1) == 0

    ; Complex boolean expressions
    complex1 = (10 > 5) && (20 < 30)
    assert complex1 == true

    complex2 = (10 > 5) && (20 > 30)
    assert complex2 == false

    complex3 = (10 < 5) || (20 < 30)
    assert complex3 == true

    complex4 = (10 < 5) || (20 > 30)
    assert complex4 == false

    ; Truthiness of non-boolean values
    ; Non-zero is truthy
    assert (5 ? true : false) == true
    assert (1 ? true : false) == true
    assert (-1 ? true : false) == true

    ; Zero is falsy
    assert (0 ? true : false) == false

    ; Using booleans to control assembly
    DEBUG_MODE = false
    RELEASE_MODE = true

    assert DEBUG_MODE == false
    assert RELEASE_MODE == true

    ret

Boolean operators include:

  • Logical AND: &&
  • Logical OR: ||
  • Logical NOT: !, NOT
  • Comparison: ==, !=, <, >, <=, >=

Labels

Labels can be referenced in expressions and resolve to addresses:

    ; Labels example
    org $4000

start:
    LD A, 5
    JP start     ; Label reference

    ret

The special symbol $ represents the current program counter.

Lists

Lists are heterogeneous collections enclosed in square brackets:

    ; Lists example
    org $4000

    ; Basic list creation
    list1 = [1, 2, 3, 4, 5]
    assert list1 == [1, 2, 3, 4, 5]

    ; Empty list
    list2 = []
    assert list2 == []

    ; list_len - get the length of a list
    len1 = list_len(list1)
    assert len1 == 5

    len2 = list_len(list2)
    assert len2 == 0

    ; list_get - get element at index (0-based)
    first = list_get(list1, 0)
    assert first == 1

    second = list_get(list1, 1)
    assert second == 2

    last = list_get(list1, 4)
    assert last == 5

    ; list_new - create a new list with n elements (all initialized to given value)
    list3 = list_new(3, 0)
    assert list_len(list3) == 3
    assert list_get(list3, 0) == 0
    assert list_get(list3, 1) == 0
    assert list_get(list3, 2) == 0

    ; list_new with non-zero initial value
    list3b = list_new(2, 42)
    assert list_get(list3b, 0) == 42
    assert list_get(list3b, 1) == 42

    ; list_set - set element at index
    list4 = list_set(list3, 0, 10)
    assert list_get(list4, 0) == 10
    assert list_get(list4, 1) == 0
    assert list_get(list4, 2) == 0

    list4 = list_set(list4, 1, 20)
    list4 = list_set(list4, 2, 30)
    assert list4 == [10, 20, 30]

    ; list_push - append an element to the end
    list5 = list_push(list1, 6)
    assert list_len(list5) == 6
    assert list_get(list5, 5) == 6

    ; list_sublist - extract a sublist (start_index, end_index - not included)
    ; list1 = [1, 2, 3, 4, 5], extract from index 1 to 4 (not included) = [2, 3, 4]
    sublist = list_sublist(list1, 1, 4)
    assert list_len(sublist) == 3
    assert list_get(sublist, 0) == 2
    assert list_get(sublist, 1) == 3
    assert list_get(sublist, 2) == 4

    ; list_extend - concatenate two lists
    list6 = [10, 20]
    list7 = [30, 40]
    combined = list_extend(list6, list7)
    assert list_len(combined) == 4
    assert combined == [10, 20, 30, 40]

    ; list_sort - sort list in ascending order
    unsorted = [5, 2, 8, 1, 9]
    sorted = list_sort(unsorted)
    assert sorted == [1, 2, 5, 8, 9]

    ; list_argsort - return indices that would sort the list
    indices = list_argsort(unsorted)
    assert list_len(indices) == 5
    ; indices should point to sorted order
    assert list_get(unsorted, list_get(indices, 0)) == 1
    assert list_get(unsorted, list_get(indices, 1)) == 2
    assert list_get(unsorted, list_get(indices, 4)) == 9

    ; Mixed types list
    mixed = [1, 2.5, 3]
    assert list_len(mixed) == 3
    assert list_get(mixed, 0) == 1
    assert list_get(mixed, 1) == 2.5
    assert list_get(mixed, 2) == 3

    ret

Lists support:

  • Indexing and slicing: list[0], list[1..3] - see Indexing and Slicing
  • Nesting: [[1, 2], [3, 4]]
  • Functions: list_len(), list_get(), etc.

Ranges

A range denotes a sequence of integers without writing every value out by hand. Range syntax matches Rust's own exactly:

  • a..b - exclusive of b
  • a..=b - inclusive of b
    ; Ranges example
    org $4000

    ; a..b - exclusive of b, same semantics as Rust's own range
    r1 = 0..5
    assert list_len(r1) == 5
    assert list_get(r1, 0) == 0
    assert list_get(r1, 4) == 4

    ; a..=b - inclusive of b
    r2 = 0..=5
    assert list_len(r2) == 6
    assert list_get(r2, 5) == 5

    ; a > b is empty, not auto-descending
    r3 = 5..1
    assert list_len(r3) == 0

    ; a range bound can be any expression, including a label
    count equ 3
    r4 = 0..count
    assert list_len(r4) == 3

    ; DB/DEFW/STR emit every value in the range directly, exactly as if it
    ; had been written out by hand
start:
    db 0..4
    db 0..=4
end_marker:
    assert end_marker - start == 4 + 5

    ; ITERATE ... IN accepts a range the same way it accepts a list
iterate_start:
    iterate i in 0..4
        db {i}
    endi
iterate_end:
    assert iterate_end - iterate_start == 4

    ; range_step_by - there is no dedicated a..step..b syntax; step through
    ; a range by calling this instead
    stepped = range_step_by(0..10, 2)
    assert list_len(stepped) == 5
    assert list_get(stepped, 0) == 0
    assert list_get(stepped, 1) == 2
    assert list_get(stepped, 4) == 8

    ; list_sublist(a_list, a_range) - a range used as an index selector into
    ; an ordinary list, gathering the elements at those positions
    source = [10, 20, 30, 40, 50]
    picked = list_sublist(source, 1..3)
    assert picked == [20, 30]

    ret

A range is empty when a > b - there is no auto-descending. There is no dedicated stepped-range syntax (no a..step..b); to step through a range, either call range_step_by(a_range, step) or combine a range with broadcasting, e.g. base + (0..n) * stride.

A range behaves like a list wherever a list is expected - list_len(), list_get(), DB/DEFW/STR emission, and ITERATE ... IN all accept a range directly, with no conversion needed. Unlike a list literal, a range never allocates its elements up front: db 0..65536 and list_len(0..65536) compute directly from the range's bounds instead of building a 65536-element list first.

A range used unparenthesized inside arithmetic is a parse error (the range operator has the same low precedence Rust's own does) - write (0..5) * 2, not 0..5 * 2.

Broadcasting

Arithmetic (+ - * / %), bitwise (&, |), and relational (< > <= >=) operators apply element-wise when one or both operands is a list (or a range):

    ; Broadcasting example: an operator applied element-wise across a list
    org $4000

    ; arithmetic operators broadcast a scalar against every element, in
    ; either order
    a1 = [1, 2, 3] + 10
    assert a1 == [11, 12, 13]

    a2 = 10 + [1, 2, 3]
    assert a2 == [11, 12, 13]

    a3 = [10, 20, 30] - 5
    assert a3 == [5, 15, 25]

    a4 = [1, 2, 3] * 2
    assert a4 == [2, 4, 6]

    a5 = [10, 20, 30] / 10
    assert a5 == [1.0, 2.0, 3.0]

    ; bitwise AND/OR broadcast too
    b1 = [6, 5] & 3
    assert b1 == [2, 1]

    b2 = [4, 1] | 2
    assert b2 == [6, 3]

    ; comparisons broadcast into a list of booleans
    c1 = [1, 2, 3] < 2
    assert list_get(c1, 0) == true
    assert list_get(c1, 1) == false
    assert list_get(c1, 2) == false

    ; == and != do NOT broadcast - they compare the whole list at once,
    ; same as they always have, and return a single boolean
    assert ([1, 2, 3] == [1, 2, 3]) == true
    assert ([1, 2] == [1, 2, 3]) == false

    ; a range broadcasts too - it is converted to a list first, since a
    ; scaled or shifted range is no longer a contiguous range
    d1 = (0..3) * 2
    assert d1 == [0, 2, 4]

    ; two lists of the same length still combine element-wise, as before
    e1 = [1, 2, 3] + [10, 20, 30]
    assert e1 == [11, 22, 33]

    ; nested lists broadcast recursively
    f1 = [[1, 2], [3, 4]] + 1
    assert f1 == [[2, 3], [4, 5]]

    ret
  • List and scalar, either order: the scalar combines with every element - [1,2,3] + 10 and 10 + [1,2,3] both give [11,12,13].
  • Two lists of the same length: elements combine pairwise - [1,2,3] + [10,20,30] gives [11,22,33]. Lists of different lengths are a hard error.
  • Nested lists: broadcast recursively through every level.
  • A range: converts to a list first, since a scaled or shifted range (e.g. (0..1000) * 2) is no longer a contiguous range.
  • == and != do not broadcast. They compare the whole list (or range) at once and return a single boolean, exactly as they always have - [1,2] == [1,2] is true, not [true,true]. Broadcasting ==/!= would silently change that shape, breaking anything using such a comparison as an IF/ASSERT condition.
  • A relational comparison broadcasts into a list of booleans, which - like any other list - cannot be used directly as an IF/ASSERT condition without picking an element out of it first.

Matrices

Matrices are 2D arrays, created via matrix_new() or from nested lists:

list1 = [[5, 5, 5],[5, 5, 5],[5, 5, 5]]

mat1 = matrix_new(3, 3, 5)
mat2 = matrix_new([[5, 5, 5],[5, 5, 5],[5, 5, 5]])
mat3 = matrix_new(list1)

print mat1
print mat2
print mat3

assert mat1 == mat2
assert mat1 == mat3
assert mat2 == mat3

Matrices support various operations through built-in functions (see functions).

Matrices support specialized access functions documented in the functions page, and the [x, y] bracket form below.

Indexing and Slicing

target[...] accesses an element or a slice of a list, string, range, or matrix - the same bracket notation used to write a list literal ([1, 2, 3]), applied after an existing value instead:

    ; Indexing and slicing example
    org $4000

    ; target[i] - a single element, 0-based
    numbers = [10, 20, 30, 40]
    assert numbers[0] == 10
    assert numbers[3] == 40

    ; target[a..b] - a slice, using a range
    assert numbers[1..3] == [20, 30]

    ; target[[i, j, ...]] - gather several positions into a new list
    assert numbers[[0, 2]] == [10, 30]

    ; the same [i]/[a..b] forms work on strings too
    greeting = "hello world"
    assert greeting[0] == 'h'
    assert greeting[0..5] == "hello"

    ; and directly on a range, in O(1) - no list is ever built to answer this
    assert (0..1000000)[500000] == 500000

    ; a literal can be indexed directly, no named variable required
    assert [1, 2, 3][1] == 2

    ; subscripts chain - each [] applies to the result of the previous one
    nested = [[1, 2], [3, 4]]
    assert nested[1][0] == 3

    ; a matrix needs two indices, x (column) then y (row)
    grid = matrix_new([[1, 2], [3, 4], [5, 6]])
    assert grid[0, 0] == 1
    assert grid[1, 2] == 6

    ret
  • target[i] - a single element (0-based). On a list or range this gives a value; on a string it gives a character.
  • target[a..b] - a slice, using a range as the index. Works on lists and strings, giving back the same kind of value (a sub-list or a sub-string).
  • target[[i, j, ...]] - a gather: gives back a new list holding the elements at each of the given positions, in order. Works anywhere target[i] does (list, string, range).
  • target[x, y] - two indices, for a matrix only: x is the column, y is the row.
  • Indexing a range is constant-time, just like list_len/list_get on a range - no list is materialized to answer (0..1000000)[500000].
  • Subscripts bind as tightly as possible, directly to the value they follow, before any binary operator - a[0] + b[1] is (a[0]) + (b[1]). They also chain: a[0][1] applies the second [1] to the result of a[0].
  • A literal can be indexed directly, without a named variable: [1, 2, 3][1], "abc"[0], (0..5)[2].
  • A MACRO parameter can be indexed too, when the call passes a list literal directly (GET_ITEM([1, 2, 3], 0) with a body of db {l}[{idx}]) - the substitution is automatically re-wrapped in brackets so the following [...] indexes into it, without disturbing {l} alone (no [...] after it in the body), which keeps spreading a list argument flat across a data line (db {l} with l=[1,2,3] still gives db 1,2,3, not db [1,2,3]) - both forms can be used with the same parameter in the same macro body. A single argument that merely evaluates to a list (an identifier, a function call, ...) needs no such handling - it already substitutes as plain text and indexes correctly on its own.

Operators

Binary Operators

Listed by precedence (highest to lowest). Indexing/slicing (target[...]) binds tighter than any of these - it applies directly to the value it follows, before any operator below gets a chance to.

  1. Multiplication/Division: *, / (real division), // (integer division), % (modulo)
  2. Addition/Subtraction: +, -
  3. Bitwise Shift: <<, >>
  4. Relational: <, >, <=, >=
  5. Equality: ==, !=
  6. Bitwise AND: &
  7. Bitwise XOR: ^
  8. Bitwise OR: |
  9. Logical AND: &&
  10. Logical OR: ||

/ always divides as a real number, even for two integer operands (e.g. 7 / 2 is 3.5). // always divides as an integer, truncating toward zero (e.g. 7 // 2 is 3, -7 // 2 is -3). Loading a real value into a register (e.g. ld a, 7 / 2) emits a warning, since Z80 registers can only hold integers.

Breaking change

// used to also work as a line-comment marker, in addition to ;. As of this release it is exclusively the integer-division operator - only ; starts a line comment now.

Unary Operators

  • Negation: -x (arithmetic)
  • Bitwise NOT: ~x
  • Logical NOT: !x
  • Low byte: <x (equivalent to low(x))
  • High byte: >x (equivalent to high(x))

Operator Examples

    ; Operators example
    org $4000

    value = (5 + 3) * 2       ; = 16
    mask = $FF & %00001111    ; = $0F
    shifted = 1 << 4          ; = 16
    high_byte = high($1234)   ; = $12
    low_byte = low($1234)     ; = $34
    int_div = 7 // 2          ; = 3 (integer division, always truncates toward zero)

    ret

Type Conversions

Implicit conversions occur in expressions:

  • Integer to Float: automatic when mixed with floats
  • Boolean to Integer: true → 1, false → 0
  • Integer to Boolean: 0 → false, non-zero → true
  • Character to Integer: automatic (ASCII value)

Function Calls

Functions are called with parentheses:

    ; Function calls example
    org $4000

    ; Functions in expressions
    ld a, high($ABCD)
    ld b, low($ABCD)
    ld c, max(10, 20, 30)

    ret

See the functions page for a complete list of built-in functions.

Lambda Expressions

(params) => expr is an inline, unnamed function - most useful as a callback for list_map/list_filter/list_fold/list_position_predicate and similar functions that expect a function name:

    ; Lambda expressions example
    org $4000

    ; (params) => expr - an inline, unnamed function, most useful as a
    ; callback for list_map/list_filter/list_fold/list_position_predicate
    numbers = [1, 2, 3, 4, 5]

    doubled = list_map(numbers, (x) => x * 2)
    assert doubled == [2, 4, 6, 8, 10]

    evens = list_filter(numbers, (x) => x % 2 == 0)
    assert evens == [2, 4]

    total = list_fold(numbers, 0, (acc, x) => acc + x)
    assert total == 15

    ret
  • The parameter list always needs parentheses, even for a single parameter ((x) => x * 2, not x => x * 2) - this keeps the grammar unambiguous with a bare identifier starting some other expression.
  • A lambda is sugar over the same machinery as a named FUNCTION, not a real closure: its body only ever sees its own parameters and true global symbols, exactly like a FUNCTION would - it cannot see a variable local to whatever is calling it (e.g. an enclosing FUNCTION's own parameter).

Special Symbols

  • $ - Current program counter (assembly address)
  • $$ - Start of current section
  • $-$$ - Offset within current section
    ; Special symbols example
    org $4000

start:
    ld a, ($ + 5)  ; Reference current address + 5
    db $ - $$      ; Offset from section start

    ret

Conditional Expressions

The ternary operator for inline conditionals:

    ; Ternary operator example
    org $4000

start:
    ; Ternary in instruction
    ld a, (1 > 0) ? 42 : 0
    assert memory(start) == $3e     ; ld a, nn opcode
    assert memory(start+1) == 42    ; Should be 42 (true branch)

    ; Max using ternary
    ld b, (10 > 20) ? 10 : 20
    assert memory(start+2) == $06   ; ld b, nn opcode
    assert memory(start+3) == 20    ; Should be 20 (false branch, 10 < 20)

    ; Simple data bytes with ternary
data_start:
    db (1 > 0) ? 42 : 0
    assert memory(data_start) == 42

    db (0 > 1) ? 42 : 0
    assert memory(data_start+1) == 0

    ; Nested ternary
    db (1 > 0) ? ((2 > 1) ? 100 : 50) : 0
    assert memory(data_start+2) == 100

    ; With arithmetic
    db (5 * 2 > 8) ? (10 + 5) : (2 + 3)
    assert memory(data_start+3) == 15

    ; Boolean conditions
    db true ? 1 : 0
    assert memory(data_start+4) == 1

    db false ? 1 : 0
    assert memory(data_start+5) == 0

    ; Edge case: zero condition (falsy)
    db 0 ? 99 : 77
    assert memory(data_start+6) == 77

    ; Edge case: non-zero condition (truthy)
    db 5 ? 99 : 77
    assert memory(data_start+7) == 99

    ret