Parse and evaluate a policy expression
“An admin tool stores small policies such as
role == "admin" OR active == TRUE AND tier == 2. Evaluate them against a record. Parentheses must work, AND must bind more tightly than OR, and missing fields must not accidentally grant access. Reject malformed policy text even when its first branch would already be true.”
Write this:
def evaluate(expression, record):
...
def lex(expression):
...
Constructed practice question. Prerequisites: stack state and recursive structure. A lexer converts characters to tokens. A parser assigns grammatical structure. An evaluator computes that structure's meaning; combining these jobs carelessly hides syntax errors.
| Contract | Required behavior |
|---|---|
| Input | Policy string and field mapping; field names match [A-Za-z_][A-Za-z0-9_.]* |
| Literals | JSON double-quoted strings, integers, uppercase TRUE/FALSE |
| Operators | ==, !=, AND, OR, parentheses; AND precedes OR |
| Missing/types | Missing field or mismatched scalar type makes either comparison false |
| Failure | Malformed input raises ValueError with location; no host-language evaluation |
| Limits/scope | At most 65,536 characters, 4,096 tokens, 100 nested parentheses; no calls/NOT |
Optional refresher · the underlying tool
An expression evaluator has two jobs: turn characters into tokens, then interpret tokens according to precedence and parentheses. Do not call Python eval on a user-supplied policy:
expression = 'role == "admin" AND active == TRUE'
tokens = ["role", "==", "admin", "AND", "active", "==", True]
# Illustrative decoded token values; the real lexer also records positions.
Ask whether AND binds more tightly than OR, how missing fields behave, and which operators are permitted. A parser should reject a trailing unexpected token rather than silently accept part of the input.
A design choice worth saying aloud
Give lexing, parsing, and evaluation separate responsibilities. A token includes its location for useful errors; the parser must consume the entire input before the evaluator can short-circuit safely. Restrict field lookup to the supplied mapping—Python eval or attribute traversal would grant behavior the policy language never promised.
What the interviewer expects
The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.
Done means: Return the exact Boolean value defined by the fully parsed policy, or raise ValueError for malformed or over-budget input.
Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.
Test-case scenarios to settle before coding
01 · Precedence
- Input / starting state
a==TRUE OR b==TRUE AND c==TRUEwith T,F,F- Expected result
True
What it is testing: AND binds before OR.
02 · Parentheses
- Input / starting state
(a==TRUE OR b==TRUE) AND c==TRUE- Expected result
False
What it is testing: Grouping changes authority.
03 · Missing
- Input / starting state
missing != "admin"- Expected result
False
What it is testing: Absence cannot accidentally grant access.
04 · Malformed right branch
- Input / starting state
a==TRUE OR ???- Expected result
ValueError
What it is testing: Short-circuit evaluation must not skip parsing.
05 · Quoted content
- Input / starting state
name == "Ada \"OR\" Lovelace", record{"name": 'Ada "OR" Lovelace'}- Expected result
True
What it is testing: Escaped quotes and OR inside the string are not operators.
06 · Type boundary
- Input / starting state
tier != 2, record{"tier": "2"}- Expected result
False
What it is testing: Mismatched scalar types make both comparisons false.
07 · Token limit
- Input / starting state
'a==TRUE OR ' * 2049 + 'a==TRUE', record{"a": True}- Expected result
ValueError
What it is testing: Exceeds the 4,096-token admission limit before evaluation.
For each case, show which branch or state change produces that result.
Given {a:True,b:False,c:False}, a == TRUE OR b == TRUE AND c == TRUE is true;
(a == TRUE OR b == TRUE) AND c == TRUE is false. missing != "admin" is false.
a == TRUE OR ??? raises, despite the true left branch. Dotted field names are
literal mapping keys, not property traversal. Clarify missing-field behavior first.
Try independently. First write the grammar and tokenize a quoted string containing
an escaped quote; do not start by splitting text on spaces or AND.
Solution, grammar, and evolving semantics
Space splitting fails on name == "Ada Lovelace"; splitting on OR fails when a
string contains those letters. Left-to-right evaluation gives the wrong precedence.
Passing untrusted text to Python eval gives the host language capabilities absent
from this contract. The baseline should instead be a deliberately small language.
expression := conjunction (OR conjunction)*
conjunction := factor (AND factor)*
factor := '(' expression ')' | IDENT ('==' | '!=') literal
literal := STRING | INTEGER | TRUE | FALSE
The lexer advances from the current offset, refusing unmatched characters and decoding strings with JSON's escape rules. Recursive descent follows the grammar: expression consumes OR groups, conjunction consumes AND groups, and factor handles parentheses or one comparison. Requiring EOF rejects trailing junk. Parse the entire policy into an abstract syntax tree before accessing the record.
each parser routine consumes exactly its grammatical production
and returns a subtree with the declared precedence. The evaluator then uses an
explicit stack and short-circuits AND/OR; skipping a right subtree skips record
access, never syntax validation. Strict scalar types prevent Python's True == 1
from leaking into policy semantics. Missing != is false, not accidental permission.
| Phase | Example evidence | Why it matters |
|---|---|---|
| Lex | ID(a), EQ, TRUE, OR, ID(b), EQ, TRUE, AND… | Quoted content remains one token |
| Parse | OR(a, AND(b,c)) | AND grouped first |
| Evaluate | a=true; OR skips right subtree | Short circuit after complete validation |
Watch the same expression's persistent token, syntax-tree, and evaluation states
in the , or use
the .
Pause before evaluation and predict which fully parsed branch will be skipped.
For C characters and T tokens, scanning/parsing/tree traversal use O(C+T) ordinary token work and O(C+T) retained text/tree/stack space. Integer conversion/comparison also depends on literal bit length; resource limits bound admitted work, and Python may reject extremely long integer literals. No claim of constant-time arbitrary precision arithmetic is intended. Long operator chains use iterative evaluation; parenthesis recursion is explicitly bounded.
Follow-up 1 — parentheses change authority. Predict the regrouped example's result before viewing its tree. The final c check now applies even when a is true.
Follow-up 2 — add NOT or an unknown result. Adding NOT requires a new precedence production above comparisons. If missing becomes “unknown,” define three-valued truth tables: simply negating today's missing=false would turn absent attributes into true. Agree on authorization semantics and compatibility before extending syntax.
Senior depth tests malformed right branches, quoting, precedence, missing/type rules, and short-circuit accesses independently. Lead depth adds grammar versions, resource budgets, and policy migration; this interpreter is a teaching language, not a complete authorization system.
Reference: solution.py (download file, source below); tests include malformed syntax, escaped strings, guarded record access, long chains, and depth/token limits.
"""A small explicit policy language; never use Python eval on policy input."""
from collections.abc import Mapping
from dataclasses import dataclass
import json
import re
MAX_CHARS = 65536
MAX_TOKENS = 4096
MAX_DEPTH = 100
@dataclass(frozen=True)
class Token:
kind: str
value: object
position: int
_TOKEN = re.compile(
r'(?P<SPACE>\s+)|(?P<STRING>"(?:\\.|[^"\\\x00-\x1f])*")'
r'|(?P<NUMBER>-?(?:0|[1-9][0-9]*))|(?P<EQ>==)|(?P<NE>!=)'
r'|(?P<LPAREN>\()|(?P<RPAREN>\))|(?P<ID>[A-Za-z_][A-Za-z0-9_.]*)'
)
def lex(expression):
if not isinstance(expression, str) or len(expression) > MAX_CHARS:
raise ValueError("policy must be a string of at most 65536 characters")
tokens, position = [], 0
while position < len(expression):
match = _TOKEN.match(expression, position)
if match is None:
raise ValueError(f"invalid token at position {position}")
kind, value = match.lastgroup, match.group()
if kind != 'SPACE':
if kind == 'ID' and value in ('AND', 'OR', 'TRUE', 'FALSE'):
kind = value
if kind == 'STRING':
try:
value = json.loads(value)
except ValueError as exc:
raise ValueError(f"invalid string at position {position}") from exc
elif kind == 'NUMBER':
value = int(value)
elif kind in ('TRUE', 'FALSE'):
value = kind == 'TRUE'
tokens.append(Token(kind, value, position))
if len(tokens) > MAX_TOKENS:
raise ValueError("policy exceeds 4096 tokens")
position = match.end()
tokens.append(Token('EOF', None, position))
return tokens
class Parser:
def __init__(self, tokens):
self.tokens, self.position = tokens, 0
def take(self, kind):
token = self.tokens[self.position]
if token.kind != kind:
raise ValueError(f"expected {kind} at position {token.position}")
self.position += 1
return token.value
def parse(self):
node = self.expression(0)
self.take('EOF')
return node
def expression(self, depth):
node = self.conjunction(depth)
while self.tokens[self.position].kind == 'OR':
self.take('OR')
node = ('OR', node, self.conjunction(depth))
return node
def conjunction(self, depth):
node = self.factor(depth)
while self.tokens[self.position].kind == 'AND':
self.take('AND')
node = ('AND', node, self.factor(depth))
return node
def factor(self, depth):
if self.tokens[self.position].kind == 'LPAREN':
if depth >= MAX_DEPTH:
raise ValueError("policy exceeds 100 nested parentheses")
self.take('LPAREN')
node = self.expression(depth + 1)
self.take('RPAREN')
return node
field = self.take('ID')
kind = self.tokens[self.position].kind
if kind not in ('EQ', 'NE'):
raise ValueError(f"expected comparison at position {self.tokens[self.position].position}")
self.take(kind)
literal_kind = self.tokens[self.position].kind
if literal_kind not in ('STRING', 'NUMBER', 'TRUE', 'FALSE'):
raise ValueError(f"expected literal at position {self.tokens[self.position].position}")
return ('PRED', field, kind, self.take(literal_kind))
def evaluate(expression, record):
if not isinstance(record, Mapping):
raise ValueError("record must be a mapping")
tree = Parser(lex(expression)).parse()
# Explicit traversal avoids recursion on long left-associated AND/OR chains.
pending, results = [(tree, False)], []
while pending:
node, left_done = pending.pop()
if node[0] == 'PRED':
_, field, operator, expected = node
if field not in record:
results.append(False)
else:
actual = record[field]
equal = type(actual) is type(expected) and actual == expected
# A mismatched type fails both comparison operators.
results.append(equal if operator == 'EQ' else
type(actual) is type(expected) and not equal)
elif not left_done:
pending.append((node, True))
pending.append((node[1], False))
else:
left = results.pop()
if (node[0] == 'AND' and not left) or (node[0] == 'OR' and left):
results.append(left)
else:
pending.append((node[2], False))
return results[0]
python -m unittest discover -s curriculum/01-code/02-data-structures-algorithms/problems/39-policy-expression-evaluator -p 'test_*.py'