CodeBlanks Logo Icon CodeBlanks

Challenge 4: Mini compiler

Solve a task testing the full advanced level.

Mini compiler: AST + guard + exhaustive

The advanced level in summary: model an AST with a discriminated union, narrow with type guards and switch, enforce exhaustiveness with never, and build the expression tree with a recursive type. Together they yield a mini compiler that is compile-time safe and runtime-correct.

type Expr =
  | { kind: "num"; value: number }
  | { kind: "add"; left: Expr; right: Expr };

function evalE(e: Expr): number {
  switch (e.kind) {
    case "num": return e.value;
    case "add": return evalE(e.left) + evalE(e.right);
  }
}

If you add a new node kind, the never check forces every switch to be updated.

Practice Expression evaluator

Write an Expr discriminated union: num (value: number), add (left, right: Expr), mul (left, right: Expr). Write evalE(e: Expr): number with a switch and an exhaustive const _: never = e in the default branch. Build the expression (1 + 2) * 3 and print the result.

Starter code

index.ts
// Expr: num | add | mul (discriminated union, recursive)
// evalE(e: Expr): number — switch ile daralt, never ile exhaustive yap
// (1 + 2) * 3 ifadesini kur ve sonucu yazdır

Sample solution

index.ts
type Expr =
  | { kind: "num"; value: number }
  | { kind: "add"; left: Expr; right: Expr }
  | { kind: "mul"; left: Expr; right: Expr };

function evalE(e: Expr): number {
  switch (e.kind) {
    case "num":
      return e.value;
    case "add":
      return evalE(e.left) + evalE(e.right);
    case "mul":
      return evalE(e.left) * evalE(e.right);
    default:
      const _: never = e;
      throw new Error("exhaustive");
  }
}

const expr: Expr = { kind: "mul", left: { kind: "add", left: { kind: "num", value: 1 }, right: { kind: "num", value: 2 } }, right: { kind: "num", value: 3 } };
console.log(evalE(expr));

Expected output

9

Practice Evaluate with a type guard

Write the same Expr union. Define a type guard isNum(e: Expr): e is { kind: "num"; value: number }. In evalE, narrow first with isNum(e), then handle add and mul. Build the expression 5 + 10 and print the result.

Starter code

index.ts
// Expr discriminated union (num | add | mul)
// isNum(e: Expr): e is { kind: "num"; value: number } type guard'ı yaz
// evalE içinde isNum ile daralt, toplamı yazdır
// add(5, num(10)) → 15

Sample solution

index.ts
type Expr =
  | { kind: "num"; value: number }
  | { kind: "add"; left: Expr; right: Expr }
  | { kind: "mul"; left: Expr; right: Expr };

function isNum(e: Expr): e is { kind: "num"; value: number } {
  return e.kind === "num";
}

function evalE(e: Expr): number {
  if (isNum(e)) return e.value;
  if (e.kind === "add") return evalE(e.left) + evalE(e.right);
  return evalE(e.left) * evalE(e.right);
}

const expr: Expr = { kind: "add", left: { kind: "num", value: 5 }, right: { kind: "num", value: 10 } };
console.log(evalE(expr));

Expected output

15

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 What does the exhaustive check with `never` guarantee?

  • Every case contains a `return`
  • The switch never loops forever
  • A new variant causes a compile error Correct answer
  • The function runs faster

Explanation: The exhaustive `never` check produces a compile error when a new variant is added to the union but the `switch` is not updated, catching missed cases early.

2 Exhaustive variable

Question
function evalE(e: Expr): number {
  switch (e.kind) {
    case "num": return e.value;
    case "add": return evalE(e.left) + evalE(e.right);
    default:
      const _: ____ = e;
      throw new Error("exhaustive");
  }
}
Answer
function evalE(e: Expr): number {
  switch (e.kind) {
    case "num": return e.value;
    case "add": return evalE(e.left) + evalE(e.right);
    default:
      const _: never = e;
      throw new Error("exhaustive");
  }
}

Explanation: For an exhaustive check the variable must be `never`; it is assignable only when `e` is narrowed to `never`. `any` or `unknown` would defeat the check.

3 Compiler parts

Term Meaning
Discriminated union AST node types
switch + case type narrowing
never exhaustive check
Recursive type nested expression tree

All lessons in Advanced