Chapter 3

Values and Types

This chapter covers the building blocks: variables, the scalar types, and the rules of arithmetic. Most of it will look familiar, but Goose is stricter than C about integer widths and looser than Rust about literals, and it has one rule about % that is different from all three of the languages you know. Those are the parts to read carefully.

#Declaring variables3.1

Goose has three ways to declare a variable:

fn main() {
    let flock_size = 24;        // cannot be reassigned
    var hatched: i32 = 0;       // can be reassigned
    const pens = [4, 8, 12];    // cannot be reassigned or written into
    hatched += 3;
    hatched++;
    print(flock_size, " ", hatched, " ", pens);
}
24 4 [4, 8, 12]

The type comes after the name, separated by a colon, and it can almost always be left out: an un-annotated declaration takes the type of its initializer. hatched++ is a statement, not an expression, so x = y++ is not something you can write. That removes a whole class of puzzles about evaluation order.

The difference between let and const deserves a moment. let stops you reassigning the name, but the contents of a let array or struct are as writable as their type allows. const makes the contents read-only too:

fn main() {
    let shelf = [1, 2, 3];
    shelf[0] = 10;              // fine: the elements of a let are writable
    print(shelf);
}
[10, 2, 3]
fn main() {
    const shelf = [1, 2, 3];
    shelf[0] = 10;
}
error: cannot assign through this path (const, or a read-only instantiation, §9.5)

Reassigning a let is an error that names the rule it breaks:

fn main() {
    let count = 3;
    count = 4;
}
error: cannot assign to let count (§4.4)

The §4.4 refers to the section of the language specification that has the rule. You will see these references in many error messages.

#Assigned exactly once3.1.1

A let can be declared without an initializer, as long as it is assigned exactly once on every path before it is read. The compiler checks this:

fn main() {
    let honks = 3;
    let verdict: i64;
    if honks > 2 { verdict = 1; } else { verdict = 0; }
    print(verdict);
}
1

Reading a variable that might not have been assigned is an error, for let and var alike. Goose never gives you an uninitialized value:

fn main() {
    let x: i64;
    print(x);
}
error: variable x may be used before it is assigned

#Integers3.2

The integer types are i8, i16, i32 and i64, and their unsigned counterparts u8 to u64. There is no int, usize or size_t. By convention i64 is the type for counts, sizes and indices, and it is the type that .len returns and that an unconstrained integer literal becomes.

The narrower types are for data: fields and array elements that should be compact, and loops that need a particular width.

#Literals adapt3.2.1

An integer literal has no type of its own. It takes whatever integer type its context needs, provided its value fits:

fn main() {
    let weight: u8 = 200;       // 200 fits a u8
    let total = 42;             // nothing says otherwise: an i64
    let sum = weight + 50;      // 50 adapts to u8, so this is a u8 add
    print(weight, " ", total, " ", sum);
}
200 42 250

A literal that does not fit is a compile error, not a silent truncation:

fn main() {
    let x: u8 = 256;
}
error: constant 256 does not fit u8

Integer literals can be written in decimal or hexadecimal (0xFF). There are no binary literals; use hexadecimal or a shift. A character literal such as 'A' is just an integer, the value of that byte, so 'A' + 1 is 66.

#Arithmetic happens at the operands' width3.2.2

This is the rule that matters most: an operation is performed at the width of its operands. Adding two u8 values is an 8-bit addition. Multiplying two i32 values is a 32-bit multiplication. Nothing is promoted to a wider type behind your back.

When the two operands have different types, Goose looks for one type that both can be converted to without losing information:

These are the implicit widenings:

i8 u8 i16 u16 i32 u32 i64 u64 signed unsigned f32 f64 each arrow is an implicit conversion; anything else needs as or as! any integer also converts to either float
Implicit conversions between scalar types. Narrowing, changing sign, and converting a float to an integer all need a cast.

An unsigned type widens into a strictly wider signed type, because every u8 value fits in an i16. But a u32 and an i32 have no common type that holds every value of both without going to 64 bits, and Goose will not silently pick one for you:

fn main() {
    let eggs: u32 = 7;
    let change: i32 = -3;
    print(eggs + change);
}
error: operands of + have no common type: u32 and i32 (convert one with `as`)

The fix is to say what you mean, for example eggs as i64 + change, which converts one operand to i64 and lets the other widen to meet it.

Because literals adapt to the other operand, you will occasionally be surprised in the other direction:

fn main() {
    let small: u8 = 200;
    let total = small + 1000;
}
error: constant 1000 does not fit u8

The literal tried to become a u8 to match small. Write small as i64 + 1000, or store small in an i64 first.

#Conversions3.2.3

A conversion that might lose information is never implicit. Narrowing, changing signedness, and converting a float to an integer all need a cast:

fn main() {
    let big: i64 = 1000;
    let small: i32 = big;
}
error: expected a value of type i32, got i64 (narrowing and sign changes require an explicit `as`)

There are two cast operators:

fn main() {
    let weight = 4.75;
    let whole = weight as i64;          // rounds toward zero: 4
    let big = 300;
    let low_byte = big as! u8;          // 300 - 256 = 44, deliberately
    let neg = -1;
    print(whole, " ", low_byte, " ", neg as! u32, " ", -2.7 as i64);
}
4 44 4294967295 -2

The compiler also tells you when a cast is unnecessary, which happens more often than you would think once you are used to C:

fn main() {
    let b: u8 = 7;
    let w: i64 = b as i64;
    print(w);
}
warning: redundant `as i64`: a u8 converts to i64 implicitly here
7

