Numbers
On this page
Every example so far has quietly trusted its numbers to fit. Here is one that does not. An old legend says the inventor of chess asked a king for a modest reward: one grain of wheat on the first square of the board, two on the second, four on the third, doubling on every square up to the 64th. Counting it up takes a number type big enough, a shift of bits, and a conversion from a count to a weight. By the end of this page you can do all three, with any number type Cone has.
mod chessboard;
import stdio use *;
fn main() {
// 1 has no suffix, so grains is an i32: a 32-bit signed integer
var grains = 1;
each square in 2 .. 33 { // square 1 already has its grain
grains *= 2;
if square >= 30 { // show only the last few squares
print <- "Square ", square, ": ", grains, " grains\n";
}
}
// A u64 is a 64-bit unsigned integer. Each square holds 1 doubled
// once for every square before it, and doubling n times is
// 1u64 << n: a single 1 bit, shifted n places left
var total = 0u64;
each n in 0u64 ..< 64u64 {
total += 1u64 << n;
}
print <- "All 64 squares: ", total, " grains\n";
print <- "Bits that are 1 in the total: ", total.countOnes(), "\n";
// f64.from converts the count to a 64-bit floating-point number.
// Call a grain 65 mg; a tonne is a billion (1e9) milligrams
let tonnes = f64.from(total) * 65d / 1e9d;
print <- "Weight: ", tonnes / 1e9d, " billion tonnes\n";
}
It prints:
Square 30: 536870912 grains Square 31: 1073741824 grains Square 32: -2147483648 grains Square 33: 0 grains All 64 squares: 18446744073709551615 grains Bits that are 1 in the total: 64 Weight: 1199.04 billion tonnes
The world grows something like 800 million tonnes of wheat a year, so the king owed roughly 1,500 years of the whole world's harvest (fewer if his grains were lighter than 65 mg, but not by enough to help him).
Try this: print total + 1u64, one grain more, and see why the total is exactly the largest number a u64 can hold.
Number types and their sizes#
Square 32 is where the i32 gave out. An i32 has 32 bits, one of them for the sign, so it holds whole numbers from -2,147,483,648 to 2,147,483,647, and square 32 needs 2,147,483,648. The arithmetic wraps around silently: one past the largest i32 is the smallest, and doubling that gives 0. An unsigned type wraps the same way below zero: 3u32 - 5u32 is 4,294,967,294. So the type has to be big enough for the largest value it will ever hold.
Every number type says its size, in bits, in its name:
| i8, i16, i32, i64 | Signed whole numbers. An i8 holds -128 to 127; an i64 reaches about 9.2 billion billion either side of 0. |
| u8, u16, u32, u64 | Unsigned whole numbers, from 0. A u8 holds 0 to 255; a u64 0 to 18,446,744,073,709,551,615. |
| f32, f64 | Floating-point numbers, with about 7 and about 16 significant digits. |
| usize, isize | Unsigned and signed whole numbers as wide as a memory address: 64 bits on today's machines. They count sizes and positions in memory. |
A floating-point number keeps a fixed number of significant digits and, separately, how far to move the point, so it trades exactness for range: an f64 reaches past 10308. That suits the weight, which nobody needs to the milligram. print shows a float to six significant digits, which is why the weight came out as 1199.04.
Writing numbers: literals and suffixes#
A whole number with no suffix, such as 42, is an i32, and one with a decimal point or an exponent, such as 2.5 or 1e9, is an f32. A suffix names the type instead: 65u8, 5000000000i64, and the short forms u for u32, f for f32 and d for f64, as in 65d. A whole number with no suffix also takes the type its context wants: in total += 1, where total is a u64, the 1 is a u64 too.
Underscores group digits and are otherwise ignored, as in 1_000_000, and 0x starts a hexadecimal number: 0xFF is 255.
Arithmetic and comparison#
+, -, *, / and % work with the usual precedence. Dividing two whole numbers gives a whole number, rounding toward zero, and % gives the remainder, which takes the sign of the number divided:
let a = 7 / 2; // 3 let b = -7 / 2; // -3 let c = -7 % 2; // -1 let d = 7.0 / 2.0; // 3.5, between floats
The comparisons ==, !=, <, <=, > and >= give a bool, as Conditionals showed. Give an operator two numbers of the same type; when they differ, convert one with from, below.
Working with bits#
The total's surprise is in its bits. Each square's grains are a single 1 bit, one place further left than on the square before, so adding all 64 squares sets every one of the u64's 64 bits. The total is 264 - 1, the largest u64 there is, and countOnes() finds 64 ones. The bitwise operators work on integers bit by bit, and the shifts move all the bits at once:
0xF0u & 0x3Cu // 48, 0x30: the bits set in both 0xE0u | 0xB8u // 248, 0xF8: the bits set in either 0xFFu ^ 0x0Fu // 240, 0xF0: the bits set in one but not both ~0u8 // 255: every bit flipped 0x0Cu << 1u // 24: one place left doubles it -8 >> 1 // -4: on a signed number, >> keeps the sign
The same &, |, ^ and ~ work on bool values too, as one bit each: true ^ false is true. Unlike and and or, they always work out both sides.
The amount to shift by is the same type as the number shifted, which is why the program counts its doublings with u64s, 0u64 ..< 64u64. << fills the bits it vacates with zeros. >> fills with zeros on an unsigned number, and with copies of the sign bit on a signed one, so a negative number stays negative. Every amount has an answer, the same in every build: an amount of the type's width or more shifts every bit out, so 1u64 << 64u64 is 0, and >> on a signed number leaves the sign in every bit, 0 or -1. The amount is read as an unsigned number of its width, so a negative amount is past the width too. In a constant, where the compiler works the value out, such a shift is an error instead: "Error 1231: This constant's shift is by 40, so it has no value: a shift of u32 is by 0 to 31."
Every integer type also has methods that count and move its bits:
total.countOnes() // 64: how many bits are 1 40u32.trailingZeros() // 3: the zeros below the lowest 1, as 40 is 8 * 5 32u32 - 1000u32.leadingZeros() // 10: the bits it takes to write 1000 0x81u8.rotateLeft(1) // 3: the bit shifted out of the top comes in at the bottom 1u32.shlMasked(33) // 2: the shift a processor does, by 33 modulo 32
Each count is a u32, and each is defined for 0, as the width: 0u32.trailingZeros() is 32. A signed integer's bits are counted with its sign's: (-1i16).countOnes() is 16. The methods that move bits take an amount, a u32, used modulo the width: rotateLeft and rotateRight bring the bits shifted out of one end back in at the other, and shlMasked and shrMasked are the one-instruction shifts most processors have, which differ from << and >> only past the width. shrMasked fills as >> does. The same operations are core's intrinsics, mem.countOnes(x) and the rest (Intrinsics).
Changing a variable in place: += and ++#
grains *= 2 and total += 1u64 << n change a var variable using its own value. Every arithmetic and bitwise operator has this form: +=, -=, *=, /=, %=, &=, |=, ^=, <<= and >>=. ++ adds one and -- takes one away. Written before the variable, the expression gives the new value; after it, the old one:
var count = 10; count += 5; // 15 count %= 4; // 3 ++count; // 4 let was = count++; // was is 4, and count is 5
Converting between number types: from#
Other than a whole number widening into a bigger one, a number changes type only when the program asks. To convert, call from on the type you want, with the value: T.from(x) gives x as a T. That is how the program turned its count into something it could weigh, f64.from(total). A conversion does what the machine does:
f64.from(7) // 7.0 i32.from(-2.75) // -2: a float loses its fraction, rounding toward zero u8.from(300) // 44: a narrower integer keeps only the low 8 bits i8.from(200u8) // -56: the same 8 bits, read as signed
Converting first is also how to compute past a type's limits:
let million = 1000000; let wrong = million * million; // -727379968: too big for an i32 let right = i64.from(million) * i64.from(million); // 1000000000000
Every number type has from, and it takes a value of any number type. Conversion: from gives every case, among them bool.from(n) and a number's conversion to and from a char.
Methods on numbers: sqrt, sin and cos#
Floats have sqrt, sin and cos, called like any method; sin and cos take an angle in radians:
(3d * 3d + 4d * 4d).sqrt() // 5 let pi = 3.14159265358979d; (30d * pi / 180d).sin() // 0.5
A number has no abs, min or max yet; an if does the job: if x < 0 { -x; } else { x; }.
You can now pick a number type big enough for the job, write its literals, compute with it down to the bits, and convert between types. Every example has also been printing text; next comes String, text that a program builds and changes as it runs.