npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@wiredwp/robinpath

v0.20.9

Published

RobinPath - A lightweight, fast, and easy-to-use scripting language for automation and data processing.

Downloads

7,650

Readme

RobinPath

A scripting language interpreter with a REPL interface and built-in modules for math, strings, JSON, time, arrays, and more.

Installation

Install RobinPath as a dependency in your project:

npm i @wiredwp/robinpath

Integration

Basic Usage

Import 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); // 60

REPL Mode (Persistent State)

Use 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()); // 60

Working with Variables

Get 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"

Threads (Isolated Execution Contexts)

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')); // 10

Registering Custom Functions

Extend 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!"

Registering Custom Modules

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
`);

Getting Available Commands

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 functions

AST with Execution State

Get 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 frames

Checking for Incomplete Blocks

Check 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 }

Error Handling

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"
}

CLI Usage

Installation

Install globally to use the robinpath command:

npm i -g @wiredwp/robinpath

Or use it directly with npx:

npx @wiredwp/robinpath

Starting the REPL

Start the interactive REPL:

robinpath

Or if installed locally:

npm run cli

This will start an interactive session where you can type commands and see results immediately.

REPL Commands

  • help or .help - Show help message
  • exit, quit, .exit, .quit - Exit the REPL
  • clear or .clear - Clear the screen
  • .. - Show all available commands as JSON

REPL Features

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>

Basic Syntax

Commands

Commands are executed by typing the command name followed by arguments:

add 10 20
log "Hello, World!"
multiply 5 3

Variables

Variables are prefixed with $:

$name = "Alice"
$age = 25
log $name $age

Last Value Reference

Use $ to reference the last computed value:

add 10 20
multiply $ 2    # Uses 30 (result of add)
log $           # Prints 60

Shorthand Assignment

Assign the last value to a variable by simply referencing it:

add 5 3
$sum            # Assigns 8 to $sum
log $sum        # Prints 8

Variable-to-Variable Assignment

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

Attribute Access and Array Indexing

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 null or undefined throws 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

Native Reserved Methods

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:

  • null or undefined
  • 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 null if 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 forget was 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 null if not provided
  • Variables created inside an isolated scope don't leak to parent scopes
  • Useful for creating clean, isolated execution contexts

Comments

Lines starting with # are comments:

# This is a comment
add 1 2  # Inline comment

Conditionals

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

Loops

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 for loop (will throw an error if used outside)
  • Preserves the last value ($) from the iteration where break was 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

Functions

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:

  1. Numeric position variables: $1, $2, $3, etc.
  2. Named parameter aliases: $name, $age, etc. (if defined in function signature)
  3. Named argument map: $args.keyName for 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, $c are aliases for $1, $2, $3 respectively
  • 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 42 or return "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"

Modules

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 module
  • time - Date and time operations
  • random - Random number generation
  • array - 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

Inline Subexpressions

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.

String Literals

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

Numbers can be integers or decimals:

$int = 42
$float = 3.14

Backslash Line Continuation

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.)

Creating Custom Modules

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.

Module Structure

A module consists of three main parts:

  1. Functions - The actual function implementations
  2. Function Metadata - Documentation and type information for each function
  3. Module Metadata - Overall module description and method list

Step-by-Step Guide

1. Create a Module File

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;

2. Register the Module

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
];

3. Use Your Module

Once registered, you can use your module in RobinPath scripts:

use mymodule
mymodule.greet "Alice"
mymodule.double 7
mymodule.delay 500

Function Implementation Guidelines

  1. Function Signature: Functions must match the BuiltinHandler type:

    (args: Value[]) => Value | Promise<Value>
  2. 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
  3. Error Handling: Throw descriptive errors:

    if (num < 0) {
        throw new Error('Number must be non-negative');
    }
  4. Async Functions: Functions can return Promise<Value> for async operations:

    asyncFunction: async (args) => {
        await someAsyncOperation();
        return result;
    }

Metadata Guidelines

  1. Parameter Metadata: Each parameter should include:

    • name: Parameter name
    • dataType: One of 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null' | 'any'
    • description: Human-readable description
    • formInputType: UI input type (see FormInputType in code)
    • required: Whether parameter is required (defaults to true)
    • defaultValue: Optional default value
  2. Function Metadata: Each function should include:

    • description: What the function does
    • parameters: Array of parameter metadata
    • returnType: Return data type
    • returnDescription: What the function returns
    • example: Optional usage example
  3. Module Metadata: Should include:

    • description: Overall module description
    • methods: Array of all function names in the module

Example: Complete Custom 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;

Best Practices

  1. Naming: Use lowercase module names (e.g., mymodule, util, custom)
  2. Organization: Group related functions together
  3. Documentation: Provide clear descriptions and examples
  4. Error Messages: Use descriptive error messages
  5. Type Safety: Validate input types and handle edge cases
  6. Consistency: Follow the same patterns as existing modules

Testing Your Module

After creating your module, test it in the REPL:

npm run cli

Then try:

use mymodule
mymodule.greet "Test"

You can also check available modules:

module list

Examples

Basic Math

add 10 20
$result
log "Sum:" $result

multiply $result 2
log "Double:" $

Variable Assignment

# 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

Using assign and empty Commands

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

Conditional Logic

$age = 18
$citizen = "yes"
if ($age >= 18) && ($citizen == "yes") then log "Loan approved"

Working with Arrays

$arr = range 1 5
for $num in $arr
  log "Number:" $num
endfor

Working with Objects and Attribute Access

# 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"

Function with Return Value

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

Testing

Run the test suite:

npm test

This will execute the test script located in test/test.rp.

Building

Build the project:

npm run build