Simple8

Types and Variables

A program works with numbers: a score, a position, a number of lives. This chapter shows how to store them in variables, choose their type, compute with them, and convert from one type to another.

Types

Each variable has a type, which sets its size in memory and the values it can hold. Simple8 has six integer types:

TypeSizeValuesFor…
ubyte8 bits (1 byte)0 to 255a counter, a color, a screen coordinate
byte8 bits (1 byte)-128 to 127a small speed, positive or negative
uint16 bits (2 bytes)0 to 65,535a score, a memory address
int16 bits (2 bytes)-32,768 to 32,767a position that can go off-screen
ulong32 bits (4 bytes)0 to 4,294,967,295a very large number
long32 bits (4 bytes)about ± 2 billiona very large signed number

There is no char and no bool: a character or a “true / false” value is stored in a ubyte. There is also a string type for character strings; arrays and strings have their own chapter, chapter 6.

Eight, sixteen or thirty-two bits

The processors in retro machines compute eight bits at a time. A ubyte is handled in a single operation; a uint takes two, a ulong four. Here is what the same line, + 10, gives on three variables of different types, in the assembly generated for the C64:

.petit = .petit + 10; ubyte: 4 instructions

LDA Main_petit
CLC
ADC #<10
STA Main_petit

.grand = .grand + 10; uint: 7 instructions

LDA Main_grand
CLC
ADC #<10
STA Main_grand
LDA Main_grand+1
ADC #>10
STA Main_grand+1

.enorme = .enorme + 10; ulong: 13 instructions

LDA Main_enorme
CLC
ADC #<10
STA Main_enorme
LDA Main_enorme+1
ADC #>10
STA Main_enorme+1
LDA Main_enorme+2
ADC #0
STA Main_enorme+2
LDA Main_enorme+3
ADC #0
STA Main_enorme+3

The golden rule: the smallest type that is big enough. A ubyte whenever the value fits between 0 and 255, a uint or an int beyond that, a long only when you really need it.

Signed or unsigned

The types without a u (byte, int, long) are signed: they accept negative numbers, but only go half as high. The u types (unsigned) only go from 0 upward.

Machine data (a byte of graphics, a color, an address) is never negative: it is a ubyte or a uint. The compiler checks this, and rejects a value that does not fit in the type:

byte x = 200;      // error: a byte stops at 127
Value out of range for x (-128..127) -> 200: use ubyte for 0..255

Writing a number

FormExampleValue
Decimal250, -5250, -5
Hexadecimal0xD02053,280
Binary0b10100000160
Character'A', '0'65, 48 (its ASCII code)

The assembler notation $D020 is only accepted inside an _asm block. In Simple8 code, you write 0xD020.

Declaring a variable

A declaration gives the type, then the name, and optionally an initial value. It ends with a semicolon, and declares only one variable at a time:

ubyte vies = 3;      // a type, a name, an initial value
uint score;          // no value: it is 0 at startup
int vx;
int vy;              // int vx, vy; is not accepted

There are no global variables: a variable always belongs to a unit or to a function.

Unit variables

Declared at the top of the unit, before main(), they exist for the whole program. Inside a function, you write them with a dot in front: .score.

unit {
    uint score;                 // unit variable

    main() {
        .score = 100;           // the dot: the unit variable
    }
}

Without the dot, the compiler looks for a local variable with the same name, does not find it, and stops: Variable 'score' not found !

Local variables

Declared inside a function, they belong to it alone and are written without a dot. No other function can read them.

main() {
    ubyte gemmes = 7;           // local to main()
    .score = .score + gemmes;
}

The initial value

In the previous chapter, boot.asm prepared the machine before calling main(). It also takes care of the variables:

Constants

A value that never changes is declared const. It goes at the top of the unit and is also written with a dot:

unit {
    const uint POINTS_PAR_GEMME = 250;
    const uint BONUS = POINTS_PAR_GEMME * 4;    // computed from another one: 1000

    main() {
        .score = .POINTS_PAR_GEMME * 7;
    }
}

By convention, constants are written in uppercase, variables in lowercase.

Fast variables: fast

On 6502 processors (C64, NES, PC Engine), the first 256 bytes of memory, zero page, can be accessed faster than the rest. The fast keyword puts a unit variable there:

fast ubyte image;    // read every frame: in zero page

