Modules, Use Declarations, and Source Visibility
Directory Modules
Nocter has no module declaration. Physical placement determines module ownership. A directory containing index.nct defines one module, and the directory path defines that module's identity.
project/
index.nct package root and root-module source
app.nct root-module source
parser/
index.nct child module `parser`
lexer.nct child-module source
internal/
scanner.nct root-module source in a source folder
Every .nct file belongs to the nearest ancestor directory containing index.nct. app.nct and internal/scanner.nct therefore belong to the root module; parser/lexer.nct belongs to parser. A directory without index.nct is a source folder, not a module or namespace. A source basename never creates a module.
Selecting a module inventories all physical .nct files owned by that module. Inventory descends through source folders and stops before a descendant directory containing index.nct, because that directory starts another module or package. Source inventory and source visibility are independent: an inventoried source is checked even when no other source writes see for it, but its declarations remain private to that source until an authored see grants direct visibility.
index.nct is the module root source and the conventional public contract. The canonical guidance for keeping that contract readable and separating substantial bodies belongs to Source Style and Formatting.
Import paths use / and omit .nct:
/work/project/index.nct => /
/work/project/parser/index.nct => /parser
~/.nocter/std/io/index.nct => std/io
Module Imports
use is lexical compile-time syntax, not a runtime statement. Module imports may introduce a namespace, select public type names, or define public re-exports:
use std/io
use std/io as console
use std/io.{File, Writer}
use std/io.File as StdFile
use ./parser.Parser
use ../shared/path.Path
pub use ./parser.Parser
pub use std/string.String
Meaning:
use pathintroduces the module namespace under the path's final segment.use path as Aliasintroduces the module namespace under an explicit alias.use path.Nameintroduces one exported type-namespace name.use path.Name as Aliasintroduces one exported type-namespace name under an alias.use path.{A, B}introduces several exported type-namespace names; each may useas.pub use pathre-exports a module namespace under its final segment.pub use path.Nameandpub use path.{...}re-export selected public type names.
The default namespace name is the final path segment. use std/io introduces io and use ./path/to/parser introduces parser. An alias is required when that name collides with an existing visible name. use / as root is the explicit spelling for a package-root namespace, because / has no final path segment.
Namespace members use . after import. The same selection shape works in value and type position; resolution keeps those namespaces distinct:
use ./parser
func parse_text(text: &str): parser.Value! {
return parser.parse(text)
}
Selected imports accept only names in the type namespace: built-in or nominal types, type aliases, and interfaces. Functions, constants, statics, and other values remain owned by their module namespace:
use std/io.File
use std/io
func write_message(file: &+File): void! {
file.write_text("ready\n")?
return
}
use std/io.write is an error. This keeps the subject of an external value operation visible at every call site and prevents unrelated modules from flattening values into one local namespace. Selected-name import braces are comma-delimited. One trailing comma is valid on either a single line or multiple lines under Comma-Delimited Lists.
Top-level imports precede non-import declarations. Block-scope module imports precede executable statements in their block:
func greet(debug: bool): void {
if debug {
use std/io
io.print("debug mode")
}
}
A block import is a compile-time dependency even when its block is not executed. It cannot use pub. Its bindings apply uniformly to value expressions, type positions, and constant subexpressions inside body type annotations. Imports cannot shadow or collide with another visible name; aliases resolve collisions.
Unsupported forms include wildcard imports, dotted module paths, explicit .nct suffixes, and namespace alias re-exports:
use std/io.*
use std/io.print
use std.io.File
use ./config.nct.Config
pub use std/io as console
Module paths are valid only in use. An expression cannot call std/io.print() or ./parser.parse() directly. use always resolves a directory module; it never probes for or selects an ordinary .nct source.
Source Visibility
see makes declarations authored in one physical source directly visible from another physical source. It does not load the target, add it to a module, or create a namespace.
// index.nct
see ./search.nct
pub func contains(text: &str, needle: &str): bool {
return find(text, needle)
}
// search.nct
func find(text: &str, needle: &str): bool {
...
}
Here search.nct does not introduce a search namespace. index.nct may use find exactly as if it were a private declaration written in index.nct, but this visibility does not spread to any third source.
A see declaration has the following closed form and behavior:
- it is a private top-level declaration and cannot use
pub, a selection, an alias, or block scope; - its path begins with
./or one or more../components, resolves relative to the authored source, and ends with the complete.nctfilename; - package-absolute paths, dependency aliases, module paths, directories, omitted extensions, and
normalized
./../forms are invalid; - the canonical target must belong to the same physical directory module as the authored source; it cannot cross into a child module, parent module, another package, or nested package;
- it exposes only declarations authored in the exact target source;
- declarations visible to the target through its own
see,use, lexical scopes, or synthetic prelude are not re-exposed; - visibility is directional: if
a.nctseesb.nct,b.nctdoes not see declarations froma.nctunless it writes its own reciprocalsee; - cycles are valid and idempotent because direct visibility is a set relation, not recursive source loading.
For example, direct-only visibility requires a.nct to name every source whose authored declarations it uses:
// a.nct
see ./b.nct
func a(): i32 {
return b() + c() // error: c is not visible
}
// b.nct
see ./c.nct
func b(): i32 {
return c()
}
The fix is see ./c.nct in a.nct. The compiler does not compute a transitive source namespace.
Public Contracts and Private Definitions
index.nct is a readable module contract. A public callable may omit its body when one directly seen source supplies its private definition:
// index.nct
see ./parse.nct
pub func parse(text: &str): Value!
instance Value {
pub method &self.render(): String
}
// parse.nct
see ./index.nct
func parse(text: &str): Value! {
...
}
instance Value {
method &self.render(): String {
...
}
}
The private declaration completes the visible public declaration; it does not define a second callable. The contract source must directly see the definition source, and the definition source must directly see that index.nct; one-way or transitive visibility cannot form a contract/definition pair. The compiler joins the pair by module identity, declaration kind, owner, name, generic parameters and bounds, receiver, parameter names and types, result type, authored from clause, and every kind-specific contract modifier. These parts must have identical canonical source notation. Visibility is written only on the public contract. Missing, mismatched, and duplicate definitions are errors independent of source traversal order.
This rule applies to top-level functions, inherent methods, construction functions, typed literals, coercion entries, and source-defined operators. An interface implementation is a bodyless impl Interface member of an instance in index.nct; it does not participate in contract/body joining. Private sources provide its required behavior through ordinary inherent method bodies. The interface is the sole source of required signatures, so the root does not repeat a private implementation method unless that method is also part of the public inherent API. Interface requirements remain intrinsically bodyless. Interface defaults write default explicitly and may use the same split: a bodyless pub default method in the root interface is completed by one private default method body in a reciprocally visible implementation interface fragment. drop always has a body and does not participate in contract/body joining.
A drop declaration has no separately callable public contract. In a contract-first directory module its mandatory body belongs to a private implementation source and is omitted from index.nct; the type's ownership semantics expose destruction behavior without exporting that body as an API entry.
// index.nct
see ./iterator_defaults.nct
pub interface Source {
pub type Item
pub method &+self.next(): Self.Item?
pub default method self.count(): usize
}
// iterator_defaults.nct
see ./index.nct
interface Source {
default method self.count(): usize {
var source = move self
var total: usize = 0
while true {
source.next() otherwise { return total }
total = total + 1
}
}
}
The private interface fragment may complete only contracted default methods. It cannot declare associated types, requirements, or a new interface surface.
Calls, imports, hover, completion, signature help, definition, and public diagnostics use the contract in index.nct. Body checking and body diagnostics retain the implementation source. Definition navigation selects the contract; implementation navigation selects the body. References and rename treat both declarations and all uses as one semantic callable.
Opaque Nominal Contracts
A public struct may omit its representation in index.nct:
// index.nct
see ./document.nct
pub struct Document
construct Document {
/// Copies a static string view into owned storage.
pub literal ""(text: &str): Self
pub noalloc func empty(): Self
}
instance Document {
/// Exposes the initialized UTF-8 prefix without transferring ownership.
pub noalloc coerce &self as &str
}
One directly seen source completes the representation and callable bodies:
// document.nct
see ./index.nct
struct Document {
storage: Storage
len: usize
}
construct Document {
literal ""(text: &str): Self {
return copy_document(text)
}
noalloc func empty(): Self {
return Document {
storage: empty_page_buffer(1),
len: 0,
}
}
}
instance Document {
noalloc coerce &self as &str {
return view(self)
}
}
pub struct Document is an opaque public nominal contract, not a fieldless struct. struct Document { ... } completes that same nominal identity and owns its private representation. It cannot carry visibility. A bodyless public nominal contract must have exactly one complete private definition; an inline braced declaration already owns its representation and cannot be completed again.
An opaque contract exposes no fields or enum variants. A type that intentionally exposes fields or variants writes its complete braced declaration in index.nct. A private nominal type may be declared and defined in any reached source without a separate contract. copy is an observable ownership contract: a separated copyable struct repeats copy on both its public contract and its private definition, and the definition must satisfy the ordinary structural copy rules.
Only declarations authored in index.nct can define the module's exported namespace. An ordinary source made visible with see cannot add a public name, member, construction entry, coercion, operator, or interface implementation to that namespace. It may define private helpers and may complete declarations already contracted in index.nct. Thus documentation, hover, completion, signature help, and ordinary source review can derive the complete public use surface without reading implementation sources.
Re-exports
A public re-export can expose a child module namespace or selected public type names:
pub use ./parser
pub use ./parser.Parser
pub use std/io.File as StdFile
Rules:
- re-exports are allowed only in a module root source
- a namespace re-export does not flatten the target module
- a re-export boundary must be contained by the target name's boundary and can never widen it
- re-exported names participate in ordinary collision checks
- wildcard and namespace-alias re-exports are invalid
- selected-name re-exports do not also create a namespace alias
- functions, constants, statics, and other values cannot be selected or re-exported without their module namespace
Synthetic Standard Prelude
Every eligible user module receives a compiler-managed prelude from <Nocter-home>/std/prelude/index.nct. The compiler does not rewrite source text or synthesize a visible source-level import.
Rules:
- the prelude is applied to every user directory module
- every reached source in that module receives the same prelude fallback independently
- files inside the active Nocter home do not receive the synthetic prelude
std/preludeitself does not receive the prelude- a project path cannot shadow the compiler-selected prelude
- source-level
use std/preludeand selected prelude imports are invalid - prelude exports are fallback names: an explicit module declaration or import with the same local name takes precedence
- parameters, local bindings, and block imports likewise take precedence over a prelude name
- two authored names in the same scope remain an ordinary collision; fallback priority applies only to synthetic prelude exports
- project-wide prelude configuration is not supported
The exact export set belongs to the compiler-checked std/prelude contract. Changing that source changes the fallback names without changing the import mechanism in this chapter.
Named builtins such as str and primitive numeric types come from the compiler-managed universal declaration fallback, while structural forms such as [T] come from the type grammar. Neither is a prelude export. Any standard declaration absent from the selected prelude contract requires an explicit import from its owning module.
Package Boundary
Packages select root and child directory modules without changing their module semantics. Package root discovery, package directives, dependency declarations, target records, and the implicit standard package are defined by Packages and Package Source.
Compile Units
Package build, run, and check begin with resolved target modules. Explicit file mode remains available for isolated scripts and diagnostics as specified by Command Line Interface.
A compile unit contains every physical source owned by each selected module, plus every module reached recursively through use or the synthetic prelude. see contributes direct visibility between already inventoried sources; it does not expand the compile unit. Physical sources are loaded by canonical path at most once.
Rules:
- see cycles are valid
- module import and re-export cycles are errors
- executable entry lookup selects top-level
mainin the selected directory module, not an imported module - the complete unit is resolved, type-checked, ownership-checked, and lowered as one program
- separate compilation, cached module artifacts, and link-time composition are not supported
Source and Module Identity
Module identity is the exact package identity plus normalized module-directory path. Physical source identity is a canonical absolute path.
Canonical source paths are used for loading, duplicate suppression, dependency invalidation, and editor document mapping. Diagnostics retain a human display path and an optional canonical absolute path. A declaration's definition location remains its physical source even though lookup uses the shared directory-module namespace.
Example diagnostic paths:
cwd: /Users/me/project
source: /Users/me/project/parser/lexer.nct
display: parser/lexer.nct
absolute: /Users/me/project/parser/lexer.nct
Nocter home: /Users/me/.nocter
source: /Users/me/.nocter/std/io/index.nct
display: std/io/index.nct
absolute: /Users/me/.nocter/std/io/index.nct
Path Resolution
A see path begins with ./ or repeated ../, resolves from the authored source's directory, and names exactly one existing .nct file. It does not probe an extensionless alternative or a directory module. Canonical resolution must keep both sources in the same module.
Relative module imports begin with ./ or ../, resolve from the importing source's directory, and select only a directory containing index.nct. A module path omits both index.nct and the .nct extension.
Package-absolute paths begin with / and resolve directory modules from the owning package root:
use /parser.Parser
use /.RootValue
The second form selects a type name directly from the package root module. Bare use / is invalid because a namespace import requires a local name; write use / as root instead.
Non-relative paths begin with a declared dependency alias or std and resolve directory modules only:
use json/value.Value
use std/io
Rules:
- relative module paths cannot leave their package or select an ordinary source file
- a leading
/is package-absolute, never filesystem-absolute use config.Configrequires a dependency alias namedconfig; it does not search project filesstdis a reserved implicit dependency bound to the active toolchain standard-library package- packages must not declare or lock a dependency named
std .nctis required inseeand omitted fromuseindex.nctis the only directory-module root convention- Nocter home comes from
NOCTER_HOMEwhen set, otherwise from the real running compiler path
Name Resolution
Every physical source has its own authored source namespace. Unqualified lookup from that source uses:
- 1. current and enclosing lexical bindings
- function parameters
- declarations authored in the current source
- declarations authored in sources named by a direct
see - explicit module imports authored in the current source and synthetic prelude names
- built-in types and syntax forms
Shadowing authored names is not supported. Parameters, locals, block imports, module declarations, authored imports, built-in type names, and the contextual Self type form must not introduce the same visible name. The synthetic prelude is the sole exception: it is a fallback layer, so any authored source name or valid lexical name with the same spelling takes precedence. Two private declarations with the same spelling may exist in different sources when no one source sees both. If one source sees both through its direct see set, lookup reports an authored-name collision; source traversal order never selects one.
Visibility
Definitions are private by default. A pub(...) scope exposes a name to a selected ancestor module tree or to its package. Bare pub exposes a name to every package.
// store/index.nct
pub struct Entry {
pub id: usize
}
construct Entry {
pub func new(id: usize): Self {
...
}
}
pub func first(): Entry? {
...
}
// internal/index.nct
pub(/) func package_seed(): u64
Rules:
- public declarations may be written only in
index.nct - top-level types, aliases, interfaces, functions, primitives, fields, methods, interface members, construction entries, coercion entries, and re-exports follow this rule
- an implementation definition that completes an
index.nctcontract omits visibility - private declarations are visible only in their authored source and in sources that directly see it
pub(./)exposes the declaring module and all descendant modules- each
../inpub(../),pub(../../), and deeper forms moves the boundary to one ancestor module; the boundary cannot move above the package root pub(/)exposes every module in the declaring package- bare
pubexposes every package - scoped visibility is interpreted from the declaring directory module
- names, dependency aliases, and arbitrary module paths are not valid inside
pub(...) - a re-export may narrow a boundary but cannot widen it
- variants declared inline in
index.nctfollow their enum's visibility; variants supplied by a private representation definition remain private to their authored source and direct seers instancedeclarations and theirimpl Interfacemembers are not themselves marked public- there is no
privatekeyword, friend namespace, or named visibility scope
Visibility grants source access only. The exact implicit std package identity separately grants authority to declare registered primitives and provide compiler-owned runtime roles. Writing pub(/) in an ordinary package never grants that authority.