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.141592.5-0.51.0e-6(scientific notation)1.5e3equals 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 stringstring_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:
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 ofba..=b- inclusive ofb
; 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] + 10and10 + [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]istrue, not[true,true]. Broadcasting==/!=would silently change that shape, breaking anything using such a comparison as anIF/ASSERTcondition.- A relational comparison broadcasts into a list of booleans, which - like any other list - cannot be
used directly as an
IF/ASSERTcondition 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 anywheretarget[i]does (list, string, range).target[x, y]- two indices, for a matrix only:xis the column,yis the row.- Indexing a range is constant-time, just like
list_len/list_geton 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 ofa[0]. - A literal can be indexed directly, without a named variable:
[1, 2, 3][1],"abc"[0],(0..5)[2]. - A
MACROparameter can be indexed too, when the call passes a list literal directly (GET_ITEM([1, 2, 3], 0)with a body ofdb {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}withl=[1,2,3]still givesdb 1,2,3, notdb [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.
- Multiplication/Division:
*,/(real division),//(integer division),%(modulo) - Addition/Subtraction:
+,- - Bitwise Shift:
<<,>> - Relational:
<,>,<=,>= - Equality:
==,!= - Bitwise AND:
& - Bitwise XOR:
^ - Bitwise OR:
| - Logical AND:
&& - 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 tolow(x)) - High byte:
>x(equivalent tohigh(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, notx => 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 aFUNCTIONwould - it cannot see a variable local to whatever is calling it (e.g. an enclosingFUNCTION'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