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:

  1. Entry point files: What distinguishes lib.ori, mod.ori, and main.ori?
  2. Circular dependencies: How are they detected and reported?
  3. Re-export chains: What happens with pub use across multiple levels?
  4. Nested modules: How does visibility work in nested directories?
  5. 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

FilePurpose
main.oriBinary entry point (must contain @main)
lib.oriLibrary entry point (defines public API)
mod.oriDirectory module entry point (within a package)

lib.ori and mod.ori serve similar purposes at different levels:

  • lib.ori is the package-level public interface
  • mod.ori is 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:

  1. Start with the entry module (e.g., main.ori)
  2. For each use statement, add an edge from current module to target
  3. Detect cycles using depth-first traversal
  4. 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:

  1. Extract shared types/functions to a third module
  2. Use dependency injection (pass functions as parameters)
  3. 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

  1. Relative path ("./...", "../..."): Resolve relative to current file’s directory
  2. Package path ("pkg_name"): Look up in ori.toml dependencies
  3. 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:

  1. Entry point file conventions (lib.ori, mod.ori, main.ori)
  2. Circular dependency detection algorithm and error format
  3. Re-export chain rules
  4. Visibility in nested modules
  5. Binary access to library (public API only)

Add Package Section

Document:

  1. Package structure (library vs binary)
  2. ori.toml manifest format
  3. Dependency resolution

Summary

AspectSpecification
lib.oriLibrary package entry point
mod.oriDirectory module entry point
main.oriBinary entry point (requires @main)
Binary-libraryBinary accesses library via public API only
Circular depsCompile error with full path shown
Re-export chainsAll levels must be pub
Diamond importsSame item = no conflict
Private access:: prefix for explicit access
SiblingsCannot access each other’s private items
Import path resolutionRelative → Package → Stdlib

Design Decisions

  1. lib.ori vs mod.ori: Explicit distinction — lib.ori for package-level API, mod.ori for directory modules within a package
  2. Binary-library separation: Binary uses public API only (no :: private access) to enforce clean separation
  3. Workspaces: Deferred to future proposal
  4. Resolution terminology: “Import Path Resolution” distinct from name resolution