Graph-structured stack in Python
Shared-stack representation of all parallel LR parser states in generalized LR parsing.
- File
graph_structured_stack.py, 117 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/graph-structured-stack/. The authors are listed in prov/5TVZ6C.jsonl.
"""Graph-structured stack (GSS): many parser stacks stored as one graph.
A node is a stack cell, identified by a pair (state, level): the state of the
parser and the number of input symbols read when the cell was pushed. An edge
v -> x means that v lies directly on x, so every path from a node down to the
root is one stack, read from top to bottom. Two stacks with the same top cell
share one node, and two stacks with the same cell below the top share the part
under it. Each edge carries a label, for example the parse tree of the grammar
symbol that the cell stands for. Edges never go up a level.
"""
from typing import Any, Hashable, Optional
Edge = tuple[int, int]
class GraphStructuredStack:
"""A graph-structured stack with find-or-create nodes, edges and path pops."""
def __init__(self, root_state: Hashable) -> None:
self.state: list[Hashable] = []
self.level: list[int] = []
self.out: list[dict[int, Any]] = []
self.index: dict[tuple[Hashable, int], int] = {}
self.by_level: dict[int, list[int]] = {}
self.root, _ = self.node(root_state, 0)
def push(self, s: Hashable, j: int, x: int, label: Any = None) -> tuple[int, bool, bool]:
"""Push a cell (s, j) on every stack whose top is x.
Return (v, created, new_edge): the node v of the cell, whether v was
made now, and whether the edge v -> x is new.
"""
if self.level[x] > j: # @line L1
raise ValueError("a cell may not lie on a cell of a higher level")
v, created = self.node(s, j) # @line L2
new_edge = self.add_edge(v, x, label) # @line L3
return v, created, new_edge # @line L4
def node(self, s: Hashable, j: int) -> tuple[int, bool]:
"""Return (v, created): the node for state s at level j, made if absent."""
if (s, j) not in self.index: # @line L5
v = len(self.state)
self.state.append(s)
self.level.append(j)
self.out.append({})
self.index[(s, j)] = v
self.by_level.setdefault(j, []).append(v)
return v, True
return self.index[(s, j)], False # @line L6
def add_edge(self, v: int, x: int, label: Any = None) -> bool:
"""Put v directly on x; return False if the edge was already there."""
if self.level[x] > self.level[v]:
raise ValueError("an edge may not go up a level")
if x in self.out[v]: # @line L7
return False
self.out[v][x] = label # @line L8
return True
def pop(self, u: int, m: int, through: Optional[Edge] = None) -> list[tuple[int, tuple[Any, ...]]]:
"""Return (x, labels) for every path of m edges from u, labels bottom first.
If `through` is an edge, only the paths that use it are returned. The
search keeps its own stack and does not recurse.
"""
result: list[tuple[int, tuple[Any, ...]]] = [] # @line L9
floor = self.level[through[0]] if through is not None else 0
todo: list[tuple[int, int, bool, tuple[Any, ...]]] = [(u, 0, through is None, ())]
while todo: # @line L10
y, d, used, labels = todo.pop() # @line L11
if d == m: # @line L12
if used:
result.append((y, labels[::-1]))
continue
if not used and self.level[y] < floor: # @line L13
continue
for x, t in self.out[y].items(): # @line L14
todo.append((x, d + 1, used or (y, x) == through, labels + (t,)))
return result # @line L15
def tops(self, j: int) -> list[int]:
"""Return the nodes of level j, in the order they were made."""
return list(self.by_level.get(j, []))
def stacks(self, u: int, limit: int) -> set[tuple[int, ...]]:
"""Return the stacks (paths from u down to the root, bottom first)
with at most `limit` cells. Used to read the represented stack set."""
found: set[tuple[int, ...]] = set()
todo: list[tuple[int, ...]] = [(u,)]
while todo:
path = todo.pop()
if path[-1] == self.root:
found.add(path[::-1])
if len(path) < limit:
for x in self.out[path[-1]]:
todo.append(path + (x,))
return found
def check_invariant(self) -> None:
"""Check the index, the levels and the direction of the edges."""
n = len(self.state)
assert len(self.level) == n and len(self.out) == n
assert self.level[self.root] == 0
assert len(self.index) == n
for (s, j), v in self.index.items():
assert self.state[v] == s and self.level[v] == j
seen = sorted(v for vs in self.by_level.values() for v in vs)
assert seen == list(range(n))
for j, vs in self.by_level.items():
assert all(self.level[v] == j for v in vs)
for v in range(n):
for x in self.out[v]:
assert 0 <= x < n and self.level[x] <= self.level[v]
test_graph_structured_stack.py 146 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/graph-structured-stack/. The authors are listed in prov/5TVZ6C.jsonl.
import itertools
import random
import pytest
from graph_structured_stack import GraphStructuredStack
def walks(nodes, edges, u, m, through):
"""Oracle: try every sequence of m + 1 nodes and keep the walks of the edge list."""
out = []
for seq in itertools.product(nodes, repeat=m):
path = (u,) + seq
steps = [(path[k], path[k + 1]) for k in range(m)]
if all(s in edges for s in steps) and (through is None or through in steps):
out.append((path[-1], tuple(edges[s] for s in reversed(steps))))
return sorted(out)
def splice_closure(stacks):
"""Oracle: close a set of stacks (bottom first) under exchange of the parts
below a shared cell."""
stacks = set(stacks)
changed = True
while changed:
changed = False
for s1 in list(stacks):
for s2 in list(stacks):
for i, c in enumerate(s1):
for k, d in enumerate(s2):
if c == d:
new = s2[: k + 1] + s1[i + 1:]
if new not in stacks:
stacks.add(new)
changed = True
return stacks
def test_example():
# The stacks of the example on the page: three parses of the prefix "aa".
g = GraphStructuredStack("q0")
r = g.root
a1, _ = g.node("q2", 1)
g.add_edge(a1, r, "a")
s1, _ = g.node("q1", 1)
g.add_edge(s1, r, "S")
a2, _ = g.node("q2", 2)
g.add_edge(a2, s1, "a")
s3, _ = g.node("q3", 2)
g.add_edge(s3, s1, "S")
s12, _ = g.node("q1", 2)
g.add_edge(s12, r, "S")
a3, created = g.node("q2", 3)
assert created
g.add_edge(a3, s3, "a")
g.add_edge(a3, s12, "a")
assert g.node("q2", 3) == (a3, False)
assert sorted(g.pop(a3, 1)) == sorted([(s3, ("a",)), (s12, ("a",))])
assert g.pop(s3, 2) == [(r, ("S", "S"))]
assert g.stacks(a3, 10) == {(r, s1, s3, a3), (r, s12, a3)}
assert g.tops(2) == [a2, s3, s12]
g.check_invariant()
def test_no_upward_edge():
g = GraphStructuredStack(0)
v, _ = g.node(1, 2)
with pytest.raises(ValueError):
g.add_edge(g.root, v)
with pytest.raises(ValueError):
g.push(5, 1, v)
w, created, new_edge = g.push(5, 2, v, "x")
assert created and new_edge and g.out[w] == {v: "x"}
assert g.push(5, 2, v, "y") == (w, False, False) and g.out[w] == {v: "x"}
def test_random_pop_against_walks():
rng = random.Random(7)
checked = 0
for _ in range(300):
g = GraphStructuredStack(0)
nodes = [g.root]
edges = {}
for _ in range(rng.randint(1, 9)):
s, j = rng.randint(0, 2), rng.randint(0, 3)
v, created = g.node(s, j)
if created:
nodes.append(v)
for _ in range(rng.randint(0, 12)):
v, x = rng.choice(nodes), rng.choice(nodes)
if g.level[x] <= g.level[v]:
lab = (v, x)
new = g.add_edge(v, x, lab)
assert new == ((v, x) not in edges)
edges.setdefault((v, x), lab)
g.check_invariant()
for _ in range(3):
u = rng.choice(nodes)
m = rng.randint(0, 3)
through = rng.choice(list(edges)) if edges and rng.random() < 0.5 else None
if through is not None and m == 0:
through = None
got = sorted(g.pop(u, m, through))
assert got == walks(nodes, edges, u, m, through)
checked += 1
assert checked >= 500
def test_stack_set_is_splice_closure():
rng = random.Random(11)
for _ in range(300):
g = GraphStructuredStack(0)
explicit = {(g.root,)}
for _ in range(rng.randint(1, 7)):
x = rng.randrange(len(g.state))
if rng.random() < 0.6:
s, j = rng.randint(1, 3), g.level[x] + rng.randint(0, 1)
if (s, j) in g.index and g.index[(s, j)] <= x:
continue
had = x in g.out[g.index[(s, j)]] if (s, j) in g.index else False
v, created, new_edge = g.push(s, j, x)
assert created == (v == len(g.state) - 1 and len(g.out[v]) == 1 and not had)
assert new_edge == (not had)
else:
v = rng.randrange(len(g.state))
if v <= x or g.level[v] < g.level[x]:
continue
g.add_edge(v, x)
explicit = splice_closure(explicit | {s + (v,) for s in explicit if s[-1] == x})
g.check_invariant()
for u in range(len(g.state)):
assert g.stacks(u, 50) == {s for s in explicit if s[-1] == u}
def test_long_chain_no_recursion():
g = GraphStructuredStack(0)
prev = g.root
for j in range(1, 3001):
v, _ = g.node(0, j)
g.add_edge(v, prev, j)
prev = v
(end, labels), = g.pop(prev, 3000)
assert end == g.root and labels == tuple(range(1, 3001))
What the next level needs
- the tests did not run in this build