Evaluate a Lisp-Style Prefix Expression Language With ADD, MULT and LET Variables

Read the full interview experience this question came from →

Quick Overview

Implement an evaluator for a small Lisp-style prefix language with ADD and MULT, then extend it with LET variable bindings. It tests recursive parsing of nested expressions, settling ambiguous language rules before coding, and choosing a data structure for nested variable scopes.

Evaluate a Lisp-Style Prefix Expression Language With ADD, MULT and LET Variables

Company: Attentive

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

Implement `parse(expression)`, which evaluates an expression in a small Lisp-style language and returns its integer value. Despite its name, the function both parses and evaluates. The language uses prefix notation. An expression is either an integer literal or a parenthesized list whose first item is an operator and whose remaining items are operand expressions. Operands can themselves be nested expressions. The base language has two operators: `ADD` returns the sum of its operands, and `MULT` returns their product. ```text parse("( ADD 3 4 )") == 7 parse("( MULT 3 4 )") == 12 parse("( MULT 3 ( ADD 3 4 ) )") == 21 ``` After the base version works and passes some tests, the interviewer extends the language with variables, and then asks how the bindings should be stored when they can be nested. ### Constraints and Clarifications - In the examples, every token, including each parenthesis, is separated by a single space. - Operator names are uppercase. ### Clarifying Questions - Does every operator take exactly two operands, as in the examples, or any number of them? - Can integer literals be negative? - Can a parenthesis touch a neighboring token, as in `(ADD 3 4)`, or is the spacing always as in the examples? - How should malformed input be reported: unbalanced parentheses, an unknown operator, a wrong number of operands, or an empty string? ### Part 1 — Evaluate ADD and MULT Implement `parse` for the base language and test it, including the three examples above. State its time and space complexity. ```hint Let the grammar shape the code Write the grammar in one line. Notice that every operand has the same shape as the whole expression, and that each nested expression ends exactly where its matching parenthesis closes. ``` #### What This Part Should Cover - Turning the string into tokens, and consuming nested expressions so that each one hands its value back to its parent - Rejecting malformed input with clear errors - Tests that cover both operators, nesting, and invalid input - Complexity in terms of input length and nesting depth ### Part 2 — Add LET Extend the language with `LET`, which binds a variable name to a value so that later expressions can use the name as an operand. The input can now contain more than one top-level expression: ```text parse("( LET x 3 ) ( ADD 4 x )") == 7 ``` The example leaves several rules open. Settle them with the interviewer, then update your implementation and your tests. ```hint Where bindings live Evaluation now needs state that outlives a single expression. Decide what owns that state, when it is created, and what the value of the whole program is when there are several top-level expressions. ``` #### Clarifying Questions for this Part - May the same variable be assigned more than once? If so, which value do later expressions see? - What should a program consisting of a single `LET` expression return? - What should a program made only of several `LET` expressions, with no other expression, return? - Can the value in a `LET` be a nested expression or another variable, and what happens when an expression uses a variable that was never bound? #### What This Part Should Cover - An explicit answer to each open rule, reflected in both the code and the tests - How a sequence of top-level expressions is evaluated, and which value is returned - Variable lookup, rebinding, and the error for an unbound name ### Part 3 — Nested LET scopes Suppose `LET` can appear at several levels of nesting. What data structure would you use to hold the bindings? One proposal is a dictionary per nesting level, deep-copying the enclosing level's dictionary whenever a new level starts. The interviewer asked whether you would use a stack instead. An objection raised against the stack was that a variable bound once but used several times might no longer be on the stack when it is used the second time. Evaluate both options, settle that objection, and choose one. ```hint Name the events List the exact events that add or remove a binding, and check which structure makes each event cheap without changing what a later read returns. ``` #### Clarifying Questions for this Part - Is a binding made inside a nested expression still visible after that expression closes? For example, in `( ADD ( LET x 2 ) x ) ( MULT x 10 )`, is `x` bound in the second top-level expression? - If an inner level binds a name that an outer level already bound, should the inner binding shadow the outer one, and should the outer value come back afterwards? #### What This Part Should Cover - Scope rules: when a binding becomes visible and when it disappears - The time and memory cost of copying per level versus a shared structure - Whether the stack objection holds, shown with a concrete trace ### What a Strong Answer Covers - A correct, tested base evaluator finished quickly enough to leave time for the extension - Open language rules raised and resolved before the code changes, not discovered through failing tests - Tokenizing, parsing, and evaluation kept separate enough that new operators are cheap to add - A scope structure justified by its complexity and by the chosen semantics - Clear errors for malformed input and unbound variables ### Follow-up Questions - How would you support a `LET` with a body, such as `( LET x 3 ( ADD x 1 ) )`, where the binding is visible only inside the body? - How would you evaluate very deeply nested input without hitting the language's recursion limit? - What would user-defined functions with closures require from your binding structure?

