Module System

Behl’s module system enables code organization and reusability through file-based modules with explicit exports and imports.

Table of contents

  1. Overview
  2. The import() Function
    1. Basic Usage
    2. Module Caching
  3. Module Path Resolution
    1. 1. Relative Imports
    2. 2. Same Directory
    3. 3. Modules Subdirectory
    4. 4. Current Working Directory
    5. File Extension
  4. Module Mode
    1. Declaring a Module
    2. Script Mode vs Module Mode
  5. Exporting from Modules
    1. 1. Manual Export Table (Explicit)
    2. 2. Export Keyword (Recommended)
    3. 3. Script Mode Return
  6. Module Examples
    1. Simple Module
    2. Stateful Module
    3. Relative Import Example
  7. Built-in Modules
    1. Accessing Standard Modules
  8. C++ API for Modules
    1. Registering C++ Modules
  9. Best Practices
    1. 1. Use Module Mode for Libraries
    2. 2. Explicit Exports
    3. 3. Relative Imports for Project Files
    4. 4. Module State is Shared
  10. Limitations
  11. Limitations
  12. See Also

Overview

Behl uses a file-based module system similar to Node.js/ES6 modules. Each .behl file can be either:

  • Script mode (default) - Normal script with access to global scope
  • Module mode - Isolated scope with explicit exports, declared with module; at the top

The import() Function

Basic Usage

// Import a module
const math = import("math");
print(math.pi);

// Import with relative path
const utils = import("./utils");

// Import from subdirectory
const db = import("database/postgres");

Module Caching

Modules are executed once and cached. Subsequent imports return the same cached table:

const logger1 = import("utils/logger");
logger1.log("Hello");

// Returns the same instance - state is preserved
const logger2 = import("utils/logger");
print(logger2.getLogCount()); // Includes the "Hello" log

Key Points:

  • Modules execute only on first import
  • State persists across all importers
  • Cache key is the resolved absolute path

Module Path Resolution

The import() function resolves module paths using a specific search strategy:

1. Relative Imports

Paths starting with ./ or ../ are resolved relative to the importing file:

// In /project/src/main.behl
const helper = import("./helper");      // → /project/src/helper.behl
const shared = import("../shared");     // → /project/shared.behl

2. Same Directory

Non-relative imports first search in the importing file’s directory:

// In /project/src/main.behl
const config = import("config");        // → /project/src/config.behl

3. Modules Subdirectory

Next, searches in a modules/ subdirectory relative to the importing file:

// In /project/src/main.behl
const utils = import("utils/logger");   // → /project/src/modules/utils/logger.behl

4. Current Working Directory

Finally, searches relative to the current working directory where the script was executed:

// If running from /project/
const math = import("math");            // → /project/modules/math.behl
                                        //    or /project/math.behl

Note for Embedders: Additional search paths can be configured via the C++ API by modifying State.module_paths.

File Extension

The .behl extension is automatically added if not present:

const math = import("math");            // → math.behl
const config = import("config.behl");   // → config.behl (no change)

Module Mode

Declaring a Module

Add module; as the first statement to enable module mode:

module;

// Module code here

Effects of Module Mode:

  • No global scope access - Cannot read/write global variables
  • Local by default - Functions and variables are private unless exported
  • Must export explicitly - Use export keyword (returns are automatically added)
  • Must import all modules - Standard library modules (math, string, table, os, gc, debug) must be imported with import("math")
  • Only builtins accessible - Core functions like print, typeof, tostring, tonumber, import, error, pcall, etc. are available without import

Script Mode vs Module Mode

Script Mode (default):

// Can access and modify globals
let x = 10;
globalVar = 20;

function helper() {
    return 42;
}
// helper is accessible globally

Module Mode:

module;

// Must import - no access to global math
const math = import("math");

// Private - not accessible outside module
let count = 0;

function increment() {
    count++;
}

// Must export to make available
export function getCount() {
    return count;
}

Exporting from Modules

Behl supports three export patterns:

1. Manual Export Table (Explicit)

Return a table containing exported values:

module;

let privateVar = 42;

function publicFunc() {
    return privateVar * 2;
}

// Build and return export table
let exports = {};
exports.publicFunc = publicFunc;
return exports;

Usage:

const mymod = import("mymod");
print(mymod.publicFunc()); // 84
module;

export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

// Private function
function helper() {
    return 42;
}

Important: Exported items are automatically collected and returned in an __EXPORTS__ table. You don’t need to manually return anything:

const mymod = import("mymod");
print(mymod.PI);        // 3.14159
print(mymod.add(2, 3)); // 5

Export Rules:

  • Can only export const declarations, not let (mutable variables cannot be exported)
  • Can export functions
  • Can export multiple items in one statement: export { PI, add, multiply }

