Programs, Namespaces and Packages
Every example so far has been a single file. This chapter covers what happens when a program grows: splitting it across files, keeping names apart with namespaces, and using libraries that live in other directories or other repositories.
#Whole-program compilation19.1
The first thing to understand is that Goose compiles a whole program at once. You name one file on the command line, the root file. The compiler follows its imports, reads everything, and produces one C file for the entire program. There is no separate compilation, no object file per module, and no linking of Goose code at all.
That decision underlies much of what you have seen. Function values can be
compile-time entities because the compiler sees every call. Roots can be
inferred because the compiler sees every caller. return … from can be
checked because the compiler knows every path to it. The cost is that you
cannot ship a precompiled Goose library, and a large program is recompiled
in full. The compiler is fast enough that the second cost rarely matters in
practice.
#Splitting a program into files19.2
import a.b.c; includes the file a/b/c.goose, once, however many files
import it. The path is looked up relative to the root file's directory
first, then among the packages the program declares, and finally in the
standard library. A leading dot, import .helpers;, looks next to the
importing file instead.
An import includes declarations and nothing else. It does not create a scope or rename anything. A file that wants its names kept apart from everyone else's declares a namespace.
#Namespaces19.3
namespace barn; at the top of a file puts all of the file's declarations
into the namespace barn. From other files, they are reached as
barn::name:
namespace barn;
struct Stall { number: i64, occupant: u8[..8] }
fn format(out: u8[>..]&, s: Stall) { format(out, "stall ", s.number, " (", s.occupant, ")"); }
fn is_empty(s: Stall) -> bool { s.occupant.len == 0 }
var stalls_built = 0;
fn build(n: i64) -> Stall {
stalls_built += 1;
Stall { number: n, occupant: "" }
}
import farm.barn;
fn is_empty(x: i64) -> bool { x == 0 }
fn main() {
var s = barn::build(3);
print(s, " ", s.is_empty(), " ", barn::is_empty(s), " ", is_empty(0));
s.occupant = "Ada";
print(s, " ", s.is_empty(), " ", barn::stalls_built, " built");
}
The first block is farm/barn.goose, the second the root file:
$ goose main.goose
stall 3 () true true true stall 3 (Ada) false 1 built
Three things in that output are worth noticing.
barn::Stallprinted usingbarn::format, even thoughmainlives in the global namespace. Theformathook is looked up in the namespace where the type is declared.s.is_empty()foundbarn::is_empty, not the globalis_empty(i64). A method-style call looks in the namespace of its receiver's type first.- The plain call
is_empty(0)found the global function.
So a type's namespace travels with the type. Code that uses a barn::Stall
can call its functions with a dot from anywhere, without importing names
or writing the prefix.
Namespaces do not nest, have no privacy and cannot re-export anything.
They only keep names apart. A declaration can also name its namespace
directly, fn barn::repair(…), and ::name refers to the global
declaration that a namespaced one would otherwise hide.
#main and globals19.4
The program's entry point is fn main() in the global namespace of the
root file. A main in an imported file is ignored, so any file can contain
a small main that runs its own demo or tests when you run it directly, and
that main is skipped when another program imports the file. In a file
with a namespace line, write it as fn ::main(), so that it is declared
in the global namespace where the compiler looks for it. Inside it, the
file's own declarations then need their prefix, as they do anywhere else
outside the namespace.
Globals are initialized before main runs, imported files first, then in
declaration order within each file. Their initializers can call functions.
The whole program runs inside an outermost scope that owns the globals, so
for the lifetime rules a global outlives everything.
#Packages19.5
A package is a library in a directory of its own: a module named after the directory, plus any further modules beside it, and possibly some C. The root file declares each package it uses, saying where it comes from:
namespace mylib;
fn honks(n: i64) -> u8[>..] {
var s: u8[>..] = [];
for i in n { s.append("honk "); }
return s;
}
import mylib from "./mylib";
fn main() { print(mylib::honks(3)); }
honk honk honk
A path source like "./mylib" is relative to the file that declares it,
and is used in place, so you can edit the library and rerun the program. A
git source names a repository and a ref after an @: a tag or commit, such
as "joe/mylib@v2" for a GitHub repository at tag v2, or any git URL. The
compiler fetches it once into its cache directory (~/.goose, or wherever
GOOSE_HOME points) and builds any C it contains.
Only the root file declares packages. When a library file needs another
package, it writes the same import other from "…"; line, but the compiler
reads it as a requirement that the root must meet. If the root has not
declared that package, the error says exactly which line to add. So a
program's dependencies are listed in one place, pinned, and flat: there is
only ever one version of each package in a program.
goose fetch program.goose downloads everything ahead of time, and
--offline makes sure nothing is fetched during a build.
#Blessed packages19.5.1
A few packages are known to the compiler by name: the graphics, audio, UI
and physics libraries built on SDL3 and Box3D. You import them like
standard modules, with import gfx;, and the compiler fetches and builds
them the first time. A compiler built with
-DGOOSE_BUILTIN_LIBS=all has them compiled in, and can run such programs
with nothing else installed.
#Embedding files19.6
embed_file("path") reads a file when the program is compiled, and gives
you its bytes as a const u8[:] in the program's read-only data. The path
is relative to the source file that calls it. It is the easy way to ship a
font, a table or a level with a program. This program embeds its own
source code:
import std;
let SOURCE = embed_file("main.goose");
fn main() {
let lines = split(SOURCE, '\n');
print(lines[0], " ... ", lines.len, " lines");
}
import std; ... 9 lines
#A program with C of its own19.7
C code that belongs to a program goes in a sidecar file, foo.build
beside foo.goose, which lists C sources, include directories, defines
and libraries. The build compiles them with the program. Chapter 22 shows
how Goose calls into that C.
#Exercises19.8
-
Move the
Goosestruct and itsformatoverload from Chapter 9 into a filegeese.goosewithnamespace geese;, and use it from a root file. Which calls need thegeese::prefix, and which do not? -
Add a
fn main()to yourgeese.goosethat prints a test goose. Run the file directly, then run the root file. What happens to the library'smain? -
What would go wrong if two libraries in one program each declared a function
fn load(path: u8[:])without a namespace? Try it.