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.

The CLS language is split into two independent Rust crates — cls-core (the compiler frontend: lexer, parser, AST, type checker) and cls-runtime (the tree-walking interpreter, value system, module resolver, and scheduler). Neither crate depends on filesystem access, a CLI, or any node-specific functionality. This means you can embed them directly into any Rust application and use CLS as a sandboxed scripting language, an expression evaluator, a configuration format, or a plugin system — with full control over which modules and globals are available to scripts.

Cargo.toml

Add both crates as path dependencies (once CLS is available on crates.io the same block will use registry versions):
[dependencies]
cls-core = { path = "cls-core" }
cls-runtime = { path = "cls-runtime" }

Minimal Working Example

The following Rust program tokenizes a CLS source string, parses it into an AST module, creates an interpreter configured with core stdlib and desktop globals, executes the module, and then calls main:
use cls_core::frontend::{Lexer, Parser};
use cls_runtime::{Intrinsics, Interpreter, ModuleResolver};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let source = r#"
        function main(args: String[]) -> int {
            print("Hello from embedded CLS!");
            return 0;
        };
    "#;

    // 1. Tokenize
    let tokens = Lexer::new(source).tokenize()?;

    // 2. Parse into an AST Module
    let module = Parser::new(tokens).parse()?;

    // 3. Create the interpreter
    //    - Intrinsics::desktop_defaults adds print, input, args
    //    - ModuleResolver::with_core_stdlib adds math, json, async
    let mut interp = Interpreter::new(
        Intrinsics::desktop_defaults(vec![]),
        ModuleResolver::new().with_core_stdlib(),
    );

    // 4. Execute all top-level statements (defines functions, runs expressions)
    interp.execute(&module)?;

    // 5. Call main() and use its return value as the process exit code
    let code = interp.call_main()?;
    std::process::exit(code);
}
1

Tokenize

Lexer::new(source).tokenize() produces a Vec<Token> or a ClsError on a lexical error.
2

Parse

Parser::new(tokens).parse() produces a Module (the root AST node) or a ClsError on a syntax error.
3

Create the interpreter

Interpreter::new(intrinsics, resolver) initialises the environment, registers all global intrinsics, and builds the primitive method dispatch tables.
4

Execute the module

interp.execute(&module) runs all top-level statements: function declarations are registered, variable initialisers are evaluated, class and structure definitions are stored.
5

Call main

interp.call_main() looks up the main function in the environment, calls it with args, and returns the integer exit code (or 0 if main returns a non-integer or does not exist).

Intrinsics API

Intrinsics is the struct that supplies the node-provided global environment — the functions and values that scripts can use without importing any module.
let intr = Intrinsics::empty();
// Scripts have no print, input, or args.
// Core intrinsics (toString, int, float, etc.) are still registered
// by the interpreter itself regardless.
MethodDescription
Intrinsics::empty()No globals — not even print or args
Intrinsics::desktop_defaults(args: Vec<String>)Provides print, input, and args
intr.add(name: &str, value: Value)Inject any additional global by name

ModuleResolver API

ModuleResolver is the pluggable system that controls how import "name" statements are resolved. See Module Resolvers for the full guide; the key methods are summarised here.
MethodDescription
ModuleResolver::new()Creates an empty resolver (no modules available)
.with_core_stdlib()Registers math, json, async as built-in internals
.add_internal(name: &str, value: Value)Inject a named module backed by a Value::Record of native functions
.set_external(closure)Provide a closure hook for resolving user modules from files or other sources
use cls_runtime::{ModuleResolver, Intrinsics, Interpreter, Value};
use cls_core::error::ClsResult;

let resolver = ModuleResolver::new()
    .with_core_stdlib()
    .set_external(|path: String, _env| -> 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),
        }
    });

let mut interp = Interpreter::new(Intrinsics::desktop_defaults(vec![]), resolver);

Type Checking Before Execution

To run the CLS type checker on an AST module before executing it, use TypeChecker from cls-core:
use cls_core::middleware::TypeChecker;
use cls_core::config::types::TypesConfig;

let mut checker = TypeChecker::new(TypesConfig {
    check: true,
    strict: true,
    ..Default::default()
});

checker.check(&module)?;

let diagnostics = checker.diagnostics();
for diag in diagnostics {
    eprintln!("[{}] {}", diag.severity, diag.message);
}
The type checker runs statically over the AST and populates a Vec<Diagnostic>. It does not execute any code. You can choose to treat warnings as errors, or simply log them and proceed to execution.

Error Reporting

When execute or call_main returns an Err, use build_error_report and format_error to produce human-readable output with source context and a call stack trace:
use cls_runtime::{ErrorFormat, format_error};

match interp.execute(&module) {
    Ok(_) => {}
    Err(e) => {
        let report = interp.build_error_report(e);
        eprintln!("{}", format_error(&report, &ErrorFormat::Console));
        std::process::exit(1);
    }
}
ErrorFormat variants control the output style:
VariantOutput
ErrorFormat::ConsoleColour-coded terminal output with source line highlight
ErrorFormat::PlainPlain text, suitable for log files
ErrorFormat::HtmlHTML fragment for embedding in web UIs
ErrorFormat::JsonMachine-readable JSON object

Attaching Source Context

If your source came from a file, pass the path to the interpreter before executing so that error reports can show the file name:
interp.set_source_file("scripts/main.clsx".to_string());
interp.execute(&module)?;

Notes

Interpreter is not Send. It holds Arc<Mutex<Environment>> closures internally and relies on a single-threaded cooperative scheduler for async coroutines. Always create and run an interpreter on the same thread.
The desktop node modules (fs, http, Lib) are not part of cls-runtime. If your embedded application needs them, you must build the corresponding Value::Record of native functions and register them with resolver.add_internal("fs", fs_module) before passing the resolver to the interpreter.

Build docs developers (and LLMs) love