TRSE Syntax

Program structure

Programs must start with the “program” keyword. Variable declarations can happen anywhere after a var is defined. Procedures and functions are defined by their respective keywords. The main block starts with a “begin” and “end”, as does code blocks.

Program MyProgram;
var
    a,b,c : byte = 0;
 
Procedure Myprocedure(mp_i:byte);
var
  somProcedureVariable : byte;
begin
  // Do someting
end;
 
// This is the main block.
begin
   // Call user-defined procedures etc
   MyProcedure(2);
end.

Types

TRSE supports several data types:

  • byte : values 0-255. Supported on all systems.
  • integer: values 0-65535. Supported on all systems.
  • long : 32-bit double word. Supported on all x86 and m68k systems.
  • pointer : Points to an address in memory. Uses zero pages on the 6502, “long” on the x86/m68k and is simply an integer on the (GB)Z80
  • lpointer: 24-bit pointers for the 65c816.
  • boolean: Syntactically identical to a byte.
  • string : An array of characters. Syntactically identical to an array of bytes.
  • incbin : include a binary file
  • incsid : include SID music file (C64 only)
  • incnsf : include NSF music file (NES only)

Type modifiers

TRSE allows types to have modifiers in order to instruct the compiler to either keep certain things in line (such as forcing a parameter to be pure numeric/variable) or modify the data (automatic lz4 compression of data, or placement in special memory blocks etc). For instance, on some systems it is necessary to attribute a specific type in order to tell TRSE where the block should be placed. This is mostly relevant for the Amiga and the Gameboy.

The type modifiers must be added as a list of keywords directly before or after the type itself (“global” must be placed before the type).

Examples:

Program MyProgram;
var
   myArray: array[$100] of byte chipmem; // Places data in chipmem (AMIGA only)
   sprite_data : array[4*3] of byte sprram; // Puts data in sprite ram (GAMEBOY only)  
   image_data : incbin("myImage.bin") compressed; // Automatically LZ77-compresses data and includes    (allowed on all systems. Use the TRSE "decompression" library to decompress real-time.)  
   myString : string = ("HELLO"); // normal null-terminated string
   myInfiniteString : string no_term = ("HI","THERE"); //no null-termination
   myVar : byte at $3030; // Place variable at an absolute value
 
   // "global" implies that the variable 'b' is already defined elsewhere, and will be reused (saving a byte)
   // "pure" means that the value *must* be a pure variable or constant, not an expression
   // Inline procedures famously inserts "b" as macros, hence the need for "pure" to eliminate the
   // possibility of the user inserting an advanced expressions.
   procedure MyProcedure( b : global pure byte ) inline;

Arrays

TRSE supports arrays of base types (which includes addresses of symbols), records and classes.

If the number of elements in the array doesn’t match the declared count, TRSE will pad the missing items with “0”.

If you don’t know the number of elements in the array – but have a list of data – you can declare the count to be missing by using [].

// Preprocessor constants are basically string-replace operation performed before compile
@define somePreprocessorConstant 25
 
var
   // Real constant
   const someConstant : byte = 25;
   myArray1 : array[128] of integeger; // 128*2 bytes of zeros are declared
   myArray2 : array[4*someConstant] of byte = (0,5,$90, someConstant*2);
   myArray3 : array[] of integer = ($1000, $2000, $3000, someConstant*$100); // unspecified array count
   myArray4 : array[255-55] of byte = (1,2,3); // only 3 values defined, rest will be padded with zeros.
   myMonsters : array[10] of Monster; // "Monster" is either a record or class
  // Buildtable fills arrays compile-time with user-defined javascript code (i is a counter here ranging from 0-256) 
   sin : array[256] of byte = BuildTable("Math.sin(i/256.0*2*3.14159)*127 +128");

Arrays are accessed as such:

myArray[i]:=someValue*5;

there is no out-of bounds testing in TRSE.

Note that on the 6502 you cannot have arrays of pointers, as they must reside in the zeropage address space. Instead, use an array of integers, and simply assing a pointer value to the integer array element in order to use it, i.e.