Overview: Implement an evaluator for a small Lisp-style prefix language with ADD and MULT, then extend it with LET variable bindings. It tests recursive parsing of nested expressions, settling ambiguous language rules before coding, and choosing a data structure for nested variable scopes.

Read the full Attentive Software Engineer interview experience this question came from

|Home/Software Engineering Fundamentals/Attentive
Attentive logo
Attentive
Sep 24, 2026
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
0
0

Implement parse(expression), which evaluates an expression in a small Lisp-style language and returns its integer value. Despite its name, the function both parses and evaluates.

The language uses prefix notation. An expression is either an integer literal or a parenthesized list whose first item is an operator and whose remaining items are operand expressions. Operands can themselves be nested expressions. The base language has two operators: ADD returns the sum of its operands, and MULT returns their product.

parse("( ADD 3 4 )")            == 7
parse("( MULT 3 4 )")           == 12
parse("( MULT 3 ( ADD 3 4 ) )") == 21

After the base version works and passes some tests, the interviewer extends the language with variables, and then asks how the bindings should be stored when they can be nested.

Constraints and Clarifications

  • In the examples, every token, including each parenthesis, is separated by a single space.
  • Operator names are uppercase.

Clarifying Questions Guidance

  • Does every operator take exactly two operands, as in the examples, or any number of them?
  • Can integer literals be negative?
  • Can a parenthesis touch a neighboring token, as in (ADD 3 4) , or is the spacing always as in the examples?
  • How should malformed input be reported: unbalanced parentheses, an unknown operator, a wrong number of operands, or an empty string?

Part 1 — Evaluate ADD and MULT

Implement parse for the base language and test it, including the three examples above. State its time and space complexity.

What This Part Should Cover Guidance

  • Turning the string into tokens, and consuming nested expressions so that each one hands its value back to its parent
  • Rejecting malformed input with clear errors
  • Tests that cover both operators, nesting, and invalid input
  • Complexity in terms of input length and nesting depth

Part 2 — Add LET

Extend the language with LET, which binds a variable name to a value so that later expressions can use the name as an operand. The input can now contain more than one top-level expression:

parse("( LET x 3 ) ( ADD 4 x )") == 7

The example leaves several rules open. Settle them with the interviewer, then update your implementation and your tests.

Clarifying Questions for this Part Guidance

  • May the same variable be assigned more than once? If so, which value do later expressions see?
  • What should a program consisting of a single LET expression return?
  • What should a program made only of several LET expressions, with no other expression, return?
  • Can the value in a LET be a nested expression or another variable, and what happens when an expression uses a variable that was never bound?

What This Part Should Cover Guidance

  • An explicit answer to each open rule, reflected in both the code and the tests
  • How a sequence of top-level expressions is evaluated, and which value is returned
  • Variable lookup, rebinding, and the error for an unbound name

Part 3 — Nested LET scopes

Suppose LET can appear at several levels of nesting. What data structure would you use to hold the bindings?

One proposal is a dictionary per nesting level, deep-copying the enclosing level's dictionary whenever a new level starts. The interviewer asked whether you would use a stack instead. An objection raised against the stack was that a variable bound once but used several times might no longer be on the stack when it is used the second time. Evaluate both options, settle that objection, and choose one.

Clarifying Questions for this Part Guidance

  • Is a binding made inside a nested expression still visible after that expression closes? For example, in ( ADD ( LET x 2 ) x ) ( MULT x 10 ) , is x bound in the second top-level expression?
  • If an inner level binds a name that an outer level already bound, should the inner binding shadow the outer one, and should the outer value come back afterwards?

What This Part Should Cover Guidance

  • Scope rules: when a binding becomes visible and when it disappears
  • The time and memory cost of copying per level versus a shared structure
  • Whether the stack objection holds, shown with a concrete trace

What a Strong Answer Covers Guidance

  • A correct, tested base evaluator finished quickly enough to leave time for the extension
  • Open language rules raised and resolved before the code changes, not discovered through failing tests
  • Tokenizing, parsing, and evaluation kept separate enough that new operators are cheap to add
  • A scope structure justified by its complexity and by the chosen semantics
  • Clear errors for malformed input and unbound variables

Follow-up Questions Guidance

  • How would you support a LET with a body, such as ( LET x 3 ( ADD x 1 ) ) , where the binding is visible only inside the body?
  • How would you evaluate very deeply nested input without hitting the language's recursion limit?
  • What would user-defined functions with closures require from your binding structure?
Loading comments...