A cast that chooses something is not redundant and gets no warning. For example, let id = 5 as u32; picks a type that nothing else gives, and (a as i64) * b with i32 operands picks the width of the multiply.

#% is Euclidean3.2.4

Here is the rule that differs from C, Odin and Rust alike. In Goose, the result of % is never negative. For any a and any non-zero b, a % b is in the range 0 up to |b| - 1:

fn main() {
    print(-7 % 3, " ", 7 % -3, " ", -7 / 3);
}
2 1 -2

In C, -7 % 3 is -1. The Goose rule is the one you actually want when you use % to wrap an index, because x % n is then a valid index into an array of length n for every x, negative or not. It also means the compiler can drop the bounds check on arr[x % arr.len], as Chapter 23 shows.

Division still truncates toward zero, as in C, so / and % no longer pair up for negative numbers. The std module provides the two missing functions: rem(a, b) is the C-style remainder that pairs with /, and div_euclid(a, b) is the quotient that pairs with %.

import std;

fn main() {
    print(rem(-7, 3), " ", div_euclid(-7, 3));
    // which 10-wide pen is position -13 in, and where in it?
    let pos = -13;
    print("pen ", div_euclid(pos, 10), ", offset ", pos % 10);
}
-1 -3
pen -2, offset 7

On floats, % is C's fmod, so -7.5 % 2.0 is -1.5. The Euclidean rule is for integers only.

#Overflow3.2.5

What happens on overflow depends on signedness:

Here is the 32-bit FNV-1a hash of a string, which depends on unsigned wrap-around:

fn main() {
    var h: u32 = 2166136261;
    for c in "goose" { h = (h ^ c) * 16777619; }
    print(h);
}
3265791416

Division by zero aborts in every build.

#Floats3.3

f32 and f64 are IEEE floats. Arithmetic on f32 stays 32-bit. An f32 widens to f64 implicitly, never the reverse, and every integer type converts to either float type implicitly.

A float literal adapts, like an integer literal, to the float type around it. Where nothing gives it a type it is an f64. The same goes for an expression made only of literals and integers, which is computed at whatever width its destination needs:

fn main() {
    let weight: f32 = 4.5;
    let geese: i32 = 3;
    let total = weight * geese;          // the i32 converts: an f32 multiply
    let third: f32 = 1.0 / 3.0;          // computed in f32
    let third64 = 1.0 / 3.0;             // an f64
    print(total, " ", third, " ", third64);
}
13.5 0.33333334 0.3333333333333333

Floats print in the shortest form that reads back as the same value of their own type, which is why the f32 third has fewer digits. A whole number prints with .0, so you can always tell a float from an integer in output:

fn main() {
    print(2.0, " ", 0.1 + 0.2, " ", 1e20, " ", 1e-7, " ", 1.0 / 0.0);
}
2.0 0.30000000000000004 1e+20 1e-07 inf

#Booleans3.4

bool is true or false, stored in one byte. Comparisons produce one, && and || short-circuit, and ! negates. if and while need a bool, and there is no conversion from integers:

fn main() {
    let eggs = 5;
    if eggs { print("there are eggs"); }
}
error: condition must be bool or an optional, got i64

Write if eggs != 0. The "or an optional" in that message is a hint of things to come: an optional can be tested directly, and Chapter 11 is about that.

#Bitwise operators3.5

&, |, ^, ~, << and >> work as you expect, and keep the type of their operand. >> fills with zeroes on unsigned types and copies the sign bit on signed ones.

The precedence is Rust's, not C's, and there is one difference that will bite you if you are used to C: bitwise operators bind tighter than comparisons. So this does what it looks like it does:

fn main() {
    let flags: u8 = 0x35;
    let mask: u8 = 0x0F;
    print(flags & mask != 0, " ", flags >> 4, " ", ~mask, " ", flags ^ 0xFF);
}
true 3 240 202

In C, flags & mask != 0 parses as flags & (mask != 0), which is a classic bug. In Goose it is (flags & mask) != 0.

#Named constants and type aliases3.6

A let at the top level of a file, with no type written and a constant initializer, is a named constant. A named constant behaves exactly like the literal it stands for: it adapts to any type its value fits, and it can be used as an array size.

let FLOCK_SIZE = 24;
let HALF = FLOCK_SIZE / 2;

fn main() {
    let small: u8 = FLOCK_SIZE;        // adapts, as 24 would
    let pens: i32[HALF] = [0; HALF];   // an array of 12 zeroes
    print(small, " ", pens.len);
}
24 12

Give the declaration a type, let FLOCK_SIZE: i64 = 24;, and it becomes an ordinary i64 value that no longer adapts.

type Name = T; gives a type a second name. An alias is the same type, not a new one, so values convert freely between the two names:

type Grams = u32;

fn main() {
    let egg: Grams = 61;
    let plain: u32 = egg;
    print(egg + plain);
}
122

#Exercises3.7

  1. Without running it, work out what this prints. Then run it and check.

goose check fn main() { let a: u8 = 250; let b: u8 = 10; let c: i64 = 250; print(a + b, " ", c + b, " ", -1 % 5, " ", -1 / 5); }

  1. Write a function-free program that converts a temperature of -40 degrees Celsius, stored in an i32, into Fahrenheit as an f32, using f = c * 9 / 5 + 32. Make sure the division happens in floating point. How many casts do you need?

  2. A clock shows hours 0 to 23. Write the expression for the hour that is k hours before hour h, for any k, and check it for h = 3, k = 50.