TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    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:

    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:

    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:

    .

    Three CLI options let a program splice additional flags onto this line for FFI :

    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:

    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:

    1. 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.
    2. Scan its .ty files in sorted order. All *.ty files 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 .ty files is an error.
    3. 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.
    4. Require the declared package name. Every file in the directory MUST carry a package declaration, and it MUST equal the package name expected for this directory (§28.1); a missing declaration or a mismatch MUST be rejected.
    5. 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:

    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).