Section 2.3

Numbers

Integers and floats, size as a suffix, sign as a prefix — and division, which does not do what you expect.

Archon has two families of numbers, and the border between them is visible to the naked eye: a literal with a point is a float, without one it is an integer. That difference changes the result of a computation, and it is the subject of this sub-section.

@entry(start)
object MyProgram {
    start() {
        var total := 7;
        var parts := 3;

        Console.print(text: $"{total} / {parts} = {total / parts}, remainder {total % parts}");
        Console.print(text: $"in floats: {7.0 / 3.0}");
    }
}

Read the first line of output before running it, then run it. 7 / 3 = 2.

Integer division truncates

This is not rounding, it is truncation: 7 / 3 is 2, not 2.33 and not 3. Dividing two integers yields an integer, because the result of an operation between two integers is an integer — the rule has no exception, and the language would rather hold it than change type behind your back.

What was thrown away is not lost for all that: % gives it back. 7 % 3 is 1, the remainder. Together the two tell the whole of the division: 7 is three times 2, and 1 is left.

To get 2.33, you have to ask for a division of floats — hence the program’s second line, 7.0 / 3.0. The point changes everything: two floats divided yield a float.

int, float, and size as a suffix

An integer is an int, a float a float. The size in bits is an optional suffix, stuck to the name:

var a: int := 5;        // the machine word size
var b: int32 := 5;      // 32 bits, guaranteed, whatever the machine
var c: int64 := 5;

var x: float := 3.14;   // same logic
var y: float64 := 3.14;

The names are written out in full, never abbreviated — int32, never i32. And the suffix is written only when the size matters: a binary structure, a file format, a call into a library written elsewhere. When you simply want “an integer”, int is enough, and the compiler takes the machine word size.

Unlike C’s int, that choice is settled once and for all: it will not change from one platform to another under your feet.

unsigned — an integer that never goes below zero

An integer is signed by default: it can be negative. When a value never will be — a count, an index, a byte — the word unsigned says so, as a prefix, written out in full as well:

var counter: unsigned int := 5;     // never negative, the machine word size
var octet: unsigned int8 := 200;    // from 0 to 255

This is not an int with a higher ceiling, it is another type, whose values start at zero — and the compiler holds it to that. A literal that does not fit is refused before anything runs; var counter: unsigned int := -1; does not pass:

error[T033]: the literal '-1' does not fit in 'unsigned int64', whose values run from 0 to 18446744073709551615

And a computation that would leave the range stops: 0 - 1 on an unsigned has nowhere to go, the program raises an OverflowError — an error of the same kind as integer division by zero, just below. Another rule to keep in mind: signed and unsigned do not mix. Adding an int and an unsigned int is refused, and it is up to you to say which of the two you meant. Finally, unsigned applies to integers only — an unsigned float makes no sense, and the compiler says so.

Dividing by zero: an error among integers, a value among floats

var zero := 0;
Console.print(text: $"{7 / zero}");

The program stops, and says so:

uncaught error — DivideByZeroError : division by zero

This is a choice, and it is consistent with the rest of the language: among integers, a division by zero has no result — no integer could carry it — so the program does not invent one. It returns neither zero nor some arbitrary number: it raises an error, which a program can catch if it expects it. How to catch one is the subject of section 8.

Among floats the answer is the opposite, and the very same reason gives it: they do have something to carry the result in.

7.0 / 0.0      // inf
-7.0 / 0.0     // -inf
0.0 / 0.0      // nan

inf is infinity, nan is “not a number” — two values the float standard, IEEE 754, defines, and that a float holds the way it holds 3.14. The language spells them itself, always the same way, rather than letting the machine choose its own spelling.

So keep the rule the right way round: it is not zero that raises, it is the integer zero.

Your turn

  • Change 7 to 9 and read again: 9 / 3 = 3, remainder 0. A remainder of zero is a division that comes out even.
  • Put a point on only one of the two numbers — 7.0 / 3 — and read what the compiler answers. Mixing an integer and a float does not pass in silence: it asks you to say it was intended.
  • Replace parts with 0 and run. The error names what happened, and the program stops there rather than carrying on with an invented result.
  • Then put a point on both numbers — 7.0 / 0.0 — and run: this time nothing stops, and inf is printed.
  • Declare var counter: unsigned int := 0; then, on the next line, counter := counter - 1;. Run it: there is no −1 among the unsigned, and the program stops and says so. Then put -1 directly in the declaration: this time the refusal comes before anything runs, and it names the permitted range.