3. Script Mode Return

Even without module;, you can return a table to create an exportable interface:

// No "module;" - script mode

let counter = 0;

function increment() {
    counter++;
}

function get() {
    return counter;
}

// Return exports
return {
    increment = increment,
    get = get
};

Module Examples

Simple Module

modules/math.behl:

module;

function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

let exports = {};
exports.add = add;
exports.multiply = multiply;
return exports;

main.behl:

const math = import("modules/math");
print(math.add(5, 3));      // 8
print(math.multiply(4, 7)); // 28

Stateful Module

logger.behl:

module;

let logCount = 0;

function log(message) {
    logCount++;
    print("[LOG] " + message);
}

function getCount() {
    return logCount;
}

return {
    log = log,
    getCount = getCount
};

Usage:

const logger = import("logger");
logger.log("Starting app");
logger.log("Loading config");
print("Total logs: " + tostring(logger.getCount())); // 2

Relative Import Example

database/shared.behl:

module;

function validateConnection(host) {
    return host != nil && typeof(host) == "string";
}

return { validateConnection = validateConnection };

database/postgres.behl:

module;

// Import from same directory
const shared = import("./shared");

let queryCount = 0;

function connect(host, port) {
    if (!shared.validateConnection(host)) {
        error("Invalid host");
    }
    print("Connected to " + host + ":" + tostring(port));
}

function query(sql) {
    queryCount++;
    return { result = "success", count = queryCount };
}

return {
    connect = connect,
    query = query,
    getQueryCount = function() { return queryCount; }
};

Built-in Modules

Behl provides standard modules that are registered when load_stdlib(S) is called:

  • math - Mathematical functions and constants
  • string - String manipulation utilities
  • table - Table manipulation functions
  • os - Operating system functions
  • gc - Garbage collector control
  • debug - Debugging utilities

Accessing Standard Modules

All standard library modules must be explicitly imported, regardless of whether you’re in script mode or module mode:

Script Mode (no module; declaration):

// Must explicitly import
const math = import("math");
const string = import("string");
print(math.pi);              // 3.14159
let upper = string.upper("hello");

Module Mode (with module; declaration):

module;

// Must explicitly import
const math = import("math");
const string = import("string");
print(math.pi);              // 3.14159

See Standard Library for complete API documentation.

C++ API for Modules

Registering C++ Modules

Use create_module() to register native modules:

#include <behl/behl.hpp>

static int add(behl::State* S) {
    int a = behl::to_integer(S, 0);
    int b = behl::to_integer(S, 1);
    behl::push_integer(S, a + b);
    return 1;
}

int main() {
    behl::State* S = behl::new_state();
    
    // Define module
    behl::ModuleReg funcs[] = {
        {"add", add}
    };
    behl::ModuleDef def = { .funcs = funcs };
    
    // Register module
    behl::create_module(S, "mymath", def);
    
    // Now available via import("mymath")
    behl::load_string(S, R"(
        const mymath = import("mymath");
        print(mymath.add(5, 3));
    )");
    behl::call(S, 0, 0);
    
    behl::close(S);
}

Best Practices

1. Use Module Mode for Libraries

module;  // Isolate from global scope

// Define your library

2. Explicit Exports

Be clear about what’s public:

module;

// Private helpers
function validateInput(x) { /* ... */ }

// Public API
return {
    processData = function(data) {
        if (!validateInput(data)) {
            error("Invalid data");
        }
        // ...
    }
};

3. Relative Imports for Project Files

// Prefer relative imports within your project
const config = import("./config");
const utils = import("../utils/helpers");

// Use module paths for third-party or standard libs
const math = import("math");

4. Module State is Shared

Remember that module state persists:

module;

letError Messages

Module mode provides clear compile-time errors when accessing undeclared variables:

```cpp
module;

// Error: Variable 'math' is not declared. Use 'let' or 'const' to declare 
// local variables, or 'import()' to load modules.
print(math.pi);

// Error: Variable 'globalVar' is not declared.
let x = globalVar;

These are compile-time errors (thrown as SemanticError during parsing), not runtime errors.

Limitations

  • No circular imports protection (may cause infinite loops)
  • No dynamic module reloading (cache cannot be invalidated)
  • Module mode validates identifiers at compile time, so any undefined variable access fails even if it would exist at runtime connectionPool = createPool(); } return connectionPool; }

return { getConnection = getConnection }; ```

All importers share the same connectionPool.

Limitations

  • No circular imports protection (may cause infinite loops)
  • No dynamic module reloading (cache cannot be invalidated)

See Also


Copyright © 2025 behl Project. Distributed under MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.