This area is small and shared: 206 bytes on 6502 machines, 127 on the Game Boy (where fast uses high memory). The libraries take their share, then the optimizer itself puts the local variables most used in loops there.

Use fast sparingly. Every byte declared fast is a byte the optimizer can no longer give to a loop variable. When you are starting out, you can do without it: the compiler already makes the best choice. If the area overflows, compilation stops with Zero page overflow.

Computing

An assignment, =, stores the result of a computation in a variable:

.score = .POINTS_PAR_GEMME * gemmes;

Arithmetic operations

The four operations, plus the remainder of division. With a equal to 17 and b equal to 5:

OperatorOperationExampleResult
+additiona + b22
-subtractiona - b12
*multiplicationa * b85
/integer division: the whole part is kepta / b3
%remainder of integer division (called “modulo”)a % b2

17 divided by 5 gives 3, with 2 left over: 3 × 5 + 2 = 17. The remainder is often used to make a value cycle: .image % 4 goes through 0, 1, 2, 3, then back to 0.

Comparing

A comparison answers true or false:

OperatorReads asExample, with a = 17 and b = 5
==is equal toa == b: false
!=is not equal toa != b: true
<is less thana < b: false
>is greater thana > b: true
<=is less than or equal toa <= 17: true
>=is greater than or equal tob >= 6: false

Be careful not to confuse ==, which compares, with =, which stores a value. Comparisons are used to make decisions and to repeat instructions; they will be covered in detail in the chapter on conditions and loops, together with the logical operators && (and), || (or) and ! (not), which combine them.

Bitwise operations

In memory, a number is a sequence of bits, 0s and 1s. A ubyte has 8 of them. They are numbered from right to left: bit 0 on the right is worth 1, bit 1 is worth 2, bit 2 is worth 4… up to bit 7, which is worth 128. The number is the sum of the bits set to 1:

bit76543210
value1286432168421
1200001100

12 is written 0b00001100: bits 2 and 3 are set to 1, and 4 + 8 = 12.

Bitwise operators work on each bit separately. They are mainly used to control the hardware, whose registers store several settings in a single byte, one per bit. With p = 12 and q = 10:

& and: 1 only if both bits are 1

p       0 0 0 0 1 1 0 0   12
q       0 0 0 0 1 0 1 0   10
p & q   0 0 0 0 1 0 0 0   8

| or: 1 if at least one of the two bits is 1

p       0 0 0 0 1 1 0 0   12
q       0 0 0 0 1 0 1 0   10
p | q   0 0 0 0 1 1 1 0   14

^ exclusive or: 1 if the two bits are different

p       0 0 0 0 1 1 0 0   12
q       0 0 0 0 1 0 1 0   10
p ^ q   0 0 0 0 0 1 1 0   6

~ not: every bit is inverted

p    0 0 0 0 1 1 0 0   12
~p   1 1 1 1 0 0 1 1   243

<< shift left: the bits move to the left

3        0 0 0 0 0 0 1 1   3
3 << 2   0 0 0 0 1 1 0 0   12

>> shift right: the bits move back to the right

12        0 0 0 0 1 1 0 0   12
12 >> 2   0 0 0 0 0 0 1 1   3

Shifting one bit to the left multiplies by 2, to the right divides by 2: x << 3 multiplies by 8. The processor does this much faster than a multiplication.

The most common uses:

To…You writeExample
keep the 4 rightmost bitsx & 0x0F0b10110110 & 0x0F gives 0b00000110 (6)
set bit 7 to 1x | 0x800b00000011 | 0x80 gives 0b10000011 (131)
flip bit 0x ^ 10b10110110 ^ 1 gives 0b10110111 (183)
multiply by 8x << 33 << 3 gives 24

Modifying a variable

Instead of repeating its name, you can modify a variable directly:

.vies--;            // .vies = .vies - 1
.image++;           // .image = .image + 1
.score += 250;      // .score = .score + 250
.couleur <<= 1;     // .couleur = .couleur << 1

Every operator has its compound form: += -= *= /= %= &= |= ^= <<= >>=.

Overflow

A variable cannot go beyond the maximum value of its type. If a computation exceeds it, it wraps around to the other end, like a car's odometer:

ubyte x = 250;
x = x + 10;         // 4, not 260: 255 is the maximum, it starts again from 0

int y = 30000;
y = y + 10000;      // -25,536: the maximum of an int is 32,767

