Module Roots and Manifests¶
Accepted
Accepted for V1 module manifests, module namespace boundaries, and direct local dependency resolution. Full package-manager fetching, registries, version solving, and provider metadata are deferred to CEP-0003: Package Manager and Manifest Evolution.
A module has one root namespace: the namespace of its manifest-defined main .ct file. That namespace defines the public outside-world API for the module:
const json = module("json")
json.JsonValue
json.parse
json.parser.Token
The module may contain many internal files, but external code sees only declarations exported by the main file namespace and any public namespaces re-exported from it. Re-export is the mechanism for curating module API. V1 does not add friend, internal, or caller-sensitive visibility categories.
Manifest¶
V1 module roots contain a module.toml manifest. The manifest format is TOML.
Required compiler-semantic fields:
Field rules:
nameis the module's own source-facing root name and must be a valid Catalyst identifier.versionis a required opaque string. V1 recommends SemVer or YearVer-style strings, but the compiler does not parse compatibility from it.mainis an explicit.ctfile path inside the module root after normalization and canonicalization.[dependencies]is optional when empty.
Accepted non-semantic metadata fields:
author = ["Bas", "Ada"]
description = "JSON parser and value model."
homepage = "https://example.com/json"
license = "MIT"
repository = "https://github.com/example/json"
keywords = ["json", "parser"]
author may be a string or an array of strings. keywords is an array of strings. The other metadata fields are strings. V1 validates their basic types but does not interpret URLs, SPDX expressions, or author formatting.
Metadata fields do not affect module identity, resolution, caching, or compilation. They can still produce manifest validation diagnostics if malformed.
Unknown fields are hard manifest errors in V1. Future package-manager fields should be added explicitly rather than silently ignored.
Dependencies¶
V1 dependencies are direct, local module roots. Dependency table keys are source-facing aliases accepted by module("alias"), so they must be valid Catalyst identifiers and should follow lower_snake_case as lint policy.
[dependencies]
testing_helpers = "../testing_helpers"
image_v1 = { path = "../image-1" }
image_v2 = { path = "../image-2" }
A string dependency value is shorthand for { path = "..." }. A dependency path identifies a module root containing module.toml, not an arbitrary source subtree. Paths are resolved relative to the depending manifest directory unless build policy explicitly allows an absolute path. The resolved dependency root is canonicalized through the filesystem abstraction.
Dependency aliases are local to the depending module. A module's own source resolves itself by its manifest name, not by aliases chosen by importers. Two dependents may use different aliases for the same dependency without changing the dependency's own internal resolution.
Aliases may differ from the dependency manifest name. This is semantically valid and useful for side-by-side versions:
Alias/name mismatch should lint unless the alias clearly disambiguates versions or local policy.
Module identity includes the dependency module's manifest name, exact version string, provider/root identity, active build context, and resolved direct dependency identities. Two aliases may resolve to the same module identity only when the resolver explicitly maps them to the same instance. Different versions or module instances remain distinct even if their source bytes happen to match.
All listed dependencies are resolved and their manifests validated before source checking. Unused dependencies are lintable, not hard source errors. Manifest dependency cycles are hard resolver diagnostics even if source never calls module(...) for the cyclic aliases.
V1 dependency resolution is direct-dependency-only. If module app depends on a, and a depends on b, app cannot directly resolve module("b") or module://b/... unless app also declares b as a dependency or b is a toolchain-provided root. If a wants to expose b API, it should re-export the relevant names through its own public namespace.
Toolchain Roots¶
std is a toolchain-provided module. It has a normal module.toml, version, and main .ct file, but consumers do not list it in [dependencies]. The toolchain makes std resolvable by module("std").
std and prelude are reserved names. A normal module manifest or dependency alias must not use those names. std is reserved for the toolchain-provided standard library module. prelude is reserved for the compiler-owned prelude namespace and is not a module.
Alternate std replacement is deferred. It belongs to future toolchain/build configuration, not dependency alias shadowing.
std should not depend on project modules. It may depend only on toolchain-provided modules if the toolchain defines any.
Deferred Manifest Work¶
V1 manifests do not support:
- conditional dependencies
- optional dependencies
- feature flags
- version requirements
- registry identities
- npm-style external package identities such as
@vendor/pkgname - lockfiles
- publishing metadata
- remote fetching
- target-specific dependency selection
- alternate
stdreplacement through ordinary dependency aliases
These are V2 package-manager or future toolchain/build-configuration work. See CEP-0003: Package Manager and Manifest Evolution.
Re-Export¶
Canonical re-exporting uses ordinary public declarations:
const parser_impl = include("parser_impl.ct")
pub const Parser = parser_impl.Parser
pub const parse = parser_impl.parse
Multiple names can be selected with ordinary destructuring before re-exporting:
const { ParserImpl = Parser, parse_impl = parse } = include("parser_impl.ct")
pub const Parser = ParserImpl
pub const parse = parse_impl
Re-exporting does not need a separate export-list syntax in V1. A module API is the set of public declarations in its main namespace, including public namespace aliases and public impls declared there.
A public declaration may re-export a namespace:
pub const parser = include("parser.ct")
Because namespace values expose only public declarations, this does not leak private internals. It gives module APIs a way to expose curated subnamespaces:
json.parser.Parser
json.parser.parse
This re-exports the namespace value as a namespace value. It is not a snapshot of the target namespace's public declarations. If the target namespace's public API changes, the re-exported namespace reflects that change. API stability is managed by versioning and review, not snapshot semantics.
Re-exporting a dependency namespace is allowed:
const base_json = module("json")
pub const json = base_json
The re-export preserves the dependency namespace's original declaration, type, and impl identities. It does not make the dependency's resolver alias available to downstream modules. Downstream code can use pkg.json, but it must still declare json directly if it wants module("json") or module://json/....
Public namespace aliases preserve namespace identity. Importing or destructuring a public alias behaves as if operating on the original namespace identity.
Public API Validation¶
Top-level declarations are private by default. pub exports a declaration to the namespace public surface. priv is accepted as an explicit private marker equivalent to omitting pub.
A pub const alias can intentionally promote a private declaration or a public member of a private namespace binding under a public name:
const parser_impl = include("parser_impl.ct")
pub const Parser = parser_impl.Parser
Public signatures must be renderable through public names. If a public function signature uses a private spelling for a type that has a public alias, V1 should diagnose the private spelling and require the source to use the public alias explicitly.
Public namespace re-exports require the re-exported namespace's public surface to be valid as public API. This validates the public declarations reachable through the alias; it does not expose private internals.
Imports are not re-exported. Imported named declarations and imported impls are local checking facts only. To expose a name, declare a public alias. To expose a conformance as part of a module API, declare a pub impl in the exporting namespace.
Loose File Mode¶
V1 supports loose file compilation and running as a required tool mode. Loose file mode synthesizes a module context instead of changing source semantics:
name = "main"version = "0.0.0"mainis the requested file- the root boundary defaults to the file's containing directory
- dependencies are empty unless supplied explicitly by compiler/build command arguments
- toolchain roots such as
stdremain available
Loose file mode does not automatically discover nearby module.toml files. If the user wants manifest-backed behavior, they should invoke module/project mode. Command-line dependency aliases in loose mode follow the same validation and identity rules as manifest dependencies and must not shadow main, std, or prelude.
include("module://main/path.ct") is valid in loose mode and resolves inside the synthesized module root. It is redundant with root-relative includes but keeps the resolver model uniform.