Skip to content

Core Module API Reference

The core module (jsl.core) provides the fundamental data structures, evaluator, and environment management for JSL.

Overview

The jsl.core module provides the fundamental data structures that represent the state of a JSL program: Env for environments and Closure for functions. These are the building blocks used by the Evaluator and managed by the JSLRunner.

Classes

Env

The environment class manages variable bindings and scope chains.

jsl.core.Env(bindings=None, parent=None)

Represents a JSL environment - a scope containing variable bindings.

Environments form a chain: each environment has an optional parent. When looking up a variable, we search the current environment first, then its parent, and so on until we find it or reach the root.

Source code in jsl/core.py
def __init__(self, bindings: Optional[Dict[str, Any]] = None, parent: Optional['Env'] = None):
    self.bindings = bindings or {}
    self.parent = parent
    # Prelude metadata (set by make_prelude)
    self._prelude_id = None
    self._prelude_version = None
    self._is_prelude = False

__contains__(name)

Check if a variable exists in this environment or its parents.

Source code in jsl/core.py
def __contains__(self, name: str) -> bool:
    """Check if a variable exists in this environment or its parents."""
    if name in self.bindings:
        return True
    elif self.parent:
        return name in self.parent
    else:
        return False

__eq__(other)

Check if two environments are equal.

Source code in jsl/core.py
def __eq__(self, other: Any) -> bool:
    """Check if two environments are equal."""
    if not isinstance(other, Env):
        return False

    # Check prelude compatibility
    if self._is_prelude and other._is_prelude:
        # Both are preludes - compare by ID
        return self._prelude_id == other._prelude_id
    elif self._is_prelude or other._is_prelude:
        # One is prelude, other isn't - not equal
        return False

    # Get all bindings from both environments (including parents)
    self_bindings = self.to_dict()
    other_bindings = other.to_dict()

    # Check if they have the same keys
    if set(self_bindings.keys()) != set(other_bindings.keys()):
        return False

    # Check if all values are equal
    for key in self_bindings:
        self_val = self_bindings[key]
        other_val = other_bindings[key]

        # Special handling for Closures - compare structure not identity
        if isinstance(self_val, Closure) and isinstance(other_val, Closure):
            if not self._closures_equal(self_val, other_val):
                return False
        elif callable(self_val) and callable(other_val):
            # For callable functions (like prelude), just check they're both callable
            # Can't really compare lambdas/functions for equality in Python
            continue
        elif self_val != other_val:
            return False

    return True

content_hash()

Generate a content-addressable hash with cycle detection.

Source code in jsl/core.py
def content_hash(self) -> str:
    """Generate a content-addressable hash with cycle detection."""
    # Thread-local cycle detection for rock-solid safety
    import threading
    if not hasattr(Env, '_cycle_detection'):
        Env._cycle_detection = threading.local()

    if not hasattr(Env._cycle_detection, 'computing'):
        Env._cycle_detection.computing = set()

    env_id = id(self)
    if env_id in Env._cycle_detection.computing:
        # Cycle detected - return deterministic placeholder
        return f"cycle_{env_id:016x}"

    # Add to cycle detection set
    Env._cycle_detection.computing.add(env_id) 

    try:
        canonical = {
            "bindings": self._serialize_bindings(),
            "parent_hash": self.parent.content_hash() if self.parent else None
        }
        # Convert to string - handle special cases
        try:
            content = json.dumps(canonical, sort_keys=True)
        except (TypeError, ValueError):
            # If we can't serialize (due to complex objects), use a fallback
            # This can happen when bindings contain data structures with Closures
            content = str(sorted(canonical.get("bindings", {}).keys())) + str(canonical.get("parent_hash"))
        return hashlib.sha256(content.encode()).hexdigest()[:16]

    finally:
        # Always clean up, even on exceptions
        Env._cycle_detection.computing.discard(env_id)

        # Clean up thread-local storage when empty
        if not Env._cycle_detection.computing:
            delattr(Env._cycle_detection, 'computing')

deepcopy()

Create a deep copy of this environment, including all parents.

