Modules
Splitting programs across files with import and export.
Achronyme supports splitting code across multiple .ach files using modules. Export declarations to make them available, and import modules by namespace or by name.
Exporting Declarations
Inline Exports
Use export before fn or let to make a declaration available to other files.
// math.ach
export fn add(a, b) { a + b }
export fn mul(a, b) { a * b }
export let PI = 3
Only fn and let can be exported. Mutable variables (mut), public, and witness declarations cannot be exported.
// These are errors:
export mut x = 5 // ✗ cannot export mut
export public y // ✗ cannot export public
Export Lists
You can also collect exports at the end of a file using export { ... }:
// math.ach
fn add(a, b) { a + b }
fn mul(a, b) { a * b }
let PI = 3
export { add, mul, PI }
This is equivalent to marking each declaration with export individually. You can mix both styles in the same file. Exporting the same name twice (inline + list, or listed twice) is an error.
Declarations without export are private to the file. They can be used by exported functions internally but are not accessible from outside.
// helpers.ach
fn internal_helper(x) { x * x } // private
export fn square(x) {
internal_helper(x) // works inside this file
}
Importing a Module
Namespace Import
Use import "path" as alias to load a module. The alias is mandatory and becomes the namespace for accessing the module’s exports.
// main.ach
import "./math.ach" as math
print(math.add(1, 2)) // 3
print(math.PI) // 3
Access exported values using dot notation: alias.name.
Selective Import
Use import { name1, name2 } from "path" to import specific names directly into scope, without a namespace prefix:
import { add, PI } from "./math.ach"
print(add(1, 2)) // 3 — no prefix needed
print(PI) // 3
Selective imports copy the value at import time (snapshot semantics). Changes to the original module’s globals after import are not reflected.
If a requested name is not exported by the module, the compiler suggests the closest match:
error: module "math.ach" does not export `ad`. Did you mean `add`?
You can combine both styles for the same module:
import { add } from "./math.ach"
import "./math.ach" as math
print(add(1, 2)) // selective — no prefix
print(math.mul(3, 4)) // namespace — with prefix
Module Paths
Paths are relative to the importing file and must include the .ach extension.
import "./utils.ach" as utils // same directory
import "../lib/hash.ach" as hash // parent directory
import "./crypto/poseidon.ach" as pos // subdirectory
Absolute paths are not supported. All imports use relative paths.
Transitive Imports
Modules can import other modules. The chain is resolved automatically.
// a.ach
export fn fa() { 1 }
// b.ach
import "./a.ach" as a
export fn fb() { a.fa() + 1 }
// c.ach
import "./b.ach" as b
print(b.fb()) // 2
Circular Imports
Circular dependencies are detected and produce an error.
// x.ach
import "./y.ach" as y // error: CircularImport
// y.ach
import "./x.ach" as x
Modules in Circuits
Imports work with ach circuit the same way as ach run. Imported functions are inlined at each call site.
// hash_lib.ach
export fn my_hash(a, b) { poseidon(a, b) }
// circuit.ach
import "./hash_lib.ach" as h
circuit hash_check(out: Public, a: Witness, b: Witness) {
assert_eq(h.my_hash(a, b), out)
}
Errors
| Error | Cause |
|---|---|
ModuleNotFound | The file path does not resolve to an existing file |
CircularImport | Two or more modules import each other |
ModuleLoadError | The file was found but contains parse errors |
DuplicateModuleAlias | Two different files imported with the same alias |
| Name not exported | import { x } where x is not exported by the module (with “did you mean?” suggestion) |
| Duplicate export | The same name is exported more than once (inline + list, or listed twice) |
| Import conflict | A selectively imported name conflicts with an existing global or a prior import from a different module |
| Export list undefined | export { x } where x is not defined in the module |
Warnings
| Code | Cause |
|---|---|
| W005 | A selectively imported name is never used (prefix with _ to suppress) |
Restrictions
importandexportare only allowed at the top level (not inside functions or blocks)import,export, andasare reserved keywordsfromis not a keyword — it is recognized contextually only afterimport { ... }, so it can be used as a variable name elsewhere- Selective imports use snapshot semantics (the value is copied at import time)
- Re-exports (
export import) are not supported - Importing the same file with two different aliases is allowed (the file is parsed once)
Quick Reference
| Syntax | Description |
|---|---|
export fn name() { ... } | Export a function (inline) |
export let X = value | Export a constant (inline) |
export { a, b, c } | Export multiple names (list) |
import "./file.ach" as alias | Namespace import |
import { a, b } from "./file.ach" | Selective import |
alias.name | Access a namespace binding |