← Interpreter lesson
Lab

Interpreter

// A help-desk ticket, and what one evaluation reads: the ticket and who is
// looking (null until the viewer's sign-in has finished).
type Ticket = {
  id: string;
  status: 'open' | 'closed';
  priority: 'urgent' | 'normal';
  assignee: string | null;
};
type Context = { ticket: Ticket; viewer: string | null };
type Result = { ok: true; value: boolean } | { ok: false; error: string };

// Every expression in the filter language answers for itself in a context.
interface Expression {
  interpret(context: Context): Result;
}

// An atom reads one fact about the ticket or the viewer.
class Atom implements Expression {
  name: 'open' | 'urgent' | 'mine';
  constructor(name: 'open' | 'urgent' | 'mine') {
    this.name = name;
  }
  interpret({ ticket, viewer }: Context): Result {
    if (this.name === 'open') {
      return { ok: true, value: ticket.status === 'open' };
    }
    if (this.name === 'urgent') {
      return { ok: true, value: ticket.priority === 'urgent' };
    }
    if (viewer === null) return { ok: false, error: 'missing-viewer' };
    return { ok: true, value: ticket.assignee === viewer };
  }
}

// AND reads its left side first; only a true left needs the right side.
class And implements Expression {
  left: Expression;
  right: Expression;
  constructor(left: Expression, right: Expression) {
    this.left = left;
    this.right = right;
  }
  interpret(context: Context): Result {
    const left = this.left.interpret(context);
    if (!left.ok || !left.value) return left;
    return this.right.interpret(context);
  }
}

// OR reads its left side first; only a false left needs the right side.
class Or implements Expression {
  left: Expression;
  right: Expression;
  constructor(left: Expression, right: Expression) {
    this.left = left;
    this.right = right;
  }
  interpret(context: Context): Result {
    const left = this.left.interpret(context);
    if (!left.ok || left.value) return left;
    return this.right.interpret(context);
  }
}

// NOT negates an answer and passes an error on unchanged.
class Not implements Expression {
  child: Expression;
  constructor(child: Expression) {
    this.child = child;
  }
  interpret(context: Context): Result {
    const child = this.child.interpret(context);
    return child.ok ? { ok: true, value: !child.value } : child;
  }
}

// The saved view's caller: the matching IDs in order, or the first failure,
// never a partial list.
function search(
  rule: Expression,
  tickets: Ticket[],
  viewer: string | null,
): string {
  const ids: string[] = [];
  for (const ticket of tickets) {
    const result = rule.interpret({ ticket, viewer });
    if (!result.ok) return `error ${result.error} at ${ticket.id}`;
    if (result.value) ids.push(ticket.id);
  }
  return ids.join(', ') || '(none)';
}

const tickets: Ticket[] = [
  { id: 'T-101', status: 'open', priority: 'urgent', assignee: 'rian' },
  { id: 'T-102', status: 'open', priority: 'normal', assignee: 'maya' },
  { id: 'T-103', status: 'closed', priority: 'normal', assignee: 'maya' },
];

// open AND (urgent OR mine): saved once, answered for each viewer.
const saved = new And(
  new Atom('open'),
  new Or(new Atom('urgent'), new Atom('mine')),
);
const forMaya = search(saved, tickets, 'maya');
const forRian = search(saved, tickets, 'rian');
console.log('maya: ' + forMaya);
console.log('rian: ' + forRian);