Hone logo
Hone
Problems

Type-Level Interpreter with Memory in TypeScript

Develop a type-level interpreter in TypeScript that can represent and execute simple arithmetic expressions, with the added capability of managing a memory store at the type level. This challenge will test your understanding of advanced TypeScript features like conditional types, infer keywords, and recursive type definitions to build a sophisticated interpreter without relying on runtime JavaScript execution.

Problem Description

Your task is to create a set of TypeScript types that can interpret and evaluate arithmetic expressions. These expressions will be defined as nested tuples representing operations and their operands. The interpreter should also support a memory store, allowing variables to be declared and referenced within expressions.

Key Requirements:

  1. Expression Representation:

    • Numbers will be represented as the number type.
    • Variables will be represented as string literals (e.g., "x").
    • Addition will be represented as a tuple ["add", Operand1, Operand2].
    • Subtraction will be represented as a tuple ["sub", Operand1, Operand2].
    • Multiplication will be represented as a tuple ["mul", Operand1, Operand2].
    • Division will be represented as a tuple ["div", Operand1, Operand2].
    • Variable declaration will be represented as a tuple ["let", VariableName, ValueExpression].
    • Variable lookup will be represented as a tuple ["get", VariableName].
  2. Memory Store:

    • The interpreter needs to maintain a memory of declared variables.
    • The memory will be represented as a type, likely an object type where keys are variable names and values are their corresponding types (which will eventually resolve to numbers).
  3. Type-Level Evaluation:

    • Create a main Evaluate<Expression, Memory> type that takes an Expression and a Memory and returns the evaluated number.
    • The Evaluate type should recursively handle nested expressions.
    • Handle variable lookup using the provided Memory.
    • Handle variable declaration by updating the Memory for subsequent evaluations within the same scope (this is the trickiest part and will require careful consideration of how to pass updated memory).

Expected Behavior:

  • The Evaluate type should correctly compute the result of arithmetic operations based on their operands.
  • Variable lookups should retrieve the value from the Memory.
  • Variable declarations should store the value in the Memory for subsequent operations.

Important Edge Cases:

  • Division by Zero: Define how your interpreter handles division by zero. For this challenge, we can assume it results in a specific error type or a designated value (e.g., never or a custom error type).
  • Undefined Variables: Handle cases where a get operation attempts to access a variable not present in the Memory.
  • Nested Declarations/Scope: Consider how let declarations might nest and how the memory should be updated. For simplicity, let's assume a single top-level scope where declarations are additive.

Examples

Example 1: Simple Arithmetic

// Expression: 5 + (10 * 2)
type Expr1 = ["add", 5, ["mul", 10, 2]];

// Memory: Empty
type Memory1 = {};

// Expected Output: 25
// Evaluate<Expr1, Memory1>

Explanation: The interpreter first evaluates ["mul", 10, 2] which results in 20. Then it evaluates ["add", 5, 20] which results in 25.

Example 2: With Variable Declaration and Lookup

// Expression: x + y
type Expr2 = ["add", ["get", "x"], ["get", "y"]];

// Memory: x = 10, y = 20
type Memory2 = { x: number; y: number };

// Expected Output: 30
// Evaluate<Expr2, Memory2>

Explanation: The interpreter looks up "x" in Memory2, getting 10. It looks up "y" in Memory2, getting 20. It then evaluates ["add", 10, 20], resulting in 30.

Example 3: Chained Declarations and Operations

// Expression: z * 3
type Expr3 = ["mul", ["get", "z"], 3];

// Memory: x = 5, then declare z = x + 2
// This requires handling the "let" type, which modifies the memory.
// For this example, we'll simulate the memory update manually.
type IntermediateMemory = { x: number; z: number };
type InitialMemory = { x: number };

// First, evaluate the declaration to get the updated memory:
type DeclaredMemory = Evaluate<["let", "z", ["add", ["get", "x"], 2]], InitialMemory>;

// Then, evaluate the final expression with the updated memory:
// Evaluate<Expr3, DeclaredMemory>

Expected Output (Conceptual): If InitialMemory is { x: 5 }:

  1. ["let", "z", ["add", ["get", "x"], 2]] evaluated with { x: 5 } should produce an updated memory { x: 5, z: 7 }.
  2. ["mul", ["get", "z"], 3] evaluated with { x: 5, z: 7 } should result in 21.

(Note: The actual implementation of Evaluate will need to handle returning both the result and potentially an updated memory, or structure the types to implicitly handle this in recursive calls. For this problem, the primary goal is the Evaluate type that returns the final numerical result given a static memory. The let declaration type will be the mechanism to conceptually update the memory for subsequent types.)

Constraints

  • Expressions will be well-formed tuples.
  • All numbers will be non-negative integers.
  • Variable names will be strings.
  • The Memory will be an object where keys are string literals and values are number or other defined variable types.
  • Division by zero should result in never.
  • Accessing an undefined variable should result in never.

Notes

  • This challenge heavily relies on recursive conditional types and the infer keyword.
  • Think carefully about how to represent and pass the Memory through recursive calls.
  • You might need helper types to manage the expression parsing and memory updates.
  • The let declaration type will be crucial. Consider how it can "return" an updated memory type that is then used by subsequent evaluation steps. The core Evaluate type might need to be structured to handle this, perhaps by returning a tuple of [Result, NewMemory] if you need explicit memory passing, or by ensuring the recursive Evaluate calls naturally pick up the "new" memory definition. For the purpose of this challenge, focus on the Evaluate<Expr, Memory> returning the final number. The let mechanism will be the means to define the memory for subsequent steps.
Loading editor...
typescript