Skip to content

ADR 0013 — M10 T5–T6: modules and packages ​

  • Status: Accepted (project owner, 2026-10-26). The owner took every recommendation of §4: package-level names (option A) with namespace retired, src/ as the default sources, git rev, tag or branch pinned in the lock, and dependencies' C names prefixed with their package's name. The wording is the agent's; the owner may revise it. Not yet implemented: the decisions go into language-spec.md §5, type-system.md §1 (scopes), grammar.md, c-backend.md §2.1 and tooling.md (a new §4) before any code.
  • Scope: M10 T5 (modules) and T6 (packages and building), milestone-10.md. It settles OD-11 (open-decisions.md: "nova.toml with name, version, profile, dependencies (path-only in v0)"). The owner has already chosen git dependencies as well as path ones, with a lock file (2026-10-26).

1. Context ​

  • One compilation is one package. All its files share one item scope; names are simple. namespace X; is parsed and recorded but qualifies nothing, and two items of the same name are N0201 even in different namespaces (type-system.md §1). import is a reserved keyword, but the grammar has no import declaration.
  • Visibility today: public, internal (the default) and private are parsed. Only private on struct fields has an effect, and it means visible within the file (§12, C7c).
  • lib/core is part of every package: its items are in scope unqualified, with no import (§29.2).
  • The compiler is whole-program. Generics are monomorphised (§20), and the ownership analyses, placement and the optimisations (optimisations.md) run over the whole NIR. The C backend writes one main.c per program, with names such as nvF_area and nvT_Point (c-backend.md §2.1), unique only within one package.
  • Programs that would use packages: CosmoOS integration code that today shares bindings by copying files (integrations/cosmoos/c5/kernel.nova into c7 and c8), the benchmark programs, nwc, and code outside this repository (ADR 0008: Nova also serves Linux and other C codebases, not only CosmoOS).
  • The kernel profile needs nothing to run at build time that is not Nova source or C, and builds must be reproducible.

2. Decisions proposed ​

D1. A package is a directory with a manifest ​

nova.toml at the package's root; its sources are every .nova file under the directory src/ (or the directories the manifest's sources lists), in ordinal path order. The manifest is a small subset of TOML: tables, strings, booleans, arrays of strings and inline tables. novac parses it itself, with no new dependency.

toml
[package]
name = "geometry"            # an identifier: what dependents call the package
version = "0.3.1"            # recorded; not used to choose versions (D6)
kind = "library"             # "executable" (has main), "library", or "object" (one .o, cosmoos/profiles.md §4.3)
profile = "hosted"           # or "freestanding"
float = true                 # false is --no-float
c = ["src/shim.c"]           # C sources built with it (type-system.md §28.6)

[dependencies]
shapes = { path = "../shapes" }
lz4 = { git = "https://example.org/lz4-nova.git", rev = "3f2a9c0d…" }    # a full commit id
util = { git = "https://example.org/util.git", tag = "v1.2.0" }          # resolved to a commit in nova.lock

D2. Names: one scope per package, qualified across packages ​

Recommended (option A in §3):

  • Within a package nothing changes: its files share one item scope, and names are simple.
  • A dependency's public items are reached through the dependency's name as the manifest writes it: geometry.Point, geometry.area(p), as types and in expressions.
  • import geometry; brings every public name of geometry into the file's scope; import geometry.Point; brings one. An imported name never hides a name of the package itself or of lib/core. Two imports giving the same name are an error where the name is used, not where it is imported.
  • namespace declarations are retired: N0106 for any namespace after a grace period in which it is accepted and ignored. (Alternatively, kept as documentation with no effect.)
  • lib/core stays unqualified, in every package.

D3. Visibility between packages ​

  • Only public items, and a public struct's public fields and methods, are visible to dependents. internal, the default, stays within the package; private stays within the file (fields: §12).
  • A public item's signature must not name a type its dependents cannot see (a public function returning an internal struct): a new error. Implementations of an interface (impl I for T) are visible wherever both I and T are.

D4. Building stays whole-program ​

novac build (no file arguments, in a package directory) checks and compiles the package with all its dependencies, as one program, as today. Generated C names of items from a package other than the one being built gain the package's name (nvF_geometry_area, nvT_geometry_Point), so two packages may declare the same name. No separate compilation and no cache in this ADR: they would need generic code checked once per package and a stable interface format, and would be decided after measuring build times (the M10 plan's "cache of generated C per package" is deferred until then).

D5. Sources: path and git dependencies ​

  • path is relative to the manifest. git takes rev (a full commit id), or tag or branch, which novac fetch resolves to a commit and records.
  • Git sources are fetched with the git executable into a cache ($NOVA_HOME/git, default ~/.nova/git), one checkout per commit.
  • Nothing runs at fetch or build time but git and the C compiler: a package has no build scripts.

D6. The lock file ​

  • nova.lock, written by novac fetch and committed with the package. Each dependency, transitively: its name, source, commit for git, and a SHA-256 of its manifest and sources.
  • novac build uses the lock and fails if a package's sources no longer match their hash, or if nova.toml names a dependency the lock lacks (run novac fetch). --offline never runs git.
  • No version solving. Two packages that depend on a package of the same name must agree on its source and commit; otherwise it is an error that names both. version is recorded but not compared. Amended by ADR 0017 (2026-10-28): registry dependencies are version requirements solved against an index; path and git dependencies keep this rule.

D7. Tools follow the package ​

check, build, run, doc and fmt without file arguments act on the package of the current directory. The language server checks a document with its package when there is one (tooling.md §2.1). novac new <name> writes a manifest and src/main.nova.

3. Options for names (D2) ​

A. Package-level names (proposed)B. Hierarchical namespacesC. File modules
Inside a packageone scope, as todaynamespace X.Y; qualifies every itemeach file its own module
Across packagesgeometry.Point, import geometry;import geometry.shapes;import geometry/shapes;
Existing codeunchangedevery file needs a namespace or movesevery multi-file package needs imports
Compiler workqualified names through dependencies, imports, visibilitythe above, plus namespaces within a packagethe above, plus per-file scopes

A keeps everything written so far valid, and qualifies only what crosses a package boundary, where names can collide. B and C give finer structure inside a large package; nothing in this repository needs it yet, and A does not prevent adding namespaces later.

4. Questions for the owner ​

  1. Names: A (recommended), B or C (§3)? And under A, retire namespace (recommended) or keep it as a comment?
  2. Sources: src/ by default (recommended), or every .nova file under the package directory?
  3. Git references: rev, tag and branch, each resolved to a commit in the lock (recommended), or rev only?
  4. C names: prefix items of dependencies with the package name (recommended, D4)?
  5. novac new: wanted now, or later?

5. Consequences ​

  • New: a manifest parser, a dependency resolver and fetcher (git), the lock file, qualified names and imports in the type checker, visibility errors, package-prefixed C names, and package-aware commands and language server.
  • New diagnostics (codes assigned when specified): manifest errors, an unknown dependency, an unresolved or ambiguous imported name, a non-public item used from another package, a public signature naming a non-public type, a hash mismatch, a dependency conflict.
  • Tests: packages in tests/packages/ (path dependencies, a diamond, a conflict, visibility, imports); git sources from a local bare repository made by the test, so the suite needs no network.
  • Risks: R-12 (new) for dependencies: a fetched package is code the build trusts. Mitigated by commits pinned in the lock, verified hashes, and no build scripts; not by review, which stays the user's.

Pre-1.0: the language and its tools are still changing.