Modules
10.1 Module Definitions
Modules are the primary unit of code organization, encapsulation, and reuse in AIVI. They define a closed scope and explicitly export symbols for public use.
Modules can be written in a flat form that keeps file indentation shallow. The module body runs until end-of-file:
module my.utility.math
export add, subtract
export pi
pi = 3.14159
add = a b => a + b
subtract = a b => a - b
// Internal helper, not exported
abs = n => if n < 0 then -n else nWhen using the flat form, the module declaration must be the last top-level item in the file and its body extends to EOF.
The explicit braced form is still supported (and required for multiple modules in one file):
module my.utility.math = {
export add, subtract
export pi
pi = 3.14159
add = a b => a + b
subtract = a b => a - b
// Internal helper, not exported
abs = n => if n < 0 then -n else n
}10.2 Module Pathing (Dot Separator)
Modules are identified by hierarchical paths using common dot notation. This separates logical namespaces. By convention:
aivi.*— Standard libraryvendor.name.*— Foreign librariesuser.app.*— Application-specific logic
Module resolution is static and determined at compile time based on the project manifest.
10.3 Importing and Scope
Use the use keyword to bring symbols from another module into the current scope.
Basic Import
use aiviSelective / Selective Hiding
use aivi.calendar (Date, isLeapYear)
use aivi.list hiding (map, filter)Renaming / Aliasing
use aivi.calendar as Cal
use vendor.legacy.math (v1_add as add)Compiler checks:
- Importing a missing module or symbol is a compile-time error.
- Unused imports produce a warning (suppressed if importing solely for a domain side-effect in v0.1).
10.4 Domain Exports
Modules are the primary vehicle for delivering Domains. Exporting a domain automatically exports its carrier type, delta types, and operators.
module geo.vector = {
export domain Vector
export Vec2
Vec2 = { x: Float, y: Float }
domain Vector over Vec2 = {
(+) : Vec2 -> Vec2 -> Vec2
(+) a b = { x: a.x + b.x, y: a.y + b.y }
}
}When another module calls use geo.vector, it gains the ability to use + on Vec2 records.
10.5 First-Class Modules
Modules are statically resolved but behave like first-class records within the compiler's intermediate representation. This enables powerful composition patterns.
Nested Modules
module aivi = {
module calendar = { ... }
module number = { ... }
}Module Re-exports
A module can aggregate other modules, acting as a facade.
module aivi.prelude = {
export domain Calendar, Color
export List, Result, Ok, Err
use aivi.calendar (domain Calendar)
use aivi.color (domain Color)
use aivi (List, Result, Ok, Err)
}10.6 The Prelude
Every AIVI module implicitly starts with use aivi.prelude. This provides access to the core language types and the most common domains without boilerplate.
To opt-out of this behavior (mandatory for the core stdlib itself):
@no_prelude
module aivi.bootstrap = {
// Pure bootstrap logic
}10.7 Circular Dependencies
Circular module dependencies are strictly prohibited at the import level. The compiler enforces a Directed Acyclic Graph (DAG) for module resolution. For mutually recursive types or functions, they must reside within the same module or be decoupled via higher-order abstractions.
10.8 Expressive Module Orchestration
Modules allow for building clean, layered architectures where complex internal implementations are hidden behind simple, expressive facades.
Clean App Facade
// Aggregate multiple sub-modules into a single clean API
module my.app.api = {
export login, fetchDashboard, updateProfile
use my.app.auth (login)
use my.app.data (fetchDashboard)
use my.app.user (updateProfile)
}Domain Extension Pattern
// Enhance an existing domain with local helpers
module my.geo.utils = {
export domain Vector
export distanceToOrigin, isZero
use geo.vector (domain Vector, Vec2)
distanceToOrigin = v => sqrt (v.x * v.x + v.y * v.y)
isZero = v => v.x == 0 && v.y == 0
}Context-Specific Environments (Static Injection)
This pattern allows you to statically swap entire module implementations for different build contexts (e.g., Test vs. Prod). This is not for runtime configuration (see below), but for compile-time substitution of logic.
// 1. Define the production module
module my.app.api = {
export fetchDashboard
fetchDashboard = ... // Real HTTP call
}
// 2. Define the test module (same interface, different logic)
module my.app.api.test = {
export fetchDashboard
fetchDashboard = _ => { id: 1, title: "Mock Dash" }
}To use the test environment, your test entry point (tests/main.aivi) simply imports the test module instead of the production one:
// within tests/main.aivi
use my.app.api.test (fetchDashboard) // injected mock10.9 Runtime Configuration (Env Vars)
For values that change between deployments (like API URLs or DB passwords) without changing code, use Runtime Configuration via the Env source.
Do not use module swapping for this. Instead, inject the configuration as data.
See 12.4 Environment Sources for details.
// Instead of hardcoding, load from environment
config : Source Env { apiUrl: Text }
config = env.decode { apiUrl: "https://localhost:8080" }
connect = effect {
cfg <- load config
// ... use cfg.apiUrl
}