Hone logo
Hone
Problems

Type-Level Stack Machine in TypeScript

Can you design a type-level stack machine in TypeScript that can execute a sequence of operations encoded as types? This challenge tests your understanding of advanced TypeScript features like conditional types, template literal types, and type manipulation to build a purely static computation.

Problem Description

The goal is to create a type StackMachine<Program, InitialStack> that simulates a stack machine's execution at the type level. The Program will be a tuple of operation types, and InitialStack will represent the starting state of the stack. The machine should process the program instructions sequentially, modifying the stack accordingly, and ultimately return the final state of the stack.

Key Requirements:

  1. Operations: Define a set of distinct operation types. For this challenge, we'll focus on:

    • PUSH<T>: Pushes a value of type T onto the stack.
    • POP: Removes the top element from the stack.
    • ADD: Pops the top two elements, adds them (assuming they are numbers), and pushes the result.
    • SUB: Pops the top two elements, subtracts the second-to-top from the top (top - second-to-top), and pushes the result.
    • DUP: Duplicates the top element of the stack.
  2. Program Representation: The Program will be a tuple of these operation types. For example: [PUSH<1>, PUSH<2>, ADD, DUP].

  3. Stack Representation: The InitialStack will be a tuple representing the initial elements on the stack. The head of the tuple represents the top of the stack. For example, [3, 2, 1] means 3 is at the top, followed by 2, then 1.

  4. Execution: The StackMachine type should recursively process the Program tuple. Each step takes the current Program (or remaining program) and the current Stack, applies the first operation, and updates both.

  5. Result: The StackMachine type should resolve to the final stack tuple after all operations in the program have been executed.

Expected Behavior:

The machine should handle the operations correctly, modifying the stack as described. For example, ADD should expect at least two elements on the stack. If an operation is invalid (e.g., POP on an empty stack, ADD with fewer than two elements), the type should ideally resolve to an error or a specific sentinel type (for simplicity, we can assume valid programs for now, or handle common errors).

Edge Cases to Consider:

  • Empty program: The machine should return the initial stack.
  • Empty stack: Operations like POP, ADD, SUB, DUP on an empty stack.
  • Stack with insufficient elements for operations like ADD or SUB.

Examples

Example 1:

// Program: Push 1, then push 2, then add
type Program1 = [PUSH<1>, PUSH<2>, ADD];
type InitialStack1 = [];
type Result1 = StackMachine<Program1, InitialStack1>;
// Expected Result1: [3]

Explanation:

  1. Start with Stack: [], Program: [PUSH<1>, PUSH<2>, ADD]
  2. Execute PUSH<1>: Stack: [1], Program: [PUSH<2>, ADD]
  3. Execute PUSH<2>: Stack: [2, 1], Program: [ADD]
  4. Execute ADD: Pops 2 and 1, adds them (3), pushes 3. Stack: [3], Program: []
  5. Program finished. Final Stack: [3]

Example 2:

// Program: Push 5, Push 3, Subtract (top - second-to-top), Duplicate
type Program2 = [PUSH<5>, PUSH<3>, SUB, DUP];
type InitialStack2 = [];
type Result2 = StackMachine<Program2, InitialStack2>;
// Expected Result2: [-2, -2]

Explanation:

  1. Start with Stack: [], Program: [PUSH<5>, PUSH<3>, SUB, DUP]
  2. Execute PUSH<5>: Stack: [5], Program: [PUSH<3>, SUB, DUP]
  3. Execute PUSH<3>: Stack: [3, 5], Program: [SUB, DUP]
  4. Execute SUB: Pops 3 and 5. Computes 3 - 5 = -2. Pushes -2. Stack: [-2, 5], Program: [DUP]
  5. Execute DUP: Duplicates the top element (-2). Stack: [-2, -2, 5], Program: []
  6. Program finished. Final Stack: [-2, -2, 5] (Wait, the problem states the top of the tuple is the top of the stack. Let's re-evaluate the stack representation for clarity.)

Correction/Clarification for Example 2 Explanation: Let's assume the head of the tuple is the top of the stack.

  1. Start with Stack: [], Program: [PUSH<5>, PUSH<3>, SUB, DUP]
  2. Execute PUSH<5>: Stack: [5], Program: [PUSH<3>, SUB, DUP]
  3. Execute PUSH<3>: Stack: [3, 5], Program: [SUB, DUP] (3 is top, 5 is below)
  4. Execute SUB: Pops 3 (top) and 5 (second-to-top). Computes 3 - 5 = -2. Pushes -2. Stack: [-2, 5], Program: [DUP]
  5. Execute DUP: Duplicates the top element (-2). Stack: [-2, -2, 5], Program: []
  6. Program finished. Final Stack: [-2, -2, 5]

Example 3 (Edge Case - Empty Program):

// Program: No operations
type Program3 = [];
type InitialStack3 = [10, 20];
type Result3 = StackMachine<Program3, InitialStack3>;
// Expected Result3: [10, 20]

Explanation: The program is empty, so no operations are performed. The machine returns the InitialStack unchanged.

Constraints

  • The stack elements will be restricted to number and boolean types for operations. For simplicity, assume ADD and SUB only operate on number types.
  • The Program tuple will contain valid operation types (e.g., PUSH<number>, POP, ADD, SUB, DUP).
  • The stack can grow dynamically based on the operations.
  • Focus on correctness of type computation. Performance is secondary but avoid excessively deep recursion that might hit TypeScript's recursion limits for typical inputs.

Notes

  • This challenge requires a deep understanding of TypeScript's conditional types, inference, and recursive type definitions.
  • Consider how to represent the "head" of a tuple for stack operations (pushing to head, popping from head).
  • You'll likely need helper types to manage the program execution flow.
  • For operations like ADD and SUB, you'll need to extract and infer the types of the popped elements.
  • Think about how to handle type errors gracefully if an operation is invalid (e.g., popping from an empty stack). You might want to define a specific ERROR or INVALID type for such cases, or simply rely on TypeScript's natural error reporting if your inference becomes impossible.
Loading editor...
typescript