Source | Playground

Why ?

No-Code tools like Klaro Cards generally require an expression language for user
to manipulate data easily. This language must be :

See also the Related work section below.

Current Features

Installation

npm install
npm run build

Testing

Elo 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 real runtimes (Ruby, Node.js, PostgreSQL)

npm run test:unit
npm run test:integration
npm run test:acceptance

Command Line Interface

Elo provides two CLI tools:
- eloc - The compiler (for developers integrating Elo into their products)
- elo - The evaluator (for quickly running Elo expressions)

Compiler (eloc)

The compiler translates Elo expressions to Ruby, JavaScript, or SQL:

# Compile expression to JavaScript (default)
./bin/eloc -e "2 + 3 * 4"

# Compile expression to Ruby
./bin/eloc -e "2 + 3 * 4" -t ruby

# Compile expression to SQL
./bin/eloc -e "2 + 3 * 4" -t sql

# Compile with prelude (includes required runtime libraries)
./bin/eloc -e "NOW + PT2H" -t ruby -p

# Output only the prelude (useful for bundling)
./bin/eloc --prelude-only -t js

# Compile from file (each line is compiled separately)
./bin/eloc input.elo -t ruby

# Compile to file
./bin/eloc -e "2 + 3" -t ruby -f output.rb

# Compile from stdin
echo "2 + 3 * 4" | ./bin/eloc -
cat input.elo | ./bin/eloc - -t ruby

Options:
- -e, --expression <expr> - Expression to compile
- -t, --target <lang> - Target language: ruby, js (default), sql
- -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

Evaluator (elo)

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: 20

Options:
- -e, --expression <expr> - Expression to evaluate
- -d, --data <data> - Input data for _ variable (or @file to read from file)
- --stdin - Read input data from stdin
- -f, --input-format <fmt> - Input data format: json (default) or csv
- -o, --output-format <fmt> - Output format: json (default), elo, or csv
- -h, --help - Show help message

Using Elo in JavaScript/TypeScript

The simplest way to use Elo is with the compile() function, which creates a callable JavaScript function from an Elo expression:

import { compile } from '@enspirit/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 false

The runtime option injects dependencies (like DateTime and Duration from luxon) into the compiled function. This avoids global variables and keeps the compiled code portable.

Data Format Adapters

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.

Lower-Level API

For more control, you can use the lower-level parsing and compilation functions:

import { parse, compileToRuby, compileToJavaScript, compileToSQL } from '@enspirit/elo';

// Parse an expression
const ast = parse(`
  let
    x = TODAY,
    y = 3
  in
    assert(x + y * P1D == TODAY + P3D)
`);

// Compile to different targets
console.log(compileToRuby(ast));
console.log(compileToJavaScript(ast));
console.log(compileToSQL(ast));

Programmatic AST Construction

import { binary, variable, literal } from './src';

// Build: (price * quantity) - discount
const ast = binary(
  '-',
  binary('*', variable('price'), variable('quantity')),
  variable('discount')
);

Examples

Run the examples:

npm run build
node dist/examples/basic.js     # Arithmetic expressions
node dist/examples/boolean.js   # Boolean expressions
node dist/examples/temporal.js  # Temporal expressions (dates, durations)
node dist/examples/demo.js      # Quick demo

Project Structure

elo/
├── src/              # Compiler source code
│   ├── parser.ts     # Lexer and parser
│   ├── ast.ts        # AST definitions
│   ├── types.ts      # Type system
│   ├── ir.ts         # Intermediate representation
│   ├── transform.ts  # AST → IR transformation with type inference
│   ├── stdlib.ts     # Standard library abstraction
│   ├── compilers/    # Code generators (Ruby, JavaScript, SQL)
│   └── preludes/     # Runtime support libraries
├── test/             # Test suite
│   ├── fixtures/     # Test cases
│   ├── unit/         # Component tests
│   ├── integration/  # Compilation tests
│   └── acceptance/   # Runtime execution tests
├── examples/         # Usage examples
├── bin/eloc          # Compiler CLI
├── bin/elo           # Evaluator CLI
└── CLAUDE.md         # Developer guide

For detailed architecture documentation, see CLAUDE.md.

Related work

Enspirit's previous research work includes a lot of places where such expressions are used, calling for a shared solution for the future.

In many cases, observe that we require compiling expressions that amount to a single function evaluating on a context object, sometimes a scalar (Finitio), sometimes a current Tuple (Bmg), sometimes json data received from an API (Webspicy), or a current Card (Klaro Cards, similar to Bmg's Tuple).

See https://elo-lang.org for more documentation.

Finitio

The Finitio data validation language supports subtypes by constraints such as:

PositiveInt = Int( i | i > 0 )

Currently, the constraint expression is written in the host language (js or ruby) and would require a portable expression language to go further.

See https://finitio.io, https://github.com/enspirit/finitio-rb, https://github.com/enspirit/finitio.js

Bmg

The Bmg relational algebra requires expressions for the restrict and extend operators inspired by Tutorial D. We currently rely on ruby code in some cases, but that prevents compiling relational expressions to SQL :

r.restrict(->(t){ t[:budget] >= 120 })
r.extend(:upcased => ->(t) { t[:name].upcase })

See https://www.relational-algebra.dev/, https://github.com/enspirit/bmg

Webspicy

The Webspicy test framework requires a better expression language for data assertions. We currently rely on an hardcoded expression language that is very limited:

assert:
- isEmpty
- size(10)

See https://github.com/enspirit/webspicy

Klaro Cards

The Klaro Cards No-Code tool uses various data expressions here and there :

See https://klaro.cards


Tags: language   no-code   declarative  

Last modified 17 February 2026