Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elfrask/cls/llms.txt

Use this file to discover all available pages before exploring further.

Every CLS import statement goes through a ModuleResolver. The resolver is the single, pluggable component that decides where modules come from — whether from the built-in standard library, from native Rust values injected by the host application, from .clsx source files on disk, or from any other custom source. The cls-runtime crate is intentionally agnostic about module origins: it only defines the resolver contract. It is the node (the clx desktop CLI, the clxr lightweight runtime, or your own embedding code) that configures the resolver with the right internals and the right external hook.

Resolution Order

When the interpreter encounters import "name", ModuleResolver::resolve is called. The lookup follows a strict chain:
1

Cache

If "name" was already resolved in this interpreter session, the cached Value is returned immediately. This means each module is executed at most once.
2

Internals

The resolver checks its internal map (HashMap<String, Value>) for an entry keyed exactly by "name". Internal modules are registered with add_internal or with_core_stdlib.
3

External hook

If no internal matches, the resolver calls the closure registered with set_external, passing the path string and a mutable reference to the current Environment. The hook returns Ok(Some(module)) if found, Ok(None) if not found, or Err for an error.
4

Error

If no step above resolved the module, a RuntimeError: module 'name' not found is returned.

Core API

use cls_runtime::ModuleResolver;

// Empty resolver — nothing is available
let resolver = ModuleResolver::new();

// Register the three core standard library modules
let resolver = ModuleResolver::new().with_core_stdlib();

// Add a custom internal module (e.g. backed by native Rust code)
resolver.add_internal("mydb", db_module_value);

// Set the external hook for user-authored modules
resolver.set_external(|path, env| { /* ... */ });
MethodDescription
ModuleResolver::new()Creates an empty resolver with no modules and no hook
.with_core_stdlib()Registers math, json, and async as internal modules (builder pattern — consumes self)
.add_internal(name: &str, value: Value)Registers a named module as a Value (typically a Value::Record of functions)
.set_external(fn)Sets the closure-based hook for resolving modules not found in internals

Writing an External Hook

The external hook is a closure (or function pointer) with the signature:
Fn(String, &mut Environment) -> ClsResult<Option<Value>>
  • Return Ok(Some(value)) — module found; value is stored in the cache and returned to the importer.
  • Return Ok(None) — module not found at this hook; fall through to the error step.
  • Return Err(e) — a load error; wrapped in a RuntimeError and propagated to the script.
The most common use of the external hook is to resolve user-authored .clsx modules from the filesystem. The correct approach is to compile and execute the source file using a nested Interpreter and return the collected exports via load_module_source:
use cls_runtime::{Interpreter, Intrinsics, ModuleResolver, Value};
use cls_core::error::ClsResult;

fn file_hook(path: String, _env: &mut cls_runtime::Environment) -> ClsResult<Option<Value>> {
    match std::fs::read_to_string(format!("{}.clsx", path)) {
        Ok(source) => {
            let mut inner = Interpreter::new(
                Intrinsics::empty(),
                ModuleResolver::new().with_core_stdlib(),
            );
            Ok(Some(inner.load_module_source(&path, &source)?))
        }
        Err(_) => Ok(None),  // file not found — not an error; try next step
    }
}

let resolver = ModuleResolver::new()
    .with_core_stdlib()
    .set_external(file_hook);
Interpreter::load_module_source handles the full compile → execute → collect-exports pipeline internally:
  1. Tokenizes and parses the source string.
  2. Executes the resulting Module in an isolated environment.
  3. Collects every name that was declared with export visibility.
  4. Returns a Value::Record mapping exported names to their values.
Only the exported symbols are visible to the importing module — unexported names remain private.

Injecting Native Modules with add_internal

add_internal is ideal when you want to expose parts of your Rust application — its data model, services, or APIs — to CLS scripts as a module. Build a Value::Record from FunValue::new_native entries and register it:
use cls_runtime::value::{FunValue, Value};
use std::collections::HashMap;

fn build_database_module() -> Value {
    let mut m = HashMap::new();

    m.insert("query".into(), Value::Fun(FunValue::new_native(
        "query",
        vec!["sql".into()],
        |args| {
            let sql = match args.first() {
                Some(Value::String(s)) => s.clone(),
                _ => return Err(cls_core::error::ClsError::RuntimeError(
                    "query: expected String".into()
                )),
            };
            // ... call your actual database ...
            Ok(Value::String(format!("results for: {}", sql)))
        },
    )));

    Value::Record(m)
}

// In your setup:
let mut resolver = ModuleResolver::new().with_core_stdlib();
resolver.add_internal("db", build_database_module());

let mut interp = Interpreter::new(Intrinsics::desktop_defaults(vec![]), resolver);
Scripts can then write:
import "db" as db;

var results = db.query("SELECT * FROM users");
print(results);
Use add_internal whenever a module is backed entirely by native Rust code — your application’s models, configuration, telemetry, or platform APIs. The module is pre-evaluated (it’s a live Value), so there is zero CLS compilation overhead at import time.

ClsLibResolver — Compiled .clslib Libraries

Pre-compiled CLS libraries (.clslib files) use a separate resolver: ClsLibResolver (in cls-runtime/src/clslib.rs). It is configured by the node and is used internally by Lib.load(path) calls inside CLS scripts. Search order for a .clslib:
  1. Current working directory
  2. Each path listed in the $CLS_LIB_PATH environment variable
  3. Node-configured search paths (set by the clx or clxr node at startup)
Each .clslib is identified by its SHA-256 content hash after loading, which prevents the same library from being loaded twice. The resolver returns the raw bytes of the library file, which the runtime then indexes and executes. ClsLibResolver is equivalent to a dynamic library loader (.dll/.so) for CLS — it provides binary modules that were compiled offline with clx build.

Design Rules

The following rules govern how nodes and embedders should use the resolver:

The node decides; the runtime executes

cls-core and cls-runtime know nothing about where modules come from. All module sourcing logic lives in the node or embedding code, registered via add_internal and set_external.

load_module_source is the compile+run entry point

When your external hook needs to compile a .clsx source file, always delegate to Interpreter::load_module_source. Never manually invoke Lexer, Parser, and execute in the hook — load_module_source correctly isolates the module environment and collects only exported symbols.

Node internals are not in the runtime

The fs, http, and Lib modules are node-level concerns. They are built and injected by clx. The cls-runtime crate has no knowledge of them. Embedders that need those capabilities must build equivalent native modules and register them with add_internal.

Packaging uses the resolver

When clx build packages a project into a .clsapp, it uses the resolver to discover all transitive module dependencies. Each discovered .clsx module is compiled to a .clbin AST bundle and stored inside the .clsapp archive.

Build docs developers (and LLMs) love