Hello, Goose
This chapter gets a compiler onto your machine, runs a first program, and looks at the handful of commands you will use every day. By the end of it you will know how Goose programs are compiled, which is a little different from what you are used to.
#Getting the compiler2.1
The Goose compiler is a single executable called goose. The project
publishes prebuilt compilers for Windows, Linux and macOS on its releases
page: unpack one somewhere and add that directory to your PATH.
You can also build it from source. You need CMake, a C++20 compiler and Python 3:
git clone https://github.com/aardappel/goose.git
cd goose
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
That leaves the compiler at build/goose. The standard library is not a
separate download: the compiler finds it in the stdlib/ folder of the
source tree it came from.
To check that everything works, ask for the version:
$ goose version
goose runtime 4d3da949e563
#The first program2.2
Create a file called hello.goose:
fn main() {
print("Hellope from Goose!");
}
and run it:
$ goose hello.goose
Hellope from Goose!
That is the whole ceremony. There is no project file, no package
declaration and no build directory to set up. Let's go through the program
line by line anyway, because even three lines have something to say.
fn main() { … } declares the program's entry point. Functions are
declared with fn, parameters go in the parentheses, and a result type
would follow a ->. Like Odin and Rust, and unlike C, there is no header
and no forward declaration: top-level declarations can appear in any order.
print("Hellope from Goose!"); calls the built-in print, which writes its
arguments followed by a newline. The string literal is a run of bytes in
the program's read-only data. Goose has no string type; you will see in
Chapter 9 what a string literal really is.
The semicolon ends the statement. Goose needs semicolons after expression
statements, but not after a statement that ends with a block, such as an
if or a loop.
#Two ways to run a program2.3
The command you just ran compiled the program and ran it inside the
compiler's own process. Goose compiles every program to C, and the
compiler carries a small C compiler of its own, TinyCC, which turns that C
into machine code in memory and calls main. That makes goose file.goose
(or goose run file.goose, which is the same thing) about as quick as
running a script, and you do not need any other tools installed.
When you want a real executable, use goose build:
$ goose build hello.goose
built hello
$ ./hello
Hellope from Goose!
build writes the same C, but hands it to the optimizing C compiler it
finds on your machine (Clang, GCC or MSVC), and puts the executable next to
the source file. This is what you use for anything you want to ship or
measure, and it is required for programs that use threads, which the
in-process backend cannot run.
You can also stop one step earlier and look at the C:
$ goose -o hello.c hello.goose
The result is a single C file holding the whole program. It is readable, but it is not meant to be edited; think of it as the compiler's assembly output.
#Checking and formatting2.4
Two more commands are worth knowing from the start.
goose check file.goose parses and type-checks a program and everything it
imports, without generating or running anything. Editors use the same
checks through goose lsp, the compiler's language server.
goose format file.goose rewrites a file in the standard layout:
indentation, the spacing between tokens, blank lines and trailing commas.
It keeps your line breaks where they are. goose format --check writes
nothing and only lists the files that are not formatted, which is handy in
continuous integration.
#Printing2.5
You will see print in almost every example, so it is worth knowing
exactly what it does. It takes any number of arguments, of any types,
renders each one as text, and writes them all with a newline at the end. It
inserts nothing between the arguments, so you add your own spaces:
fn main() {
let geese = 12;
let average_weight = 4.25;
print("geese: ", geese, ", average weight: ", average_weight, " kg");
print("more than ten? ", geese > 10);
}
geese: 12, average weight: 4.25 kg more than ten? true
There is no format string, so there is nothing to get wrong about %d
versus %ld. Every value can be printed, not just numbers and strings:
arrays print in brackets, structs print as their type name and fields.
That makes print the quickest debugging tool you have.
struct Egg { weight: f32, day: i32 }
fn main() {
let nest = [Egg { 61.5, 3 }, Egg { 72.0, 4 }];
print(nest);
}
[Egg { 61.5, 3 }, Egg { 72.0, 4 }]
Each call to print is a single write to standard output, so lines printed
by different threads never get mixed together. print has two relatives
that you will meet in Chapter 9: str, which builds the same text as a new
string, and format, which appends it to an existing one.
#Comments2.6
Line comments start with //. Block comments are /* … */, and unlike in
C they nest, so you can comment out a region that already contains a block
comment:
fn main() {
/* print("not today");
/* a comment inside a comment */
print("nor this"); */
print("honk"); // only this line runs
}
honk
#When something is wrong2.7
Misspell a function name and the compiler tells you where:
fn main() {
prnt("Hellope from Goose!");
}
hello.goose:2: error: unknown function: prnt
prnt("Hellope from Goose!");
in fn main() instantiated from hello.goose:1
The last line is the first sign of something you will see a lot. Goose
type-checks generic code only where it is used, with concrete types, so
every error comes with the chain of calls that led to it. For a mistake in
main the chain is one line long. For a mistake deep inside a generic
library function, the chain tells you which of your calls caused it.
Some mistakes can only be caught while the program runs. An index out of range is one of them, and Goose always checks:
fn main() {
let eggs: i64[3] = [61, 72, 58];
var i = 0;
for day in 5 { i += 1; }
print(eggs[i]);
}
goose runtime error: index 5 out of bounds (length 3) (hello.goose:5)
The program stops with a message naming the line. It never reads past the end of the array. As you will see in Chapter 23, the compiler removes most of these checks from the compiled program when it can prove they cannot fail.
#Release and debug2.7.1
By default a program is compiled for release, which has one effect you should know about early: signed integer overflow wraps around. Here an 8-bit counter is pushed past its maximum of 127:
fn main() {
var honks: i8 = 120;
for i in 10 { honks += 1; }
print(honks);
}
-126
Compile with --debug and the same program stops at the overflow instead,
naming the operation and the line:
fn main() {
var honks: i8 = 120;
for i in 10 { honks += 1; }
print(honks);
}
goose runtime error: integer overflow (debug): 127 + 1 at i8 (hello.goose:3)
--debug also checks conversions that lose information. Use it while you
develop and test. Unsigned arithmetic is different: it wraps in every
build, because that is what unsigned arithmetic is for. Chapter 3 has the
details.
#Program arguments2.8
A program's command-line arguments come from the os module in the
standard library. import os; makes its functions available, and args()
returns all the arguments as an array of strings, with the program itself
first. When you run a program in-process, everything after -- is passed
to it:
import os;
fn main() {
let a = args();
print(a.len, " arguments");
for x in a[1..] { print(" ", x); }
}
$ goose args.goose -- one "two three"
3 arguments one two three
a[1..] is a slice of the array that leaves out the first element. Slices
get a whole chapter later on. To end a program early with an exit code,
call exit(code); it is a built-in and needs no import.
#Exercises2.9
-
Write a program that prints the numbers from 1 to 5, each on its own line, followed by a line that says
liftoff. You will needfor i in n, which counts from 0 up ton - 1. -
Run the overflow example above with a
u8counter that starts at 250 instead of ani8that starts at 120, with and without--debug. What happens, and why is it the same in both builds?