// SOURCE: ARCHITECTURE/32-LANGUAGE-OFFICIAL-13-IMPLEMENTATION-11.md
// AUTO-EXTRACTED - Do not edit directly. Edit the source document.

import type { Syntax, SIdent, SList } from "../syntax/syntax";
import { isIdent, isList } from "../syntax/syntax";
import type { Env } from "../syntax/binding";
import { resolveIdent } from "../syntax/binding";
import type { Expr, HandlerExpr, Pattern } from "../ast";

function expectIdent(stx: Syntax, msg: string): SIdent {
  if (!isIdent(stx)) throw new Error(msg);
  return stx;
}
function expectList(stx: Syntax, msg: string): SList {
  if (!isList(stx)) throw new Error(msg);
  return stx;
}

function lowerVar(id: SIdent, env: Env, phase: number): Expr {
  const b = resolveIdent(id, env, phase);
  if (!b) return { tag: "Var", name: id.name };
  return { tag: "Var", name: b.value as string };
}

/**
 * Check if a syntax item is a define form (either (define x e) or (define (f args...) body...))
 */
function isDefineForm(stx: Syntax): boolean {
  if (!isList(stx)) return false;
  const items = stx.items;
  if (items.length < 2) return false;
  const head = items[0];
  return isIdent(head) && head.name === "define";
}

/**
 * Lower a body sequence that may contain internal defines (Racket-style).
 * Transforms leading defines into a letrec wrapper.
 *
 * (define (f x) x)
 * (define y 5)
 * (+ (f y) 1)
 *
 * becomes:
 *
 * (letrec ((f (lambda (x) x))
 *          (y 5))
 *   (+ (f y) 1))
 */
function lowerBodyWithDefines(bodyForms: Syntax[], env: Env, phase: number): Expr {
  // Separate leading defines from rest
  let defCount = 0;
  while (defCount < bodyForms.length && isDefineForm(bodyForms[defCount])) {
    defCount++;
  }

  // If no internal defines, just lower normally
  if (defCount === 0) {
    const lowered = bodyForms.map(x => lowerSyntax(x, env, phase));
    return lowered.length === 1 ? lowered[0] : { tag: "Begin", exprs: lowered };
  }

  // Extract define forms and remaining body
  const defines = bodyForms.slice(0, defCount);
  const restBody = bodyForms.slice(defCount);

  // If no body after defines, use (void) as body
  if (restBody.length === 0) {
    restBody.push({ tag: "Atom", value: null } as Syntax);
  }

  // Convert defines to letrec bindings
  const bindings: Array<{ name: string; init: Expr }> = [];

  for (const def of defines) {
    const defList = def as SList;
    const nameOrSig = defList.items[1];

    if (isIdent(nameOrSig)) {
      // Simple define: (define x e)
      const b = resolveIdent(nameOrSig, env, phase);
      const name = b ? (b.value as string) : nameOrSig.name;
      const init = lowerSyntax(defList.items[2], env, phase);
      bindings.push({ name, init });
    } else if (isList(nameOrSig)) {
      // Function define: (define (f args...) body...)
      const sigList = nameOrSig as SList;
      if (sigList.items.length < 1) {
        throw new Error("define: function signature must have at least a name");
      }
      const fnName = expectIdent(sigList.items[0], "define: function name must be ident");
      const b = resolveIdent(fnName, env, phase);
      const name = b ? (b.value as string) : fnName.name;

      // Build lambda from params and body
      const params = sigList.items.slice(1).map(p => {
        const pid = expectIdent(p, "define: param must be ident");
        const pb = resolveIdent(pid, env, phase);
        return pb ? (pb.value as string) : pid.name;
      });

      const fnBodyForms = defList.items.slice(2);
      // Recursively handle internal defines in function body
      const fnBody = lowerBodyWithDefines(fnBodyForms, env, phase);

      const lambda: Expr = { tag: "Lambda", params, body: fnBody };
      bindings.push({ name, init: lambda });
    } else {
      throw new Error("define: name must be identifier or (name params...)");
    }
  }

  // Lower the rest of the body (which may also have internal defines)
  const body = lowerBodyWithDefines(restBody, env, phase);

  // Build letrec expression
  // letrec is desugared to: let with *uninit*, then set! for each
  // But we can use the Letrec AST node directly if available, or simulate with let + set!

  // For simplicity, use the existing letrec lowering pattern:
  // (letrec ((x init) ...) body)
  // -> (let ((x *uninit*) ...)
  //      (begin (set! x init) ... body))

  const uninitExprs: Array<{ name: string; init: Expr }> = bindings.map(b => ({
    name: b.name,
    init: { tag: "App", fn: { tag: "Var", name: "*uninit*" }, args: [] },
  }));

  const setExprs: Expr[] = bindings.map(b => ({
    tag: "Set" as const,
    name: b.name,
    rhs: b.init,
  }));

  // Build: ((lambda (x y ...) (begin (set! x init-x) (set! y init-y) ... body)) *uninit* *uninit* ...)
  const paramNames = bindings.map(b => b.name);
  const innerBody: Expr = { tag: "Begin", exprs: [...setExprs, body] };
  const lambda: Expr = { tag: "Lambda", params: paramNames, body: innerBody };
  const uninitArgs = bindings.map(() => ({ tag: "App" as const, fn: { tag: "Var" as const, name: "*uninit*" }, args: [] }));

  return { tag: "App", fn: lambda, args: uninitArgs };
}

