How the Nelson Type Overload System Implements Custom Behaviors: A Complete Guide

The Nelson type overload system maps function calls to specialized M-files using a runtime lookup that converts expressions like a + b into @ClassName/plus.m based on operand types.

The nelson-lang/nelson interpreter provides a sophisticated type overload system that allows developers to define custom behaviors for operators and functions on user-defined classes. This mechanism relies on a deterministic naming convention, a runtime dispatcher, and configurable compatibility levels to resolve which implementation executes for any given call.

Core Architecture of the Nelson Type Overload System

The implementation centers on three interconnected components that transform a generic function call into a specific method invocation.

Naming Convention and Symbol Generation

The system generates overload symbols using the pattern @ClassName/FunctionName. This deterministic path construction allows the interpreter to locate the correct M-file without ambiguity.

In modules/overload/src/include/OverloadName.hpp, the getOverloadFunctionName function constructs these symbols:

std::string overloadName = "@" + typeName + "/" + functionName;   // e.g., "@complexObj/plus"

This naming convention applies to both named functions and operators. When you write a + b, the parser maps the + operator to the function name plus, then searches for @ClassName/plus.m.

Runtime Dispatch Mechanism

The callOverloadedFunction routine in modules/overload/src/include/OverloadHelpers.hpp serves as the central dispatcher. This function executes several critical steps:

  1. Checks the Evaluator::withOverload flag (default true) and the current OverloadLevelCompatibility setting
  2. Computes the common type of all arguments to determine which class directory to search
  3. Builds the overload symbol using getOverloadFunctionName
  4. Queries FunctionsInMemory to verify the M-file exists
  5. Invokes the user-defined function or, for handle objects, calls HandleGenericObject::invokeMethod

The dispatcher also supports handle objects through a secondary path that invokes C++ methods directly when the overload symbol matches a handle class method.

Module Registration

The overload subsystem registers itself through modules/overload/loader.m, which executes during interpreter startup. This registration ensures the evaluator recognizes the overload subsystem and initializes the necessary function tables.

How Overload Resolution Works at Runtime

When the interpreter encounters a function call with custom types, it follows a deterministic resolution path controlled by compatibility settings.

Compatibility Levels

Nelson provides three overload resolution modes defined in the configuration system (modules/nelson_manager/src/include/NelsonConfiguration.hpp):

  • NLS_OVERLOAD_NONE (0): Disables all overload resolution; only built-in functions execute
  • NLS_OVERLOAD_OBJECT_TYPES_ONLY (1): Resolves overloads only for user-defined class types
  • NLS_OVERLOAD_ALL_TYPES (2): Enables overloads for any type, including built-in primitives

The setOverloadLevelCompatibility method stores the selected mode, which callOverloadedFunction checks via a switch statement (lines 73-94 in OverloadHelpers.hpp) before attempting resolution.

Resolution Sequence

For a call like plus(a, b) where a and b are custom objects:

  1. The parser identifies the function name plus and arguments a, b
  2. The evaluator checks if withOverload is enabled and verifies the compatibility level permits resolution for these types
  3. The system determines the common type (e.g., complexObj)
  4. getOverloadFunctionName constructs @complexObj/plus
  5. The function table searches for modules/overload/examples/complex/@complexObj/plus.m
  6. Upon finding the file, the interpreter executes the M-file with a and b as arguments

Implementing Custom Type Overloads in Nelson

Creating custom behaviors requires organizing M-files into class directories following the @ClassName convention.

Creating a Class with Operator Overloading

To implement a custom complex number class that overloads the + operator:

  1. Create the class directory structure:
% Save as: modules/overload/examples/complex/@complexObj/complexObj.m
function obj = complexObj(r, i)
   obj.r = r;          % real part
   obj.i = i;          % imaginary part
   obj = class(obj, 'complexObj');
end
  1. Implement the addition operator:
% Save as: modules/overload/examples/complex/@complexObj/plus.m
function r = plus(a, b)
   % Custom addition for complexObj instances
   R1 = a.r + b.r;
   R2 = a.i + b.i;
   r = complexObj(R1, R2);
end
  1. Add the class to the path and test:
addpath([nelsonroot(), '/modules/overload/examples/complex']);
a = complexObj(1, 2);
b = complexObj(3, 4);
c = a + b;      % Dispatches to @complexObj/plus.m
disp(c);

