Skip to main content
Lion is designed to sit on top of ordinary JavaScript. Any value you place in the evaluation environment — a plain object, a class constructor, a function, or a live instance — becomes a first-class citizen inside a Lion program. The object and func standard library modules expose the full surface of host JavaScript through simple, composable JSON expressions.

Accessing object properties

Use object/get to read a single named property from a host object, and object/get-path to traverse a nested structure with a dot-separated path. Numeric segments in a path index into arrays.
["object/get", { "a": 1 }, "a"]
["object/get-path", { "a": { "b": { "c": 42 } } }, "a.b.c"]
When a host object is available in the environment under a string key, pass the key instead of the literal object:
["object/get-path", "user", "stats.score"]
With { user: { name: "Ada", stats: { score: 92 } } } in the environment, this returns 92. The path "stats.score" resolves two levels deep. A path segment like "users.1.name" would index the second element of an array named users.

Calling object methods

object/call-method invokes a method by name on a host object, forwarding any additional arguments:
["object/call-method", "obj", "methodName", "arg1", "arg2"]
For methods nested deeper in an object tree, use object/call-method-path with a dot path:
["object/call-method-path", "obj", "nested.methodName", "arg1"]
Both forms bind this correctly — the method is called on the object at the path before the final key, so prototype methods work as expected. object/get-method and object/get-method-path retrieve the bound method without calling it immediately, which is handy when you want to store a method reference for later use:
["object/get-method", "obj", "methodName"]

Constructing classes

object/new calls a constructor with new, forwarding all arguments:
["object/new", "SomeClass", "arg1", "arg2"]
SomeClass must be present in the environment as a function. The result is a fully constructed instance that can then be used with object/call-method or stored with define.

Binding Lion lambdas as JavaScript callbacks

func/callback wraps a Lion lambda so it can be handed to any JavaScript API that expects a plain function — promise handlers, event listeners, array iterators, and so on. The wrapper captures the current Lion environment so the lambda continues to have access to all bindings:
["func/callback", ["lambda", ["x"], ["number/add", "x", 1]]]
When the callback is invoked by the host, it runs the lambda synchronously and returns the result, or returns a Promise if the lambda body yields an effectful value.

Partial application

func/partial pre-fills the leading arguments of any callable — host functions and Lion lambdas alike:
["func/partial", "fn", 1, 2]
The returned function accepts the remaining arguments when called. This is particularly useful inside match structural predicates, where you need a single-argument predicate but only have a two-argument equality function:
["func/partial", "string/equals?", ["quote", "user"]]
func/partial works on any value stored in the environment under a string key, including stdlib entries such as "string/equals?" or "number/greaterThan".

Real-world example: the agent package

The packages/agent package is the most complete demonstration of host interop. It injects into the Lion environment:
  • OpenTUI UI classes and layout enums
  • AI SDK streaming helpers
  • A model instance
  • Node fs
  • Zod schema constructors
  • A live renderer instance
The agent program in agent.json then orchestrates the entire terminal UI using only Lion expressions — constructing components with object/new, traversing nested APIs with object/get-path, calling methods with object/call-method, and attaching event handlers with func/callback.

Walkthrough: injecting a class, constructing it, and calling its methods

The following steps show the full lifecycle from TypeScript to Lion.
1

Inject the class into the environment

Add the constructor as an environment entry alongside stdlib. Any string key works; choose a name your Lion code will reference.
import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

class Counter {
  private count: number;
  constructor(initial: number) {
    this.count = initial;
  }
  increment(by: number) {
    this.count += by;
    return this.count;
  }
  value() {
    return this.count;
  }
}

const env = {
  ...stdlib,
  Counter,
};
2

Construct an instance with object/new

Inside a Lion begin block, use object/new to create an instance and define to bind it:
[
  "begin",
  ["define", "counter", ["object/new", "Counter", 0]],
  ["object/call-method", "counter", "increment", 5],
  ["object/call-method", "counter", "value"]
]
This evaluates to 5.
3

Run the program from TypeScript

Pass the program and environment to run, then execute with Effect:
const program = [
  "begin",
  ["define", "counter", ["object/new", "Counter", 0]],
  ["object/call-method", "counter", "increment", 5],
  ["object/call-method", "counter", "value"],
];

const result = await Effect.runPromise(run(program, env));
// => 5
4

Pass a Lion callback to a host method

If Counter had an onChange method that accepted a listener, you could attach a Lion lambda using func/callback:
[
  "begin",
  ["define", "counter", ["object/new", "Counter", 0]],
  [
    "object/call-method",
    "counter",
    "onChange",
    ["func/callback", ["lambda", ["newValue"], ["console/log", "newValue"]]]
  ],
  ["object/call-method", "counter", "increment", 10]
]
The lambda runs inside the Lion environment each time the host calls the listener.