Lion evaluation is a three-stage pipeline: the raw input is decoded against the LionExpressionSchema, dispatched to a handler based on its runtime type, and executed inside an Effect computation that carries the current environment as a service. Every step is modelled with Effect, giving Lion safe error propagation, dependency injection, and composable async execution with no boilerplate.
The evaluate function
evaluate is the central dispatcher. It receives an already-decoded LionExpressionType and uses Match to route to one of three base-form handlers:
export const evaluate = (expression: LionExpressionType): EvaluateResult =>
pipe(
Match.value(expression),
Match.when(Schema.is(LionArrayExpressionSchema), (_) => evaluateArray(_)),
Match.when(Schema.is(LionRecordExpressionSchema), (_) => evaluateRecord(_)),
Match.when(Schema.is(JsonPrimitiveSchema), (_) => evaluatePrimitive(_)),
Match.exhaustive
);
Match.exhaustive ensures the TypeScript compiler guarantees all three expression types are handled — no runtime fall-through is possible.
The run entry point
run is the public API. It accepts raw unknown input and a plain JavaScript environment object, then wires the pipeline together:
export const run = (
expression: unknown,
environment: Record<string, unknown>
) =>
pipe(
Schema.decodeUnknown(LionExpressionSchema)(expression),
Effect.flatMap(evaluate),
Effect.provideServiceEffect(
LionEnvironmentService,
makeEnvironment(environment)
)
);
Three things happen in order:
- Decode —
Schema.decodeUnknown validates the raw value and narrows it to LionExpressionType. Invalid input fails the Effect here without reaching the evaluator.
- Evaluate —
evaluate is flatMap-ed onto the decoded expression, producing an EvaluateResult Effect.
- Provide environment —
makeEnvironment wraps the caller-supplied bindings in a Ref-backed environment and provides it to LionEnvironmentService so all nested calls can access it.
Array expressions go through a second dispatch layer inside evaluateArray:
export const evaluateArray = (expression: LionArrayExpressionType) =>
pipe(
Match.value(expression),
Match.when(Arr.isEmptyReadonlyArray, Effect.succeed),
Match.when(Schema.is(EvalFormSchema), (_) => evaluateEval(_)),
Match.when(Schema.is(QuoteFormSchema), (_) => evaluateQuote(_)),
Match.when(Schema.is(BeginFormSchema), (_) => evaluateBegin(_)),
Match.when(Schema.is(DefineFormSchema), (_) => evaluateDefine(_)),
Match.when(Schema.is(LambdaFormSchema), (_) => evaluateLambda(_)),
Match.when(Schema.is(CondFormSchema), (_) => evaluateCond(_)),
Match.when(Schema.is(MatchFormSchema), (_) => evaluateMatch(_)),
Match.when(Schema.is(FunctionCallFormSchema), (_) => evaluateFunctionCall(_)),
Match.orElse(() => Effect.fail(new InvalidFunctionCallError({ expression })))
);
Special forms are checked before the generic FunctionCallFormSchema. That ordering is what gives keywords like "define" and "lambda" their privileged status — they match a schema that requires a specific literal string as the first element, so they take priority over any function the environment might have bound to the same name.
Primitive evaluation
Strings are the only primitive type with non-trivial behaviour. evaluatePrimitive checks whether the value is a string; if so it delegates to evaluateReference:
export const evaluateReference = (name: string) =>
pipe(
getService(LionEnvironmentService),
Effect.flatMap((environment) => getBinding(environment, name)),
Effect.map(Option.getOrElse(() => name))
);
getBinding walks the environment chain and returns Option.some(value) if found. Option.getOrElse(() => name) ensures an unbound string falls back to itself rather than failing — making bare string literals safe anywhere in an expression.
Record evaluation
Record evaluation is the simplest case — map evaluate over every value and collect results:
export const evaluateRecord = (expression: LionRecordExpressionType) =>
pipe(expression, Record.map(evaluate), Effect.all);
Effect.all runs all value Effects in parallel (subject to Effect’s concurrency model) and returns a new record with the same keys.
Running an expression
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
Effect.runPromise is the standard way to execute an Effect in an async context. For synchronous testing use Effect.runSync when the computation contains no async steps.