No error reports this at run time: it is up to the developer to choose a type that is large enough. Sometimes it is intentional, for example for a frame counter that cycles.

Conversions

To a larger type

Storing a value in a larger type happens automatically, with no loss. A negative number stays negative:

byte b = -3;
int a = b;          // -3

To a smaller type: the cast

In the other direction, part of the value can be lost. So the compiler asks you to write it explicitly, with the target type in parentheses: this is a cast.

int a = 300;
byte c = a;         // error
byte d = (byte)a;   // accepted: d is 44
Cannot assign int to byte without explicit cast. Use: c = (byte)expression;

The cast keeps the low bits and throws away the others: 300 is written 0x012C on two bytes, only the low byte remains, 0x2C, which is 44.

Computations are done in int

In a computation, byte and ubyte values are first converted to int. The result of a computation on small types is therefore an int, and it can be negative:

ubyte u1 = 1;
ubyte u2 = 2;
int d = u1 - u2;            // -1

That is why you need a cast to store such a computation in a small type, as in the example below: (ubyte)('0' + .vies).

Division is integer division. 7 / 2 is 3 and 7 % 2 is 1. With a negative number, the result is rounded toward zero: -7 / 2 is -3, and -7 % 2 is -1.

An example: the scoreboard

This program computes a score and a number of lives, then displays them. There is no function yet to write a number: we write it digit by digit, using division and the remainder.

// Types and variables: a scoreboard (Commodore 64)
architecture commodore.c64;

addlib vic.base;
addlib vic.screentext;

unit {
    const uint POINTS_PAR_GEMME = 250;  // a constant, in ROM
    ubyte vies = 3;                     // a unit variable, with its initial value
    uint score;                         // a unit variable, 0 at startup

    main() {
        ubyte gemmes = 7;               // a local variable of main()

        // the computations
        .score = .POINTS_PAR_GEMME * gemmes;    // 1750
        .vies--;                                // 2

        // the screen
        vic.setBorderColor(0);
        vic.setBackgroundColor(6);
        vic.initScreen();
        vic.clearScreen();
        vic.setFontColor(1);

        // the score, digit by digit
        vic.setCursor(14, 10);
        vic.print("SCORE ");
        vic.putChar((ubyte)('0' + .score / 10000));
        vic.putChar((ubyte)('0' + (.score / 1000) % 10));
        vic.putChar((ubyte)('0' + (.score / 100) % 10));
        vic.putChar((ubyte)('0' + (.score / 10) % 10));
        vic.putChar((ubyte)('0' + .score % 10));

        // the lives
        vic.setCursor(14, 12);
        vic.print("VIES  ");
        vic.putChar((ubyte)('0' + .vies));
    }
}
Commodore 64 screen: SCORE 01750 and VIES 2
The scoreboard on the Commodore 64 (“VIES” means lives).
The PC Engine version

The computations are identical. Only the library (vdc. instead of vic.) and the screen setup, seen in chapter 2, change.

// Types and variables: a scoreboard (PC Engine)
architecture nec.pce;

addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;

unit {
    const int font = addResource("font.bin");
    const uint POINTS_PAR_GEMME = 250;  // a constant, in ROM
    ubyte vies = 3;                     // a unit variable, with its initial value
    uint score;                         // a unit variable, 0 at startup

    main() {
        ubyte gemmes = 7;               // a local variable of main()
        int source;
        int length;

        // the computations
        .score = .POINTS_PAR_GEMME * gemmes;    // 1750
        .vies--;                                // 2

        // the screen, the font and the colors, as in chapter 2
        vdc.setBATSize(vdc.BAT_32x32);
        vdc.initScreen();
        resource.mapToMPR2(.font);
        source = 0x4000 + resource.getOffset(.font);
        length = resource.getLength(.font);
        vdc.writeToVRAM(source, 0x1000, length);
        vce.setPalette(0, 0, RGB(0, 0, 7));
        vce.setPalette(0, 1, RGB(7, 7, 7));
        vdc.clearScreen();
        vdc.setFontColor(0);

        // the score, digit by digit
        vdc.setCursor(10, 12);
        vdc.print("SCORE ");
        vdc.putChar((ubyte)('0' + .score / 10000));
        vdc.putChar((ubyte)('0' + (.score / 1000) % 10));
        vdc.putChar((ubyte)('0' + (.score / 100) % 10));
        vdc.putChar((ubyte)('0' + (.score / 10) % 10));
        vdc.putChar((ubyte)('0' + .score % 10));

        // the lives
        vdc.setCursor(10, 14);
        vdc.print("VIES  ");
        vdc.putChar((ubyte)('0' + .vies));
    }
}
PC Engine screen: SCORE 01750 and VIES 2
The scoreboard on the PC Engine.

