Skip to content

andrueandersoncs/lion

Repository files navigation

Lion

Lion is a JSON-based Lisp. Every Lion program is valid JSON, which makes it easy to serialize, store, inspect, and generate from other systems.

Lion evaluates:

  • JSON primitives directly
  • strings as environment references when a binding exists
  • arrays as special forms or function-call expressions
  • objects by recursively evaluating their values

The result is a small homoiconic language that can describe both computation and data interchange without a separate parser.

Packages

packages/
  core/   evaluator, special forms, stdlib
  repl/   interactive web REPL
  agent/  real-world Lion program driving an OpenTUI + AI agent

Basic Usage

import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

const result = await Effect.runPromise(
	run(["number/add", 1, 2], stdlib)
);

// => 3

Custom Environment

Any string can resolve against the evaluation environment. That includes values, objects, classes, and functions.

import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

const env = {
	...stdlib,
	price: 100,
	taxRate: 0.08,
	clamp: (min: number, max: number, value: number) =>
		Math.min(max, Math.max(min, value)),
};

await Effect.runPromise(
	run(["number/add", "price", ["number/multiply", "price", "taxRate"]], env)
);
// => 108

await Effect.runPromise(run(["clamp", 0, 100, 150], env));
// => 100

Expression Model

Primitives

Numbers, booleans, and null evaluate to themselves. Strings are treated as references first; if no binding exists, the original string is returned.

42
true
null
"x"

With { "x": 10 } in the environment, "x" evaluates to 10. Otherwise it evaluates to "x".

Arrays

Arrays are always treated as executable expressions:

  1. special forms such as ["define", "x", 1]
  2. function calls when the first element evaluates to a function
["number/add", 1, 2]

Evaluates to 3.

Objects

Objects preserve their keys and evaluate each value.

{
  "sum": ["number/add", 1, 2],
  "ok": true
}

Evaluates to { "sum": 3, "ok": true }.

Special Forms

quote

Returns its argument without evaluating it.

["quote", ["number/add", 1, 2]]

eval

Evaluates its argument, then evaluates the result.

["eval", ["quote", ["number/add", 1, 2]]]

begin

Evaluates expressions in order and returns the last result.

["begin", ["define", "x", 1], ["define", "x", 2], "x"]

define

Evaluates a value, stores it in the global environment, and returns the stored value.

["define", "answer", 42]

lambda

Creates a Lion function with lexical scope.

["lambda", ["x"], ["number/add", "x", 1]]

Lambdas close over surrounding scope, and they observe updated global bindings when invoked later.

cond

Evaluates condition/result pairs in order and returns the first matching result. An else branch is optional.

["cond",
  [["number/greaterThan", "score", 90], "great"],
  [["number/greaterThan", "score", 70], "pass"],
  ["else", "retry"]]

If no branch matches and no else is provided, cond returns null.

match

Pattern matches a value against predicate/function pairs, then applies the first matching branch. The last argument is a fallback function.

["match",
  "value",
  ["value/number?", ["lambda", ["x"], ["number/add", "x", 1]]],
  ["value/string?", ["lambda", ["x"], ["string/concat", "x", "!"]]],
  ["lambda", ["x"], "x"]]

match also supports structural predicates:

["match",
  { "type": "user", "profile": { "name": "Ada" } },
  [
    {
      "type": ["func/partial", "string/equals?", "user"],
      "profile": { "name": "value/string?" }
    },
    "value/identity"
  ],
  ["lambda", ["x"], null]]

Standard Library

The default environment exported as stdlib namespaces entries as <module>/<name>.

number

["number/add", 1, 2]
["number/subtract", 10, 3]
["number/multiply", 5, 4]
["number/divide", 20, 4]
["number/equals?", 5, 5]
["number/lessThan", 3, 5]
["number/greaterThan", 5, 3]
["number/lessThanOrEqualTo", 3, 5]
["number/greaterThanOrEqualTo", 5, 5]

boolean

["boolean/not", true]
["boolean/and", true, false]
["boolean/or", false, true]
["boolean/xor", true, false]
["boolean/nand", true, true]
["boolean/nor", false, false]
["boolean/equals?", true, true]

array

["array/make", 1, 2, 3]
["array/head", ["array/make", 1, 2, 3]]
["array/tail", ["array/make", 1, 2, 3]]
["array/length", ["array/make", 1, 2, 3]]
["array/concat", ["array/make", 1, 2], ["array/make", 3, 4]]
["array/includes?", ["array/make", 1, 2, 3], 2]
["array/map", ["array/make", 1, 2, 3], ["lambda", ["x"], ["number/add", "x", 1]]]
["array/flat-map", ["array/make", 1, 2], ["lambda", ["x"], ["array/make", "x", "x"]]]
["array/reduce", ["array/make", 1, 2, 3], 0, ["lambda", ["acc", "x"], ["number/add", "acc", "x"]]]

string

["string/equals?", "a", "a"]
["string/length", "lion"]
["string/concat", "lion", "ized"]
["string/startsWith", "lion", "li"]
["string/endsWith", "lion", "on"]
["string/includes", "lion", "io"]
["string/indexOf", "lion", "o"]

object

["object/get", {"a": 1}, "a"]
["object/get-path", {"a": {"b": {"c": 42}}}, "a.b.c"]
["object/keys", {"a": 1, "b": 2}]
["object/values", {"a": 1, "b": 2}]
["object/json-stringify", {"a": 1}, null, 2]
["object/get-method", "obj", "methodName"]
["object/call-method", "obj", "methodName", 1, 2]
["object/get-method-path", "obj", "nested.methodName"]
["object/call-method-path", "obj", "nested.methodName", 1, 2]
["object/new", "SomeClass", "arg1", "arg2"]

object/get-path understands dot paths and numeric array segments such as "users.1.name".

func

["func/bind", "fn", "obj"]
["func/apply", "fn", "obj", 1, 2]
["func/callback", ["lambda", ["x"], ["number/add", "x", 1]]]
["func/partial", "fn", 1, 2]

func/callback is especially useful for turning Lion lambdas into JavaScript callbacks while preserving access to the Lion environment.

value

["value/function?", "x"]
["value/number?", 1]
["value/string?", "lion"]
["value/boolean?", true]
["value/object?", {"a": 1}]
["value/array?", ["array/make", 1, 2]]
["value/null?", null]
["value/identity", 42]

value/undefined? and value/symbol? are mainly useful when Lion is evaluating host values supplied through the environment.

console

["console/log", "hello"]
["console/log-json", {"hello": "world"}]

Host Interop

Lion works well as a configuration and orchestration layer over ordinary JavaScript objects and functions.

The packages/agent package is the clearest example. It injects:

  • OpenTUI classes and enums
  • AI SDK helpers
  • a model instance
  • Node fs
  • Zod constructors
  • a renderer instance

Then Lion code in packages/agent/src/agent.json uses:

  • object/get-path to traverse host APIs
  • object/new to construct classes
  • object/call-method via aliases such as "()"
  • func/callback to hand Lion lambdas to promise and event APIs
  • match, cond, begin, and define to orchestrate application flow

That lets Lion drive a non-trivial terminal UI agent entirely from JSON.

Development

From the repo root:

bun install
bun run prepare
bun test
bun run build

To work on a specific package:

turbo test --filter=@lionlang/core
turbo dev --filter=@lionlang/repl

About

Lisp Object Notation - an advanced data format built on JSON capable of representing arbitrary computations

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors