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 type | Example | How Lion uses it |
|---|
| Number | price: 100 | Resolves as a numeric literal |
| String | greeting: "hello" | Resolves as a string literal |
| Boolean | debug: false | Resolves as a boolean |
| Object | user: { name: "Ada" } | Traversable with object/get-path |
| Function | clamp: (min, max, v) => ... | Callable as ["clamp", ...] |
| Class constructor | Counter: Counter | Constructable with object/new |
| Class instance | db: dbInstance | Methods 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:
Returns 100.
Object path lookup
Dot-path traversal works on any nested object in the environment:
["object/get-path", "user", "stats.score"]
Returns 92.