Key points

Your turn

  1. Change to 300 gems: what does the compiler say, and why? Then try 200, then 255.
    Show solutionHide solution

    gemmes (gems) is a ubyte: it goes from 0 to 255. The value 300 does not fit in it, and the compiler rejects the program right at the declaration:

    ubyte gemmes = 300;              // a local variable of main()
    Value out of range for gemmes (0..255) -> 300

    It does not silently truncate the value: it stops, and tells you which type accepts which values. To count 300 gems, you would need a uint; but the score, 250 × 300 = 75,000, would no longer fit in a uint either.

    With 200, the program compiles and displays SCORE 50000. With 255, the largest value of a ubyte, it displays SCORE 63750 (250 × 255). The score is a uint, which goes up to 65,535: it has enough room. With 263 gems or more, the score would exceed 65,535 and overflow; but a ubyte cannot be 263, so here the choice of types protects the computation.

    Commodore 64 screen: SCORE 63750, VIES 2
    With 255 gems: 250 × 255 = 63750.
  2. Add a BONUS constant of 500 points and add it to the score with +=.
    Show solutionHide solution

    A constant is declared at the top of the unit, with const. It is then written with a dot, like .POINTS_PAR_GEMME. The += operator adds a value to a variable without repeating its name.

    const uint POINTS_PAR_GEMME = 250;  // a constant, in ROM
    const uint BONUS = 500;             // the bonus, another constant
    ubyte vies = 3;                     // a unit variable, with its initial value
    uint score;                         // a unit variable, 0 at startup
    // the computations
    .score = .POINTS_PAR_GEMME * gemmes;    // 1750
    .score += .BONUS;                       // 2250

    The score goes from 1750 to 2250, on the C64 as on the PC Engine (the lines are the same in both programs). .score += .BONUS; does exactly the same thing as .score = .score + .BONUS;, only shorter. Since BONUS is a constant, the compiler puts 500 directly in the code.

  3. Declare vies (lives) as a byte and take 5 lives away from it: what is displayed?
    Show solutionHide solution

    A byte accepts negative numbers, from -128 to 127. Change the type of vies, then subtract 5 with -=. After the example's .vies--, it was 2: it is now -3.

    byte vies = 3;                      // a byte: it can become negative
    // the computations
    .score = .POINTS_PAR_GEMME * gemmes;    // 1750
    .vies--;                                // 2
    .vies -= 5;                             // -3
    Commodore 64 screen: SCORE 01750, VIES followed by a dash
    With -3 lives, the screen shows a dash.

    The screen shows VIES -. The computation '0' + .vies gives 48 - 3 = 45, and 45 is the code of the - character. The program has no error, but the digit-by-digit display only handles numbers from 0 to 9. To display a negative number, you would need to test whether it is less than 0, write a minus sign, then write its opposite: this is what the conditions in the next chapter will let you do.

  4. Replace uint score with ubyte score: what error does the compiler give, and how do you fix it?
    Show solutionHide solution

    1750 does not fit in a ubyte, which stops at 255. The computation .POINTS_PAR_GEMME * gemmes gives a uint, and the compiler refuses to store it in a smaller type:

    ubyte score;                        // a ubyte: 0 to 255 only
    Cannot assign uint to ubyte without explicit cast. Use: score = (ubyte)expression;

    The message suggests a cast. It silences the error, but the result is wrong: the cast keeps the low byte of 1750 (0x06D6), which is 0xD6, that is, 214.

    .score = (ubyte)(.POINTS_PAR_GEMME * gemmes);   // 1750 does not fit: 214
    Commodore 64 screen: SCORE 00214
    With the cast, the displayed score is 214 instead of 1750.

    The right fix is to go back to uint score. A cast is justified when you know the value fits in the small type, like (ubyte)('0' + .vies); it must not be used to hide a value that is too large. Here, the compiler's error was pointing to a real problem.