Source code in jsl/core.py
def deepcopy(self) -> 'Env':
    """Create a deep copy of this environment, including all parents."""
    # First, gather all bindings from this env and parents
    all_bindings = self.to_dict()

    # Create a mapping of old closures to new closures to handle cycles
    closure_map = {}

    # Create new environment with copied bindings
    new_env = Env()

    # Copy prelude metadata if present
    new_env._prelude_id = self._prelude_id
    new_env._prelude_version = self._prelude_version
    new_env._is_prelude = self._is_prelude

    # First pass: copy non-closure values
    for name, value in all_bindings.items():
        if not isinstance(value, Closure):
            # For non-closures, just copy the value
            if isinstance(value, (list, dict)):
                import copy
                new_env.bindings[name] = copy.deepcopy(value)
            else:
                new_env.bindings[name] = value

    # Second pass: copy closures with updated env references
    for name, value in all_bindings.items():
        if isinstance(value, Closure):
            # Deep copy the closure with the new environment
            new_closure = value.deepcopy(env=new_env)
            new_env.bindings[name] = new_closure
            closure_map[id(value)] = new_closure

    return new_env

define(name, value)

Define a variable in this environment.

Source code in jsl/core.py
def define(self, name: str, value: Any) -> None:
    """Define a variable in this environment."""
    # Prevent modification of immutable preludes
    if self._is_prelude:
        raise JSLError("Cannot modify prelude environment. Use extend() to create a new environment.")
    self.bindings[name] = value

extend(new_bindings)

Create a new environment that extends this one with additional bindings.

Source code in jsl/core.py
def extend(self, new_bindings: Dict[str, Any]) -> 'Env':
    """Create a new environment that extends this one with additional bindings."""
    return Env(new_bindings, parent=self)

get(name)

Look up a variable in this environment or its parents.

Source code in jsl/core.py
def get(self, name: str) -> Any:
    """Look up a variable in this environment or its parents."""
    if name in self.bindings:
        return self.bindings[name]
    elif self.parent:
        return self.parent.get(name)
    else:
        raise SymbolNotFoundError(f"Symbol '{name}' not found")

to_dict()

Convert environment bindings to a dictionary (for serialization).

Source code in jsl/core.py
def to_dict(self) -> Dict[str, Any]:
    """Convert environment bindings to a dictionary (for serialization)."""
    result = {}
    if self.parent:
        result.update(self.parent.to_dict())
    result.update(self.bindings)
    return result

Key Concepts

  • Scope Chain: When looking up a variable, if it's not found in the current Env, the search continues up to its parent, and so on, until the root prelude is reached.
  • Immutability: Methods like extend create a new child environment rather than modifying the parent, preserving functional purity.
from jsl.core import Env

# Create a new environment
env = Env({"x": 10, "y": 20})

# Create a child environment that inherits from the parent
child_env = env.extend({"z": 30})

# Variable resolution follows the chain
print(child_env.get("x"))  # 10 (from parent)
print(child_env.get("z"))  # 30 (from child)

Closure

Represents a user-defined function with captured lexical environment.

jsl.core.Closure(params, body, env) dataclass

Represents a JSL function (closure).

A closure captures three things: 1. The parameter names it expects 2. The body expression to evaluate when called 3. The environment where it was defined (lexical scoping)

__call__(evaluator, args)

Apply this closure to the given arguments.

Source code in jsl/core.py
def __call__(self, evaluator: 'Evaluator', args: List[JSLValue]) -> JSLValue:
    """Apply this closure to the given arguments."""
    if len(args) != len(self.params):
        raise JSLTypeError(f"Function expects {len(self.params)} arguments, got {len(args)}")

    # Create new environment extending the closure's captured environment
    call_env = self.env.extend(dict(zip(self.params, args)))
    return evaluator.eval(self.body, call_env)

deepcopy(env=None)

Create a deep copy of this closure.

Parameters:

Name Type Description Default
env Optional[Env]

Optional environment to use for the copy. If not provided, deep copies the closure's environment.

None
Source code in jsl/core.py
def deepcopy(self, env: Optional['Env'] = None) -> 'Closure':
    """
    Create a deep copy of this closure.

    Args:
        env: Optional environment to use for the copy. If not provided,
             deep copies the closure's environment.
    """
    # Deep copy the body
    def copy_expr(expr):
        if isinstance(expr, list):
            return [copy_expr(item) for item in expr]
        elif isinstance(expr, dict):
            return {k: copy_expr(v) for k, v in expr.items()}
        else:
            return expr

    new_body = copy_expr(self.body)
    new_params = self.params[:]  # Copy params list

    # Use provided env or deep copy the closure's env
    if env is not None:
        new_env = env
    else:
        new_env = self.env.deepcopy() if self.env else None

    return Closure(new_params, new_body, new_env)

Closures capture their defining environment:

from jsl.core import Closure, Env

# Create an environment that the closure will capture
env = Env({"multiplier": 3})

# Create a closure that captures the 'multiplier' variable from its environment
closure = Closure(
    params=["x"],
    body=["*", "multiplier", "x"],
    env=env
)

