Compare commits

..

96 Commits

Author SHA1 Message Date
5bff77e4a7 Merge branch 'master' of https://github.com/rm-dr/lamb 2023-11-27 14:23:00 -08:00
8c8ea69890 Update README.md 2023-11-27 14:21:13 -08:00
6d14333e52 Update README.md 2023-11-27 14:19:24 -08:00
ab9057148a Merge branch 'master' of ssh://git.betalupi.com:33/Mark/lamb 2023-10-17 18:37:38 -07:00
c07edf1b50 Updated README.md 2023-10-16 21:17:27 -07:00
1bb0b91e20 Fixed a link in README 2023-10-16 15:31:49 -07:00
b20604de5c Fixed a path 2023-10-10 22:08:07 -07:00
d2f8b1c8fb Updated version 2023-04-02 20:54:26 -07:00
3022c2ffc0 Removed type hints to support older python versions 2023-04-02 20:52:59 -07:00
acbc247e10 Merge branch 'master' of ssh://git.betalupi.com:33/Mark/lamb 2023-04-02 07:56:55 -07:00
907d2d9e79 Added links to readme 2023-04-02 07:55:37 -07:00
73f4c60c06 Updated install instructions 2023-03-30 20:29:29 -07:00
fe7e6fca13 Added social media banner 2023-01-09 17:36:49 -08:00
866cb64485 Updated links 2023-01-09 17:15:52 -08:00
09a389857a Hide time when reducing by steps 2022-11-12 19:31:12 -08:00
da997b80c7 Added demo script 2022-11-12 19:15:08 -08:00
1b951813f4 Fixed command parser 2022-11-12 19:14:58 -08:00
12d6176f63 Updated README 2022-11-12 18:56:29 -08:00
787dbd9091 Added vhs tape 2022-11-12 18:16:35 -08:00
67b2332e1c Removed screenshot 2022-11-12 18:16:23 -08:00
115b7289e7 Updated README 2022-11-12 18:07:04 -08:00
69bf43f295 Updated README 2022-11-12 18:02:19 -08:00
495c947441 More reliable ctrl-c 2022-11-12 17:47:41 -08:00
b26d968884 Notes 2022-11-11 18:32:40 -08:00
e8bd6997b9 Added support for arguments 2022-11-11 18:31:37 -08:00
97aecb01f0 Fixed build files 2022-11-11 18:19:38 -08:00
45493c1093 Renamed package for pypi 2022-11-11 17:20:59 -08:00
a30bd7b870 Updated screenshot 2022-11-11 17:11:09 -08:00
f3d02721ad Version & README 2022-11-11 17:07:45 -08:00
48e7d405dd Improved history & highlighting 2022-11-11 17:04:05 -08:00
78cc118bfc Added :expand 2022-11-11 15:45:22 -08:00
dfbc5f3704 Added :step and fixed cloning bug 2022-11-11 15:30:21 -08:00
bd13b10f76 Added subscript parsing 2022-11-10 09:08:58 -08:00
0c215a5df4 Fixed print method 2022-11-10 08:02:28 -08:00
39d32a9925 Updated README 2022-11-09 21:54:10 -08:00
89bc4bd2c0 Minor warning cleanup 2022-11-09 21:47:55 -08:00
062a4b4d1b Updated README 2022-11-09 21:44:58 -08:00
4e3d5fa341 Fixed a macro bug 2022-11-09 21:44:32 -08:00
04ce18c891 Updated README 2022-11-07 20:18:33 -08:00
9f467fa223 Cleaned up files 2022-11-07 20:15:04 -08:00
231c873b1c File cleanup 2022-11-07 19:58:11 -08:00
ac08c5be59 Spelling 2022-11-07 19:02:35 -08:00
ab682ef700 Fixed :save & added new commands 2022-11-07 19:02:27 -08:00
cf2d82acc6 README 2022-11-01 21:33:01 -07:00
60823f1922 Color fix 2022-11-01 21:31:18 -07:00
740c562dbe Colors and small fixes 2022-11-01 21:29:39 -07:00
d88a7ff77f README 2022-11-01 20:48:43 -07:00
d3917b1f58 Version 2022-11-01 20:47:01 -07:00
04880b7724 Did the following:
- Added Root node
  - Created node.prepare() method
  - Added "macro show" warning
  - Rewrote TreeWalker & simplified a few methods
2022-11-01 20:46:45 -07:00
ecbb8661ce Name cleanup 2022-10-31 08:27:56 -07:00
73b803a1b0 Minor cleanup 2022-10-31 08:27:28 -07:00
c7464076ff Improved full-reduction logic 2022-10-31 08:20:27 -07:00
33c8d5bb66 Updated README 2022-10-30 22:43:19 -07:00
8d05de58f7 Updated README 2022-10-29 15:45:27 -07:00
01e542f88c Added basic history recall 2022-10-29 15:44:17 -07:00
c58819a7d6 Cleaned up style 2022-10-29 15:43:59 -07:00
09f78a7642 Improved printing 2022-10-29 13:25:37 -07:00
81c78d7921 Added macro full-expansion 2022-10-29 10:25:06 -07:00
f67d1e2730 Updated README 2022-10-28 20:45:39 -07:00
03e742821d Comments 2022-10-28 20:45:26 -07:00
602bf52983 Better number formatting 2022-10-28 20:43:00 -07:00
4847f0a9a4 Minor improvements 2022-10-28 19:48:12 -07:00
fbf2d6f36d Added a new command 2022-10-28 19:47:50 -07:00
6e17963d91 Changed icon 2022-10-28 19:42:32 -07:00
393e52327e Added runtime measurement 2022-10-28 19:05:38 -07:00
6e46f485c1 Added live reduction counter 2022-10-28 18:58:25 -07:00
e41de764e8 Fixed a bug 2022-10-28 17:40:48 -07:00
8871f1430d Fixed a bad macro 2022-10-28 17:40:11 -07:00
affcbc33ee Cleanup 2022-10-28 16:01:58 -07:00
a1d8714f2f Re-integrated a few features 2022-10-28 14:19:29 -07:00
d74922a363 Reorganized nodes 2022-10-28 08:33:52 -07:00
8d1abe2712 Fixed a few final bugs 2022-10-27 21:06:07 -07:00
0dae1afb61 Added tree iterator, cloning 2022-10-27 20:48:03 -07:00
a991c3bb91 Rewrite continues 2022-10-25 13:26:55 -07:00
455e447999 Added variable binding 2022-10-23 11:24:27 -07:00
c5df3fcbed Started conversion to Tree reduction 2022-10-23 08:53:26 -07:00
044ec60a49 Added RecursionError handling and macro name check 2022-10-22 19:16:46 -07:00
123d885adf Rearranged code 2022-10-22 18:53:40 -07:00
239aa210c5 Added a few macros 2022-10-22 18:21:07 -07:00
31ce605674 Changed church handling 2022-10-22 18:20:48 -07:00
6a8d057425 Updated README 2022-10-22 14:38:39 -07:00
657d5b66af Fixed a bug 2022-10-22 13:07:31 -07:00
218af2cd49 Cleaned up parser 2022-10-22 12:59:42 -07:00
3cd0762d16 Prettier help message 2022-10-22 09:54:50 -07:00
fa02c2aa5b Made commands more powerful, added :load and :save 2022-10-22 09:50:04 -07:00
1bbca094dd Added basic lexer 2022-10-22 08:45:27 -07:00
0ef0e8e585 Cleaned up commands and styling 2022-10-22 08:37:19 -07:00
d11c9a5a7e Improved parser 2022-10-22 08:28:05 -07:00
a7078f9a77 Added mdel command 2022-10-22 07:48:17 -07:00
8da3282edf updated README 2022-10-21 21:07:56 -07:00
e1b79cd59a Cleanup & build files 2022-10-21 21:01:06 -07:00
8558c484c5 Fixed parser, added macro printout 2022-10-21 19:55:15 -07:00
ee744b5245 Type fixes 2022-10-21 19:39:45 -07:00
b5d97cf5c6 Added Church numeral generation 2022-10-21 19:39:37 -07:00
7a1077e371 Added error handling 2022-10-21 19:24:47 -07:00
ef19fc42a6 Added support for curry shorthand 2022-10-21 19:13:07 -07:00
29 changed files with 3061 additions and 1114 deletions

11
.gitignore vendored
View File

@ -1,2 +1,13 @@
# Python dev files
venv
__pycache__
# Python build files
*.egg-info
*.spec
build
dist
# Misc
*.gif
misc/secrets.sh

11
.vscode/settings.json vendored
View File

@ -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
View 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>.

130
README.md
View File

