-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrules.py
More file actions
65 lines (51 loc) · 2.34 KB
/
Copy pathrules.py
File metadata and controls
65 lines (51 loc) · 2.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"""Grammar-as-data: nested tuples, one recursive evaluator.
The tree needs no class per rule. A sentence is a value or a tuple whose
head names an operation: ``("*", ("+", 2, 3), 4)``. Extending the language
is a dict entry, not a class — and the evaluator is depth-capped so a
hostile, deeply nested input fails with ``ValueError`` instead of blowing
the recursion limit.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping
#: Deeper than any human-written sentence; shallower than the recursion
#: limit, so hostile nesting gets a clean ValueError, not a RecursionError.
MAX_DEPTH = 50
Value = int | float | str | bool
Expr = Value | tuple[object, ...]
#: An operation receives its already-evaluated operands.
Operation = Callable[[tuple[Value, ...]], Value]
#: Resolves a leaf — the hook where "age" becomes the user's age. Default:
#: leaves are literals.
Resolver = Callable[[Value], Value]
class Interpreter:
"""Evaluate tuple-tree sentences against an operation table."""
def __init__(
self,
operations: Mapping[str, Operation],
*,
resolve: Resolver | None = None,
max_depth: int = MAX_DEPTH,
) -> None:
self._operations = dict(operations)
self._resolve: Resolver = resolve if resolve is not None else lambda leaf: leaf
self._max_depth = max_depth
def evaluate(self, expr: Expr) -> Value:
"""Interpret one sentence; reject unknown operations and deep nesting."""
return self._walk(expr, depth=0)
def _walk(self, expr: Expr, depth: int) -> Value:
if depth > self._max_depth:
raise ValueError("expression too deeply nested")
if not isinstance(expr, tuple):
return self._resolve(expr)
if not expr or not isinstance(expr[0], str):
raise ValueError(f"malformed expression: {expr!r}")
head = expr[0]
if head not in self._operations:
raise ValueError(f"unknown operation: {head!r}")
operands = tuple(self._walk(_as_expr(arg), depth + 1) for arg in expr[1:])
return self._operations[head](operands)
def _as_expr(node: object) -> Expr:
"""Narrow a tuple element back to Expr, rejecting foreign objects."""
if isinstance(node, int | float | str | bool | tuple):
return node
raise ValueError(f"unsupported node: {node!r}")