Compare commits
104 Commits
1a4ed032c2
...
master
Author | SHA1 | Date | |
---|---|---|---|
5bff77e4a7 | |||
8c8ea69890 | |||
6d14333e52 | |||
ab9057148a | |||
c07edf1b50 | |||
1bb0b91e20
|
|||
b20604de5c | |||
d2f8b1c8fb
|
|||
3022c2ffc0
|
|||
acbc247e10
|
|||
907d2d9e79
|
|||
73f4c60c06 | |||
fe7e6fca13
|
|||
866cb64485
|
|||
09a389857a
|
|||
da997b80c7
|
|||
1b951813f4
|
|||
12d6176f63
|
|||
787dbd9091
|
|||
67b2332e1c
|
|||
115b7289e7
|
|||
69bf43f295
|
|||
495c947441
|
|||
b26d968884
|
|||
e8bd6997b9
|
|||
97aecb01f0
|
|||
45493c1093
|
|||
a30bd7b870
|
|||
f3d02721ad
|
|||
48e7d405dd
|
|||
78cc118bfc
|
|||
dfbc5f3704
|
|||
bd13b10f76
|
|||
0c215a5df4
|
|||
39d32a9925
|
|||
89bc4bd2c0
|
|||
062a4b4d1b
|
|||
4e3d5fa341
|
|||
04ce18c891
|
|||
9f467fa223
|
|||
231c873b1c
|
|||
ac08c5be59
|
|||
ab682ef700
|
|||
cf2d82acc6
|
|||
60823f1922
|
|||
740c562dbe
|
|||
d88a7ff77f
|
|||
d3917b1f58
|
|||
04880b7724
|
|||
ecbb8661ce
|
|||
73b803a1b0
|
|||
c7464076ff
|
|||
33c8d5bb66
|
|||
8d05de58f7
|
|||
01e542f88c
|
|||
c58819a7d6
|
|||
09f78a7642
|
|||
81c78d7921
|
|||
f67d1e2730
|
|||
03e742821d
|
|||
602bf52983
|
|||
4847f0a9a4
|
|||
fbf2d6f36d
|
|||
6e17963d91
|
|||
393e52327e
|
|||
6e46f485c1
|
|||
e41de764e8
|
|||
8871f1430d
|
|||
affcbc33ee
|
|||
a1d8714f2f
|
|||
d74922a363
|
|||
8d1abe2712
|
|||
0dae1afb61
|
|||
a991c3bb91
|
|||
455e447999
|
|||
c5df3fcbed
|
|||
044ec60a49
|
|||
123d885adf
|
|||
239aa210c5
|
|||
31ce605674
|
|||
6a8d057425
|
|||
657d5b66af
|
|||
218af2cd49
|
|||
3cd0762d16
|
|||
fa02c2aa5b
|
|||
1bbca094dd
|
|||
0ef0e8e585
|
|||
d11c9a5a7e
|
|||
a7078f9a77
|
|||
8da3282edf
|
|||
e1b79cd59a
|
|||
8558c484c5
|
|||
ee744b5245
|
|||
b5d97cf5c6
|
|||
7a1077e371
|
|||
ef19fc42a6
|
|||
3ff496e1d6
|
|||
84c5229fbf
|
|||
a4af47435a
|
|||
01632f6ec3
|
|||
696ede7a9c
|
|||
92e741549f
|
|||
6a818b42fe
|
|||
bcd7a9d888
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,2 +1,13 @@
|
||||
# Python dev files
|
||||
venv
|
||||
__pycache__
|
||||
__pycache__
|
||||
|
||||
# Python build files
|
||||
*.egg-info
|
||||
*.spec
|
||||
build
|
||||
dist
|
||||
|
||||
# Misc
|
||||
*.gif
|
||||
misc/secrets.sh
|
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -1,7 +1,18 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"appendleft",
|
||||
"autochurch",
|
||||
"delmac",
|
||||
"Endnodes",
|
||||
"freevar",
|
||||
"mdel",
|
||||
"onefile",
|
||||
"Packrat",
|
||||
"printables",
|
||||
"pyparsing",
|
||||
"rlimit",
|
||||
"runstatus",
|
||||
"srange",
|
||||
"subvar"
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
119
README.md
Normal file
119
README.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 🐑 Lamb: A Lambda Calculus Engine
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## :brain: What is lambda calculus?
|
||||
|
||||
- [video 1](https://www.youtube.com/watch?v=3VQ382QG-y4): Introduction and boolean logic. The first few minutes are a bit confusing, but it starts to make sense at about [`6:50`](https://youtu.be/3VQ382QG-y4?t=400)
|
||||
|
||||
- [video 2](https://www.youtube.com/watch?v=pAnLQ9jwN-E): Continuation of video 1. Features combinators and numerals.
|
||||
|
||||
- [blog](https://www.driverlesscrocodile.com/technology/lambda-calculus-for-people-a-step-behind-me-1): Another introduction. Moves slower than the two videos above and doesn't assume CS knowledge. Four-part series.
|
||||
|
||||
- [handout](https://static.betalupi.com/ormc/Advanced/Lambda%20Calculus.pdf): A handout I've written on lambda calculus.
|
||||
|
||||
## :package: Installation
|
||||
|
||||
### Method 1: [PyPi](https://pypi.org/project/lamb-engine)
|
||||
1. *(Optional but recommended)* make and enter a [venv](https://docs.python.org/3/library/venv.html)
|
||||
- **On Windows, run the following in cmd or powershell:**
|
||||
- `cd Desktop`
|
||||
- `python -m venv lamb`
|
||||
- `.\Scripts\activate`
|
||||
2. `pip install lamb-engine`
|
||||
3. `lamb`
|
||||
|
||||
### Method 2: Git
|
||||
1. Clone this repository.
|
||||
2. Make and enter a [virtual environment](https://docs.python.org/3/library/venv.html).
|
||||
3. ``cd`` into this directory
|
||||
4. Run ``pip install .``
|
||||
5. Run ``lamb``
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
|
||||
Type expressions into the prompt, and Lamb will evaluate them. \
|
||||
Use your `\` (backslash) key to type a `λ`. \
|
||||
To define macros, use `=`. For example,
|
||||
```
|
||||
==> T = λab.a
|
||||
==> F = λab.a
|
||||
==> NOT = λa.a F T
|
||||
```
|
||||
|
||||
Note that there are spaces in `λa.a F T`. With no spaces, `aFT` will be parsed as one variable. \
|
||||
Lambda functions can only take single-letter, lowercase arguments. `λA.A` is not valid syntax. \
|
||||
Free variables will be shown with a `'`, like `a'`.
|
||||
|
||||
Macros are case-sensitive. If you define a macro `MAC` and accidentally write `mac` in the prompt, `mac` will become a free variable.
|
||||
|
||||
Numbers will automatically be converted to Church numerals. For example, the following line will reduce to `T`.
|
||||
```
|
||||
==> 3 NOT F
|
||||
```
|
||||
|
||||
If an expression takes too long to evaluate, you may interrupt reduction with `Ctrl-C`. \
|
||||
Exit the prompt with `Ctrl-C` or `Ctrl-D`.
|
||||
|
||||
There are many useful macros in [macros.lamb](./macros.lamb). Download the file, then load them with the `:load` command:
|
||||
```
|
||||
==> :load macros.lamb
|
||||
```
|
||||
|
||||
You can also pass files to lamb directly to have them loaded at startup:
|
||||
```
|
||||
lamb file1 file2
|
||||
```
|
||||
|
||||
Use your up/down arrows to recall history.
|
||||
|
||||
Have fun!
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
## :card_file_box: Commands
|
||||
|
||||
Lamb understands many commands. Prefix them with a `:` in the prompt.
|
||||
|
||||
`:help` Print a help message
|
||||
|
||||
`:clear` Clear the screen
|
||||
|
||||
`:rlimit [int | None]` Set maximum reduction limit. `:rlimit none` sets no limit.
|
||||
|
||||
`:macros` List macros.
|
||||
|
||||
`:mdel [macro]` Delete a macro
|
||||
|
||||
`:delmac` Delete all macros
|
||||
|
||||
`:step [yes | no]` Enable or disable step-by-step reduction. Toggle if no argument is given. When reducing by steps, the prompt tells you what kind of reduction was done last:
|
||||
|
||||
- `M`: Macro expansion
|
||||
- `C`: Church expansion
|
||||
- `H`: History expansion
|
||||
- `F`: Function application
|
||||
|
||||
`:expand [yes | no]` Enable or disable full expansion. Toggle if no argument is given. If full expansion is enabled, ALL macros will be expanded when printing output.
|
||||
|
||||
`:save [filename]` \
|
||||
`:load [filename]` \
|
||||
Save or load macros from a file.
|
||||
The lines in a file look exactly the same as regular entries in the prompt, but can only contain macro definitions. See [macros.lamb](./macros.lamb) for an example.
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
## Todo:
|
||||
- Prevent macro-chaining recursion
|
||||
- Cleanup warnings
|
||||
- Truncate long expressions in warnings
|
||||
- Loop detection
|
||||
- Unchurch command: make church numerals human-readable
|
||||
- Better syntax highlighting
|
||||
- Tab-complete file names and commands
|
||||
- Load default macros without manually downloading `macros.lamb` (via `requests`, maybe?)
|
53
greeting.py
53
greeting.py
@ -1,53 +0,0 @@
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.formatted_text import HTML, to_formatted_text
|
||||
from prompt_toolkit import print_formatted_text
|
||||
|
||||
|
||||
|
||||
# | _.._ _.|_
|
||||
# |_(_|| | ||_)
|
||||
# 1.1.0
|
||||
#
|
||||
# __ __
|
||||
# ,-` `` `,
|
||||
# (` \ )
|
||||
# (` \ `)
|
||||
# (, / \ _)
|
||||
# (` / \ )
|
||||
# `'._.--._.'
|
||||
#
|
||||
# A λ calculus engine
|
||||
|
||||
|
||||
style = Style.from_dict({
|
||||
# Heading
|
||||
"_h": "#FFFFFF bold",
|
||||
|
||||
# Version
|
||||
"_v": "#B4EC85 bold",
|
||||
|
||||
# Lambda
|
||||
"_l": "#FF6600 bold",
|
||||
|
||||
# Subtitle
|
||||
"_s": "#B4EC85 bold"
|
||||
})
|
||||
|
||||
html = HTML(f"""
|
||||
<_h> | _.._ _.|_
|
||||
|_(_|| | ||_)</_h>
|
||||
<_v>1.1.0</_v>
|
||||
__ __
|
||||
,-` `` `,
|
||||
(` <_l>\\</_l> )
|
||||
(` <_l>\\</_l> `)
|
||||
(, <_l>/ \\</_l> _)
|
||||
(` <_l>/ \\</_l> )
|
||||
`'._.--._.'
|
||||
|
||||
<_s> A λ calculus engine</_s>
|
||||
|
||||
"""[1:-1])
|
||||
|
||||
def show():
|
||||
print_formatted_text(html, style = style)
|
8
lamb_engine/__init__.py
Normal file
8
lamb_engine/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from . import utils
|
||||
from . import nodes
|
||||
from . import parser
|
||||
|
||||
from .runner import Runner
|
||||
from .runner import StopReason
|
||||
|
||||
from .__main__ import main
|
79
lamb_engine/__main__.py
Executable file
79
lamb_engine/__main__.py
Executable file
@ -0,0 +1,79 @@
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.formatted_text import to_plain_text
|
||||
from pyparsing import exceptions as ppx
|
||||
import sys
|
||||
|
||||
import lamb_engine
|
||||
|
||||
def main():
|
||||
|
||||
lamb_engine.utils.show_greeting()
|
||||
|
||||
|
||||
r = lamb_engine.Runner(
|
||||
prompt_session = PromptSession(
|
||||
style = lamb_engine.utils.style,
|
||||
lexer = lamb_engine.utils.LambdaLexer(),
|
||||
key_bindings = lamb_engine.utils.bindings
|
||||
),
|
||||
prompt_message = FormattedText([
|
||||
("class:prompt", "==> ")
|
||||
])
|
||||
)
|
||||
|
||||
# Load files passed as arguments
|
||||
if len(sys.argv) > 1:
|
||||
for i in range(1, len(sys.argv)):
|
||||
try:
|
||||
printf(FormattedText([
|
||||
("class:warn", "\nLoading file "),
|
||||
("class:code", sys.argv[i]),
|
||||
]), style = lamb_engine.utils.style)
|
||||
r.run(":load " + sys.argv[i])
|
||||
except:
|
||||
printf(FormattedText([
|
||||
("class:err", "Error. Does this file exist?"),
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
print("")
|
||||
|
||||
while True:
|
||||
try:
|
||||
i = r.prompt()
|
||||
|
||||
# Catch Ctrl-C and Ctrl-D
|
||||
except KeyboardInterrupt:
|
||||
printf("\n\nGoodbye.\n")
|
||||
break
|
||||
except EOFError:
|
||||
printf("\n\nGoodbye.\n")
|
||||
break
|
||||
|
||||
# Skip empty lines
|
||||
if i.strip() == "":
|
||||
continue
|
||||
|
||||
# Try to run an input line.
|
||||
# Catch parse errors and point them out.
|
||||
try:
|
||||
x = r.run(i)
|
||||
except ppx.ParseException as e:
|
||||
l = len(to_plain_text(r.prompt_session.message))
|
||||
printf(FormattedText([
|
||||
("class:err", " "*(e.loc + l) + "^\n"),
|
||||
("class:err", f"Syntax error at char {e.loc}."),
|
||||
("class:text", "\n")
|
||||
]), style = lamb_engine.utils.style)
|
||||
continue
|
||||
except lamb_engine.nodes.ReductionError as e:
|
||||
printf(FormattedText([
|
||||
("class:err", f"{e.msg}\n")
|
||||
]), style = lamb_engine.utils.style)
|
||||
continue
|
||||
|
||||
printf("")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
3
lamb_engine/nodes/__init__.py
Normal file
3
lamb_engine/nodes/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .misc import *
|
||||
from .nodes import *
|
||||
from .functions import *
|
287
lamb_engine/nodes/functions.py
Normal file
287
lamb_engine/nodes/functions.py
Normal file
@ -0,0 +1,287 @@
|
||||
import lamb_engine
|
||||
import lamb_engine.nodes as lbn
|
||||
|
||||
def print_node(node: lbn.Node, *, export: bool = False) -> str:
|
||||
if not isinstance(node, lbn.Node):
|
||||
raise TypeError(f"I don't know how to print a {type(node)}")
|
||||
|
||||
out = ""
|
||||
|
||||
bound_subs = {}
|
||||
|
||||
for s, n in node:
|
||||
if isinstance(n, lbn.EndNode):
|
||||
if isinstance(n, lbn.Bound):
|
||||
out += bound_subs[n.identifier]
|
||||
else:
|
||||
out += n.print_value(export = export)
|
||||
|
||||
elif isinstance(n, lbn.Func):
|
||||
# This should never be true, but
|
||||
# keep this here to silence type checker.
|
||||
if not isinstance(n.input, lbn.Bound):
|
||||
raise Exception("input is macro, something is wrong.")
|
||||
|
||||
if s == lbn.Direction.UP:
|
||||
o = n.input.print_value(export = export)
|
||||
if o in bound_subs.values():
|
||||
i = -1
|
||||
p = o
|
||||
while o in bound_subs.values():
|
||||
o = p + lamb_engine.utils.subscript(i := i + 1)
|
||||
bound_subs[n.input.identifier] = o
|
||||
else:
|
||||
bound_subs[n.input.identifier] = n.input.print_value()
|
||||
|
||||
if isinstance(n.parent, lbn.Call):
|
||||
out += "("
|
||||
|
||||
if isinstance(n.parent, lbn.Func):
|
||||
out += bound_subs[n.input.identifier]
|
||||
else:
|
||||
out += "λ" + bound_subs[n.input.identifier]
|
||||
if not isinstance(n.left, lbn.Func):
|
||||
out += "."
|
||||
|
||||
elif s == lbn.Direction.LEFT:
|
||||
if isinstance(n.parent, lbn.Call):
|
||||
out += ")"
|
||||
del bound_subs[n.input.identifier]
|
||||
|
||||
elif isinstance(n, lbn.Call):
|
||||
if s == lbn.Direction.UP:
|
||||
out += "("
|
||||
elif s == lbn.Direction.LEFT:
|
||||
out += " "
|
||||
elif s == lbn.Direction.RIGHT:
|
||||
out += ")"
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def clone(node: lbn.Node):
|
||||
if not isinstance(node, lbn.Node):
|
||||
raise TypeError(f"I don't know what to do with a {type(node)}")
|
||||
|
||||
macro_map = {}
|
||||
if isinstance(node, lbn.Func):
|
||||
c = node.copy()
|
||||
macro_map[node.input.identifier] = c.input.identifier # type: ignore
|
||||
else:
|
||||
c = node.copy()
|
||||
|
||||
out = c
|
||||
out_ptr = out # Stays one step behind ptr, in the new tree.
|
||||
ptr = node
|
||||
from_side = lbn.Direction.UP
|
||||
|
||||
|
||||
if isinstance(node, lbn.EndNode):
|
||||
return out
|
||||
|
||||
# We're not using a TreeWalker here because
|
||||
# we need more control over our pointer when cloning.
|
||||
while True:
|
||||
if isinstance(ptr, lbn.EndNode):
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root):
|
||||
if from_side == lbn.Direction.UP:
|
||||
from_side, ptr = ptr.go_left()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_left()
|
||||
elif from_side == lbn.Direction.LEFT:
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
elif isinstance(ptr, lbn.Call):
|
||||
if from_side == lbn.Direction.UP:
|
||||
from_side, ptr = ptr.go_left()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_left()
|
||||
elif from_side == lbn.Direction.LEFT:
|
||||
from_side, ptr = ptr.go_right()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_right()
|
||||
elif from_side == lbn.Direction.RIGHT:
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
|
||||
if ptr is node.parent:
|
||||
break
|
||||
return out
|
||||
|
||||
def prepare(root: lbn.Root, *, ban_macro_name = None) -> list:
|
||||
"""
|
||||
Prepare an expression for expansion.
|
||||
This will does the following:
|
||||
- Binds variables
|
||||
- Turns unbound macros into free variables
|
||||
- Generates warnings
|
||||
"""
|
||||
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||||
|
||||
bound_variables = {}
|
||||
|
||||
warnings = []
|
||||
|
||||
it = iter(root)
|
||||
for s, n in it:
|
||||
if isinstance(n, lbn.History):
|
||||
if root.runner.history[0] == None:
|
||||
raise lbn.ReductionError("There isn't any history to reference.")
|
||||
else:
|
||||
warnings += [
|
||||
("class:code", "$"),
|
||||
("class:warn", " will be expanded to ")
|
||||
] + lamb_engine.utils.lex_str(str(n.expand()[1]))
|
||||
|
||||
# If this expression is part of a macro,
|
||||
# make sure we don't reference it inside itself.
|
||||
elif isinstance(n, lbn.Macro):
|
||||
if (n.name == ban_macro_name) and (ban_macro_name is not None):
|
||||
raise lbn.ReductionError("Macro cannot reference self")
|
||||
|
||||
# Bind variables
|
||||
if n.name in bound_variables:
|
||||
n.parent.set_side(
|
||||
n.parent_side,
|
||||
clone(bound_variables[n.name])
|
||||
)
|
||||
it.ptr = n.parent.get_side(n.parent_side)
|
||||
|
||||
# Turn undefined macros into free variables
|
||||
elif n.name not in root.runner.macro_table:
|
||||
warnings += [
|
||||
("class:warn", "Name "),
|
||||
("class:code", n.name),
|
||||
("class:warn", " is a free variable\n"),
|
||||
]
|
||||
n.parent.set_side(
|
||||
n.parent_side,
|
||||
n.to_freevar()
|
||||
)
|
||||
it.ptr = n.parent.get_side(n.parent_side)
|
||||
|
||||
|
||||
# Save bound variables when we enter a function's sub-tree,
|
||||
# delete them when we exit it.
|
||||
elif isinstance(n, lbn.Func):
|
||||
if s == lbn.Direction.UP:
|
||||
# Add this function's input to the table of bound variables.
|
||||
# If it is already there, raise an error.
|
||||
if (n.input.name in bound_variables):
|
||||
raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"")
|
||||
else:
|
||||
bound_variables[n.input.name] = lbn.Bound(
|
||||
lamb_engine.utils.remove_sub(n.input.name),
|
||||
macro_name = n.input.name
|
||||
)
|
||||
n.input = bound_variables[n.input.name]
|
||||
|
||||
elif s == lbn.Direction.LEFT:
|
||||
del bound_variables[n.input.macro_name] # type: ignore
|
||||
|
||||
return warnings
|
||||
|
||||
# Apply a function.
|
||||
# Returns the function's output.
|
||||
def call_func(fn: lbn.Func, arg: lbn.Node):
|
||||
for s, n in fn:
|
||||
if isinstance(n, lbn.Bound) and (s == lbn.Direction.UP):
|
||||
if n == fn.input:
|
||||
if n.parent is None:
|
||||
raise Exception("Tried to substitute a None bound variable.")
|
||||
|
||||
n.parent.set_side(n.parent_side, clone(arg)) # type: ignore
|
||||
return fn.left
|
||||
|
||||
# Do a single reduction step
|
||||
def reduce(root: lbn.Root) -> tuple[lbn.ReductionType, lbn.Root]:
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I can't reduce a {type(root)}")
|
||||
|
||||
out = root
|
||||
for s, n in out:
|
||||
if isinstance(n, lbn.Call) and (s == lbn.Direction.UP):
|
||||
if isinstance(n.left, lbn.Func):
|
||||
n.parent.set_side(
|
||||
n.parent_side, # type: ignore
|
||||
call_func(n.left, n.right)
|
||||
)
|
||||
|
||||
return lbn.ReductionType.FUNCTION_APPLY, out
|
||||
|
||||
elif isinstance(n.left, lbn.ExpandableEndNode):
|
||||
r, n.left = n.left.expand()
|
||||
return r, out
|
||||
return lbn.ReductionType.NOTHING, out
|
||||
|
||||
|
||||
def expand(root: lbn.Root, *, force_all = False) -> tuple[int, lbn.Root]:
|
||||
"""
|
||||
Expands expandable nodes in the given tree.
|
||||
|
||||
If force_all is false, this only expands
|
||||
ExpandableEndnodes that have "always_expand" set to True.
|
||||
|
||||
If force_all is True, this expands ALL
|
||||
ExpandableEndnodes.
|
||||
"""
|
||||
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||||
|
||||
out = root
|
||||
macro_expansions = 0
|
||||
|
||||
it = iter(root)
|
||||
for s, n in it:
|
||||
if (
|
||||
isinstance(n, lbn.ExpandableEndNode) and
|
||||
(force_all or n.always_expand)
|
||||
):
|
||||
|
||||
n.parent.set_side(
|
||||
n.parent_side, # type: ignore
|
||||
n.expand()[1]
|
||||
)
|
||||
it.ptr = n.parent.get_side(
|
||||
n.parent_side # type: ignore
|
||||
)
|
||||
macro_expansions += 1
|
||||
return macro_expansions, out
|
44
lamb_engine/nodes/misc.py
Normal file
44
lamb_engine/nodes/misc.py
Normal file
@ -0,0 +1,44 @@
|
||||
import enum
|
||||
|
||||
class Direction(enum.Enum):
|
||||
UP = enum.auto()
|
||||
LEFT = enum.auto()
|
||||
RIGHT = enum.auto()
|
||||
|
||||
|
||||
class ReductionType(enum.Enum):
|
||||
# Nothing happened. This implies that
|
||||
# an expression cannot be reduced further.
|
||||
NOTHING = enum.auto()
|
||||
|
||||
# We replaced a macro with an expression.
|
||||
MACRO_EXPAND = enum.auto()
|
||||
|
||||
# We expanded a history reference
|
||||
HIST_EXPAND = enum.auto()
|
||||
|
||||
# We turned a church numeral into an expression
|
||||
AUTOCHURCH = enum.auto()
|
||||
|
||||
# We applied a function.
|
||||
# This is the only type of "formal" reduction step.
|
||||
FUNCTION_APPLY = enum.auto()
|
||||
|
||||
# Pretty, short names for each reduction type.
|
||||
# These should all have the same length.
|
||||
reduction_text = {
|
||||
ReductionType.NOTHING: "N",
|
||||
ReductionType.MACRO_EXPAND: "M",
|
||||
ReductionType.HIST_EXPAND: "H",
|
||||
ReductionType.AUTOCHURCH: "C",
|
||||
ReductionType.FUNCTION_APPLY: "F",
|
||||
}
|
||||
|
||||
class ReductionError(Exception):
|
||||
"""
|
||||
Raised when we encounter an error while reducing.
|
||||
|
||||
These should be caught and elegantly presented to the user.
|
||||
"""
|
||||
def __init__(self, msg: str):
|
||||
self.msg = msg
|
452
lamb_engine/nodes/nodes.py
Normal file
452
lamb_engine/nodes/nodes.py
Normal file
@ -0,0 +1,452 @@
|
||||
import lamb_engine
|
||||
import lamb_engine.nodes as lbn
|
||||
|
||||
class TreeWalker:
|
||||
"""
|
||||
An iterator that walks the "outline" of a tree
|
||||
defined by a chain of nodes.
|
||||
|
||||
It returns a tuple: (out_side, out)
|
||||
|
||||
out is the node we moved to,
|
||||
out_side is the direction we came to the node from.
|
||||
"""
|
||||
|
||||
def __init__(self, expr):
|
||||
self.expr = expr
|
||||
self.ptr = expr
|
||||
self.first_step = True
|
||||
self.from_side = lbn.Direction.UP
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
# This could be implemented without checking the node type,
|
||||
# but there's no reason to do that.
|
||||
# Maybe later?
|
||||
|
||||
|
||||
if self.first_step:
|
||||
self.first_step = False
|
||||
return self.from_side, self.ptr
|
||||
|
||||
if isinstance(self.ptr, Root):
|
||||
if self.from_side == lbn.Direction.UP:
|
||||
self.from_side, self.ptr = self.ptr.go_left()
|
||||
elif isinstance(self.ptr, EndNode):
|
||||
self.from_side, self.ptr = self.ptr.go_up()
|
||||
elif isinstance(self.ptr, Func):
|
||||
if self.from_side == lbn.Direction.UP:
|
||||
self.from_side, self.ptr = self.ptr.go_left()
|
||||
elif self.from_side == lbn.Direction.LEFT:
|
||||
self.from_side, self.ptr = self.ptr.go_up()
|
||||
elif isinstance(self.ptr, Call):
|
||||
if self.from_side == lbn.Direction.UP:
|
||||
self.from_side, self.ptr = self.ptr.go_left()
|
||||
elif self.from_side == lbn.Direction.LEFT:
|
||||
self.from_side, self.ptr = self.ptr.go_right()
|
||||
elif self.from_side == lbn.Direction.RIGHT:
|
||||
self.from_side, self.ptr = self.ptr.go_up()
|
||||
else:
|
||||
raise TypeError(f"I don't know how to iterate a {type(self.ptr)}")
|
||||
|
||||
# Stop conditions
|
||||
if isinstance(self.expr, Root):
|
||||
if self.ptr is self.expr:
|
||||
raise StopIteration
|
||||
else:
|
||||
if self.ptr is self.expr.parent:
|
||||
raise StopIteration
|
||||
|
||||
return self.from_side, self.ptr
|
||||
|
||||
class Node:
|
||||
"""
|
||||
Generic class for an element of an expression tree.
|
||||
All nodes are subclasses of this.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# The node this one is connected to.
|
||||
# None if this is the top objects.
|
||||
self.parent: Node = None # type: ignore
|
||||
|
||||
# What direction this is relative to the parent.
|
||||
# Left of Right.
|
||||
self.parent_side: Direction = None # type: ignore
|
||||
|
||||
# Left and right nodes, None if empty
|
||||
self._left = None
|
||||
self._right = None
|
||||
|
||||
# The runner this node is attached to.
|
||||
# Set by Node.set_runner()
|
||||
self.runner: lamb_engine.runner.Runner = None # type: ignore
|
||||
|
||||
def __iter__(self):
|
||||
return TreeWalker(self)
|
||||
|
||||
def _set_parent(self, parent, side):
|
||||
"""
|
||||
Set this node's parent and parent side.
|
||||
This method shouldn't be called explicitly unless
|
||||
there's no other option. Use self.left and self.right instead.
|
||||
"""
|
||||
|
||||
if (parent is not None) and (side is None):
|
||||
raise Exception("If a node has a parent, it must have a lbn.direction.")
|
||||
if (parent is None) and (side is not None):
|
||||
raise Exception("If a node has no parent, it cannot have a lbn.direction.")
|
||||
self.parent = parent
|
||||
self.parent_side = side
|
||||
return self
|
||||
|
||||
@property
|
||||
def left(self):
|
||||
return self._left
|
||||
|
||||
@left.setter
|
||||
def left(self, node):
|
||||
if node is not None:
|
||||
node._set_parent(self, lbn.Direction.LEFT)
|
||||
self._left = node
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
return self._right
|
||||
|
||||
@right.setter
|
||||
def right(self, node):
|
||||
if node is not None:
|
||||
node._set_parent(self, lbn.Direction.RIGHT)
|
||||
self._right = node
|
||||
|
||||
|
||||
def set_side(self, side: lbn.Direction, node):
|
||||
"""
|
||||
A wrapper around Node.left and Node.right that
|
||||
automatically selects a side.
|
||||
"""
|
||||
|
||||
if side == lbn.Direction.LEFT:
|
||||
self.left = node
|
||||
elif side == lbn.Direction.RIGHT:
|
||||
self.right = node
|
||||
else:
|
||||
raise TypeError("Can only set left or right side.")
|
||||
|
||||
def get_side(self, side: lbn.Direction):
|
||||
if side == lbn.Direction.LEFT:
|
||||
return self.left
|
||||
elif side == lbn.Direction.RIGHT:
|
||||
return self.right
|
||||
else:
|
||||
raise TypeError("Can only get left or right side.")
|
||||
|
||||
|
||||
def go_left(self):
|
||||
"""
|
||||
Go down the left branch of this node.
|
||||
Returns a tuple (from_dir, node)
|
||||
|
||||
from_dir is the direction from which we came INTO the next node.
|
||||
node is the node on the left of this one.
|
||||
"""
|
||||
|
||||
if self._left is None:
|
||||
raise Exception("Can't go left when left is None")
|
||||
return lbn.Direction.UP, self._left
|
||||
|
||||
def go_right(self):
|
||||
"""
|
||||
Go down the right branch of this node.
|
||||
Returns a tuple (from_dir, node)
|
||||
|
||||
from_dir is the direction from which we came INTO the next node.
|
||||
node is the node on the right of this one.
|
||||
"""
|
||||
if self._right is None:
|
||||
raise Exception("Can't go right when right is None")
|
||||
return lbn.Direction.UP, self._right
|
||||
|
||||
def go_up(self):
|
||||
"""
|
||||
Go up th the parent of this node.
|
||||
Returns a tuple (from_dir, node)
|
||||
|
||||
from_dir is the direction from which we came INTO the parent.
|
||||
node is the node above of this one.
|
||||
"""
|
||||
return self.parent_side, self.parent
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
Return a copy of this node.
|
||||
parent, parent_side, left, and right should be left
|
||||
as None, and will be filled later.
|
||||
"""
|
||||
raise NotImplementedError("Nodes MUST provide a `copy` method!")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return lbn.print_node(self)
|
||||
|
||||
def export(self) -> str:
|
||||
"""
|
||||
Convert this tree to a parsable string.
|
||||
"""
|
||||
return lbn.print_node(self, export = True)
|
||||
|
||||
def set_runner(self, runner):
|
||||
for s, n in self:
|
||||
if s == lbn.Direction.UP:
|
||||
n.runner = runner # type: ignore
|
||||
return self
|
||||
|
||||
class EndNode(Node):
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
raise NotImplementedError("EndNodes MUST provide a `print_value` method!")
|
||||
|
||||
class ExpandableEndNode(EndNode):
|
||||
always_expand = False
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!")
|
||||
|
||||
class FreeVar(EndNode):
|
||||
def __init__(self, name: str, *, runner = None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<freevar {self.name}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
if export:
|
||||
return f"{self.name}'"
|
||||
else:
|
||||
return f"{self.name}'"
|
||||
|
||||
def copy(self):
|
||||
return FreeVar(self.name)
|
||||
|
||||
class Macro(ExpandableEndNode):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return Macro(results[0])
|
||||
|
||||
def __init__(self, name: str, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<macro {self.name}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return self.name
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
if self.name in self.runner.macro_table:
|
||||
# The element in the macro table will be a Root node,
|
||||
# so we clone its left element.
|
||||
return (
|
||||
lbn.ReductionType.MACRO_EXPAND,
|
||||
lbn.clone(self.runner.macro_table[self.name].left)
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Macro {self.name} is not defined")
|
||||
|
||||
def to_freevar(self):
|
||||
return FreeVar(self.name, runner = self.runner)
|
||||
|
||||
def copy(self):
|
||||
return Macro(self.name, runner = self.runner)
|
||||
|
||||
class Church(ExpandableEndNode):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return Church(int(results[0]))
|
||||
|
||||
def __init__(self, value: int, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.value = value
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<church {self.value}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return str(self.value)
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
f = Bound("f")
|
||||
a = Bound("a")
|
||||
chain = a
|
||||
|
||||
for i in range(self.value):
|
||||
chain = Call(lbn.clone(f), lbn.clone(chain))
|
||||
|
||||
return (
|
||||
lbn.ReductionType.AUTOCHURCH,
|
||||
Func(f, Func(a, chain)).set_runner(self.runner)
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
return Church(self.value, runner = self.runner)
|
||||
|
||||
class History(ExpandableEndNode):
|
||||
always_expand = True
|
||||
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return History()
|
||||
|
||||
def __init__(self, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<$>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return "$"
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
# We shouldn't ever get here, prepare()
|
||||
# catches empty history.
|
||||
if self.runner.history[0] == None:
|
||||
raise Exception(f"Tried to expand empty history.")
|
||||
# .left is VERY important!
|
||||
# self.runner.history will contain Root nodes,
|
||||
# and we don't want those *inside* our tree.
|
||||
return lbn.ReductionType.HIST_EXPAND, lbn.clone(self.runner.history[0].left)
|
||||
|
||||
def copy(self):
|
||||
return History(runner = self.runner)
|
||||
|
||||
bound_counter = 0
|
||||
class Bound(EndNode):
|
||||
def __init__(self, name: str, *, forced_id = None, runner = None, macro_name = None):
|
||||
self.name = name
|
||||
global bound_counter
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
# The name of the macro this bound came from.
|
||||
# Always equal to self.name, unless the macro
|
||||
# this came from had a subscript.
|
||||
self.macro_name = macro_name
|
||||
|
||||
if forced_id is None:
|
||||
self.identifier = bound_counter
|
||||
bound_counter += 1
|
||||
else:
|
||||
self.identifier = forced_id
|
||||
|
||||
def copy(self):
|
||||
return Bound(
|
||||
self.name,
|
||||
forced_id = self.identifier,
|
||||
runner = self.runner
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Bound):
|
||||
raise TypeError(f"Cannot compare bound_variable with {type(other)}")
|
||||
return self.identifier == other.identifier
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.name} {self.identifier}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return self.name
|
||||
|
||||
class Func(Node):
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
if len(result[0]) == 1:
|
||||
return Func(
|
||||
result[0][0],
|
||||
result[1]
|
||||
)
|
||||
else:
|
||||
return Func(
|
||||
result[0].pop(0),
|
||||
Func.from_parse(result)
|
||||
)
|
||||
|
||||
def __init__(self, input, output: Node, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.input = input
|
||||
self.left: Node = output
|
||||
self.right: None = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<func {self.input!r} {self.left!r}>"
|
||||
|
||||
def copy(self):
|
||||
return Func(
|
||||
Bound(
|
||||
self.input.name,
|
||||
runner = self.runner
|
||||
),
|
||||
None, # type: ignore
|
||||
runner = self.runner
|
||||
)
|
||||
|
||||
class Root(Node):
|
||||
"""
|
||||
Root node.
|
||||
Used at the top of an expression.
|
||||
"""
|
||||
|
||||
def __init__(self, left: Node, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.left: Node = left
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Root {self.left!r}>"
|
||||
|
||||
def copy(self):
|
||||
return Root(None, runner = self.runner) # type: ignore
|
||||
|
||||
class Call(Node):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
if len(results) == 2:
|
||||
return Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)
|
||||
else:
|
||||
this = Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)
|
||||
|
||||
return Call.from_parse(
|
||||
[Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)] + results[2:]
|
||||
)
|
||||
|
||||
def __init__(self, fn: Node, arg: Node, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.left: Node = fn
|
||||
self.right: Node = arg
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<call {self.left!r} {self.right!r}>"
|
||||
|
||||
def copy(self):
|
||||
return Call(None, None, runner = self.runner) # type: ignore
|
101
lamb_engine/parser.py
Executable file
101
lamb_engine/parser.py
Executable file
@ -0,0 +1,101 @@
|
||||
import pyparsing as pp
|
||||
|
||||
# Packrat gives a significant speed boost.
|
||||
pp.ParserElement.enablePackrat()
|
||||
|
||||
class LambdaParser:
|
||||
def make_parser(self):
|
||||
self.lp = pp.Suppress("(")
|
||||
self.rp = pp.Suppress(")")
|
||||
self.pp_expr = pp.Forward()
|
||||
|
||||
# Bound variables are ALWAYS lowercase and single-character.
|
||||
# We still create macro objects from them, they are turned into
|
||||
# bound variables after the expression is built.
|
||||
self.pp_macro = pp.Word(pp.alphas + "_")
|
||||
self.pp_bound = pp.Regex("[a-z][₀₁₂₃₄₅₆₈₉]*")
|
||||
self.pp_name = self.pp_bound ^ self.pp_macro
|
||||
self.pp_church = pp.Word(pp.nums)
|
||||
self.pp_history = pp.Char("$")
|
||||
|
||||
# Function calls.
|
||||
#
|
||||
# <exp> <exp>
|
||||
# <exp> <exp> <exp>
|
||||
self.pp_call = pp.Forward()
|
||||
self.pp_call <<= (self.pp_expr | self.pp_bound | self.pp_history)[2, ...]
|
||||
|
||||
# Function definitions, right associative.
|
||||
# Function args MUST be lowercase.
|
||||
#
|
||||
# <var> => <exp>
|
||||
self.pp_lambda_fun = (
|
||||
(pp.Suppress("λ") | pp.Suppress("\\")) +
|
||||
pp.Group(self.pp_bound[1, ...]) +
|
||||
pp.Suppress(".") +
|
||||
(self.pp_expr ^ self.pp_call)
|
||||
)
|
||||
|
||||
# Assignment.
|
||||
# Can only be found at the start of a line.
|
||||
#
|
||||
# <name> = <exp>
|
||||
self.pp_macro_def = (
|
||||
pp.line_start() +
|
||||
self.pp_macro +
|
||||
pp.Suppress("=") +
|
||||
(self.pp_expr ^ self.pp_call ^ self.pp_history)
|
||||
)
|
||||
|
||||
self.pp_expr <<= (
|
||||
self.pp_church ^
|
||||
self.pp_lambda_fun ^
|
||||
self.pp_name ^
|
||||
(self.lp + self.pp_expr + self.rp) ^
|
||||
(self.lp + self.pp_call + self.rp) ^
|
||||
(self.lp + self.pp_history + self.rp)
|
||||
)
|
||||
|
||||
self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.printables)[0, ...]
|
||||
|
||||
|
||||
self.pp_all = (
|
||||
self.pp_expr ^
|
||||
self.pp_macro_def ^
|
||||
self.pp_command ^
|
||||
self.pp_call ^
|
||||
self.pp_history
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
action_command,
|
||||
action_macro_def,
|
||||
action_church,
|
||||
action_func,
|
||||
action_bound,
|
||||
action_macro,
|
||||
action_call,
|
||||
action_history
|
||||
):
|
||||
|
||||
self.make_parser()
|
||||
|
||||
self.pp_command.set_parse_action(action_command)
|
||||
self.pp_macro_def.set_parse_action(action_macro_def)
|
||||
self.pp_church.set_parse_action(action_church)
|
||||
self.pp_lambda_fun.set_parse_action(action_func)
|
||||
self.pp_macro.set_parse_action(action_macro)
|
||||
self.pp_bound.set_parse_action(action_bound)
|
||||
self.pp_call.set_parse_action(action_call)
|
||||
self.pp_history.set_parse_action(action_history)
|
||||
|
||||
def parse_line(self, line: str):
|
||||
return self.pp_all.parse_string(
|
||||
line,
|
||||
parse_all = True
|
||||
)[0]
|
||||
|
||||
def run_tests(self, lines: list[str]):
|
||||
return self.pp_all.run_tests(lines)
|
2
lamb_engine/runner/__init__.py
Normal file
2
lamb_engine/runner/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .runner import Runner
|
||||
from .runner import StopReason
|
420
lamb_engine/runner/commands.py
Normal file
420
lamb_engine/runner/commands.py
Normal file
@ -0,0 +1,420 @@
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from prompt_toolkit.shortcuts import clear as clear_screen
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
import os.path
|
||||
from pyparsing import exceptions as ppx
|
||||
|
||||
import lamb_engine
|
||||
|
||||
commands = {}
|
||||
help_texts = {}
|
||||
|
||||
def lamb_command(
|
||||
*,
|
||||
command_name = None,
|
||||
help_text: str
|
||||
):
|
||||
"""
|
||||
A decorator that allows us to easily make commands
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
name = func.__name__ if command_name is None else command_name
|
||||
|
||||
commands[name] = func
|
||||
help_texts[name] = help_text
|
||||
return inner
|
||||
|
||||
@lamb_command(
|
||||
command_name = "step",
|
||||
help_text = "Toggle step-by-step reduction"
|
||||
)
|
||||
def cmd_step(command, runner) -> None:
|
||||
if len(command.args) > 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes no more than one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = not runner.step_reduction
|
||||
if len(command.args) == 1:
|
||||
if command.args[0].lower() in ("y", "yes"):
|
||||
target = True
|
||||
elif command.args[0].lower() in ("n", "no"):
|
||||
target = False
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Usage: <code>:step [yes|no]</code></err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if target:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Enabled step-by-step reduction.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.step_reduction = True
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Disabled step-by-step reduction.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.step_reduction = False
|
||||
|
||||
@lamb_command(
|
||||
command_name = "expand",
|
||||
help_text = "Toggle full expansion"
|
||||
)
|
||||
def cmd_expand(command, runner) -> None:
|
||||
if len(command.args) > 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes no more than one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = not runner.full_expansion
|
||||
if len(command.args) == 1:
|
||||
if command.args[0].lower() in ("y", "yes"):
|
||||
target = True
|
||||
elif command.args[0].lower() in ("n", "no"):
|
||||
target = False
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Usage: <code>:expand [yes|no]</code></err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if target:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Enabled complete expansion.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.full_expansion = True
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Disabled complete expansion.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.full_expansion = False
|
||||
|
||||
|
||||
@lamb_command(
|
||||
command_name = "save",
|
||||
help_text = "Save macros to a file"
|
||||
)
|
||||
def cmd_save(command, runner) -> None:
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if os.path.exists(target):
|
||||
confirm = prompt(
|
||||
message = FormattedText([
|
||||
("class:warn", "File exists. Overwrite? "),
|
||||
("class:text", "[yes/no]: ")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
).lower()
|
||||
|
||||
if confirm != "yes":
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Cancelled.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
with open(target, "w") as f:
|
||||
f.write("\n".join(
|
||||
[f"{n} = {e.export()}" for n, e in runner.macro_table.items()]
|
||||
))
|
||||
|
||||
printf(
|
||||
HTML(
|
||||
f"Wrote {len(runner.macro_table)} macros to <code>{target}</code>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
@lamb_command(
|
||||
command_name = "load",
|
||||
help_text = "Load macros from a file"
|
||||
)
|
||||
def cmd_load(command, runner):
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if not os.path.exists(target):
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>File {target} doesn't exist.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
with open(target, "r") as f:
|
||||
lines = [x.strip() for x in f.readlines()]
|
||||
|
||||
for i in range(len(lines)):
|
||||
l = lines[i].strip()
|
||||
|
||||
# Skip comments and empty lines
|
||||
if l.startswith("#"):
|
||||
continue
|
||||
if l == "":
|
||||
continue
|
||||
|
||||
try:
|
||||
x = runner.parse(l)[0]
|
||||
except ppx.ParseException as e:
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Syntax error on line {i+1:02}: "),
|
||||
("class:code", l[:e.loc]),
|
||||
("class:err", l[e.loc]),
|
||||
("class:code", l[e.loc+1:])
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(x, lamb_engine.runner.runner.MacroDef):
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Skipping line {i+1:02}: "),
|
||||
("class:code", l),
|
||||
("class:warn", f" is not a macro definition.")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.save_macro(x, silent = True)
|
||||
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:ok", f"Loaded {x.label}: ")
|
||||
] + lamb_engine.utils.lex_str(str(x.expr))),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Delete a macro"
|
||||
)
|
||||
def mdel(command, runner) -> None:
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if target not in runner.macro_table:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Macro \"{target}\" is not defined</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
del runner.macro_table[target]
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Delete all macros"
|
||||
)
|
||||
def delmac(command, runner) -> None:
|
||||
confirm = prompt(
|
||||
message = FormattedText([
|
||||
("class:warn", "Are you sure? "),
|
||||
("class:text", "[yes/no]: ")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
).lower()
|
||||
|
||||
if confirm != "yes":
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Cancelled.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.macro_table = {}
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Show macros"
|
||||
)
|
||||
def macros(command, runner) -> None:
|
||||
if len(runner.macro_table) == 0:
|
||||
printf(FormattedText([
|
||||
("class:warn", "No macros are defined."),
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
printf(FormattedText([
|
||||
("class:cmd_h", "\nDefined Macros:\n"),
|
||||
] +
|
||||
[
|
||||
("class:text", f"\t{name} \t {exp}\n")
|
||||
for name, exp in runner.macro_table.items()
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Clear the screen"
|
||||
)
|
||||
def clear(command, runner) -> None:
|
||||
clear_screen()
|
||||
lamb_engine.utils.show_greeting()
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Get or set reduction limit"
|
||||
)
|
||||
def rlimit(command, runner) -> None:
|
||||
if len(command.args) == 0:
|
||||
if runner.reduction_limit is None:
|
||||
printf(
|
||||
HTML(
|
||||
"<ok>No reduction limit is set</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Reduction limit is {runner.reduction_limit:,}</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
elif len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
t = command.args[0]
|
||||
if t.lower() == "none":
|
||||
runner.reduction_limit = None
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Removed reduction limit</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
t = int(t)
|
||||
except ValueError:
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Reduction limit must be a positive integer or \"none\".</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
if 50 > t:
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Reduction limit must be at least 50.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.reduction_limit = t
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Set reduction limit to {t:,}</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Print this help"
|
||||
)
|
||||
def help(command, runner) -> None:
|
||||
printf(
|
||||
HTML(
|
||||
"\n<text>" +
|
||||
|
||||
"<cmd_h>Usage:</cmd_h>" +
|
||||
"\n" +
|
||||
"\tWrite lambda expressions using your <cmd_key>\\</cmd_key> key." +
|
||||
"\n" +
|
||||
"\tMacros can be defined using <cmd_key>=</cmd_key>, as in <code>T = λab.a</code>" +
|
||||
"\n" +
|
||||
"\tRun commands using <cmd_key>:</cmd_key>, for example <code>:help</code>" +
|
||||
"\n" +
|
||||
"\tHistory can be accessed with <cmd_key>$</cmd_key>, which will expand to the result of the last successful reduction." +
|
||||
"\n\n" +
|
||||
"<cmd_h>Commands:</cmd_h>"+
|
||||
"\n" +
|
||||
"\n".join([
|
||||
f"\t<code>{name}</code> \t {text}"
|
||||
for name, text in help_texts.items()
|
||||
]) +
|
||||
"\n\n"
|
||||
"<muted>Detailed documentation can be found on this project's git page.</muted>" +
|
||||
"</text>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
42
lamb_engine/runner/misc.py
Normal file
42
lamb_engine/runner/misc.py
Normal file
@ -0,0 +1,42 @@
|
||||
import enum
|
||||
import lamb_engine
|
||||
|
||||
class StopReason(enum.Enum):
|
||||
BETA_NORMAL = ("class:text", "β-normal form")
|
||||
LOOP_DETECTED = ("class:warn", "Loop detected")
|
||||
MAX_EXCEEDED = ("class:err", "Too many reductions")
|
||||
INTERRUPT = ("class:warn", "User interrupt")
|
||||
SHOW_MACRO = ("class:text", "Displaying macro content")
|
||||
|
||||
class MacroDef:
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return MacroDef(
|
||||
result[0].name,
|
||||
result[1]
|
||||
)
|
||||
|
||||
def __init__(self, label: str, expr: lamb_engine.nodes.Node):
|
||||
self.label = label
|
||||
self.expr = expr
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.label} := {self.expr!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} := {self.expr}"
|
||||
|
||||
def set_runner(self, runner):
|
||||
return self.expr.set_runner(runner)
|
||||
|
||||
class Command:
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return Command(
|
||||
result[0],
|
||||
result[1:]
|
||||
)
|
||||
|
||||
def __init__(self, name, args):
|
||||
self.name = name
|
||||
self.args = args
|
303
lamb_engine/runner/runner.py
Normal file
303
lamb_engine/runner/runner.py
Normal file
@ -0,0 +1,303 @@
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
import collections
|
||||
import math
|
||||
import time
|
||||
|
||||
import lamb_engine
|
||||
|
||||
from lamb_engine.runner.misc import MacroDef
|
||||
from lamb_engine.runner.misc import Command
|
||||
from lamb_engine.runner.misc import StopReason
|
||||
from lamb_engine.runner import commands as cmd
|
||||
|
||||
|
||||
# Keybindings for step prompt.
|
||||
# Prevents any text from being input.
|
||||
step_bindings = KeyBindings()
|
||||
@step_bindings.add("<any>")
|
||||
def _(event):
|
||||
pass
|
||||
|
||||
|
||||
class Runner:
|
||||
def __init__(
|
||||
self,
|
||||
prompt_session: PromptSession,
|
||||
prompt_message
|
||||
):
|
||||
self.macro_table = {}
|
||||
self.prompt_session = prompt_session
|
||||
self.prompt_message = prompt_message
|
||||
self.parser = lamb_engine.parser.LambdaParser(
|
||||
action_func = lamb_engine.nodes.Func.from_parse,
|
||||
action_bound = lamb_engine.nodes.Macro.from_parse,
|
||||
action_macro = lamb_engine.nodes.Macro.from_parse,
|
||||
action_call = lamb_engine.nodes.Call.from_parse,
|
||||
action_church = lamb_engine.nodes.Church.from_parse,
|
||||
action_macro_def = MacroDef.from_parse,
|
||||
action_command = Command.from_parse,
|
||||
action_history = lamb_engine.nodes.History.from_parse
|
||||
)
|
||||
|
||||
# Maximum amount of reductions.
|
||||
# If None, no maximum is enforced.
|
||||
# Must be at least 1.
|
||||
self.reduction_limit = 1_000_000
|
||||
|
||||
# Ensure bound variables are unique.
|
||||
# This is automatically incremented whenever we make
|
||||
# a bound variable.
|
||||
self.bound_variable_counter = 0
|
||||
|
||||
# Update iteration after this many iterations
|
||||
# Make sure every place value has a non-zero digit
|
||||
# so that all digits appear to be changing.
|
||||
self.iter_update = 231
|
||||
|
||||
self.history = collections.deque(
|
||||
[None] * 10,
|
||||
10)
|
||||
|
||||
|
||||
# If true, reduce step-by-step.
|
||||
self.step_reduction = False
|
||||
|
||||
# If true, expand ALL macros when printing output
|
||||
self.full_expansion = False
|
||||
|
||||
def prompt(self):
|
||||
return self.prompt_session.prompt(
|
||||
message = self.prompt_message
|
||||
)
|
||||
|
||||
def parse(self, line): # -> tuple[lamb_engine.nodes.Root | MacroDef | Command, list]
|
||||
e = self.parser.parse_line(line)
|
||||
|
||||
w = []
|
||||
if isinstance(e, MacroDef):
|
||||
e.expr = lamb_engine.nodes.Root(e.expr)
|
||||
e.set_runner(self)
|
||||
w = lamb_engine.nodes.prepare(e.expr, ban_macro_name = e.label)
|
||||
elif isinstance(e, lamb_engine.nodes.Node):
|
||||
e = lamb_engine.nodes.Root(e)
|
||||
e.set_runner(self)
|
||||
w = lamb_engine.nodes.prepare(e)
|
||||
|
||||
return e, w
|
||||
|
||||
|
||||
def reduce(self, node: lamb_engine.nodes.Root, *, warnings = []) -> None:
|
||||
|
||||
# Reduction Counter.
|
||||
# We also count macro (and church) expansions,
|
||||
# and subtract those from the final count.
|
||||
k = 0
|
||||
macro_expansions = 0
|
||||
|
||||
stop_reason = StopReason.MAX_EXCEEDED
|
||||
start_time = time.time()
|
||||
out_text = []
|
||||
|
||||
only_macro = (
|
||||
isinstance(node.left, lamb_engine.nodes.Macro) or
|
||||
isinstance(node.left, lamb_engine.nodes.Church)
|
||||
)
|
||||
if only_macro:
|
||||
stop_reason = StopReason.SHOW_MACRO
|
||||
m, node = lamb_engine.nodes.expand(node, force_all = only_macro)
|
||||
macro_expansions += m
|
||||
|
||||
if len(warnings) != 0:
|
||||
printf(FormattedText(warnings), style = lamb_engine.utils.style)
|
||||
|
||||
if self.step_reduction:
|
||||
printf(FormattedText([
|
||||
("class:warn", "Step-by-step reduction is enabled.\n"),
|
||||
("class:muted", "Press "),
|
||||
("class:cmd_key", "ctrl-c"),
|
||||
("class:muted", " to continue automatically.\n"),
|
||||
("class:muted", "Press "),
|
||||
("class:cmd_key", "enter"),
|
||||
("class:muted", " to step.\n"),
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
|
||||
skip_to_end = False
|
||||
try:
|
||||
while (
|
||||
(
|
||||
(self.reduction_limit is None) or
|
||||
(k < self.reduction_limit)
|
||||
) and not only_macro
|
||||
):
|
||||
|
||||
# Show reduction count
|
||||
if (
|
||||
( (k >= self.iter_update) and (k % self.iter_update == 0) )
|
||||
and not (self.step_reduction and not skip_to_end)
|
||||
):
|
||||
print(f" Reducing... {k:,}", end = "\r")
|
||||
|
||||
# Reduce
|
||||
red_type, node = lamb_engine.nodes.reduce(node)
|
||||
|
||||
# If we can't reduce this expression anymore,
|
||||
# it's in beta-normal form.
|
||||
if red_type == lamb_engine.nodes.ReductionType.NOTHING:
|
||||
stop_reason = StopReason.BETA_NORMAL
|
||||
break
|
||||
|
||||
# Count reductions
|
||||
k += 1
|
||||
if red_type == lamb_engine.nodes.ReductionType.FUNCTION_APPLY:
|
||||
macro_expansions += 1
|
||||
|
||||
# Pause after step if necessary
|
||||
if self.step_reduction and not skip_to_end:
|
||||
try:
|
||||
s = prompt(
|
||||
message = FormattedText([
|
||||
("class:prompt", lamb_engine.nodes.reduction_text[red_type]),
|
||||
("class:prompt", f":{k:03} ")
|
||||
] + lamb_engine.utils.lex_str(str(node))),
|
||||
style = lamb_engine.utils.style,
|
||||
key_bindings = step_bindings
|
||||
)
|
||||
except KeyboardInterrupt or EOFError:
|
||||
skip_to_end = True
|
||||
printf(FormattedText([
|
||||
("class:warn", "Skipping to end."),
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
# Gracefully catch keyboard interrupts
|
||||
except KeyboardInterrupt:
|
||||
stop_reason = StopReason.INTERRUPT
|
||||
|
||||
# Print a space between step messages
|
||||
if self.step_reduction:
|
||||
print("")
|
||||
|
||||
# Clear reduction counter if it was printed
|
||||
if k >= self.iter_update:
|
||||
print(" " * round(14 + math.log10(k)), end = "\r")
|
||||
|
||||
# Expand fully if necessary
|
||||
if self.full_expansion:
|
||||
o, node = lamb_engine.nodes.expand(node, force_all = True)
|
||||
macro_expansions += o
|
||||
|
||||
if only_macro:
|
||||
out_text += [
|
||||
("class:ok", f"Displaying macro content")
|
||||
]
|
||||
|
||||
else:
|
||||
if not self.step_reduction:
|
||||
out_text += [
|
||||
("class:ok", f"Runtime: "),
|
||||
("class:text", f"{time.time() - start_time:.03f} seconds"),
|
||||
("class:text", "\n")
|
||||
]
|
||||
|
||||
out_text += [
|
||||
("class:ok", f"Exit reason: "),
|
||||
stop_reason.value,
|
||||
("class:text", "\n"),
|
||||
|
||||
("class:ok", f"Macro expansions: "),
|
||||
("class:text", f"{macro_expansions:,}"),
|
||||
("class:text", "\n"),
|
||||
|
||||
("class:ok", f"Reductions: "),
|
||||
("class:text", f"{k:,}\t"),
|
||||
("class:muted", f"(Limit: {self.reduction_limit:,})")
|
||||
]
|
||||
|
||||
if self.full_expansion:
|
||||
out_text += [
|
||||
("class:text", "\n"),
|
||||
("class:ok", "All macros have been expanded")
|
||||
]
|
||||
|
||||
if (
|
||||
stop_reason == StopReason.BETA_NORMAL or
|
||||
stop_reason == StopReason.LOOP_DETECTED or
|
||||
only_macro
|
||||
):
|
||||
out_text += [
|
||||
("class:ok", "\n\n => ")
|
||||
] + lamb_engine.utils.lex_str(str(node))
|
||||
|
||||
|
||||
printf(
|
||||
FormattedText(out_text),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
# Save to history
|
||||
# Do this at the end so we don't always fully expand.
|
||||
self.history.appendleft(
|
||||
lamb_engine.nodes.expand( # type: ignore
|
||||
node,
|
||||
force_all = True
|
||||
)[1]
|
||||
)
|
||||
|
||||
def save_macro(
|
||||
self,
|
||||
macro: MacroDef,
|
||||
*,
|
||||
silent = False
|
||||
) -> None:
|
||||
was_rewritten = macro.label in self.macro_table
|
||||
self.macro_table[macro.label] = macro.expr
|
||||
|
||||
if not silent:
|
||||
printf(FormattedText([
|
||||
("class:text", "Set "),
|
||||
("class:code", macro.label),
|
||||
("class:text", " to "),
|
||||
("class:code", str(macro.expr))
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
# Apply a list of definitions
|
||||
def run(
|
||||
self,
|
||||
line: str,
|
||||
*,
|
||||
silent = False
|
||||
) -> None:
|
||||
e, w = self.parse(line)
|
||||
|
||||
# If this line is a macro definition, save the macro.
|
||||
if isinstance(e, MacroDef):
|
||||
self.save_macro(e, silent = silent)
|
||||
|
||||
# If this line is a command, do the command.
|
||||
elif isinstance(e, Command):
|
||||
if e.name not in cmd.commands:
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Unknown command \"{e.name}\"")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
cmd.commands[e.name](e, self)
|
||||
|
||||
# If this line is a plain expression, reduce it.
|
||||
elif isinstance(e, lamb_engine.nodes.Node):
|
||||
self.reduce(e, warnings = w)
|
||||
|
||||
# We shouldn't ever get here.
|
||||
else:
|
||||
raise TypeError(f"I don't know what to do with a {type(e)}")
|
||||
|
||||
|
||||
def run_lines(self, lines: list[str]):
|
||||
for l in lines:
|
||||
self.run(l, silent = True)
|
165
lamb_engine/utils.py
Normal file
165
lamb_engine/utils.py
Normal file
@ -0,0 +1,165 @@
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.lexers import Lexer
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from importlib.metadata import version
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
import re
|
||||
|
||||
|
||||
style = Style.from_dict({ # type: ignore
|
||||
# Basic formatting
|
||||
"text": "#FFFFFF",
|
||||
"warn": "#FFA700",
|
||||
"err": "#FF3809",
|
||||
"prompt": "#05CFFF",
|
||||
"ok": "#00EF7C",
|
||||
"code": "#AAAAAA italic",
|
||||
"muted": "#AAAAAA",
|
||||
|
||||
# Syntax highlighting colors
|
||||
"syn_cmd": "#FFFFFF italic",
|
||||
"syn_lambda": "#AAAAAA",
|
||||
"syn_paren": "#AAAAAA",
|
||||
|
||||
# Command formatting
|
||||
# cmd_h: section titles
|
||||
# cmd_key: keyboard keys, usually one character
|
||||
"cmd_h": "#FF3809 bold",
|
||||
"cmd_key": "#00EF7C bold",
|
||||
|
||||
# Only used in greeting
|
||||
"_v": "#00EF7C bold",
|
||||
"_l": "#FF3809 bold",
|
||||
"_s": "#00EF7C bold",
|
||||
"_p": "#AAAAAA"
|
||||
})
|
||||
|
||||
|
||||
# Replace "\" with pretty "λ"s
|
||||
bindings = KeyBindings()
|
||||
@bindings.add("\\")
|
||||
def _(event):
|
||||
event.current_buffer.insert_text("λ")
|
||||
|
||||
# Simple lexer for highlighting.
|
||||
# Improve this later.
|
||||
class LambdaLexer(Lexer):
|
||||
def lex_document(self, document):
|
||||
def inner(line_no):
|
||||
out = []
|
||||
tmp_str = []
|
||||
d = str(document.lines[line_no])
|
||||
|
||||
if d.startswith(":"):
|
||||
return [
|
||||
("class:syn_cmd", d)
|
||||
]
|
||||
|
||||
for c in d:
|
||||
if c in "\\λ.":
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
out.append(("class:syn_lambda", c))
|
||||
tmp_str = []
|
||||
elif c in "()":
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
out.append(("class:syn_paren", c))
|
||||
tmp_str = []
|
||||
else:
|
||||
tmp_str.append(c)
|
||||
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
return out
|
||||
return inner
|
||||
|
||||
|
||||
|
||||
def lex_str(s: str) -> list[tuple[str, str]]:
|
||||
return LambdaLexer().lex_document(Document(s))(0)
|
||||
|
||||
def show_greeting():
|
||||
# | _.._ _.|_
|
||||
# |_(_|| | ||_)
|
||||
# 0.0.0
|
||||
#
|
||||
# __ __
|
||||
# ,-` `` `,
|
||||
# (` \ )
|
||||
# (` \ `)
|
||||
# (, / \ _)
|
||||
# (` / \ )
|
||||
# `'._.--._.'
|
||||
#
|
||||
# A λ calculus engine
|
||||
|
||||
printf(HTML("\n".join([
|
||||
"",
|
||||
"<_h> | _.._ _.|_",
|
||||
" |_(_|| | ||_)</_h>",
|
||||
f" <_v>{version('lamb_engine')}</_v>",
|
||||
" __ __",
|
||||
" ,-` `` `,",
|
||||
" (` <_l>\\</_l> )",
|
||||
" (` <_l>\\</_l> `)",
|
||||
" (, <_l>/ \\</_l> _)",
|
||||
" (` <_l>/ \\</_l> )",
|
||||
" `'._.--._.'",
|
||||
"",
|
||||
"<_s> A λ calculus engine</_s>",
|
||||
"<_p> Type :help for help</_p>",
|
||||
""
|
||||
])), style = style)
|
||||
|
||||
def remove_sub(s: str):
|
||||
return re.sub("[₀₁₂₃₄₅₆₈₉]*", "", s)
|
||||
|
||||
def base4(n: int):
|
||||
if n == 0:
|
||||
return [0]
|
||||
digits = []
|
||||
while n:
|
||||
digits.append(n % 4)
|
||||
n //= 4
|
||||
return digits[::-1]
|
||||
|
||||
def subscript(num: int):
|
||||
|
||||
# unicode subscripts ₀₁₂₃ and ₄₅₆₈₉
|
||||
# usually look different,
|
||||
# so we'll use base 4.
|
||||
qb = base4(num)
|
||||
|
||||
sub = {
|
||||
"0": "₀",
|
||||
"1": "₁",
|
||||
"2": "₂",
|
||||
"3": "₃",
|
||||
"4": "₄",
|
||||
"5": "₅",
|
||||
"6": "₆",
|
||||
"7": "₇",
|
||||
"8": "₈",
|
||||
"9": "₉"
|
||||
}
|
||||
|
||||
sup = {
|
||||
"0": "⁰",
|
||||
"1": "¹",
|
||||
"2": "²",
|
||||
"3": "³",
|
||||
"4": "⁴",
|
||||
"5": "⁵",
|
||||
"6": "⁶",
|
||||
"7": "⁷",
|
||||
"8": "⁸",
|
||||
"9": "⁹"
|
||||
}
|
||||
|
||||
return "".join(
|
||||
[sub[str(x)] for x in qb]
|
||||
)
|
77
macros.lamb
Normal file
77
macros.lamb
Normal file
@ -0,0 +1,77 @@
|
||||
# How to use exported files in lamb:
|
||||
#
|
||||
# [Syntax Highlighting]
|
||||
# Most languages' syntax highlighters will
|
||||
# highlight this code well. Set it manually
|
||||
# in your editor.
|
||||
#
|
||||
# Don't use a language for which you have a
|
||||
# linter installed, you'll get lots of errors.
|
||||
#
|
||||
# Choose a language you don't have extenstions for,
|
||||
# and a language that uses # comments.
|
||||
#
|
||||
# The following worked well in vscode:
|
||||
# - Julia
|
||||
# - Perl
|
||||
# - Coffeescript
|
||||
# - R
|
||||
|
||||
# [Writing macros]
|
||||
# If you don't have a custom keyboard layout that can
|
||||
# create λs, you may use backslashes instead.
|
||||
# (As in `T = \ab.b`)
|
||||
#
|
||||
# This file must only contain macro definitons. Commands will be ignored.
|
||||
# Statements CANNOT be split among multiple lines.
|
||||
# Comments CANNOT be on the same line as macro defintions.
|
||||
# All leading whitespace is ignored.
|
||||
|
||||
|
||||
# Misc Combinators
|
||||
M = λx.(x x)
|
||||
W = (M M)
|
||||
Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )
|
||||
|
||||
|
||||
# Booleans
|
||||
T = λab.a
|
||||
F = λab.b
|
||||
NOT = λa.(a F T)
|
||||
AND = λab.(a b F)
|
||||
OR = λab.(a T b)
|
||||
XOR = λab.((a (NOT b)) b)
|
||||
|
||||
|
||||
# Numbers
|
||||
# PAIR: prerequisite for H.
|
||||
# Makes a two-value tuple, indexed with T and F.
|
||||
#
|
||||
# H: shift-and-add, prerequisite for D
|
||||
#
|
||||
# S: successor (adds 1)
|
||||
#
|
||||
# D: predecessor (subtracts 1)
|
||||
#
|
||||
# Z: tests if a number is zero
|
||||
# NZ: equivalent to `NOT Z`
|
||||
#
|
||||
# ADD: adds two numbers
|
||||
#
|
||||
# MULT: multiply two numbers
|
||||
#
|
||||
# FAC:
|
||||
# Recursive factorial. Call with `Y FAC <number>`
|
||||
# Don't call this with numbers bigger than 5 unless you're very patient.
|
||||
#
|
||||
# `Y FAC 6` required 867,920 reductions and took 10 minutes to run.
|
||||
|
||||
PAIR = λabi.(i a b)
|
||||
S = λnfa.(f (n f a))
|
||||
H = λp.((PAIR (p F)) (S (p F)))
|
||||
D = λn.(n H (PAIR 0 0) T)
|
||||
Z = λn.(n (λa.F) T)
|
||||
NZ = λn.(n (λa.T) F)
|
||||
ADD = λmn.(m S n)
|
||||
MULT = λnmf.(n (m f))
|
||||
FAC = λyn.(Z n) (1) (MULT n (y (D n)))
|
87
main.py
87
main.py
@ -1,87 +0,0 @@
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.completion import WordCompleter
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.formatted_text import to_plain_text
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
|
||||
from pyparsing import exceptions as ppx
|
||||
|
||||
from parser import Parser
|
||||
from runner import Runner
|
||||
import tokens
|
||||
import greeting
|
||||
|
||||
|
||||
# Replace "\" with a pretty "λ" in the prompt
|
||||
bindings = KeyBindings()
|
||||
@bindings.add("\\")
|
||||
def _(event):
|
||||
event.current_buffer.insert_text("λ")
|
||||
|
||||
session = PromptSession(
|
||||
message = FormattedText([
|
||||
("#00FFFF", "~~> ")
|
||||
]),
|
||||
key_bindings = bindings
|
||||
)
|
||||
|
||||
|
||||
greeting.show()
|
||||
|
||||
|
||||
|
||||
|
||||
r = Runner()
|
||||
|
||||
r.run_lines([
|
||||
"T = λa.λb.a",
|
||||
"F = λa.λb.b",
|
||||
"NOT = \\a.(a F T)",
|
||||
#"AND = a -> b -> (a F b)",
|
||||
#"OR = a -> b -> (a T b)",
|
||||
#"XOR = a -> b -> (a (NOT a b) b)",
|
||||
#"w = x -> (x x)",
|
||||
#"W = (w w)",
|
||||
#"Y = f -> ( (x -> (f (x x))) (x -> (f (x x))) )",
|
||||
#"l = if_true -> if_false -> which -> ( which if_true if_false )"
|
||||
#"inc = n -> f -> x -> (f (n f x))",
|
||||
#"zero = a -> x -> x",
|
||||
#"one = f -> x -> (f x)",
|
||||
])
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
i = session.prompt()
|
||||
|
||||
# Catch Ctrl-C and Ctrl-D
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
break
|
||||
except EOFError:
|
||||
print("")
|
||||
break
|
||||
|
||||
if i.strip() == "":
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
x = r.run(i)
|
||||
except ppx.ParseException as e:
|
||||
l = len(to_plain_text(session.message))
|
||||
print_formatted_text(FormattedText([
|
||||
("#FF0000", " "*(e.loc + l) + "^\n"),
|
||||
("#FF0000", f"Syntax error at char {e.loc}."),
|
||||
("#FFFFFF", "\n")
|
||||
]))
|
||||
continue
|
||||
|
||||
|
||||
print_formatted_text(FormattedText([
|
||||
("#00FF00", " = "),
|
||||
("#FFFFFF", str(x))
|
||||
]))
|
||||
|
||||
print("")
|
84
misc/demo.tape
Normal file
84
misc/demo.tape
Normal file
@ -0,0 +1,84 @@
|
||||
# See makedemo.sh
|
||||
|
||||
#Output lambdemo.mp4
|
||||
Output lambdemo.gif
|
||||
|
||||
Set FontSize 30
|
||||
Set Width 2000
|
||||
Set Height 1500
|
||||
Set FontFamily "FantasqueSansMono NF"
|
||||
Set TypingSpeed 60ms
|
||||
Set Framerate 30
|
||||
|
||||
# Intro
|
||||
Sleep 2000ms
|
||||
Type "lamb"
|
||||
Sleep 1000ms
|
||||
Enter
|
||||
Sleep 2000ms
|
||||
|
||||
# Demo 1: load
|
||||
Type ":load ../macros.lamb"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 2000ms
|
||||
|
||||
Type "NOT T"
|
||||
Sleep 1000ms
|
||||
Enter
|
||||
Sleep 6s
|
||||
|
||||
Type ":clear"
|
||||
Sleep 1000ms
|
||||
Enter
|
||||
Sleep 1500ms
|
||||
|
||||
|
||||
# Demo 2: stepping
|
||||
Type ":step"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 1500ms
|
||||
|
||||
Type "NOT T"
|
||||
Sleep 100ms
|
||||
Enter
|
||||
Sleep 1500ms
|
||||
Enter
|
||||
Sleep 760ms
|
||||
Enter
|
||||
Sleep 850ms
|
||||
Enter
|
||||
Sleep 650ms
|
||||
Enter
|
||||
Sleep 700ms
|
||||
Enter
|
||||
|
||||
Sleep 3000ms
|
||||
|
||||
Type ":step"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 6s
|
||||
Type ":clear"
|
||||
Sleep 1000ms
|
||||
Enter
|
||||
Sleep 1500ms
|
||||
|
||||
# Demo 3: macros
|
||||
Type "M = \x.x x"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 500ms
|
||||
|
||||
Type "M M"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 3s
|
||||
Ctrl+c
|
||||
Sleep 1000ms
|
||||
|
||||
Type "Y FAC 3"
|
||||
Sleep 500ms
|
||||
Enter
|
||||
Sleep 6s
|
132
misc/lamb.svg
Normal file
132
misc/lamb.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 30 KiB |
34
misc/makedemo.sh
Executable file
34
misc/makedemo.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# Should be run from the misc directory.
|
||||
# Will not work with any other root.
|
||||
|
||||
# Create this file.
|
||||
# Should define two variables:
|
||||
# DAV_USER="name:password"
|
||||
# DAV_URL="https://site.com/dav-path"
|
||||
|
||||
if [[ -f "secrets.sh" ]]; then
|
||||
source secrets.sh
|
||||
else
|
||||
echo "Cannot run without secrets.sh"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Activate venv if not in venv
|
||||
if [[ "$VIRTUAL_ENV" == "" ]]; then
|
||||
source ../venv/bin/activate
|
||||
fi
|
||||
|
||||
# Make sure our venv is running the latest
|
||||
# version of lamb.
|
||||
pip install --editable ..
|
||||
|
||||
|
||||
# Make gif
|
||||
vhs < demo.tape
|
||||
|
||||
# Upload
|
||||
curl \
|
||||
--user $DAV_USER \
|
||||
--url $DAV_URL \
|
||||
--upload-file "lambdemo.gif"
|
69
parser.py
69
parser.py
@ -1,69 +0,0 @@
|
||||
import pyparsing as pp
|
||||
import tokens
|
||||
|
||||
class Parser:
|
||||
"""
|
||||
Macro_def must be on its own line.
|
||||
macro_def :: var = expr
|
||||
|
||||
var :: word
|
||||
lambda_fun :: var -> expr
|
||||
call :: '(' (var | expr) ')' +
|
||||
expr :: define | var | call | '(' expr ')'
|
||||
"""
|
||||
|
||||
lp = pp.Suppress("(")
|
||||
rp = pp.Suppress(")")
|
||||
|
||||
# Simple tokens
|
||||
pp_expr = pp.Forward()
|
||||
pp_macro = pp.Word(pp.alphas + "_")
|
||||
pp_macro.set_parse_action(tokens.macro.from_parse)
|
||||
|
||||
# Function definitions.
|
||||
# Right associative.
|
||||
#
|
||||
# <var> => <exp>
|
||||
pp_lambda_fun = (pp.Suppress("λ") | pp.Suppress("\\")) + pp_macro + pp.Suppress(".") + pp_expr
|
||||
pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse)
|
||||
|
||||
# Assignment.
|
||||
# Can only be found at the start of a line.
|
||||
#
|
||||
# <var> = <exp>
|
||||
pp_macro_def = pp.line_start() + pp_macro + pp.Suppress("=") + pp_expr
|
||||
pp_macro_def.set_parse_action(tokens.macro_expression.from_parse)
|
||||
|
||||
# Function calls.
|
||||
# `tokens.lambda_func.from_parse` handles chained calls.
|
||||
#
|
||||
# <var>(<exp>)
|
||||
# <var>(<exp>)(<exp>)(<exp>)
|
||||
# (<exp>)(<exp>)
|
||||
# (<exp>)(<exp>)(<exp>)(<exp>)
|
||||
pp_call = pp.Forward()
|
||||
pp_call <<= pp_expr[2, ...]
|
||||
pp_call.set_parse_action(tokens.lambda_apply.from_parse)
|
||||
|
||||
pp_expr <<= pp_lambda_fun ^ (lp + pp_expr + rp) ^ pp_macro ^ (lp + pp_call + rp)
|
||||
pp_all = pp_expr | pp_macro_def
|
||||
|
||||
pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_")
|
||||
pp_command.set_parse_action(tokens.command.from_parse)
|
||||
|
||||
@staticmethod
|
||||
def parse_line(line):
|
||||
k = (
|
||||
Parser.pp_expr ^
|
||||
Parser.pp_macro_def ^
|
||||
Parser.pp_command ^ Parser.pp_call
|
||||
).parse_string(
|
||||
line,
|
||||
parse_all = True
|
||||
)[0]
|
||||
print(k)
|
||||
return k
|
||||
|
||||
@staticmethod
|
||||
def run_tests(lines):
|
||||
return Parser.pp_macro_def.run_tests(lines)
|
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[build-system]
|
||||
requires = [ "setuptools>=61.0" ]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = [ "." ]
|
||||
include = ["lamb_engine*"]
|
||||
namespaces = false
|
||||
|
||||
[project.scripts]
|
||||
lamb = "lamb_engine:main"
|
||||
|
||||
|
||||
|
||||
[project]
|
||||
name = "lamb_engine"
|
||||
description = "A lambda calculus engine"
|
||||
version = "1.1.9"
|
||||
dependencies = [
|
||||
"prompt-toolkit==3.0.31",
|
||||
"pyparsing==3.0.9"
|
||||
]
|
||||
authors = [
|
||||
{ name="Mark", email="mark@betalupi.com" }
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
license = {text = "GNU General Public License v3 (GPLv3)"}
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Environment :: Console"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://git.betalupi.com/Mark/lamb"
|
||||
|
||||
# To build:
|
||||
# pip install build twine
|
||||
# python -m build
|
||||
# twine upload dist/lamb_engine-1.1.6* (change the version)
|
50
runner.py
50
runner.py
@ -1,50 +0,0 @@
|
||||
import tokens
|
||||
from parser import Parser
|
||||
|
||||
class Runner:
|
||||
def __init__(self):
|
||||
self.macro_table = {}
|
||||
self.expr = None
|
||||
|
||||
def exec_command(self, command: str):
|
||||
if command == "help":
|
||||
print("This is a help message.")
|
||||
|
||||
# Apply a list of definitions
|
||||
def run(self, line: str):
|
||||
e = Parser.parse_line(line)
|
||||
|
||||
if isinstance(e, tokens.macro_expression):
|
||||
if e.label in self.macro_table:
|
||||
raise NameError(f"Label {e.label} exists!")
|
||||
e.exp.bind_variables()
|
||||
self.macro_table[e.label] = e.exp
|
||||
|
||||
elif isinstance(e, tokens.command):
|
||||
self.exec_command(e.name)
|
||||
else:
|
||||
e.bind_variables()
|
||||
self.expr = e
|
||||
|
||||
outs = [str(e)]
|
||||
for i in range(300):
|
||||
r = self.expr.reduce(self.macro_table)
|
||||
self.expr = r.output
|
||||
s = str(r.output)
|
||||
p = s if len(s) < 100 else s[:97] + "..."
|
||||
|
||||
#if s in outs:
|
||||
#print(p)
|
||||
#print("\nLoop detected, exiting.")
|
||||
#break
|
||||
|
||||
if not r.was_reduced:
|
||||
print("\nCannot evaluate any further.")
|
||||
break
|
||||
|
||||
print(f"Performed {i} {'operations' if i != 1 else 'operation'}.")
|
||||
return self.expr
|
||||
|
||||
def run_lines(self, lines):
|
||||
for l in lines:
|
||||
self.run(l)
|
484
tokens.py
484
tokens.py
@ -1,484 +0,0 @@
|
||||
from ast import Lambda
|
||||
import enum
|
||||
|
||||
class ReductionType(enum.Enum):
|
||||
MACRO_EXPAND = enum.auto()
|
||||
MACRO_TO_FREE = enum.auto()
|
||||
FUNCTION_APPLY = enum.auto()
|
||||
|
||||
|
||||
class ReductionStatus:
|
||||
"""
|
||||
This object helps organize reduction output.
|
||||
An instance is returned after every reduction step.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
output,
|
||||
was_reduced: bool,
|
||||
reduction_type: ReductionType | None = None
|
||||
):
|
||||
# The new expression
|
||||
self.output = output
|
||||
|
||||
# What did we do?
|
||||
# Will be None if was_reduced is false.
|
||||
self.reduction_type = reduction_type
|
||||
|
||||
# Did this reduction change anything?
|
||||
# If we try to reduce an irreducible expression,
|
||||
# this will be false.
|
||||
self.was_reduced = was_reduced
|
||||
|
||||
|
||||
class LambdaToken:
|
||||
"""
|
||||
Base class for all lambda tokens.
|
||||
"""
|
||||
|
||||
def bind_variables(self) -> None:
|
||||
pass
|
||||
|
||||
def reduce(self, macro_table) -> ReductionStatus:
|
||||
return ReductionStatus(
|
||||
was_reduced = False,
|
||||
output = self
|
||||
)
|
||||
|
||||
class free_variable(LambdaToken):
|
||||
"""
|
||||
Represents a free variable.
|
||||
|
||||
This object does not reduce to
|
||||
anything, since it has no meaning.
|
||||
|
||||
Any name in an expression that isn't
|
||||
a macro or a bound variable is assumed
|
||||
to be a free variable.
|
||||
"""
|
||||
|
||||
def __init__(self, label: str):
|
||||
self.label = label
|
||||
|
||||
def __repr__(self):
|
||||
return f"<freevar {self.label}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label}"
|
||||
|
||||
class command:
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return command(
|
||||
result[0],
|
||||
)
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
class macro(LambdaToken):
|
||||
"""
|
||||
Represents a "macro" in lambda calculus,
|
||||
a variable that reduces to an expression.
|
||||
|
||||
These don't have any inherent logic, they
|
||||
just make writing and reading expressions
|
||||
easier.
|
||||
|
||||
These are defined as follows:
|
||||
<macro name> = <expression>
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return macro(
|
||||
result[0],
|
||||
)
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
def __repr__(self):
|
||||
return f"<{self.name}>"
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, macro):
|
||||
raise TypeError("Can only compare macro with macro")
|
||||
return self.name == other.name
|
||||
|
||||
def reduce(self, macro_table = {}, *, auto_free_vars = True) -> ReductionStatus:
|
||||
if self.name in macro_table:
|
||||
return ReductionStatus(
|
||||
output = macro_table[self.name],
|
||||
reduction_type = ReductionType.MACRO_EXPAND,
|
||||
was_reduced = True
|
||||
)
|
||||
elif not auto_free_vars:
|
||||
raise NameError(f"Name {self.name} is not defined!")
|
||||
else:
|
||||
return ReductionStatus(
|
||||
output = free_variable(self.name),
|
||||
reduction_type = ReductionType.MACRO_TO_FREE,
|
||||
was_reduced = True
|
||||
)
|
||||
|
||||
class macro_expression:
|
||||
"""
|
||||
Represents a line that looks like
|
||||
<name> = <expression>
|
||||
|
||||
Doesn't do anything particularly interesting,
|
||||
just holds an expression until it is stored
|
||||
in the runner's macro table.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return macro_expression(
|
||||
result[0].name,
|
||||
result[1]
|
||||
)
|
||||
|
||||
def __init__(self, label: str, exp: LambdaToken):
|
||||
self.label = label
|
||||
self.exp = exp
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.label} := {self.exp!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} := {self.exp}"
|
||||
|
||||
|
||||
bound_variable_counter = 0
|
||||
class bound_variable(LambdaToken):
|
||||
def __init__(self, forced_id = None):
|
||||
global bound_variable_counter
|
||||
|
||||
if forced_id is None:
|
||||
self.identifier = bound_variable_counter
|
||||
bound_variable_counter += 1
|
||||
else:
|
||||
self.identifier = forced_id
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, bound_variable):
|
||||
raise TypeError(f"Cannot compare bound_variable with {type(other)}")
|
||||
return self.identifier == other.identifier
|
||||
|
||||
def __repr__(self):
|
||||
return f"<in {self.identifier}>"
|
||||
|
||||
class lambda_func(LambdaToken):
|
||||
"""
|
||||
Represents a function.
|
||||
Defined like λa.aa
|
||||
|
||||
After being created by the parser, a function
|
||||
needs to have its variables bound. This cannot
|
||||
happen during parsing, since the parser creates
|
||||
functions "inside-out," and we need all inner
|
||||
functions before we bind variables.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return lambda_func(
|
||||
result[0],
|
||||
result[1]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_var: macro | bound_variable,
|
||||
output: LambdaToken
|
||||
):
|
||||
self.input: macro | bound_variable = input_var
|
||||
self.output: LambdaToken = output
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.input!r} → {self.output!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"λ{self.input}.{self.output}"
|
||||
|
||||
def bind_variables(
|
||||
self,
|
||||
placeholder: macro | None = None,
|
||||
val: bound_variable | None = None,
|
||||
*,
|
||||
binding_self: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Go through this function and all the functions inside it,
|
||||
and replace the strings generated by the parser with bound
|
||||
variables or free variables.
|
||||
|
||||
If values are passed to `placeholder` and `val,`
|
||||
we're binding the variable of a function containing
|
||||
this one. If they are both none, start the binding
|
||||
chain with this function.
|
||||
|
||||
If only one of those arguments is None, something is very wrong.
|
||||
|
||||
`placeholder` is a macro, NOT A STRING!
|
||||
The parser assumes all names are macros at first, variable
|
||||
binding fixes those that are actually bound variables.
|
||||
|
||||
If `binding_self` is True, don't throw an error on a name conflict
|
||||
and don't bind this function's input variable.
|
||||
This is used when we're calling this method to bind this function's
|
||||
variable.
|
||||
"""
|
||||
|
||||
|
||||
if (placeholder is None) and (val != placeholder):
|
||||
raise Exception(
|
||||
"Error while binding variables: placeholder and val are both None."
|
||||
)
|
||||
|
||||
# We only need to check for collisions if we're
|
||||
# binding another function's variable. If this
|
||||
# function starts the bind chain, skip that step.
|
||||
if not ((placeholder is None) and (val is None)):
|
||||
if not binding_self and isinstance(self.input, macro):
|
||||
if self.input == placeholder:
|
||||
raise NameError("Bound variable name conflict.")
|
||||
|
||||
# If this function's variables haven't been bound yet,
|
||||
# bind them BEFORE binding the outer function's.
|
||||
#
|
||||
# If we bind inner functions' variables before outer
|
||||
# functions' variables, we won't be able to detect
|
||||
# name conflicts.
|
||||
if isinstance(self.input, macro) and not binding_self:
|
||||
new_bound_var = bound_variable()
|
||||
self.bind_variables(
|
||||
self.input,
|
||||
new_bound_var,
|
||||
binding_self = True
|
||||
)
|
||||
self.input = new_bound_var
|
||||
|
||||
|
||||
# Bind variables inside this function.
|
||||
if isinstance(self.output, macro):
|
||||
if self.output == placeholder:
|
||||
self.output = val
|
||||
elif isinstance(self.output, lambda_func):
|
||||
self.output.bind_variables(placeholder, val)
|
||||
elif isinstance(self.output, lambda_apply):
|
||||
self.output.bind_variables(placeholder, val)
|
||||
|
||||
def reduce(self, macro_table = {}) -> ReductionStatus:
|
||||
|
||||
r = self.output.reduce(macro_table)
|
||||
|
||||
# If a macro becomes a free variable,
|
||||
# reduce twice.
|
||||
if r.reduction_type == ReductionType.MACRO_TO_FREE:
|
||||
self.output = r.output
|
||||
return self.reduce(macro_table)
|
||||
|
||||
return ReductionStatus(
|
||||
was_reduced = r.was_reduced,
|
||||
reduction_type = r.reduction_type,
|
||||
output = lambda_func(
|
||||
self.input,
|
||||
r.output
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def apply(
|
||||
self,
|
||||
val,
|
||||
*,
|
||||
bound_var: bound_variable | None = None
|
||||
):
|
||||
"""
|
||||
Substitute `bound_var` into all instances of a bound variable `var`.
|
||||
If `bound_var` is none, use this functions bound variable.
|
||||
Returns a new object.
|
||||
"""
|
||||
|
||||
calling_self = False
|
||||
if bound_var is None:
|
||||
calling_self = True
|
||||
bound_var = self.input
|
||||
new_out = self.output
|
||||
if isinstance(self.output, bound_variable):
|
||||
if self.output == bound_var:
|
||||
new_out = val
|
||||
elif isinstance(self.output, lambda_func):
|
||||
new_out = self.output.apply(val, bound_var = bound_var)
|
||||
elif isinstance(self.output, lambda_apply):
|
||||
new_out = self.output.sub_bound_var(val, bound_var = bound_var)
|
||||
|
||||
# If we're applying THIS function,
|
||||
# just give the output
|
||||
if calling_self:
|
||||
return new_out
|
||||
|
||||
# If we're applying another function,
|
||||
# return this one with substitutions
|
||||
else:
|
||||
return lambda_func(
|
||||
self.input,
|
||||
new_out
|
||||
)
|
||||
|
||||
|
||||
class lambda_apply(LambdaToken):
|
||||
"""
|
||||
Represents a function application.
|
||||
Has two elements: fn, the function,
|
||||
and arg, the thing it acts upon.
|
||||
|
||||
Parentheses are handled by the parser, and
|
||||
chained functions are handled by from_parse.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
if len(result) == 2:
|
||||
return lambda_apply(
|
||||
result[0],
|
||||
result[1]
|
||||
)
|
||||
elif len(result) > 2:
|
||||
return lambda_apply.from_parse([
|
||||
lambda_apply(
|
||||
result[0],
|
||||
result[1]
|
||||
)] + result[2:]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fn: LambdaToken,
|
||||
arg: LambdaToken
|
||||
):
|
||||
self.fn: LambdaToken = fn
|
||||
self.arg: LambdaToken = arg
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.fn!r} | {self.arg!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"({self.fn} {self.arg})"
|
||||
|
||||
def bind_variables(
|
||||
self,
|
||||
placeholder: macro | None = None,
|
||||
val: bound_variable | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Does exactly what lambda_func.bind_variables does,
|
||||
but acts on applications instead.
|
||||
|
||||
There will be little documentation in this method,
|
||||
see lambda_func.bind_variables.
|
||||
"""
|
||||
|
||||
if (placeholder is None) and (val != placeholder):
|
||||
raise Exception(
|
||||
"Error while binding variables: placeholder and val are both None."
|
||||
)
|
||||
|
||||
# If val and placeholder are None,
|
||||
# everything below should still work as expected.
|
||||
if isinstance(self.fn, macro) and placeholder is not None:
|
||||
if self.fn == placeholder:
|
||||
self.fn = val
|
||||
elif isinstance(self.fn, lambda_func):
|
||||
self.fn.bind_variables(placeholder, val)
|
||||
elif isinstance(self.fn, lambda_apply):
|
||||
self.fn.bind_variables(placeholder, val)
|
||||
|
||||
if isinstance(self.arg, macro) and placeholder is not None:
|
||||
if self.arg == placeholder:
|
||||
self.arg = val
|
||||
elif isinstance(self.arg, lambda_func):
|
||||
self.arg.bind_variables(placeholder, val)
|
||||
elif isinstance(self.arg, lambda_apply):
|
||||
self.arg.bind_variables(placeholder, val)
|
||||
|
||||
def sub_bound_var(
|
||||
self,
|
||||
val,
|
||||
*,
|
||||
bound_var: bound_variable
|
||||
):
|
||||
|
||||
new_fn = self.fn
|
||||
if isinstance(self.fn, bound_variable):
|
||||
if self.fn == bound_var:
|
||||
new_fn = val
|
||||
elif isinstance(self.fn, lambda_func):
|
||||
new_fn = self.fn.apply(val, bound_var = bound_var)
|
||||
elif isinstance(self.fn, lambda_apply):
|
||||
new_fn = self.fn.sub_bound_var(val, bound_var = bound_var)
|
||||
|
||||
new_arg = self.arg
|
||||
if isinstance(self.arg, bound_variable):
|
||||
if self.arg == bound_var:
|
||||
new_arg = val
|
||||
elif isinstance(self.arg, lambda_func):
|
||||
new_arg = self.arg.apply(val, bound_var = bound_var)
|
||||
elif isinstance(self.arg, lambda_apply):
|
||||
new_arg = self.arg.sub_bound_var(val, bound_var = bound_var)
|
||||
|
||||
return lambda_apply(
|
||||
new_fn,
|
||||
new_arg
|
||||
)
|
||||
|
||||
def reduce(self, macro_table = {}) -> ReductionStatus:
|
||||
|
||||
# If we can directly apply self.fn, do so.
|
||||
if isinstance(self.fn, lambda_func):
|
||||
return ReductionStatus(
|
||||
was_reduced = True,
|
||||
reduction_type = ReductionType.FUNCTION_APPLY,
|
||||
output = self.fn.apply(self.arg)
|
||||
)
|
||||
|
||||
# Otherwise, try to reduce self.fn.
|
||||
# If that is impossible, try to reduce self.arg.
|
||||
else:
|
||||
r = self.fn.reduce(macro_table)
|
||||
# If a macro becomes a free variable,
|
||||
# reduce twice.
|
||||
if r.reduction_type == ReductionType.MACRO_TO_FREE:
|
||||
self.fn = r.output
|
||||
return self.reduce(macro_table)
|
||||
|
||||
if r.was_reduced:
|
||||
return ReductionStatus(
|
||||
was_reduced = True,
|
||||
reduction_type = r.reduction_type,
|
||||
output = lambda_apply(
|
||||
r.output,
|
||||
self.arg
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
r = self.arg.reduce(macro_table)
|
||||
|
||||
if r.reduction_type == ReductionType.MACRO_TO_FREE:
|
||||
self.arg = r.output
|
||||
return self.reduce(macro_table)
|
||||
|
||||
return ReductionStatus(
|
||||
was_reduced = r.was_reduced,
|
||||
reduction_type = r.reduction_type,
|
||||
output = lambda_apply(
|
||||
self.fn,
|
||||
r.output
|
||||
)
|
||||
)
|
Reference in New Issue
Block a user