@ -1,19 +1,119 @@
# Lamb: A Lambda Calculus Engine
# 🐑 Lamb: A Lambda Calculus Engine
![Lamb Demo](https://github.com/rm-dr/lamb/assets/96270320/d518e344-e7c8-47ed-89c4-7ce273bf4e2d)
## Todo (pre-release):
- Fix parser (call parentheses)
- Good command parsing (`:save`, `:load`, are a bare minimum)
- Python files: installable, package list, etc
- $\alpha$-equivalence check
- Versioning
## :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:
- live syntax check
- Command and macro autocomplete
- step-by-step reduction
- Documentation in README
- Maybe a better icon?
- Warn when overwriting macro
- Syntax highlighting: parenthesis, bound variables, macros, etc
- Pin header to top of screen
- 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?)

View File

@ -1,96 +0,0 @@
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.shortcuts import clear as clear_screen
from runstatus import CommandStatus
import greeting
commands = {}
help_texts = {}
def lamb_command(*, help_text: str):
def inner(func):
commands[func.__name__] = func
help_texts[func.__name__] = help_text
return inner
def run(command, runner):
return commands[command](runner)
@lamb_command(help_text = "Show macros")
def macros(runner):
return CommandStatus(
formatted_text = FormattedText([
("#FF6600 bold", "\nDefined Macros:\n"),
] +
[
("#FFFFFF", f"\t{name} \t {exp}\n")
for name, exp in runner.macro_table.items()
]
)
)
@lamb_command(help_text = "Clear the screen")
def clear(runner):
clear_screen()
greeting.show()
@lamb_command(help_text = "Print this help")
def help(runner):
return CommandStatus(
formatted_text = FormattedText([
("#FF6600 bold", "\nUsage:\n"),
(
"#FFFFFF",
"\tWrite lambda expressions using your "
),
(
"#00FF00",
"\\"
),
(
"#FFFFFF",
" key.\n" +
"\tMacros can be defined using "
),
("#00FF00", "="),
(
"#FFFFFF",
", as in "
),
(
"#AAAAAA bold",
"T = λab.a\n"
),
(
"#FFFFFF",
"\tRun commands using "
),
(
"#00FF00",
":"
),
(
"#FFFFFF",
", for example "
),
(
"#AAAAAA bold",
":help"
),
("#FF6600 bold", "\n\nCommands:\n")
] +
[
("#FFFFFF", f"\t{name} \t {text}\n")
for name, text in help_texts.items()
]
)
)

View File

@ -1,57 +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 as printf
# | _.._ _.|_
# |_(_|| | ||_)
# 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",
# :help message
"_p": "#AAAAAA"
})
html = HTML(f"""
<_h> | _.._ _.|_
|_(_|| | ||_)</_h>
<_v>1.1.0</_v>
__ __
,-` `` `,
(` <_l>\\</_l> )
(` <_l>\\</_l> `)
(, <_l>/ \\</_l> _)
(` <_l>/ \\</_l> )
`'._.--._.'
<_s> A λ calculus engine</_s>
<_p> Type :help for help</_p>
"""[1:-1])
def show():
printf(html, style = style)

8
lamb_engine/__init__.py Normal file
View 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
View 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()

View File

@ -0,0 +1,3 @@
from .misc import *
from .nodes import *
from .functions import *

View 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
View 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
View 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
View 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)

View File

@ -0,0 +1,2 @@
from .runner import Runner
from .runner import StopReason

View 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
)

View 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

View 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
View 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
View 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)))

107
main.py
View File

@ -1,107 +0,0 @@
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
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 prompt_toolkit.key_binding import KeyBindings
from pyparsing import exceptions as ppx
from parser import Parser
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
)
printf("\n")
greeting.show()
r = runner.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:
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(session.message))
printf(FormattedText([
("#FF0000", " "*(e.loc + l) + "^\n"),
("#FF0000", f"Syntax error at char {e.loc}."),
("#FFFFFF", "\n")
]))
continue
# If this line defined a macro, print nothing.
if isinstance(x, runner.MacroStatus):
pass
if isinstance(x, runner.CommandStatus):
printf(x.formatted_text)
# If this line was an expression, print reduction status
elif isinstance(x, runner.ReduceStatus):
printf(FormattedText([
("#00FF00 bold", f"\nExit reason: "),
x.stop_reason.value,
("#00FF00 bold", f"\nReduction count: "),
("#FFFFFF", str(x.reduction_count)),
("#00FF00 bold", "\n\n => "),
("#FFFFFF", str(x.result)),
]))
print("")

84
misc/demo.tape Normal file
View 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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 30 KiB

34
misc/makedemo.sh Executable file
View 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"

View File

@ -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
View 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)

View File

