JSL (JSON Serializable Language) is a functional programming language where code is JSON. The whole point: if your code is already valid JSON, serialization stops being a problem you solve and starts being a property you have.
The Problem
Most languages treat serialization as an afterthought. You write code in one representation, data lives in another, and moving computation across a network requires marshalling, pickling, or worse.
The traditional approach:
# Code: Python AST, bytecode, machine code
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
# Data: JSON
data = {"n": 5}
# Problem: Can't serialize the function, can't send it over network
JSL’s approach:
["do",
["def", "factorial",
["lambda", ["n"],
["if", ["<=", "n", 1],
1,
["*", "n", ["factorial", ["-", "n", 1]]]]]],
["factorial", 5]]
That program is valid JSON. Any JSON parser reads it. Any HTTP endpoint transmits it. Any database stores it. Any program generates it. Code and data are the same thing, which is Lisp’s oldest idea wearing a new coat.
Design Principles
JSON as Code and Data
All JSL programs and data structures are representable as standard JSON. This means universal parsing, generation, and compatibility with every tool that speaks JSON (which is basically every tool).
Serializable Closures
This is the thing I actually care about. Closures (functions with captured environment) are fully serializable:
from jsl import JSLRunner
runner = JSLRunner()
# Create a closure that captures 'multiplier'
runner.execute('''
(do
(def multiplier 10)
(def make-multiplier (lambda (x) (* x multiplier)))
(def my-func (make-multiplier 5)))
''')
# Serialize the closure
serialized = runner.serialize_value(runner.env.get('my-func'))
# Send over network, store in database, etc.
# Later, deserialize and execute
deserialized_func = runner.deserialize_value(serialized)
result = runner.apply(deserialized_func, [3]) # 30
The closure retains its captured multiplier variable even after serialization. In Python you’d reach for pickle, which is unsafe and fragile. Here it just works because the closure was JSON the whole time.
Effect Reification
Side effects are not executed directly. They’re described as data structures:
; This doesn't perform I/O directly
(host file-read "/tmp/data.json")
; Instead, it produces a data structure:
{
"effect": "host",
"command": "file-read",
"args": ["/tmp/data.json"]
}
The host environment controls, audits, or modifies these effects before execution. This is basically the algebraic effects pattern: pure computation produces descriptions of what it wants done, and the runtime decides whether to actually do it.
Deterministic Evaluation
The core JSL evaluation (excluding host interactions) is deterministic. Same input, same output. This makes testing and debugging distributed computations tractable, which matters more than it sounds like it does.
Dual Evaluation Engines
JSL provides two complementary evaluation strategies. I built both because they solve different problems.
Recursive Evaluator
The traditional tree-walking interpreter:
from jsl.evaluator import Evaluator
evaluator = Evaluator()
result = evaluator.eval(["*", ["+", 2, 3], 4]) # 20
Direct AST evaluation, natural semantics, good for development and debugging. Nothing fancy here.
Stack-Based Evaluator
This is the interesting one. It compiles to JPN (JSL Postfix Notation) bytecode and executes on a stack machine:
from jsl.stack_evaluator import StackEvaluator
evaluator = StackEvaluator()
# Compile to JPN bytecode
jpn = compile_to_postfix(["*", ["+", 2, 3], 4])
# Result: [2, 3, "+", 4, "*"]
# Execute with step limits
result, state = evaluator.eval_partial(jpn, max_steps=100)
if state: # Computation paused
# State is fully serializable
serialized = state.to_dict()
# Resume later, possibly on different machine
final_result = evaluator.resume(serialized)
The key property: you can pause a computation, serialize the entire execution state to JSON, ship it to another machine, and resume there. The program counter, the operand stack, the environment, the call stack, all of it is just JSON.
JPN: JSL Postfix Notation
JPN is the compiled bytecode format. Postfix notation makes the stack machine simple and the execution state trivially serializable.
Compilation
from jsl.compiler import compile_to_postfix, decompile_from_postfix
# S-expression
expr = ["if", ["=", "x", 5], "yes", "no"]
# Compile to JPN
jpn = compile_to_postfix(expr)
# JPN uses opcodes for control flow
# [var_lookup("x"), 5, "=", "if_start", "yes", "if_branch", "no", "if_end"]
# Can decompile back
original = decompile_from_postfix(jpn)
Resumable Computation
This is where it gets useful:
# Execute with step limit
result, state = evaluator.eval_partial(jpn, max_steps=1000)
if state: # Paused at step 1000
# Serialize entire execution state
checkpoint = {
"program": jpn,
"pc": state.pc, # Program counter
"stack": state.stack, # Operand stack
"env": state.env.to_dict(), # Environment
"call_stack": state.call_stack
}
# Send checkpoint to another machine
# Resume there
new_evaluator = StackEvaluator()
final = new_evaluator.eval_partial(jpn, state=State.from_dict(checkpoint))
This enables fault tolerance (checkpoint and restart), load balancing (move computation to less-busy nodes), edge computing (pause on edge, resume in cloud), and fair scheduling (time-slice multiple programs). All because the execution state is just data.
Syntax Options
JSL offers two syntactic representations:
JSON Array Syntax (Network-Native)
["do",
["def", "square", ["lambda", ["x"], ["*", "x", "x"]]],
["map", ["square"], ["@", [1, 2, 3, 4, 5]]]]
Valid JSON, parseable anywhere, easy to generate programmatically.
Lisp-Style Syntax (Human-Friendly)
(do
(def square (lambda (x) (* x x)))
(map square (@ [1 2 3 4 5])))
More readable for humans, familiar to Lisp programmers. Both compile to the same internal representation.
Query and Transform Operations
JSL provides declarative operations for data manipulation. These look a lot like what you’d write in a query language, except they compose naturally because they’re just functions.
Where (Filtering)
; Define data
(def users [@
{"name": "Alice", "age": 30, "role": "admin"}
{"name": "Bob", "age": 25, "role": "user"}
{"name": "Charlie", "age": 35, "role": "admin"}
])
; Filter by role (fields auto-bind)
(where users (= role "admin"))
; Result: [{"name": "Alice", ...}, {"name": "Charlie", ...}]
; Complex conditions
(where users (and (>= age 30) (= role "admin")))
; Result: [{"name": "Alice", ...}, {"name": "Charlie", ...}]
In where predicates, field names automatically bind to the current object’s fields. No explicit path notation needed.
Transform (Data Shaping)
; Pick specific fields
(transform users (pick "name" "email"))
; Add computed fields
(transform products (assign "discounted" (* price 0.9)))
; Remove sensitive fields
(transform users (omit "password" "ssn"))
; Rename fields
(transform users (rename "email" "contact_email"))
Composition
Operations compose naturally:
(do
(def expensive (where products (> price 50)))
(def simplified (transform expensive (pick "name" "price")))
(pluck simplified "name"))
Resource Management
For safe distributed execution, you need to bound what untrusted code can do.
Gas Metering
runner = JSLRunner(config={
"max_gas": 10000,
"gas_costs": {
"+": 1,
"*": 2,
"lambda": 10,
"apply": 5
}
})
result, state = runner.execute_partial(expensive_program)
print(f"Gas consumed: {state.gas_consumed}")
print(f"Gas remaining: {state.gas_remaining}")
Step Limiting
runner = JSLRunner(config={"max_steps": 1000})
result, state = runner.execute_partial(program)
if state: # Paused at step 1000
print(f"Paused at step {state.step_count}")
# Resume with more steps
final = runner.execute_partial(program, state=state, max_steps=1000)
Fair Scheduling
programs = [prog1, prog2, prog3]
states = [None, None, None]
# Round-robin execution
for i in range(100): # 100 rounds
for j, prog in enumerate(programs):
result, states[j] = runner.execute_partial(
prog,
state=states[j],
max_steps=100 # 100 steps per round
)
Content-Addressable Serialization
JSL uses content-addressable storage for efficient serialization of complex object graphs. Simple values serialize directly:
value = [1, 2, {"a": "hello"}]
serialized = runner.serialize_value(value)
# {"result": [1, 2, {"a": "hello"}]}
Closures with captured environments use a content-addressable format:
runner.execute('(def x 10)')
runner.execute('(def f (lambda (y) (+ x y)))')
closure = runner.env.get('f')
serialized = runner.serialize_value(closure)
# Result:
{
"__cas_version__": "1.0",
"root": "hash_of_closure",
"objects": {
"hash_of_closure": {
"type": "closure",
"params": ["y"],
"body": ["+", "x", "y"],
"env": "hash_of_env"
},
"hash_of_env": {
"type": "env",
"bindings": {"x": 10},
"parent": null
}
}
}
Identical objects are stored once, circular references are handled via hashes, and large environments are shared across multiple closures. Same value always produces the same hash, so you get deduplication for free.
Security Model
JSL’s security is capability-based. No arbitrary code execution (everything runs in the interpreter). All side effects go through the host:
; This produces a data structure, doesn't execute directly
(host file-write "/tmp/output.txt" "data")
; Host decides whether to allow this
Host implementation:
class SecureHost:
def handle_effect(self, effect):
command = effect["command"]
if command == "file-write":
path = effect["args"][0]
if not self.is_allowed_path(path):
raise PermissionError(f"Cannot write to {path}")
return self.do_file_write(path, effect["args"][1])
raise ValueError(f"Unknown command: {command}")
JSL programs run within the interpreter’s sandbox: no system calls, no file access (unless the host allows it), no network access (unless the host allows it), bounded computation via gas and step limits. Effect requests are just JSON data, so you can log, audit, or modify them before they execute.
Architecture
Seven layers, each cleanly separated:
- Wire Layer: JSON representation for transmission/storage
- Compilation Layer: Parser + JPN compiler/decompiler
- Runtime Layer: Dual evaluators + environment management
- Prelude Layer: Standard library (arithmetic, lists, strings, etc.)
- User Code Layer: JSL programs and libraries
- Host Interaction Layer: Effect requests as data
- Host Environment: Capability control and execution
Theoretical Foundations
Homoiconicity
Like Lisp, JSL code and data share the same structural representation. The difference is that JSL uses JSON arrays and objects instead of S-expressions:
; Lisp S-expression
(+ 1 2 3)
; JSL JSON array
["+", 1, 2, 3]
This enables code generation (programs that write programs), macros (code transformations), and metaprogramming (inspect and modify code as data). The homoiconicity buys you the same things it buys you in Lisp, but now it also buys you wire compatibility.
Lexical Scoping and Closures
(do
(def make-adder (lambda (n)
(lambda (x) (+ x n))))
(def add5 (make-adder 5))
(def add10 (make-adder 10))
(add5 3) ; 8
(add10 3) ; 13
)
Both add5 and add10 are closures that captured different values of n from their lexical environment.
Separation of Pure Computation and Effects
; Pure computation (deterministic)
(def sum (lambda (xs) (fold + 0 xs)))
; Effect request (non-deterministic)
(host http-get "https://api.example.com/data")
Pure computations can be cached, parallelized, tested deterministically, and optimized by the compiler. Effects are isolated and controlled. This separation is old hat in the Haskell world, but it falls out naturally here because effects are literally just data structures.
Use Cases
I keep finding more of these. A few that I think are genuinely useful:
Distributed computing: Send computation to where data lives. The function is JSON, the data is JSON, the result is JSON.
Serverless functions: Define functions as JSON, store them in a database, execute on demand with resource limits.
API query language: Let users submit queries that run sandboxed with gas metering. No injection attacks, no arbitrary code execution, bounded resource usage.
Edge computing: Deploy logic to edge devices as JSON. Update dynamically without redeployment. Pause on edge, resume in cloud.
Plugin systems: Users extend your application with sandboxed plugins. You control what capabilities they have.
Real-World Examples
ETL Pipeline
(do
; Load data
(def raw-data (host file-read "sales.jsonl"))
; Filter completed sales
(def completed (where raw-data (= status "completed")))
; Transform to summary format
(def summary (transform completed
(pick "customer_id" "amount" "date")))
; Group by customer and sum
(def totals (group-by summary "customer_id"
[(sum "amount")]))
; Save results
(host file-write "totals.json" totals))
Data Validation
(def validate-user (lambda (user)
(and
(has-path user "email")
(str-matches (get user "email") "^[^@]+@[^@]+\\.[^@]+$")
(> (get user "age") 0)
(< (get user "age") 150))))
(def valid-users (where users validate-user))
Prelude Functions
JSL includes a standard library covering the basics:
- Arithmetic:
+,-,*,/,%,=,<,>,<=,>= - Lists:
cons,first,rest,map,filter,fold,take,drop,length - Strings:
str-concat,str-matches,str-replace,str-find-all,str-length - Objects:
get,get-path,set-path,has-path,keys,values - Query:
where,transform,pluck,index-by,group-by - Logic:
and,or,not,if - Functional:
lambda,apply,map,filter,fold
Quick Start
# Install
pip install jsl-lang
# Interactive REPL
jsl --repl
# Evaluate expression
jsl --eval '["+", 1, 2, 3]'
# Run file
jsl program.jsl
Python usage:
from jsl import JSLRunner
runner = JSLRunner()
# Execute program
result = runner.execute('(+ 1 2 3)') # 6
# Define and use functions
runner.execute('''
(do
(def double (lambda (x) (* x 2)))
(map double (@ [1 2 3 4 5])))
''') # [2, 4, 6, 8, 10]
# Query and transform data
runner.execute('(def users [@ {...}, {...}])')
admins = runner.execute('(where users (= role "admin"))')
Comparison
| Feature | JSL | Python | JavaScript | Scheme |
|---|---|---|---|---|
| Serializable closures | Yes | No | No (limited) | No |
| Resumable execution | Yes | No | No | No |
| Gas metering | Yes | No | No | No |
| Effect reification | Yes | No | No | No |
| JSON-native | Yes | No | Yes (values) | No |
| Network transmission | Yes | Via pickle (unsafe) | Limited | No |
| Sandboxing | Yes | Limited | Limited | No |
Resources
- PyPI: pypi.org/project/jsl-lang/
- Repository: github.com/queelius/jsl
- Documentation: Full API docs and tutorials
- Examples: See
examples/directory
License
MIT
Discussion