Aquarius DevStudio Cb Compiler - fvdhoef/aquarius-plus GitHub Wiki
Aquarius DevStudio comes with a compiler for the Cb language (pronounced: C-flat).
The Cb programming language is a new programming language designed by Frank van den Hoef for the Aquarius+. The language is based on the C programming language, but is simplified to make it possible to run the compiler on actual Aquarius+ hardware.
It is not the aim of the compiler to produce the most optimized code, but it will generate code that is much faster than programs coded in BASIC. Furthermore, it is possible to mix Cb and assembler code in the same file to allow speed-critical parts of a program to be optimized in assembly.
The language is kept as simple as possible. There are no structs, unions, enums, do/while loops, etc.
The compiler will compile a .cb
source file into a .asm
file, which then can be compiled with the assembler. The output assembly file will also contain the lines from the source file as comment interleaved with the generated code, so that generated code can by easily inspected.
Two base types are supported:
- char (unsigned 1-byte value)
- int (signed 2-byte value)
Variable names should start with one of: _
a
-z
A
-Z
and can be followed by _
a
-z
A
-Z
0
-9
.
When global variables and function names are emitted into the assembler output file they are prefixed by a _
.
Local variables can be defined anywhere in a function.
Examples of various variable declarations:
char a; // Uninitialized character variable
char b = 42; // Initialized character variable
int c; // Uninitialized integer variable
// Global variables are initialized to 0, local variables will
// have a random stack value and should always be assigned a value
// before using it.
int d = 10 + 3 * 2; // Global variables can be initialized with a compile time expression
// Local variables can be initialized with any expression
char *scr = 0x3000; // A pointer to memory location 0x3000
// Arrays (can only be defined at the global level)
int e[10] = {1,2,3,4}; // This will use 20 bytes of RAM of which the
// first 8 bytes are initialized by the values given
int f[] = {1,2,3}; // This will use 6 bytes of RAM
int g[] = "Test"; // This will use 5 bytes of RAM (of which 1 byte for zero-termination)
const int h = 1234; // Const variables don't consume any RAM, but can only take a
// compile-time expression.
ioport IO_VCTRL = 0xE0; // IO-port variables will emit a OUT or IN instruction with the given
// value as IO port (0xE0 in this case)
extern char mydata[]; // Useful to use data provided by assembly files. In this case with the label _mydata.
Kind | Example |
---|---|
Decimal | 1234 |
Hexadecimal | 0xBEEF |
Character | 'A' |
Character constants and string literals support the following escape sequences:
Sequence | Description |
---|---|
\n |
10 - newline |
\r |
13 - carriage return |
\b |
8 - backspace |
\\ |
\ |
\' |
' |
\" |
" |
When a string literal is used, it is automatically emitted at the global scope and a label is assigned to it. The pointer of the label is then used in the expression.
puts("Hello world!\n");
char *p = "Test";
A function can be defined as follows:
myfunction1(int a, int b) {
return a * b;
}
myfunction2() {
int a = myfunction1(1, 2);
// Do stuff
}
A return value is optional, but if used, is always a 16-bit signed value (int).
Conditional code can be made using the if
and else
keywords.
if (a < 10) {
// Do something
} else {
// Do something else
}
For takes up to 3 expressions:
int a = 0;
for (a=0; a<10; a=a+1) {
// Do things
}
Within a for loop, the continue
statement can be used to directly go the next iteration of the loop and the break
statement can be used to exit the loop.
While takes an expression that is evaluated. If the result is non-zero the code block is executed. Within a while loop, the continue
statement can be used to directly go the next iteration of the loop and the break
statement can be used to exit the loop.
int a = 0;
while (a < 10) {
a = a + 1;
}
Return from a function with an optional return value. Functions will automatically return at the end of the function and don't require the return
keyword for this.
Complex expression are supported and follow the C precedence rules. The following operators are supported (listed in descending precedence):
Operator | Description |
---|---|
() []
|
Function call, array subscripting |
- + ~ ! * &
|
Unary negate, Unary plus, Unary bitwise-not, Logical not, Dereference pointer, Address-of |
(char *) (int *)
|
Cast to character pointer, cast to integer pointer |
* / %
|
Multiplication, division, and remainder |
+ -
|
Addition and subtraction |
<< >>
|
Bitwise left shift and right shift |
<= >= < >
|
Relational operators: less-or-equal, greater-or-equal, less-than, greater-than |
== !=
|
Equal (==), Not equal |
& |
Bitwise AND |
^ |
Bitwise XOR |
| |
Bitwise OR |
&& |
Logical AND |
|| |
Logical OR |
= |
Assign |
When comparison operators are used with a pointer type the comparison is done unsigned, otherwise comparisons are done signed.
Computations are always done in 16-bits. Character variables are extended to 16-bit when accessed (zeroes in upper 8 bit). When a value is assigned to a character variable, the upper 8 bits are discarded.
Assignments are part of the expression and will have the value of the right hand-side of the assignment. This allows performing an assignment within an expression, for example:
if ((a = myfunc()) != 0) {
}
or
int a;
int b;
a = b = 5;
The #asm
directive can be used to directly insert assembly code into a Cb file. All lines that are enclosed between #asm
and #endasm
are output verbatim to the output assembler file.
The #include
directive can be used to include other Cb files into the current file. This way code can be split up over multiple files.
#include "other_file.cb"
When interfacing with assembler code it is necessary to know how the compiler will call the function. Arguments to a function are pushed from right to left onto the stack. Arguments are always pushed as 16-bit value even when passing a 8-bit value.
The return value of a function if returned in the HL
register. The caller is responsible for cleaning the arguments from the stack after the functions returns.
The IX
register should be preserved by the callee. This is used as the frame pointer.
Example putchar
function, calling the Aquarius ROM entry point $1D94:
// putchar(char c)
#asm
_putchar:
push ix ; Preserve IX
ld ix,0 ; IX=SP
add ix,sp
ld a,(ix+4) ; Get variable 'c' from stack
call $1D94 ; Call ROM function
ld sp,ix ; SP=IX
pop ix ; Restore IX
ret ; Return to caller
#endasm