Overloading Arithmetic Operators

For a custom numeric wrapper that overloads subtraction:

% @myNumber/myNumber.m – constructor
function obj = myNumber(v)
    obj.value = v;
    obj = class(obj, 'myNumber');
end

% @myNumber/minus.m – binary subtraction overload
function r = minus(a, b)
    r = myNumber(a.value - b.value);
end

Usage:

addpath('path/to/@myNumber');
x = myNumber(10);
y = myNumber(3);
z = x - y;    % Returns myNumber(7)
disp(z.value); % Output: 7

Configuring Overload Behavior

Control the Nelson type overload system through the configuration API to optimize performance or ensure compatibility.

Runtime Configuration

Adjust overload resolution without restarting the interpreter:

% Disable overloads completely for maximum performance
NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_NONE);

% Enable overloads only for user-defined classes (safer)
NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_OBJECT_TYPES_ONLY);

% Enable full overload support (default)
NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_ALL_TYPES);

Temporary Disabling Pattern

For performance-critical sections where built-in behavior is required:

% Save current setting
prevSetting = NelsonConfiguration().getOverloadLevelCompatibility();

% Turn overloads off
NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_NONE);

% Execute performance-critical code with built-in functions only
result = computeIntensiveOperation(data);

% Restore previous setting
NelsonConfiguration().setOverloadLevelCompatibility(prevSetting);

The Evaluator::withOverload flag (stored in modules/interpreter/src/include/Evaluator.hpp) provides an additional internal check that can disable the entire subsystem when set to false.

Summary

The Nelson type overload system enables custom behaviors through a deterministic runtime lookup mechanism:

  • Naming Convention: Overload functions reside in @ClassName/FunctionName.m files, constructed dynamically by getOverloadFunctionName in OverloadName.hpp
  • Runtime Dispatch: The callOverloadedFunction routine in OverloadHelpers.hpp resolves calls by computing common argument types and querying the function table
  • Configuration Control: Three compatibility levels (NLS_OVERLOAD_NONE, NLS_OVERLOAD_OBJECT_TYPES_ONLY, NLS_OVERLOAD_ALL_TYPES) allow fine-grained control via NelsonConfiguration
  • Implementation Pattern: Create class directories, implement methods following the @ClassName/function pattern, and add the directory to the Nelson path

Frequently Asked Questions

How does Nelson resolve which overloaded function to call when arguments have different types?

Nelson computes the common type of all arguments to determine the overload resolution path. The callOverloadedFunction dispatcher in OverloadHelpers.hpp examines the types of all operands, identifies the dominant class, and constructs the overload symbol @ClassName/FunctionName based on that common type. If the arguments belong to different user-defined classes, the left-most operand's type typically determines the dispatch according to the interpreter's type precedence rules.

Can I overload built-in operators like + or - for existing primitive types?

Yes, but only when the NLS_OVERLOAD_ALL_TYPES compatibility level is enabled. By default, Nelson uses NLS_OVERLOAD_OBJECT_TYPES_ONLY, which restricts overloading to user-defined classes. To enable primitive type overloading, call NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_ALL_TYPES). However, overloading built-in primitives can impact performance and may cause unexpected behavior in library code, so use this capability cautiously.

What is the performance impact of using type overloads in Nelson?

The Nelson type overload system introduces a small runtime overhead because every function call must check Evaluator::withOverload, compute the common argument type, construct the overload symbol via getOverloadFunctionName, and query FunctionsInMemory for the M-file existence. For performance-critical sections, you can eliminate this overhead by temporarily setting the compatibility level to NLS_OVERLOAD_NONE using NelsonConfiguration().setOverloadLevelCompatibility(), which bypasses all overload resolution logic entirely.

How do I disable overload resolution for debugging purposes?

You can disable the Nelson type overload system globally by setting the compatibility level to NLS_OVERLOAD_NONE through the configuration API. Execute NelsonConfiguration().setOverloadLevelCompatibility(NLS_OVERLOAD_NONE) to prevent the interpreter from searching for @ClassName/FunctionName M-files and force all calls to use built-in implementations. To re-enable overloads later, restore the previous setting or set it to NLS_OVERLOAD_OBJECT_TYPES_ONLY or NLS_OVERLOAD_ALL_TYPES depending on your requirements.

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 →