Skip to content

Imports and modules (how-to)

This page shows how to structure multi-file projects and use imports in practice.

Prerequisite: follow Install, build, and run so you can run incan.

Contributor source-build fallback

If you are working from a compiler checkout instead of a toolchain install, you can run the repository-built binary directly:

  • from the repository root:
./target/release/incan ...
  • or via an absolute path (from anywhere):
/absolute/path/to/incan run path/to/file.incn

Simple multi-file project

Recommended structure:

myproject/
├── main.incn
├── models.incn
└── utils.incn

Example imports:

from models import User
import utils::format_currency

When an import list gets long, put it in parentheses, one name per line, with a trailing comma; this works for rust:: imports too.

Nested projects

Recommended structure:

myproject/
└── src/
    ├── main.incn
    ├── db/
    │   └── models.incn
    └── shared/
        └── utils.incn

Example imports:

from db.models import User
import shared::utils::format_date

Publish a structured library

Create the library with its public declarations in ordinary source modules:

hyperquant-lib/
├── loaf.toml
└── src/
    ├── lib.incn
    └── hyperquant/
        ├── index.incn
        └── search.incn
# src/hyperquant/index.incn
pub model HyperquantIndex:
  pub size: int

pub def build_index(size: int) -> HyperquantIndex:
  return HyperquantIndex(size=size)

def pack_bits(size: int) -> int:
  return size
# src/hyperquant/search.incn
from hyperquant.index import HyperquantIndex

pub def search(index: HyperquantIndex) -> int:
  return index.size

Build the checked library artifact:

cd hyperquant-lib
incan build --lib

Add the library to the consumer:

[dependencies]
hyperquant_lib = { path = "../hyperquant-lib" }

Import the source-derived namespace:

from pub::hyperquant_lib import hyperquant

def main() -> None:
  index = hyperquant.build_index(1024)
  println(hyperquant.search(index))

No entry in src/lib.incn is needed merely to make hyperquant exist. Keep explicit pub from entries there when you also want a curated flat facade:

# src/lib.incn
pub from hyperquant.index import HyperquantIndex, build_index

When sibling modules publish the same declaration name, import through the exact child module instead of the parent:

from pub::codecs.encoding.base64 import encode
from pub::codecs.encoding.hex import encode as encode_hex

The compiler reports the conflicting source modules if you try the ambiguous parent import.

How module discovery works (practical view)

When you import a local module, the compiler:

  1. Resolves the path (handling ., .., super, crate).
  2. Looks for the .incn file (or mod.incn for directories).
  3. Parses and type-checks that file.
  4. Makes its types and functions available in your importing file.

Published-library consumers follow a different trust boundary: they resolve pub:: imports from the checked .incnlib artifact and generated library crate rather than re-reading the dependency's source.

flowchart LR
  A["main.incn import"] --> B["resolve module path"]
  B --> C["models.incn or models/mod.incn"]
  C --> D["parse and type-check module"]
  D --> E["checked public symbols"]
  E --> F["importing module scope"]

Imports resolve files first, then expose checked symbols. They do not paste source text into the caller.

Examples from the repo

  • Multi-file example: examples/advanced/multifile/

Run:

incan run examples/advanced/multifile/main.incn
  • Nested project example: examples/advanced/nested_project/

Run:

incan run examples/advanced/nested_project/src/main.incn

If you prefer browsing first, see the examples directory on GitHub: https://github.com/encero-systems/incan/tree/main/examples.

Share module-owned state across files

If a module needs to export live runtime state, use pub static:

This example shares live runtime state across module boundaries with pub static:

counters.incn
pub static hits: int = 0

pub def record_hit() -> None:
    hits += 1
main.incn
from counters import hits, record_hit

def main() -> None:
    record_hit()
    record_hit()
    println(hits)

This prints 2.

For the full walkthrough, see: Module state (how-to).

Import two names that would collide

Two imports that bind the same local name are refused, whether they name the same declaration or different ones. When both targets are intentional, give each an explicit alias:

import codecs.prelude as codecs_prelude
import compression.prelude as compression_prelude

Import a standard-library declaration

from std import name binds a standard-library module, not a declaration. When from std import Debug is refused, the diagnostic names the module that declares Debug when it is known; import the declaration from that module:

from std.derives.string import Debug

Activate async and await

Import std.async, or one of its modules such as std.async.time, before the first async def or await in the file. Without that import, async and await are refused (INCAN-P0001), and the diagnostic names the import to add.