Algorithm Search⌘K

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
graph_structured_stack.pyRawGitHub
# 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

Read the full page about Graph-structured stack