Skip to content

Commit d6ebcd4

Browse files
committed
add CLI interface
1 parent 7b7c279 commit d6ebcd4

File tree

7 files changed

+247
-17
lines changed

7 files changed

+247
-17
lines changed

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,33 @@
33
![Tests](https://github.com/Deric-W/lambda_repl/actions/workflows/Tests.yaml/badge.svg)
44
[![codecov](https://codecov.io/gh/Deric-W/lambda_repl/branch/main/graph/badge.svg?token=SU3982mC17)](https://codecov.io/gh/Deric-W/lambda_repl)
55

6-
REPL for the lambda calculus, implemented in Python
6+
The `lambda_repl` package contains a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for the [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus).
7+
8+
To use it, execute `lambda-repl` or `python3 -m lambda_repl` and enter commands.
9+
10+
## Requirements
11+
12+
Python >= 3.10 and the `lambda_calculus` package are required to use this package.
13+
14+
## Installation
15+
16+
```sh
17+
python3 -m pip install lambda-repl
18+
```
19+
20+
## Examples
21+
22+
```
23+
python3 -m lambda_repl
24+
Welcome to the the Lambda REPL, type 'help' for help
25+
λ alias I = \x.x
26+
λ alias K = λx.λy.x
27+
λ aliases
28+
I = (λx.x)
29+
K = (λx.(λy.x))
30+
λ trace K a b
31+
β ((λy.a) b)
32+
β a
33+
λ exit
34+
Exiting REPL...
35+
```

lambda_repl/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
from .parsing import LambdaTransformer
1515
from .aliases import Aliases
1616

17-
__version__ = "0.6.0"
17+
__version__ = "1.0.0"
1818
__author__ = "Eric Niklas Wolf"
1919
__email__ = "eric_niklas.wolf@mailbox.tu-dresden.de"
2020
__all__ = (
2121
"LambdaREPL",
22-
"parsing",
23-
"aliases"
22+
"aliases",
23+
"main",
24+
"parsing"
2425
)
2526

2627

@@ -38,6 +39,7 @@ def __init__(self, aliases: Aliases[str], transformer: LambdaTransformer, visito
3839
self.aliases = aliases
3940
self.transformer = transformer
4041
self.visitor = visitor
42+
self.intro = "Welcome to the the Lambda REPL, type 'help' for help"
4143
self.prompt = "λ "
4244

4345
def parse_term(self, term: str) -> Term[str] | None:
@@ -96,7 +98,7 @@ def do_alias(self, arg: str) -> bool:
9698
def do_aliases(self, _: object) -> bool:
9799
"""list defined aliases"""
98100
for alias, term in self.aliases.items():
99-
self.stdout.write(f"{alias}: {term}\n")
101+
self.stdout.write(f"{alias} = {term}\n")
100102
return False
101103

102104
def do_clear(self, arg: str) -> bool:

lambda_repl/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/python3
2+
3+
"""CLI entry point"""
4+
5+
import sys
6+
from .main import main_cli
7+
8+
sys.exit(main_cli())

lambda_repl/main.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/python3
2+
3+
"""CLI entry point utilities"""
4+
5+
from argparse import ArgumentParser, Namespace, FileType
6+
from lambda_calculus.visitors.normalisation import BetaNormalisingVisitor
7+
from lambda_calculus.visitors.substitution.renaming import CountingSubstitution
8+
from . import LambdaREPL, __doc__ as description, __version__
9+
from .aliases import LetAliases
10+
from .parsing import LambdaTransformer
11+
12+
__all__ = (
13+
"ARGUMENT_PARSER",
14+
"main",
15+
"main_cli"
16+
)
17+
18+
ARGUMENT_PARSER = ArgumentParser(description=description)
19+
ARGUMENT_PARSER.add_argument(
20+
"-v",
21+
"--version",
22+
action="version",
23+
version=f"%(prog)s {__version__}"
24+
)
25+
ARGUMENT_PARSER.add_argument(
26+
"-f",
27+
"--file",
28+
type=FileType("r"),
29+
help="file which should be executed in the REPL"
30+
)
31+
32+
33+
def main(args: Namespace) -> int:
34+
"""Entry point for the REPL"""
35+
repl = LambdaREPL(
36+
LetAliases(CountingSubstitution),
37+
LambdaTransformer(),
38+
BetaNormalisingVisitor()
39+
)
40+
if args.file is not None:
41+
for line in args.file:
42+
repl.cmdqueue.append(line)
43+
repl.cmdloop()
44+
return 0
45+
46+
47+
def main_cli() -> int:
48+
"""CLI entry point"""
49+
return main(ARGUMENT_PARSER.parse_args())

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "lambda_repl"
3-
version = "0.6.0"
3+
version = "1.0.0"
44
description = "REPL for the lambda calculus"
55
requires-python = ">=3.10"
66
keywords = []
@@ -30,6 +30,9 @@ email = "eric_niklas.wolf@mailbox.tu-dresden.de"
3030
Repository = "https://github.com/Deric-W/lambda_repl"
3131
Bugtracker = "https://github.com/Deric-W/lambda_repl/issues"
3232

33+
[project.scripts]
34+
lambda-repl = "lambda_repl.main:main_cli"
35+
3336
[build-system]
3437
requires = ["setuptools >= 61.0.0"]
3538
build-backend = "setuptools.build_meta"

tests/test_aliases.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ def test_set(self) -> None:
2323
self.aliases["b"] = Variable("a").apply_to(Variable("c"))
2424
self.aliases["c"] = Variable("2")
2525
self.assertEqual(
26-
self.aliases,
27-
{
28-
"a": Variable("1"),
29-
"b": Variable("1").apply_to(Variable("c")),
30-
"c": Variable("2")
31-
}
26+
list(self.aliases.items()),
27+
[
28+
("a", Variable("1")),
29+
("b", Variable("1").apply_to(Variable("c"))),
30+
("c", Variable("2"))
31+
]
3232
)
3333

3434
def test_override(self) -> None:
@@ -37,11 +37,11 @@ def test_override(self) -> None:
3737
self.aliases["b"] = Variable("a").apply_to(Variable("c"))
3838
self.aliases["a"] = Variable("2")
3939
self.assertEqual(
40-
self.aliases,
41-
{
42-
"a": Variable("2"),
43-
"b": Variable("1").apply_to(Variable("c")),
44-
}
40+
list(self.aliases.items()),
41+
[
42+
("b", Variable("1").apply_to(Variable("c"))),
43+
("a", Variable("2"))
44+
]
4545
)
4646

4747
def test_apply(self) -> None:

tests/test_repl.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/python3
2+
3+
"""Tests for the REPL"""
4+
5+
from io import StringIO
6+
from unittest import TestCase
7+
from lambda_calculus.terms import Variable
8+
from lambda_calculus.visitors.substitution.renaming import CountingSubstitution
9+
from lambda_calculus.visitors.normalisation import BetaNormalisingVisitor
10+
from lambda_repl import LambdaREPL
11+
from lambda_repl.aliases import LetAliases
12+
from lambda_repl.parsing import LambdaTransformer
13+
14+
15+
class REPLTest(TestCase):
16+
"""Test for the REPL"""
17+
18+
repl: LambdaREPL
19+
20+
stdin: StringIO
21+
22+
stdout: StringIO
23+
24+
def setUp(self) -> None:
25+
"""create a REPL"""
26+
self.stdin = StringIO()
27+
self.stdout = StringIO()
28+
self.repl = LambdaREPL(
29+
LetAliases(CountingSubstitution),
30+
LambdaTransformer(),
31+
BetaNormalisingVisitor(),
32+
stdin=self.stdin,
33+
stdout=self.stdout
34+
)
35+
self.repl.use_rawinput = False
36+
37+
def test_empty(self) -> None:
38+
"""test handling of empty lines"""
39+
self.assertFalse(self.repl.onecmd(""))
40+
self.assertEqual(self.stdout.getvalue(), "")
41+
42+
def test_evaluate(self) -> None:
43+
"""test evaluating terms"""
44+
self.assertFalse(self.repl.onecmd(r"evaluate (\x.\y.x) a b"))
45+
self.assertEqual(
46+
self.stdout.getvalue(),
47+
"a\n"
48+
)
49+
50+
def test_eval(self) -> None:
51+
"""test eval alias"""
52+
self.assertFalse(self.repl.onecmd(r"eval (\x.\y.x) a b"))
53+
self.assertEqual(
54+
self.stdout.getvalue(),
55+
"a\n"
56+
)
57+
58+
def test_trace(self) -> None:
59+
"""test tracing term evaluation"""
60+
self.assertFalse(self.repl.onecmd(r"trace (\x.\y.x) a b"))
61+
self.assertEqual(
62+
self.stdout.getvalue(),
63+
"β ((λy.a) b)\nβ a\n"
64+
)
65+
66+
def test_syntax_error(self) -> None:
67+
"""test handling of syntax errors while parsing"""
68+
self.assertFalse(self.repl.onecmd(r"eval (\x.\y.x) a b."))
69+
self.assertTrue(self.stdout.getvalue().startswith("Error while parsing: "))
70+
self.assertTrue(self.stdout.getvalue().endswith("\n"))
71+
72+
def test_alias(self) -> None:
73+
"""test setting aliases"""
74+
self.assertFalse(self.repl.onecmd("alias a = b c"))
75+
self.assertEqual(
76+
self.repl.aliases,
77+
{
78+
"a": Variable("b").apply_to(Variable("c"))
79+
}
80+
)
81+
82+
def test_invalid_alias(self) -> None:
83+
"""test handling invalid aliases"""
84+
self.assertFalse(self.repl.onecmd("alias a = b c."))
85+
self.assertEqual(self.repl.aliases, {})
86+
self.assertTrue(self.stdout.getvalue().startswith("Error while parsing: "))
87+
self.assertTrue(self.stdout.getvalue().endswith("\n"))
88+
89+
def test_no_alias_value(self) -> None:
90+
"""test handling missing alias values"""
91+
self.assertFalse(self.repl.onecmd("alias a"))
92+
self.assertEqual(self.repl.aliases, {})
93+
self.assertTrue(self.stdout.getvalue().startswith("invalid Command: "))
94+
self.assertTrue(self.stdout.getvalue().endswith("\n"))
95+
96+
def test_aliases(self) -> None:
97+
"""test listing aliases"""
98+
self.assertFalse(self.repl.onecmd("alias x = 1"))
99+
self.assertFalse(self.repl.onecmd("alias a = x b"))
100+
self.assertFalse(self.repl.onecmd("alias b = b c"))
101+
self.assertFalse(self.repl.onecmd("aliases"))
102+
self.assertEqual(
103+
self.stdout.getvalue(),
104+
"x = 1\na = (1 b)\nb = (b c)\n"
105+
)
106+
107+
def test_clear(self) -> None:
108+
"""test clearing aliases"""
109+
self.assertFalse(self.repl.onecmd("alias x = 1"))
110+
self.assertFalse(self.repl.onecmd("alias a = x b"))
111+
self.assertFalse(self.repl.onecmd("alias b = b c"))
112+
self.assertFalse(self.repl.onecmd("clear x"))
113+
self.assertEqual(
114+
self.repl.aliases,
115+
{
116+
"a": Variable("1").apply_to(Variable("b")),
117+
"b": Variable("b").apply_to(Variable("c"))
118+
}
119+
)
120+
self.assertEqual(self.stdout.getvalue(), "")
121+
122+
def test_clear_all(self) -> None:
123+
"""test clearing all aliases"""
124+
self.assertFalse(self.repl.onecmd("alias x = 1"))
125+
self.assertFalse(self.repl.onecmd("alias a = x b"))
126+
self.assertFalse(self.repl.onecmd("alias b = b c"))
127+
self.assertFalse(self.repl.onecmd("clear"))
128+
self.assertEqual(self.repl.aliases, {})
129+
self.assertEqual(self.stdout.getvalue(), "")
130+
131+
def test_exit(self) -> None:
132+
"""test exiting the REPL"""
133+
self.assertTrue(self.repl.onecmd("exit"))
134+
self.assertTrue(self.stdout.getvalue().startswith("Exiting "))
135+
136+
def test_eof(self) -> None:
137+
"""test handling EOF"""
138+
self.assertTrue(self.repl.onecmd("EOF"))
139+
self.assertTrue(self.stdout.getvalue().startswith("Exiting "))

0 commit comments

Comments
 (0)