Skip to main content
Every Lion program runs inside an evaluation environment — a plain JavaScript object whose keys are the names Lion can resolve. The standard library is itself just an environment object exported as stdlib, which means extending it is as simple as spreading it alongside your own entries. This gives you a clean boundary between the Lion evaluator and the host application without any plugin system or registration API.

Spreading stdlib with custom entries

Import stdlib from @lionlang/core/modules, spread it into a new object, and add whatever keys your Lion programs need:
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)),
};
Pass the environment as the second argument to run:
const result = await Effect.runPromise(
  run(["number/add", "price", ["number/multiply", "price", "taxRate"]], env)
);
// => 108

const clamped = await Effect.runPromise(run(["clamp", 0, 100, 150], env));
// => 100
When a string appears as an expression and a matching key exists in the environment, Lion resolves it to that value. If no binding exists, the string evaluates to itself. This means "price" in a Lion expression becomes 100, and "unknown" stays "unknown".

What can go in the environment

The environment can hold any JavaScript value. Lion will pass it through transparently to the object and func modules when needed:
Value typeExampleHow Lion uses it
Numberprice: 100Resolves as a numeric literal
Stringgreeting: "hello"Resolves as a string literal
Booleandebug: falseResolves as a boolean
Objectuser: { name: "Ada" }Traversable with object/get-path
Functionclamp: (min, max, v) => ...Callable as ["clamp", ...]
Class constructorCounter: CounterConstructable with object/new
Class instancedb: dbInstanceMethods callable with object/call-method

Organizing custom modules with namespaceEntries

The namespaceEntries helper exported from @lionlang/core/modules takes a namespace string and a record, and returns a new record with each key prefixed by namespace/. This is exactly how stdlib is assembled from its individual modules:
import { namespaceEntries, stdlib } from "@lionlang/core/modules";

const mathHelpers = {
  clamp: (min: number, max: number, value: number) =>
    Math.min(max, Math.max(min, value)),
  lerp: (a: number, b: number, t: number) => a + (b - a) * t,
};

const env = {
  ...stdlib,
  ...namespaceEntries("math", mathHelpers),
};
This adds math/clamp and math/lerp to the environment, consistent with how number/add, string/concat, and the other stdlib entries are named.
["math/clamp", 0, 100, 150]
Evaluates to 100.

Complete examples

The following programs are drawn directly from packages/examples/index.ts. All of them run against this shared environment:
const env = {
  ...stdlib,
  price: 100,
  taxRate: 0.08,
  clamp: (min: number, max: number, value: number) =>
    Math.min(max, Math.max(min, value)),
  user: {
    name: "Ada",
    stats: {
      score: 92,
    },
  },
};

Arithmetic

References to environment variables resolve to their values before the arithmetic operation runs:
["number/add", "price", ["number/multiply", "price", "taxRate"]]
Returns 108 — equivalent to 100 + 100 * 0.08.

Conditionals

object/get-path traverses the nested user object, and cond branches on the result:
[
  "cond",
  [["number/greaterThan", ["object/get-path", "user", "stats.score"], 90], "great"],
  [["number/greaterThan", ["object/get-path", "user", "stats.score"], 70], "pass"],
  ["else", "retry"]
]
Returns "great" because user.stats.score is 92.

Structural match

match compares a value against structural predicates. func/partial produces a single-argument predicate by pre-filling string/equals?:
[
  "match",
  ["quote", { "type": "user", "profile": { "name": "Ada" } }],
  [
    {
      "type": ["func/partial", "string/equals?", ["quote", "user"]],
      "profile": { "name": "value/string?" }
    },
    ["lambda", ["value"], ["string/concat", ["object/get-path", "value", "profile.name"], " matched"]]
  ],
  ["lambda", ["value"], "value"]
]
Returns "Ada matched".

Host interop

A plain JavaScript function in the environment is called like any other:
["clamp", 0, 100, 150]
Returns 100.

Object path lookup

Dot-path traversal works on any nested object in the environment:
["object/get-path", "user", "stats.score"]
Returns 92.