export function lowerSyntax(stx: Syntax, env: Env, phase = 0): Expr {
  if (stx.tag === "Atom") {
    const v = stx.value;
    if (typeof v === "number" || typeof v === "string" || typeof v === "boolean" || v === null) {
      return { tag: "Lit", value: v };
    }
    // Character literal: { $char: "x" }
    if (typeof v === "object" && v !== null && "$char" in v) {
      return { tag: "Lit", value: { $char: (v as any).$char } };
    }
    return { tag: "Lit", value: String(v) };
  }

  if (stx.tag === "Ident") {
    // Keywords (symbols starting with :) are self-evaluating
    if (stx.name.startsWith(":")) {
      return { tag: "Quote", datum: { sym: stx.name } };
    }
    return lowerVar(stx, env, phase);
  }

  const items = stx.items;
  if (items.length === 0) return { tag: "Quote", datum: [] };

  const h = items[0];

  if (isIdent(h)) {
    switch (h.name) {
      case "quote": {
        if (items.length !== 2) throw new Error("quote: expected (quote datum)");
        return { tag: "Quote", datum: stripDatum(items[1]) };
      }

      case "quasiquote": {
        if (items.length !== 2) throw new Error("quasiquote: expected (quasiquote template)");
        return lowerQuasiquote(items[1], env, phase);
      }

      case "begin": {
        // Top-level begin: keep defines as Define expressions (they extend the env)
        // Internal defines in lambda/let bodies use lowerBodyWithDefines for letrec semantics
        const lowered = items.slice(1).map(x => lowerSyntax(x, env, phase));
        return lowered.length === 1 ? lowered[0] : { tag: "Begin", exprs: lowered };
      }

      case "if": {
        if (items.length !== 4) throw new Error("if: expected (if t c a)");
        return {
          tag: "If",
          test: lowerSyntax(items[1], env, phase),
          conseq: lowerSyntax(items[2], env, phase),
          alt: lowerSyntax(items[3], env, phase),
        };
      }

      case "lambda": {
        if (items.length < 3) throw new Error("lambda: expected (lambda (params) body...)");

        // Support variadic lambda forms:
        //   (lambda args body)          — all args collected into rest
        //   (lambda (x y) body)         — normal params
        //   (lambda (x y . rest) body)  — mixed: required params + rest
        let params: string[] = [];
        let rest: string | undefined;

        if (isIdent(items[1])) {
          // (lambda args body) — bare symbol, all args go to rest
          const id = items[1] as SIdent;
          const b = resolveIdent(id, env, phase);
          if (!b) throw new Error("lambda rest param did not resolve: " + id.name);
          rest = b.value as string;
        } else {
          const paramsList = expectList(items[1], "lambda: params must be list or ident");
          // Check for dotted pair: (x y . rest) — "." appears as a symbol in the list
          const dotIdx = paramsList.items.findIndex(p => isIdent(p) && (p as SIdent).name === ".");
          if (dotIdx >= 0 && dotIdx === paramsList.items.length - 2) {
            // Dotted pair form: elements before "." are required, element after is rest
            const reqParams = paramsList.items.slice(0, dotIdx);
            params = reqParams.map(p => {
              const id = expectIdent(p, "lambda param must be ident");
              const b = resolveIdent(id, env, phase);
              if (!b) throw new Error("lambda param did not resolve");
              return b.value as string;
            });
            const restId = expectIdent(paramsList.items[dotIdx + 1], "lambda rest param must be ident");
            const bRest = resolveIdent(restId, env, phase);
            if (!bRest) throw new Error("lambda rest param did not resolve: " + restId.name);
            rest = bRest.value as string;
          } else {
            // Normal params list
            params = paramsList.items.map(p => {
              const id = expectIdent(p, "lambda param must be ident");
              const b = resolveIdent(id, env, phase);
              if (!b) throw new Error("lambda param did not resolve");
              return b.value as string;
            });
          }
        }

        // Support internal defines in lambda body
        const body = lowerBodyWithDefines(items.slice(2), env, phase);
        return { tag: "Lambda", params, ...(rest ? { rest } : {}), body };
      }

      case "oracle-lambda": {
        // (oracle-lambda (params...) spec)
        // Creates an OracleProc - first-class inference procedure
        if (items.length !== 3) throw new Error("oracle-lambda: expected (oracle-lambda (params) spec)");
        const paramsList = expectList(items[1], "oracle-lambda: params must be list");
        const params = paramsList.items.map(p => {
          const id = expectIdent(p, "oracle-lambda param must be ident");
          const b = resolveIdent(id, env, phase);
          if (!b) throw new Error("oracle-lambda param did not resolve");
          return b.value as string;
        });
        const spec = lowerSyntax(items[2], env, phase);
        return { tag: "OracleLambda", params, spec };
      }

      case "let": {
        if (items.length < 3) throw new Error("let: expected (let ((x e) ...) body...)");
        const bindsList = expectList(items[1], "let: bindings must be list");
        const bindPairs = bindsList.items.map(bp0 => {
          const bp = expectList(bp0, "let: binding must be list");
          if (bp.items.length !== 2) throw new Error("let: binding must be (x init)");
          const id = expectIdent(bp.items[0], "let: binder must be ident");
          const b = resolveIdent(id, env, phase);
          if (!b) throw new Error("let binder did not resolve");
          return { internal: b.value as string, init: lowerSyntax(bp.items[1], env, phase) };
        });

        const params = bindPairs.map(p => p.internal);
        const args = bindPairs.map(p => p.init);

        // Support internal defines in let body
        const body = lowerBodyWithDefines(items.slice(2), env, phase);

        return { tag: "App", fn: { tag: "Lambda", params, body }, args };
      }

      case "letrec": {
        if (items.length < 3) throw new Error("letrec: expected (letrec ((x e) ...) body...)");
        const bindsList = expectList(items[1], "letrec: bindings must be list");

        const bindPairs = bindsList.items.map(bp0 => {
          const bp = expectList(bp0, "letrec: binding must be list");
          if (bp.items.length !== 2) throw new Error("letrec: binding must be (x init)");
          const id = expectIdent(bp.items[0], "letrec: binder must be ident");
          const b = resolveIdent(id, env, phase);
          if (!b) throw new Error("letrec binder did not resolve");
          return { internal: b.value as string, init: lowerSyntax(bp.items[1], env, phase) };
        });

        const params = bindPairs.map(p => p.internal);

        const placeholder: Expr = { tag: "App", fn: { tag: "Var", name: "*uninit*" }, args: [] };
        const args = bindPairs.map(_ => placeholder);

        const sets: Expr[] = bindPairs.map(p => ({ tag: "Set", name: p.internal, rhs: p.init }));
        // Support internal defines in letrec body
        const loweredBodyForms = lowerBodyWithDefines(items.slice(2), env, phase);
        const bodySeq: Expr[] = [...sets, loweredBodyForms];
        const body: Expr = bodySeq.length === 1 ? bodySeq[0] : { tag: "Begin", exprs: bodySeq };

        return { tag: "App", fn: { tag: "Lambda", params, body }, args };
      }

      case "let*": {
        // (let* ((x1 e1) (x2 e2) ...) body...)
        // Desugars to nested let: (let ((x1 e1)) (let ((x2 e2)) ... body))
        if (items.length < 3) throw new Error("let*: expected (let* ((x e) ...) body...)");
        const bindsList = expectList(items[1], "let*: bindings must be list");
        const bodyForms = items.slice(2);

        if (bindsList.items.length === 0) {
          // Empty bindings: just evaluate body with internal defines support
          return lowerBodyWithDefines(bodyForms, env, phase);
        }

        // Build nested let from inside out
        // Support internal defines in innermost body
        let result: Expr = lowerBodyWithDefines(bodyForms, env, phase);

        for (let i = bindsList.items.length - 1; i >= 0; i--) {
          const bp = expectList(bindsList.items[i], "let*: binding must be list");
          if (bp.items.length !== 2) throw new Error("let*: binding must be (x init)");
          const id = expectIdent(bp.items[0], "let*: binder must be ident");
          const b = resolveIdent(id, env, phase);
          if (!b) throw new Error("let* binder did not resolve");
          const internal = b.value as string;
          const init = lowerSyntax(bp.items[1], env, phase);

          // Wrap in: ((lambda (x) result) init)
          result = {
            tag: "App",
            fn: { tag: "Lambda", params: [internal], body: result },
            args: [init],
          };
        }

        return result;
      }

      case "define": {
        if (items.length !== 3) throw new Error("define: expected (define x rhs)");
        const id = expectIdent(items[1], "define: name must be ident");
        const b = resolveIdent(id, env, phase);
        if (!b) throw new Error("define binder did not resolve");
        return { tag: "Define", name: b.value as string, rhs: lowerSyntax(items[2], env, phase) };
      }

      case "set!": {
        if (items.length !== 3) throw new Error("set!: expected (set! x rhs)");
        const id = expectIdent(items[1], "set!: target must be ident");
        const b = resolveIdent(id, env, phase);
        if (!b) throw new Error("set! target did not resolve");
        return { tag: "Set", name: b.value as string, rhs: lowerSyntax(items[2], env, phase) };
      }

      case "try": {
        // Clojure-style: (try expr (catch e handler-body...))
        // Also supports: (try expr (catch e handler1 handler2...))
        if (items.length < 3) throw new Error("try: expected (try expr (catch e body...))");
        const body = lowerSyntax(items[1], env, phase);
        const catchForm = expectList(items[2], "try: expected (catch e body...)");
        if (catchForm.items.length < 3) throw new Error("try catch: expected (catch e body...)");
        const catchHead = expectIdent(catchForm.items[0], "try: catch keyword expected");
        if (catchHead.name !== "catch") throw new Error("try: second form must be (catch ...)");
        const errId = expectIdent(catchForm.items[1], "try catch: error binder must be ident");
        // Use raw name for catch variable — it's a simple lexical binding introduced
        // by the catch form, not an existing binding that needs hygiene resolution.
        const errVar = errId.name;
        const handlerBody = catchForm.items.slice(2).map(x => lowerSyntax(x, env, phase));
        const handler = handlerBody.length === 1 ? handlerBody[0] : { tag: "Begin" as const, exprs: handlerBody };
        return { tag: "TryCatch", body, errVar, handler };
      }

      case "effect": {
        if (items.length < 2) throw new Error("effect: expected (effect op arg...)");
        const opStx = items[1];
        const op = opStx.tag === "Ident" ? opStx.name : String((opStx as any).value);
        return { tag: "Effect", op, args: items.slice(2).map(x => lowerSyntax(x, env, phase)) };
      }

      case "handle": {
        if (items.length < 2) throw new Error("handle: expected (handle body clauses...)");
        const body = lowerSyntax(items[1], env, phase);
      const handler = lowerHandler(items.slice(2), env, phase);
      return { tag: "Handle", body, handler };
    }

    case "handler-bind": {
      if (items.length < 3) throw new Error("handler-bind: expected (handler-bind ((type handler) ...) body...)");
      const bindsList = expectList(items[1], "handler-bind: bindings must be list");
      const handlers = bindsList.items.map(bp0 => {
        const bp = expectList(bp0, "handler-bind: binding must be list");
        if (bp.items.length !== 2) throw new Error("handler-bind: binding must be (type handler)");
        const typeId = expectIdent(bp.items[0], "handler-bind: type must be ident");
        const handlerExpr = lowerSyntax(bp.items[1], env, phase);
        return { type: typeId.name, handler: handlerExpr };
      });
      const bodyForms = items.slice(2).map(x => lowerSyntax(x, env, phase));
      const body: Expr = bodyForms.length === 1 ? bodyForms[0] : { tag: "Begin", exprs: bodyForms };
      const handlerPairs: Expr[] = handlers.map(h => ({
        tag: "App",
        fn: { tag: "Var", name: "cons" },
        args: [
          { tag: "Quote", datum: { sym: h.type } },
          h.handler,
        ],
      }));
      const handlerList: Expr = { tag: "App", fn: { tag: "Var", name: "list" }, args: handlerPairs };
      const thunk: Expr = { tag: "Lambda", params: [], body };
      return { tag: "App", fn: { tag: "Var", name: "handler-bind" }, args: [handlerList, thunk] };
    }

    case "restart-bind": {
      if (items.length < 3) throw new Error("restart-bind: expected (restart-bind ((name fn) ...) body...)");
      const bindsList = expectList(items[1], "restart-bind: bindings must be list");
      const restarts = bindsList.items.map(bp0 => {
        const bp = expectList(bp0, "restart-bind: binding must be list");
        if (bp.items.length !== 2) throw new Error("restart-bind: binding must be (name fn)");
        const nameId = expectIdent(bp.items[0], "restart-bind: name must be ident");
        const fnExpr = lowerSyntax(bp.items[1], env, phase);
        return { name: nameId.name, fn: fnExpr };
      });
      const bodyForms = items.slice(2).map(x => lowerSyntax(x, env, phase));
      const body: Expr = bodyForms.length === 1 ? bodyForms[0] : { tag: "Begin", exprs: bodyForms };
      const restartPairs: Expr[] = restarts.map(r => ({
        tag: "App",
        fn: { tag: "Var", name: "cons" },
        args: [
          { tag: "Quote", datum: { sym: r.name } },
          r.fn,
        ],
      }));
      const restartsList: Expr = { tag: "App", fn: { tag: "Var", name: "list" }, args: restartPairs };
      const thunk: Expr = { tag: "Lambda", params: [], body };
      return { tag: "App", fn: { tag: "Var", name: "restart-bind" }, args: [restartsList, thunk] };
    }

    case "match": {
      // (match scrutinee (pat1 body1) (pat2 body2) ...)
      if (items.length < 2) throw new Error("match: expected (match scrutinee clauses...)");
      const scrutinee = lowerSyntax(items[1], env, phase);
        const clauses = items.slice(2).map(cl => {
          const clauseList = expectList(cl, "match clause must be list");
          if (clauseList.items.length < 2) throw new Error("match clause must have pattern and body");
          const pat = lowerPattern(clauseList.items[0], env, phase);
          const bodyForms = clauseList.items.slice(1).map(x => lowerSyntax(x, env, phase));
          const body: Expr = bodyForms.length === 1 ? bodyForms[0] : { tag: "Begin", exprs: bodyForms };
          return { pat, body };
        });
        return { tag: "Match", scrutinee, clauses };
      }

      case "amb": {
        // (amb e1 e2 ... en) - nondeterministic choice
        // Desugar to: (effect amb.choose (list (lambda () e1) (lambda () e2) ...))
        // Each alternative is wrapped in a thunk to preserve laziness
        if (items.length < 2) throw new Error("amb: expected at least one alternative");
        const thunks: Expr[] = items.slice(1).map(alt => ({
          tag: "Lambda",
          params: [],
          body: lowerSyntax(alt, env, phase),
        }));
        // Build (list thunk1 thunk2 ...)
        const listExpr: Expr = {
          tag: "App",
          fn: { tag: "Var", name: "list" },
          args: thunks,
        };
        return { tag: "Effect", op: "amb.choose", args: [listExpr] };
      }

      case "mplus": {
        // (mplus e1 e2 ... en) - binary/variadic monadic choice (lazy)
        // Desugar to amb.choose over zero-arg thunks to avoid eager branch evaluation
        if (items.length === 1) {
          return { tag: "Effect", op: "amb.fail", args: [] };
        }
        const thunks: Expr[] = items.slice(1).map(alt => ({
          tag: "Lambda",
          params: [],
          body: lowerSyntax(alt, env, phase),
        }));
        const listExpr: Expr = { tag: "App", fn: { tag: "Var", name: "list" }, args: thunks };
        return { tag: "Effect", op: "amb.choose", args: [listExpr] };
      }

      case "require": {
        // (require pred) - fail if predicate is false
        // Desugar to: (if pred unit (effect amb.fail "require failed"))
        if (items.length !== 2) throw new Error("require: expected (require pred)");
        const pred = lowerSyntax(items[1], env, phase);
        return {
          tag: "If",
          test: pred,
          conseq: { tag: "Lit", value: null }, // unit
          alt: { tag: "Effect", op: "amb.fail", args: [{ tag: "Lit", value: "require failed" }] },
        };
      }

      case "delay": {
        // (delay expr) - create a memoizing thunk
        // Desugar to: (make-promise (lambda () expr))
        if (items.length !== 2) throw new Error("delay: expected (delay expr)");
        const body = lowerSyntax(items[1], env, phase);
        const thunk: Expr = { tag: "Lambda", params: [], body };
        return {
          tag: "App",
          fn: { tag: "Var", name: "make-promise" },
          args: [thunk],
        };
      }

      case "cons-stream": {
        // (cons-stream a b) - create stream pair with delayed tail
        // Desugar to: (cons a (delay b))
        if (items.length !== 3) throw new Error("cons-stream: expected (cons-stream head tail)");
        const head = lowerSyntax(items[1], env, phase);
        const tail = lowerSyntax(items[2], env, phase);
        const delayedTail: Expr = {
          tag: "App",
          fn: { tag: "Var", name: "make-promise" },
          args: [{ tag: "Lambda", params: [], body: tail }],
        };
        return {
          tag: "App",
          fn: { tag: "Var", name: "cons" },
          args: [head, delayedTail],
        };
      }

      case "cond": {
        // (cond (test1 e1...) (test2 e2...) ... (else en...))
        // Desugars to nested if: (if test1 (begin e1...) (if test2 (begin e2...) ...))
        const clauses = items.slice(1);
        if (clauses.length === 0) {
          // Empty cond returns #f
          return { tag: "Lit", value: false };
        }

        // Process clauses from right to left to build nested if
        let result: Expr = { tag: "Lit", value: false }; // default: #f if no else

        for (let i = clauses.length - 1; i >= 0; i--) {
          const clause = clauses[i];
          const clauseList = expectList(clause, "cond clause must be list");
          if (clauseList.items.length === 0) {
            throw new Error("cond: empty clause");
          }

          const testStx = clauseList.items[0];
          const bodyItems = clauseList.items.slice(1);

          // Check for 'else' clause
          const isElse = isIdent(testStx) && testStx.name === "else";

          if (isElse) {
            // (else e1 e2 ...) → body with internal defines support
            if (bodyItems.length === 0) {
              result = { tag: "Lit", value: null }; // unit
            } else {
              // Support internal defines in cond else branch
              result = lowerBodyWithDefines(bodyItems, env, phase);
            }
          } else {
            // (test e1 e2 ...) → (if test body <rest>)
            let body: Expr;
            if (bodyItems.length === 0) {
              // (test) with no body - return test value (like Scheme)
              body = lowerSyntax(testStx, env, phase);
            } else {
              // Support internal defines in cond branch
              body = lowerBodyWithDefines(bodyItems, env, phase);
            }

            result = {
              tag: "If",
              test: lowerSyntax(testStx, env, phase),
              conseq: body,
              alt: result,
            };
          }
        }

        return result;
      }

      case "case": {
        // (case expr ((val1 val2) body1) ((val3) body2) (else body3))
        // Desugars to: (let ((tmp expr))
        //               (cond ((or (eq? tmp 'val1) (eq? tmp 'val2)) body1)
        //                     ((eq? tmp 'val3) body2)
        //                     (else body3)))
        if (items.length < 2) throw new Error("case: expected (case expr clauses...)");

        const keyExpr = lowerSyntax(items[1], env, phase);
        const clauses = items.slice(2);

        if (clauses.length === 0) {
          return { tag: "Lit", value: false };
        }

        // We'll desugar directly to nested if without creating a let
        // Each clause tests (or (eq? key 'val1) (eq? key 'val2) ...)
        let result: Expr = { tag: "Lit", value: false };

        for (let i = clauses.length - 1; i >= 0; i--) {
          const clause = clauses[i];
          const clauseList = expectList(clause, "case clause must be list");
          if (clauseList.items.length < 2) {
            throw new Error("case: clause must have data list and body");
          }

          const datumsPart = clauseList.items[0];
          const bodyItems = clauseList.items.slice(1);

          // Check for 'else' clause
          const isElse = isIdent(datumsPart) && datumsPart.name === "else";

          let body: Expr;
          if (bodyItems.length === 1) {
            body = lowerSyntax(bodyItems[0], env, phase);
          } else {
            body = { tag: "Begin", exprs: bodyItems.map(b => lowerSyntax(b, env, phase)) };
          }

          if (isElse) {
            result = body;
          } else {
            // datumsPart should be a list of values to match: (val1 val2 ...)
            const datumsList = expectList(datumsPart, "case: datum list must be list");
            const datums = datumsList.items;

            if (datums.length === 0) {
              // Empty datum list - skip this clause
              continue;
            }

            // Build test: (or (eq? key 'val1) (eq? key 'val2) ...)
            // For simplicity, we'll build nested if for multiple values
            // Actually, we'll build: (if (eq? key 'val1) #t (if (eq? key 'val2) #t ... #f))
            // Then wrap: (if <test> body result)

            // Build the combined test expression
            let test: Expr;
            if (datums.length === 1) {
              // Single value: (eq? key 'val)
              test = {
                tag: "App",
                fn: { tag: "Var", name: "eq?" },
                args: [keyExpr, { tag: "Quote", datum: stripDatum(datums[0]) }],
              };
            } else {
              // Multiple values: (or (eq? key 'val1) (eq? key 'val2) ...)
              // Since we don't have 'or', use nested if:
              // (if (eq? key 'val1) #t (if (eq? key 'val2) #t ... #f))
              let orTest: Expr = { tag: "Lit", value: false };
              for (let j = datums.length - 1; j >= 0; j--) {
                const eqTest: Expr = {
                  tag: "App",
                  fn: { tag: "Var", name: "eq?" },
                  args: [keyExpr, { tag: "Quote", datum: stripDatum(datums[j]) }],
                };
                orTest = {
                  tag: "If",
                  test: eqTest,
                  conseq: { tag: "Lit", value: true },
                  alt: orTest,
                };
              }
              test = orTest;
            }

            result = {
              tag: "If",
              test,
              conseq: body,
              alt: result,
            };
          }
        }

        return result;
      }
    }
  }

  return { tag: "App", fn: lowerSyntax(items[0], env, phase), args: items.slice(1).map(x => lowerSyntax(x, env, phase)) };
}

