Skip to main content
Special forms are array expressions whose first element is a reserved keyword. Unlike ordinary function calls, special forms are recognized by the evaluator before argument evaluation occurs, giving them control over when and whether their sub-expressions are evaluated. Lion has exactly seven special forms, and together they are sufficient to express any computation the language supports.
quote suppresses evaluation and returns its argument as a raw Lion value. It is the primary way to produce an array or string literal that should not be treated as a function call or an environment reference.Syntax: ["quote", <expression>]Returns: the argument, unchanged.
["quote", ["number/add", 1, 2]]
Returns the array ["number/add", 1, 2] — not 3.
["quote", "hello"]
Returns the string "hello" regardless of whether "hello" is bound in the environment.
Use quote any time you need to treat an array as data rather than as a call. It pairs naturally with eval to implement deferred or code-generating programs.
eval evaluates its argument and then evaluates the result a second time. This allows a Lion program to construct an expression at runtime and immediately execute it.Syntax: ["eval", <expression>]Returns: the result of evaluating the produced expression.
["eval", ["quote", ["number/add", 1, 2]]]
First pass: quote returns ["number/add", 1, 2]. Second pass: that array is evaluated as a function call, returning 3.Internally, evaluateEval validates the first-pass result against LionExpressionSchema before the second evaluation, so only valid Lion expressions can be double-evaluated.
export const evaluateEval = ([_, args]: typeof EvalFormSchema.Type) =>
  pipe(
    evaluate(args),
    Effect.flatMap(Schema.decodeUnknown(LionExpressionSchema)),
    Effect.flatMap(evaluate)
  );
begin evaluates a sequence of expressions in order and returns the value of the last one. It is the standard way to execute side-effecting forms (such as define) before producing a final result.Syntax: ["begin", <expr1>, <expr2>, ..., <exprN>]Returns: the value of <exprN>.
["begin",
  ["define", "x", 1],
  ["define", "x", 2],
  "x"]
Evaluates both define forms in order, then resolves "x" — returns 2.
["begin",
  ["console/log", "step 1"],
  ["console/log", "step 2"],
  42]
Logs both messages in order, returns 42.
define evaluates an expression and stores the result under a name in the toplevel (global) environment. The stored value is also returned, making define composable inside begin chains.Syntax: ["define", "<name>", <expression>]Returns: the evaluated value.
["define", "answer", 42]
Stores 42 under "answer" globally and returns 42.
["define", "double",
  ["lambda", ["x"], ["number/multiply", "x", 2]]]
Stores a lambda under "double". Later expressions can call it:
["double", 5]
Returns 10.
define always writes to the global environment, not to the nearest enclosing scope. Use lambda parameters to introduce locally-scoped bindings.
lambda creates a Lion function. It captures the environment at definition time (lexical closure) and creates a fresh inner environment for each call, binding parameter names to the supplied arguments.Syntax: ["lambda", ["<param1>", "<param2>", ...], <body>]Returns: a JavaScript function that, when called, evaluates <body> in the captured scope extended with the argument bindings.
["lambda", ["x"], ["number/add", "x", 1]]
A function that adds 1 to its argument.
[["lambda", ["x", "y"], ["number/multiply", "x", "y"]], 3, 4]
Immediately-invoked lambda — returns 12.Closures:
["begin",
  ["define", "base", 10],
  ["define", "addBase", ["lambda", ["n"], ["number/add", "base", "n"]]],
  ["addBase", 5]]
Returns 15. The lambda closes over "base" through the environment chain.Lambdas also observe define mutations made after creation, because they walk the environment chain at call time rather than copying values at definition time.
cond evaluates condition/result pairs in order and returns the result of the first pair whose condition is truthy. An optional else branch acts as a catch-all. If no condition matches and no else is present, cond returns null.Syntax:
["cond",
  [<condition1>, <result1>],
  [<condition2>, <result2>],
  ["else", <default-result>]]
Returns: the result of the first matching branch, or null.
["cond",
  [["number/greaterThan", "score", 90], "great"],
  [["number/greaterThan", "score", 70], "pass"],
  ["else", "retry"]]
With "score" bound to 85, the second branch matches — returns "pass".Without else:
["cond",
  [false, "never"]]
No branch matches — returns null.Conditions are evaluated lazily: as soon as one returns a truthy value, subsequent conditions are not evaluated.
match evaluates a value, tests it against a sequence of predicate/handler pairs, and calls the handler of the first matching pair with the value. The last argument is always a fallback handler that runs when no pattern matches.Syntax:
["match",
  <value-expression>,
  [<predicate1>, <handler1>],
  [<predicate2>, <handler2>],
  <fallback-handler>]
Returns: the result of calling the matched handler with the value.

Functional predicates

Any function that returns a boolean can be a predicate. The stdlib value/*? functions are designed for this:
["match",
  "value",
  ["value/number?", ["lambda", ["x"], ["number/add", "x", 1]]],
  ["value/string?", ["lambda", ["x"], ["string/concat", "x", "!"]]],
  ["lambda", ["x"], "x"]]
With "value" bound to 42 (a number), the first branch matches — returns 43. With "value" bound to "hi" (a string), the second branch matches — returns "hi!". Anything else falls through to the identity fallback.

Structural predicates

A predicate can also be a plain object whose keys must exist on the matched value and whose leaf values are predicates applied to the corresponding fields:
["match",
  { "type": "user", "profile": { "name": "Ada" } },
  [
    {
      "type": ["func/partial", "string/equals?", "user"],
      "profile": { "name": "value/string?" }
    },
    "value/identity"
  ],
  ["lambda", ["x"], null]]
The structural predicate checks that:
  • the value has a "type" field that equals "user" (using func/partial to apply string/equals?)
  • the value has a "profile" object with a "name" field that is a string
When the input object satisfies all field predicates, "value/identity" is called with the whole value and it is returned unchanged.
Structural predicates are evaluated recursively. A field value can itself be a structural predicate object, enabling deep pattern matching on nested data shapes without writing a ladder of cond expressions.