# The closure remembers the 'multiplier' value

Evaluator

The main JSL expression evaluator:

jsl.core.Evaluator(host_dispatcher=None, resource_limits=None, host_gas_policy=None)

The core JSL evaluator - recursive evaluation engine.

This is a clean, elegant reference implementation that uses traditional recursive tree-walking to evaluate S-expressions. It serves as the specification for JSL's semantics.

Characteristics: - Simple and easy to understand - Direct mapping from S-expressions to evaluation - Perfect for learning and testing JSL semantics - Limited by Python's recursion depth for deep expressions

For production use with resumption and better performance, use the stack-based evaluator which compiles to JPN (JSL Postfix Notation).

Source code in jsl/core.py
def __init__(self, host_dispatcher: Optional[HostDispatcher] = None, 
             resource_limits: Optional[ResourceLimits] = None,
             host_gas_policy: Optional['HostGasPolicy'] = None):
    self.host = host_dispatcher or HostDispatcher()
    self.resources = ResourceBudget(resource_limits, host_gas_policy) if resource_limits else None

eval(expr, env)

Evaluate a JSL expression in the given environment.

This is a pure recursive evaluator without resumption support. For resumable evaluation, use the stack-based evaluator.

Source code in jsl/core.py
def eval(self, expr: JSLExpression, env: Env) -> JSLValue:
    """
    Evaluate a JSL expression in the given environment.

    This is a pure recursive evaluator without resumption support.
    For resumable evaluation, use the stack-based evaluator.
    """
    # Resource checking
    if self.resources:
        # Check time periodically
        self.resources.check_time()

        # Consume gas based on expression type
        if isinstance(expr, (int, float, bool)) or expr is None:
            self.resources.consume_gas(GasCost.LITERAL)
        elif isinstance(expr, str):
            if expr.startswith("@"):
                self.resources.consume_gas(GasCost.LITERAL)
            else:
                self.resources.consume_gas(GasCost.VARIABLE)
        elif isinstance(expr, dict):
            self.resources.consume_gas(GasCost.DICT_CREATE + 
                                      len(expr) * GasCost.DICT_PER_ITEM)

    # Literals: numbers, booleans, null, objects
    if isinstance(expr, (int, float, bool)) or expr is None:
        return expr

    # Objects: evaluate both keys and values, keys must be strings
    if isinstance(expr, dict):
        return self._eval_dict(expr, env)

    # Strings: variables or string literals
    if isinstance(expr, str):
        return self._eval_string(expr, env)

    # Arrays: function calls or special forms
    if isinstance(expr, list):
        return self._eval_list(expr, env)

    raise JSLTypeError(f"Cannot evaluate expression of type {type(expr)}")

HostDispatcher

Manages host interactions for side effects:

jsl.core.HostDispatcher()

Handles JHIP (JSL Host Interaction Protocol) requests.

This is where all side effects are controlled. The host environment registers handlers for specific commands and decides what operations are permitted.

Source code in jsl/core.py
def __init__(self):
    self.handlers: Dict[str, Callable] = {}

dispatch(command, args)

Dispatch a host command with arguments.

Source code in jsl/core.py
def dispatch(self, command: str, args: List[Any]) -> Any:
    """Dispatch a host command with arguments."""
    if command not in self.handlers:
        raise JSLError(f"Unknown host command: {command}")

    try:
        return self.handlers[command](*args)
    except Exception as e:
        raise JSLError(f"Host command '{command}' failed: {e}")

register(command, handler)

Register a handler for a specific host command.

Source code in jsl/core.py
def register(self, command: str, handler: Callable) -> None:
    """Register a handler for a specific host command."""
    self.handlers[command] = handler

Resource Management

ResourceBudget

jsl.core.ResourceBudget(limits=None, host_gas_policy=None)

Comprehensive resource tracking for secure JSL execution.

Tracks gas consumption, memory allocation, execution time, and stack depth to prevent DOS attacks and ensure fair resource allocation.

Initialize resource budget.

Parameters:

Name Type Description Default
limits Optional[ResourceLimits]

Resource limits configuration

None
host_gas_policy Optional[HostGasPolicy]

Gas cost policy for host operations

None

allocate_memory(bytes_count, description='')

Account for memory allocation.

Parameters:

Name Type Description Default
bytes_count int

Number of bytes to allocate

required
description str

Description of allocation

''

Raises:

Type Description
MemoryExhausted

If memory limit would be exceeded

check_collection_size(size)

Check if a collection size is within limits.

Parameters:

Name Type Description Default
size int

Size of the collection

required

Raises:

