A scripting language interpreter with a REPL interface and built-in modules for math, strings, JSON, time, arrays, and more.
Install RobinPath as a dependency in your project:
npm i @wiredwp/robinpathImport and create a RobinPath instance to execute scripts in your application:
import { RobinPath } from '@wiredwp/robinpath';
// Create an interpreter instance
const rp = new RobinPath();
// Execute a script
const result = await rp.executeScript(`
add 10 20
multiply $ 2
`);
console.log('Result:', result); // 60Use executeLine() for REPL-like behavior where state persists between calls:
const rp = new RobinPath();
// First line - sets $result
await rp.executeLine('$result = add 10 20');
console.log(rp.getLastValue()); // 30
// Second line - uses previous result
await rp.executeLine('multiply $result 2');
console.log(rp.getLastValue()); // 60Get and set variables programmatically:
const rp = new RobinPath();
// Set a variable from JavaScript
rp.setVariable('name', 'Alice');
rp.setVariable('age', 25);
// Execute script that uses the variable
await rp.executeScript(`
log "Hello" $name
log "Age:" $age
`);
// Get a variable value
const name = rp.getVariable('name');
console.log(name); // "Alice"Create isolated execution contexts with threads:
const rp = new RobinPath({ threadControl: true });
// Create a new thread
const thread1 = rp.createThread('user-123');
await thread1.executeScript('$count = 10');
// Create another thread with separate variables
const thread2 = rp.createThread('user-456');
await thread2.executeScript('$count = 20');
// Each thread maintains its own state
console.log(thread1.getVariable('count')); // 10
console.log(thread2.getVariable('count')); // 20
// Switch between threads
rp.useThread('user-123');
console.log(rp.currentThread?.getVariable('count')); // 10Extend RobinPath with your own builtin functions:
const rp = new RobinPath();
// Register a simple builtin
rp.registerBuiltin('greet', (args) => {
const name = String(args[0] ?? 'World');
return `Hello, ${name}!`;
});
// Use it in scripts
await rp.executeScript('greet "Alice"');
console.log(rp.getLastValue()); // "Hello, Alice!"Create and register custom modules:
const rp = new RobinPath();
// Register module functions
rp.registerModule('myapp', {
process: (args) => {
const data = args[0];
// Process data...
return processedData;
},
validate: (args) => {
const input = args[0];
return isValid(input);
}
});
// Register function metadata for documentation
rp.registerModuleFunctionMeta('myapp', 'process', {
description: 'Processes input data',
parameters: [
{
name: 'data',
dataType: 'object',
description: 'Data to process',
formInputType: 'json',
required: true
}
],
returnType: 'object',
returnDescription: 'Processed data'
});
// Register module-level metadata
rp.registerModuleInfo('myapp', {
description: 'Custom application module',
methods: ['process', 'validate']
});
// Use in scripts
await rp.executeScript(`
use myapp
myapp.process $data
`);Query available commands for autocomplete or help:
const rp = new RobinPath();
const commands = rp.getAvailableCommands();
console.log(commands.native); // Language keywords (if, def, etc.)
console.log(commands.builtin); // Root-level builtins
console.log(commands.modules); // Available modules
console.log(commands.moduleFunctions); // Module.function names
console.log(commands.userFunctions); // User-defined functionsGet the AST with execution state for debugging or visualization:
const rp = new RobinPath({ threadControl: true });
const thread = rp.createThread('debug');
const script = `
add 5 5
$result = $
if $result > 5
multiply $result 2
endif
`;
const astResult = await thread.getASTWithState(script);
console.log(astResult.ast); // AST with lastValue at each node
console.log(astResult.variables); // Thread and global variables
console.log(astResult.lastValue); // Final result
console.log(astResult.callStack); // Call stack framesCheck if a script needs more input (useful for multi-line input):
const rp = new RobinPath();
const check1 = rp.needsMoreInput('if $x > 5');
console.log(check1); // { needsMore: true, waitingFor: 'endif' }
const check2 = rp.needsMoreInput('if $x > 5\n log "yes"\nendif');
console.log(check2); // { needsMore: false }Handle errors from script execution:
const rp = new RobinPath();
try {
await rp.executeScript('unknown_function 123');
} catch (error) {
console.error('Script error:', error.message);
// "Unknown function: unknown_function"
}Install globally to use the robinpath command:
npm i -g @wiredwp/robinpathOr use it directly with npx:
npx @wiredwp/robinpathStart the interactive REPL:
robinpathOr if installed locally:
npm run cliThis will start an interactive session where you can type commands and see results immediately.
helpor.help- Show help messageexit,quit,.exit,.quit- Exit the REPLclearor.clear- Clear the screen..- Show all available commands as JSON
Multi-line Blocks: The REPL automatically detects incomplete blocks and waits for completion:
> if $x > 5
... log "yes"
... endif
Backslash Line Continuation:
Use \ at the end of a line to continue the command on the next line:
> log "this is a very long message " \
... "that continues on the next line"
The backslash continuation works with any statement type and can be chained across multiple lines.
Thread Management: When thread control is enabled, the prompt shows the current thread and module:
default@math> add 5 5
10
default@math> use clear
Cleared module context
default> thread list
Threads:
- default (current)
- user-123
default> thread use user-123
Switched to thread: user-123
user-123>
Module Context:
The prompt shows the current module when using use:
> use math
Using module: math
default@math> add 5 5
10
default@math> use clear
Cleared module context
default>
Commands are executed by typing the command name followed by arguments:
add 10 20
log "Hello, World!"
multiply 5 3
Variables are prefixed with $:
$name = "Alice"
$age = 25
log $name $age
Use $ to reference the last computed value:
add 10 20
multiply $ 2 # Uses 30 (result of add)
log $ # Prints 60
Assign the last value to a variable by simply referencing it:
add 5 3
$sum # Assigns 8 to $sum
log $sum # Prints 8
Assign the value of one variable to another:
$city = "New York"
$city2 = $city # Copies "New York" to $city2
log $city2 # Prints "New York"
$number1 = 42
$number2 = $number1 # Copies 42 to $number2
$number3 = $number2 # Can chain assignments
RobinPath supports accessing object properties and array elements directly using dot notation and bracket notation:
Property Access:
json.parse '{"name": "John", "age": 30, "address": {"city": "NYC"}}'
$user = $
# Access properties using dot notation
log $user.name # Prints "John"
log $user.age # Prints 30
log $user.address.city # Prints "NYC" (nested property access)
# Use in expressions
if $user.age >= 18
log "Adult"
endif
# Use as function arguments
math.add $user.age 5 # Adds 30 + 5 = 35
Array Indexing:
$arr = range 10 15
log $arr[0] # Prints 10 (first element)
log $arr[2] # Prints 12 (third element)
log $arr[5] # Prints 15 (last element)
# Out of bounds access returns null
log $arr[10] # Prints null
# Use in expressions
if $arr[0] == 10
log "First is 10"
endif
Combined Access: You can combine property access and array indexing:
json.parse '{"items": [{"name": "item1", "price": 10}, {"name": "item2", "price": 20}]}'
$data = $
log $data.items[0].name # Prints "item1"
log $data.items[0].price # Prints 10
log $data.items[1].name # Prints "item2"
# Assign to variables
$firstItem = $data.items[0].name
log $firstItem # Prints "item1"
Error Handling:
- Accessing a property of
nullorundefinedthrows an error:Cannot access property 'propertyName' of null - Accessing a property of a non-object throws an error:
Cannot access property 'propertyName' of <type> - Array indexing on a non-array throws an error:
Cannot access index X of non-array value - Out-of-bounds array access returns
null(doesn't throw)
Note: Assignment targets must be simple variable names. You cannot assign directly to attributes (e.g., $user.name = "Jane" is not supported). Use the set function from the Object module instead:
set $user "name" "Jane" # Use Object.set for attribute assignment
RobinPath includes several built-in reserved methods:
log - Output values:
log "Hello, World!"
log $name $age
log "Result:" $(add 5 5)
obj - Create objects using JSON5 syntax:
# Create empty object
obj
$empty = $
# Create object with JSON5 syntax (unquoted keys, trailing commas allowed)
obj '{name: "John", age: 30}'
$user = $
# Nested objects and arrays
obj '{nested: {key: "value"}, items: [1, 2, 3]}'
$data = $
# JSON5 features: unquoted keys, single quotes, trailing commas
obj '{unquoted: "works", singleQuotes: "also works", trailing: true,}'
$config = $
# Access properties
log $user.name # Prints "John"
log $user.age # Prints 30
log $data.nested.key # Prints "value"
log $data.items[0] # Prints 1
The obj command uses JSON5 syntax, which is more flexible than standard JSON:
- Keys don't need quotes (unless they contain special characters)
- Single quotes are allowed for strings
- Trailing commas are allowed
- Comments are supported (though not shown in examples above)
array - Create arrays from arguments:
# Create empty array
array
$empty = $
# Create array with elements
array 1 2 3
$numbers = $
# Mixed types
array "hello" "world" 42 true
$mixed = $
# Access elements
log $numbers[0] # Prints 1
log $numbers[1] # Prints 2
log $mixed[0] # Prints "hello"
log $mixed[2] # Prints 42
# Use in expressions
array 10 20 30
$values = $
math.add $values[0] $values[1] # Adds 10 + 20 = 30
The array command creates an array from all its arguments. If called without arguments, it returns an empty array [].
assign - Assign a value to a variable (with optional fallback):
# Basic assignment
assign $myVar "hello"
assign $myVar 42
assign $myVar $sourceVar
# Assignment with fallback (3rd parameter used if 2nd is empty/null)
assign $result $maybeEmpty "default value"
assign $count $maybeNull 0
assign $name $maybeEmpty "Unknown"
# Fallback is only used when the value is:
# - null or undefined
# - empty string (after trimming)
# - empty array
# - empty object
empty - Clear/empty a variable:
$myVar = "some value"
empty $myVar
log $myVar # Prints null
$arr = range 1 5
empty $arr
log $arr # Prints null
fallback - Return variable value or fallback if empty/null:
# Return variable value or fallback
$maybeEmpty = null
fallback $maybeEmpty "default value" # Returns "default value"
$maybeEmpty = ""
fallback $maybeEmpty "Unknown" # Returns "Unknown"
$hasValue = "Alice"
fallback $hasValue "Unknown" # Returns "Alice" (fallback not used)
# Without fallback, returns the variable value (even if null)
$maybeEmpty = null
fallback $maybeEmpty # Returns null
The fallback command checks if a variable is empty/null and returns the fallback value if provided. A value is considered empty if it is:
nullorundefined- Empty string (after trimming)
- Empty array
- Empty object
meta - Add metadata to variables or functions:
# Add metadata to a variable
$testVar = 100
meta $testVar description "A variable to store test values"
meta $testVar version 5
meta $testVar author "Test Author"
# Add metadata to a function
def calculate
math.add $1 $2
enddef
meta calculate description "A function to calculate sum"
meta calculate version 1
meta calculate category "math"
The meta command stores arbitrary key-value metadata for variables or functions. The metadata is stored separately and does not affect the variable or function itself.
getMeta - Retrieve metadata from variables or functions:
# Get all metadata for a variable
$testVar = 100
meta $testVar description "A variable"
meta $testVar version 5
getMeta $testVar
$allMeta = $ # Returns {description: "A variable", version: 5}
# Get specific metadata key
getMeta $testVar description # Returns "A variable"
getMeta $testVar version # Returns 5
getMeta $testVar nonexistent # Returns null
# Get all metadata for a function
def calculate
math.add $1 $2
enddef
meta calculate description "A function"
meta calculate version 1
getMeta calculate
$funcMeta = $ # Returns {description: "A function", version: 1}
# Get specific metadata key
getMeta calculate description # Returns "A function"
getMeta calculate version # Returns 1
The getMeta command retrieves metadata:
- With one argument: returns all metadata as an object
- With two arguments: returns the value for the specific key (or
nullif not found) - Returns an empty object
{}if no metadata exists
clear - Clear the last return value ($):
# Clear the last value
math.add 10 20 # $ = 30
clear # $ = null
# Clear after chained operations
math.add 5 5
math.multiply $ 2 # $ = 20
clear # $ = null
# Clear doesn't affect variables
$testVar = 42
math.add 10 20
clear
log $testVar # Still prints 42
log $ # Prints null
The clear command sets the last return value ($) to null. It does not affect variables or any other state.
forget - Hide a variable or function in the current scope:
# Forget a variable in current scope
$testVar = 100
scope
forget $testVar
log $testVar # Prints null (variable is hidden)
endscope
log $testVar # Prints 100 (variable accessible again after scope)
# Forget a function in current scope
def my_function
return 42
enddef
scope
forget my_function
my_function # Error: Function is forgotten in current scope
endscope
my_function # Works normally (function accessible after scope)
# Forget a built-in command in current scope
scope
forget log
log "test" # Error: Built-in command is forgotten in current scope
endscope
log "test" # Works normally (built-in accessible after scope)
# Forget only affects the current scope
$outerVar = 200
scope
forget $outerVar
scope
log $outerVar # Prints 200 (accessible in child scope)
endscope
log $outerVar # Prints null (forgotten in current scope)
endscope
log $outerVar # Prints 200 (accessible again after scope)
The forget command hides a variable or function only in the current scope:
- When a variable is forgotten, accessing it returns
null - When a function or built-in is forgotten, calling it throws an error
- The forget effect only applies to the scope where
forgetwas called - After the scope ends, the variable/function is accessible again
- Child scopes can still access forgotten items from parent scopes
- Useful for temporarily hiding variables or functions to avoid name conflicts
getType - Get the type of a variable:
# Get type of different variables
$str = "hello"
getType $str # Returns "string"
$num = 42
getType $num # Returns "number"
$bool = true
getType $bool # Returns "boolean"
$nullVar = null
getType $nullVar # Returns "null"
$arr = range 1 5
getType $arr # Returns "array"
obj '{name: "John"}'
$obj = $
getType $obj # Returns "object"
The getType command returns the type of a variable as a string. Possible return values:
"string"- String values"number"- Numeric values"boolean"- Boolean values (true/false)"null"- Null values"array"- Array values"object"- Object values (including empty objects)"undefined"- Undefined values (rare in RobinPath)
Isolated Scopes with Parameters: Scopes can be declared with parameters to create isolated execution contexts that don't inherit from parent scopes:
# Regular scope (inherits from parent)
$parentVar = 100
scope
log $parentVar # Prints 100 (can access parent)
$localVar = 200
endscope
# Isolated scope with parameters (no parent access)
$outerVar = 300
scope $a $b
log $outerVar # Prints null (cannot access parent)
log $a # Prints null (parameter, defaults to null)
log $b # Prints null (parameter, defaults to null)
$localVar = 400
log $localVar # Prints 400 (local variable works)
endscope
log $outerVar # Prints 300 (unchanged)
# Isolated scope with multiple parameters
scope $x $y $z
log $x # Prints null (first parameter)
log $y # Prints null (second parameter)
log $z # Prints null (third parameter)
$local = 500
endscope
# Nested isolated scopes
scope $x
log $x # Prints null (parameter)
scope $y
log $x # Prints null (cannot access outer scope parameter)
log $y # Prints null (own parameter)
endscope
endscope
When a scope is declared with parameters:
- The scope is isolated - it cannot access variables from parent scopes or globals
- Only the declared parameters and variables created inside the scope are accessible
- Parameters default to
nullif not provided - Variables created inside an isolated scope don't leak to parent scopes
- Useful for creating clean, isolated execution contexts
Lines starting with # are comments:
# This is a comment
add 1 2 # Inline comment
Inline if:
if $age >= 18 then log "Adult"
Block if:
if $score >= 90
log "Grade: A"
elseif $score >= 80
log "Grade: B"
else
log "Grade: F"
endif
For loops:
for $i in range 1 5
log "Iteration:" $i
endfor
For loop with array:
$numbers = range 10 12
for $num in $numbers
log "Number:" $num
endfor
Break statement:
Use break to exit a for loop early:
for $i in range 1 10
if $i == 5
break # Exits the loop when $i equals 5
endif
log "Iteration:" $i
endfor
# This will only log iterations 1-4
The break statement:
- Exits the innermost loop immediately
- Can only be used inside a
forloop (will throw an error if used outside) - Preserves the last value (
$) from the iteration wherebreakwas executed - Works with nested loops (only breaks the innermost loop)
Example with nested loops:
for $i in range 1 3
log "Outer:" $i
for $j in range 1 5
if $j == 3
break # Only breaks the inner loop
endif
log "Inner:" $j
endfor
endfor
# Outer loop continues, inner loop breaks at $j == 3
Define custom functions:
def greet
$1
$2
log "Hello" $1
log "Your age is" $2
add $2 1
enddef
greet "Alice" 25
log "Next year:" $ # Prints 26
Function Definition with Named Parameters: You can define functions with named parameter aliases:
def greet $name $age
log "Hello" $name
log "Your age is" $age
# $name is an alias for $1, $age is an alias for $2
# You can still use $1, $2, etc.
enddef
greet "Alice" 25
Function Call Syntax: RobinPath supports two ways to call functions:
CLI-style (existing):
greet "Alice" 25
math.add 10 20 key1=value1 key2=value2
Parenthesized style (new, recommended for complex calls):
greet("Alice" 25)
math.add(10 20 key1=value1 key2=value2)
# Multi-line parenthesized calls (recommended for longer calls)
greet(
"Alice"
25
)
math.add(
10
20
key1=value1
key2=value2
)
Both forms are equivalent. The parenthesized form is optimized for readability, multiline usage, and IDE tooling.
Named Arguments:
Functions can accept named arguments using key=value syntax:
def process $data
log "Data:" $data
log "Retries:" $args.retries
log "Timeout:" $args.timeout
log "Verbose:" $args.verbose
enddef
# CLI-style with named arguments
process $myData retries=3 timeout=30 verbose=true
# Parenthesized style with named arguments
process($myData retries=3 timeout=30 verbose=true)
# Multi-line parenthesized call
process(
$myData
retries=3
timeout=30
verbose=true
)
Accessing Arguments Inside Functions: Inside a function, you can access arguments in three ways:
- Numeric position variables:
$1,$2,$3, etc. - Named parameter aliases:
$name,$age, etc. (if defined in function signature) - Named argument map:
$args.keyNamefor named arguments
def example $a $b $c
# Positional arguments
log "First:" $1 # Same as $a
log "Second:" $2 # Same as $b
log "Third:" $3 # Same as $c
# Named parameter aliases
log "a:" $a # Same as $1
log "b:" $b # Same as $2
log "c:" $c # Same as $3
# Named arguments (from key=value in function call)
log "key:" $args.key
log "flag:" $args.flag
enddef
example("x" "y" "z" key=1 flag=true)
Parameter Binding Rules:
$a,$b,$care aliases for$1,$2,$3respectively- If more positional args are passed than declared, they're still accessible as
$4,$5, etc. - If fewer arguments are passed than declared, missing ones are treated as
null - Named arguments are always accessible via
$args.<name> - If a named argument has the same name as a parameter, the parameter variable refers to the positional argument, and
$args.<name>refers to the named argument
Functions can return values in two ways:
Implicit return (last value): Functions automatically return the last computed value:
def sum_and_double
add $1 $2
multiply $ 2
enddef
sum_and_double 10 20
log $ # Prints 60
Explicit return statement:
Use the return statement to return a value and terminate function execution:
def calculate
if $1 > 10
return 100
endif
multiply $1 2
enddef
calculate 5
log $ # Prints 10
calculate 15
log $ # Prints 100 (returned early)
The return statement can return:
- A literal value:
return 42orreturn "hello" - A variable:
return $result - The last value (
$):return(no value specified) - A subexpression:
return $(add 5 5)
Return in global scope:
The return statement also works in global scope to terminate script execution:
log "This will execute"
return "done"
log "This will not execute"
Use modules to access specialized functions:
use math
math.add 5 10
use string
string.length "hello"
string.toUpperCase "world"
Available Modules:
math- Mathematical operations (add, subtract, multiply, divide, etc.)string- String manipulation (length, substring, replace, etc.)json- JSON parsing and stringification (parse, stringify, isValid)object- Object manipulation operations (get, set, keys, values, entries, merge, clone) - Global moduletime- Date and time operationsrandom- Random number generationarray- Array operations (push, pop, slice, etc.)
Object Module (Global):
The Object module provides object manipulation functions and is available globally (no use command needed):
# Create objects using obj command (JSON5 syntax)
obj '{name: "John", age: 30}'
$user = $
# Or use json.parse for standard JSON
json.parse '{"name": "John", "age": 30}'
$user2 = $
# Get a value using dot-notation path
get $user "name" # Returns "John"
get $user "age" # Returns 30
# Alternative: Use attribute access syntax (see Attribute Access section)
# $user.name # Also returns "John"
# $user.age # Also returns 30
# Set a value using dot-notation path
set $user "city" "NYC" # Sets user.city = "NYC"
get $user "city" # Returns "NYC"
# Get object keys, values, and entries
keys $user # Returns ["name", "age", "city"]
values $user # Returns ["John", 30, "NYC"]
entries $user # Returns [["name", "John"], ["age", 30], ["city", "NYC"]]
# Merge objects
obj '{a: 1}'
$obj1 = $
obj '{b: 2}'
$obj2 = $
merge $obj1 $obj2 # Returns {a: 1, b: 2}
# Clone an object (deep copy)
clone $user # Returns a deep copy of $user
Use $( ... ) for inline subexpressions to evaluate code and use its result:
# Single-line subexpression
log "Result:" $(add 10 20)
# Multi-line subexpression
$result = $(
add 5 5
multiply $ 2
)
log $result # Prints 20
# Nested subexpressions
$value = $(add $(multiply 2 3) $(add 1 1))
log $value # Prints 8
# Subexpression in conditionals
if $(math.add 5 5) == 10
log "Equal to 10"
endif
# Function calls in subexpressions use parenthesized syntax
if $(test.isBigger $value 5)
log "Value is bigger than 5"
endif
Variable Scope in Subexpressions:
Subexpressions can read and modify variables from parent scopes. If a variable exists in a parent scope, the assign command will modify that variable:
$testVar = 10
$result = $(
assign $testVar 42 # This modifies the parent $testVar
math.add $testVar 8 # Uses 42, returns 50
)
log $result # Prints 50
log $testVar # Prints 42 (modified by subexpression)
If a variable doesn't exist in parent scopes, it will be created in the global scope:
$result = $(
assign $newVar 100 # Creates $newVar in global scope
math.add $newVar 1
)
log $result # Prints 101
log $newVar # Prints 100 (created in global scope)
Note: Functions (def/enddef) maintain their own local scope and do not modify parent variables.
Strings can use single quotes, double quotes, or backticks:
$msg1 = "Hello"
$msg2 = 'World'
$msg3 = `Template`
Automatic String Concatenation: Adjacent string literals are automatically concatenated:
$long = "hello " "world " "from RobinPath"
log $long # Prints "hello world from RobinPath"
This is particularly useful with backslash line continuation (see below).
Numbers can be integers or decimals:
$int = 42
$float = 3.14
The backslash (\) allows a single logical command to be written across multiple physical lines. If a line's last non-whitespace character is a backslash, that line is continued onto the next line.
Basic Usage:
log "this is a very long message " \
"that continues on the next line"
Multiple Continuations:
do_something $a $b $c \
$d $e $f \
$g $h $i
With String Concatenation:
$long = "hello " \
"world " \
"from RobinPath"
# Becomes: $long = "hello " "world " "from RobinPath"
# Which is automatically concatenated to: "hello world from RobinPath"
With Function Calls:
fn($a $b $c \
key1=1 \
key2=2)
With If Conditions:
if $a > 0 && \
$b < 10 && \
$c == "ok"
log "conditions met"
endif
With Assignments:
$query = "SELECT * FROM users " \
"WHERE active = 1 " \
"ORDER BY created_at DESC"
Rules:
- The backslash must be the last non-whitespace character on the line
- The newline and any leading whitespace on the next line are replaced with a single space
- The continuation ends at the first line that doesn't end with
\ - Works with any statement type (assignments, function calls, conditionals, etc.)
You can extend RobinPath by creating your own custom modules. Modules provide a way to organize related functions and make them available through the use command.
A module consists of three main parts:
- Functions - The actual function implementations
- Function Metadata - Documentation and type information for each function
- Module Metadata - Overall module description and method list
Create a new TypeScript file in src/modules/ directory, for example src/modules/MyModule.ts:
import type {
BuiltinHandler,
FunctionMetadata,
ModuleMetadata,
ModuleAdapter
} from '../index';
/**
* MyModule for RobinPath
* Provides custom functionality
*/
// 1. Define your functions
export const MyModuleFunctions: Record<string, BuiltinHandler> = {
greet: (args) => {
const name = String(args[0] ?? 'World');
return `Hello, ${name}!`;
},
double: (args) => {
const num = Number(args[0]) || 0;
return num * 2;
},
// Functions can be async
delay: async (args) => {
const ms = Number(args[0]) || 1000;
await new Promise(resolve => setTimeout(resolve, ms));
return `Waited ${ms}ms`;
}
};
// 2. Define function metadata (for documentation and type checking)
export const MyModuleFunctionMetadata: Record<string, FunctionMetadata> = {
greet: {
description: 'Greets a person by name',
parameters: [
{
name: 'name',
dataType: 'string',
description: 'Name of the person to greet',
formInputType: 'text',
required: false,
defaultValue: 'World'
}
],
returnType: 'string',
returnDescription: 'Greeting message',
example: 'mymodule.greet "Alice" # Returns "Hello, Alice!"'
},
double: {
description: 'Doubles a number',
parameters: [
{
name: 'value',
dataType: 'number',
description: 'Number to double',
formInputType: 'number',
required: true
}
],
returnType: 'number',
returnDescription: 'The input number multiplied by 2',
example: 'mymodule.double 5 # Returns 10'
},
delay: {
description: 'Waits for a specified number of milliseconds',
parameters: [
{
name: 'ms',
dataType: 'number',
description: 'Number of milliseconds to wait',
formInputType: 'number',
required: true
}
],
returnType: 'string',
returnDescription: 'Confirmation message',
example: 'mymodule.delay 1000 # Waits 1 second'
}
};
// 3. Define module metadata
export const MyModuleModuleMetadata: ModuleMetadata = {
description: 'Custom module providing greeting and utility functions',
methods: [
'greet',
'double',
'delay'
]
};
// 4. Create and export the module adapter
const MyModule: ModuleAdapter = {
name: 'mymodule',
functions: MyModuleFunctions,
functionMetadata: MyModuleFunctionMetadata,
moduleMetadata: MyModuleModuleMetadata
};
export default MyModule;In src/index.ts, import your module and add it to the NATIVE_MODULES array:
// Add import at the top with other module imports
import MyModule from './modules/MyModule';
// Add to NATIVE_MODULES array (around line 2504)
private static readonly NATIVE_MODULES: ModuleAdapter[] = [
MathModule,
StringModule,
JsonModule,
TimeModule,
RandomModule,
ArrayModule,
TestModule,
MyModule // Add your module here
];Once registered, you can use your module in RobinPath scripts:
use mymodule
mymodule.greet "Alice"
mymodule.double 7
mymodule.delay 500
-
Function Signature: Functions must match the
BuiltinHandlertype:(args: Value[]) => Value | Promise<Value>
-
Argument Handling: Always handle missing or undefined arguments:
const value = args[0] ?? defaultValue; const num = Number(args[0]) || 0; // For numbers const str = String(args[0] ?? ''); // For strings
-
Error Handling: Throw descriptive errors:
if (num < 0) { throw new Error('Number must be non-negative'); }
-
Async Functions: Functions can return
Promise<Value>for async operations:asyncFunction: async (args) => { await someAsyncOperation(); return result; }
-
Parameter Metadata: Each parameter should include:
name: Parameter namedataType: One of'string' | 'number' | 'boolean' | 'object' | 'array' | 'null' | 'any'description: Human-readable descriptionformInputType: UI input type (seeFormInputTypein code)required: Whether parameter is required (defaults totrue)defaultValue: Optional default value
-
Function Metadata: Each function should include:
description: What the function doesparameters: Array of parameter metadatareturnType: Return data typereturnDescription: What the function returnsexample: Optional usage example
-
Module Metadata: Should include:
description: Overall module descriptionmethods: Array of all function names in the module
Here's a complete example of a utility module:
import type {
BuiltinHandler,
FunctionMetadata,
ModuleMetadata,
ModuleAdapter
} from '../index';
export const UtilFunctions: Record<string, BuiltinHandler> = {
reverse: (args) => {
const str = String(args[0] ?? '');
return str.split('').reverse().join('');
},
capitalize: (args) => {
const str = String(args[0] ?? '');
if (str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
},
isEmpty: (args) => {
const value = args[0];
if (value === null || value === undefined) return true;
if (typeof value === 'string') return value.length === 0;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === 'object') return Object.keys(value).length === 0;
return false;
}
};
export const UtilFunctionMetadata: Record<string, FunctionMetadata> = {
reverse: {
description: 'Reverses a string',
parameters: [
{
name: 'str',
dataType: 'string',
description: 'String to reverse',
formInputType: 'text',
required: true
}
],
returnType: 'string',
returnDescription: 'Reversed string',
example: 'util.reverse "hello" # Returns "olleh"'
},
// ... other function metadata
};
export const UtilModuleMetadata: ModuleMetadata = {
description: 'Utility functions for common operations',
methods: ['reverse', 'capitalize', 'isEmpty']
};
const UtilModule: ModuleAdapter = {
name: 'util',
functions: UtilFunctions,
functionMetadata: UtilFunctionMetadata,
moduleMetadata: UtilModuleMetadata
};
export default UtilModule;- Naming: Use lowercase module names (e.g.,
mymodule,util,custom) - Organization: Group related functions together
- Documentation: Provide clear descriptions and examples
- Error Messages: Use descriptive error messages
- Type Safety: Validate input types and handle edge cases
- Consistency: Follow the same patterns as existing modules
After creating your module, test it in the REPL:
npm run cliThen try:
use mymodule
mymodule.greet "Test"
You can also check available modules:
module list
add 10 20
$result
log "Sum:" $result
multiply $result 2
log "Double:" $
# Direct assignment
$name = "Alice"
$age = 25
# Variable-to-variable assignment
$name2 = $name
$age2 = $age
# Chained assignments
$original = 100
$copy1 = $original
$copy2 = $copy1
log $name2 $age2 # Prints "Alice" 25
log $copy2 # Prints 100
assign command:
# Basic assignment
assign $result "success"
assign $count 42
# Assignment with fallback
$maybeEmpty = null
assign $result $maybeEmpty "default" # $result = "default"
$maybeEmpty = ""
assign $name $maybeEmpty "Unknown" # $name = "Unknown"
$hasValue = "Alice"
assign $name $hasValue "Unknown" # $name = "Alice" (fallback not used)
empty command:
$data = "some data"
empty $data
log $data # Prints null
$arr = range 1 5
empty $arr
log $arr # Prints null
fallback command:
# Use fallback when variable might be empty
$name = null
$displayName = fallback $name "Guest"
log $displayName # Prints "Guest"
$name = "Alice"
$displayName = fallback $name "Guest"
log $displayName # Prints "Alice"
# Chain with other operations
$count = null
add fallback $count 0 10 # Adds 0 + 10 = 10
$age = 18
$citizen = "yes"
if ($age >= 18) && ($citizen == "yes") then log "Loan approved"
$arr = range 1 5
for $num in $arr
log "Number:" $num
endfor
# Create objects using obj command (JSON5 syntax - more flexible)
obj '{name: "John", age: 30, address: {city: "NYC"}, scores: [85, 90, 95]}'
$user = $
# Or use json.parse for standard JSON
json.parse '{"name": "John", "age": 30, "address": {"city": "NYC"}, "scores": [85, 90, 95]}'
$user2 = $
# Access properties using dot notation
log "Name:" $user.name
log "Age:" $user.age
log "City:" $user.address.city
# Access array elements
log "First score:" $user.scores[0]
log "Last score:" $user.scores[2]
# Use in conditionals
if $user.age >= 18
log "Adult user"
endif
# Use in calculations
math.add $user.scores[0] $user.scores[1]
log "Sum of first two scores:" $
# Create objects with obj command (JSON5 features)
obj '{unquoted: "keys work", trailing: "comma", allowed: true,}'
$config = $
log $config.unquoted # Prints "keys work"
Implicit return:
def calculate
multiply $1 $2
add $ 10
enddef
calculate 5 3
log "Result:" $ # Prints 25
Explicit return:
def calculate
if $1 > 10
return 100
endif
multiply $1 $2
add $ 10
enddef
calculate 15 3
log "Result:" $ # Prints 100 (returned early)
calculate 5 3
log "Result:" $ # Prints 25
Run the test suite:
npm testThis will execute the test script located in test/test.rp.
Build the project:
npm run build