Skip to content

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

Notifications You must be signed in to change notification settings

wiredwp/robinpath

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

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

Resources

Stars

Watchers

Forks

Packages

No packages published