Fork Notice: This is a JavaScript-only fork of enspirit/elo, maintained by ContextVM. The original multi-target (Ruby, JavaScript, PostgreSQL) version is available at the upstream repository.
A simple, well-designed, portable and safe data expression language that compiles to JavaScript. This fork focuses on a JS-only architecture with support for plugin programs using the plan/do syntax.
Try Elo online - Interactive playground and documentation (upstream)
This fork exists to provide:
- JavaScript-only runtime for lightweight, portable deployments
- Plugin program architecture with capability-based execution (
plan/dosyntax) - Strategy-1 planner for advanced runtime control
- Faster releases with simplified build pipeline
No-Code tools like Klaro Cards generally require an expression language for user to manipulate data easily. This language must be :
- simple, because No-Code tools are used by non-tech people
- portable, because they are implemented in various frontend/backend/db technologies
- safe, because end-users writing code yield serious security issues
- well-designed, because there are too many ill-designed programming languages already
See also the Related work section below.
This fork introduces the following new features:
-
plan/thenrounds: Multi-stage computation model for plugin programsplan a = 1, b = _.x in then c = 2 in a + b + c -
docapability calls: Call external capabilities from within plugin programsplan result = do 'nostr.query' {kinds: [0]} in result | null -
Strategy-1 planner/runtime driver: Advanced execution planning for complex workflows
-
JSON boundary helpers: Utilities for working with JSON data at system boundaries
All original Elo features are preserved.
npm install
npm run buildElo uses a comprehensive test suite that verifies:
- Unit tests: Parser, AST, and compiler components
- Integration tests: End-to-end compilation output
- Acceptance tests: Compiled code execution in JavaScript runtime (Node.js)
npm run test:unit
npm run test:integration
npm run test:acceptanceElo provides two CLI tools:
eloc- The compiler (for developers integrating Elo into their products)elo- The evaluator (for quickly running Elo expressions)
The compiler translates Elo expressions to JavaScript:
# Compile expression to JavaScript (default)
./bin/eloc -e "2 + 3 * 4"
# Compile with prelude (includes required runtime libraries)
./bin/eloc -e "NOW + PT2H" -p
# Output only the prelude (useful for bundling)
./bin/eloc --prelude-only
# Compile from file (each line is compiled separately)
./bin/eloc input.elo
# Compile to file
./bin/eloc -e "2 + 3" -f output.js
# Compile from stdin
echo "2 + 3 * 4" | ./bin/eloc -
cat input.elo | ./bin/eloc -Options:
-e, --expression <expr>- Expression to compile-t, --target <lang>- Target language:js(default and only option in this fork)-p, --prelude- Include necessary library imports/requires--prelude-only- Output only the prelude (no expression needed)-f, --file <path>- Output to file instead of stdout-h, --help- Show help message
The evaluator compiles to JavaScript and immediately evaluates the expression:
# Evaluate a simple expression
./bin/elo -e "2 + 3 * 4"
# Outputs: 14
# Evaluate with input data (JSON)
./bin/elo -e "_.x + _.y" -d '{"x": 1, "y": 2}'
# Outputs: 3
# Evaluate with CSV input data
./bin/elo -e "map(_, fn(r ~> r.name))" -d @data.csv -f csv
# Outputs: ["Alice","Bob"]
# Evaluate with data from file (format auto-detected from extension)
./bin/elo -e "_.name" -d @data.json
# Output in different formats
./bin/elo -e "{a: 1, b: 2}" -o elo # Elo code format
./bin/elo -e "[{name: 'Alice'}]" -o csv # CSV format
# Evaluate from .elo file
./bin/elo expressions.elo
# Pipe data through stdin
echo '{"x": 10}' | ./bin/elo -e "_.x * 2" --stdin
# Outputs: 20Options:
-e, --expression <expr>- Expression to evaluate-d, --data <data>- Input data for_variable (or@fileto read from file)--stdin- Read input data from stdin-f, --input-format <fmt>- Input data format:json(default) orcsv-o, --output-format <fmt>- Output format:json(default),elo, orcsv-h, --help- Show help message
The simplest way to use Elo is with the compile() function, which creates a callable JavaScript function from an Elo expression:
import { compile } from "@contextvm/elo";
import { DateTime, Duration } from "luxon";
// Compile an expression to a callable function
// Every Elo expression takes _ (implicit input) as parameter
const addTen = compile<(x: number) => number>("_ + 10", {
runtime: { DateTime, Duration },
});
addTen(5); // => 15
// Temporal expressions work too
const inThisWeek = compile<(d: unknown) => boolean>("_ in SOW ... EOW", {
runtime: { DateTime, Duration },
});
inThisWeek(DateTime.now()); // => true or falseThe runtime option injects dependencies (like DateTime and Duration from luxon) into the compiled function. This avoids global variables and keeps the compiled code portable.
The CLI and playground support multiple input/output formats (JSON, CSV). The format system is pluggable—you can provide custom adapters using libraries like PapaParse or SheetJS. See src/formats.ts for the FormatAdapter interface and built-in implementations.
For more control, you can use the lower-level parsing and compilation functions:
import {
parse,
compile,
compileToJavaScript,
parsePluginProgram,
compilePlugin,
} from "@contextvm/elo";
// Parse a simple expression
const ast = parse(`
let
x = TODAY,
y = 3
in
assert(x + y * P1D == TODAY + P3D)
`);
// Compile to JavaScript
console.log(compileToJavaScript(ast));
// Parse and compile a plugin program with plan/then/do syntax
const pluginSrc = "plan result = do 'cap.query' {limit: 10} in result | null";
const plugin = parsePluginProgram(pluginSrc);
const jsCode = compilePlugin(pluginSrc);This fork introduces plugin program support for capability-based execution:
parsePluginProgram(src: string)- Parse a plugin program withplan/thenroundscompilePlugin(src: string)- Compile a plugin program to JavaScriptparseWithMeta(src: string)- Parse with diagnostics (errors, warnings)validatePluginProgram(src: string, options)- Validate plugin semantics, includingdocapability usagevalidateExpressionAst(expr, options)- Validate an expression AST against a caller-provided scope
Object literals accept both bare identifier keys and quoted string keys, so protocol-shaped data such as {kinds: [1], '#e': ['event-id'], '#K': ['kind']} can be written directly in Elo.
Plugin validation is intentionally split between the Elo library and the host application:
- Elo parses plugin syntax and validates generic expression semantics
- the host provides the list of allowed
docapabilities - the host may optionally attach argument validators for each capability
This keeps capability policy out of the parser and compiler while still giving plugin authors useful diagnostics.
Use validatePluginProgram() to validate a plugin program and inject the capabilities your host supports:
import { validatePluginProgram } from "@contextvm/elo";
const source = "plan events = do 'nostr.query' {kinds: [1]} in events | null";
const result = validatePluginProgram(source, {
capabilities: {
"nostr.query": { name: "nostr.query" },
},
});
console.log(result.diagnostics);If a program uses an unknown capability name, validatePluginProgram() returns a diagnostic instead of throwing.
do is a host-level escape hatch, not a generic Elo runtime primitive.
That means the library should validate the structure of do, but it should not hardcode a global catalog of supported capabilities. Different hosts can expose different operations, or none at all.
The recommended pattern is:
- keep the capability registry in your app or runtime layer
- pass it into
validatePluginProgram() - use tests to emulate host capabilities where needed
Each capability can define a PluginCapabilitySpec with a validateArgs callback.
That callback receives a PluginCapabilityValidationContext, which includes:
argsExpr: the raw AST passed todoallowedVariables: variables visible at that binding siteroundIndex: the current round numberbindingName: the binding receiving thedoresultvalidateExpressionAst: helper for reusing Elo's generic expression validation inside your capability validator
Example:
import { validatePluginProgram } from "@contextvm/elo";
const source = `
plan author = _.pubkey,
events = do 'nostr.query' {authors: [author], limit: 10}
in events | null
`;
const result = validatePluginProgram(source, {
capabilities: {
"nostr.query": {
name: "nostr.query",
validateArgs: ({ argsExpr, allowedVariables, validateExpressionAst }) => {
if (!allowedVariables.includes("author")) {
return [
{
message: "author is expected to be in scope",
severity: "error",
},
];
}
return validateExpressionAst(argsExpr);
},
},
},
});
console.log(result.diagnostics);This pattern keeps the API simple:
- Elo remains the source of truth for parsing and generic semantic validation
- the host remains the source of truth for which capabilities exist
- consumers do not need a built-in capability catalog in the library
For plugin programs, validatePluginProgram() currently helps detect:
- parse errors in plugin syntax
- sequential binding-scope errors across
planandthenrounds - unknown variables and unknown functions in normal expressions
- unknown capability names in
do - invalid capability argument expressions
- illegal
dousage in score expressions
This makes it suitable as the main library-side validator, with host-specific capability policy layered on top.
Getting started: See HACKING.md for:
- Development environment setup (local or Docker)
- Running the test suite
- Project structure overview
For developers and AI assistants: See CLAUDE.md for:
- Detailed development workflow
- How to add new features and operators
- Architecture documentation