/**
 * Lower quasiquote template to Expr.
 * `(a ,b ,@c d) → (append (list (quote a)) (list b) c (list (quote d)))
 *
 * Simple atoms/idents: quote them.
 * (unquote x): evaluate x.
 * (unquote-splicing x): splice x into the list.
 * Lists: recursively process.
 */
function lowerQuasiquote(stx: Syntax, env: Env, phase: number): Expr {
  // Non-list: just quote it (or evaluate if it's an unquote)
  if (!isList(stx)) {
    if (isIdent(stx)) {
      // Keywords are self-evaluating even in quasiquote
      if (stx.name.startsWith(":")) {
        return { tag: "Quote", datum: { sym: stx.name } };
      }
      return { tag: "Quote", datum: { sym: stx.name } };
    }
    if (stx.tag === "Atom") return { tag: "Quote", datum: stx.value };
    return { tag: "Quote", datum: null };
  }

  const items = stx.items;
  if (items.length === 0) return { tag: "Quote", datum: [] };

  // Check for (unquote expr)
  if (items.length === 2 && isIdent(items[0]) && items[0].name === "unquote") {
    return lowerSyntax(items[1], env, phase);
  }

  // Check for (unquote-splicing expr) — error at top level
  if (items.length === 2 && isIdent(items[0]) && items[0].name === "unquote-splicing") {
    throw new Error("unquote-splicing not inside list");
  }

  // List: build via cons/list/append
  // Each element becomes either (list (quote x)), (list evaluated), or a splice
  const segments: Expr[] = [];
  for (const item of items) {
    if (isList(item) && item.items.length === 2 && isIdent(item.items[0]) && item.items[0].name === "unquote-splicing") {
      // ,@expr → expr (must produce a list)
      segments.push(lowerSyntax(item.items[1], env, phase));
    } else if (isList(item) && item.items.length === 2 && isIdent(item.items[0]) && item.items[0].name === "unquote") {
      // ,expr → (list expr)
      segments.push({
        tag: "App",
        fn: { tag: "Var", name: "list" },
        args: [lowerSyntax(item.items[1], env, phase)],
      });
    } else {
      // Regular element → (list (quasiquote-expand element))
      segments.push({
        tag: "App",
        fn: { tag: "Var", name: "list" },
        args: [lowerQuasiquote(item, env, phase)],
      });
    }
  }

  // If single segment, return it directly
  if (segments.length === 1) return segments[0];

  // Multiple segments: (append seg1 seg2 ...)
  return {
    tag: "App",
    fn: { tag: "Var", name: "append" },
    args: segments,
  };
}

