Modules and Use Declarations
This file is part of the Nocter language specification. The specification entry point is README.md.
Modules
Adopted: Nocter modules are derived from file paths. The language does not have a module declaration.
A module identity is derived from the canonical source file path. One .nct file defines one module.
Import paths use / as the path separator:
examples/word_count.nct => examples/word_count
~/.nocter/std/io.nct => std/io
~/.nocter/std/os.nct => std/os
The file path is the source of truth. There is no separate module name inside the file.
use declarations make names from another module available. A use declaration is lexical compile-time syntax, not a runtime statement.
use std/io.{File, stdout}
use std/mem.Allocator
Adopted use forms:
use std/mem.Allocator
use std/io.{File, stdout, stderr}
use std/io.File as StdFile
use std/io
use ./config.AppConfig
use ./config
use ../shared/path.Path
pub use std/string.String
use std/io as console
Top-level use declarations must appear at the start of the source file before non-use declarations. Block-scope use declarations must appear at the start of a { ... } block before executable statements, bindings, or result expressions:
func greet(): void {
use std/io.print
print("hello")
}
The scope of a block-scope use starts after the declaration and ends at the end of that block. Nested blocks may use their own block-scope imports:
func process(debug: bool): void {
if debug {
use std/io.print
print("debug mode")
}
print("done")
// error: print is not visible outside the if block
}
pub use is allowed only at the top level. A block-scope use cannot re-export anything.
Even inside if, match, while, or loop, a block-scope use is not a conditional dependency. The imported module is loaded as part of the compile unit whenever the containing file is compiled.
Meaning:
use pathimports the module namespace under its default name.use path.Nameimports one exported name into the current file.use path.Name as Aliasimports one exported name under an alias.use path.{Name}is accepted as the braced single-name spelling.use path.{NameA, NameB}imports multiple exported names from one file.- Each imported item in a braced
uselist may independently useas Alias. pub use path.Nameorpub use path.{Name}imports and re-exports one public name.use path as aliasimports the module namespace under an alias.- The default namespace name is the final non-relative segment of the module path.
use std/iointroducesio;use ./path/to/dirintroducesdir, even when the path resolves to./path/to/dir/index.nct.
Examples:
use std/io
use std/io as console
use std/io.File as StdFile
var out = io.stdout()
var err = console.stderr()
let file: StdFile = StdFile.open(path)?
Relative and absolute module path prefixes are valid only inside use declarations. Code must not call a module by writing a relative or absolute path-like expression:
./path/to/file.something()
../shared/file.something()
/absolute/path/file.something()
// invalid: import the module namespace first, then call file.something()
This rule does not give special expression meaning to non-relative module paths. Outside use, text such as std/io.print("hello") is parsed as ordinary expression tokens, the same as std / io.print("hello"). If std and io are not ordinary visible values, normal name resolution fails.
use std/prelude is not a source-level import form. User project modules receive the standard prelude synthetically as described in Synthetic Standard Prelude.
use std/prelude
// invalid: the prelude is compiler-managed
Name collisions are compile errors.
use std/io.File
use ./my/fs.File
// error: File is imported twice
Use aliases to resolve collisions.
use std/io.File as StdFile
use ./my/fs.File as MyFile
Block-scope imports follow the same collision rule. They must not shadow an outer visible name, a parameter, a local binding, another import, a top-level declaration, a prelude name, or a built-in type name:
use std/io.print
func debug(): void {
use debug/console.print
// error: print is already visible; write `as debug_print`
}
Not adopted:
import std/io
use std/io.*
pub use std/io.*
import std/io.File
pub use std/io as io
pub use std/io
use std/prelude
use std/prelude.Error
use ./config.nct.Config
include std/prelude
./path/to/file.something()
Wildcard imports, bare public re-exports, dotted module paths, namespace alias re-exports, source-level prelude imports, explicit .nct extensions in import paths, textual include, and relative or absolute path-like module expressions are not part of the initial language.
Re-exports
Adopted: public re-export uses pub use path.Name or pub use path.{...}.
pub use std/string.String
pub use std/io.File as StdFile
pub use path.Name and pub use path.{Name} mean:
- load the module at
path - import the public name
Nameinto the current module - expose that imported name as part of the current module's public API
Rules:
pub use path.Nameandpub use path.{...}are allowed only at top level.pub use path.Nameandpub use path.{...}can re-export only public names from the source module.pub(nocter)names are not public names forpub use path.Nameorpub use path.{...}.- Each item in a
pub uselist may independently useas Alias. - Re-exported names participate in the same name collision checks as other imports and top-level declarations.
pub use path.*is invalid.pub use path as aliasis invalid in v0.pub use path.Nameandpub use path.{...}do not make private names public.pub use path.Nameandpub use path.{...}do not create a namespace alias.- Import cycles involving
pub use path.Nameorpub use path.{...}are still import cycles and are errors in the initial design.
Synthetic Standard Prelude
Adopted: user project modules receive a compiler-managed synthetic standard prelude loaded from std/prelude.nct in the active Nocter home.
The compiler does not rewrite source text and does not model the prelude as a source-level use std/prelude item. Diagnostics, formatting, AST source spans, and editor views should continue to refer to the user's original source.
The purpose is to avoid requiring this boilerplate in every file while keeping prelude behavior defined as an import rule rather than as special compiler treatment for ordinary standard-library names. Built-in forms such as str, &str, [T], &[T], and &+[T] are not provided by the prelude.
Initial rules:
- Every user project module receives a synthetic file-local prelude import from
std/prelude.nct. - The synthetic prelude is applied independently to each user project module.
- The synthetic prelude does not propagate from one file to another; each user project file gets its own synthetic prelude.
- The synthetic prelude is not applied to files inside the active Nocter home.
- The synthetic prelude is not applied to common standard-library files under
std/. - The synthetic prelude is not applied to
std/prelude.nctitself. - The synthetic prelude path is resolved directly under the active Nocter home;
a user project file such as
std/prelude.nctdoes not shadow it. - A source-level
use std/prelude,use std/prelude.Name,use std/prelude.{...}, oruse std/prelude as nameis invalid in v0. - The prelude imports all public exported names from
std/prelude.nctinto the current file. - Source-level
use pathdoes not import every public exported name frompath; it imports only the module namespace. include std/preludeis invalid.- Names introduced by the synthetic prelude participate in the same collision checks as explicit imports.
- If a prelude name collides with a local declaration, top-level declaration, parameter, local binding, explicit import, or built-in name, the program is invalid.
- Diagnostics should identify collisions with the synthetic prelude as prelude collisions, not as hidden compiler built-ins.
- Project-wide prelude configuration is not part of the initial design.
Initial prelude surface direction:
pub use std/error.{Error, ErrorCode}
pub use std/string.String
The prelude must remain small. Int is not part of v0; write i32 or define a project-local alias. Vec is not re-exported by the prelude in v0 because collections remain an explicit domain module surface. Names such as Vec, File, Allocator, Layout, RawBuffer, print, stdout, stderr, args, env, cwd, exit, and abort should be imported explicitly from their domain modules.
Package Layout
Adopted: v0 has no package manifest and no project-root discovery.
The source file passed to build, run, or check is the root file for that command.
project/
app.nct
src/
config.nct
parser.nct
nocter build app.nct -o app
nocter build
nocter run app.nct
nocter run
nocter check app.nct
nocter check
Rules:
- A package manifest such as
nocter.tomlis not part of v0. - The compiler does not search upward for a project root.
- The compiler does not infer a package name from a directory name.
- If a file is named on the command line, that file is the entry file.
- If no file is named,
main.nctis the entry file. - The source root is the canonical parent directory of the entry file.
- Relative imports are resolved from the directory containing the importing file, not from the root file directory.
- Absolute imports are resolved from the filesystem root.
- Non-relative imports from user project files are resolved from the source root first, then the active Nocter home, as specified in Import Path Resolution. Files inside the active Nocter home resolve non-relative imports from that home.
- Package registries, dependency version solving, lockfiles, workspaces, and package-level configuration are not part of v0.
Example:
// app.nct
use std/io.print
use ./src/config.Config
func main(): i32! {
let config = Config.default()
print(config.name)?
return 0
}
// src/config.nct
pub struct Config {
pub name: &str
}
pub func Config.default(): Config {
return Config{
name: "Nocter",
}
}
Compile Unit
nocter build app.nct, nocter run app.nct, and nocter check app.nct treat app.nct as the root file. The CLI contract is specified in Command Line Interface.
The compile unit is the root file plus every .nct file reached by following top-level use, block-scope use, public re-export, and eligible synthetic prelude loads recursively.
Rules:
- Top-level
useandpub usedeclarations are allowed only at the start of a source file. Block-scopeusedeclarations are allowed only at the start of a block. pub useis allowed only at top level.- The synthetic prelude load is compiler-internal and behaves as if its names are introduced before source-level imports for eligible user project modules.
- Top-level executable statements are not allowed.
- A root executable must define top-level
mainin the root file. - Entry lookup does not select imported functions.
- Imported files may define ordinary functions named
main, subject to normal name visibility and duplicate-name rules. - The same canonical file path is loaded at most once, even if reached through different relative paths.
- Import cycles are errors in the initial design.
- The whole compile unit is name-resolved, type-checked, ownership-checked, and lowered as one program.
- Separate compilation, incremental compilation, cached module artifacts, and link-time composition of multiple Nocter compile units are not part of v0.
Source File Identity
Adopted: compiler-internal source file identity is the canonical absolute path.
Rules:
- Every loaded source file has a canonical absolute path.
- Canonicalization resolves
.and..path components. - Canonicalization resolves symlinks when the host filesystem can report the real path.
- The import graph uses canonical absolute paths for duplicate detection.
- The same canonical absolute path is one source file, even if reached through multiple relative import paths.
- A symlink path and its real path refer to the same source file when canonicalization resolves them to the same path.
- Import cycles are detected using canonical absolute paths.
- Filesystem errors during canonicalization are reported as command-line, filesystem, or import diagnostics depending on which path triggered the failure.
- The language does not expose canonical file paths to Nocter source code.
Diagnostic display path rules:
- Diagnostics keep both a display path and a canonical absolute path.
- The display path is intended for humans.
- The canonical absolute path is intended for editor integrations, LSP document mapping, and compiler de-duplication.
- If a file is under the command working directory, the display path is relative to that working directory.
- If a file is under the common Nocter home
std/, the display path starts withstd/. - Otherwise, the display path is the canonical absolute path.
- Display paths use
/as the separator in diagnostics, even on future non-macOS hosts.
Examples:
cwd: /Users/me/project
source file: /Users/me/project/src/parser.nct
display: src/parser.nct
absolute: /Users/me/project/src/parser.nct
Nocter home: /Users/me/.nocter
source file: /Users/me/.nocter/std/io.nct
display: std/io.nct
absolute: /Users/me/.nocter/std/io.nct
Import Path Resolution
Import paths are source paths, not package names. The .nct extension is omitted.
Relative import paths start with ./ or ../.
use ./config.AppConfig
use ../shared/path.Path
Relative paths are resolved from the directory containing the current file:
current file: app/main.nct
import path: ./config
resolved: app/config.nct
Non-relative import paths start with a directory or file name.
use std/io.print
use std/fs.File
use app/config.Config
Absolute import paths start with / and are resolved from the filesystem root.
use /opt/nocter/shared.Config
Non-relative paths from user project files are resolved from the source root first and the active Nocter home second. This means a user project can intentionally shadow standard-library paths such as std/io for its own source imports.
Non-relative paths from files inside the active Nocter home are resolved from the active Nocter home. They do not search the user source root. Distributed standard-library internals must not bind to user project shadow modules.
For an entry file at /work/app/main.nct and active Nocter home /opt/nocter, import path std/io searches:
/work/app/std/io.nct
/work/app/std/io/index.nct
/opt/nocter/std/io.nct
/opt/nocter/std/io/index.nct
Each module path first tries path.nct, then path/index.nct. If both exist in the same import root, the import is ambiguous and must be reported as an error.
use vendor/json.Parser
/work/app/vendor/json.nct
/work/app/vendor/json/index.nct
Rules:
- Local project imports may use
./or../from the importing file, or a non-relative path from the source root. use config.Configdoes not search next to the current file; it searches the source root first and the active Nocter home second.- Inside the active Nocter home,
use config.Configsearches from the active Nocter home and does not search the user source root. /absolute/pathimports are resolved from the filesystem root..is not a module separator in import paths..nctis not written in import declarations.- Directory modules use
index.nct;mod.nctdirectory modules are not part of v0. - The compiler locates Nocter home from
NOCTER_HOMEif set, otherwise from the resolved real path of the runningnocterexecutable and its parent directory. This supports normal installs where aPATHdirectory contains a symlink to~/.nocter/nocter. - The compiler does not automatically search
cwd/.nocteror~/.nocter. - The repository local release image
dist/.nocter/may act as Nocter home during local development. This is a development detail, not the user-facing installation convention.
Name Resolution
Unqualified names are resolved inside one file after imports are loaded and visibility is checked.
Lookup order:
- 1. Current lexical block bindings.
- Outer lexical block bindings.
- Function parameters.
- Same-file top-level declarations.
- Explicitly imported names and names introduced by the synthetic prelude.
- Built-in types and reserved syntax forms.
Initial rules:
- Shadowing is not allowed.
- Function parameter names must be unique within the parameter list.
- A function parameter must not reuse a visible local, parameter, top-level, imported, prelude, or built-in type name.
- A local binding must not reuse a visible local, parameter, top-level, imported, or built-in type name.
- Two imports, a re-export, or a prelude name introducing the same local name are errors.
- A same-file top-level declaration and an imported name must not have the same local name.
use pathintroduces only the default namespace name.use path as aliasintroduces only the explicit namespace alias name.- Names inside an imported namespace are accessed with member syntax, such as
io.stdout(). - There is no wildcard import.
- There is no implicit import of every name from
std. - The synthetic prelude is limited to
std/prelude; it is not a general implicit import facility.
Visibility
Adopted: definitions are private by default. Public API is marked with pub. Nocter-distribution-internal API is marked with pub(nocter).
pub struct File {
fd: i32
}
pub func File.open(path: &str): File! {
...
}
impl File {
method &self.raw_fd(): i32 {
return self.fd
}
}
pub func stdout(): File {
...
}
pub(nocter) primitive from_addr<T>(address: usize): *T
Rules:
- Top-level definitions are private to their module by default.
pubon a top-level definition makes it importable from other modules.pub(nocter)on a top-level definition makes it importable only from modules inside the active Nocter home.- The active Nocter home includes the common
std/tree. pub(nocter)is intended for distributed standard-library internals such as restricted pointer APIs and target primitive boundaries.pub(nocter)may be written only in modules inside the active Nocter home.pub(nocter)is not user-project package visibility.nocteris contextual inside thepub(nocter)modifier. It is not a globally reserved keyword.- Type aliases are top-level definitions.
type Name = Targetis private by default,pub type Name = Targetmakes the alias importable and re-exportable, andpub(nocter) type Name = Targetmakes the alias importable only inside the active Nocter home. - Interfaces are top-level definitions.
interface Name { ... }is private by default,pub interface Name { ... }makes the contract importable and re-exportable, andpub(nocter) interface Name { ... }makes the contract importable only inside the active Nocter home. usecan importpubnames from any module.usecan importpub(nocter)names only when the importing module is inside the active Nocter home.- User project modules cannot import
pub(nocter)names. pub use path.Nameandpub use path.{...}can re-export onlypubnames from the source module as part of the current module's public API.pub use path.Nameandpub use path.{...}cannot re-exportpub(nocter)names as public API.pub use path.Nameandpub use path.{...}re-export the imported name as part of the current module's public API.- Struct fields are private by default.
- Public struct fields must be marked with
pub. pub(nocter)struct fields are visible only to modules inside the active Nocter home.- Functions and methods are private by default.
- Public associated functions declared as
func Type.nameand public methods insideimplblocks must be marked withpub. - Nocter-distribution-internal associated functions and methods may be marked with
pub(nocter). implblocks themselves are not markedpub.- Enum variants follow the visibility of their enum in the initial design.
- Interface members must be explicitly marked
pub. - There is no
privatekeyword in the initial design. - There is no standalone
exportdeclaration in the initial design. pub(package),pub(crate),pub(std),pub(home), andpub(trusted)are not part of v0.
Example:
pub struct Point {
pub x: i32
pub y: i32
}
pub enum Direction {
north
south
east
west
}
Initial rules:
- One
.nctfile defines one module. - The
.nctextension is removed. - File and directory names used for modules must be snake_case identifiers as defined by Lexical Grammar.
moduleis not a keyword.- Directory modules use
index.nct. - Standard library modules live under
std. /work/app/std/io.nctresolves from import pathstd/iobefore/opt/nocter/std/io.nctwhen the entry file is under/work/app.- Target-dependent standard-library declarations are selected by
#target("...")inside stable module files such as~/.nocter/std/os.nct; target names are not required in import paths.
Import roots:
- 1. The current file directory for
./and../paths. - The filesystem root for
/...paths. - The source root, then the active Nocter home, for non-relative paths from user project files.
- The active Nocter home for non-relative paths from files inside that home.
The compiler locates Nocter home in this order:
- 1.
NOCTER_HOME, if set. - The directory containing the running
nocterexecutable. - Otherwise, report a clear configuration error.