@ -1,99 +0,0 @@
from prompt_toolkit.formatted_text import FormattedText
import tokens
from parser import Parser
import commands
from runstatus import RunStatus
from runstatus import MacroStatus
from runstatus import StopReason
from runstatus import ReduceStatus
from runstatus import CommandStatus
class Runner:
def __init__(self):
self.macro_table = {}
# Maximum amount of reductions.
# If None, no maximum is enforced.
self.reduction_limit: int | None = 300
def exec_command(self, command: str) -> CommandStatus:
if command in commands.commands:
return commands.run(command, self)
# Handle unknown commands
else:
return CommandStatus(
formatted_text = FormattedText([
("#FFFF00", f"Unknown command \"{command}\"")
])
)
def reduce_expression(self, expr: tokens.LambdaToken) -> ReduceStatus:
# Reduction Counter.
# We also count macro expansions,
# and subtract those from the final count.
i = 0
macro_expansions = 0
while i < self.reduction_limit:
r = expr.reduce(self.macro_table)
expr = r.output
# If we can't reduce this expression anymore,
# it's in beta-normal form.
if not r.was_reduced:
return ReduceStatus(
reduction_count = i - macro_expansions,
stop_reason = StopReason.BETA_NORMAL,
result = r.output
)
# Count reductions
i += 1
if r.reduction_type == tokens.ReductionType.MACRO_EXPAND:
macro_expansions += 1
return ReduceStatus(
reduction_count = i - macro_expansions,
stop_reason = StopReason.MAX_EXCEEDED,
result = r.output
)
# Apply a list of definitions
def run(self, line: str) -> RunStatus:
e = Parser.parse_line(line)
# If this line is a macro definition, save the macro.
if isinstance(e, tokens.macro_expression):
was_rewritten = e.label in self.macro_table
e.exp.bind_variables()
self.macro_table[e.label] = e.exp
return MacroStatus(
was_rewritten = was_rewritten,
macro_label = e.label
)
# If this line is a command, do the command.
elif isinstance(e, tokens.command):
return self.exec_command(e.name)
# If this line is a plain expression, reduce it.
else:
e.bind_variables()
return self.reduce_expression(e)
def run_lines(self, lines: list[str]):
for l in lines:
self.run(l)

View File

@ -1,73 +0,0 @@
import enum
import tokens
from prompt_toolkit.formatted_text import FormattedText
class RunStatus:
"""
Base class for run status.
These are returned whenever the runner does something.
"""
pass
class MacroStatus(RunStatus):
"""
Returned when a macro is defined.
Values:
`was_rewritten`: If true, an old macro was replaced.
`macro_label`: The name of the macro we just made.
"""
def __init__(
self,
*,
was_rewritten: bool,
macro_label: str
):
self.was_rewritten = was_rewritten
self.macro_label = macro_label
class StopReason(enum.Enum):
BETA_NORMAL = ("#FFFFFF", "β-normal form")
LOOP_DETECTED = ("#FFFF00", "loop detected")
MAX_EXCEEDED = ("#FFFF00", "too many reductions")
INTERRUPT = ("#FF0000", "user interrupt")
class ReduceStatus(RunStatus):
"""
Returned when an expression is reduced.
Values:
`reduction_count`: How many reductions were made.
`stop_reason`: Why we stopped. See `StopReason`.
"""
def __init__(
self,
*,
reduction_count: int,
stop_reason: StopReason,
result: tokens.LambdaToken
):
self.reduction_count = reduction_count
self.stop_reason = stop_reason
self.result = result
class CommandStatus(RunStatus):
"""
Returned when a command is executed.
Values:
`formatted_text`: What to print after this command is executed
"""
def __init__(
self,
*,
formatted_text: FormattedText
):
self.formatted_text = formatted_text

2
setup.py Normal file
View File

@ -0,0 +1,2 @@
from setuptools import setup
setup()

514
tokens.py
View File

@ -1,514 +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 = {},
*,
# To keep output readable, we avoid expanding macros as often as possible.
# Macros are irreducible if force_substitute is false.
force_substitute = False,
# If this is false, error when macros aren't defined instead of
# invisibly making a free variable.
auto_free_vars = True
) -> ReductionStatus:
if (self.name in macro_table) and force_substitute:
if force_substitute: # Only expand macros if we NEED to
return ReductionStatus(
output = macro_table[self.name],
reduction_type = ReductionType.MACRO_EXPAND,
was_reduced = True
)
else: # Otherwise, do nothing.
return ReductionStatus(
output = self,
reduction_type = ReductionType.MACRO_EXPAND,
was_reduced = False
)
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:
if isinstance(self.fn, macro):
# Macros must be reduced before we apply them as functions.
# This is the only place we force substitution.
r = self.fn.reduce(
macro_table,
force_substitute = True
)
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
)
)