Skip to main content
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:
  1. DecodeSchema.decodeUnknown validates the raw value and narrows it to LionExpressionType. Invalid input fails the Effect here without reaching the evaluator.
  2. Evaluateevaluate is flatMap-ed onto the decoded expression, producing an EvaluateResult Effect.
  3. Provide environmentmakeEnvironment wraps the caller-supplied bindings in a Ref-backed environment and provides it to LionEnvironmentService so all nested calls can access it.

Array dispatch and special forms

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.