How Nelson's Module System Works and How to Create a Custom Module

Nelson's module system uses a C++ ModulesManager to register directories under modules/, exposing functionality through built-ins like addmodule and modulepath, while each module uses loader.m, startup.m, and finish.m scripts to initialize and clean up resources.

Nelson organizes its functionality into discrete units called modules. Each module lives in its own directory under the modules/ folder and registers with the runtime through a small amount of boilerplate code. Understanding how Nelson's module system works is essential for extending the language with custom functionality or packaging existing code for redistribution.

Core Architecture of Nelson's Module System

ModulesManager and C++ Registration Layer

At the heart of Nelson's module system lies the ModulesManager class implemented in modules/modules_manager/src/cpp/ModulesManager.cpp. This singleton maintains a vector of registered modules, storing metadata including modulename, modulepath, isprotected, version, and library path.

When you register a module, the manager inserts a new entry via RegisterModule:

modulesMap.emplace_back(modulename, path, L"", protectedModule, version);

The manager provides APIs such as UnregisterModule, IsExistingModuleName, IsProtectedModuleName, and GetModules to query and manipulate the module registry at runtime.

Built-in Functions for Module Management

The C++ layer exposes functionality to the Nelson language through several built-in functions:

Module Lifecycle and Initialization

loader.m: Registration

Every module contains a loader.m file in its root directory (e.g., modules/xml/loader.m). This script executes automatically at startup and calls addmodule to register the module with the runtime:

% modules/my_module/loader.m
addmodule([nelsonroot() '/modules/' 'my_module'], 'my_module');

This invocation stores the module metadata in the ModulesManager vector, making the module discoverable by ismodule and modulepath.

startup.m: Initialization

After registration, Nelson executes etc/startup.m within the module directory. This script typically performs two critical tasks:

  1. Register the gateway: Calls addgateway to map function calls to the module's compiled library.
  2. Add functions to path: Calls addpath with the -frozen flag to include the module's functions/ directory.

Example from modules/xml/etc/startup.m:

% modules/my_module/etc/startup.m
% Register the built-in library gateway
addgateway(modulepath('my_module','builtin'), 'my_module');

% Add functions directory to path (frozen prevents overwriting)
addpath(modulepath('my_module','functions'), '-frozen');

finish.m: Cleanup

When Nelson shuts down or the module unloads, it executes etc/finish.m to reverse the initialization steps:

% modules/my_module/etc/finish.m
% Remove functions from path
rmpath(modulepath('my_module','functions'));

% Remove the gateway
removegateway(modulepath('my_module','builtin'));

How to Create a Custom Module in Nelson

Creating a custom module involves creating a directory structure and three mandatory scripts. Below is a complete, runnable example.

Step 1: Create the Directory Structure

Create the following hierarchy under modules/my_module:


modules/
└─ my_module/
   ├─ loader.m
   ├─ etc/
   │  ├─ startup.m
   │  └─ finish.m
   ├─ functions/
   │   └─ hello.m
   └─ tests/
       └─ test_hello.m

Step 2: Write loader.m

This file registers your module with the ModulesManager:

% modules/my_module/loader.m
addmodule([nelsonroot() '/modules/' 'my_module'], 'my_module');

Step 3: Configure startup.m and finish.m

startup.m initializes the module:

% modules/my_module/etc/startup.m
% Register gateway for built-in functions (if you have a .dll/.so)
addgateway(modulepath('my_module','builtin'), 'my_module');

% Add functions directory to Nelson path
addpath(modulepath('my_module','functions'), '-frozen');

finish.m cleans up:

% modules/my_module/etc/finish.m
rmpath(modulepath('my_module','functions'));
removegateway(modulepath('my_module','builtin'));

Step 4: Add Functions and Tests

Create a simple function in functions/hello.m:

