Chapter 19

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.

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

  1. Move the Goose struct and its format overload from Chapter 9 into a file geese.goose with namespace geese;, and use it from a root file. Which calls need the geese:: prefix, and which do not?

  2. Add a fn main() to your geese.goose that prints a test goose. Run the file directly, then run the root file. What happens to the library's main?

  3. What would go wrong if two libraries in one program each declared a function fn load(path: u8[:]) without a namespace? Try it.