27. Program structure · 28. Packages and modules
A Tycho program is the whole set of definitions reachable from a single entry point, gathered across one or more packages and compiled as one unit. This chapter defines the program's shape — its entry point, its compilation unit, its foreign extern declarations, and how it is linked into an executable (§27) — and then the package system that supplies its definitions: package identity, import, name resolution and visibility, import resolution, the multi-file merge, and per-package foreign dependencies (§28).
The type-level meaning of the declarations a program contains is given in §5; their scoping within a package is §12; the FFI contract an extern declaration participates in — which types may cross the C boundary, and how — is §24. This chapter specifies only *program- and package-level* structure and does not restate those rules.
27. Program structure
27.1 The entry point
A program MUST define exactly one procedure named main, and it MUST take no parameters — the command line arrives through the args() builtin (§29). main has two legal return shapes:
fn main():— returns nothing. The process exits0.fn main() -> Result(void, string):— soor_return(§19) works at the entry point. OnOkthe process exits0; onErr(msg)the implementation MUST writemsgfollowed by a newline to standard error and exit with a non-zero status (the reference exits1).
The error type MUST be string. An Err reaching the entry point is *printed*, and has_str admits no enum, so the message is the error itself. The ok payload MUST be void: a value there would make the exit status ambiguous.
The message is written bare, with no implementation-supplied prefix — the same treatment die(msg) gives a user message (§29), because it is the program's error and not a runtime fault.
A minimal complete program is therefore:
fn main():
println("hello from tycho")
hello from tycho
main is the sole entry point: execution begins by calling it and the program terminates when it returns (the runtime allocates the program's root arena around this call — §10). main is an ordinary procedure in every other respect; it MAY be called recursively and MAY appear in any file of the entry package.
The program's command-line arguments are obtained through the builtin args(), which returns a [string] (§29). The array mirrors the process argv exactly, so args()[0] is the program name (the path by which the executable was invoked) and the operands follow at args()[1] onward. args() is never empty for a normally-launched program.
27.2 The compilation unit
A Tycho program is compiled as one whole-program unit. The compiler reads the entry file, follows every import transitively, merges all reachable definitions into a single AST, and emits a single C translation unit, which is compiled and linked into a single executable. There is no separate compilation, no per-package object file, and no linker step that joins independently-compiled packages (packages.md §"How it builds").
A conforming implementation is not required to transpile to C; the single-unit merge, however, is observable and normative — it determines name resolution and mangling (§28.3), cross-package type identity (§5.1), and the fact that a definition's behavior does not depend on which package it came from.
Whether a source file participates in the package system is decided by the presence of a package declaration. The entry file is compiled in package mode (the whole directory plus its import graph, §28.5) iff it begins with a package declaration; a file with no package declaration is a single-file program compiled alone. A single-file program has the same entry-point rule (§27.1) and is a degenerate one-package unit.
27.3 extern declarations
An extern declaration introduces a procedure that is bodyless and bound to a foreign C symbol rather than to Tycho code:
extern [ "Lib" ] fn name(p: T, …) [ -> T ]
The following rules are normative:
- An
externdeclaration has no body: it ends at the newline after its signature, so a following indented block is a parse error. - Its
nameis the literal C symbol and is not package-mangled (§28.3), because a C symbol is global. Twoexterndeclarations naming the same C symbol therefore refer to the same foreign function regardless of the Tycho packages they appear in. - An
externprocedure is invoked without the implicit arena argument that every Tycho procedure receives (§10); it calls the C symbol directly. - The optional leading string literal names a link library (
extern "Lib" fn …), which the implementation adds to the link line as-lLib(§27.4).
The set of types an extern parameter or return may name — including the fixed-width integer types (u8, u16, i8, i16, i32, i64), inout out-parameters, and the Option(string) nullable return — is defined by the FFI chapter (§24), which is the single normative home for the crossable-type rules. This section governs only the *declaration form* and its role in program structure.
27.4 Linking and the C compiler invocation
For a program that is not stopped early (e.g. --emit-c, --symbols), the reference implementation compiles the emitted C and links it into the output executable with a single C-compiler invocation. The invocation has the shape:
cc {-O3 | -O0 -g} -fwrapv [-march=native | -march=<level>] -pthread -o OUT gen.c \
<auto shims> -lm <-lLib per extern lib> \
<-L/-I/--link/--pkg passthrough> <pkg-config flags from deps>
with these normative properties:
-fwrapvis required semantics, not an optimization flag. It makes signed integer overflow defined as two's-complement wrapping rather than C undefined behavior, which is precisely Tycho's integer-overflow contract (§5.2.1): a conforming realization on C MUST compile such that signed overflow wraps and never traps or miscompiles
.
-lmis always passed, so bare libc math externs (e.g.extern fn sqrt) link with no"m"annotation.-pthreadis always passed, supporting the concurrency runtime (§20).- Optimization / debug:
-O3is the portable default;-gselects-O0 -g(unoptimized with debug info) instead. -march=nativeis opt-in via--native. It is host-CPU-specific and MUST NOT be assumed portable across machines.- A raised ISA baseline is opt-in via
--target <level>, whose levels are the x86-64 microarchitecture levelsx86-64-v2,x86-64-v3andx86-64-v4, plusbaselinefor the default.baselineand the absence of the flag add NOTHING to this line, so an unflagged build is byte-identical to one made before the flag existed; any other level adds-march=<level>and the binary MUST NOT be assumed to run on a machine below it. An unknown level, and a level the C compiler does not accept, are both hard errors . - The default C compiler is
cc;--cc <compiler>overrides it.
Three CLI options let a program splice additional flags onto this line for FFI :
--link <lib>appends a raw-l<lib>.--pkg <name>runspkg-config --cflags --libs <name>and appends the result.--shim <file.c>adds a companion C source that is compiled and linked alongside the generated C.
A build that stops early at --emit-c and links the generated C itself needs the same auto-discovered shim set that §27.4's line would have received. --print-shims prints it: the *transitive* closure of companion <pkg>_shim.c sources (§28.6), one absolute path per line on stdout, then exits without code generation. The closure is not recomputed for the flag — it is the g_shims set that merge_pkg fills as it walks the import graph, so a shim reached only through an indirect import is listed on the same terms as a directly imported one. A program with no package prints nothing and exits 0; an empty closure is an answer, not an error.
--print-deps reads the same walk out the other way: the *transitive* closure of pkg-config package names declared by each package's deps file (§28.6), one per line on stdout, then exits without code generation. It answers a different question from --print-shims, for a caller that does not link the C itself — a harness that lets the reference compiler build, and only needs to know whether the link line *can* resolve on this host, so that a missing library is a skip rather than a failure. The names are recorded as the walk enters each package and before resolution is attempted, so they are printed whether or not pkg-config can resolve them here; the resolved --cflags --libs form that reaches the cc line is empty on exactly the host where the library is absent, which makes it indistinguishable from a package that declared no dependencies at all. Duplicates are collapsed. A program whose closure declares no deps prints nothing and exits 0.
-L<dir> and -I<dir> (attached or separated) also accumulate onto the line . Every library/package name that reaches the shell — from extern "Lib", --link, or --pkg — MUST be restricted to a conservative character set ([A-Za-z0-9._+-]) and rejected otherwise, so compiling an untrusted source cannot inject a shell command. The extern "Lib" libraries, the auto-discovered shims, and the deps flags are contributed automatically by the sources they belong to (§27.3, §28.6); the CLI options above are the manual escape hatch.
The exact command line — flag spelling and order, the choice of cc, and the --native/-g couplings — belongs to the reference implementation and is outside the language proper (§1.1 excludes the CLI and the generated-C layout). The normative content of this section is narrow: -fwrapv two's-complement wrapping is the language's integer contract, and a program that names an external link library (via extern "Lib", a deps entry, or --link/--pkg) depends on that library being present at link time. The precise realization is implementation-defined (Appendix F).
28. Packages and modules
28.1 Packages and namespaces
A package is a *directory* of .ty files that all declare the same package NAME and share one flat namespace. Every top-level definition in any file of the directory — function, struct, enum, handle, newtype, const, and enum variant — belongs to that one namespace; the split into files is not semantically significant (packages.md §"Surface syntax").
A package's symbols are reached from another package by a qualified name pkg.symbol, where pkg is the importing binding (§28.2). The qualified form applies to types, functions, struct/enum constructors, and enum variants alike — geom.Point, geom.add, geom.Red ( handles a payload-less pkg.Variant).
Every non-entry package's package NAME MUST equal the final component of the import path that reached it — which, for a relative import, is its directory name. The entry package may be named anything: its name is taken from the entry file's own package declaration and is not constrained to the directory name.
28.2 Import declarations
An import is a top-level declaration in a package's file:
The plain form binds the package under the final component of its path:
package main
import "core:math"
fn main():
println(str(math.abs(-7)))
7
The aliased form binds it under the alias instead, and the original name is then not in scope:
package main
import m "core:math"
fn main():
println(str(m.abs(-7)))
7
The grammar is "import" IDENT? STRING NEWLINE. The plain form import "PATH" binds the package under the final component of PATH (import "math/geom" binds geom); the aliased form import ALIAS "PATH" binds the package under ALIAS instead .
package and import are contextual: they are special only as the leading identifier of a top-level item and remain ordinary identifiers elsewhere, so they are not reserved words. A bound package name, however, is effectively reserved within the file: because a bare pkg.symbol is disambiguated by treating pkg as an imported package first, a program SHOULD NOT shadow an imported binding with a local of the same name (packages.md §"Surface syntax"). Import cycles are an error (§28.5).
28.3 Name resolution and visibility
Within the single merged unit (§27.2), every definition carries a per-package prefix. The entry package uses the empty prefix, so its symbols keep their plain names; every imported package P uses the prefix P__, applied uniformly to every definition and every reference — including the generated type families for its arrays, tuples, maps, and helper functions (packages.md §"How it builds"). Two identically-named symbols in different packages are therefore distinct after mangling. extern C symbols are the sole exception and are never prefixed (§27.3).
Visibility is by a leading underscore. A top-level symbol whose name begins with _ is private to its own package: it is usable from any file *within* that package, but a qualified pkg._name from another package MUST be rejected with a diagnostic. Every other top-level symbol is exported and visible to importers. There is no visibility keyword; the underscore convention is the whole of the rule.
That includes top-level consts: pkg.NAME reads an exported constant and is folded to its literal at the use site, exactly as an unqualified const is, so no runtime global is emitted and the reference costs nothing. A pkg._NAME is refused by the same underscore rule. A qualified const is accepted wherever an expression is; it is not accepted as a fixed-array length ([pkg.CAP]int), because an array length is resolved while parsing and the imported package may not be parsed yet — an unqualified const still works there. A const initializer is also literal-only: it folds literals and backward unqualified constants, not pkg.NAME; use the qualified constant at the use site or a local literal.
28.4 Import resolution
An import PATH resolves to a package directory as follows:
core:collection. IfPATHbegins withcore:, it resolves to<corelib_root>/<rest>and binds the name<rest>'s final component — e.g.import "core:strings"resolves to<corelib_root>/stringsand bindsstrings;import "core:text/utf8"resolves to<corelib_root>/text/utf8and bindsutf8.- Relative imports. Any other
PATHresolves relative to the importing package's own directory, independent of the current working directory or the entry file's location.
The corelib root is determined in order: the environment variable TYCHO_CORELIB if set and non-empty; otherwise <exe_dir>/corelib (next to the compiler binary, so an in-tree build needs no configuration); otherwise <exe_dir>/../share/tycho/corelib (the installed layout); otherwise resolution fails with a diagnostic . core: is currently the only collection root (packages.md §"The core: collection").
28.5 The multi-file merge
Package mode assembles the program by a post-order depth-first traversal of the import graph, merging every reachable package's definitions into one AST . For each package directory the implementation MUST:
- Enter with cycle and depth checks. If the directory is already on the active DFS path, it is an import cycle and MUST be rejected; if it was already merged (a shared dependency), it is skipped; the active path is capped at 64 and deeper nesting MUST be rejected.
- Scan its
.tyfiles in sorted order. All*.tyfiles in the directory are collected and sorted by name, so the merge order is deterministic and independent of directory-listing order. A directory with no.tyfiles is an error. - Load imports first (post-order). Every import found in the package's file headers is resolved (§28.4) and merged before this package's own definitions, so a definition's dependencies are already registered when it is parsed.
- Require the declared package name. Every file in the directory MUST carry a
packagedeclaration, and it MUST equal the package name expected for this directory (§28.1); a missing declaration or a mismatch MUST be rejected. - Append into the one shared AST, mangling each package's names by its
P__prefix (§28.3) into the single flat namespace.
The resulting program is then finiteness-checked, resolved, and code-generated as a whole (§5, §6).
28.6 Package foreign dependencies (deps and shims)
A package MAY carry C-backed machinery that is built and linked automatically when the package is imported:
- Companion shim. A co-located
<pkg>/<pkg>_shim.cis auto-added to the link line as a compiled companion source, so a C-backed corelib package (e.g.core:regexover<regex.h>) is turnkey with no manual--shim. One shim per package; it is deduplicated. depsfile (pkg-config). The package's sibling<pkg>/depsfile is read: each line names a pkg-config package (blank lines and lines beginning with#are ignored); for each name the implementation runspkg-config --cflags --libs <name>and splices the result onto the C compiler line (§27.4), so a shim that#includes a system header builds against the right flags. Adepsname that pkg-config cannot resolve prints a diagnostic; the missing library then surfaces as a link error at theccstage. Adepsfile is read whether or not the package has a shim:core:sqlitehas none — it binds libsqlite3 throughextern "sqlite3"— and its dependency is reported and linked on the same terms as a shim-backed one.
deps-backed packages are the boundary between the two conformance tiers (§1.3). They belong to the extended tier: a conforming implementation MAY omit them and still conform at the core tier, but a program that imports an absent extended package MUST be diagnosed rather than silently mis-linked. The reference test harness embodies this split — it probes the same deps and skips (rather than fails) a package's tests when the external library is unavailable (corelib/run.sh).