var 
    addresses : array[ 10 ] of integer = (#sym1, $2020, ... )
    zp : pointer;
...
zp := addresses[i];
zp[0] := ...
This restriction does not apply on the x86, z80 or m68k where pointers are simply 16/32 bit numbers.

User-defined types

It is possible to define your own types in TRSE:
  type mat4 = array[16] of integer;
  rotMat : mat4;

Pointers and References

Pointers are the back-bone of any programming language, and are simply variables that contain (“points to”) a memory address. A TRSE pointer p can point to any of the three standard built-in types : byte, integer or long, where the corresponding lookup p[ index ] automatically adjusts for 1,2 or 4 byte data lengths. Pointers can also point to class objects, but not to records (since these are laid out non-chunky in memory by TRSE).

The reference symbol in TRSE is #, analogous to C’s &. # returns the address of a variable/data, and can be assigned to pointers – or 16/32 bit integers/longs. Example:

var 
   myArray[512] of byte = BuildTable(" i*2 "); // builds a table with values 0,2,4,6,8 .....
   someText : string="SOME TEXT";
   p: pointer; // default byte pointer, same as Pascal ^byte or "pointer of byte"
   ip : pointer of integer; // integer pointer, same as Pascal ^integer
   ip2 : ^integer; // same as "pointer of integer"
begin
   p:=#myArray; // p now points to myArray
   p[2] := 10; // same as myArray[2] := 10;
   p:=#myArray + i*8; // p now points to myArray + i*8
   p:=#someText + 3; // points to the 3rd letter in the string
   ip := #myIntegerArray;  // ip will access elements as integers (16-bit, or 2 byte numbers)

Array of arrays/strings

TRSE does not allow for direct declaration of arrays of strings / arrays, but a nice workaround exists: Declaring arrays of symbol addresses. Example:
var
   // declare two strings
   s1 : string ="I AM HUNGRY";
   s2 : string = "WHERE IS THE TUNA";
   // Create an array of the addresses of the strings (integers because we are assuming 6502)
   strings : array[ 2 ] of integer = (#s1,#s2);
   zp : pointer; // a pointer
...
 zp := strings[1]; // read the 1st string address
 printstring(zp,0,40); // will print out "WHERE IS THE TUNA" on the 6502.

Conditional statements

if

An if-else block in TRSE is defined as such:

if (a=1) then DoSomething();
if (a<>3) then  // not equals
begin
   DoSomethingElse();
   IgnoreMe(4);
end;
if (a*2=b*3) then=return() else somethingelse();

Note that an there should be no semicolon “;” before an “else”.
A final note: The “empty” conditional

if (a) then DoSomething() else DoSomethingElse();

is the same as typing “if (a<>false)”

Nested conditionals

Nested conditionals must be enclosed with parentheses in order to remove any ambiguity:

if (a and (b<>c or (d > e))) then DoSomething();
This requirement also applies when all the conditionals are “and”
if (a and (b and (c and (d and (e>2))))) then DoSomething();

while

While conditionals works similar to if statements:

while (a>0) do 
begin
  PrintSomething(a);
  dec(a);
end;

Multiple conditionals:

Multiple conditionals needs to be nested in a hierarchy as such:
if (a>0 and (c<10 or d<>5)) then ... 
if (j<>k*2 or (c<10 or (b=c and d=e))) then ...

Cases

Cases tests a single variable for multiple values as such:

 case i of
   0: DoSomething();
   1: begin 
        HelloThere();
        b:=b+5;
      end;
   2+i : ThisIsWeird();
  else 
     begin
       PerformElseBlock();
     end;

The else block is optional.

Procedures

Procedures are defined as such:

Procedure NoParameters();
begin
   // .. code here
end;
 
// Procedure with single parameter and variable block
procedure DoSomething(a,b:byte; zp: pointer);
var
  localVar : byte;
 
begin
   zp:=zp+a;
   localVar := a+b; 
   // ...
end;

You can return a value from a procedure by using ‘ReturnValue(myValue)’.

Functions

Functions are procedures that can return values. TRSE currently only supports ‘byte’ and ‘integer’ as return values.

function Calc( p : byte ) : byte;
begin
   // return some random calculation
   Calc := p*p + 2; 
end;
begin
   // Call function
   i:=Calc( j+2 );
   // ...
end;

For loops

For 6502 systems, due to the nature of pointers and arrays, only bytes are allowed as counter variables.

for i:=0 to 10 do 
begin
   doSomething(i);
end;
 
for j:=a to b step 2 do  // increases j with 2 for each step
  someArray[j]:=a+b*j;

Preprocessors

TRSE has a lot of preprocessor directives defined. These can be used to give messages to the compiler before the actual parsing of the program is executed.

Constants

Constants are preprocessors that will be string replaced before compilation occurs.

@define myConstant 4
@define myText "WHOO PARTY"
 
...
a:=@myConstant * 5; // same as 4*5

Preprocessor ifdef / if / else / endif

You can define preprocessor constants and use them in preprocessor expressions as such:
@define L 1
@define N 10
 
 
@ifdef L // If @L is defined...
	@if N = 10    // if @N holds the value of 10
		screen_bg_col:=1; 
	@else
		screen_bg_col:=2;
	@endif
@else // if L is not defined
	screen_bg_col:=4;
@endif

Pre-existing preprocessor defines

You can define your own preprocessor for a given project in the project settings->target output->global defines. Simply type “@define IS_DEBUG 1” etc in order to add the preprocessor to your project.

TRSE also provides the user with some handy preprocessor defines for determining the current CPU / system:

...
@ifdef C64
 // the following line will only be compiled if the current project is for the C64
 screen_bg_col := black; 
@endif
@ifdef CPU_Z80
 // the following line will only be compiled if the current project is for a Z80 CPU
   asm(" push bc ");
@endif
The syntax for cpu-specific defines are CPU_MOS6502, CPU_Z80, CPU_PX86, CPU_M68K, CPU_GBZ80.

Records

A record is similar to a “typedef” in C, and lets the programmer bundle a set of variables together to a primitive object. TRSE allows for creation of these objects as arrays. Internally, each element of the record (or array) is stored as individual arrays. This allows for fast lookup on the 6502 and z80 etc, but prevents any pointers to records as the data isn’t bundled together.
program SomeRecords;
var
	monster_type = record
		x,y : byte; // Current position of monster 
		color : byte; // Current colour of monster
                health : byte;
	end;
 
        player : monster_type; // Declare "player" as a single monster type
        const noMonsters: byte = 10;
        // Declare an array of monsters_type
        monsters : array[noMonsters] of monster_type;

Classes

Classes are still being introduced in TRSE (as of 0.12.4), and is not yet fully supported. A class is similar to a Record, but has a couple major distinctions. First, you can define methods (procedures) within the class that operates on its properties. Also, class data is stored sequentially – or chunky – as opposed to records. This makes lookup a bit slower on the 6502, but you can now have pointers to the objects. Third, it is possible to have arrays and strings located inside classes (as opposed to records).

Use the “this” pointer in order to access local class variables within a method.

var
	Monster = class
		x,y : byte; // Current position of monster 
		color : byte; // Current colour of monster
                health : byte;
 
                procedure Draw();
                begin
                    moveto(this.x, this.y,hi(screen_char_loc));
                    screenmemory[0] := 0; // draw a @
                end;
 
	end;
 
        player : Monster; // Declare "player" as a single monster type
        const noMonsters: byte = 10;
        // Declare an array of monsters_type
        monsters : array[noMonsters] of Monster;
        pm : pointer of Monster;
 
  ..
pm := #monsters[3]; // Point to the 3rd monster
pm.x := 10;
pm.Draw();
monsters[3].Draw(); // same as pm.Draw();
Please note: Classes are still in its very early implementation, and does not yet support inheritance/constructors/private/public properties etc. Also, in order to ensure good coding practice (= I haven’t managed to write an optimizer for this yet), setting properties directly of arrays of objects is not allowed:
monsters[i].x := 10; // NOT allowed in TRSE
monsters[i].y := 20; // NOT allowed in TRSE
These calls would have to calculate i*sizeof(Monster) on every step and set a temporary pointer to the memory location. Instead, you’ll have to do this manually – enforcing good coding standards:
var
   pm : pointer of Monster;
...
pm := #monsters[i]; // Points to the i'th element
pm.x := 10;
pm.y := 20;

Units

A Turbo Rascal Unit file (.tru) is a re-usable library (containing rascal source code) that can be included and compiled on build. Units are extremely handy when creating re-usable pieces of code (also across all platforms). Units are not pre-compiled, but are included compile-time.

Units can be placed in 4 different “levels”, depending on the usage:

  1. Project level: The .tru file is located somewhere in your project folder, and can only be accessed from your project. Example: game monster library, inventory library
  2. System level: The .tru file (located in TRSE/units/[SYSTEM]) can be accessed from all projects for the given system (ie C64, Amstrad, X86, Amiga). These libraries should typically contain system-specific functions such as screen setup, text output, line drawing, file handling etc.
  3. CPU level: The .tru file (located in TRSE/units/cpu_specific/[CPU]) can be accessed from all projects for any system that uses the specific CPU (ie 6502, Z80, M68K, X86 etc). These libraries should contain system-independent methods that are useful for all projects of the same CPU. Examples include: compression/decompression, math methods (matrix operations, vectors, operations etc)
  4. Global level: the .tru file (located in TRSE/units/global) is accessible for *all* systems using *all* CPU types. This means that the code is completely platform independent. Examples include: string manipulation libraries

It is possible to mix levels of units, where global units contain “high level code” and system-specific units contain “low-level” code (perhaps with direct asm + system-specific calls). As an example, think of a wireframe 3D renderer:

  • A global unit containing high-level methods such as mesh creation, draw calls, sorting methods, list of of 3d objects etc. Accessible from all systems.
  • CPU-specific units containing mathematical matrix/vector operations. These units are used in the main global 3d egnine. (example: 6502 matrix class, z80 matrix class etc)
  • A system-specific graphics class for line drawing: when the “global” unit calls the respective “draw line” method from the graphics unit, the actual implementation is performed on a system level (since a drawline method would vary greatly between the C64, BBC Micro and Amiga 500).

Declaration:

Unit MyUnit;
var
   var1, var2 : byte;
   const ANumber : integer = 200;
   Monster = record
      x,y,health : byte;
   end;   
 
// keyword "global" reuses var1
procedure Proc1(var1 : global byte);
begin
   // .. do some stuff
end; 
 
end.

Usage:

Program myProgram;
@use "MyUnit"
var
   monsters : array[ MyUnit::ANumber ] of MyUnit::Monster;
...
 
  MyUnit::Proc1( 10 );

Note that you access unit variable/methods by “Unit::var”, where “::” is the default separator. “::” is also synonymous to “_”, so you can also access the members by “Unit_var” instead. Using “_” us mandatory when accessing variables/methods within assembly blocks.