% modules/my_module/functions/hello.m
function out = hello(name)
%HELLO  Simple greeting example
%   out = hello('Nelson')  returns  "Hello, Nelson!"
    if nargin == 0
        name = 'World';
    end
    out = sprintf('Hello, %s!', name);
end

Optional test in tests/test_hello.m:

% modules/my_module/tests/test_hello.m
function test_hello
    assert_isequal(hello('Nel'), 'Hello, Nel!');
end

Step 5: Load and Verify

You can manually load your module in a Nelson session:

>> addmodule([nelsonroot() '/modules/my_module'], 'my_module');
>> hello('Nelson')
ans =  "Hello, Nelson!"

Alternatively, restart Nelson. The Modules Manager automatically executes every loader.m found under modules/ during startup.

Using Module Management Built-ins

Nelson provides several built-in functions to query and manage modules at runtime:

Check if a module is loaded:

>> ismodule('my_module')
ans = true

Retrieve specific paths within a module:

>> modulepath('my_module','etc')
ans = "/path/to/nelson/modules/my_module/etc"

>> modulepath('my_module','functions')
ans = "/path/to/nelson/modules/my_module/functions"

List all loaded modules with metadata:

>> mods = getmodules()
mods = 
  struct with fields:
    name: {'my_module'  'xml'  ...}
    path: {"/path/to/..."  ...}
    protected: [0  1  ...]
    version: {'1.0.0'  ...}

Summary

  • Nelson's module system is managed by a C++ ModulesManager class that maintains a registry of all loaded modules, storing metadata such as name, path, version, and protection status.
  • Registration occurs through loader.m scripts that call the addmodule built-in, which invokes RegisterModule in modules/modules_manager/src/cpp/ModulesManager.cpp.
  • Initialization happens via etc/startup.m, which typically calls addgateway to link compiled libraries and addpath to expose .m functions.
  • Cleanup is handled by etc/finish.m using rmpath and removegateway.
  • Query functions like ismodule, modulepath, and getmodules provide runtime introspection into the module registry.
  • Custom modules require a specific directory layout (loader.m, etc/startup.m, etc/finish.m, functions/) and can be auto-loaded at startup or manually registered via addmodule.

Frequently Asked Questions

What is the difference between addmodule and addpath in Nelson?

addmodule registers a directory as a formal module within the C++ ModulesManager, creating a persistent entry with metadata (name, path, version, protection status) that survives for the session and can be queried via ismodule and getmodules. In contrast, addpath simply adds a directory to Nelson's function search path for .m files; it does not create a module entry, support versioning, or trigger startup.m/finish.m lifecycle scripts.

How do I protect a custom module from being unloaded?

Protected status is determined by the isprotected flag stored in the ModulesManager vector (see modules/modules_manager/src/cpp/ModulesManager.cpp). When calling addmodule, you cannot directly set this flag from Nelson script—it is typically reserved for core modules distributed with Nelson itself. To prevent accidental modification of your custom module's functions, use the -frozen flag when calling addpath in your startup.m file: addpath(modulepath('my_module','functions'), '-frozen'). This prevents user paths from shadowing your module's functions.

Can I distribute a Nelson module as a standalone package?

Yes. A Nelson module is self-contained within its directory structure (loader.m, etc/, functions/, etc.). You can distribute this directory as a zip file or Git repository. Users simply place it under their modules/ directory, and Nelson's ModulesManager will automatically execute loader.m at startup, registering the module. For convenience, Nelson provides a skeleton generator via just/get_module_skeleton.just, which clones the official template repository to help you package your module correctly with all required boilerplate files.

Where does Nelson store module version information?

Version metadata is read from each module's module.json file via the readVersionFromJson function inside modules/modules_manager/src/cpp/ModulesManager.cpp. When addmodule registers a module, the ModulesManager stores the version string alongside the module name, path, and protection status in its internal vector. You can inspect the version field for all loaded modules by calling getmodules(), which returns a struct array containing the version field for each registered module.

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 →