How KCL Module Loading Works: Search Path Resolution and Module Discovery

KCL resolves module imports through a multi-phase loader that converts dotted import paths to file system locations using an ordered search path consisting of the entry directory, the KCL_PATH environment variable, the built-in standard library, and the current working directory.

KCL (Kusion Configuration Language) uses a sophisticated module loading system to discover and compile dependencies before evaluation begins. Understanding how the module search path is constructed and how import resolution works is essential for organizing large configuration projects. The loading mechanism is implemented in the crates/loader directory of the KCL source code and centers around the KCLModuleCache structure for caching parsed modules.

The Module Loading Architecture

The KCL compiler delegates dependency discovery to a dedicated loader crate that operates before the evaluator runs. This architecture ensures that all imported modules are parsed and cached before execution begins.

KCLModuleCache and Entry Points

The KCLModuleCache structure, defined in crates/loader/src/lib.rs, serves as the central registry for parsed modules. It stores Module objects keyed by their absolute file paths to avoid re-parsing the same file multiple times during a compilation session.

The primary entry point is the load_program function, which accepts the entry file path and orchestrates the loading process. This function initiates an AST traversal using the ImportVisitor to identify all import statements within the entry module.

The Three-Phase Loading Process

The loader executes in distinct phases:

  1. Parse the entry module — The file supplied to the CLI (kcl run <file>) is parsed into an AST.
  2. Collect import statements — The ImportVisitor walks the AST and records every import clause.
  3. Resolve dependent modules — For each import, the resolve_import function (located in crates/loader/src/resolver.rs) probes the search path, parses found files into Module objects, and inserts them into KCLModuleCache.

Circular imports are detected during phase three and reported as diagnostics before evaluation begins.

How the Module Search Path is Constructed

The module search path is an ordered list of directories that the resolver scans when locating a module file. The KCLSearchPath struct in crates/loader/src/search_path.rs constructs this list at startup.

Search Path Priority Order

KCL resolves imports by checking directories in the following strict priority:

  1. The directory containing the entry file — Relative imports (e.g., import .subdir.mod) resolve against this location first.
  2. KCL_PATH environment variable — A platform-specific delimited list (: on Unix, ; on Windows).
  3. Built-in standard library — Compiled into the binary and referenced as kcl/std (located under crates/stdlib).
  4. Current working directory — Used when the entry file is specified as a relative path.

KCL_PATH Environment Variable

Developers can extend the search path by setting the KCL_PATH variable:

export KCL_PATH=$HOME/.kcl/packages:/opt/kcl/stdlib
kcl run project/main.k

The KCLSearchPath::new implementation constructs the directory list as follows:

impl KCLSearchPath {
    pub fn new(entry_dir: &Path) -> Self {
        let mut dirs = vec![entry_dir.to_path_buf()];
        if let Ok(env) = std::env::var("KCL_PATH") {
            dirs.extend(env.split(PATH_SEPARATOR).map(PathBuf::from));
        }
        dirs.push(PathBuf::from(crate::stdlib::STD_LIB_PATH));
        dirs
    }
}

Import Resolution Mechanics

When the loader encounters an import statement, it must transform the dotted module name into a concrete file system path.

From Dotted Names to File Paths

The resolve_import function in crates/loader/src/resolver.rs performs the following conversion:

  • The import string my_pkg.utils becomes the relative path my_pkg/utils.k.
  • The resolver iterates over the ordered search path directories.
  • The first directory containing the target file wins.

For example, given import helper in project/main.k with KCL_PATH set to $(pwd)/lib:

  1. Check project/helper.k — not found.
  2. Check project/lib/helper.k — found and parsed.

Circular Import Detection

The loader tracks module loading state within KCLModuleCache to detect circular dependencies. If module A imports B, and B attempts to import A during the same loading phase, the resolver identifies the cycle and emits a diagnostic error rather than entering infinite recursion.

Practical Configuration Example

Consider a project with the following layout:


project/
├── main.k
├── my_pkg/
│   └── utils.k
└── lib/
    └── helper.k

main.k contains:

import my_pkg.utils  # Resolves to ./my_pkg/utils.k

import helper        # Resolves via KCL_PATH to ./lib/helper.k

Execution:

export KCL_PATH=$(pwd)/project/lib
kcl run project/main.k

The loader resolves my_pkg.utils from the entry directory and helper from the KCL_PATH extension. If a module cannot be located, the compiler produces an error message specifying the import source and the search path locations checked:


Cannot find the module 'nonexistent' from /path/to/project/main.k

Summary

  • KCLModuleCache in crates/loader/src/lib.rs stores parsed modules keyed by absolute paths to prevent duplicate parsing.
  • The search path priority is: entry file directory → KCL_PATH → built-in standard library → current working directory.
  • Import resolution converts dotted names (e.g., pkg.mod) to file paths (e.g., pkg/mod.k) and scans the search path sequentially.
  • The KCL_PATH environment variable uses colon or semicolon delimiters to specify additional module directories.
  • Circular imports are detected early in the loading phase and reported as errors before evaluation.

Frequently Asked Questions

How does KCL handle relative imports?

Relative imports (starting with .) are resolved against the directory containing the file that contains the import statement. This allows modules to import sibling or child modules without relying on the global KCL_PATH environment variable.

What file extension does KCL use for module files?

KCL module files use the .k extension. When resolving an import like import my_pkg.utils, the loader searches for my_pkg/utils.k within the search path directories.

Can I override the standard library location?

The built-in standard library path is compiled into the binary and referenced as crate::stdlib::STD_LIB_PATH in crates/loader/src/search_path.rs. While you cannot override this specific entry, you can place modules with identical names earlier in the search path (via the entry directory or KCL_PATH) to shadow standard library modules.

What happens if the same module is imported from multiple files?

The KCLModuleCache structure ensures that each absolute file path is parsed only once per compilation. Subsequent imports of the same module retrieve the cached Module object, improving performance and ensuring consistent state across the program.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →