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

> Learn how KCL module loading works. Discover the KCL module search path, including entry directory, KCL_PATH, standard library, and current directory for efficient module resolution.

- Repository: [The KCL Programming Language/kcl](https://github.com/kcl-lang/kcl)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/kcl-lang/kcl/blob/main/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`](https://github.com/kcl-lang/kcl/blob/main/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`](https://github.com/kcl-lang/kcl/blob/main/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:

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

```

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

```rust
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`](https://github.com/kcl-lang/kcl/blob/main/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:

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

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

```

**Execution:**

```bash
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`](https://github.com/kcl-lang/kcl/blob/main/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`](https://github.com/kcl-lang/kcl/blob/main/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.