Within Bounds
Chapter 1 stored three numbers in u16 variables and never came close to the edges of that type. Real programs do come close. A byte read from the keyboard has to fit in a byte, and a sensor reading may be negative. A buffer has a last element, and an index one past it points at somebody else's storage.
Each of Basie's types for single values has a fixed range, constants give fixed facts a name and two checks keep values inside their limits. One check makes sure a value fits the type it is being converted to. The other makes sure an index falls inside the object it selects. In C or assembly the same mistakes corrupt memory quietly and surface much later somewhere else.
Integer and Boolean types
Basie has six integer types, a floating-point type and a Boolean type. Each integer type has a fixed width and range that doesn't change from one machine to another:
| Type | Size | Values |
|---|---|---|
u8 | 1 byte | 0 through 255 |
i8 | 1 byte | -128 through 127 |
u16 | 2 bytes | 0 through 65,535 |
i16 | 2 bytes | -32,768 through 32,767 |
u32 | 4 bytes | 0 through 4,294,967,295 |
i32 | 4 bytes | -2,147,483,648 through 2,147,483,647 |
boolean | 1 byte | false or true |
The u types are unsigned and the i types are signed. Unsigned types suit bytes, sizes, counts and indexes, which never go below zero. Signed types suit quantities that can cross zero, such as a temperature or a change in position.
Choose the smallest type that comfortably holds the values you need, but don't squeeze. A u8 saves a byte over a u16, and the saving adds up across a large array. A 32-bit type costs extra code for every operation, because the Z80 has no 32-bit arithmetic of its own. For most counts and amounts, u16 is the natural choice.
The floating-point type f32 holds fractional values. Chapter 11 introduces it at the point where a calculation needs it.
A Boolean is not a number in Basie. Some languages treat zero as false and anything else as true. In Basie a condition must be a boolean, and Booleans and integers can't be converted to each other. A count therefore can't be tested as if it were a flag.
Constants for fixed facts
A program often starts from facts rather than storage. A screen has a fixed number of rows. A protocol assigns a particular byte to each command. A feature is switched on or off. Basie gives those facts names with const:
const rows = 4
const columns = $08
const enabled = true
const stepBack = -1A constant is a value, not storage. Its value is fixed at compile time and stays the same for the whole run. Using it costs no more than writing the number itself.
The constants above are untyped. enabled is a Boolean because its initial value is true. The other three are exact integers: the compiler keeps their mathematical value and gives them a type only when they are used. That lets rows serve as a u8 in one place and a u16 in another, because 4 fits both. If you used a constant somewhere its value didn't fit, such as 300 where a u8 was required, the compiler would report that use.
To give a constant one particular type everywhere, write the type:
const limit: u32 = 70000limit is a u32 wherever it appears, just as a u32 variable would be. A floating-point constant must always be written this way, as in const half: f32 = 0.5.
Writing numbers
Decimal numbers need no prefix. A $ introduces a hexadecimal number and a % introduces a binary one. $08, %1000 and 8 are all the same value. A character in single quotes, such as 'A', is also an integer: the value of that byte, which for A is 65. Basie has no separate character type, because a character is just a byte.
All of these forms are exact integers, so the place where they are used determines their type, just as it does for an untyped constant.
One character at a time
A character typed at the console is a byte as well. This short program reads one and writes it back:
sub main() fails
try writeOutputByte('?')
var character: u8 = try readInputByte()
try writeOutputByte(character)
try writeText(console, "\r\n")
endThe program first writes a ? as a prompt. readInputByte then waits for a key and returns the byte it read as a u8, which the program stores in the local variable character. writeOutputByte writes that byte back to the console. Both are services built into the runtime, so this program needs no library include.
readInputByte echoes the key it reads, so the user sees what they typed. Typing A shows the prompt, the echoed A and a second A from the program, so the screen reads ?AA. The final writeText sends a carriage return and a line feed, written in the string as \r\n, to finish the line.
CP/M treats Control-Z as the end of input. readInputByte reports that as a failure rather than returning a byte, and try passes the failure out of main.
Passing character to writeOutputByte copies the byte, just as the assignments in Chapter 1 did with numbers. The service has no access to the variable character itself.
Checked conversion
Some conversions always keep the value. Every u8 value also fits in a u16, so Basie widens a u8 to a u16 automatically wherever one is needed. The same is true for u8 to i16, for either 16-bit type to i32 and for the other conversions that keep every possible value.
Narrowing can lose information. A u16 might hold 300, which is too large for a u8. Basie therefore narrows a value only when you write the conversion, using the target type's name:
var byteValue: u8 = u8(wordValue)u8(wordValue) means "this value, as a u8, provided it fits". The conversion checks the value when the program runs. A value that fits becomes the same number in the smaller type. A value that doesn't fit stops the program with a trap before any wrong value can be produced.
In C or assembly, converting 300 to a byte silently keeps the low eight bits and gives you 44. The program carries on with a wrong number, and you find out much later, if ever.
This program converts 300 to a u8:
var wide: u16 = 300
sub main()
var small = u8(wide)
endRunning it produces a trap report instead of a result:
TRAP narrowing at 029Dnarrowing names the failed check, and the number is its address in the program. The compiler makes the check itself when it can work out the value. It rejects u8(300) with the constant 300, because that conversion can never succeed.
Staying inside an object
A value can fit its type perfectly well and still be wrong for the job. Here's an array of two readings:
var readings: u8[2] = [12, 20]The suffix [2] makes readings an array of two u8 elements, stored one after the other inside the array's storage. readings[0] is the first element and readings[1] is the second. Indexes start at zero, so the element count, 2, is also the first position past the end.
The number 2 fits in a u8, so type checking can't catch readings[2]. The array's length sets which positions exist. Basie checks every index against the length of the array it selects. An index outside the array stops the program with a bounds trap before anything is read or written.
This example converts a measurement to a byte and reads the second element of the array, and Basie checks both operations:
var measurement: u16 = 32
var readings: u8[2] = [12, 20]
sub main()
var small: u8 = u8(measurement)
var index: u8 = 1
assert small = 32
assert readings[index] = 20
endBoth checks pass here, but a single different number makes either one fail:
- Change
measurementto 300 and the conversion traps withnarrowing. 300 is a validu16, butu8can't hold it. - Change
indexto 2 and the array access traps withbounds. 2 is a validu8, butreadingshas no element at that position.
In each case the variable holds a valid value. The trap comes from an operation that needs more than the type guarantees. A bounds failure looks like this:
TRAP bounds at 02A0Without that check, readings[2] would read whatever byte happens to follow the array in memory, and readings[2] = 99 would overwrite it. On a CP/M machine that byte could belong to another variable, the program's code or the stack. Such bugs are some of the hardest to find, because the damage shows up far from the line that caused it. The bounds trap stops the program at that line.
Compile time or run time
Basie checks as much as it can while compiling and leaves the rest to the running program. The compiler evaluates a check whose values are all constants, and rejects the program if it fails. A check that involves a variable happens at run time, when the operation runs.
So readings[2] with the constant 2 is a compile error, reported as "index 2 is out of range for u8[2]". readings[index] with index holding 2 is a run-time trap. The compiler doesn't trace values through variables to predict failures. That keeps the rules for what compiles clear and the same for every compiler.
An array's length doesn't create a special range type for its index. The index is an ordinary u8 or u16, and Basie checks it each time it selects an element of a particular array.
Assertions that check the design
The constants rows and columns can be checked together with an assertion:
assert rows * columns = %100000This one sits at the top level of the program, outside any routine. A top-level assert must use only constants. The compiler checks it and generates no code for it. Here it records a design decision, that the screen holds 32 cells. The program stops compiling at this line if someone later edits rows and leaves columns as it is.
Inside a routine, assert is the run-time check from Chapter 1. Both forms state something that must be true. The top-level form is checked once, at compile time. The routine form is checked each time execution reaches it.
The companion program
This program combines the pieces from this chapter. It uses all three ways of writing a number, a character value, a negative constant, a checked conversion and both kinds of assertion.
const rows = 4
const columns = $08
const enabled = true
const stepBack = -1
assert rows * columns = %100000
var observed: u16 = 0
sub main()
assert enabled
observed = u16(rows * columns) + u16('A') + u16(i8(stepBack) + 1)
assert observed = 97
endThe calculation in main adds three parts:
rows * columnsis 32, converted tou16.'A'is 65.i8(stepBack) + 1converts -1 to ani8and adds 1, giving 0, which converts safely tou16.
The total is 97, and the final assertion confirms it. Now try setting stepBack to -2. The sum inside the last conversion becomes -1, and a negative number can't be converted to u16. Every value in that expression is a constant. The compiler therefore finds the failed conversion and rejects the program with a narrowing diagnostic.
Summary
- Basie has six signed and unsigned integer types of 8, 16 and 32 bits, plus
f32andboolean. - A Boolean is not an integer. Conditions must be Boolean and there is no conversion either way.
- An untyped constant such as
const rows = 4is an exact integer that takes a suitable type at each use. A typed constant such asconst limit: u32 = 70000keeps its type. Floating-point constants must be typed. $introduces hexadecimal,%introduces binary and'A'is the byte value of a character.- Widening that keeps every value happens automatically. Narrowing must be written, as
u8(x), and traps withnarrowingif the value doesn't fit. - Every array index is checked against the array's length, and an index outside it traps with
bounds. - A check on constants is made while compiling. A check that depends on a variable is made when the operation runs.
- A top-level
assertchecks a constant condition at compile time and generates no code.
The precise rules are in the specification chapters on source text, types and declarations.