function stripDatum(stx: Syntax): unknown {
  if (stx.tag === "Atom") return stx.value;
  if (stx.tag === "Ident") return { sym: stx.name };
  return stx.items.map(stripDatum);
}

function lowerHandler(clauses: Syntax[], env: Env, phase: number): HandlerExpr {
  const on: HandlerExpr["on"] = [];
  let ret: HandlerExpr["ret"] | undefined;
  let fin: HandlerExpr["fin"] | undefined;

  for (const cl of clauses) {
    const lst = expectList(cl, "handle clause must be list");
    if (lst.items.length < 2) throw new Error("handle clause too short");
    const tag = expectIdent(lst.items[0], "handle clause tag must be ident").name;

    if (tag === "on") {
      if (lst.items.length !== 4) throw new Error("on: expected (on op (x k) body)");
      const op = expectIdent(lst.items[1], "on: op must be ident").name;
      const argsList = expectList(lst.items[2], "on: (x k) must be list");
      if (argsList.items.length !== 2) throw new Error("on: expected (x k)");
      const x = expectIdent(argsList.items[0], "on: x must be ident").name;
      const k = expectIdent(argsList.items[1], "on: k must be ident").name;
      const body = lowerSyntax(lst.items[3], env, phase);
      on.push({ op, params: [x], k, body });
      continue;
    }

    if (tag === "return") {
      if (lst.items.length !== 3) throw new Error("return: expected (return (v) body)");
      const vList = expectList(lst.items[1], "return: (v) must be list");
      if (vList.items.length !== 1) throw new Error("return: expected one var");
      const v = expectIdent(vList.items[0], "return var must be ident").name;
      const body = lowerSyntax(lst.items[2], env, phase);
      ret = { v, body };
      continue;
    }

    if (tag === "finally") {
      if (lst.items.length !== 2) throw new Error("finally: expected (finally body)");
      fin = { body: lowerSyntax(lst.items[1], env, phase) };
      continue;
    }

    throw new Error(`unknown handle clause: ${tag}`);
  }

  return { on, ret, fin };
}

