Proposal: Module System Details
Status: Approved Author: Eric (with AI assistance) Created: 2026-01-29 Approved: 2026-01-30 Affects: Compiler, module resolution
Summary
This proposal specifies module system details including entry point files (lib.ori vs mod.ori vs main.ori), circular dependency detection and error reporting, re-export chains, nested module visibility rules, and package structure for library vs binary projects.
Problem Statement
The spec describes imports but doesn’t specify:
- Entry point files: What distinguishes
lib.ori,mod.ori, andmain.ori? - Circular dependencies: How are they detected and reported?
- Re-export chains: What happens with
pub useacross multiple levels? - Nested modules: How does visibility work in nested directories?
- Package structure: Library vs binary visibility distinctions
Module Structure
File-Based Modules
Each .ori file is a module:
src/
├── main.ori # Module: main
├── utils.ori # Module: utils
└── math.ori # Module: math
Import:
use "./utils" { helper }
use "./math" { sqrt }
Directory-Based Modules
A directory with mod.ori is also a module:
src/
├── main.ori
└── http/
├── mod.ori # Module entry point
├── client.ori # Submodule
└── server.ori # Submodule
The mod.ori file defines what the directory module exports.
mod.ori Purpose
mod.ori serves as the public interface for a directory module:
// http/mod.ori
pub use "./client" { Client, get, post }
pub use "./server" { Server, listen }
// Private implementation detail
use "./internal" { ... }
Import the directory module:
use "./http" { Client, Server } // Imports from mod.ori
Implicit mod.ori
If a directory has no mod.ori, it cannot be imported as a module:
// ERROR: cannot import directory without mod.ori
use "./http" { ... } // Error if http/mod.ori doesn't exist
Individual files can still be imported:
use "./http/client" { Client } // OK: imports client.ori directly
Entry Point Files
| File | Purpose |
|---|---|
main.ori | Binary entry point (must contain @main) |
lib.ori | Library entry point (defines public API) |
mod.ori | Directory module entry point (within a package) |
lib.ori and mod.ori serve similar purposes at different levels:
lib.oriis the package-level public interfacemod.oriis a directory-level public interface within a package
A package root cannot use mod.ori as its library entry point; lib.ori is required.
Circular Dependency Detection
The specification prohibits circular dependencies (see Modules § Resolution). This section details how the compiler detects and reports them.
Detection Algorithm
The compiler builds a dependency graph during import resolution:
- Start with the entry module (e.g.,
main.ori) - For each
usestatement, add an edge from current module to target - Detect cycles using depth-first traversal
- Report all cycles found (not just the first)
Error Reporting
error[E1100]: circular dependency detected
--> src/a.ori:1:1
|
= note: cycle: a.ori -> b.ori -> c.ori -> a.ori
|
::: src/a.ori:1:1
|
1 | use "./b" { ... }
| ----------------- a.ori imports b.ori
|
::: src/b.ori:1:1
|
1 | use "./c" { ... }
| ----------------- b.ori imports c.ori
|
::: src/c.ori:1:1
|
1 | use "./a" { ... }
| ----------------- c.ori imports a.ori, completing the cycle
|
= help: consider extracting shared code to a separate module
Breaking Cycles
Common strategies:
- Extract shared types/functions to a third module
- Use dependency injection (pass functions as parameters)
- Reorganize to have one-way dependencies
Before (circular):
A -> B
B -> A
After (extracted):
A -> Common
B -> Common
Re-export Chains
Single-Level Re-export
// internal.ori
pub @helper () -> int = 42
// api.ori
pub use "./internal" { helper }
// main.ori
use "./api" { helper } // Gets internal.helper via api
Multi-Level Re-export
Re-exports can chain through multiple levels:
// level3.ori
pub @deep () -> str = "deep"
// level2.ori
pub use "./level3" { deep }
// level1.ori
pub use "./level2" { deep }
// main.ori
use "./level1" { deep } // Works through the chain
Visibility Through Chain
An item must be pub at every level of the chain:
// internal.ori
@private_fn () -> int = 42 // Not pub
// api.ori
pub use "./internal" { private_fn } // ERROR: private_fn is not pub
// Correct:
// internal.ori
pub @helper () -> int = 42 // Must be pub
// api.ori
pub use "./internal" { helper } // OK
Re-export Aliasing
Aliases work through chains:
// math.ori
pub @square (x: int) -> int = x * x
// utils.ori
pub use "./math" { square as sq }
// main.ori
use "./utils" { sq } // Gets math.square as sq
Diamond Re-exports
When the same item is accessible through multiple paths:
// base.ori
pub type Value = int
// path_a.ori
pub use "./base" { Value }
// path_b.ori
pub use "./base" { Value }
// main.ori
use "./path_a" { Value }
use "./path_b" { Value } // Same type, no conflict
The same underlying item imported multiple times is NOT an error — it’s the same type/function.
Nested Module Visibility
Parent Cannot Access Child Private
src/
├── parent.ori
└── child/
└── mod.ori
// child/mod.ori
@private_fn () -> int = 42 // Private to child
pub @public_fn () -> int = 84
// parent.ori
use "./child" { public_fn } // OK
use "./child" { private_fn } // ERROR: not visible
Child Cannot Access Parent Private
// parent.ori
@parent_private () -> int = 42
// child/mod.ori
use "../parent" { parent_private } // ERROR: not visible
Sibling Visibility
Siblings cannot access each other’s private items:
src/
├── a.ori
└── b.ori
// a.ori
@a_private () -> int = 1
// b.ori
use "./a" { a_private } // ERROR: a_private is private
Private Access via ::
The :: prefix allows importing private items for testing:
// In test file or same module
use "./internal" { ::private_helper } // Explicit private access
This is intentional — tests need access to internals.
Package Structure
Library Package
A library package exports its public API:
my_lib/
├── ori.toml # Package manifest
├── src/
│ ├── lib.ori # Library entry point
│ └── internal.ori # Internal implementation
// lib.ori
pub use "./internal" { PublicType, public_fn }
// Only pub items are visible to consumers
Binary Package
A binary package has an entry point:
my_app/
├── ori.toml
├── src/
│ ├── main.ori # Binary entry point (@main)
│ └── utils.ori
Library + Binary
A package can be both:
my_pkg/
├── ori.toml
├── src/
│ ├── lib.ori # Library API
│ ├── main.ori # Binary using the library
│ └── internal.ori
Binary Access to Library
When a package contains both lib.ori and main.ori, the binary imports from the library using the package name. The binary can only access public (pub) items — private items are not accessible even within the same package:
// lib.ori
pub @exported () -> int = 42
@internal () -> int = 1 // Private
// main.ori
use "my_pkg" { exported } // OK: public
use "my_pkg" { ::internal } // ERROR: private access not allowed
This enforces clean API boundaries and ensures the binary “dogfoods” the public interface.
Import Path Resolution
When processing a use statement, the compiler determines the target module:
Path Types
- Relative path (
"./...","../..."): Resolve relative to current file’s directory - Package path (
"pkg_name"): Look up inori.tomldependencies - Standard library (
std.xxx): Built-in stdlib modules
This is distinct from name resolution within a module, which follows: local bindings → function parameters → module-level items → imports → prelude.
Path Resolution
// In src/utils/helpers.ori:
use "./sibling" // src/utils/sibling.ori
use "../parent" // src/parent.ori
use "../../other" // other.ori (outside src)
Package Dependencies
Defined in ori.toml:
[project]
name = "my_project"
version = "0.1.0"
[dependencies]
some_lib = "1.0.0"
use "some_lib" { Thing } // From dependency
Error Messages
Missing Module
error[E1101]: cannot find module
--> src/main.ori:1:1
|
1 | use "./nonexistent" { helper }
| ^^^^^^^^^^^^^^^ module not found
|
= note: looked for: src/nonexistent.ori, src/nonexistent/mod.ori
Missing Export
error[E1102]: item `foo` is not exported from module
--> src/main.ori:1:1
|
1 | use "./utils" { foo }
| ^^^ not found in utils
|
= note: available exports: helper, process, transform
= help: did you mean `bar`?
Private Item
error[E1103]: `secret` is private
--> src/main.ori:1:1
|
1 | use "./internal" { secret }
| ^^^^^^ cannot import private item
|
= help: use `::secret` for explicit private access (testing)
= help: or make `secret` public with `pub`
Examples
Organizing a Library
my_lib/
├── ori.toml
└── src/
├── lib.ori # Public API
├── types/
│ ├── mod.ori # Type exports
│ ├── user.ori
│ └── post.ori
├── services/
│ ├── mod.ori
│ ├── auth.ori
│ └── db.ori
└── internal/
└── helpers.ori # Not re-exported
// lib.ori
pub use "./types" { User, Post }
pub use "./services" { authenticate, query }
// internal not re-exported — implementation detail
// types/mod.ori
pub use "./user" { User }
pub use "./post" { Post }
Avoiding Circular Dependencies
// BAD: circular
// user.ori
use "./post" { Post }
type User = { posts: [Post] }
// post.ori
use "./user" { User }
type Post = { author: User }
// GOOD: extract to shared
// types.ori
type UserId = int
type PostId = int
// user.ori
use "./types" { UserId, PostId }
type User = { id: UserId, post_ids: [PostId] }
// post.ori
use "./types" { UserId, PostId }
type Post = { id: PostId, author_id: UserId }
Spec Changes Required
Update 12-modules.md
Add:
- Entry point file conventions (
lib.ori,mod.ori,main.ori) - Circular dependency detection algorithm and error format
- Re-export chain rules
- Visibility in nested modules
- Binary access to library (public API only)
Add Package Section
Document:
- Package structure (library vs binary)
ori.tomlmanifest format- Dependency resolution
Summary
| Aspect | Specification |
|---|---|
| lib.ori | Library package entry point |
| mod.ori | Directory module entry point |
| main.ori | Binary entry point (requires @main) |
| Binary-library | Binary accesses library via public API only |
| Circular deps | Compile error with full path shown |
| Re-export chains | All levels must be pub |
| Diamond imports | Same item = no conflict |
| Private access | :: prefix for explicit access |
| Siblings | Cannot access each other’s private items |
| Import path resolution | Relative → Package → Stdlib |
Design Decisions
lib.orivsmod.ori: Explicit distinction —lib.orifor package-level API,mod.orifor directory modules within a package- Binary-library separation: Binary uses public API only (no
::private access) to enforce clean separation - Workspaces: Deferred to future proposal
- Resolution terminology: “Import Path Resolution” distinct from name resolution