Skip to content

Commit

Permalink
Use iteration for flat parts of sequences (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
kavigupta authored Sep 10, 2024
1 parent 2048102 commit 1d0e295
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 24 deletions.
78 changes: 61 additions & 17 deletions s_expression_parser/parser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

import attr

from .lexer import PARENS, lex
Expand Down Expand Up @@ -26,11 +28,51 @@ def symbols(self):
return set(self.prefix_symbols) | ({"."} if self.dots_are_cons else set())


@attr.s
@attr.s(eq=False, repr=False)
class Pair:
car = attr.ib()
cdr = attr.ib()

def __eq__(self, other):
comparisons_stack = [(self, other)]
while comparisons_stack:
a, b = comparisons_stack.pop()
if isinstance(a, Pair) != isinstance(b, Pair):
return False
if not isinstance(a, Pair) and not isinstance(b, Pair):
if a != b:
return False
continue
comparisons_stack.append((a.cdr, b.cdr))
comparisons_stack.append((a.car, b.car))
return True

def __repr__(self):
repr_each = {}
attempted = set()
stack = [self]
while stack:
current = stack.pop()
if not isinstance(current, Pair):
repr_each[id(current)] = repr(current)
continue
if id(current) in repr_each:
continue
if id(current.car) in repr_each and id(current.cdr) in repr_each:
repr_car, repr_cdr = (
repr_each[id(current.car)],
repr_each[id(current.cdr)],
)
repr_each[id(current)] = f"Pair({repr_car}, {repr_cdr})"
continue
if id(current) in attempted:
return "..."
attempted.add(id(current))
stack.append(current)
stack.append(current.cdr)
stack.append(current.car)
return repr_each[id(self)]


@attr.s
class nil:
Expand Down Expand Up @@ -69,23 +111,25 @@ def parse_atom(close_paren=None):
return start

def parse_tail(close_paren):
first = parse_atom(close_paren)
if first is None:
return nil
if first == "." and config.dots_are_cons:
rest = parse_atom(close_paren)
if not token_stream or token_stream[-1] != close_paren:
raise ValueError(
(
"If dots are used to represent cons, then a dot"
" must be followed by a single atom, but instead was followed by "
+ (token_stream[-1] if token_stream else "EOF")
elements = []
while token_stream and token_stream[-1] != close_paren:
elements.append(parse_atom(close_paren))
if not token_stream:
raise ValueError("Unexpected end of file")
assert token_stream.pop() == close_paren
result: Union[Pair, nil] = nil
for element in reversed(elements):
if element == ".":
# pylint: disable=no-member
if result is nil or result.cdr is not nil:
raise ValueError(
"Invalid use of . in list; must be followed by a single atom, but was followed by "
+ repr(result)
)
)
assert token_stream.pop() == close_paren
return rest
rest = parse_tail(close_paren)
return Pair(first, rest)
result = result.car
else:
result = Pair(element, result)
return result

expressions = []
while token_stream:
Expand Down
29 changes: 23 additions & 6 deletions s_expression_parser/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,27 @@ def _indent(self, string, indentation):
return " " * indentation * self.indent + string

def _to_list(self, parsed):
if parsed == nil:
if parsed is nil:
return []
if isinstance(parsed, Pair):
if isinstance(parsed.cdr, Pair) or parsed.cdr is nil:
return [self._to_list(parsed.car), *self._to_list(parsed.cdr)]
return [self._to_list(parsed.car), ".", self._to_list(parsed.cdr)]
return parsed
if not isinstance(parsed, Pair):
return parsed
stack = [([], parsed)]
while True:
current_list, current_pair = stack.pop()
if current_pair is nil:
if not stack:
return current_list
stack[-1][0].append(current_list)
continue
if not isinstance(current_pair, Pair):
if current_list:
current_list.append(".")
current_list.append(current_pair)
else:
current_list = current_pair
if not stack:
return current_list
stack[-1][0].append(current_list)
continue
stack.append((current_list, current_pair.cdr))
stack.append(([], current_pair.car))
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="s-exp-parser",
version="1.3.2",
version="1.4.0",
author="Kavi Gupta",
author_email="[email protected]",
description="S expression parser for python.",
Expand Down
10 changes: 10 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ def test_multi_char_prefix(self):
)
],
)

def test_parse_extremely_long(self):
count = 10**4
structure = nil
for _ in range(count):
structure = Pair(nil, structure)
self.assertEqual(
parse("(" + "()" * count + ")", ParserConfig({}, False)),
[structure],
)
11 changes: 11 additions & 0 deletions tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,14 @@ def test_nil_as_word_large(self):
),
)
self.assertFalse("()" in text)

def test_render_extremely_long(self):
count = 10**4
structure = nil
for _ in range(count):
structure = Pair(nil, structure)
# print(structure)
self.assertEqual(
"(\n" + " ()\n" * count + ")",
Renderer(columns=1).render(structure),
)

0 comments on commit 1d0e295

Please sign in to comment.