/**
 * Lower a pattern syntax to a Pattern AST node.
 * Supports:
 * - _ (wildcard)
 * - ?x (pattern variable binding)
 * - literals (numbers, strings, booleans, null)
 * - symbols (quoted identifiers become literal symbols)
 * - (p1 p2 ...) or [p1 p2 ...] (list/vector patterns)
 * - else (wildcard, same as _)
 */
function lowerPattern(stx: Syntax, env: Env, phase: number): Pattern {
  // Atom: literal pattern (number, string, boolean, null)
  if (stx.tag === "Atom") {
    const v = stx.value;
    if (typeof v === "number" || typeof v === "string" || typeof v === "boolean" || v === null) {
      return { tag: "PLit", value: v };
    }
    return { tag: "PLit", value: String(v) };
  }

  // Identifier: check for special patterns
  if (stx.tag === "Ident") {
    const name = stx.name;

    // Wildcard patterns
    if (name === "_" || name === "else") {
      return { tag: "PWild" };
    }

    // Pattern variable: starts with ?
    if (name.startsWith("?")) {
      const varName = name.slice(1);
      // Resolve through binding env if possible, otherwise use as-is
      const b = resolveIdent({ ...stx, name: varName }, env, phase);
      return { tag: "PVar", name: b ? (b.value as string) : varName };
    }

    // Quoted symbol pattern: match a symbol with this name
    // For bare identifiers in patterns, treat as literal symbol match
    return { tag: "PLit", value: name };
  }

  // List: vector pattern
  if (stx.tag === "List") {
    // Check for special pattern forms
    if (stx.items.length > 0) {
      const head = stx.items[0];
      if (isIdent(head)) {
        // (quote x) in pattern means literal value
        if (head.name === "quote" && stx.items.length === 2) {
          const datum = stripPatternDatum(stx.items[1]);
          return datumToPattern(datum);
        }
      }
    }

    // Regular list/vector pattern
    const items = stx.items.map(item => lowerPattern(item, env, phase));
    return { tag: "PVector", items };
  }

  throw new Error(`Cannot lower pattern: ${JSON.stringify(stx)}`);
}

/** Convert a stripped datum to a pattern */
function datumToPattern(d: unknown): Pattern {
  if (d === null) return { tag: "PLit", value: null };
  if (typeof d === "number") return { tag: "PLit", value: d };
  if (typeof d === "boolean") return { tag: "PLit", value: d };
  if (typeof d === "string") return { tag: "PLit", value: d };
  if (typeof d === "object" && d !== null && "sym" in d) {
    // Symbol: match as literal symbol name
    return { tag: "PLit", value: (d as { sym: string }).sym };
  }
  if (Array.isArray(d)) {
    return { tag: "PVector", items: d.map(datumToPattern) };
  }
  return { tag: "PLit", value: JSON.stringify(d) };
}

function stripPatternDatum(stx: Syntax): unknown {
  if (stx.tag === "Atom") return stx.value;
  if (stx.tag === "Ident") return { sym: stx.name };
  return stx.items.map(stripPatternDatum);
}