Type Description
MemoryExhausted

If collection size exceeds limit

check_result(result)

Check resource constraints for a computed result.

This centralizes checking for collection sizes, string lengths, etc.

Parameters:

Name Type Description Default
result Any

The result value to check

required

check_string_length(length)

Check if a string length is within limits.

Parameters:

Name Type Description Default
length int

Length of the string

required

Raises:

Type Description
MemoryExhausted

If string length exceeds limit

check_time()

Check if time limit has been exceeded.

Raises:

Type Description
TimeExhausted

If time limit has been exceeded

checkpoint()

Create a checkpoint of current resource usage.

Returns:

Type Description
Dict[str, Any]

Dictionary with current resource usage

consume_gas(amount, operation='')

Consume gas for an operation.

Parameters:

Name Type Description Default
amount int

Gas amount to consume

required
operation str

Description of operation (for error messages)

''

Raises:

Type Description
GasExhausted

If gas limit would be exceeded

consume_host_gas(operation)

Consume gas for a host operation based on namespace.

Parameters:

Name Type Description Default
operation str

Host operation path (e.g., "@file/read")

required

enter_call()

Enter a function call (increase stack depth).

Raises:

Type Description
StackOverflow

If stack depth limit would be exceeded

exit_call()

Exit a function call (decrease stack depth).

restore(checkpoint)

Restore resource usage from a checkpoint.

Parameters:

Name Type Description Default
checkpoint Dict[str, Any]

Previously saved checkpoint

required

ResourceLimits

jsl.core.ResourceLimits(max_gas=None, max_memory=None, max_time_ms=None, max_stack_depth=100, max_collection_size=10000, max_string_length=100000)

Configuration for resource limits.

GasCost

jsl.core.GasCost

Bases: IntEnum

Gas costs for different operation types.

Error Types

JSLError

jsl.core.JSLError

Bases: Exception

Base exception for all JSL runtime errors.

SymbolNotFoundError

jsl.core.SymbolNotFoundError

Bases: JSLError

Raised when a symbol cannot be found in the current environment.

JSLTypeError

jsl.core.JSLTypeError

Bases: JSLError

Raised when there's a type mismatch in JSL operations.

Global State

prelude

The global prelude environment containing all built-in functions. A global, read-only instance of Env that contains all the JSL built-in functions (e.g., +, map, get). It serves as the ultimate parent of all other environments.

from jsl.core import prelude

# Access built-in functions
plus_func = prelude.get("+")
map_func = prelude.get("map")

Implementation Details

Environment Chains

JSL uses environment chains for variable resolution:

  1. Current Environment: Look for variable in current scope
  2. Parent Environment: If not found, check parent scope
  3. Continue Chain: Repeat until variable found or chain ends
  4. Prelude Access: All chains eventually reach the global prelude

Closure Serialization

Closures are designed for safe serialization:

  • Parameters: Always serializable (list of strings)
  • Body: Always serializable (JSON expression)
  • Environment: Only user-defined bindings are serialized
  • Prelude: Built-in functions are reconstructed, not serialized

This ensures transmitted closures are safe and can be reconstructed in any compatible JSL runtime.

Type Definitions

The module defines the following type aliases for clarity:

from typing import Union, List, Dict, Any

JSLValue = Union[None, bool, int, float, str, List[Any], Dict[str, Any], Closure]
JSLExpression = Union[JSLValue, List[Any], Dict[str, Any]]

Usage Examples

Basic Evaluation

from jsl.core import Evaluator, Env
from jsl.prelude import make_prelude

# Create evaluator and environment
evaluator = Evaluator()
env = make_prelude()

# Evaluate an expression
result = evaluator.eval(["+", 1, 2, 3], env)
print(result)  # Output: 6

Working with Closures

from jsl.core import Evaluator, Env
from jsl.prelude import make_prelude

evaluator = Evaluator()
env = make_prelude()

# Define a function
evaluator.eval(["def", "square", ["lambda", ["x"], ["*", "x", "x"]]], env)

# Call the function
result = evaluator.eval(["square", 5], env)
print(result)  # Output: 25

Resource-Limited Execution

from jsl.core import Evaluator, ResourceBudget, ResourceLimits

# Create evaluator with resource limits
limits = ResourceLimits(max_steps=1000, max_gas=10000)
budget = ResourceBudget(limits=limits)
evaluator = Evaluator(resource_budget=budget)

# Execute with resource tracking
result = evaluator.eval(expensive_computation, env)
print(f"Gas used: {budget.gas_used}")
print(f"Steps taken: {budget.steps_taken}")