Compare commits

...

No commits in common. "release" and "init_release" have entirely different histories.

67 changed files with 603 additions and 20514 deletions

1
.envrc
View File

@ -1 +0,0 @@
use flake

1
.gitattributes vendored
View File

@ -1 +0,0 @@
*.wasm filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
/target
/Cargo.lock
/node_modules
flake-inputs
.direnv

View File

@ -1,22 +0,0 @@
[package]
name = "rudus"
version = "0.0.1"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
chumsky = "0.10.1"
imbl = "5.0.0"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.11.1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.50"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
console_error_panic_hook = "0.1.7"
struct_scalpel = "0.1.1"
serde-wasm-bindgen = "0.6.5"
ordered-float = "5.0.0"

235
LICENSE
View File

@ -1,235 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
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 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 work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero 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 Affero 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 Affero 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 Affero 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.
rudus
Copyright (C) 2024 scott
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
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 AGPL, see <http://www.gnu.org/licenses/>.

View File

@ -1,87 +0,0 @@
![Ludus logo](logo.png)
## Ludus: A friendly, dynamic, functional language
Ludus is a scripting programming language that is friendly, dynamic, and functional.
This repo contains a work-in-progress implementation of an interpreter for the Ludus programming language, using Rust as the host language.
Ludus is part of the [_Thinking with Computers_ project](https://alea.ludus.dev/twc/), run by Scott Richmond at the University of Toronto, with collaborator Matt Nish-Lapidus; Bree Lohman and Mynt Marsellus are the RAs for the project. Ludus is our research language, which aspires to be a free translation of Logo for the 2020s.
Here are our design goals:
#### Friendly
Ludus, like Logo, is meant to be a teaching language, often for students who don't think of themselves as "computer people." Our intended audience are humanities and art people at the university level (undergrads, grads, faculty). Everything is kept as simple as possible, but no simpler. Everything is consistent as possible. We aspire to the best error messages we can muster, which is important for a language to be teachable. That means being as strict as we can muster, _in order to be friendlier_.
Our current development target is Ludus on the web: https://web.ludus.dev. That wires what we do on the langauge interpreter (here in this repo) to a web frontend.
Naturally, it starts with Logo's famed turtle graphics.
#### Dynamic
Statically typed programming languages generally give more helpful error messages than dynamic ones, but learning a type system (even one with robust type inference) requires learning two parallel systems: the type system and the expression system (well, and the pattern system). Type systems only really make sense once you've learned why they're necessary. And their benefits seem (to us, anyway) to be largely necessary when writing long-lived, maintainable, multi-author code. Ludus code is likely to be one-off, expressive, and single-authored.
To stay friendly, Ludus is dynamic. But: despite the dynamism, we aim to be as strict as possible. Certainly, we want to avoid the type conversion shenanigans of a language like JavaScript.
#### Functional
Ludus is emphatically functional: it uses functions for just about everything. This is both because your humble PI had his world reordered when he learned his first functional language (Elixir), and because the research into Logo and the programming cultures of MIT in the 1970s revolve around extremely functional Lisp code (i.e., Scheme). Logo is a weird little language, but it is a descendant of Lisp. So is Ludus.
Also, we believe that Ludus's immutable bindings and persistent or immutable data structures and careful approach to manipulating state lead to a lot of good pedagogical results. Learning a programming language involves learning how to model what's going on inside the computer; Ludus, we think, makes that both simpler and easier.
If you're looking for cognate languages, Ludus takes a _lot_ of design inspiration from Clojure and Elixir (which itself took a lot from Clojure). Clojure and Elixir are great! If you're asking why you should use Ludus instead of them, you're already at the point where you should be using them. Ludus is, maybe, for the people whom you'd like to work with in 5 years at your Pheonix shop (but even then, probably not).
### Status
Pre-alpha, still under active development. Lots of things change all the time. Definitely some broken bits, especially around processes.
### Use
Current emphasis is on the web version: https://web.ludus.dev.
### Main features
* Expression-oriented: everything returns a value
* Pattern matching in all the places
* No operators: everything is called as a function
* Easy-peasy partial application with placeholders
* Function pipelines
* Persistent or immutable data structures
* Careful, explicit state management using `box`es
* Clean, concise, expressive syntax
* Value-based equality; only functions are reference types
* Persistent, immutable data structures, à la Clojure
### `Hello, world!`
Ludus is a scripting language. At current it does not have a good REPL. Our aim is to get interactive coding absolutely correct, and our efforts in [ludus-web](https://github.com/thinking-with-computers/ludus-web) are currently under way to surface the right interactivity models for Ludus.
Either:
```
"Hello, world!"
```
`=> "Hello, world!"`
Ludus scripts (and blocks) simply return their last expression; this script returns the bare string and exits.
Or:
```
print! ("Hello, world!")
```
```
=> Hello, world!
=> :ok
```
Here, we use the `print!` function, which sends a string to a console (`stdout` on Unix, or a little console box on the web). Because `print!` returns the keyword `:ok` when it completes, that is the result of the last expression in the script--and so Ludus also prints this.
### Some code
Fibonacci numbers:
```
& fibonacci!, with multi-clause fns/pattern matching
fn fib {
"Returns the nth fibonacci number."
(1) -> 1
(2) -> 1
(n) -> add (
fib (sub (n, 1))
fib (sub (n, 2)))
}
fib (10) &=> 55
```
### More on Ludus
See the [language reference](language.md) and [the documentation for the prelude](prelude.md).

View File

@ -1,31 +0,0 @@
fn agent (val) -> receive {
(:set, new) -> agent (new)
(:get, pid) -> {
send (pid, (:response, val))
agent (val)
}
(:update, f) -> agent (f (val))
}
fn agent/set (pid, val) -> {
send (pid, (:set, val))
val
}
fn agent/get (pid) -> {
send (pid, (:get, self ()))
receive {
(:response, val) -> val
}
}
fn agent/update (pid, f) -> {
send (pid, (:update, f))
agent/get (pid)
}
let myagent = spawn! (fn () -> agent (42))
print! ("incrementing agent value to", agent/update (myagent, inc))
:done!

File diff suppressed because it is too large Load Diff

View File

@ -1,469 +0,0 @@
# Ludus for programmers
## A brief introduction
Ludus is mostly understood by its research and design team as a language for _learners_.
It is a _pedagogical_ language, whose primary purpose is to lead students to critical encounters with the history and present of computing.
The design principles, then, lead with learnability as well as making certain key examples in the history of computing easy.
Because of that, Ludus has some weird features.
It will likely not feel especially familiar, particularly if you have not written funtional code before.
We encourage you to feel disoriented by it, and to lean into that disorientation.
Instead of trying to write code like you have in the past, write code like Ludus wants you to.
There are two big influences on Ludus.
In terms of historical languages, Ludus draws a lot from Logo and Scheme.
In terms of contemporary languages, Ludus has deep affinities with Elixir and Clojure.
To put it in an abstraction cluster, Ludus is a dynamically typed, extremely strict functional programming language with persistent data structures, deep immutability, and pattern matching.
None of these are especially mainstream.
It is not "batteries included," but rather offers a quite minimalistic set of capabilities.
These are devised, as I noted above, to make encountering key concepts from the history of comptuing easy.
But beyond that, Ludus is extremely minimal, in the tradition of Scheme and Logo.
The profound pedagogical perspective behind Scheme and Logo is that building the things you want is an important motivator for learning how to make computers do things.
Ludus follows in this path.
If you've mostly written object-oriented code, Ludus will, frankly, feel weird.
And that's awesome.
### Ludus is expression based
Ludus has no statements, only expressions.
Every expression returns a value, including conditional forms like `if`, `when`, and `match`.
That said, not every kind of expression can go anywhere.
In Ludus, different types of expressions are called _forms_, riffing on the grand Lisp tradition.
### Ludus is dynamically typed
Like its inspirations, Elixir and Clojure and the whole family of Lisps, Ludus is dynamically typed.
It is _strictly_ typed, however.
Unlike Javascript, Ludus will never convert between values of one type or another.
Ludus has the following types:
* `:nil`: The type of `nil`, Ludus's name for nothing.
* `:bool`: Boolean--`true` or `false`.
* `:number`: IEEE-754 64-bit floating point numbers. Ludus does not have an integer type. That said, Ludus avoids `NaN` as much as possible.
* `:string`: UTF-8 strings.
* `:keyword`: Keywords are self-identical atoms, evaluating only to themselves. The equivalent of a `Symbol` in Javascript (or a keyword in Clojure or an atom in Elixir). (The types in this list--and in Ludus--are represented as keywords.)
* `:tuple`: Fixed-length, fully immutable collections of between zero and 7 values (inclusive). Tuples are comma-or-newline separated values, surrounded by parentheses: `(1, 2, 3)`.
* `:list`: Persistent, immutable ordered list of any number of Ludus values. Lists are comma-or-newline separated values, surrounded by square brackets: `[:foo, :bar, :baz]`.
* `:dict`: Persistent, immutable associative collection of keyword keys and any Ludus values. Dicts are comma-or-newline separated keyword-and-value pairs, introduced by `#{` and closed with a curly brace: `#{:a 1, :b 2}`. Keys may be keywords or strings.
* `:fn`: Functions!
* `:box`: A holder for any value, which can change over time. A cognate of Clojure's atom. This is the only data type in Ludus that can hold mutable state.
At current, three other types are planned but not implemented: `:set`, `:pkg`, `:process`.
Ludus does not allow creating new nominal types.
### Ludus has a weird comment character
It uses the ampersand--`&`--to introduce comments.
It does not have mulitline comments.
### Ludus does not have variables, it has bindings
The basic form of assignment in Ludus looks very familiar:
```
let foo = 42
let bar = :quux
let baz = "hello, world"
```
These are _let bindings_.
#### Let bindings are extremely immutable
They may not change.
In addition, you may not shadow let bindings.
You may not shadow a binding, like in Rust, where you can re-use the name and discard the old binding.
You may also not bind the same name in a nested scope (e.g., inside a function).
Once you bind a name, it is forever bound to that value.
The value in this is that language learners need (almost) never wonder what value a name is bound to, since it can never change.
Except, of course, with function calls.
#### The left-hand side of a let binding is a _pattern_
Ludus makes extensive use of pattern matching.
The left-hand side of a let binding need not be a simple name.
A simple name is only one kind of pattern.
For example, this is valid Ludus:
```
let foo = 42
let 42 = foo
let nil = nil
```
The second line does nothing _except_ match the value on the left hand side to the value on the right hand side.
If a let binding does not match, e.g., `let 1 = 2`, then Ludus will panic.
Patterns can also be used to destructure all Ludus collections:
```
let (:ok, x) = (:ok, 42) & tuple pattern: x is now 42
let [l, m, ...] = [1, 2, 3] & list pattern: l = 1, m = 2
let #{a, b} = #{:a 1, :b 2} & dict pattern: a = 1, b = 2
```
#### Collection patterns are exact & complete, unless otherwise specified
In the second line in the example above, the pattern `[l, m, ...]` includes a splat pattern (or splattern).
If we had written `let [l, m] = [1, 2, 3]`, Ludus would have panicked with `no match`.
There are three list members on the right, only two on the left.
The splat, `...` (or ellipsis) matches "anything else in the list."
You may also include a name after the splat, which will be bound to "anything else in the list," e.g.
```
let [head, ...tail] = [1, 2, 3, 4, 5]
head &=> 1
tail &=> [2, 3, 4, 5]
```
#### The placeholder is a special pattern
A placholder pattern, `_`, matches against anything but does not bind a name.
Also, you may name your placholders, e.g., `_ignored`, but that is for the programmer only.
Named or unnamed placeholder patterns are strictly equivalent.
### Ludus panics
Ludus has exactly one type of runtime error: a panic.
Panics will always crash the program.
You cannot `catch` a panic.
You can raise a panic thusly:
```
panic! "oh shit"
```
`panic!` may only take a single value, but that value can be a collection.
Panics bring down processes, and any linked processes. More on processes below.
### Almost everything is a function
Ludus does not have operators.
In the grand Lisp tradition, all operations look like (and, for the most part, substantively are) function calls.
* To add two numbers in Ludus: `add (1, 2) &=> 3`.
* To subtract one number from another: `sub (2, 1) &=> 1`
* To determine, are two things equal?: `eq? (3, "three") &=> false`
### The Prelude is loaded before every evaluation
The Ludus Prelude is its standard library, and all functions in the Prelude are available in every Ludus script.
Consult the [Prelude documentation](./prelude.md) for information for all functions in Prelude.
Everything you'll want to do with Ludus involves the Prelude in some way.
Note that most Prelude function names can, in fact, be shadowed by local bindings in a script.
That said, there are several functions that, for optimization reasons, are "builtin," whose names may never be used, e.g., `add`, `sub`, `eq?`, `inc`, `dec`, and so on.
#### Boolean "functions" are actually "special forms"
`and` and `or` are special, in that they are compiled differently than other functions.
Their arguments are evaluated lazily, rather than eagerly, so they can short-circuit (and prevent panics).
Because of that, they may not be referred to or passed around like other functions.
### Ludus lists and dicts are persistent
Dicts and lists are persistent.
This means you cannot mutate them.
However, you can still add things to a list--you just get back a _new_ list with the value added:
```
let foo = [1, 2, 3]
let bar = append (foo, 4) &=> [1, 2, 3, 4]
let baz = #{:a 1, :b 2}
let quux = assoc (baz, :c, 3) &=> #{:a 1, :b 2, :c 3}
foo &=> [1, 2, 3]
baz &=> #{:a 1, :b 2}
```
Persistent data structures are wonderful, and use a lot of computer science magic to make them competitive in terms of performance: they use "structural sharing" and attempt "opportunistic mutation."
### Ludus has three conditional forms
The three conditional forms in Ludus are `if`, `when`, and `match`.
#### The `if` form
Ludus's base conditional form is `if`:
```
if foo then bar else baz
```
Does what you'd expect!
But with two caveats.
(Before the caveats: you can put newlines before `then` and `else`.)
#### Falsy values: `nil` and `false`
The condition (`foo` in the example above) is evaluated not strictly as `true` or `false`.
Ludus "falsy" values are `nil` and `false`.
Everything else is truthy, including `0` and `()` (the empty tuple), and `""` (the empty string).
This holds across anywhere in the language you are dealing with notions of truth and falsity: `if` and `when` forms, `guard` expressions in `match` forms, `and` and `or`, etc.
#### Both `then` and `else` are obligatory
`if` forms in Ludus _must_ have both `then` and `else` branches.
This is because every expression in Ludus must return a value.
If you want to throw away a value, you can do that, but you'll need something like `else nil` or `else :nothing`.
#### The `when` form
If you have multiple conditions you'd like to chain together, `when` forms are what you want.
(Ludus does not have an `else if` form, although you can certainly chain `if` forms together.)
`when` puts multiple clauses together, each of which has a left-hand condition expression and a right-hand body expression: `<condition expr> -> <body expr>`
Ludus will evaluate the left-hand expression, and, if it's truthy, evaluate and return the corresponding right-hand expression:
```
when {
eq? (1, 2) -> :nope
eq? (3, 4) -> :not_this_either
eq? (0, 0) -> :this!
} &=> :this!
```
If no clause in a when form has a truthy left-hand side, Ludus panics.
Any truthy value will do if you want the equivalent of an `else` branch.
By convention, `:else` is used as the catch-all at the end of a match form.
#### The `match` form
A `match` form is the equivalent of a `switch` statement in C-family languages.
It is much more powerful, however.
`match` is much beloved by functional programmers.
`match` forms are similar to `when` forms, but they require a value--a "scrutinee."
And, in place of expressions on the left-hand side of their clauses, they have patterns: `<pattern> -> <expr>`.
They attempt to match the value against each pattern until there is a match.
If no clause matches, then Ludus panics.
This is an extremely common pattern in Ludus:
```
let might_fail = (:ok, 42)
match might_fail with {
(:ok, value) -> print! ("got {value}!")
(:err, _) -> print! ("the thing failed")
} &=> :ok, prints "got 42!"
```
##### Match clauses may have a guard expression
A match clause may also have a guard expression.
Afer the pattern and before the arrow, you may put `if <expr>`.
(Here you may not use `then` or `else`.)
Bindings made in the pattern are valid in that expression.
If the expression is truthy, then that's a match.
If it's falsy, no match:
```
let foo = 42
let bar = 23
match bar with {
x if even? (x) -> :even
x if eq? (foo, x) -> :foo
_ -> :odd_not_foo
} &=> :odd_not_foo
```
### Ludus groups expressions together with blocks
A block groups expressions together.
Ludus blocks must have at least one expression (because everything in Ludus must return a value).
A block evaluates to its last expression.
Expressions are separated by one or more terminators--newlines or semicolons.
Use curly braces to form a block:
```
if true
then {
:first; :second & these are two different expressions
:third
}
else {
:nothing
} &=> :third
```
Blocks can go most anywhere single expressions can go.
### Ludus has synthetic expressions
We have already seen function calls, e.g., `add (1, 2)`.
This is a _synthetic_ expression, which is a chained combination of bound names, tuples, keywords, and method calls (more on these below).
The root of a synthetic expression may be either a name or a keyword.
Subsequent terms must either be tuples or keywords.
They are evaluated by applying the second term to the first, then applying the third term to the result of that first application, and applying the fourth to the second result, and so on.
Applying a tuple will call something as a function: `add (1, 2)`.
Applying a keyword will access the value stored at that key in a dict: `foo :bar`.
These may be chained arbitrarily.
Take, for example, `foo :bar (1, 2) :baz`.
This accesses `:bar` on `foo`, applies the arguments `(1, 2)` to that value (presumably a function), and then access `:baz` on value returned by that function.
#### Keywords may be called as functions
Following Clojure's example, you may call a keyword as a function: `foo :bar` and `:bar (foo)` are strictly equivalent.
Keywords may also be called when they are not keyword literals but bound names, e.g., this is valid:
```
let foo = :foo
let bar = #{:foo :baz}
foo (bar) &=> :baz
```
### Ludus has function pipelines
In addition to normal function application, Ludus also has function pipelines, equivalent to Elixir's pipelines or Clojure's thread macros.
In these, the first term is applied, as a single argument, to the second. The result of that is then applied, as a single argument, to the third, and so on.
Function pipelines are introduced by the reserved word, `do`.
These two expressions are exactly equivalent:
```
do foo > bar >
baz > quux
quux (baz (bar (foo)))
```
Newlines may be inserted _after_ the `>` pipeline symbol, not before.
Note that a line ending with the pipeline symbol will "eat" the line after it, even if separated by many terminators, so be careful.
Because keywords can be called like functions, bare keywords may be used in function pipelines.
### Ludus has partial function application
Any function in Ludus may be partially applied by using the placholder, `_`, in place of an argument.
Doing so returns a function that takes a single argument.
When that function is called, it calls the original function with that argument put in the placeholder's position.
Here's a simple example:
```
let double = mult (2, _)
double (3) &=> 6
double (12) &=> 24
```
Partially applied functions play very nicely with pipelines:
```
let double = mult (2, _)
let mynums = [1, 2, 3, 4, 5, 6]
do mynums >
filter (even?, _) > &-> [2, 4, 6]
map (double, _) &=> [4, 8, 12]
```
### Ludus function definitions
Functions come in three flavours, all of which have a concept of a function clause.
A function clause is a special case of a match clause: it has a _tuple_ pattern on its left hand side (since we call functions with tuples).
Otherwise,
#### Anonymous lambdas
An anonymous lambda is the `fn` reserved word, followed by a function clause:
```
let double = fn (x) -> mult (x, 2)
double &=> fn anon.
double (13) &=> 26
```
#### Named functions
Named functions are exactly the same as anonyomous lambdas, but they have a name between `fn` and the clause:
```
fn double (x) -> mult (x, 2)
double &=> fn double
double (-4) &=> -8
```
#### Compound functions
Compound functions have multiple clauses, separated off by curly braces:
```
fn foo? {
("foo") -> true
(:foo) -> true
(_) -> false
}
foo? (:bar) &=> false
```
There's a very close relationship between match forms and function definitions.
##### docstrings
A compound function may, optionally, take a string before any of its clauses, that serves as documentation for the function:
```
fn foo? {
"Tells if its argument is a `foo`."
("foo") -> true
(:foo) -> true
(_) -> false
}
```
Ludus will print the documentation for a function by means of the `doc!` function.
`doc!` will also display all the patterns in a function's clauses.
### Ludus has a convention of "commands": they end with a bang
By convention, Ludus functions that end in an exclamation point have side effects.
These are called _commands_.
`doc!` is a command; so is `print!`.
Ludus commands typically return the keyword `:ok` rather than `nil`.
Much of Ludus involves manipulating turtle graphics commands, `forward!`, and so on.
### Ludus has loops, but you should probably use recursion
Ludus, in the grand Lisp (and Logo) tradition, eschews looping constructs in favour of functional recursion.
Ludus is tail-call optimized, which means that recursion, even mutual recursion, is as fast as looping.
The `loop` form, anyway, isn't anything like you're expecting; it's basically function calls.
Two examples of factorial, looping and recurisve:
```
loop (6, 1) with {
(0, acc) -> acc
(n, acc) -> recur (dec (n), mult (n, acc))
} &=> 720
fn fact {
(n) -> fact (n, 1)
(0, acc) -> acc
(n, acc) -> fact (dec (n), mult (n, acc))
}
fact (6) &=> 720
```
The difference between these is that Ludus will throw a compile error if `recur` isn't in tail position.
In addition, all clauses in a loop form, and all invocations of `recur` must have the same arity and may not have splats in patterns, whereas functions may have clauses of arbitrary arity.
### Ludus has multiple "levels" of expressions
Not all Ludus expressions can appear anywhere you need an expression.
Ludus has four levels of expressions that restrict where they may go: simple, nonbinding, expressions, and toplevel.
* _Simple_ expressions include all literals as well as bare names, synthetic expressions, anonymous `fn` lambdas, `do` forms, and `panic`s. They may go anywhere you expect an expression, e.g. in the condition position in if or when forms. But in these positions, you may not use, say, another conditional form, nor bind a name.
* _Nonbinding_ forms include all expressions _except_ those that bind a name. To simple expressions, nonbinding expressions extend to conditional forms (`if`, `when`, `match`, `receive`).
* _Expressions_ (tout court) include all Ludus expressions, including those that bind names: `let`, named `fn`s, and `box`.
### Ludus has carefully managed state
At some point, you need state.
(You need far less than you think!)
For that, you need a `box`.
A box holds a value that can change over time.
It can hold any other Ludus value, including a box.
Getting a value out of a box isn't as simple, however, as using its name.
The name is bound to the _box_, not its value.
To get the value out of a box, you use the `unbox` function:
```
box foo = 42
foo &=> box [42]
unbox (foo) &=> 42
```
To change the value in a box, you use either the `store!` command, or the `update!` command.
`store!` takes a box and a value, and simply puts the new value in the box.
`update!` (not to be confused with the function `update`) takes a box and a function, and updates the value in the box by applying the function to the value in the box:
```
box foo = 42 &=> box [42]
store! (foo, 23) &=> box [23]
update! (foo, add(13, _)) &=> box [36]
unbox (foo) &=> 36
```
#### Boxes are not variables
We have put the section on boxes last in this introduction because boxes are _not_ variables.
Most state can actually be, and within Ludus, absolutely ought to be, modeled not with boxes but with recursive functions.
Consider the factorial example from earlier.
A straightforward Javascript implementation might look like this:
```javascript
function fact (n) {
let acc = 1;
while n > 1 {
acc = n * acc;
n--;
}
return acc;
}
```
You'll note that the `while` statement doesn't have an easy equivalent in Ludus.
But if you were really stubborn about wanting to twised boxes into variables, you could do something like this:
```
fn fact (n) -> {
box acc = 1
loop (n) with (m) -> if lt? (m, 1)
then unbox (acc)
else {
store! (acc, mult (m, unbox (acc)))
recur (dec (m))
}
}
```
Let me tell you, this is _wild_ Ludus.
The `loop` there is very weird indeed.
`box`es, also, are much, much slower than variables.
The short version is, if you can possibly avoid it--and you probably can--don't use boxes.
The more complex version is this:
The functional and immutable nature of Ludus will change your ideas about programming.
This is part of the point.
(More to come...)

View File

@ -1,513 +0,0 @@
# Ludus language reference
This is not intended for beginners, but to be a language overview for experienced programmers. That said, it may help beginners orient themselves in the language.
## Comments
Ludus's comment character is `&`. Anything after an ampersand on a line is ignored. There are no multiline comments.
## Atomic values
Ludus has four types of atomic values.
### `nil`
`nil` is Ludus's representation of nothing. In the grand Lisp tradition, Ludus can, and occasionally does, use `nil`-punning. Its type is `:nil`.
### Booleans
`true` and `false`. That said, in all conditional constructs, `nil` and `false` are "falsy," and everything else is "truthy." Their type is `:boolean`.
### Numbers
Ludus has numbers, which are IEEE-754 64-bit floats. Numbers are more complicated than you think, probably.
Number literals in Ludus are either integers or decimal floating point numbers, e.g. `32.34`, `42`, `-0.23`. Underscores in numbers are ignored, and can be used to separate long numbers, e.g. `1_234_567_890`.
Numbers' type is `:number`.
### Keywords
Ludus keywords begin with a colon and a letter, e.g. `:keyword`. Types are represented as keywords. Some functions take an optional units argument as a keyword, e.g. `:radians`. Keywords are also used as keys for associative collections. Keywords' type is `:keyword`.
Keywords must begin with an ASCII upper- or lower-case letter, and can then include any letter character, as well as `_`, `/`, `!`, `?`, and `*`.
## Strings
Ludus strings are UTF-8 strings, and only use double quotes. Strings may be multiline. For example, this is a string: `"foo"`. So is this:
```
"foo
bar baz"
```
Strings use backslashes for escapes, including `\n` for newline, `\t` for tab, `\"` for a double quote, and `\{` for an open curly brace (see below on interpolation).
Strings' type is `:string`.
### String interpolation
Strings may also insert a string representation of any Ludus value that is bound to a name, by inserting that name in curly braces. To wit,
```
let foo = :foo
let bar = 42
let baz = [1, 2, 3]
"{foo} {bar} {baz}" &=> ":foo 42 1, 2, 3"
```
Interpolations may _not_ be arbitrary expressions: only bound names may be used in interpolations.
## Collections
Ludus has a few different types of collections, in increasing order of complexity: tuples, lists, sets, dicts, and packages. All collections are immutable.
#### Separators
In all collection literals, members are written with a separator between them. On the same line, use a comma; or a newline will also separate elements. You may use as many separators as you wish at any point inside a collection or pattern. `(,,,,,,,3,,4,,,,,,)` and `(3, 4)` are the same value.
#### Efficiency
At the current moment, Ludus collections are all copy-on-write; this means that Ludus is _not_ performant with large collections. Eventually, Ludus will have Clojure-style persistent, immutable collections.
### Tuples
Tuples are fully-immutable, ordered collections of any kinds of values, delimited by parentheses, e.g. `(1, :a, "foo")`. At current, they have no length limit (although they eventually will). Unlike in some languages, tuples can be empty or contain a single element: `()` and `(:foo)` are both just fine. Tuples largely cannot be manipulated functionally; they must be written as literals and unpacked using pattern matching. They can, however, be converted to lists, either through pattern matching or the `list` function. Their type is `:tuple`.
### Lists
Lists are persistent and immutable ordered collections of any kinds of values, delimited by square braces, e.g. `[1, :a, "foo"]`. Their type is `:list`.
Lists may be combined using splats, written with ellipses, e.g., `[...foo, ...bar]`.
### Sets
Sets are persistent and immutable unordered collections of any kinds of values, which can only contain one instance of any given value. They are written similarly to ordered collections: `${1, :a, "foo"}`. Their type is `:set`.
### Dictionaries, or dicts
Dicts are persistent and immutable associative collections of any kinds of values. Dicts use keywords as keys (and cannot use any other kind of Ludus value as a key, not even strings), but can store any values. Dict literals are written as keyword-value pairs: `#{:a 1, :b false}`. Single words may be used as a shorthand for a key-value pair. Accessing a key that holds no value returns `nil`. Their type is `:dict`.
### Packages
Packages are immutable collections of bindings. They may only be described at the top level of a script, and their names must begin with a capital letter. Accessing a key that has no value on a package results in a validation error. They may not be accessed using functions, but only direct keyword access. Their type is `:pkg`.
They are written with the form `pkg`, then a package name, beginning with a capital letter, that will be bound as their name, and then an associative structure (pairs or word shorthands), delimited by `{}`, e.g.:
```
pkg Foo {
:bar "bar"
:baz 42
quux
}
```
### Working with collections
Ludus names are bound permanently and immutably. Collections are immutable. How do you add something to a list or a dict? How do you get things out of them?
Ludus provides functions that allow working with persistent collections. They're detailed in [the Prelude](prelude.md). That said, all functions that modify collections take a collection and produce the modified collection _as a return value_, without changing the original collection. E.g., `append ([1, 2, 3], 4)` will produce `[1, 2, 3, 4]`, but the original list is unchanged. (For dicts, the equivalent is `assoc`.)
## Expressions
Ludus is an expression-based language: all forms in the language are expressions and return values, except `panic!`. That said, not all expressions may be used everywhere.
### Terminating expressions
Expressions in scripts and blocks are terminated by a newline or a semicolon. In compound forms, like, `if`, the terminator comes after the `else` expression.
In forms with multiple clauses surrounded by curly braces (i.e., function bodies, `match`, `when`, etc.), you may separate clauses with semicolons as well as newlines.
### Toplevel expressions
Some expressions may only be used in the "top level" of a script. Because they are the toplevel, they are assured to be statically knowable. These include: `pkg`, `ns`, `use`, `import`, and `test`. (NB: not all of these are yet implemented.)
### Non-binding expressions
Some forms may take any expression that does _not_ [bind a name](#words-and-bindings), for example, any entry in a collection, or the right-hand side of a `let` binding. This is because binding a name in some positions is ambiguous, or nonsensical, or leads to unwarranted complications.
### Simple expressions
Many compound forms will only accept "simple" expressions. Formally, simple expressions are either literal (atomic, string, or collection literals) or synthetic expressions. They are expressions which do not take sub-expressions: no `if`, `when`, `match`, etc. (`do` expressions are currently not simple, but that may be revised.)
## Words and bindings
Ludus uses _words_ to bind values to names. Words must start with a lower case ASCII letter, and can subsequently include any letter character (modulo backing character encoding), as well as , `_`, `/`, `?`, `!`, and `*`.
Ludus binds values to names immutably and permanently: no name in the same scope may ever be re-bound to a different value. (Although see [boxes](#boxes-and-state), below.
Attempting to use an unbound name (a word that has not had a value bound to it) will result in a validation error, and the script will not run.
### `let` bindings: a very short introduction
Ludus's basic binding form is `let`:
```
let foo = :bar & `foo` is now bound to `bar` for the rest of the scope.
let foo = :baz & Validation error: name foo was bound in line 1
```
`let` bindings are more complex; we will return to these below.
## Patterns
Ludus makes extensive use of pattern-matching. Patterns do two jobs at once: they match values (or don't); and they bind names. The left-hand side of the examples just above in the `let` binding is not just a word: it is a pattern. Patterns also arise in conditional forms and function declarations.
### The placeholder: `_`
The simplest pattern is the placeholder: it matches against anything, and does not bind a name. It is written as a single underscore: `_`, e.g., `let _ = :foo`.
#### Ignored names
If you wish to be a bit more explict than using a placeholder, you can use an ignored name, which is a name that starts with an underscore: `_foo`. This is not bound, is not a valid name, and can be used however much you wish, even multiple times in the same pattern. It is, in fact, a placeholder, plus a reader-facing description.
### Literal patterns
Patterns can be literal atomic values or strings: `0`, `false`, `nil`, `:foo`, etc. That means you can write `let 0 = 0` or `let :foo = :foo`, and, while nothing will happen, everything will be just fine.
Literals match against, well, literal values: if the pattern and the value are the same, they match! If not, they don't match.
Literal values do not bind anything.
### Word patterns
Word patterns match against any value, and bind that value to the word as a name. The scope of that binding depends on the form the pattern is in. `let foo = :bar` binds `:bar` to `foo` for the rest of the scope.
#### Typed patterns
Word patterns can, optionally, take a type, using the `as` reserved word, and the keyword representing the desired type: `let foo as :number = 42`.
### String patterns
Ludus has a simple but powerful form of string pattern matching that mirrors string interpolation. Any word inside curly braces in a string will match against a substring of a string passed into a pattern.
```
let i_am = "I am the walrus"
let "I {verb} the {noun}" = i_am
(verb, noun) &=> ("am", "walrus")
```
Note that such names may well be bound to empty strings: a match does not guarantee that there will be anything in the string. This is particularly relevant at the beginning and end of string patterns:
```
let we_are = "We are the eggmen"
let "{first}We {what}" = we_are
(first, what) &=> ("", "are the eggmen")
```
### Collection patterns
Tuples, lists, and dicts can be destructured using patterns. They are written nearly identically to their literal counterparts. Collection patterns are composed of any number of simpler patterns or other collection patterns. They bind any names nested in them, match literals in them, etc.
#### Tuple patterns
Tuple patterns are delimited using parens, using commas or newlines to separate any number of other patterns. Consider `let (x, y, z) = (1, 2, 3)`. `x`, `y`, and `z` are now bound to 1, 2, and 3, respectively.
The last item in a tuple pattern can be a splat--`...`--which either discards any remaining unenumerated values in a tuple, or binds them to a list. Without a splat, tuples patterns only match against tuples of the same length.
```
let mytup = (1, 2, 3)
let (x, _, y) = mytup & x is now 1, y is now 3
let (a, ...) = mytup & a is now 1; a bare splat (without a name) is just fine
let (_, ...cs) = mytup & cs is now [2, 3]
let (p, q) = mytup & panic! no match
let () = () & empty tuples are also patterns
```
#### List patterns
List patterns are identical to tuple patterns, but they are written using square braces. They also match against a specific number of members, and may take a splat in the last position, e.g. `let [first, ...rest] = [1, 2, 3]`.
Note that list patterns, like tuple patterns, match on explicit length. That means that if you are matching only the first items of a list, you must explicitly include a splat pattern, e.g. `let [first, second, ...] = [1, 2, 3, 4]`.
#### Dict patterns
Dict patterns are written either with shorthand words, or keyword-pattern pairs. Consider: `let #{:a foo, :b 12, c} = #{:a 1, :b 12, :c 4}`. `foo` is now 1; `b` is now 12, `c` is now 4. If a dict does not hold a value at a particular key, there is no match.
Dict patterns may also use a splat as their last member: `let #{:a 1, ...b} = #{:a 1, :b 2, :c 3}` will bind `b` to `#{:b 2, :c 3}`.
Like tuple and list patterns, dict patterns without a splat at the end match only on exact equivalence on all keys.
## `let` bindings
`let` bindings are the basic form of matching and binding in Ludus. It is written `let {pattern} = {non-binding expression}`. The pattern can be arbitrarily complex. If the left-hand side of a `let` binding does not match, Ludus will raise a panic, halting evaluation of the script.
## Scope and blocks
Ludus is lexically scoped. Bindings are valid for the remainder of the scope they act on. To introduce a new scope, Ludus uses a block, a collection of expressions delimited by curly braces and separated by semicolons or newlines. The value of a block, as well as the value of a script, is the last expression in it. In `let foo = {:this; :set; :of; :expressions; "is actually"; :ignored }`, `foo` will be bound to `:ignored`.
That said, you can use bindings in blocks, which will not be available outside that block--but blocks can use bidnings from their parent scope:
```
let outer = 42
let first = {
let inner = 23
add (outer, inner)
} & first is now bound to 65
inner & Validation error: unbound name inner
```
### Shadowing
Even though names are bound permanently in Ludus, it is perfectly possible (and occasionally quite useful) to "shadow" names from an enclosing scope.
```
let x = 42
let y = {
let first = x
let x = 23 & this is fine
let second = x
add (first, second)
} & y is now 65
```
## Conditional forms
Ludus has a robust set of conditional forms, all of which are expressions and resolve to a single value.
### `if`
`if` evaluates a condition; if the result of the condition is truthy, it evaluates is `then` branch; if the condition is falsy, it evaluates its `else` branch. Both branches must always be present. Newlines may come before `then` and `else`.
`if {simple expression} then {non-binding expression} else {non-binding expression}`
### `when`
`when` is like Lisp's `cond`: it takes a series of clauses, separated by semicolons or newlines, delimited by curly braces. Clauses are written `lhs -> rhs`. `when` expressions are extremely useful for avoiding nested `if`s.
The left hand of a clause is a simple expression; the right hand of a clause is any expression. When the left hand is truthy, the right hand is evaluated, and the result of that evaluation is returned; no further clauses are evaluated. If no clause has a truthy left-hand value, then a panic is raised. In the example below, not the use of literal `true` as an always-matching clause.
```
when {
maybe () -> :something
mabye_not () -> :something_else
true -> :always
}
```
### `match`
A `match` form is the most powerful conditional form in Ludus. It consists of a test expression, and a series of clauses. The test expression must be a simple expression, followed by `with`, and then a series of clauses separated by a semicolon or newline, delimited by curly braces.
```
match may_fail () with {
(:ok, value) -> calculate_result (value)
(:err, msg) -> { log! (msg); recover_somehow () }
}
```
The left hand of a match clause is a pattern; the right hand is an expression: `pattern -> expression`. If the pattern matches the test expression of a clause, the expression is evaluated with any bindings from the pattern, and `match` form evaluates to the result of that expression.
If a test expression does not match against any clause's pattern, a panic is raised.
Ludus does not attempt to do any exhaustiveness checking on match forms; match errors are always runtime errors.
#### Guards
`match` clauses may have a _guard expression_, which allows a clause only to match if the expression's result is truthy. In the previous example, consider that we might want different behaviour depending on the value of the number:
```
match may_fail () with {
(:ok, value) if pos? (value) -> calculate_positive_result (value)
(:ok, value) if neg? (value) -> calculate_negative_result (value)
(:ok, 0) -> do_something_with_zero ()
(:err, msg) -> { log! (msg); recover_somehow () }
}
```
## Functions
Ludus is an emphatically functional language. Almost everything in Ludus is accomplished by applying functions to values, or calling functions with arguments. (These are precise synonyms.)
Functions have the type `:fn`.
### Calling functions
Functions are called by placing a tuple with arguments immediately after a function name, e.g. `add (1, 2)` adds `1` and `2`. Because they are represented as tuples, arguments must be explicitly written; splats cannot be used to pass an arbitrary number of arguments to a function.
### Defining functions
Functions have three increasingly complex forms to define them. All of them include the concept of a function clause, which is just a match clause whose left hand side must be a _tuple_ pattern.
#### Anonymous lambda
An anonymous lambda is written `fn {tuple pattern} -> {expression}`, `fn (x, y) -> if gt? (x, y) then x else add (x, y)`. Lambdas may only have one clause.
#### Named functions
A named function is identical to a lambda, with the one change that a word follows the `fn` reserved word: `fn {name} {tuple pattern} -> {expression}`. E.g., `fn add_1 (x) -> add (x, 1)`. The name of the function is bound for the remainder of the scope.
#### Compound functions
Compound functions are functions that have multiple clauses. They must be named, and in place of a single clause after a name, they consist in one or more clauses, separated by semicolons or newlines, delimited by curly braces. Optionally, compound functions may have a docstring as their first element after the opening curly brace. The docstring explains the function's purpose and use, before any of the function clauses.
An example from Ludus's Prelude:
```
fn some {
"Takes a possibly `nil` value and a default value. Returns the value if it's not `nil`, returns the default if it's `nil`."
(nil, default) -> default
(value, _) -> value
}
```
### Closures
Functions in Ludus are closures: function bodies have access not only to their specific scope, but any enclosing scope. That said, functions only have access to names bound _before_ they are defined; nothing is hoisted in Ludus.
### Mutual recursion and forward declaration
If you try the following, you'll get a validation error:
```
fn stupid_odd? {
(0) -> false
(x) -> supid_even? (dec (x)) & Validation error: unbound name stupid_even?
}
fn stupid_even? {
(0) -> true
(x) -> stupid_odd? (dec (x))
}
```
To allow for mutual recursion, Ludus allows forward declarations, which are written `fn name` without any clauses. In the example above, we would simply put `fn stupid_even?` before we define `stupid_odd?`.
If you declare a function without defining it, however, Ludus will raise a validation error.
### The Prelude
The Prelude is a substantial set of functions that is available in any given Ludus script. (It is, itself, just a Ludus file that has special access to host functions.) Because of that, a large number of functions are always available. The prelude documentation is [here](prelude.md).
### Partial application
Functions in Ludus can be partially applied by using a placeholder in the arguments. Partial application may only use a single placeholder (partially applied functions are always unary), but it can be anywhere in the arguments: `let add_1 = add(1, _)` or `let double = mult(_, 2)`.
Unary functions and called keywords may _not_ be partially applied: it is redundant.
Because of "partial application," Ludus has a concept of an "argument tuple" (which may include a single placeholder) in addition to a tuple literal (which may not include a placeholder).
### Function pipelines, or `do` forms
In place of nesting function calls inside other function calls, Ludus allows for a more streamlined version of function application: the `do` form or function pipeline. `do` is followed by an initial expression. `do` expressions use `>` as an operator: whatever is on the left hand side of the `>` is passed in as a single argument to whatever is on its right hand side. For example:
```
let silly_result = do 23 >
mult (_, 2) > add (1, _) >
sub (_, 2) > div (_, 9) & silly_result is 5
```
Newlines may appear after any instance of `>` in a `do` expression. That does, however, mean that you must be careful not to accidentally include any trailing `>`s.
### Called keywords
Keywords may be called as functions, in which case they extract the value stored at that key in the value passed in:
```
let foo = #{:a 1, :b 2}
let bar = :a (foo) & `bar` is now 1
```
Called keywords can be used in pipelines.
In addition, keywords may be called when they are bound to names:
```
let foo = #{:a 1, :b 2}
let bar = :a
bar (foo) & => 1
```
## Synthetic expressions
Synthetic expressions are valid combinations of words, keywords, package names, and argument tuples which allow for calling functions and extracting values from associative collections. The root--first term--of a synthetic expression must be a word or a keyword; subsequent terms must be either argument tuples or keywords.
```
let foo = #{:a 1, :b #{:c "bar" :d "baz"}}
let bar = foo :b :c & `bar` is now "bar"
let baz = :b (foo) :d & `baz` is now "baz"
```
## Looping forms
Ludus has optimized tail calls--the most straightforward way to accomplish repeating behaviour is function recursion. There are two additional looping forms, `repeat` and `loop`.
### `repeat`
`repeat` is a help to learners, and is useful for executing side effects multiple times. It is written `repeat {number|word} { {exprs} }`. From turtle graphics:
```
repeat 4 {
forward! (100)
right! (0.25)
}
```
Note that `repeat` does two interesting things:
1. It never returns a value other than `nil`. If it's in the block, it disappears. This is a unique (and frankly, undesirable) property in Ludus.
2. Unlike everything else in Ludus, repeate _requires_ a block, and not simply an expression. You cannot write `repeat 4 forward! (100)`.
### `loop`/`recur`
`loop` and `recur` are largely identical to recursive functions for repetition, but use a special form to allow an anonymous construction and a few guard rails.
The syntax here is `loop <tuple> with { <function clauses> }`. (Or, you can have a single function clause instead of a set of clauses.) The tuple is passed in as the first set of arguments.
```
let xs = [1, 2, 3, 4]
loop (xs, 0) with {
([x], sum) -> add (x, sum) & matches against the last element of the list
([x, ...xs], sum) -> recur (xs, add (x, sum)) & recurs with the tail
} &=> 10
```
`recur` is the recursive call. It must be in tail position--`recur` must be the root of a synthetic expression, in return position. If `recur` is not in tail position, a validation error will be raised.
In addition, `recur` calls must have the same number of arguments as the original tuple passed to `loop`. While Ludus will allow you to write clauses in `loop` forms with a different arity than the original tuple, those will never match.
`recur` calls return to the nearest `loop`. Nested `loop`s are probably a bad idea and should be avoided when possible.
## Environment and context: the toplevel
The "toplevel" of a script are the expressions that are not embedded in other expressions or forms: not inside a block, not a member of a collection, not on the right hand side of a binding, not inside a function body. The toplevel-only forms:
### `import`
`import` allows for the evaluation of other Ludus scripts: `import "path/to/file" as name`. `import` just evaluates that file, and then binds a name to the result of evaluating that script. This, right now, is quite basic: circular imports are currently allowed but will lead to endless recursion; results are not cached, so each `import` in a chain re-evaluates the file; and so on.
Status: not yet implemented.
### `use`
`use` loads the contents of a namespace into a script's context. To ensure that this is statically checkable, this must be at the toplevel.
Status: not yet implemented.
### `pkg`
Packages, `pkg`es, may only be described at the toplevel of a script. This is to ensure they can be statically evaluatable.
### `test`
A `test` expression is a way of ensuring things behave the way you want them to. Run the script in test mode, and these are evaluated. If the expression under `test` returns a truthy value, you're all good! If the expression under `test` returns a falsy value or raises a panic, then Ludus will report which test(s) failed.
```
test "something goes right" eq? (:foo, :foo)
test "something goes wrong" {
let foo = :foo
let bar = :bar
eq? (foo, bar)
} &=> test failed: "something goes wrong" on line 3
```
`test`s must be at the toplevel--or embedded within other tests in _their_ highest level.
Formally: `test <string> <expression>`.
Status: not yet implemented.
## Changing things: `box`es
Ludus does not let you re-bind names. It does, however, have a type that allows for changing values over time: `box`. A box is a place to put things, it has its own identity, it can store whatever you put in it, but to get what's in it, you have to `unbox` it.
Syntactically and semantically, `box`es are straightforward, but do require a bit more overhead than `let` bindings. The idea is that Ludus makes it obvious where mutable state is in a program, as well as where that mutable state may change. It does so elegantly, but with some guardrails that may take a little getting used to.
The type of a `box` is, predictably, `:box`.
```
box foo = 42 & foo is now bound to a _box that contains 42_
add (1, foo) & panic! no match: foo is _not_ a number
store! (foo, 23) & foo is now a box containing 23
update! (foo, inc) & foo is now a ref containing 24
unbox (foo) &=> 23; use unbox to get the value contained in a box
```
### Ending with a bang!
Ludus has a strong naming convention that functions that change state or could cause an explicit panic end in an exclamation point (or, in computer nerd parlance, a "bang"). So anything function that mutates the value held in a reference ends with a bang: `store!` and `update!` take bangs; `unbox` does not.
This convention also includes anything that prints to the console: `print!`, `report!`, `doc!`, `update!`, `store!`, etc.
(Note that there are a few counter-examples to this: math functions that could cause a panic [in place of returning NaN] do not end with bangs: `div`, `inv`, and `mod`; each of these has variants that allow for more graceful error handling).
### Ending with a whimper?
Relatedly, just about any function that returns a boolean value is a predicate function--and has a name that ends with a question mark: `eq?` tests for equality; `box?` tells you if something is a ref or not; `lte?` is less-than-or-equal.
## Errors: panic! in the Ludus script
A special form, `panic!`, halts script execution with the expression that follows as an error message.
```
panic! :oops
if true then :ok else panic! "It's false!"
```
Panics also happen in the following cases:
* a `let` binding pattern has no match against the value of its expression
* a `match` or `when` form has no matching clause
* a function is called with arguments that do not match any of its clauses
* something that is not a function or keyword is called as a function
* a called keyword is partially applied
* `div`, `inv`, or `mod` divides by zero
* `sqrt` takes the square root of a negative number
* certain error handling functions, like `unwrap!` or `assert!`, are invoked on values that cause them to panic
In fact, the only functions in the Prelude which explicitly cause panics are, at current, `div`, `inv`, `mod`, `sqrt`, `unwrap!`, and `assert!`.
### `nil`s, not errors
Ludus, however, tries to return `nil` instead of panicking where it seems prudent. So, for example, attempting to get access a value at a keyword off a number or `nil`, while nonsensical, will return `nil` rather than panicking:
```
let a = true
a :b :c :d :e &=> nil
let b = [1, 2, 3]
at (b, 12) &=> nil
```
### Result tuples
Operations that could fail--especially when you want some information about why--don't always return `nil` on failures. Instead of exceptions or special error values, recoverable errors in Ludus are handled instead by result tuples: `(:ok, value)` and `(:err, msg)`. So, for example, `unwrap!` takes a result tuple and either returns the value in the `:ok` case, or panics in the `:err` case.
Variants of some functions that may have undesirably inexplicit behaviour are written as `{name}/safe`. So, for example, you can get a variant of `div` that returns a result tuple in `div/safe`, which returns `(:ok, result)` when everything's good; and `(:err, "division by zero")` when the divisor is 0.

File diff suppressed because one or more lines are too long

View File

@ -1,64 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1751957021,
"narHash": "sha256-2h9T/5Tyd2ounm3DAn+8LftRXctotQ/XD2aoZ9XBtsI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fcb0981c8fe996743fc57087e781017eab6e46f9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1751949589,
"narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9b008d60392981ad674e04016d25619281550a9d",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1751913732,
"narHash": "sha256-h6rTTB4MBJSIr4xsXxCi8fk8LdiFbwOw/g3QqBHLpW4=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "778e08df16294e4a2c11a40136f69f908e9be879",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,27 +0,0 @@
{
description = "Ludus dev flake (rust)";
inputs.fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {nixpkgs, fenix, ... }@inputs:
let
system = "x86_64-linux"; # your version
pkgs = nixpkgs.legacyPackages.x86_64-linux;
fx = fenix.packages.x86_64-linux;
in
{
devShells.${system}.default = pkgs.mkShell rec {
packages = [
pkgs.wasm-pack
(fx.combine [
fx.latest.cargo
fx.latest.rustc
fx.targets.wasm32-unknown-unknown.latest.rust-std
])
];
};
};
}

View File

@ -1,338 +0,0 @@
# A base library for Ludus
# Only loaded in the prelude
(import /janet/scanner :as s)
(defn bool [x] (if (= :^nil x) nil x))
(defn ludus/and [& args] (every? (map bool args)))
(defn ludus/or [& args] (some bool args))
(defn ludus/type [value]
(when (= :^nil value) (break :nil))
(def typed? (when (dictionary? value) (value :^type)))
(def the-type (if typed? typed? (type value)))
(case the-type
:buffer :string
:boolean :bool
:array :list
:table :dict
:cfunction :function
the-type))
(var stringify nil)
(defn- dict-str [dict]
(string/join
(map
(fn [[k v]] (string (stringify k) " " (stringify v)))
(pairs dict))
", "))
(defn- stringish? [x] (or (string? x) (buffer? x)))
(defn- stringify* [value]
(when (stringish? value) (break value))
(def type (ludus/type value))
(case type
:nil ""
:number (string value)
:bool (string value)
:keyword (string ":" value)
:tuple
(string/join (map stringify value) ", ")
:list
(string/join (map stringify value) ", ")
:dict (dict-str value)
:set
(string/join (map stringify (keys value)) ", ")
:box (stringify (value :^value))
:fn (string "fn " (value :name))
:function (string "builtin " (string value))
:pkg (dict-str value)
))
(set stringify stringify*)
(var show nil)
(defn- show-pkg [x]
(def tab (struct/to-table x))
(set (tab :^name) nil)
(set (tab :^type) nil)
(string "pkg " (x :^name) " {" (stringify tab) "}")
)
(defn- dict-show [dict]
(string/join
(map
(fn [[k v]] (string (show k) " " (show v)))
(pairs dict))
", "))
(defn- set-show [sett]
(def prepped (merge sett))
(set (prepped :^type) nil)
(def shown (map show (keys prepped)))
(string/join shown ", ")
)
(defn- show* [x]
(case (ludus/type x)
:nil "nil"
:string (string "\"" x "\"")
:tuple (string "(" (string/join (map show x) ", ") ")")
:list (string "[" (string/join (map show x) ", ") "]")
:dict (string "#{" (dict-show x) "}")
:set (string "${" (set-show x) "}")
:box (string "box " (x :name) " [ " (show (x :^value)) " ]")
:pkg (show-pkg x)
(stringify x)))
(set show show*)
# (var json nil)
# (defn- dict-json [dict]
# (string/join
# (map
# (fn [[k v]] (string (json k) ": " (json v)))
# (pairs dict))
# ", "))
# (defn- json* [x]
# (case (ludus/type x)
# :nil "\"null\""
# :number (string x)
# :bool (if true "\"true\"" "\"false\"")
# :string (string "\"" x "\"")
# :keyword (string "\"" x "\"")
# :tuple (string "[" (string/join (map json x) ", ") "]")
# :list (string "[" (string/join (map json x) ", ")"]")
# :dict (string "{" (dict-json x) "}")
# :set (string "[" (string/join (map json (keys x)) ", ") "]")
# (show x)))
# (set json json*)
(defn show-patt [x]
(case (x :type)
:nil "nil"
:bool (string (x :data))
:number (string (x :data))
:keyword (string ":" (x :data))
:word (x :data)
:placeholder (get-in x [:token :lexeme])
:tuple (string "(" (string/join (map show-patt (x :data)) ", ") ")")
:list (string "[" (string/join (map show-patt (x :data)) ", ")"]")
:dict (string "#{" (string/join (map show-patt (x :data)) ", ") "}")
:pair (string (show-patt (get-in x [:data 0])) " " (show-patt (get-in x [:data 1])))
:typed (string (show-patt (get-in x [:data 1])) " as " (show-patt (get-in x [:data 0])))
:interpolated (get-in x [:token :lexeme])
:string (get-in x [:token :lexeme])
:splat (string "..." (when (x :data) (show-patt (x :data))))
(error (string "cannot show pattern of unknown type " (x :type)))))
(defn pretty-patterns [fnn]
(def {:body clauses} fnn)
(string/join (map (fn [x] (-> x first show-patt)) clauses) "\n"))
(defn doc [fnn]
(when (not= :fn (ludus/type fnn)) (break "No documentation available."))
(def {:name name :doc docstring} fnn)
(string/join [name
(pretty-patterns fnn)
(if docstring docstring "No docstring available.")]
"\n"))
(defn- conj!-set [sett value]
(set (sett value) true)
sett)
(defn- conj-set [sett value]
(def new (merge sett))
(conj!-set new value))
(defn- conj!-list [list value]
(array/push list value))
(defn- conj-list [list value]
(def new (array/slice list))
(conj!-list new value))
(defn conj! [x value]
(case (ludus/type x)
:list (conj!-list x value)
:set (conj!-set x value)))
(defn conj [x value]
(case (ludus/type x)
:list (conj-list x value)
:set (conj-set x value)
(error (string "cannot conj onto " (show x)))))
(defn disj! [sett value]
(set (sett value) nil)
sett)
(defn disj [sett value]
(def new (merge sett))
(set (new value) nil)
new)
(defn assoc! [dict key value]
(set (dict key) value)
dict)
(defn assoc [dict key value]
(merge dict {key value}))
(defn dissoc! [dict key]
(set (dict key) nil)
dict)
(defn dissoc [dict key]
(def new (merge dict))
(set (new key) nil)
new)
(defn ludus/get [key dict &opt def]
(default def :^nil)
(get dict key def))
(defn rest [indexed]
(array/slice indexed 1))
(defn to_list [x]
(case (ludus/type x)
:list x
:tuple @[;x]
:dict (pairs x)
:set (-> x (dissoc :^type) keys)
@[x]))
(defn showprint [x]
(if (= :string (ludus/type x))
x
(show x)))
(defn print! [args]
(print ;(map showprint args)))
(defn prn [x]
(pp x)
x)
(defn concat [x y & zs]
(case (ludus/type x)
:string (string x y ;zs)
:list (array/concat @[] x y ;zs)
:set (merge x y ;zs)))
(defn unbox [b] (get b :^value))
(defn store! [b x] (set (b :^value) x))
(defn mod [x y]
(% x y))
(defn- byte->ascii [c i]
(if (< c 128)
(string/from-bytes c)
(error (string "non-ASCII character at index" i))))
(defn chars [str]
(def out @[])
(try
(for i 0 (length str)
(array/push out (byte->ascii (str i) i)))
([e] (break [:err e])))
[:ok out])
(defn to_number [str]
(when (string/find "&" str)
(break [:err (string "Could not parse `" str "` as a number")]))
(def scanned (s/scan (string/trim str)))
(when (< 0 (length (scanned :errors)))
(break [:err (string "Could not parse `" str "` as a number")]))
(def tokens (scanned :tokens))
(when (< 3 (length tokens))
(break [:err (string "Could not parse `" str "` as a number")]))
(def fst (first tokens))
(when (not= :number (fst :type))
(break [:err (string "Could not parse `" str "` as a number")]))
[:ok (fst :literal)])
(def ctx {
"add" +
"and" ludus/and
"assoc!" assoc!
"assoc" assoc
"atan_2" math/atan2
"bool" bool
"ceil" math/ceil
"chars" chars
"concat" concat
"conj!" conj!
"conj" conj
"cos" math/cos
"count" length
"dec" dec
"disj!" disj!
"disj" disj
"dissoc!" dissoc!
"dissoc" dissoc
"div" /
"doc" doc
"downcase" string/ascii-lower
"e" math/e
"eq?" deep=
"first" first
"floor" math/floor
"get" ludus/get
"gt" >
"gte" >=
"inc" inc
"last" last
"lt" <
"lte" <=
"mod" mod
"mult" *
"not" not
"nth" ludus/get
"or" ludus/or
"pi" math/pi
"pow" math/pow
"print!" print!
"prn" prn
"push" array/push
"random" math/random
"range" range
"rest" rest
"round" math/round
"show" show
"sin" math/sin
"slice" array/slice
"split" string/split
"sqrt" math/sqrt
"store!" store!
"str_slice" string/slice
"stringify" stringify
"sub" -
"tan" math/tan
"to_list" to_list
"to_number" to_number
"trim" string/trim
"triml" string/triml
"trimr" string/trimr
"type" ludus/type
"unbox" unbox
"upcase" string/ascii-upper
})
(def base (let [b @{:^type :dict}]
(each [k v] (pairs ctx)
(set (b (keyword k)) v))
b))

View File

@ -1,133 +0,0 @@
(import /janet/base :as base)
(import /janet/prelude :as prelude)
(defn map-values [f dict]
(from-pairs (map (fn [[k v]] [k (f v)]) (pairs dict))))
(def with-docs (map-values base/doc prelude/ctx))
(def sorted-names (-> with-docs keys sort))
(defn escape-underscores [str] (string/replace "_" "\\_" str))
(defn escape-punctuation [str] (->> str
(string/replace "?" "")
(string/replace "!" "")
(string/replace "/" "")))
(defn toc-entry [name]
(def escaped (escape-underscores name))
(string "[" escaped "](#" (escape-punctuation escaped) ")"))
(def alphabetical-list
(string/join (map toc-entry sorted-names) "&nbsp;&nbsp;&nbsp; "))
(def topics {
"math" ["abs" "add" "angle" "atan/2" "between?" "ceil" "cos" "dec" "deg/rad" "deg/turn" "dist" "div" "div/0" "div/safe" "even?" "floor" "gt?" "gte?" "heading/vector" "inc" "inv" "inv/0" "inv/safe" "lt?" "lte?" "max" "min" "mod" "mod/0" "mod/safe" "mult" "neg" "neg?" "odd?" "pi" "pos?" "pow" "rad/deg" "rad/turn" "random" "random_int" "range" "round" "sin" "sqrt" "sqrt/safe" "square" "sub" "sum_of_squares" "tan" "tau" "to_number" "turn/deg" "turn/rad" "zero?"]
"bools" ["and" "bool" "bool?" "false?" "not" "or" "true?"]
"dicts" ["any?" "assoc" "coll?" "count" "dict" "dict?" "diff" "dissoc" "empty?" "get" "has?" "keys" "random" "update" "values"]
"lists" ["any?" "append" "at" "butlast" "coll?" "concat" "count" "each!" "empty?" "filter" "first" "fold" "index_of" "indexed?" "indices_of" "join" "keep" "last" "list" "list?" "map" "random" "range" "rest" "second" "sentence" "slice"]
"llists" ["car" "cdr" "cons" "llist"]
# "sets" ["any?" "append" "coll?" "concat" "contains?" "count" "empty?" "omit" "random" "set" "set?"]
"tuples" ["any?" "at" "coll?" "count" "empty?" "first" "last" "ordered?" "rest" "second" "tuple?"]
"strings" ["any?" "at" "chars" "chars/safe" "concat" "count" "downcase" "empty?" "join" "sentence" "show" "slice" "slice_n" "split" "string" "string?" "strip" "to_number" "trim" "upcase" "words"]
"types and values" ["bool?" "box?" "coll?" "dict?" "eq?" "fn?" "indexed?" "keyword?" "list?" "nil?" "number?" "set?" "show" "some" "some?" "string?" "tuple?" "type"]
"boxes" ["box?" "unbox" "store!" "update!"]
"results" ["err" "err?" "ok" "ok?" "unwrap!" "unwrap_or"]
"errors" ["assert!"]
"turtle graphics" ["back!" "background!" "bk!" "clear!" "colors" "fd!" "forward!" "goto!" "heading" "heading/vector" "hideturtle!" "home!" "left!" "loadstate!" "lt!" "pc!" "pd!" "pencolor" "pencolour" "pencolor!" "pencolour!" "pendown!" "pendown?" "penup!" "penwidth" "penwidth!" "position" "pu!" "pw!" "render_turtle!" "reset_turtle!" "right!" "rt!" "setheading!" "showturtle!" "spawn_turtle" "turtle_state"]
"environment and i/o" ["console" "doc!" "fetch_inbox" "fetch_outbox" "input" "key_down?" "keys_down" "print!" "read_input" "report!"]
"processes" ["alive?" "await" "fledge" "flush" "heed" "hibernate!" "monitor" "self" "send!" "sleep!" "spawn" "unlink!" "yield!"]
})
(defn capitalize [str]
(def fst (slice str 0 1))
(def rest (slice str 1))
(def init_cap (string/ascii-upper fst))
(def lower_rest (string/ascii-lower rest))
(string init_cap lower_rest))
(defn topic-entry [topic]
(string "### " (capitalize topic) "\n"
(as-> topic _ (topics _) (array/slice _) (sort _) (map toc-entry _)
(string/join _ "&nbsp;&nbsp;&nbsp; "))
"\n"))
(def by-topic (let [the-topics (-> topics keys sort)
topics-entries (map topic-entry the-topics)]
(string/join topics-entries "\n")))
(defn compose-entry [name]
(def header (string "\n### " name "\n"))
(def the-doc (get with-docs name))
(when (= "No documentation available." the-doc)
(break (string header the-doc "\n")))
(def lines (string/split "\n" the-doc))
(def description (last lines))
(def patterns (string/join (slice lines 1 (-> lines length dec)) "\n"))
(def backto "[Back to top.](#ludus-prelude-documentation)\n")
(string header description "\n```\n" patterns "\n```\n" backto))
(compose-entry "update")
(def entries (string/join (map compose-entry sorted-names) "\n---"))
(def doc-file (string
```
# Ludus prelude documentation
These functions are available in every Ludus script.
The documentation for any function can be found within Ludus by passing the function to `doc!`,
e.g., running `doc! (add)` will send the documentation for `add` to the console.
For more information on the syntax & semantics of the Ludus language, see [language.md](./language.md).
The prelude itself is just a Ludus file, which you can see at [prelude.ld](../assets/prelude.ld).
## A few notes
**Naming conventions.** Functions whose name ends with a question mark, e.g., `eq?`, return booleans.
Functions whose name ends with an exclamation point, e.g., `make!`, change state in some way.
In other words, they _do things_ rather than _calculating values_.
Functions whose name includes a slash either convert from one value to another, e.g. `deg/rad`,
or they are variations on a function, e.g. `div/0` as a variation on `div`.
**How entries are formatted.** Each entry has a brief (sometimes too brief!) description of what it does.
It is followed by the patterns for each of its function clauses.
This should be enough to indicate order of arguments, types, and so on.
**Patterns often, but do not always, indicate types.** Typed patterns are written as `foo as :bar`,
where the type is indicated by the keyword.
Possible ludus types are: `:nil`, `:boolean`, `:number`, `:keyword` (atomic values);
`:string` (strings are their own beast); `:tuple` and `:list` (ordered collections), `:set`s, and `:dict`ionaries (the other collection types); `:pkg` (packages, which are quasi-collections); `:fn` (functions); and `:box`es.
**Conventional types.** Ludus has two types based on conventions.
* _Result tuples._ Results are a way of modeling the result of a calculation that might fail.
The two possible values are `(:ok, value)` and `(:err, msg)`.
`msg` is usually a string describing what went wrong.
To work with result tuples, see [`unwrap!`](#unwrap) and [`unwrap_or`](#unwrap_or).
That said, usually you work with these using pattern matching.
* _Vectors._ Vectors are 2-element tuples of x and y coordinates.
The origin is `(0, 0)`.
Many math functions take vectors as well as numbers, e.g., `add` and `mult`.
You will see vectors indicated in patterns by an `(x, y)` tuple.
You can see what this looks like in the last clause of `add`: `((x1, y1), (x2, y2))`.
## Functions by topic
```
by-topic
```
## All functions, alphabetically
```
alphabetical-list
```
## Function documentation
```
entries
))
(spit "prelude.md" doc-file)

View File

@ -1,140 +0,0 @@
(import /janet/base :as b)
(defn- get-line [source line]
((string/split "\n" source) (dec line)))
(defn- caret [source line start]
(def lines (string/split "\n" source))
(def the-line (lines (dec line)))
(def prev-lines (slice lines 0 (dec line)))
(def char-counts (map (fn [x] (-> x length inc)) prev-lines))
(def prev-line-chars (sum char-counts))
(def offset (- start prev-line-chars))
(def indent (string/repeat "." (+ 6 offset)))
(string indent "^")
)
(defn scan-error [e]
(def {:line line-num :input input :source source :start start :msg msg} e)
(print "Syntax error: " msg)
(print " on line " line-num " in " input ":")
(def source-line (get-line source line-num))
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn parse-error [e]
(def msg (e :msg))
(def {:line line-num :input input :source source :start start} (e :token))
(def source-line (get-line source line-num))
(print "Syntax error: " msg)
(print " on line " line-num " in " input ":")
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn validation-error [e]
(def msg (e :msg))
(def {:line line-num :input input :source source :start start} (get-in e [:node :token]))
(def source-line (get-line source line-num))
(case msg
"unbound name"
(do
(print "Validation error: " msg " " (get-in e [:node :data]))
(print " on line " line-num " in " input ":")
(print " >>> " source-line)
(print (caret source line-num start)))
(do
(print "Validation error: " msg)
(print " on line " line-num " in " input ":")
(print " >>> " source-line)
(print (caret source line-num start))))
e)
(defn- fn-no-match [e]
(print "Ludus panicked! no match")
(def {:line line-num :source source :input input :start start} (get-in e [:node :token]))
(def source-line (get-line source line-num))
(print " on line " line-num " in " input ", ")
(def called (e :called))
(print " calling: " (slice (b/show called) 3))
(def value (e :value))
(print " with arguments: " (b/show value))
(print " expected match with one of:")
(def patterns (b/pretty-patterns called))
(def fmt-patt (do
(def lines (string/split "\n" patterns))
(def indented (map (fn [x] (string " " x)) lines))
(string/join indented "\n")
))
(print fmt-patt)
(print " >>> " source-line)
(print (caret source line-num start))
)
(defn- let-no-match [e]
(print "Ludus panicked! no match")
(def {:line line-num :source source :input input :start start} (get-in e [:node :token]))
(def source-line (get-line source line-num))
(print " on line " line-num " in " input ", ")
(print " matching: " (b/show (e :value)))
(def pattern (get-in e [:node :data 0]))
(print " with pattern: " (b/show-patt pattern))
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn- match-no-match [e]
(print "Ludus panicked! no match")
(def {:line line-num :source source :input input :start start} (get-in e [:node :token]))
(print " on line " line-num " in " input ", ")
(def value (e :value))
(print " matching: " (b/show value))
(print " with patterns:")
(def clauses (get-in e [:node :data 1]))
(def patterns (b/pretty-patterns {:body clauses}))
(def fmt-patt (do
(def lines (string/split "\n" patterns))
(def indented (map (fn [x] (string " " x)) lines))
(string/join indented "\n")
))
(print fmt-patt)
(def source-line (get-line source line-num))
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn- generic-panic [e]
(def msg (e :msg))
(def {:line line-num :source source :input input :start start} (get-in e [:node :token]))
(def source-line (get-line source line-num))
(print "Ludus panicked! " msg)
(print " on line " line-num " in " input ":")
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn- unbound-name [e]
(def {:line line-num :source source :lexeme name :input input :start start} (get-in e [:node :token]))
(def source-line (get-line source line-num))
(print "Ludus panicked! unbound name " name)
(print " on line " line-num " in " input ":")
(print " >>> " source-line)
(print (caret source line-num start))
e)
(defn runtime-error [e]
(when (= :string (type e))
(print (string "Internal Ludus error: " e))
(print "Please file an issue at https://alea.ludus.dev/twc/ludus/issues")
(break e))
(def msg (e :msg))
(case msg
"no match: function call" (fn-no-match e)
"no match: let binding" (let-no-match e)
"no match: match form" (match-no-match e)
"no match: when form" (generic-panic e)
"unbound name" (unbound-name e)
(generic-panic e))
e)

View File

@ -1,657 +0,0 @@
# A tree walk interpreter for ludus
(import /janet/base :as b)
(var interpret nil)
(var match-pattern nil)
(defn- todo [msg] (error (string "not yet implemented: " msg)))
(defn- resolve-name [name ctx]
# # (print "resolving " name " in:")
# # (pp ctx)
(when (not ctx) (break :^not-found))
(if (has-key? ctx name)
(ctx name)
(resolve-name name (ctx :^parent))))
(defn- match-word [word value ctx]
(def name (word :data))
# # (print "matched " (b/show value) " to " name)
(set (ctx name) value)
{:success true :ctx ctx})
(defn- typed [pattern value ctx]
(def [type-ast word] (pattern :data))
(def type (type-ast :data))
(if (= type (b/ludus/type value))
(match-word word value ctx)
{:success false :miss [pattern value]}))
(defn- match-tuple [pattern value ctx]
(when (not (tuple? value))
(break {:success false :miss [pattern value]}))
(def val-len (length value))
(var members (pattern :data))
(when (empty? members)
(break (if (empty? value)
{:success true :ctx ctx}
{:success false :miss [pattern value]})))
(def patt-len (length members))
(var splat nil)
(def splat? (= :splat ((last members) :type)))
(when splat?
(when (< val-len patt-len)
# (print "mismatched splatted tuple lengths")
(break {:success false :miss [pattern value]}))
# (print "splat!")
(set splat (last members))
(set members (slice members 0 (dec patt-len))))
(when (and (not splat?) (not= val-len patt-len))
# (print "mismatched tuple lengths")
(break {:success false :miss [pattern value]}))
(var curr-mem :^nothing)
(var curr-val :^nothing)
(var success true)
(for i 0 (length members)
(set curr-mem (get members i))
(set curr-val (get value i))
# (print "in tuple, matching " curr-val " with ")
# (pp curr-mem)
(def match? (match-pattern curr-mem curr-val ctx))
# (pp match?)
(when (not (match? :success))
(set success false)
(break)))
(when (and splat? (splat :data))
(def rest (array/slice value (length members)))
(match-word (splat :data) rest ctx))
(if success
{:success true :ctx ctx}
{:success false :miss [pattern value]}))
(defn- match-list [pattern value ctx]
(when (not (array? value))
(break {:success false :miss [pattern value]}))
(def val-len (length value))
(var members (pattern :data))
(when (empty? members)
(break (if (empty? value)
{:success true :ctx ctx}
{:success false :miss [pattern value]})))
(def patt-len (length members))
(var splat nil)
(def splat? (= :splat ((last members) :type)))
(when splat?
(when (< val-len patt-len)
# (print "mismatched splatted list lengths")
(break {:success false :miss [pattern value]}))
# (print "splat!")
(set splat (last members))
(set members (slice members 0 (dec patt-len))))
(when (and (not splat?) (not= val-len patt-len))
# (print "mismatched list lengths")
(break {:success false :miss [pattern value]}))
(var curr-mem :^nothing)
(var curr-val :^nothing)
(var success true)
(for i 0 (length members)
(set curr-mem (get members i))
(set curr-val (get value i))
# (print "in list, matching " curr-val " with ")
# (pp curr-mem)
(def match? (match-pattern curr-mem curr-val ctx))
# (pp match?)
(when (not (match? :success))
(set success false)
(break)))
(when (and splat? (splat :data))
(def rest (array/slice value (length members)))
(match-word (splat :data) rest ctx))
(if success
{:success true :ctx ctx}
{:success false :miss [pattern value]}))
(defn- match-string [pattern value ctx]
(when (not (string? value))
(break {:success false :miss [pattern value]}))
(def {:compiled compiled :bindings bindings} pattern)
# (print "matching " value " with")
# (pp (pattern :grammar))
(def matches (peg/match compiled value))
(when (not matches)
(break {:success false :miss [pattern value]}))
(when (not= (length matches) (length bindings))
(error "oops: different number of matches and bindings"))
(for i 0 (length matches)
(set (ctx (bindings i)) (matches i)))
{:success true :ctx ctx})
(defn- match-dict [pattern value ctx]
(when (not (table? value))
(break {:success false :miss [pattern value]}))
(def val-size (length value))
(var members (pattern :data))
(def patt-len (length members))
(when (empty? members)
(break (if (empty? value)
{:success true :ctx ctx}
{:success false :miss [pattern value]})))
(var splat nil)
(def splat? (= :splat ((last members) :type)))
(when splat?
(when (< val-size patt-len)
# (print "mismatched splatted dict lengths")
(break {:success false :miss [pattern value]}))
# (print "splat!")
(set splat (last members))
(set members (slice members 0 (dec patt-len))))
(when (and (not splat?) (not= val-size patt-len))
# (print "mismatched dict lengths")
(break {:success false :miss [pattern value]}))
(var success true)
(def matched-keys @[])
(for i 0 (length members)
(def curr-pair (get members i))
(def [curr-key curr-patt] (curr-pair :data))
(def key (interpret curr-key ctx))
(def curr-val (value key))
(def match? (match-pattern curr-patt curr-val ctx))
(array/push matched-keys key)
(when (not (match? :success))
(set success false)
(break)))
(when (and splat? (splat :data) success)
(def rest (merge value))
(each key matched-keys
(set (rest key) nil))
(match-word (splat :data) rest ctx))
(if success
{:success true :ctx ctx}
{:success false :miss [pattern value]}))
(defn- match-pattern* [pattern value &opt ctx]
# (print "in match-pattern, matching " value " with:")
# (pp pattern)
(default ctx @{})
(def data (pattern :data))
(case (pattern :type)
# always match
:placeholder {:success true :ctx ctx}
:ignored {:success true :ctx ctx}
:word (match-word pattern value ctx)
# match on equality
:nil {:success (= :^nil value) :ctx ctx}
:bool {:success (= data value) :ctx ctx}
:number {:success (= data value) :ctx ctx}
:string {:success (= data value) :ctx ctx}
:keyword {:success (= data value) :ctx ctx}
# TODO: lists, dicts
:tuple (match-tuple pattern value ctx)
:list (match-list pattern value ctx)
:dict (match-dict pattern value ctx)
:interpolated (match-string pattern value ctx)
:typed (typed pattern value ctx)
))
(set match-pattern match-pattern*)
(defn- lett [ast ctx]
# (print "lett!")
# (pp ast)
(def [patt expr] (ast :data))
(def value (interpret expr ctx))
(def match? (match-pattern patt value))
(if (match? :success)
(do
(merge-into ctx (match? :ctx))
value)
(error {:node ast :value value :msg "no match: let binding"})))
(defn- matchh [ast ctx]
(def [to-match clauses] (ast :data))
(def value (interpret to-match ctx))
(def len (length clauses))
(when (ast :match) (break ((ast :match) 0 value ctx)))
(defn match-fn [i value ctx]
(when (= len i)
(error {:node ast :value value :msg "no match: match form"}))
(def clause (clauses i))
(def [patt guard expr] clause)
(def match? (match-pattern patt value @{:^parent ctx}))
(when (not (match? :success))
(break (match-fn (inc i) value ctx)))
(def body-ctx (match? :ctx))
(def guard? (if guard
(b/bool (interpret guard body-ctx)) true))
(when (not guard?)
(break (match-fn (inc i) value ctx)))
(interpret expr body-ctx))
(set (ast :match) match-fn)
(match-fn 0 value ctx))
(defn- script [ast ctx]
(def lines (ast :data))
(def last-line (last lines))
(for i 0 (-> lines length dec)
(interpret (lines i) ctx))
(interpret last-line ctx))
(defn- block [ast parent]
(def lines (ast :data))
(def last-line (last lines))
(def ctx @{:^parent parent})
(for i 0 (-> lines length dec)
(interpret (lines i) ctx))
(interpret last-line ctx))
(defn- to_string [ctx] (fn [x]
(if (buffer? x)
(string x)
(b/stringify (interpret x ctx)))))
(defn- interpolated [ast ctx]
(def terms (ast :data))
(def interpolations (map (to_string ctx) terms))
(string/join interpolations))
(defn- iff [ast ctx]
(def [condition then else] (ast :data))
(if (b/bool (interpret condition ctx))
(interpret then ctx)
(interpret else ctx)))
# TODO: use a tail call here
(defn- whenn [ast ctx]
(def clauses (ast :data))
(var result :^nothing)
(each clause clauses
(def [lhs rhs] clause)
(when (b/bool (interpret lhs ctx))
(set result (interpret rhs ctx))
(break)))
(when (= result :^nothing)
(error {:node ast :msg "no match: when form"}))
result)
(defn- word [ast ctx]
(def resolved (resolve-name (ast :data) ctx))
(if (= :^not-found resolved)
(error {:node ast :msg "unbound name"})
resolved))
(defn- tup [ast ctx]
(def members (ast :data))
(def the-tup @[])
(each member members
(array/push the-tup (interpret member ctx)))
[;the-tup])
(defn- args [ast ctx]
(def members (ast :data))
(def the-args @[])
(each member members
(array/push the-args (interpret member ctx)))
(if (ast :partial)
{:^type :partial :args the-args}
[;the-args]))
(defn- sett [ast ctx]
(def members (ast :data))
(def the-set @{:^type :set})
(each member members
(def value (interpret member ctx))
(set (the-set value) true))
the-set)
(defn- list [ast ctx]
(def members (ast :data))
(def the-list @[])
(each member members
(if (= :splat (member :type))
(do
(def splatted (interpret (member :data) ctx))
(when (not= :array (type splatted))
(error {:node member :msg "cannot splat non-list into list"}))
(array/concat the-list splatted))
(array/push the-list (interpret member ctx))))
the-list)
(defn- dict [ast ctx]
(def members (ast :data))
(def the-dict @{})
(each member members
(if (= :splat (member :type))
(do
(def splatted (interpret (member :data) ctx))
(when (or
(not= :table (type splatted))
(:^type splatted))
(error {:node member :msg "cannot splat non-dict into dict"}))
(merge-into the-dict splatted))
(do
(def [key-ast value-ast] (member :data))
# (print "dict key")
# (pp key-ast)
# (print "dict value")
# (pp value-ast)
(def key (interpret key-ast ctx))
(def value (interpret value-ast ctx))
(set (the-dict key) value))))
the-dict)
(defn- box [ast ctx]
(def {:data value-ast :name name} ast)
(def value (interpret value-ast ctx))
(def box @{:^type :box :^value value :name name})
(set (ctx name) box)
box)
(defn- repeatt [ast ctx]
(def [times-ast body] (ast :data))
(def times (interpret times-ast ctx))
(when (not (number? times))
(error {:node times-ast :msg (string "repeat needs a `number` of times; you gave me a " (type times))}))
(repeat times (interpret body ctx)))
(defn- panic [ast ctx]
(def info (interpret (ast :data) ctx))
(error {:node ast :msg info}))
# TODO: add docstrings & pattern docs to fns
# Depends on: good string representation of patterns
# For now, this should be enough to tall the thing
(defn- fnn [ast ctx]
(def {:name name :data clauses :doc doc} ast)
# (print "defining fn " name)
(def closure (merge ctx))
(def the-fn @{:name name :^type :fn :body clauses :ctx closure :doc doc})
(when (not= :^not-found (resolve-name name ctx))
# (print "fn "name" was forward declared")
(def fwd (resolve-name name ctx))
(set (fwd :body) clauses)
(set (fwd :ctx) closure)
(set (fwd :doc) doc)
# (print "fn " name " has been defined")
# (pp fwd)
(break fwd))
# (pp the-fn)
(set (closure name) the-fn)
(set (ctx name) the-fn)
the-fn)
(defn- is_placeholder [x] (= x :_))
(var call-fn nil)
(defn- partial [root-ast the-fn partial-args]
(when (the-fn :applied)
(error {:msg "cannot partially apply a partially applied function"
:node root-ast :called the-fn :args partial-args}))
# (print "calling partially applied function")
(def args (partial-args :args))
# (pp args)
(def pos (find-index is_placeholder args))
(def name (string (the-fn :name) " *partial*"))
(defn partial-fn [root-ast missing]
# (print "calling function with arg " (b/show missing))
# (pp partial-args)
(def full-args (array/slice args))
(set (full-args pos) missing)
# (print "all args: " (b/show full-args))
(call-fn root-ast the-fn [;full-args]))
{:^type :fn :applied true :name name :body partial-fn})
(defn- call-fn* [root-ast the-fn args]
# (print "on line " (get-in root-ast [:token :line]))
# (print "calling " (b/show the-fn))
# (print "with args " (b/show args))
# (pp args)
(when (or
(= :function (type the-fn))
(= :cfunction (type the-fn)))
# (print "Janet function")
(break (the-fn ;args)))
(def clauses (the-fn :body))
(when (= :nothing clauses)
(error {:node root-ast :called the-fn :value args :msg "cannot call function before it is defined"}))
(when (= :function (type clauses))
(break (clauses root-ast ;args)))
(def len (length clauses))
(when (the-fn :match) (break ((the-fn :match) root-ast 0 args)))
(defn match-fn [root-ast i args]
(when (= len i)
(error {:node root-ast :called the-fn :value args :msg "no match: function call"}))
(def clause (clauses i))
(def [patt guard expr] clause)
(def match?
(match-pattern patt args @{:^parent (the-fn :ctx)}))
(when (not (match? :success))
(break (match-fn root-ast (inc i) args)))
# (print "matched!")
(def body-ctx (match? :ctx))
(def guard? (if guard
(b/bool (interpret guard body-ctx)) true))
# (print "passed guard")
(when (not guard?)
(break (match-fn root-ast (inc i) args)))
(interpret expr body-ctx))
(set (the-fn :match) match-fn)
(match-fn root-ast 0 args))
(set call-fn call-fn*)
(defn- call-partial [root-ast the-fn arg] ((the-fn :body) root-ast ;arg))
(defn- apply-synth-term [root-ast prev curr]
# (print "applying " (b/show prev))
# (print "to" (b/show curr))
(def types [(b/ludus/type prev) (b/ludus/type curr)])
# (print "typle:")
# (pp types)
(match types
[:fn :tuple] (call-fn root-ast prev curr)
[:fn :partial] (partial root-ast prev curr)
[:function :tuple] (call-fn root-ast prev curr)
# [:applied :tuple] (call-partial root-ast prev curr)
[:keyword :args] (get (first curr) prev :^nil)
[:keyword :tuple] (get (first curr) prev :^nil)
[:dict :keyword] (get prev curr :^nil)
[:nil :keyword] :^nil
[:pkg :keyword] (get prev curr :^nil)
[:pkg :pkg-kw] (get prev curr :^nil)
(error (string "cannot call " (b/ludus/type prev) " `" (b/show prev) "`"))))
(defn- synthetic [ast ctx]
(def terms (ast :data))
# (print "interpreting synthetic")
# (pp ast)
# (pp terms)
(def first-term (first terms))
(def last-term (last terms))
(var prev (interpret first-term ctx))
# (print "root term: ")
# (pp prev)
(for i 1 (-> terms length dec)
(def curr (interpret (terms i) ctx))
# (print "term " i ": " curr)
(set prev (apply-synth-term first-term prev curr)))
# (print "done with inner terms, applying last term")
(apply-synth-term first-term prev (interpret last-term ctx)))
(defn- doo [ast ctx]
(def terms (ast :data))
(var prev (interpret (first terms) ctx))
(def last-term (last terms))
(for i 1 (-> terms length dec)
(def curr (interpret (terms i) ctx))
(set prev (apply-synth-term (first terms) curr [prev])))
(def last-fn (interpret last-term ctx))
(apply-synth-term (first terms) last-fn [prev]))
(defn- pkg [ast ctx]
(def members (ast :data))
(def the-pkg @{:^name (ast :name) :^type :pkg})
(each member members
(def [key-ast value-ast] (member :data))
(def key (interpret key-ast ctx))
(def value (interpret value-ast ctx))
(set (the-pkg key) value))
# (pp the-pkg)
(def out (table/to-struct the-pkg))
(set (ctx (ast :name)) out)
out)
(defn- loopp [ast ctx]
# (print "looping!")
(def data (ast :data))
(def args (interpret (data 0) ctx))
# this doesn't work: context persists between different interpretations
# we want functions to work this way, but not loops (I think)
# (when (ast :match) (break ((ast :match) 0 args)))
(def clauses (data 1))
(def len (length clauses))
(var loop-ctx @{:^parent ctx})
(defn match-fn [i args]
(when (= len i)
(error {:node ast :value args :msg "no match: loop"}))
(def clause (clauses i))
(def [patt guard expr] clause)
(def match?
(match-pattern patt args loop-ctx))
(when (not (match? :success))
# (print "no match")
(break (match-fn (inc i) args)))
# (print "matched!")
(def body-ctx (match? :ctx))
(def guard? (if guard
(b/bool (interpret guard body-ctx)) true))
# (print "passed guard")
(when (not guard?)
(break (match-fn (inc i) args)))
(interpret expr body-ctx))
(set (ast :match) match-fn)
(set (loop-ctx :^recur) match-fn)
# (print "ATTACHED MATCH-FN")
(match-fn 0 args))
(defn- recur [ast ctx]
# (print "recurring!")
(def passed (ast :data))
(def args (interpret passed ctx))
(def match-fn (resolve-name :^recur ctx))
# (print "match fn in ctx:")
# (pp (ctx :^recur))
# (pp match-fn)
# (pp ctx)
(match-fn 0 args))
# TODO for 0.1.0
(defn- testt [ast ctx] (todo "test"))
(defn- ns [ast ctx] (todo "nses"))
(defn- importt [ast ctx] (todo "imports"))
(defn- withh [ast ctx] (todo "with"))
(defn- usee [ast ctx] (todo "use"))
(defn- interpret* [ast ctx]
# (print "interpreting node " (ast :type))
(case (ast :type)
# literals
:nil :^nil
:number (ast :data)
:bool (ast :data)
:string (ast :data)
:keyword (ast :data)
:placeholder :_
# collections
:tuple (tup ast ctx)
:args (args ast ctx)
:list (list ast ctx)
:set (sett ast ctx)
:dict (dict ast ctx)
# composite forms
:if (iff ast ctx)
:block (block ast ctx)
:when (whenn ast ctx)
:script (script ast ctx)
:panic (panic ast ctx)
# looping forms
:loop (loopp ast ctx)
:recur (recur ast ctx)
:repeat (repeatt ast ctx)
# named/naming forms
:word (word ast ctx)
:interpolated (interpolated ast ctx)
:box (box ast ctx)
:pkg (pkg ast ctx)
:pkg-name (word ast ctx)
# patterned forms
:let (lett ast ctx)
:match (matchh ast ctx)
# functions
:fn (fnn ast ctx)
# synthetic
:synthetic (synthetic ast ctx)
# do
:do (doo ast ctx)
# deferred until after computer class
# :with (withh ast ctx)
# :import (importt ast ctx)
# :ns (ns ast ctx)
# :use (usee ast ctx)
# :test (testt ast ctx)
))
(set interpret interpret*)
# # repl
# (import /src/scanner :as s)
# (import /src/parser :as p)
# (import /src/validate :as v)
# (var source nil)
# (defn- has-errors? [{:errors errors}] (and errors (not (empty? errors))))
# (defn run []
# (def scanned (s/scan source))
# (when (has-errors? scanned) (break (scanned :errors)))
# (def parsed (p/parse scanned))
# (when (has-errors? parsed) (break (parsed :errors)))
# (def validated (v/valid parsed b/ctx))
# # (when (has-errors? validated) (break (validated :errors)))
# # (def cleaned (get-in parsed [:ast :data 1]))
# # # (pp cleaned)
# (interpret (parsed :ast) @{:^parent b/lett})
# # (try (interpret (parsed :ast) @{:^parent b/ctx})
# # ([e] (if (struct? e) (error (e :msg)) (error e))))
# )
# # (do
# (comment
# (set source `
# let foo = 42
# "{foo} bar baz"
# `)
# (def result (run))
# )

View File

@ -1,131 +0,0 @@
# pulled from cfiggers/jayson
(defmacro- letv [bindings & body]
~(do ,;(seq [[k v] :in (partition 2 bindings)] ['var k v]) ,;body))
(defn- read-hex [n]
(scan-number (string "0x" n)))
(defn- check-utf-16 [capture]
(let [u (read-hex capture)]
(if (and (>= u 0xD800)
(<= u 0xDBFF))
capture
false)))
(def- utf-8->bytes
(peg/compile
~{:double-u-esc (/ (* "\\u" (cmt (<- 4) ,|(check-utf-16 $)) "\\u" (<- 4))
,|(+ (blshift (- (read-hex $0) 0xD800) 10)
(- (read-hex $1) 0xDC00) 0x10000))
:single-u-esc (/ (* "\\u" (<- 4)) ,|(read-hex $))
:unicode-esc (/ (+ :double-u-esc :single-u-esc)
,|(string/from-bytes
;(cond
(<= $ 0x7f) [$]
(<= $ 0x7ff)
[(bor (band (brshift $ 6) 0x1F) 0xC0)
(bor (band (brshift $ 0) 0x3F) 0x80)]
(<= $ 0xffff)
[(bor (band (brshift $ 12) 0x0F) 0xE0)
(bor (band (brshift $ 6) 0x3F) 0x80)
(bor (band (brshift $ 0) 0x3F) 0x80)]
# Otherwise
[(bor (band (brshift $ 18) 0x07) 0xF0)
(bor (band (brshift $ 12) 0x3F) 0x80)
(bor (band (brshift $ 6) 0x3F) 0x80)
(bor (band (brshift $ 0) 0x3F) 0x80)])))
:escape (/ (* "\\" (<- (set "avbnfrt\"\\/")))
,|(get {"a" "\a" "v" "\v" "b" "\b"
"n" "\n" "f" "\f" "r" "\r"
"t" "\t"} $ $))
:main (+ (some (+ :unicode-esc :escape (<- 1))) -1)}))
(defn decode
``
Returns a janet object after parsing JSON. If `keywords` is truthy,
string keys will be converted to keywords. If `nils` is truthy, `null`
will become `nil` instead of the keyword `:json/null`.
``
[json-source &opt keywords nils]
(def json-parser
{:null (if nils
~(/ (<- (+ "null" "Null")) nil)
~(/ (<- (+ "null" "Null")) :json/null))
:bool-t ~(/ (<- (+ "true")) true)
:bool-f ~(/ (<- (+ "false")) false)
:number ~(/ (<- (* (? "-") :d+ (? (* "." :d+)))) ,|(scan-number $))
:string ~(/ (* "\"" (<- (to (* (> -1 (not "\\")) "\"")))
(* (> -1 (not "\\")) "\""))
,|(string/join (peg/match utf-8->bytes $)))
:array ~(/ (* "[" :s* (? (* :value (any (* :s* "," :value)))) "]") ,|(array ;$&))
:key-value (if keywords
~(* :s* (/ :string ,|(keyword $)) :s* ":" :value)
~(* :s* :string :s* ":" :value))
:object ~(/ (* "{" :s* (? (* :key-value (any (* :s* "," :key-value)))) "}")
,|(from-pairs (partition 2 $&)))
:value ~(* :s* (+ :null :bool-t :bool-f :number :string :array :object) :s*)
:unmatched ~(/ (<- (to (+ :value -1))) ,|[:unmatched $])
:main ~(some (+ :value "\n" :unmatched))})
(first (peg/match (peg/compile json-parser) json-source)))
(def- bytes->utf-8
(peg/compile
~{:four-byte (/ (* (<- (range "\xf0\xff")) (<- 1) (<- 1) (<- 1))
,|(bor (blshift (band (first $0) 0x07) 18)
(blshift (band (first $1) 0x3F) 12)
(blshift (band (first $2) 0x3F) 6)
(blshift (band (first $3) 0x3F) 0)))
:three-byte (/ (* (<- (range "\xe0\xef")) (<- 1) (<- 1))
,|(bor (blshift (band (first $0) 0x0F) 12)
(blshift (band (first $1) 0x3F) 6)
(blshift (band (first $2) 0x3F) 0)))
:two-byte (/ (* (<- (range "\x80\xdf")) (<- 1))
,|(bor (blshift (band (first $0) 0x1F) 6)
(blshift (band (first $1) 0x3F) 0)))
:multi-byte (/ (+ :two-byte :three-byte :four-byte)
,|(if (< $ 0x10000)
(string/format "\\u%04X" $)
(string/format "\\u%04X\\u%04X"
(+ (brshift (- $ 0x10000) 10) 0xD800)
(+ (band (- $ 0x10000) 0x3FF) 0xDC00))))
:one-byte (<- (range "\x20\x7f"))
:0to31 (/ (<- (range "\0\x1F"))
,|(or ({"\a" "\\u0007" "\b" "\\u0008"
"\t" "\\u0009" "\n" "\\u000A"
"\v" "\\u000B" "\f" "\\u000C"
"\r" "\\u000D"} $)
(string/format "\\u%04X" (first $))))
:backslash (/ (<- "\\") "\\\\")
:quote (/ (<- "\"") "\\\"")
:main (+ (some (+ :0to31 :backslash :quote :one-byte :multi-byte)) -1)}))
(defn- encodeone [x depth]
(if (> depth 1024) (error "recurred too deeply"))
(cond
(= x :json/null) "null"
(= x nil) "null"
(bytes? x) (string "\"" (string/join (peg/match bytes->utf-8 x)) "\"")
(indexed? x) (string "[" (string/join (map |(encodeone $ (inc depth)) x) ",") "]")
(dictionary? x) (string "{" (string/join
(seq [[k v] :in (pairs x)]
(string "\"" (string/join (peg/match bytes->utf-8 k)) "\"" ":" (encodeone v (inc depth)))) ",") "}")
(case (type x)
:nil "null"
:boolean (string x)
:number (string x)
(error "type not supported"))))
(defn encode
``
Encodes a janet value in JSON (utf-8). If `buf` is provided, the formated
JSON is append to `buf` instead of a new buffer. Returns the modifed buffer.
``
[x &opt buf]
(letv [ret (encodeone x 0)]
(if (and buf (buffer? buf))
(buffer/push ret)
(thaw ret))))

View File

@ -1,110 +0,0 @@
# an integrated Ludus interpreter
# devised in order to run under wasm
# takes a string, returns a string with a json object
# (try (os/cd "janet") ([_] nil)) # for REPL
(import /janet/scanner :as s)
(import /janet/parser :as p)
(import /janet/validate :as v)
(import /janet/interpreter :as i)
(import /janet/errors :as e)
(import /janet/base :as b)
(import /janet/prelude :as prelude)
(import /janet/json :as j)
(defn ludus [source]
# if we can't load prelude, bail
(when (= :error prelude/pkg) (error "could not load prelude"))
# get us a clean working slate
(def ctx @{:^parent prelude/ctx})
(def errors @[])
(var result @"")
(def console @"")
# capture all `print`s
(setdyn :out console)
# an output table
# this will change: the shape of our output
# at the moment, there's only one stack of turtle graphics
# we will be getting more
(def out @{:errors errors :result result
:io @{
:stdout @{:proto [:text-stream "0.1.0"] :data console}
:turtle @{:proto [:turtle-graphics "0.1.0"] :data @[]}}})
### start the program
# first, scanning
(def scanned (s/scan source))
(when (any? (scanned :errors))
(each err (scanned :errors)
(e/scan-error err))
(break (-> out j/encode string)))
# then, parsing
(def parsed (p/parse scanned))
(when (any? (parsed :errors))
(each err (parsed :errors)
(e/parse-error err))
(break (-> out j/encode string)))
# then, validation
(def validated (v/valid parsed ctx))
(when (any? (validated :errors))
(each err (validated :errors)
(e/validation-error err))
(break (-> out j/encode string)))
# and, finally, try interpreting the program
(try (do
# we need to do this every run or we get the very same sequence of "random" numbers every time we run a program
(math/seedrandom (os/cryptorand 8))
(set result (i/interpret (parsed :ast) ctx)))
([err]
(e/runtime-error err)
(break (-> out j/encode string))))
# stop capturing output
(setdyn :out stdout)
# update our output table with our output
(set (out :result) (b/show result))
(set (((out :io) :turtle) :data) (get-in prelude/pkg [:turtle_commands :^value]))
# run the "postlude": any Ludus code that needs to run after each program
# right now this is just resetting the boxes that hold turtle commands and state
(try
(i/interpret prelude/post/ast ctx)
([err] (e/runtime-error err)))
# json-encode our output table, and convert it from a buffer to a string (which we require for playing nice with WASM/C)
(-> out j/encode string))
#### REPL
(comment
# (do
(def start (os/clock))
(def source `
fn fib {
(1) -> 1
(2) -> 1
(n) -> add (
fib (sub (n, 1))
fib (sub (n, 2))
)
}
fib (30)
`)
(def out (-> source
ludus
j/decode
))
(def end (os/clock))
(setdyn :out stdout)
(pp out)
(def console (out "console"))
(print console)
(def result (out "result"))
(print result)
(print (- end start))
)

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
(import /janet/base :as b)
(import /janet/scanner :as s)
(import /janet/parser :as p)
(import /janet/validate :as v)
(import /janet/interpreter :as i)
(import /janet/errors :as e)
(def pkg (do
(def pre-ctx @{:^parent {"base" b/base}})
(def pre-src (slurp "./assets/prelude.ld"))
(def pre-scanned (s/scan pre-src :prelude))
(def pre-parsed (p/parse pre-scanned))
(def parse-errors (pre-parsed :errors))
(when (any? parse-errors) (each err parse-errors (e/parse-error err)) (break :error))
# (def pre-validated (v/valid pre-parsed pre-ctx))
# (def validation-errors (pre-validated :errors))
# (when (any? validation-errors) (each err validation-errors (e/validation-error err)) (break :error))
(try
(i/interpret (pre-parsed :ast) pre-ctx)
([err] (e/runtime-error err) :error))))
(def ctx (do
(def ctx @{})
(each [k v] (pairs pkg)
(set (ctx (string k)) v))
(set (ctx "^name") nil)
(set (ctx "^type") nil)
ctx))
# (def post/src (slurp "postlude.ld"))
# (def post/ast (do
# (def post-ctx @{:^parent ctx})
# (def post-scanned (s/scan post/src :postlude))
# (def post-parsed (p/parse post-scanned))
# (def parse-errors (post-parsed :errors))
# (when (any? parse-errors) (each err parse-errors (e/parse-error err)) (break :error))
# # (def post-validated (v/valid post-parsed post-ctx))
# # (def validation-errors (post-validated :errors))
# # (when (any? validation-errors) (each err validation-errors (e/validation-error err)) (break :error))
# (post-parsed :ast)))

View File

@ -1,9 +0,0 @@
(declare-project
:dependencies [
{:url "https://github.com/ianthehenry/judge.git"
:tag "v2.8.1"}
{:url "https://github.com/janet-lang/spork"}
])
(declare-source
:source ["ludus.janet"])

View File

@ -1,356 +0,0 @@
(def reserved-words
"List of Ludus reserved words."
## see ludus-spec repo for more info
{
"as" :as ## impl
"box" :box
"do" :do ## impl
"else" :else ## impl
"false" :false ## impl -> literal word
"fn" :fn ## impl
"if" :if ## impl
# "import" :import ## impl
"let" :let ## impl
"loop" :loop ## impl
"match" :match ## impl
"nil" :nil ## impl -> literal word
# "ns" :ns ## impl
"panic!" :panic ## impl (should _not_ be a function)
# "pkg" :pkg
"receive" :receive
"recur" :recur ## impl
"repeat" :repeat ## impl
# "test" :test
"then" :then ## impl
"true" :true ## impl -> literal word
# "use" :use ## wip
"when" :when ## impl, replaces cond
"with" :with ## impl
})
(def literal-words {"true" true
"false" false
"nil" nil
})
(defn- new-scanner
"Creates a new scanner."
[source input]
@{:source source
:input input
:length (length source)
:errors @[]
:start 0
:current 0
:line 1
:tokens @[]})
(defn- at-end?
"Tests if a scanner is at end of input."
[scanner]
(>= (get scanner :current) (get scanner :length)))
(defn- current-char
"Gets the current character of the scanner."
[scanner]
(let [source (get scanner :source)
current (get scanner :current)
length (length source)]
(if (>= current length)
nil
(string/from-bytes (get source current)))))
(defn- advance
"Advances the scanner by a single character."
[scanner]
(update scanner :current inc))
(defn- next-char
"Gets the next character from the scanner."
[scanner]
(let [source (get scanner :source)
current (get scanner :current)
next (inc current)
length (length source)]
(if (>= next length)
nil
(string/from-bytes (get source next)))))
(defn- current-lexeme
[scanner]
(slice (get scanner :source) (get scanner :start) (get scanner :current)))
(defn- char-code [char] (get char 0))
(defn- char-in-range? [start end char]
(and char
(>= (char-code char) (char-code start))
(<= (char-code char) (char-code end))))
(defn- digit? [c]
(char-in-range? "0" "9" c))
(defn- nonzero-digit? [c]
(char-in-range? "1" "9" c))
## for now, use very basic ASCII charset in words
## TODO: research the implications of using the whole
## (defn- alpha? [c] (boolean (re-find #"\p{L}" (string c))))
(defn- alpha? [c]
(or (char-in-range? "a" "z" c) (char-in-range? "A" "Z" c)))
(defn- lower? [c] (char-in-range? "a" "z" c))
(defn- upper? [c] (char-in-range? "A" "Z" c))
## legal characters in words
(def word-chars {"_" true "?" true "!" true "*" true "/" true})
(defn- word-char? [c]
(or (alpha? c) (digit? c) (get word-chars c)))
(defn- whitespace? [c]
(or (= c " ") (= c "\t")))
(def terminators {
":" true
";" true
"\n" true
"{" true
"}" true
"(" true
")" true
"[" true
"]" true
"$" true
"#" true
"-" true
"=" true
"&" true
"," true
">" true
"\"" true})
(defn- terminates? [c]
(or (nil? c) (whitespace? c) (get terminators c)))
(defn- add-token
[scanner token-type &opt literal]
(update scanner :tokens array/push
{:type token-type
:lexeme (current-lexeme scanner)
:literal literal
:line (get scanner :line)
:start (get scanner :start)
:source (get scanner :source)
:input (get scanner :input)}))
## TODO: errors should also be in the vector of tokens
## The goal is to be able to be able to hand this to an LSP?
## Do we need a different structure
(defn- add-error [scanner msg]
(let [token {:type :error
:lexeme (current-lexeme scanner)
:literal nil
:line (get scanner :line)
:start (get scanner :start)
:source (get scanner :source)
:input (get scanner :input)
:msg msg}]
(-> scanner
(update :errors array/push token)
(update :tokens array/push token))))
(defn- add-keyword
[scanner]
(defn recur [scanner key]
(let [char (current-char scanner)]
(cond
(terminates? char) (add-token scanner :keyword (keyword key))
(word-char? char) (recur (advance scanner) (string key char))
:else (add-error scanner (string "Unexpected " char "after keyword :" key)))))
(recur scanner ""))
(defn- add-pkg-kw [scanner]
(defn recur [scanner key]
(let [char (current-char scanner)]
(cond
(terminates? char) (add-token scanner :pkg-kw (keyword key))
(word-char? char) (recur (advance scanner) (string key char))
:else (add-error scanner (string "Unexpected " char " after pkg keyword :" key)))))
(recur scanner ""))
(defn- read-literal [lit] (-> lit parse-all first))
### TODO: consider whether Janet's number rules are right for Ludus
(defn- add-number [char scanner]
(defn recur [scanner num float?]
(let [curr (current-char scanner)]
(cond
(= curr "_") (recur (advance scanner) num float?) ## consume underscores unharmed
(= curr ".") (if float?
(add-error scanner (string "Unexpected second decimal point after " num "."))
(recur (advance scanner) (buffer/push num curr) true))
(terminates? curr) (add-token scanner :number (read-literal num))
(digit? curr) (recur (advance scanner) (buffer/push num curr) float?)
:else (add-error scanner (string "Unexpected " curr " after number " num ".")))))
(recur scanner (buffer char) false))
(def escape {
"\"" "\""
"n" "\n"
"{" "{"
"t" "\t"
"r" "\r"
"\\" "\\"
})
(defn- add-string
[scanner]
(defn recur [scanner buff interpolate?]
(let [char (current-char scanner)]
(case char
"{" (recur (advance scanner) (buffer/push buff char) true)
# allow multiline strings
"\n" (recur (update (advance scanner) :line inc) (buffer/push buff char) interpolate?)
"\"" (add-token (advance scanner) (if interpolate? :interpolated :string) (string buff))
"\\" (let [next (next-char scanner)]
(recur
(advance (advance scanner))
(buffer/push buff (get escape next next))
interpolate?))
(if (at-end? scanner)
(add-error scanner "Unterminated string.")
(recur (advance scanner) (buffer/push buff char) interpolate?)))))
(recur scanner @"" false))
(defn- add-word
[char scanner]
(defn recur [scanner word]
(let [curr (current-char scanner)]
(cond
(terminates? curr) (add-token scanner
(get reserved-words (string word) :word)
(get literal-words (string word) :none))
(word-char? curr) (recur (advance scanner) (buffer/push word curr))
:else (add-error scanner (string "Unexpected " curr " after word " word ".")))))
(recur scanner (buffer char)))
(defn- add-pkg
[char scanner]
(defn recur [scanner pkg]
(let [curr (current-char scanner)]
(cond
(terminates? curr) (add-token scanner :pkg-name :none)
(word-char? curr) (recur (advance scanner) (buffer/push pkg curr))
:else (add-error scanner (string "unexpected " curr " after pkg name " pkg)))))
(recur scanner (buffer char)))
(defn- add-ignored
[scanner]
(defn recur [scanner ignored]
(let [char (current-char scanner)]
(cond
(terminates? char) (add-token scanner :ignored)
(word-char? char) (recur (advance scanner) (buffer/push ignored char))
:else (add-error scanner (string "Unexpected " char " after word " ignored ".")))))
(recur scanner @"_"))
(defn- add-comment [char scanner]
(defn recur [scanner comm]
(let [char (current-char scanner)]
(if (or (= "\n" char) (at-end? scanner))
scanner # for now, we don't do anything with comments; can be added later
(recur (advance scanner) (buffer/push comm char)))))
(recur scanner (buffer char)))
(defn- scan-token [scanner]
(let [char (current-char scanner)
scanner (advance scanner)
next (current-char scanner)]
(case char
## one-character tokens
## :break is a special zero-char token before closing braces
## it makes parsing much simpler
"(" (add-token scanner :lparen)
")" (add-token (add-token scanner :break) :rparen)
"{" (add-token scanner :lbrace)
"}" (add-token (add-token scanner :break) :rbrace)
"[" (add-token scanner :lbracket)
"]" (add-token (add-token scanner :break) :rbracket)
";" (add-token scanner :semicolon)
"," (add-token scanner :comma)
"\n" (add-token (update scanner :line inc) :newline)
"\\" (add-token scanner :backslash)
"=" (add-token scanner :equals)
">" (add-token scanner :pipeline)
## two-character tokens
## ->
"-" (cond
(= next ">") (add-token (advance scanner) :arrow)
(digit? next) (add-number char scanner)
:else (add-error scanner (string "Expected > or negative number after `-`. Got `" char next "`")))
## dict #{
"#" (if (= next "{")
(add-token (advance scanner) :startdict)
(add-error scanner (string "Expected beginning of dict: #{. Got " char next)))
## set ${
"$" (if (= next "{")
(add-token (advance scanner) :startset)
(add-error scanner (string "Expected beginning of set: ${. Got " char next)))
## placeholders
## there's a flat _, and then ignored words
"_" (cond
(terminates? next) (add-token scanner :placeholder)
(alpha? next) (add-ignored scanner)
:else (add-error scanner (string "Expected placeholder: _. Got " char next)))
## comments
## & starts an inline comment
"&" (add-comment char scanner)
## keywords
# XXX: make sure we want only lower-only keywords
":" (cond
(lower? next) (add-keyword scanner)
(upper? next) (add-pkg-kw scanner)
:else (add-error scanner (string "Expected keyword or pkg keyword. Got " char next)))
## splats
"." (let [after_next (current-char (advance scanner))]
(if (= ".." (string next after_next))
(add-token (advance scanner) :splat)
(add-error scanner (string "Expected splat: ... . Got " (string "." next after_next)))))
## strings
"\"" (add-string scanner)
## word matches
(cond
(whitespace? char) scanner ## for now just skip whitespace characters
(digit? char) (add-number char scanner)
(upper? char) (add-pkg char scanner)
(lower? char) (add-word char scanner)
:else (add-error scanner (string "Unexpected character: " char))))))
(defn- next-token [scanner]
(put scanner :start (get scanner :current)))
(defn scan [source &opt input]
(default input :input)
(defn recur [scanner]
(if (at-end? scanner)
(let [scanner (add-token (add-token scanner :break) :eof)]
{:tokens (get scanner :tokens)
:errors (get scanner :errors [])})
(recur (-> scanner (scan-token) (next-token)))))
(recur (new-scanner source input)))
# (comment
(do
(def source " -123 ")
(length ((scan source) :tokens)))

View File

@ -1,801 +0,0 @@
### A validator for a Ludus AST
(comment
Tracking here, before I start writing this code, the kinds of validation we're hoping to accomplish:
* [x] ensure called keywords are only called w/ one arg
* [x] first-level property access with pkg, e.g. `Foo :bar`--bar must be on Foo
- [x] accept pkg-kws
* [x] validate dict patterns
* [x] compile string-patterns
* [x] `loop` form arity checking
* [x] arity checking of explicit named function calls
* [x] flag tail calls
* [x] no re-bound names
* [x] no unbound names
* [x] no unbound names with `use` forms
* [x] recur in tail position in `loop` forms
* [x] recur not called outside of `loop` forms
* [x] splats come at the end of list, tuple, and dict patterns
Deferred until a later iteration of Ludus:
* [ ] no circular imports DEFERRED
* [ ] correct imports DEFERRED
* [ ] validate `with` forms
)
(def- package-registry @{})
# (try (os/cd "janet") ([_] nil))
(import ./scanner :as s)
(import ./parser :as p)
(defn- new-validator [parser]
(def ast (parser :ast))
@{:ast ast
:errors @[]
:ctx @{}
:status @{}}
)
(var validate nil)
(def terminals [:number :string :bool :nil :placeholder])
(def simple-colls [:list :tuple :set :args])
(defn- simple-coll [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(set (validator :ast) node)
(validate validator))
validator)
(defn- iff [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(set (validator :ast) node)
(validate validator))
validator)
(defn- script [validator]
(def ast (validator :ast))
(def data (ast :data))
(def status (validator :status))
(set (status :toplevel) true)
(each node data
(set (validator :ast) node)
(validate validator))
validator)
(defn- block [validator]
(def ast (validator :ast))
(def data (ast :data))
(when (= 0 (length data))
(array/push (validator :errors)
{:node ast :msg "blocks may not be empty"})
(break validator))
(def status (validator :status))
(set (status :toplevel) nil)
(def tail? (status :tail))
(set (status :tail) false)
(def parent (validator :ctx))
(def ctx @{:^parent parent})
(set (validator :ctx) ctx)
(for i 0 (-> data length dec)
(set (validator :ast) (data i))
(validate validator))
(set (status :tail) tail?)
(set (validator :ast) (last data))
(validate validator)
(set (validator :ctx) parent)
validator)
(defn- resolve-local [ctx name]
(get ctx name))
(defn- resolve-name [ctx name]
(when (nil? ctx) (break nil))
(def node (get ctx name))
(if node node (resolve-name (get ctx :^parent) name)))
(defn- resolve-name-in-script [ctx name]
(when (ctx :^toplevel) (break nil))
(def node (ctx name))
(if node node (resolve-name-in-script (ctx :^parent) name)))
(defn- word [validator]
(def ast (validator :ast))
(def name (ast :data))
(def ctx (validator :ctx))
(def resolved (resolve-name ctx name))
(when (not resolved)
(array/push (validator :errors)
{:node ast :msg "unbound name"}))
validator)
### patterns
(var pattern nil)
(defn- lett [validator]
(def ast (validator :ast))
(def [lhs rhs] (ast :data))
# evaluate the expression first
# otherwise lhs names will appear bound
(set (validator :ast) rhs)
(validate validator)
(set (validator :ast) lhs)
(pattern validator)
validator)
(defn- splattern [validator]
(def ast (validator :ast))
(def status (validator :status))
(when (not (status :last))
(array/push (validator :errors)
{:node ast :msg "splats may only come last in collection patterns"}))
(def data (ast :data))
(when data
(set (validator :ast) data)
(pattern validator))
validator)
(defn- simple-coll-pattern [validator]
(def ast (validator :ast))
(def data (ast :data))
(when (empty? data) (break validator))
(def status (validator :status))
(for i 0 (-> data length dec)
(set (validator :ast) (get data i))
(pattern validator))
(set (status :last) true)
(set (validator :ast) (last data))
(pattern validator)
(set (status :last) nil)
validator)
(defn- word-pattern [validator]
(def ast (validator :ast))
(def name (ast :data))
(def ctx (validator :ctx))
### XXX TODO: this resolution should ONLY be for userspace, NOT prelude
(def resolved (resolve-name-in-script ctx name))
(when resolved
(def {:line line :input input} resolved)
(array/push (validator :errors)
{:node ast :msg (string "name " name " is already bound on line "
line " of " input)}))
(set (ctx name) ast)
# (pp ctx)
validator)
(def types [
:nil
:bool
:number
:keyword
:string
:set
:tuple
:dict
:list
:fn
:box
:pkg
])
(defn typed [validator]
(def ast (validator :ast))
(def [kw-type word] (ast :data))
(def type (kw-type :data))
(when (not (has-value? types type))
(array/push (validator :errors)
{:node kw-type :msg "unknown type"}))
(set (validator :ast) word)
(pattern validator))
(defn- str-pattern [validator]
(def ast (validator :ast))
(def data (ast :data))
(def last-term (-> data array/pop string))
(def grammar @{})
(def bindings @[])
(var current 0)
(each node data
(when (not (buffer? node))
(set (validator :ast) node)
(pattern validator))
(if (buffer? node)
(set (grammar (keyword current)) (string node))
(do
(set (grammar (keyword current))
~(<- (to ,(keyword (inc current)))))
(array/push bindings (node :data))))
(set current (inc current)))
(set (grammar (keyword current)) ~(* ,last-term -1))
(def rules (map keyword (range (length grammar))))
(set (grammar :main) ~(* ,;rules))
(set (ast :grammar) grammar)
(set (ast :compiled) (peg/compile grammar))
(set (ast :bindings) bindings))
(defn- pair [validator]
(def ast (validator :ast))
(def [_ patt] (ast :data))
(set (validator :ast) patt)
(pattern validator))
(defn- pattern* [validator]
# (print "PATTERN*")
(def ast (validator :ast))
(def type (ast :type))
# (print "validating pattern " type)
(cond
(has-value? terminals type) validator
(case type
:word (word-pattern validator)
:placeholder validator
:ignored validator
:word (word-pattern validator)
:list (simple-coll-pattern validator)
:tuple (simple-coll-pattern validator)
:dict (simple-coll-pattern validator)
:splat (splattern validator)
:typed (typed validator)
:interpolated (str-pattern validator)
:pair (pair validator)
)))
(set pattern pattern*)
# XXX: ensure guard includes only allowable names
# XXX: what to include here? (cf Elixir)
(defn- guard [validator])
(defn- match-clauses [validator clauses]
# (print "validating clauses in match-clauses")
(each clause clauses
(def parent (validator :ctx))
(def ctx @{:^parent parent})
(set (validator :ctx) ctx)
(def [lhs guard rhs] clause)
(set (validator :ast) lhs)
(pattern validator)
# (pp (validator :ctx))
# (pp (validator :ctx))
(when guard
(set (validator :ast) guard)
(validate validator))
(set (validator :ast) rhs)
(validate validator)
(set (validator :ctx) parent)))
(defn- matchh [validator]
# (print "validating in matchh")
(def ast (validator :ast))
(def [to-match clauses] (ast :data))
# (print "validating expression:")
# (pp to-match)
(set (validator :ast) to-match)
(validate validator)
# (print "validating clauses")
(match-clauses validator clauses)
validator)
(defn- receive [validator]
(def ast (validator :ast))
(def [clauses] (ast :data))
(match-clauses validator clauses)
validator)
(defn- declare [validator fnn]
(def status (validator :status))
(def declared (get status :declared @{}))
(set (declared fnn) true)
(set (status :declared) declared)
# (print "declared function " (fnn :name))
# (pp declared)
validator)
(defn- define [validator fnn]
(def status (validator :status))
(def declared (get status :declared @{}))
(set (declared fnn) nil)
(set (status :declared) declared)
# (print "defined function " (fnn :name))
# (pp declared)
validator)
(defn- fnn [validator]
(def ast (validator :ast))
(def name (ast :name))
# (print "function name: " name)
(def status (validator :status))
(def tail? (status :tail))
(set (status :tail) true)
(when name
(def ctx (validator :ctx))
(def resolved (ctx name))
(when (and resolved (not= :nothing (resolved :data)))
(def {:line line :input input} (get-in ctx [name :token]))
(array/push (validator :errors)
{:node ast :msg (string "name is already bound on line " line " of " input)}))
(when (and resolved (= :nothing (resolved :data)))
(define validator resolved))
(set (ctx name) ast))
(def data (ast :data))
(when (= data :nothing)
(break (declare validator ast)))
(match-clauses validator data)
(set (status :tail) tail?)
(def rest-arities @{})
(def arities @{:rest rest-arities})
(each clause data
# (print "CLAUSE:")
# (pp clause)
(def patt (first clause))
(def params (patt :data))
(def arity (length params))
# (print "checking clause with arity " arity)
(def rest-param? (and (> arity 0) (= :splat ((last params) :type))))
(if rest-param?
(set (rest-arities arity) true)
(set (arities arity) true)))
# (pp arities)
(set (ast :arities) arities)
validator)
(defn- box [validator]
(def ast (validator :ast))
(def ctx (validator :ctx))
(def expr (ast :data))
(set (validator :ast) expr)
(validate validator)
(def name (ast :name))
(def resolved (ctx name))
(when resolved
(def {:line line :input input} (get-in ctx [name :token]))
(array/push (validator :errors)
{:node ast :msg (string "name is already bound on line " line " of " input)}))
(set (ctx name) ast)
validator)
(defn- interpolated [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(when (not (buffer? node))
(set (validator :ast) node)
(validate validator))))
### TODO:
# * [ ] ensure properties are on pkgs (if *only* pkgs from root)
(defn- pkg-root [validator]
# (print "validating pkg-root access")
(def ast (validator :ast))
(def ctx (validator :ctx))
(def terms (ast :data))
(def pkg-name ((first terms) :data))
(def the-pkg (resolve-name ctx pkg-name))
(when (not the-pkg)
(array/push (validator :errors)
{:node ast :msg "unbound pkg name"})
(break validator))
(def member (get terms 1))
(def accessed (case (member :type)
:keyword (get-in the-pkg [:pkg (member :data)])
:pkg-kw (get-in the-pkg [:pkg (member :data)])
:args (do
(array/push (validator :errors)
{:node member :msg "cannot call a pkg"}
(break validator)))))
(when (not accessed)
# (print "no member " (member :data) " on " pkg-name)
(array/push (validator :errors)
{:node member :msg "invalid pkg access"})
(break validator))
# TODO: validate nested pkg access
)
# (defn- tail-call [validator]
# (def ast (validator :ast))
# (when (ast :partial) (break validator))
# (def status (validator :status))
# (when (not (status :tail)) (break validator))
# (def data (ast :data))
# (def args (last data))
# (set (args :tail-call) true))
(defn- check-arity [validator]
# (print "CHECKING ARITY")
(def ast (validator :ast))
# (when (ast :partial) (break validator))
(def ctx (validator :ctx))
(def data (ast :data))
(def fn-word (first data))
# (pp fn-word)
(def the-fn (resolve-name ctx (fn-word :data)))
# (print "the called function: " the-fn)
# (pp the-fn)
(when (not the-fn) (break validator))
# (print "the function is not nil")
# (print "the function type is " (type the-fn))
(when (= :function (type the-fn)) (break validator))
(when (= :cfunction (type the-fn)) (break validator))
# (print "the function is not a janet fn")
# (print "fn type: " (the-fn :type))
(when (not= :fn (the-fn :type)) (break validator))
# (print "fn name: " (the-fn :name))
(def arities (the-fn :arities))
# when there aren't arities yet, break, since that means we're making a recursive function call
# TODO: enahnce this so that we can determine arities *before* all function bodies; this ensures arity-checking for self-recursive calls
(when (not arities) (break validator))
# (print "arities: ")
# (pp arities)
(def args (get data 1))
(def num-args (length (args :data)))
# (print "called with #args " num-args)
# (pp (get (validator :ctx) "bar"))
(when (has-key? arities num-args) (break validator))
# (print "arities: ")
# (pp arities)
(when (not arities) (break validator))
(def rest-arities (keys (arities :rest)))
(when (empty? rest-arities)
(array/push (validator :errors)
{:node ast :msg "wrong number of arguments"})
(break validator))
(def rest-min (min ;rest-arities))
(when (< num-args rest-min)
(array/push (validator :errors)
{:node ast :msg "wrong number of arguments"}))
validator)
(defn- kw-root [validator]
(def ast (validator :ast))
(def data (ast :data))
(def [_ args] data)
(when (not= :args (args :type))
(break (array/push (validator :errors)
{:node args :msg "called keyword expects an argument"})))
(when (not= 1 (length (args :data)))
(array/push (validator :errors)
{:node args :msg "called keywords take one argument"})))
(defn- synthetic [validator]
(def ast (validator :ast))
(def data (ast :data))
(def status (validator :status))
(def ftype ((first data) :type))
(def stype ((get data 1) :type))
(def ltype ((last data) :type))
(set (status :pkg-access?) nil)
(when (= ftype :pkg-name)
(set (status :pkg-access?) true))
(each node data
(set (validator :ast) node)
(validate validator))
(set (validator :ast) ast)
# (print "ftype " ftype)
# (print "stype " stype)
# (print "ltype " ltype)
(when (= ftype :pkg-name) (pkg-root validator))
(when (= ftype :keyword) (kw-root validator))
# (when (= ltype :args) (tail-call validator))
(when (and (= ftype :word) (= stype :args))
(check-arity validator))
validator)
(defn- pair [validator]
(def ast (validator :ast))
(def [k v] (ast :data))
(set (validator :ast) k)
(validate validator)
(set (validator :ast) v)
(validate validator))
(defn- splat [validator]
(def ast (validator :ast))
(when (get-in validator [:status :pkg])
(array/push (validator :errors)
{:node ast :msg "splats are not allowed in pkgs"})
(break validator))
(def data (ast :data))
(when data
(set (validator :ast) data)
(validate validator))
validator)
(defn- dict [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(set (validator :ast) node)
(validate validator))
validator)
(defn- whenn [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(def [lhs rhs] node)
(set (validator :ast) lhs)
(validate validator)
(set (validator :ast) rhs)
(validate validator))
validator)
# XXX: do this!
(defn- withh [validator])
# XXX: tail calls in last position
(defn- doo [validator]
(def ast (validator :ast))
(def data (ast :data))
(each node data
(set (validator :ast) node)
(validate validator))
validator)
(defn- usee [validator]
(def ast (validator :ast))
(def data (ast :data))
(set (validator :ast) data)
(validate validator)
(def name (data :data))
(def ctx (validator :ctx))
(def pkg (get-in ctx [name :pkg] @{}))
(loop [[k v] :pairs pkg]
(set (ctx (string k)) v))
validator)
(defn- pkg-entry [validator pkg]
(def ast (validator :ast))
(def status (validator :status))
(when (= :pkg-pair (ast :type))
(set (status :pkg-access?) true))
(def data (ast :data))
(def [key value] (ast :data))
# (print "PKG ENTRY***")
# (pp key)
# (pp value)
(set (validator :ast) key)
(validate validator)
(set (validator :ast) value)
(validate validator)
(def entry (if (= :pkg-name (value :type))
(resolve-name (validator :ctx) (string (value :data)))
value))
# (print "entry at " (key :data))
# (pp entry)
(set (status :pkg-access?) nil)
(def kw (key :data))
# (pp kw)
(set (pkg kw) entry)
# (pp pkg)
validator)
(defn- pkg [validator]
(def ast (validator :ast))
(def data (ast :data))
(def name (ast :name))
(def pkg @{})
(each node data
(set (validator :ast) node)
(pkg-entry validator pkg))
(set (ast :pkg) pkg)
# (print "THE PACKAGE")
# (pp pkg)
(def ctx (validator :ctx))
(set (ctx name) ast)
validator)
(defn- ns [validator]
(def ast (validator :ast))
(def data (ast :data))
(def name (ast :name))
(def parent (validator :ctx))
(def ctx @{:^parent parent})
(def block (data :data))
(each node block
(set (validator :ast) node)
(validate validator))
(set (ast :pkg) ctx)
(set (parent name) ast)
validator)
(defn- loopp [validator]
(def ast (validator :ast))
(def status (validator :status))
(def data (ast :data))
(def input (first data))
# (print "LOOP INPUT")
# (pp input)
(def clauses (get data 1))
(def input-arity (length (input :data)))
(set (ast :arity) input-arity)
# (print "input arity to loop " input-arity)
(set (validator :ast) input)
(validate validator)
# harmonize arities
(def rest-arities @{})
(each clause clauses
# (print "CLAUSE:")
# (pp clause)
(def patt (first clause))
(def params (patt :data))
(def clause-arity (length params))
# (print "checking clause with arity " clause-arity)
(def rest-param? (= :splat (get (last params) :type)))
(when (and
(not rest-param?) (not= clause-arity input-arity))
(array/push (validator :errors)
{:node patt :msg "arity mismatch"}))
(when rest-param?
(set (rest-arities clause-arity) patt)))
# (pp rest-arities)
(loop [[arity patt] :pairs rest-arities]
(when (< input-arity arity)
(array/push (validator :errors)
{:node patt :msg "arity mismatch"})))
(def loop? (status :loop))
(set (status :loop) input-arity)
(def tail? (status :tail))
(set (status :tail) true)
(match-clauses validator clauses)
(set (status :loop) loop?)
(set (status :tail) tail?)
validator)
(defn- recur [validator]
(def ast (validator :ast))
(def status (validator :status))
(def loop-arity (status :loop))
(when (not loop-arity)
(array/push (validator :errors)
{:node ast :msg "recur may only be used inside a loop"})
(break validator))
(def called-with (get-in ast [:data :data]))
(def recur-arity (length called-with))
# (print "loop arity " loop-arity)
# (print "recur arity" recur-arity)
(when (not= recur-arity loop-arity)
(array/push (validator :errors)
{:node ast :msg "recur must have the same number of args as its loop"}))
(when (not (status :tail))
(array/push (validator :errors)
{:node ast :msg "recur must be in tail position"}))
(set (validator :ast) (ast :data))
(validate validator))
(defn- repeatt [validator]
(def ast (validator :ast))
(def [times body] (ast :data))
(set (validator :ast) times)
(validate validator)
(set (validator :ast) body)
(validate validator))
(defn- panic [validator]
(def ast (validator :ast))
(def data (ast :data))
(set (validator :ast) data)
(validate validator))
(defn- testt [validator]
(def ast (validator :ast))
(def [_ body] (ast :data))
(set (validator :ast) body)
(validate validator))
(defn- pkg-name [validator]
(def ast (validator :ast))
(def name (ast :data))
(def ctx (validator :ctx))
(def pkg (resolve-name ctx name))
(when (not pkg)
(array/push (validator :errors)
{:node ast :msg "unbound name"}))
validator)
(defn- pkg-kw [validator]
# (print "validating pkg-kw")
(def ast (validator :ast))
(def pkg-access? (get-in validator [:status :pkg-access?]))
# (print "pkg-access? " pkg-access?)
(when (not pkg-access?)
(array/push (validator :errors)
{:node ast :msg "cannot use pkg-kw here"}))
validator)
(defn- pkg-pair [validator]
# (print "validating pkg-pair")
(def ast (validator :ast))
(def status (validator :status))
(def [_ pkg] (ast :data))
(set (status :pkg-access?) true)
(set (validator :ast) pkg)
(validate validator)
(set (status :pkg-access?) nil)
validator)
(defn- kw [validator]
(def status (validator :status))
(set (status :pkg-access?) nil)
validator)
(defn- validate* [validator]
(def ast (validator :ast))
(def type (ast :type))
# (print "validating node " type)
(cond
(has-value? terminals type) validator
(has-value? simple-colls type) (simple-coll validator)
(case type
:keyword (kw validator)
:if (iff validator)
:let (lett validator)
:script (script validator)
:block (block validator)
:word (word validator)
:fn (fnn validator)
:match (matchh validator)
:interpolated (interpolated validator)
:synthetic (synthetic validator)
:do (doo validator)
:dict (dict validator)
:test (testt validator)
:panic (panic validator)
:repeat (repeatt validator)
:when (whenn validator)
:splat (splat validator)
:pair (pair validator)
:pkg-pair (pkg-pair validator)
:ns (ns validator)
:pkg (pkg validator)
:pkg-name (pkg-name validator)
:pkg-kw (pkg-kw validator)
:use (usee validator)
:loop (loopp validator)
:recur (recur validator)
:box (box validator)
:receive (receive validator)
(error (string "unknown node type " type)))))
(set validate validate*)
(defn- cleanup [validator]
(def declared (get-in validator [:status :declared] {}))
(when (any? declared)
(each declaration (keys declared)
(array/push (validator :errors) {:node declaration :msg "declared fn, but not defined"})))
validator)
(defn valid [ast &opt ctx]
(default ctx @{})
(set (ctx :^toplevel) true)
(def validator (new-validator ast))
(def base-ctx @{:^parent ctx})
(set (validator :ctx) base-ctx)
(validate validator)
(cleanup validator))
(import ./base :as b)
# (do
(comment
(def source `
dec (12)
`)
(def scanned (s/scan source))
(def parsed (p/parse scanned))
(def validated (valid parsed b/ctx))
# (get-in validated [:status :declared])
# (validated :ctx)
)

View File

@ -1,45 +0,0 @@
default:
@just --list
# build optimized wasm
build: && clean-wasm-pack
# build with wasm-pack
wasm-pack build --target web
# build dev wasm
dev: && clean-wasm-pack
wasm-pack build --dev --target web
# clean up after wasm-pack
clean-wasm-pack:
# delete cruft from wasm-pack
rm pkg/.gitignore pkg/package.json pkg/README.md
rm -rf pkg/snippets
# fix imports of rudus.js
cp pkg/rudus.js pkg/rudus.js.backup
echo 'import { io } from "./worker.js"' > pkg/rudus.js
cat pkg/rudus.js.backup | tail -n+2>> pkg/rudus.js
rm pkg/rudus.js.backup
from_branch := `git branch --show-current`
git_status := `git status -s`
# publish this branch into release
release:
echo {{ if git_status == "" {"git status ok"} else {error("please commit changes first")} }}
just build
-git commit -am "built for release"
git checkout release
git merge main
git push
git checkout main
# serve the pkg directory
serve:
live-server pkg
# build the documentation
doc:
janet janet/doc.janet
-rm doc/prelude.md
mv prelude.md doc/

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

379
ludus.js Normal file
View File

@ -0,0 +1,379 @@
import init, {ludus} from "./rudus.js";
await init();
let res = null
let code = null
export function run (source) {
code = source
const output = ludus(source)
res = JSON.parse(output)
return res
}
export function stdout () {
if (!res) return ""
return res.io.stdout.data
}
export function turtle_commands () {
if (!res) return []
return res.io.turtle.data
}
export function result () {
return res
}
const turtle_init = {
position: [0, 0],
heading: 0,
pendown: true,
pencolor: "white",
penwidth: 1,
visible: true
}
const colors = {
black: [0, 0, 0, 255],
silver: [192, 192, 192, 255],
gray: [128, 128, 128, 255],
white: [255, 255, 255, 255],
maroon: [128, 0, 0, 255],
red: [255, 0, 0, 255],
purple: [128, 0, 128, 255],
fuchsia: [255, 0, 255, 255],
green: [0, 128, 0, 255],
lime: [0, 255, 0, 255],
olive: [128, 128, 0, 255],
yellow: [255, 255, 0, 255],
navy: [0, 0, 128, 255],
blue: [0, 0, 255, 255],
teal: [0, 128, 128, 255],
aqua: [0, 255, 255, 255],
}
function resolve_color (color) {
if (typeof color === 'string') return colors[color]
if (typeof color === 'number') return [color, color, color, 255]
if (Array.isArray(color)) return color
return [0, 0, 0, 255] // default to black?
}
let background_color = "black"
function add (v1, v2) {
const [x1, y1] = v1
const [x2, y2] = v2
return [x1 + x2, y1 + y2]
}
function mult (vector, scalar) {
const [x, y] = vector
return [x * scalar, y * scalar]
}
function unit_of (heading) {
const turns = -heading + 0.25
const radians = turn_to_rad(turns)
return [Math.cos(radians), Math.sin(radians)]
}
function command_to_state (prev_state, curr_command) {
const verb = curr_command[0]
switch (verb) {
case "goto": {
const [_, x, y] = curr_command
return {...prev_state, position: [x, y]}
}
case "home": {
return {...prev_state, position: [0, 0], heading: 0}
}
case "right": {
const [_, angle] = curr_command
const {heading} = prev_state
return {...prev_state, heading: heading + angle}
}
case "left": {
const [_, angle] = curr_command
const {heading} = prev_state
return {...prev_state, heading: heading - angle}
}
case "forward": {
const [_, steps] = curr_command
const {heading, position} = prev_state
const unit = unit_of(heading)
const move = mult(unit, steps)
return {...prev_state, position: add(position, move)}
}
case "back": {
const [_, steps] = curr_command
const {heading, position} = prev_state
const unit = unit_of(heading)
const move = mult(unit, -steps)
return {...prev_state, position: add(position, move)}
}
case "penup": {
return {...prev_state, pendown: false}
}
case "pendown": {
return {...prev_state, pendown: true}
}
case "penwidth": {
const [_, width] = curr_command
return {...prev_state, penwidth: width}
}
case "pencolor": {
const [_, color] = curr_command
return {...prev_state, pencolor: color}
}
case "setheading": {
const [_, heading] = curr_command
return {...prev_state, heading: heading}
}
case "loadstate": {
// console.log("LOADSTATE: ", curr_command)
const [_, [x, y], heading, visible, pendown, penwidth, pencolor] = curr_command
return {position: [x, y], heading, visible, pendown, penwidth, pencolor}
}
case "show": {
return {...prev_state, visible: true}
}
case "hide": {
return {...prev_state, visible: false}
}
case "background": {
background_color = curr_command[1]
return prev_state
}
}
}
function eq_vect (v1, v2) {
const [x1, y1] = v1
const [x2, y2] = v2
return (x1 === x2) && (y1 === y2)
}
function eq_color (c1, c2) {
if (c1 === c2) return true
const res1 = resolve_color(c1)
const res2 = resolve_color(c2)
for (let i = 0; i < res1.length; ++i) {
if (res1[i] !== res2[i]) return false
}
return true
}
function states_to_call (prev, curr) {
const calls = []
// whose state should we use?
// pen states will only differ on more than one property
// if we use `loadstate`
// my sense is `prev`, but that may change
if (prev.pendown && !eq_vect(prev.position, curr.position)) {
calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]])
}
if (!eq_color(curr.pencolor, prev.pencolor)) {
calls.push(["stroke", ...resolve_color(curr.pencolor)])
}
if (curr.penwidth !== prev.penwidth) {
calls.push(["strokeWeight", curr.penwidth])
}
return calls
}
const turtle_radius = 20
const turtle_angle = 0.385
let turtle_color = [255, 255, 255, 150]
function p5_call_root () {
return [
["background", ...resolve_color(background_color)],
["push"],
["rotate", Math.PI],
["scale", -1, 1],
["stroke", ...resolve_color(turtle_init.pencolor)],
]
}
function rotate (vector, heading) {
const radians = turn_to_rad(heading)
const [x, y] = vector
return [
(x * Math.cos (radians)) - (y * Math.sin (radians)),
(x * Math.sin (radians)) + (y * Math.cos (radians))
]
}
function turn_to_rad (heading) {
return (heading % 1) * 2 * Math.PI
}
function turn_to_deg (heading) {
return (heading % 1) * 360
}
function hex (n) {
return n.toString(16).padStart(2, "0")
}
function svg_render_line (prev, curr) {
if (!prev.pendown) return ""
if (eq_vect(prev.position, curr.position)) return ""
const {position: [x1, y1], pencolor, penwidth} = prev
const {position: [x2, y2]} = curr
const [r, g, b, a] = resolve_color(pencolor)
return `
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="square" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
`
}
function escape_svg (svg) {
return svg
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export function extract_ludus (svg) {
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
return code
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, `"`)
.replace(/&apos;/g, `'`)
}
function svg_render_path (states) {
const path = []
for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1]
const curr = states[i]
path.push(svg_render_line(prev, curr))
}
return path.join("")
}
function svg_render_turtle (state) {
if (!state.visible) return ""
const [fr, fg, fb, fa] = turtle_color
const fill_alpha = fa/255
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
const origin = [0, turtle_radius]
const [x1, y1] = origin
const [x2, y2] = rotate(origin, turtle_angle)
const [x3, y3] = rotate(origin, -turtle_angle)
const [pr, pg, pb, pa] = resolve_color(pencolor)
const pen_alpha = pa/255
const ink = pendown ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
return `
<g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
${ink}
</g>
`
}
export function svg (commands) {
// console.log(commands)
const states = [turtle_init]
commands.reduce((prev_state, command) => {
const new_state = command_to_state(prev_state, command)
states.push(new_state)
return new_state
}, turtle_init)
// console.log(states)
const {maxX, maxY, minX, minY} = states.reduce((accum, {position: [x, y]}) => {
accum.maxX = Math.max(accum.maxX, x)
accum.maxY = Math.max(accum.maxY, y)
accum.minX = Math.min(accum.minX, x)
accum.minY = Math.min(accum.minY, y)
return accum
}, {maxX: 0, maxY: 0, minX: 0, minY: 0})
const [r, g, b, a] = resolve_color(background_color)
if ((r+g+b)/3 > 128) turtle_color = [0, 0, 0, 150]
const view_width = (maxX - minX) * 1.2
const view_height = (maxY - minY) * 1.2
const margin = Math.max(view_width, view_height) * 0.1
const x_origin = minX - margin
const y_origin = -maxY - margin
const path = svg_render_path(states)
const turtle = svg_render_turtle(states[states.length - 1])
return `<?xml version="1.0" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">
<rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />
<g transform="scale(-1, 1) rotate(180)">
${path}
${turtle}
</g>
<ludus>
${escape_svg(code)}
</ludus>
</svg>
`
}
function p5_render_turtle (state, calls) {
if (!state.visible) return
calls.push(["push"])
const [r, g, b, a] = turtle_color
calls.push(["fill", r, g, b, a])
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
const origin = [0, turtle_radius]
const [x1, y1] = origin
const [x2, y2] = rotate(origin, turtle_angle)
const [x3, y3] = rotate(origin, -turtle_angle)
calls.push(["translate", x, y])
// need negative turtle rotation with the other p5 translations
calls.push(["rotate", -turn_to_rad(heading)])
calls.push(["noStroke"])
calls.push(["beginShape"])
calls.push(["vertex", x1, y1])
calls.push(["vertex", x2, y2])
calls.push(["vertex", x3, y3])
calls.push(["endShape"])
calls.push(["strokeWeight", penwidth])
calls.push(["stroke", ...resolve_color(pencolor)])
if (pendown) calls.push(["line", 0, 0, x1, y1])
calls.push(["pop"])
return calls
}
export function p5 (commands) {
const states = [turtle_init]
commands.reduce((prev_state, command) => {
const new_state = command_to_state(prev_state, command)
states.push(new_state)
return new_state
}, turtle_init)
// console.log(states)
const [r, g, b, _] = resolve_color(background_color)
if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
const p5_calls = [...p5_call_root()]
for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1]
const curr = states[i]
const calls = states_to_call(prev, curr)
for (const call of calls) {
p5_calls.push(call)
}
}
p5_calls[0] = ["background", ...resolve_color(background_color)]
p5_render_turtle(states[states.length - 1], p5_calls)
p5_calls.push(["pop"])
return p5_calls
}
window.ludus = {p5, svg, run, stdout, turtle_commands, result, extract_ludus};

View File

@ -1,255 +0,0 @@
# Working notes on bytecode stuff
### 2024-12-15
So far, I've done the easy stuff: constants, and ifs.
There's still some easy stuff left:
* [ ] lists
* [ ] dicts
* [ ] when
* [ ] panic
So I'll do those next.
But then we've got two doozies: patterns and bindings, and tuples.
#### Tuples make things hard
In fact, it's tuples that make things hard.
The idea is that, when possible, tuples should be stored on the stack.
That makes them a different creature than anything else.
But the goal is to be able, in a function call, to just push a tuple onto the stack, and then match against it.
Because a tuple _isn't_ just another `Value`, that makes things challenging.
BUT: matching against all other `Values` should be straightforward enough?
I think that the way to do this is to reify patterns.
Rather than try to emit bytecodes to embody patterns, the patterns are some kind of data that get compiled and pushed onto a stack like keywords and interned strings and whatnot.
And then you can push a pattern onto the stack right behind a value, and then have a `match` opcode that pops them off.
Things get a bit gnarly since patterns can be nested. I'll start with the basic cases and run from there.
But when things get *very* gnarly is considering tuples on the stack.
How do you pop off a tuple?
Two thoughts:
1. Just put tuples on the heap. And treat function arguments/matching differently.
2. Have a "register" that stages values to be pattern matched.
##### Regarding the first option
I recall seeing somebody somewhere make a comment that trying to represent function arguments as tuples caused tons of pain.
I can see why that would be the case, from an implementation standpoint.
We should have _values_, and don't do fancy bookkeeping if we don't have to.
_Conceptually_, it makes a great deal of sense to think of tuples as being deeply the same as function invocation.
But _practically_, they are different things, especially with Rust underneath.
This feels like this cuts along the grain, and so this is what I will try.
I suspect that I'll end up specializing a lot around function arguments and calling, but that feels more tractable than the bookkeeping around stack-based tuples.
### 2024-12-17
Next thoughts: take some things systematically rather than choosing an approach first.
#### Things that always match
* Placeholder.
- I _think_ this is just a no-op. A `let` expression leaves its rhs pushed on the stack.
* Word: put something on the stack, and bind a name.
- This should follow the logic of locals as articulated in _Crafting Interpreters_.
In both of these cases, there's no conditional logic, simply a bind.
#### Things that never bind
* Atomic values: put the rhs on the stack, then do an equality check, and panic if it fails. Leave the thing on the stack.
#### Analysis
In terms of bytecode, I think one thing to do, in the simple case, is to do the following:
* `push` a `pattern` onto the stack
* `match`--pops the pattern and the value off the stack, and then applies the pattern to the value. It leaves the value on the stack, and pushes a special value onto the stack representing a match, or not.
- We'll probably want `match-1`, `match-2`, `match-3`, etc., opcodes for matching a value that's that far back in the stack. E.g., `match-1` matches against not the top element, but the `top - 1` element.
- This is _specifically_ for matching function arguments and `loop` forms.
* There are a few different things we might do from here:
- `panic_if_no_match`: panic if the last thing is a `no_match`, or just keep going if not.
- `jump_if_no_match`: in a `match` form or a function, we'll want to move to the next clause if there's no match, so jump to the next clause's `pattern` `push` code.
* Compound patterns are going to be more complex.
- I think, for example, what you're going to need to do is to get opcodes that work on our data structures, so, for example, when you have a `match_compound` opcode and you start digging into the pattern.
* Compound patterns are specifically _data structures_. So simple structures should be stack-allocated, and and complex structures should be pointers to something on the heap. Maybe?
#### A little note
For instructions that need more than 256 possibilities, we'll need to mush two `u8`s together into a `u16`. The one liner for this is:
```rust
let number = ((first as u16) << 8) | second as u16;
```
#### Oy, stacks and expressions
One thing that's giving me grief is when to pop and when to note on the value stack.
So, like, we need to make sure that a line of code leaves the stack exactly where it was before it ran, with the exception of binding forms: `let`, `fn`, `box`, etc. Those leave one (or more!) items on the stack.
In the simplest case, we have a line of code that's just a constant:
```
false
```
This should emit the bytecode instructions (more or less):
```
push false
pop
```
The push comes from the `false` value.
The pop comes from the end of a (nonbinding) line.
The problem is that there's no way (at all, in Ludus) to distinguish between an expression that's just a constant and a line that is a complete line of code that's an expression.
So if we have the following:
```
let foo = false
```
We want:
```
push false
```
Or, rather, given that `foo` is a word pattern, what we actually want is:
```
push false # constant
push pattern/word # load pattern
pop
pop # compare
push false # for the binding
```
But it's worth it here to explore Ludus's semantics.
It's the case that there are actually only three binding forms (for now): `let`, `fn`, and `box`.
Figuring out `let` will help a great deal.
Match also binds things, but at the very least, match doesn't bind with expressions on the rhs, but a single value.
Think, too about expressions: everything comes down to a single value (of course), even tuples (especially now that I'm separating function calls from tuple values (probably)).
So: anything that *isn't* a binding form should, before the `pop` from the end of a line, only leave a single value on the stack.
Which suggests that, as odd as it is, pushing a single `nil` onto the stack, just to pop it, might make sense.
Or, perhaps the thing to do is to peek: if the line in question is binding or not, then emit different bytecode.
That's probably the thing to do. Jesus, Scott.
And **another** thing worth internalizing: every single instruction that's not an explicit push or pop should leave the stack length unchanged.
So store and load need always to swap in a `nil`
### 2024-12-23
Compiling functions.
So I'm working through the functions chapter of _CI_, and there are a few things that I'm trying to wrap my head around.
First, I'm thinking that since we're not using raw pointers, we'll need some functional indirection to get our current byte.
So one of the hard things here is that, unlike with Lox, Ludus doesn't have fixed-arity functions. That means that the bindings for function calls can't be as dead simple as in Lox. More to the point, because we don't know everything statically, we'll need to do some dynamic magic.
The Bob Nystrom program uses three useful auxiliary constructs to make functions straightforward:
* `CallFrame`s, which know which function is being called, has their own instruction pointer, and an offset for the first stack slot that can be used by the function.
```c
typedef struct {
ObjFunction* function;
uint8_t* ip;
Value* slots;
} CallFrame;
```
Or the Rust equivalent:
```rust
struct CallFrame {
function: LFn,
ip: usize,
stack_root: usize,
}
```
* `Closure`s, which are actual objects that live alongside functions. They have a reference to a function and to an array of "upvalues"...
* `Upvalue`s, which are ways of pointing to values _below_ the `stack_root` of the call frame.
##### Digression: Prelude
I decided to skip the Prelude resolution in the compiler and only work with locals. But actually, closures, arguments, and the prelude are kind of the same problem: referring to values that aren't currently available on the stack.
We do, however, know at compile time the following:
* If a binding's target is on the stack, in a closure, or in the prelude.
* This does, however, require that the function arguments work in a different way.
The way to do this, I reckon, is this:
* Limit arguments (to, say, no more than 7).
* A `CallFrame` includes an arity field.
* It also includes an array of length 7.
* Each `match` operation in function arguments clones from the call frame, and the first instruction for any given body (i.e. once we've done the match) is to clear the arguments registers in the `CallFrame`, thus decrementing all the refcounts of all the heap-allocated objects.
* And the current strategy of scoping and popping in the current implementation of `match` will work just fine!
Meanwhile, we don't actually need upvalues, because bindings cannot change in Ludus. So instead of upvalues and their indirection, we can just emit a bunch of instructions to have a `values` field on a closure. The compiler, meanwhile, will know how to extract and emit instructions both to emit those values *and* to offer correct offsets.
The only part I haven't figured out quite yet is how to encode access to what's stored in a closure.
Also, I'm not certain we need the indirection of a closure object in Ludus. The function object itself can do the work, no?
And the compiler knows which function it's closing over, and we can emit a bunch of instructions to close stuff over easily, after compiling the function and putting it in the constants table. The way to do this is to yank the value to the top of the stack using normal name resolution procedures, and then use a two-byte operand, `Op::Close` + index of the function in the constants table.
##### End of digression.
And, because we know exactly is bound in a given closure, we can actually emit instructions to close over a given value easily.
#### A small optimization
The lifetimes make things complicated; but I'm not sure that I would want to actually manage them manually, given how much they make my head hurt with Rust. I do get the sense that we will, at some point, need some lifetimes. A `Chunk` right now is chunky, with lots of owned `vec`s.
Uncle Bob separates `Chunk`s and `Compiler`s, which, yes! But then we have a problem: all of the information to climb back to source code is in the `Compiler` and not in the `Chunk`. How to manage that encoding?
(Also the keyword and string intern tables should be global, and not only in a single compiler, since we're about to get nested compilers...)
### 2024-12-24
Other interesting optimizations abound:
* `add`, `sub`, `inc`, `dec`, `type`, and other extremely frequently used, simple functions can be compiled directly to built-in opcodes. We still need functions for them, with the same arities, for higher order function use.
- The special-case logic is in the `Synthetic` compiler branch, rather than anywhere else.
- It's probably best to disallow re-binding these names anywhere _except_ Prelude, where we'll want them shadowed.
- We can enforce this in `Validator` rather than `Compiler`.
* `or` and `and` are likewise built-in, but because they don't evaluate their arguments eagerly, that's another, different special case that's a series of eval, `jump_if_false`, eval, `jump_if_false`, instructions.
* More to the point, the difference between `or` and `and` here and the built-ins is that `or` and `and` are variadic, where I was originally thinking about `and` and co. as fixed-arity, with variadic behaviours defined by a shadowing/backing Ludus function. That isn't necessary, I don't think.
* Meanwhile, `and` and `or` will also, of necessity, have backing shadowing functions.
#### More on CallFrames and arg passing
* We don't actually need the arguments register! I was complicating things. The stack between the `stack_root` and the top will be _exactly_ the same as an arguments register would have been in my imagination. So we can determine the number of arguments passed in with `stack.len() - stack_root`, and we can access argument positions with `stack_root + n`, since the first argument is at `stack_root`.
- This has the added benefit of not having to do any dances to keep the refcount of any heap-allocated objects as low as possible. No extra `Clone`s here.
* In addition, we need two `check_arity` ops: one for fixed-arity clauses, and one for clauses with splatterns. Easily enough done. Remember: opcodes are for special cases!
#### Tail calls
* The way to implement tail calls is actually now really straightforward! The idea is to simply have a `TailCall` rather than a `Call` opcode. In place of creating a new stack frame and pushing it to the call stack on top of the old call frame, you pop the old call frame, then push the new one to the call stack.
* That does mean the `Compiler` will need to keep track of tail calls. This should be pretty straightforward, actually, and the logic is already there in `Validator`.
* The thing here is that the new stack frame simply requires the same return location as the old one it's replacing.
* That reminds me that there's an issue in terms of keeping track of not just the IP, but the chunk. In Lox, the IP is a pointer to a `u8`, which works great in C. But in Rust, we can't use a raw pointer like that, but an index into a `vec<u8>`. Which means the return location needs both a chunk and an index, not just a `u8` pointer:
```rust
struct StackFrame<'a> {
function: LFn,
stack_root: usize,
return: (&'a Chunk, usize),
}
```
(I hate that there's a lifetime here.)
This gives us a way to access everything we need: where to return to, the root of the stack, the chunk (function->chunk), the closures (function->closures).
### 2024-12-26
One particular concern here, which needs some work: recursion is challenging.
In particular, the issue is that if, as I have been planning, a function closes over all its values at the moment it is compiled, the only value type that requires updating is a function. A function can be declared but not yet defined, and then when another function that uses that function is defined, the closed-over value will be to the declaration but not the definition.
One way to handle this, I think is using `std::cell::OnceCell`. Rather than a `RefCell`, `OnceCell` has no runtime overhead. Instead, what happens is you effectively put a `None` in the cell. Then, once you have the value you want to put in there, you call `set` on the `OnceCell`, and it does what it needs to.
This allows for the closures to be closed over right after compilation.
### 2024-12-27
Working on `loop` and `recur`, rather than `fn`--this is the gentler slope.
And discovering that we actually need a `[Value; 15]` return register.
`recur` needs to push all the arguments to the stack, then yank them off into the return register, then pop back to the loop root, then push all the things back onto the stack, then jump to the beginning of the loop.
And that also means I need a different `Value` variant that's a true `Nothing`, not even `nil`, which will _never_ end up anywhere other than a placeholder value in the register and on the stack.
So, next steps:
1. Add `Value::Nothing` and fix all the compiler errors
2. Make the return register `[Value; 15]`, populated with `Value::Nothing`s at initialization.
3. Update `load` and `store` to work with the array rather than a single value.
4. Create `store_n` and `load_n` to work with multiple values.
5. Create a `Vm.arity` method that computes how many non-nothings were passed into the register.
6. Then, implement `recur`
7. And, then, fix up jump indexes in `loop`
8. Fix all the off-by-one errors in the jumps

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +0,0 @@
# VM thoughts
### Initial thoughts
We want numbers and bools as unboxed as possible.
Nil is a singleton, and should be static.
Strings come in two flavours:
* String literals, which are static/interned.
* Constructed strings, which should be `Rc<String>`
Keywords are static/interned.
Tuples should be refcounted for now.
### Optimization and other thoughts
2024-11-09
* To put tuples on the stack, we need to know both how long they are (number of members) and how big they are (amount of memory), since tuples can contain other tuples.
- All other values must be one stack cell:
* `nil` is its own thing
* numbers are a wrapped `f64` (at least until we get to NaN boxed values)
* booleans are a wrapped `bool`
* keywords are a wrapped `u16` or `u32`, which is an index into a vec of `&str`s, which can be read back into a string when printed
* strings are a `&str` or an `Rc<String>` (with two possible wrappers: `Value::Str` or `Value::String`)
* dicts are `imbl::HashMap<u16, Value>`, with the hash generated on the index of the keyword
* sets are `imbl::HashSet<Value>`, with the caveat that `f64` isn't `Eq`, which means that we can't use it for a hash key. The way around this, I think, is to implement `Eq` for `Value`, with a panic if we try to put NaN in a set
* functions are `Rc<LFn>`
* boxes are `Rc<RefCell>`
* That means everything is either a wrapped `Copy` (`:nil`, `:number`, `:bool`), an interned reference (`:keyword`, `:string`), `Rc` reference types (`:string`, `:box`, `:fn`), or persistent reference types that have their own `clone` (`:list`, `:dict`, `:set`)
* This doesn't cover everything, yet. But other reference types will be `Rc`ed structs: to wit, processes and packages.
- Tuples, meanwhile, have a special representation on the stack.
* They start with a `Value::TupleStart(len: u8, size: u8)`.
* They then have a number of members.
* They end with a `Value::TupleEnd(len: u8, size: u8)`.
* `len` indicates the number of members in the tuple; `size` indicates the size of the tuple on the stack, including the `TupleStart` and `TupleEnd` cells. For `()`, `len` is `0`, and `size` is `2`. Nesting tuples will lead to larger divergences, and will increase `size` but not `len`.
* If sombody tries to stuff more than 255 members in a tuple, nested or not, we get a validation error to tell them to use a list.
- Or promote it to be a reference type? The natural encoding of a list in Ludus is using a `(car, cdr)` encoding (or `(data, next)`). I believe the way to get this out of a scope (block or function) is to expand the tuple fully, which could lead very quickly to very large tuples.
- But we can easily distinguish between argument tuples and value tuples, and promote value tuples with a size larger than 255 to a `Value::BigTuple(Rc<Vec<Value>>)`.
- But in no case should we allow arguments to get bigger than 255.
- Keeping small value tuples on the stack is worthwhile, especially given the importance of result tuples, which should stay on the stack.
* This naturally leads to questions about pattern matching, especially when we get to a stack-based bytecode VM.
- A pattern, like a tuple, is a series of cells.
- The goal is to keep pattern sizes and lengths identical to the tuple data representation.
- That means that, like data representations, a pattern has to include both a set of bytecode instructions and a data representation on the stack.
- In fact, I suspect that the fastest way to encode this will be to push the data representation of the scrutinee on the stack, and then to push the pattern, and to then compare within the stack, at different offsets.
### Let's not reinvent the wheel
#### Or, crates we will use
* `chumsky` for parsing
* `ariadne` for parsing errors
* `imbl` for persistent data structures
* `boxing` for NaN boxing (eventually?)
* ~~`tailcall` for tail recursion~~ This only works for simple recursion, and we need mutual recursion.
We additionally might want crates for:
* processes/actors, although given that Ludus will be single-threaded for the forseeable future, it may be lighter weight to just write my own `process` abstraction
* in that case, we will need a ringbuffer, `ringbuf`
### On string interpolation
#### Which is proving rather harder to handle than I expected
I'm trying to use Chumsky to do this, but it's weirdly much harder to model with Chumsky's parer combinators than I expected.
I suspect the thing to do is to just brute force it in much the same way that I do in the Janet-based scanner: loop through the things and push things onto vectors in the correct ways.
This won't be a one-for-one translation, but I suspect it will be easier to manage than banging my head against, especially, the terrible error messages Chumsky's elaborate types give me.
This makes interpolated strings easy enough to work with.
That said, interpolation patterns are harder.
In particular, I worry whether I'll be able to compile a Chumsky parser with strings that aren't interned/`'static`.
Because the pattern match will actually have to be a little Chumsky parser guy (doo dah), or some equivalent.
(In the Janet-based interpreter, I used Janet's built-in PEGs.)
### On performance
The Rust tree-walk interpreter is something like two orders of magnitude faster than the Janet interpreter.
So in that sense, I think it's a worthwhile middle ground to effectively publish this first, easier-to-develop approach, and then to work on a bytecode VM later.
It's worth noting that my approach to this first tree-walk interpreter still leaves a lot on the table for optimization: the `Value` enum is 64 _bytes_.
This is because `imbl::Vector`s are 64 bytes.
I'm trying to ensure opportunistic mutation throughout, but I have found it hard with dicts.
This sort of thing.
Finally, it's clear that some perf testing will be necessary to determine the final arrangement of things.
Will `box`ing things to get heap pointers help?
Or will the extra indirection cost more speed than even if we squeeze `Value`'s size down to 8 bytes?
Will `box`ing lists, etc., mung up how `imbl` does refcounting and opportunistic mutation?
There are things like `tinyvec` which does some of the dark magic around allocating that might make using tuples easier to manage?
### On parsing in Ludus
I've been thinking about Ludus's built-in parsing capabilities.
Using the interpolition-style string pattern matching parsing for ELIZA makes a lot of sense, but we need something more robust for, say, a Lisp.
Looking at this, I think that Janet's builtin PEG parsing might be a much more interesting solution than just about anything else.
I'm pretty sure I can make a slow, but user-friendly-enough version of that that works in Ludus.
(Famous last words.)

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<title>Testing Ludus/WASM integration</title>
</head>
<body>
<script src="./ludus.js" type="module"></script>
<p>
Open the console. All the action's in there.
</p>
</body>
</html>

View File

@ -1,14 +0,0 @@
const codes = {
"Tab": "tab",
"Enter": "enter",
"Escape": "esc",
"ArrowLeft": "left",
"ArrowRight": "right",
"ArrowUp": "up",
"ArrowDown": "down",
"Delete": "delete",
}
export function get_code (key) {
return codes[key] ?? key
}

View File

@ -1,200 +0,0 @@
import {p5} from "./p5.js"
import {svg as svg_2} from "./svg.js"
import {get_code} from "./keys.js"
if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, flush_commands, commands, result, flush_result, input, is_running, key_down, key_up, is_starting_up, p5, svg}
const worker_url = new URL("worker.js", import.meta.url)
const worker = new Worker(worker_url, {type: "module"})
let outbox = []
let ludus_console = ""
let ludus_commands = []
let ludus_result = null
let code = null
let running = false
let ready = false
let io_interval_id = null
let keys_down = new Set();
worker.onmessage = handle_messages
async function handle_messages (e) {
let msgs
try {
msgs = JSON.parse(e.data)
} catch {
// console.log(e.data)
throw Error("Main: bad json from Ludus")
}
for (const msg of msgs) {
switch (msg.verb) {
case "Complete": {
// console.log("Main: ludus completed with => ", msg.data)
ludus_result = msg.data
running = false
ready = false
outbox = []
break
}
case "Error": {
// console.log("Main: ludus errored with => ", msg.data)
ludus_result = msg.data
running = false
ready = false
outbox = []
break
}
// TODO: do more than report these
case "Console": {
let new_lines = msg.data.join("\n");
ludus_console = ludus_console + new_lines
// console.log("Main: ludus says => ", new_lines)
break
}
case "Commands": {
// console.log("Main: ludus commands => ", msg.data)
for (const command of msg.data) {
// commands will arrive asynchronously; ensure correct ordering
ludus_commands[command[1]] = command
}
break
}
case "Fetch": {
// console.log("Main: ludus requests => ", msg.data)
const res = await fetch(msg.data, {mode: "cors"})
const text = await res.text()
// console.log("Main: js responds => ", text)
outbox.push({verb: "Fetch", data: [msg.data, res.status, text]})
}
case "Ready": {
// console.log("Main: ludus is ready")
ready = true
}
}
}
}
function io_poller () {
if (io_interval_id && !running) {
// flush the outbox one last time
// (presumably, with the kill message)
worker.postMessage(outbox)
// cancel the poller
clearInterval(io_interval_id)
outbox = []
}
if (ready && running) {
bundle_keys()
worker.postMessage(outbox)
outbox = []
}
}
function bundle_keys () {
outbox.push({verb: "Keys", data: Array.from(keys_down)})
}
function start_io_polling () {
io_interval_id = setInterval(io_poller, 10)
}
// runs a ludus script; does not return the result
// the result must be explicitly polled with `result`
export function run (source) {
if (running || ready) {
// console.log("Main: received bouncy `run` call");
return "TODO: handle this? should not be running"
}
// start the vm
// wrapping the Run message in an array for the worker
worker.postMessage([{verb: "Run", data: source}])
// update state for this run
code = source
running = true
// reset the rest of my state
outbox = []
ludus_console = ""
ludus_commands = []
ludus_result = null
ready = false
keys_down = new Set();
// start the polling loop
start_io_polling()
}
export function is_starting_up() {
return running && !ready
}
// tells if the ludus script is still running
export function is_running() {
return running && ready
}
// kills a ludus script
export function kill () {
running = false
outbox.push({verb: "Kill"})
// console.log("Main: Killed Ludus")
}
// sends text into ludus (status: not working)
export function input (text) {
// console.log("Main: calling `input` with ", text)
outbox.push({verb: "Input", data: text})
}
// returns the contents of the ludus console and resets the console
export function flush_stdout () {
let out = ludus_console
ludus_console = ""
return out
}
// returns the contents of the ludus console, retaining them
export function stdout () {
return ludus_console
}
// returns the array of turtle commands
export function commands () {
return ludus_commands
}
// returns the array of turtle commands and clears it
export function flush_commands () {
let out = ludus_commands
ludus_commands = []
return out
}
// returns the ludus result
// this is effectively Option<String>:
// null if no result has been returned, or
// a string representation of the result
export function result () {
return ludus_result
}
export function flush_result () {
let out = ludus_result
ludus_result = null
return out
}
export function key_down (key) {
if (is_running()) keys_down.add(get_code(key))
}
export function key_up (key) {
if (is_running()) keys_down.delete(get_code(key))
}
export {p5} from "./p5.js"
export function svg (commands) {
// console.log(`generating svg for ${code}`)
return svg_2(commands, code)
}

116
pkg/p5.js
View File

@ -1,116 +0,0 @@
import {eq_vect, eq_color, resolve_color, get_turtle_color, set_turtle_color, turtle_radius, turtle_angle, turn_to_rad, turtle_init, command_to_state, background_color, rotate, last, sub} from "./turtle_geometry.js"
function states_to_call (prev, curr, call_stack) {
// console.log(prev)
// console.log(curr)
let calls = []
if (prev.pendown && !eq_vect(prev.position, curr.position)) {
// console.log("headings", prev.heading, curr.heading)
// console.log("forward?", curr.forward)
// console.log("#calls", call_stack.length)
if (prev.heading == curr.heading && curr.forward && call_stack.length > 0) {
// console.log("extending line!")
const [x, y] = sub(curr.position, prev.position)
const last_call = call_stack.pop();
const new_call = ["line", last_call[1], last_call[2], x + last_call[3], y + last_call[4]]
calls.push(new_call)
} else {
calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]])
}
}
if (!eq_color(curr.pencolor, prev.pencolor)) {
calls.push(["stroke", ...resolve_color(curr.pencolor)])
}
if (curr.penwidth !== prev.penwidth) {
calls.push(["strokeWeight", curr.penwidth])
}
if (curr.clear) {
calls.push("clear")
}
return calls
}
function p5_call_root () {
return [
["background", ...resolve_color(background_color)],
["push"],
["rotate", Math.PI],
["scale", -1, 1],
["stroke", ...resolve_color(turtle_init.pencolor)],
]
}
function p5_render_turtle (state, calls) {
if (!state.visible) return
calls.push(["push"])
const [r, g, b, a] = get_turtle_color()
calls.push(["fill", r, g, b, a])
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
const origin = [0, turtle_radius]
const [x1, y1] = origin
const [x2, y2] = rotate(origin, turtle_angle)
const [x3, y3] = rotate(origin, -turtle_angle)
calls.push(["translate", x, y])
// need negative turtle rotation with the other p5 translations
calls.push(["rotate", -turn_to_rad(heading)])
calls.push(["noStroke"])
calls.push(["beginShape"])
calls.push(["vertex", x1, y1])
calls.push(["vertex", x2, y2])
calls.push(["vertex", x3, y3])
calls.push(["endShape"])
calls.push(["strokeWeight", penwidth])
calls.push(["stroke", ...resolve_color(pencolor)])
if (pendown) calls.push(["line", 0, 0, x1, y1])
calls.push(["pop"])
return calls
}
export function p5 (commands) {
const all_states = {}
for (const command of commands) {
const [turtle_id, _, this_command] = command
let stack = all_states[turtle_id]
if (!stack) {
const new_stack = [turtle_init]
all_states[turtle_id] = new_stack
stack = new_stack
}
let prev_state = last(all_states[turtle_id])
const new_state = command_to_state(prev_state, this_command)
all_states[turtle_id].push(new_state)
}
// console.log(all_states)
const [r, g, b, _] = resolve_color(background_color)
if ((r + g + b)/3 > 128) set_turtle_color([0, 0, 0, 150])
const p5_calls = [...p5_call_root()]
for (const states of Object.values(all_states)) {
// console.log(states)
let my_calls = []
for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1]
const curr = states[i]
const calls = states_to_call(prev, curr, my_calls)
for (const call of calls) {
// console.log(call)
if (call === "clear") {
my_calls = []
} else {
my_calls.push(call)
}
}
}
p5_calls.push(["strokeWeight", 1])
p5_calls.push(["stroke", 255])
for (const call of my_calls) {
p5_calls.push(call)
}
p5_render_turtle(last(states), p5_calls)
}
p5_calls[0] = ["background", ...resolve_color(background_color)]
p5_calls.push(["pop"])
return p5_calls
}

View File

@ -1,415 +0,0 @@
import { io } from "./worker.js"
let wasm;
function addToExternrefTable0(obj) {
const idx = wasm.__externref_table_alloc();
wasm.__wbindgen_export_2.set(idx, obj);
return idx;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const idx = addToExternrefTable0(e);
wasm.__wbindgen_exn_store(idx);
}
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b)
});
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_6.get(state.dtor)(a, state.b);
CLOSURE_DTORS.unregister(state);
} else {
state.a = a;
}
}
};
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}
/**
* @param {string} src
* @returns {Promise<void>}
*/
export function ludus(src) {
const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ludus(ptr0, len0);
return ret;
}
function __wbg_adapter_18(arg0, arg1, arg2) {
wasm.closure354_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_44(arg0, arg1, arg2, arg3) {
wasm.closure367_externref_shim(arg0, arg1, arg2, arg3);
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
const ret = arg0.call(arg1);
return ret;
}, arguments) };
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.call(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbg_io_5a3c8ea72d8c6ea3 = function() { return handleError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
const ret = io(getStringFromWasm0(arg0, arg1));
return ret;
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments) };
imports.wbg.__wbg_log_11652c6a56eeddfb = function(arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_44(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return ret;
} finally {
state0.a = state0.b = 0;
}
};
imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
const ret = new Error();
return ret;
};
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return ret;
};
imports.wbg.__wbg_now_8dddb61fa4928554 = function() {
const ret = Date.now();
return ret;
};
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
queueMicrotask(arg0);
};
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
const ret = arg0.queueMicrotask;
return ret;
};
imports.wbg.__wbg_random_57c118f142535bb6 = function() {
const ret = Math.random();
return ret;
};
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
const ret = Promise.resolve(arg0);
return ret;
};
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
const ret = typeof globalThis === 'undefined' ? null : globalThis;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
const ret = typeof self === 'undefined' ? null : self;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
const ret = arg0.then(arg1);
return ret;
};
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = arg0.original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbindgen_closure_wrapper1077 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 355, __wbg_adapter_18);
return ret;
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(arg0) === 'function';
return ret;
};
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = arg0 === undefined;
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = arg1;
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('rudus_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

BIN
pkg/rudus_bg.wasm (Stored with Git LFS)

Binary file not shown.

View File

@ -1,14 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const ludus: (a: number, b: number) => any;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_2: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export_6: WebAssembly.Table;
export const closure354_externref_shim: (a: number, b: number, c: any) => void;
export const closure367_externref_shim: (a: number, b: number, c: any, d: any) => void;
export const __wbindgen_start: () => void;

View File

@ -1,120 +0,0 @@
import {eq_vect, resolve_color, set_turtle_color, get_turtle_color, turtle_radius, rotate, turn_to_deg, command_to_state, turtle_init, background_color, turtle_angle, last} from "./turtle_geometry.js"
function hex (n) {
return n.toString(16).padStart(2, "0")
}
function svg_render_line (prev, curr) {
if (!prev.pendown) return ""
if (eq_vect(prev.position, curr.position)) return ""
const {position: [x1, y1], pencolor, penwidth} = prev
const {position: [x2, y2]} = curr
const [r, g, b, a] = resolve_color(pencolor)
return `
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="round" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
`
}
function escape_svg (svg) {
return svg
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export function extract_ludus (svg) {
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
return code
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, `"`)
.replace(/&apos;/g, `'`)
}
function svg_render_path (states) {
const path = []
for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1]
const curr = states[i]
path.push(svg_render_line(prev, curr))
}
return path.join("")
}
function svg_render_turtle (state) {
if (!state.visible) return ""
const [fr, fg, fb, fa] = get_turtle_color()
const fill_alpha = fa/255
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
const origin = [0, turtle_radius]
const [x1, y1] = origin
const [x2, y2] = rotate(origin, turtle_angle)
const [x3, y3] = rotate(origin, -turtle_angle)
const [pr, pg, pb, pa] = resolve_color(pencolor)
const pen_alpha = pa/255
const ink = pendown ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
return `
<g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
${ink}
</g>
`
}
// TODO: update this to match the new `p5` function
export function svg (commands, code) {
const all_states = {}
for (const command of commands) {
const [turtle_id, _, this_command] = command
let stack = all_states[turtle_id]
if (!stack) {
const new_stack = [turtle_init]
all_states[turtle_id] = new_stack
stack = new_stack
}
const prev_state = last(all_states[turtle_id])
const new_state = command_to_state(prev_state, this_command)
all_states[turtle_id].push(new_state)
}
let maxX = 0, maxY = 0, minX = 0, minY = 0
for (const states of Object.values(all_states)) {
for (const {position: [x, y]} of states) {
maxX = Math.max(maxX, x)
maxY = Math.max(maxY, y)
minX = Math.min(minX, x)
minY = Math.min(minY, y)
}
}
const [r, g, b] = resolve_color(background_color)
if ((r+g+b)/3 > 128) set_turtle_color([0, 0, 0, 150])
const view_width = Math.max((maxX - minX) * 1.2, 200)
const view_height = Math.max((maxY - minY) * 1.2, 200)
const margin = Math.max(view_width, view_height) * 0.1
const x_origin = minX - margin
const y_origin = -maxY - margin
let path = ""
let turtle = ""
for (const states of Object.values(all_states)) {
path = path + svg_render_path(states)
turtle = svg_render_turtle(last(states))
}
return `<?xml version="1.0" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">
<rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />
<g transform="scale(-1, 1) rotate(180)">
${path}
${turtle}
</g>
<ludus>
${escape_svg(code)}
</ludus>
</svg>
`
}

View File

@ -1,186 +0,0 @@
export const turtle_init = {
position: [0, 0],
heading: 0,
pendown: true,
pencolor: "white",
penwidth: 1,
visible: true
}
export const turtle_radius = 20
export const turtle_angle = 0.385
let turtle_color = [255, 255, 255, 150]
export function get_turtle_color () {
return turtle_color
}
export function set_turtle_color (new_color) {
turtle_color = new_color
}
export const colors = {
black: [0, 0, 0, 255],
silver: [192, 192, 192, 255],
gray: [128, 128, 128, 255],
grey: [128, 128, 128, 255],
white: [255, 255, 255, 255],
maroon: [128, 0, 0, 255],
red: [255, 0, 0, 255],
purple: [128, 0, 128, 255],
fuchsia: [255, 0, 255, 255],
green: [0, 128, 0, 255],
lime: [0, 255, 0, 255],
olive: [128, 128, 0, 255],
yellow: [255, 255, 0, 255],
navy: [0, 0, 128, 255],
blue: [0, 0, 255, 255],
teal: [0, 128, 128, 255],
aqua: [0, 255, 255, 255],
}
export function resolve_color (color) {
if (typeof color === 'string') return colors[color]
if (typeof color === 'number') return [color, color, color, 255]
if (Array.isArray(color)) return color
return [0, 0, 0, 255] // default to black?
}
export let background_color = "black"
export function add (v1, v2) {
const [x1, y1] = v1
const [x2, y2] = v2
return [x1 + x2, y1 + y2]
}
export function sub (v1, v2) {
const [x1, y1] = v1
const [x2, y2] = v2
return [x1 - x2, y1 - y2]
}
export function mult (vector, scalar) {
const [x, y] = vector
return [x * scalar, y * scalar]
}
export function unit_of (heading) {
const turns = -heading + 0.25
const radians = turn_to_rad(turns)
return [Math.cos(radians), Math.sin(radians)]
}
export function command_to_state (prev_state, curr_command) {
const [verb] = curr_command
switch (verb) {
case "goto": {
const [_, [x, y]] = curr_command
return {...prev_state, position: [x, y], clear: false}
}
case "home": {
return {...prev_state, position: [0, 0], heading: 0, clear: false}
}
case "right": {
const [_, angle] = curr_command
const {heading} = prev_state
return {...prev_state, heading: heading + angle, clear: false}
}
case "left": {
const [_, angle] = curr_command
const {heading} = prev_state
return {...prev_state, heading: heading - angle, clear: false}
}
case "forward": {
const [_, steps] = curr_command
const {heading, position} = prev_state
const unit = unit_of(heading)
const move = mult(unit, steps)
return {...prev_state, position: add(position, move), clear: false, forward: true}
}
case "back": {
const [_, steps] = curr_command
const {heading, position} = prev_state
const unit = unit_of(heading)
const move = mult(unit, -steps)
return {...prev_state, position: add(position, move), clear: false, forward: false}
}
case "penup": {
return {...prev_state, pendown: false, clear: false}
}
case "pendown": {
return {...prev_state, pendown: true, clear: false}
}
case "penwidth": {
const [_, width] = curr_command
return {...prev_state, penwidth: width, clear: false}
}
case "pencolor": {
const [_, color] = curr_command
return {...prev_state, pencolor: color, clear: false}
}
case "setheading": {
const [_, heading] = curr_command
return {...prev_state, heading: heading, clear: false}
}
case "loadstate": {
// console.log("LOADSTATE: ", curr_command)
const [_, [x, y], heading, visible, pendown, penwidth, pencolor] = curr_command
return {position: [x, y], heading, visible, pendown, penwidth, pencolor}
}
case "show": {
return {...prev_state, visible: true, clear: false}
}
case "hide": {
return {...prev_state, visible: false, clear: false}
}
case "background": {
background_color = curr_command[1]
return prev_state
}
case "clear": {
return {...prev_state, clear: true}
}
}
}
export function eq_vect (v1, v2) {
const [x1, y1] = v1
const [x2, y2] = v2
return (x1 === x2) && (y1 === y2)
}
export function eq_color (c1, c2) {
if (c1 === c2) return true
const res1 = resolve_color(c1)
const res2 = resolve_color(c2)
for (let i = 0; i < res1.length; ++i) {
if (res1[i] !== res2[i]) return false
}
return true
}
export function rotate (vector, heading) {
const radians = turn_to_rad(heading)
const [x, y] = vector
return [
(x * Math.cos (radians)) - (y * Math.sin (radians)),
(x * Math.sin (radians)) + (y * Math.cos (radians))
]
}
export function turn_to_rad (heading) {
return (heading % 1) * 2 * Math.PI
}
export function turn_to_deg (heading) {
return (heading % 1) * 360
}
export function last (arr) {
return arr[arr.length - 1]
}

View File

@ -1,55 +0,0 @@
import init, {ludus} from "./rudus.js";
let initialized_wasm = false
onmessage = run
// exposed in rust as:
// async fn io (out: String) -> Result<JsValue, JsValue>
// rust calls this to perform io
export function io (out) {
// only send messages if we have some
if (out.length > 0) postMessage(out)
// make an event handler that captures and delivers messages from the main thread
// because our promise resolution isn't about calculating a value but setting a global variable, we can't asyncify it
// explicitly return a promise
return new Promise((resolve, reject) => {
// deliver the response to ludus when we get a response from the main thread
onmessage = (e) => {
resolve(JSON.stringify(e.data))
}
// cancel the response if it takes too long
setTimeout(() => reject(`io took too long to respond to ${out}`), 1000)
})
}
// set as default event handler from main thread
async function run(e) {
// we must NEVER run `await init()` twice
if (!initialized_wasm) {
// this must come before the init call
initialized_wasm = true
await init()
console.log("Worker: Ludus has been initialized.")
}
// the data is always an array; we only really expect one member tho
let msgs = e.data
for (const msg of msgs) {
// evaluate source if we get some
if (msg.verb === "Run" && typeof msg.data === 'string') {
// temporarily stash an empty function so we don't keep calling this one if we receive additional messages
onmessage = () => {}
// actually run the ludus--which will call `io`--and replace `run` as the event handler for ipc
await ludus(msg.data)
// once we've returned from `ludus`, make this the event handler again
onmessage = run
} else {
// report and swallow any malformed startup messages
console.log("Worker: Did not get valid startup message. Instead got:")
console.log(e.data)
}
}
}

View File

@ -1,21 +1,16 @@
/* tslint:disable */
/* eslint-disable */
export function ludus(src: string): Promise<void>;
export function ludus(src: string): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly ludus: (a: number, b: number) => any;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly ludus: (a: number, b: number) => [number, number];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_6: WebAssembly.Table;
readonly closure354_externref_shim: (a: number, b: number, c: any) => void;
readonly closure367_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_start: () => void;
}

211
rudus.js Normal file
View File

@ -0,0 +1,211 @@
let wasm;
let WASM_VECTOR_LEN = 0;
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
/**
* @param {string} src
* @returns {string}
*/
export function ludus(src) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ludus(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_0;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('rudus_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

BIN
rudus_bg.wasm Normal file

Binary file not shown.

9
rudus_bg.wasm.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const ludus: (a: number, b: number) => [number, number];
export const __wbindgen_export_0: WebAssembly.Table;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_start: () => void;

View File

@ -1,473 +0,0 @@
use crate::spans::*;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StringPart {
Data(String),
Word(&'static str),
Inline(String),
}
impl fmt::Display for StringPart {
fn fmt(self: &StringPart, f: &mut fmt::Formatter) -> fmt::Result {
let rep = match self {
StringPart::Word(s) => format!("{{{s}}}"),
StringPart::Data(s) => s.to_string(),
StringPart::Inline(s) => s.to_string(),
};
write!(f, "{}", rep)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Ast {
// a special Error node
// may come in handy?
Error,
And,
Or,
// expression nodes
Placeholder,
Nil,
Boolean(bool),
Number(f64),
Keyword(&'static str),
Method(&'static str, Box<Spanned<Self>>),
Word(&'static str),
String(&'static str),
Interpolated(Vec<Spanned<StringPart>>),
Block(Vec<Spanned<Self>>),
If(Box<Spanned<Self>>, Box<Spanned<Self>>, Box<Spanned<Self>>),
Tuple(Vec<Spanned<Self>>),
Arguments(Vec<Spanned<Self>>),
List(Vec<Spanned<Self>>),
Dict(Vec<Spanned<Self>>),
Let(Box<Spanned<Self>>, Box<Spanned<Self>>),
LBox(&'static str, Box<Spanned<Self>>),
Synthetic(Box<Spanned<Self>>, Box<Spanned<Self>>, Vec<Spanned<Self>>),
When(Vec<Spanned<Self>>),
WhenClause(Box<Spanned<Self>>, Box<Spanned<Self>>),
Match(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Receive(Vec<Spanned<Self>>),
Spawn(Box<Spanned<Self>>),
MatchClause(
Box<Spanned<Self>>,
Box<Option<Spanned<Self>>>,
Box<Spanned<Self>>,
),
Fn(&'static str, Box<Spanned<Ast>>, Option<&'static str>),
FnBody(Vec<Spanned<Ast>>),
FnDeclaration(&'static str),
Panic(Box<Spanned<Self>>),
Do(Vec<Spanned<Self>>),
Repeat(Box<Spanned<Self>>, Box<Spanned<Self>>),
Splat(&'static str),
StringPair(&'static str, Box<Spanned<Self>>),
KeywordPair(&'static str, Box<Spanned<Self>>),
Loop(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Recur(Vec<Spanned<Self>>),
// pattern nodes
NilPattern,
BooleanPattern(bool),
NumberPattern(f64),
StringPattern(&'static str),
InterpolatedPattern(Vec<Spanned<StringPart>>),
KeywordPattern(&'static str),
WordPattern(&'static str),
AsPattern(&'static str, &'static str),
Splattern(Box<Spanned<Self>>),
PlaceholderPattern,
TuplePattern(Vec<Spanned<Self>>),
ListPattern(Vec<Spanned<Self>>),
StrPairPattern(&'static str, Box<Spanned<Self>>),
KeyPairPattern(&'static str, Box<Spanned<Self>>),
DictPattern(Vec<Spanned<Self>>),
}
impl Ast {
pub fn show(&self) -> String {
use Ast::*;
match self {
And => "and".to_string(),
Or => "or".to_string(),
Error => unreachable!(),
Nil | NilPattern => "nil".to_string(),
String(s) | StringPattern(s) => format!("\"{s}\""),
Interpolated(strs) | InterpolatedPattern(strs) => {
let mut out = "".to_string();
out = format!("\"{out}");
for (part, _) in strs {
out = format!("{out}{part}");
}
format!("{out}\"")
}
Boolean(b) | BooleanPattern(b) => b.to_string(),
Number(n) | NumberPattern(n) => n.to_string(),
Keyword(k) | KeywordPattern(k) => format!(":{k}"),
Method(m, args) => format!("::{m} {}", args.0),
Word(w) | WordPattern(w) => w.to_string(),
Block(lines) => {
let mut out = "{\n".to_string();
for (line, _) in lines {
out = format!("{out}\n {}", line.show());
}
format!("{out}\n}}")
}
If(cond, then, r#else) => format!(
"if {}\n then {}\n else {}",
cond.0.show(),
then.0.show(),
r#else.0.show()
),
Let(pattern, expression) => {
format!("let {} = {}", pattern.0.show(), expression.0.show())
}
Dict(entries) | DictPattern(entries) => {
format!(
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.show())
.collect::<Vec<_>>()
.join(", ")
)
}
List(members) | ListPattern(members) => format!(
"[{}]",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Arguments(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Tuple(members) | TuplePattern(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Synthetic(root, first, rest) => format!(
"{} {} {}",
root.0.show(),
first.0.show(),
rest.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" ")
),
When(clauses) | Receive(clauses) => format!(
"when {{\n {}\n}}",
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Placeholder | PlaceholderPattern => "_".to_string(),
LBox(name, rhs) => format!("box {name} = {}", rhs.0.show()),
Match(scrutinee, clauses) => format!(
"match {} with {{\n {}\n}}",
scrutinee.0.show(),
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
FnBody(clauses) => clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n "),
Fn(name, body, doc) => {
let mut out = format!("fn {name} {{\n");
if let Some(doc) = doc {
out = format!("{out} {doc}\n");
}
format!("{out} {}\n}}", body.0.show())
}
FnDeclaration(name) => format!("fn {name}"),
Panic(expr) => format!("panic! {}", expr.0.show()),
Do(terms) => {
format!(
"do {}",
terms
.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" > ")
)
}
Spawn(body) => format!("spawn! {}", body.0.show()),
Repeat(times, body) => format!("repeat {} {{\n{}\n}}", times.0.show(), body.0.show()),
Splat(word) => format!("...{}", word),
Splattern(pattern) => format!("...{}", pattern.0.show()),
AsPattern(word, type_keyword) => format!("{word} as :{type_keyword}"),
KeywordPair(key, value) | KeyPairPattern(key, value) => {
format!(":{key} {}", value.0.show())
}
StringPair(key, value) | StrPairPattern(key, value) => {
format!("\"{key}\" {}", value.0.show())
}
Loop(init, body) => format!(
"loop {} with {{\n {}\n}}",
init.0.show(),
body.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Recur(args) => format!(
"recur ({})",
args.iter()
.map(|(arg, _)| arg.show())
.collect::<Vec<_>>()
.join(", ")
),
MatchClause(pattern, guard, body) => {
let mut out = pattern.0.show();
if let Some(guard) = guard.as_ref() {
out = format!("{out} if {}", guard.0.show());
}
format!("{out} -> {}", body.0.show())
}
WhenClause(cond, body) => format!("{} -> {}", cond.0.show(), body.0.show()),
}
}
}
impl fmt::Display for Ast {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Ast::*;
match self {
And => write!(f, "And"),
Or => write!(f, "Or"),
Error => write!(f, "Error"),
Nil => write!(f, "nil"),
String(s) => write!(f, "String: \"{}\"", s),
Interpolated(strs) => {
write!(
f,
"Interpolated: \"{}\"",
strs.iter()
.map(|(s, _)| s.to_string())
.collect::<Vec<_>>()
.join("")
)
}
Boolean(b) => write!(f, "Boolean: {}", b),
Number(n) => write!(f, "Number: {}", n),
Keyword(k) => write!(f, "Keyword: :{}", k),
Method(m, args) => write!(f, "Method: ::{m} ({})", args.0),
Word(w) => write!(f, "Word: {}", w),
Block(b) => write!(
f,
"Block: <{}>",
b.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
If(cond, then_branch, else_branch) => write!(
f,
"If: {} Then: {} Else: {}",
cond.0, then_branch.0, else_branch.0
),
Let(pattern, expression) => {
write!(f, "Let: {} = {}", pattern.0, expression.0)
}
Dict(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|pair| pair.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
List(l) => write!(
f,
"List: [{}]",
l.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Arguments(a) => write!(
f,
"Arguments: ({})",
a.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Tuple(t) => write!(
f,
"Tuple: ({})",
t.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Synthetic(root, first, rest) => write!(
f,
"Synth: [{}, {}, {}]",
root.0,
first.0,
rest.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join("\n")
),
When(clauses) | Receive(clauses) => write!(
f,
"When: [{}]",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Placeholder => write!(f, "Placeholder"),
LBox(_name, _rhs) => todo!(),
Match(value, clauses) => {
write!(
f,
"match: {} with {}",
&value.0.to_string(),
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
FnBody(clauses) => {
write!(
f,
"{}",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Fn(name, body, ..) => {
write!(f, "fn: {name}\n{}", body.0)
}
FnDeclaration(_name) => todo!(),
Panic(_expr) => todo!(),
Do(terms) => {
write!(
f,
"do: {}",
terms
.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join(" > ")
)
}
Spawn(body) => write!(f, "spawn: {}", body.0),
Repeat(_times, _body) => todo!(),
Splat(word) => {
write!(f, "splat: {}", word)
}
KeywordPair(k, v) | KeyPairPattern(k, v) => {
write!(f, "key_pair: {} {}", k, v.0)
}
StringPair(k, v) | StrPairPattern(k, v) => {
write!(f, "str_pair: {k} {}", v.0)
}
Loop(init, body) => {
write!(
f,
"loop: {} with {}",
init.0,
body.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Recur(args) => {
write!(
f,
"recur: {}",
args.iter()
.map(|(arg, _)| arg.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
MatchClause(pattern, guard, body) => {
write!(
f,
"match clause: {} if {:?} -> {}",
pattern.0, guard, body.0
)
}
WhenClause(cond, body) => {
write!(f, "when clause: {} -> {}", cond.0, body.0)
}
NilPattern => write!(f, "nil"),
BooleanPattern(b) => write!(f, "{}", b),
NumberPattern(n) => write!(f, "{}", n),
StringPattern(s) => write!(f, "{}", s),
KeywordPattern(k) => write!(f, ":{}", k),
WordPattern(w) => write!(f, "{}", w),
AsPattern(w, t) => write!(f, "{} as :{}", w, t),
Splattern(p) => write!(f, "...{}", p.0),
PlaceholderPattern => write!(f, "_"),
TuplePattern(t) => write!(
f,
"({})",
t.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
ListPattern(l) => write!(
f,
"({})",
l.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
DictPattern(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.to_string())
.collect::<Vec<_>>()
.join(", ")
),
InterpolatedPattern(strprts) => write!(
f,
"interpolated: \"{}\"",
strprts
.iter()
.map(|part| part.0.to_string())
.collect::<Vec<_>>()
.join("")
),
}
}
}

View File

@ -1,780 +0,0 @@
use crate::js::*;
use crate::value::*;
use imbl::*;
use std::rc::Rc;
#[derive(Clone, Debug, Hash)]
pub enum BaseFn {
Nullary(&'static str, fn() -> Value),
Unary(&'static str, fn(&Value) -> Value),
Binary(&'static str, fn(&Value, &Value) -> Value),
Ternary(&'static str, fn(&Value, &Value, &Value) -> Value),
}
pub fn eq(x: &Value, y: &Value) -> Value {
if x == y {
Value::True
} else {
Value::False
}
}
pub fn add(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => Value::Number(x + y),
_ => unreachable!("internal Ludus error: wrong arguments to base add: {x}, {y}"),
}
}
pub fn sub(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => Value::Number(x - y),
_ => unreachable!("internal Ludus error"),
}
}
pub fn unbox(x: &Value) -> Value {
match x {
Value::Box(cell) => cell.as_ref().borrow().clone(),
_ => unreachable!("internal Ludus error"),
}
}
pub fn store(b: &Value, val: &Value) -> Value {
if let Value::Box(cell) = b {
cell.replace(val.clone());
val.clone()
} else {
unreachable!("internal Ludus error")
}
}
// TODO: do better than returning just the docstr
// name, patterns, AND docstring
pub fn doc(f: &Value) -> Value {
let f = f.as_fn();
let name = f.name();
let patterns = f.patterns();
let docstr = f.doc();
Value::from_string(format!(
"{name}\n{}\n{}",
patterns.stringify(),
docstr.stringify()
))
}
pub fn assoc(dict: &Value, key: &Value, value: &Value) -> Value {
match (dict, key) {
(Value::Dict(d), Value::Keyword(k)) => {
Value::Dict(Box::new(d.update(Key::Keyword(k), value.clone())))
}
(Value::Dict(d), Value::Interned(k)) => {
Value::Dict(Box::new(d.update(Key::Interned(k), value.clone())))
}
(Value::Dict(d), Value::String(s)) => {
Value::Dict(Box::new(d.update(Key::String(s.clone()), value.clone())))
}
_ => unreachable!("internal Ludus error calling assoc with ({dict}, {key}, {value})"),
}
}
pub fn r#bool(x: &Value) -> Value {
match x {
Value::Nil | Value::False => Value::False,
_ => Value::True,
}
}
pub fn chars(x: &Value) -> Value {
match x {
Value::Interned(s) => {
let mut charlist = vector![];
for char in s.chars() {
charlist.push_back(Value::from_string(char.to_string()))
}
Value::Tuple(Rc::new(vec![
Value::Keyword("ok"),
Value::List(Box::new(charlist)),
]))
}
Value::String(s) => {
let mut charlist = vector![];
for char in s.chars() {
charlist.push_back(Value::from_string(char.to_string()))
}
Value::Tuple(Rc::new(vec![
Value::Keyword("ok"),
Value::List(Box::new(charlist)),
]))
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn words(str: &Value) -> Value {
// console_log!("wordsing words");
let str = Value::as_string(str);
let mut words = Vector::new();
let mut word = String::new();
for char in str.chars() {
if char.is_alphanumeric() || char == '\'' {
word.push(char)
} else if !word.is_empty() {
words.push_back(Value::from_string(word));
word = String::new()
}
}
// console_log!("words gathered into vector; returning to ludus");
Value::list(words)
}
pub fn index_of(scrutinee: &Value, target: &Value) -> Value {
let scrutinee = scrutinee.as_list();
for (i, value) in scrutinee.iter().enumerate() {
if value == target {
return Value::from_usize(i);
}
}
Value::Nil
}
pub fn indices_of(scrutinee: &Value, target: &Value) -> Value {
let scrutinee = scrutinee.as_list();
let mut indices = Vector::new();
for (i, value) in scrutinee.iter().enumerate() {
if value == target {
indices.push_back(Value::from_usize(i))
}
}
Value::list(indices)
}
// TODO: figure out how to get to opportunistic mutation here
pub fn concat(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Interned(x), Value::Interned(y)) => Value::String(Rc::new(format!("{x}{y}"))),
(Value::String(x), Value::String(y)) => Value::String(Rc::new(format!("{x}{y}"))),
(Value::String(x), Value::Interned(y)) => Value::String(Rc::new(format!("{x}{y}"))),
(Value::Interned(x), Value::String(y)) => Value::String(Rc::new(format!("{x}{y}"))),
(Value::List(x), Value::List(y)) => {
let mut newlist = *x.clone();
newlist.append(*y.clone());
Value::List(Box::new(newlist))
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn append(x: &Value, y: &Value) -> Value {
match x {
Value::List(list) => {
let mut newlist = list.clone();
newlist.push_back(y.clone());
Value::List(newlist)
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn dec(x: &Value) -> Value {
Value::from_f64(x.as_f64() - 1.0)
}
pub fn inc(x: &Value) -> Value {
Value::from_f64(x.as_f64() + 1.0)
}
pub fn div(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => Value::Number(x / y),
_ => unreachable!("internal Ludus error"),
}
}
pub fn mult(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => Value::Number(x * y),
_ => unreachable!("internal Ludus error"),
}
}
pub fn pow(x: &Value, y: &Value) -> Value {
let x = x.as_f64();
let y = y.as_f64();
Value::from_f64(x.powf(y))
}
pub fn dissoc(dict: &Value, key: &Value) -> Value {
match (dict, key) {
(Value::Dict(dict), Value::Keyword(key)) => {
let mut new = dict.clone();
new.remove(&Key::Keyword(key));
Value::Dict(new)
}
(Value::Dict(dict), Value::Interned(key)) => {
let mut new = dict.clone();
new.remove(&Key::Interned(key));
Value::Dict(new)
}
(Value::Dict(dict), Value::String(key)) => {
let mut new = dict.clone();
new.remove(&Key::String(key.clone()));
Value::Dict(new)
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn first(ordered: &Value) -> Value {
match ordered {
Value::List(list) => match list.front() {
Some(n) => n.clone(),
None => Value::Nil,
},
Value::Tuple(tuple) => match tuple.first() {
Some(n) => n.clone(),
None => Value::Nil,
},
Value::String(s) => match s.chars().next() {
Some(char) => Value::from_string(char.to_string()),
None => Value::Nil,
},
Value::Interned(s) => match s.chars().next() {
Some(char) => Value::from_string(char.to_string()),
None => Value::Nil,
},
_ => unreachable!("internal Ludus error"),
}
}
// TODO: figure out how to handle negative numbers
// the cast from f64 to usize discards sign info
pub fn at(ordered: &Value, i: &Value) -> Value {
match (ordered, i) {
(Value::List(list), Value::Number(n)) => {
let i = f64::from(*n) as usize;
match list.get(i) {
Some(n) => n.clone(),
None => Value::Nil,
}
}
(Value::Tuple(tuple), Value::Number(n)) => {
let i = f64::from(*n) as usize;
match tuple.get(i) {
Some(n) => n.clone(),
None => Value::Nil,
}
}
(Value::String(s), Value::Number(n)) => {
let i = f64::from(*n) as usize;
match s.chars().nth(i) {
Some(n) => Value::from_string(n.to_string()),
None => Value::Nil,
}
}
(Value::Interned(s), Value::Number(n)) => {
let i = f64::from(*n) as usize;
match s.chars().nth(i) {
Some(n) => Value::from_string(n.to_string()),
None => Value::Nil,
}
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn get(dict: &Value, key: &Value) -> Value {
match (dict, key) {
(Value::Dict(dict), Value::Keyword(key)) => match dict.get(&Key::Keyword(key)) {
Some(x) => x.clone(),
None => Value::Nil,
},
(Value::Dict(dict), Value::Interned(key)) => match dict.get(&Key::Interned(key)) {
Some(x) => x.clone(),
None => Value::Nil,
},
(Value::Dict(dict), Value::String(key)) => match dict.get(&Key::String(key.clone())) {
Some(x) => x.clone(),
None => Value::Nil,
},
_ => unreachable!("internal Ludus error"),
}
}
pub fn last(ordered: &Value) -> Value {
match ordered {
Value::List(list) => match list.last() {
Some(x) => x.clone(),
None => Value::Nil,
},
Value::Tuple(tuple) => match tuple.last() {
Some(x) => x.clone(),
None => Value::Nil,
},
Value::String(s) => match s.chars().last() {
Some(char) => Value::from_string(char.to_string()),
None => Value::Nil,
},
Value::Interned(s) => match s.chars().last() {
Some(char) => Value::from_string(char.to_string()),
None => Value::Nil,
},
_ => unreachable!("internal Ludus error"),
}
}
pub fn print(x: &Value) -> Value {
let Value::List(args) = x else {
unreachable!("internal Ludus error")
};
let out = args
.iter()
.map(|val| format!("{val}"))
.collect::<Vec<_>>()
.join(" ");
console_log!("{out}");
Value::Keyword("ok")
}
pub fn show(x: &Value) -> Value {
Value::String(Rc::new(format!("{x}")))
}
pub fn rest(ordered: &Value) -> Value {
match ordered {
Value::List(list) => Value::List(Box::new(list.clone().split_at(1).1)),
Value::Tuple(tuple) => {
Value::List(Box::new(Vector::from_iter(tuple.iter().next().cloned())))
}
Value::Interned(str) => Value::String(Rc::new(str.get(1..).unwrap_or("").to_string())),
Value::String(str) => Value::String(Rc::new(
str.clone().as_str().get(1..).unwrap_or("").to_string(),
)),
_ => unreachable!("internal Ludus error"),
}
}
pub fn count(coll: &Value) -> Value {
match coll {
Value::Dict(d) => Value::from_usize(d.len()),
Value::List(l) => Value::from_usize(l.len()),
Value::Tuple(t) => Value::from_usize(t.len()),
Value::String(s) => Value::from_usize(s.len()),
Value::Interned(s) => Value::from_usize(s.len()),
_ => unreachable!("internal Ludus error"),
}
}
pub fn range(start: &Value, end: &Value) -> Value {
match (start, end) {
(Value::Number(start), Value::Number(end)) => {
let start = f64::from(*start) as isize;
let end = f64::from(*end) as isize;
let mut range = Vector::new();
for n in start..end {
range.push_back(Value::from_usize(n as usize))
}
Value::List(Box::new(range))
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn slice(ordered: &Value, start: &Value, end: &Value) -> Value {
match (ordered, start, end) {
(Value::List(list), Value::Number(start), Value::Number(end)) => {
let mut newlist = list.clone();
let start = std::cmp::max(f64::from(*start) as usize, 0);
let end = std::cmp::min(f64::from(*end) as usize, list.len());
Value::List(Box::new(newlist.slice(start..end)))
}
// TODO: figure out something better to do than return an empty string on a bad slice
(Value::String(string), Value::Number(start), Value::Number(end)) => {
let start = std::cmp::max(f64::from(*start) as usize, 0);
let end = std::cmp::min(f64::from(*end) as usize, string.len());
Value::String(Rc::new(
string
.clone()
.as_str()
.get(start..end)
.unwrap_or("")
.to_string(),
))
}
(Value::Interned(string), Value::Number(start), Value::Number(end)) => {
let start = std::cmp::max(f64::from(*start) as usize, 0);
let end = std::cmp::min(f64::from(*end) as usize, string.len());
Value::String(Rc::new(string.get(start..end).unwrap_or("").to_string()))
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn list(x: &Value) -> Value {
match x {
Value::List(_) => x.clone(),
Value::Tuple(t) => Value::List(Box::new(Vector::from_iter(t.iter().cloned()))),
Value::Dict(d) => {
let kvs = d.iter();
let mut list = vector![];
for (key, value) in kvs {
let kv = Value::Tuple(Rc::new(vec![key.to_value(), value.clone()]));
list.push_back(kv);
}
Value::List(Box::new(list))
}
_ => Value::List(Box::new(vector![x.clone()])),
}
}
pub fn number(x: &Value) -> Value {
match x {
Value::Interned(string) => match string.parse::<f64>() {
Ok(n) => Value::Tuple(Rc::new(vec![Value::Keyword("ok"), Value::from_f64(n)])),
Err(_) => Value::Tuple(Rc::new(vec![
Value::Keyword("err"),
Value::String(Rc::new(format!("could not parse `{string}` as a number"))),
])),
},
Value::String(string) => match string.parse::<f64>() {
Ok(n) => Value::Tuple(Rc::new(vec![Value::Keyword("ok"), Value::from_f64(n)])),
Err(_) => Value::Tuple(Rc::new(vec![
Value::Keyword("err"),
Value::String(Rc::new(format!("could not parse `{string}` as a number"))),
])),
},
_ => unreachable!("internal Ludus error"),
}
}
pub fn r#type(x: &Value) -> Value {
match x {
Value::Nil => Value::Keyword("nil"),
Value::Number(_) => Value::Keyword("number"),
Value::True | Value::False => Value::Keyword("bool"),
Value::Keyword(_) => Value::Keyword("keyword"),
Value::Tuple(_) => Value::Keyword("tuple"),
Value::Interned(_) => Value::Keyword("string"),
Value::String(_) => Value::Keyword("string"),
Value::List(_) => Value::Keyword("list"),
Value::Dict(_) => Value::Keyword("dict"),
Value::Fn(_) => Value::Keyword("fn"),
Value::Box(_) => Value::Keyword("box"),
Value::BaseFn(_) => Value::Keyword("fn"),
Value::Partial(_) => Value::Keyword("fn"),
Value::Process => Value::Keyword("process"),
Value::Nothing => unreachable!(),
}
}
pub fn split(source: &Value, splitter: &Value) -> Value {
match (source, splitter) {
(Value::String(source), Value::String(splitter)) => {
let parts = source.split(splitter.as_str());
let mut list = vector![];
for part in parts {
list.push_back(Value::String(Rc::new(part.to_string())));
}
Value::List(Box::new(list))
}
(Value::String(_), Value::Interned(splitter)) => {
split(source, &Value::String(Rc::new(splitter.to_string())))
}
(Value::Interned(source), Value::String(_)) => {
split(&Value::String(Rc::new(source.to_string())), splitter)
}
(Value::Interned(source), Value::Interned(splitter)) => {
let source = Value::String(Rc::new(source.to_string()));
let splitter = Value::String(Rc::new(splitter.to_string()));
split(&source, &splitter)
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn upcase(string: &Value) -> Value {
match string {
Value::String(string) => Value::String(Rc::new(string.to_uppercase())),
Value::Interned(string) => Value::String(Rc::new(string.to_uppercase())),
_ => unreachable!("internal Ludus error"),
}
}
pub fn downcase(string: &Value) -> Value {
match string {
Value::String(string) => Value::String(Rc::new(string.to_lowercase())),
Value::Interned(string) => Value::String(Rc::new(string.to_lowercase())),
_ => unreachable!("internal Ludus error"),
}
}
pub fn trim(string: &Value) -> Value {
match string {
Value::String(string) => Value::String(Rc::new(string.trim().to_string())),
Value::Interned(string) => Value::String(Rc::new(string.trim().to_string())),
_ => unreachable!("internal Ludus error"),
}
}
pub fn triml(string: &Value) -> Value {
match string {
Value::String(string) => Value::String(Rc::new(string.trim_start().to_string())),
Value::Interned(string) => Value::String(Rc::new(string.trim_start().to_string())),
_ => unreachable!("internal Ludus error"),
}
}
pub fn trimr(string: &Value) -> Value {
match string {
Value::String(string) => Value::String(Rc::new(string.trim_end().to_string())),
Value::Interned(string) => Value::String(Rc::new(string.trim_end().to_string())),
_ => unreachable!("internal Ludus error"),
}
}
pub fn atan_2(x: &Value, y: &Value) -> Value {
let x = x.as_f64();
let y = y.as_f64();
Value::from_f64(x.atan2(y))
}
pub fn ceil(x: &Value) -> Value {
Value::from_f64(x.as_f64().ceil())
}
pub fn cos(x: &Value) -> Value {
Value::from_f64(x.as_f64().cos())
}
pub fn floor(x: &Value) -> Value {
Value::from_f64(x.as_f64().floor())
}
pub fn base_random() -> Value {
Value::from_f64(random())
}
pub fn round(x: &Value) -> Value {
Value::from_f64(x.as_f64().round())
}
pub fn sin(x: &Value) -> Value {
Value::from_f64(x.as_f64().sin())
}
pub fn sqrt(x: &Value) -> Value {
Value::from_f64(x.as_f64().sqrt())
}
pub fn tan(x: &Value) -> Value {
Value::from_f64(x.as_f64().tan())
}
pub fn gt(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => {
if x > y {
Value::True
} else {
Value::False
}
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn gte(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => {
if x >= y {
Value::True
} else {
Value::False
}
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn lt(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => {
if x < y {
Value::True
} else {
Value::False
}
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn lte(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => {
if x <= y {
Value::True
} else {
Value::False
}
}
_ => unreachable!("internal Ludus error"),
}
}
pub fn r#mod(x: &Value, y: &Value) -> Value {
match (x, y) {
(Value::Number(x), Value::Number(y)) => Value::Number(x % y),
_ => unreachable!("internal Ludus error"),
}
}
pub fn make_base() -> Value {
let members = vec![
("add", Value::BaseFn(Box::new(BaseFn::Binary("add", add)))),
(
"append",
Value::BaseFn(Box::new(BaseFn::Binary("append", append))),
),
(
"assoc",
Value::BaseFn(Box::new(BaseFn::Ternary("assoc", assoc))),
),
("at", Value::BaseFn(Box::new(BaseFn::Binary("at", at)))),
(
"atan_2",
Value::BaseFn(Box::new(BaseFn::Binary("atan_2", atan_2))),
),
(
"bool",
Value::BaseFn(Box::new(BaseFn::Unary("bool", r#bool))),
),
("ceil", Value::BaseFn(Box::new(BaseFn::Unary("ceil", ceil)))),
(
"chars",
Value::BaseFn(Box::new(BaseFn::Unary("chars", chars))),
),
(
"concat",
Value::BaseFn(Box::new(BaseFn::Binary("concat", concat))),
),
("cos", Value::BaseFn(Box::new(BaseFn::Unary("cos", cos)))),
(
"count",
Value::BaseFn(Box::new(BaseFn::Unary("count", count))),
),
("dec", Value::BaseFn(Box::new(BaseFn::Unary("dec", dec)))),
(
"dissoc",
Value::BaseFn(Box::new(BaseFn::Binary("dissoc", dissoc))),
),
("div", Value::BaseFn(Box::new(BaseFn::Binary("div", div)))),
("doc", Value::BaseFn(Box::new(BaseFn::Unary("doc", doc)))),
(
"downcase",
Value::BaseFn(Box::new(BaseFn::Unary("downcase", downcase))),
),
("eq?", Value::BaseFn(Box::new(BaseFn::Binary("eq?", eq)))),
(
"first",
Value::BaseFn(Box::new(BaseFn::Unary("first", first))),
),
(
"floor",
Value::BaseFn(Box::new(BaseFn::Unary("floor", floor))),
),
("get", Value::BaseFn(Box::new(BaseFn::Binary("get", get)))),
("gt?", Value::BaseFn(Box::new(BaseFn::Binary("gt?", gt)))),
("gte?", Value::BaseFn(Box::new(BaseFn::Binary("gte?", gte)))),
("inc", Value::BaseFn(Box::new(BaseFn::Unary("inc", inc)))),
(
"index_of",
Value::BaseFn(Box::new(BaseFn::Binary("index_of", index_of))),
),
(
"indices_of",
Value::BaseFn(Box::new(BaseFn::Binary("indices_of", indices_of))),
),
("last", Value::BaseFn(Box::new(BaseFn::Unary("last", last)))),
("list", Value::BaseFn(Box::new(BaseFn::Unary("list", list)))),
("lt?", Value::BaseFn(Box::new(BaseFn::Binary("lt?", lt)))),
("lte?", Value::BaseFn(Box::new(BaseFn::Binary("lte?", lte)))),
("mod", Value::BaseFn(Box::new(BaseFn::Binary("mod", r#mod)))),
(
"mult",
Value::BaseFn(Box::new(BaseFn::Binary("mult", mult))),
),
(
"number",
Value::BaseFn(Box::new(BaseFn::Unary("number", number))),
),
("pi", Value::from_f64(std::f64::consts::PI)),
("pow", Value::BaseFn(Box::new(BaseFn::Binary("pow", pow)))),
(
"print!",
Value::BaseFn(Box::new(BaseFn::Unary("print!", print))),
),
("process", Value::Process),
(
"random",
Value::BaseFn(Box::new(BaseFn::Nullary("random", base_random))),
),
(
"range",
Value::BaseFn(Box::new(BaseFn::Binary("range", range))),
),
("rest", Value::BaseFn(Box::new(BaseFn::Unary("rest", rest)))),
(
"round",
Value::BaseFn(Box::new(BaseFn::Unary("round", round))),
),
("show", Value::BaseFn(Box::new(BaseFn::Unary("show", show)))),
("sin", Value::BaseFn(Box::new(BaseFn::Unary("sin", sin)))),
(
"slice",
Value::BaseFn(Box::new(BaseFn::Ternary("slice", slice))),
),
(
"split",
Value::BaseFn(Box::new(BaseFn::Binary("split", split))),
),
("sqrt", Value::BaseFn(Box::new(BaseFn::Unary("sqrt", sqrt)))),
("sqrt_2", Value::from_f64(std::f64::consts::SQRT_2)),
(
"store!",
Value::BaseFn(Box::new(BaseFn::Binary("store!", store))),
),
("sub", Value::BaseFn(Box::new(BaseFn::Binary("sub", sub)))),
("tan", Value::BaseFn(Box::new(BaseFn::Unary("tan", tan)))),
("trim", Value::BaseFn(Box::new(BaseFn::Unary("trim", trim)))),
(
"triml",
Value::BaseFn(Box::new(BaseFn::Unary("triml", triml))),
),
(
"trimr",
Value::BaseFn(Box::new(BaseFn::Unary("trimr", trimr))),
),
(
"type",
Value::BaseFn(Box::new(BaseFn::Unary("type", r#type))),
),
(
"unbox",
Value::BaseFn(Box::new(BaseFn::Unary("unbox", unbox))),
),
(
"upcase",
Value::BaseFn(Box::new(BaseFn::Unary("upcase", upcase))),
),
(
"words",
Value::BaseFn(Box::new(BaseFn::Unary("words", words))),
),
];
let members = members
.iter()
.map(|(name, bfn)| (Key::Keyword(name), bfn.clone()))
.collect::<Vec<_>>();
Value::Dict(Box::new(HashMap::from(members)))
}

View File

@ -1,102 +0,0 @@
use crate::js::*;
use crate::op::Op;
use crate::value::{Key, Value};
use chumsky::prelude::SimpleSpan;
use imbl::HashMap;
use num_traits::FromPrimitive;
use regex::Regex;
#[derive(Clone, Debug)]
pub struct StrPattern {
pub words: Vec<&'static str>,
pub re: Regex,
}
#[derive(Clone, Debug)]
pub struct Chunk {
pub constants: Vec<Value>,
pub bytecode: Vec<u8>,
pub keywords: Vec<&'static str>,
pub string_patterns: Vec<StrPattern>,
pub env: HashMap<Key, Value>,
pub msgs: Vec<String>,
pub spans: Vec<SimpleSpan>,
pub src: &'static str,
pub input: &'static str,
}
impl std::fmt::Display for Chunk {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Chunk.")
}
}
impl Chunk {
pub fn dissasemble_instr(&self, i: &mut usize) {
let op = Op::from_u8(self.bytecode[*i]).unwrap();
use Op::*;
match op {
Pop | Store | Stash | Load | Nil | True | False | MatchNil | MatchTrue | MatchFalse
| ResetMatch | GetKey | PanicWhenFallthrough | PanicNoMatch | PanicNoFnMatch
| PanicNoLetMatch | TypeOf | Duplicate | Decrement | ToInt | Noop | LoadTuple
| LoadList | Eq | Add | Sub | Mult | Div | Unbox | BoxStore | Assert | Get | At
| Not | Panic | EmptyString | ConcatStrings | Stringify | MatchType | Return
| UnconditionalMatch | Print | AppendList | ConcatList | PushList | PushDict
| AppendDict | ConcatDict | Nothing | PushGlobal | SetUpvalue | LoadMessage
| NextMessage | MatchMessage | ClearMessage | SendMethod | LoadScrutinee | Spawn => {
console_log!("{} | {i:04}: {op}", self.spans[*i])
}
Constant | MatchConstant => {
let high = self.bytecode[*i + 1];
let low = self.bytecode[*i + 2];
let idx = ((high as usize) << 8) + low as usize;
let value = &self.constants[idx].show();
console_log!(
"{} | {i:04}: {:16} {idx:05}: {value}",
self.spans[*i],
op.to_string()
);
*i += 2;
}
Msg => {
let msg_idx = self.bytecode[*i + 1];
let msg = &self.msgs[msg_idx as usize];
console_log!("{} | {i:04}: {msg}", self.spans[*i]);
*i += 1;
}
PushBinding | MatchTuple | MatchSplattedTuple | LoadSplattedTuple | MatchList
| MatchSplattedList | LoadSplattedList | MatchDict | MatchSplattedDict
| DropDictEntry | LoadDictValue | PushTuple | PushBox | MatchDepth | PopN | StoreN
| Call | GetUpvalue | Partial | MatchString | PushStringMatches | TailCall | LoadN => {
let next = self.bytecode[*i + 1];
console_log!(
"{} | {i:04}: {:16} {next:03}",
self.spans[*i],
op.to_string()
);
*i += 1;
}
Jump | JumpIfFalse | JumpIfTrue | JumpIfNoMatch | JumpIfMatch | JumpBack
| JumpIfZero => {
let high = self.bytecode[*i + 1];
let low = self.bytecode[*i + 2];
let len = ((high as u16) << 8) + low as u16;
console_log!(
"{} | {i:04}: {:16} {len:05}",
self.spans[*i],
op.to_string()
);
*i += 2;
}
}
}
pub fn dissasemble(&self) {
console_log!("IDX | CODE | INFO");
let mut i = 0;
while i < self.bytecode.len() {
self.dissasemble_instr(&mut i);
i += 1;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,177 +0,0 @@
use crate::js::*;
use crate::lexer::Token;
use crate::panic::{Panic, PanicMsg};
use crate::validator::VErr;
use crate::vm::CallFrame;
use chumsky::error::RichPattern;
use chumsky::prelude::*;
const SEPARATOR: &str = "\n\n";
pub fn line_number(src: &'static str, span: SimpleSpan) -> usize {
src.chars().take(span.start).filter(|c| *c == '\n').count()
}
fn get_line(src: &'static str, line: usize) -> String {
src.split("\n").nth(line).unwrap().to_string()
}
pub fn lexing(errs: Vec<Rich<'static, char>>, src: &'static str, input: &'static str) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(src, *err.span());
let line = get_line(src, line_number);
let char = src.chars().nth(err.span().start).unwrap();
msg.push(format!("Syntax error: unexpected {char}"));
msg.push(format!(" on line {} in {}", line_number + 1, input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"));
}
msgs.join(SEPARATOR)
}
pub fn validation(errs: Vec<VErr>) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(err.src, *err.span);
let line = get_line(err.src, line_number);
msg.push(format!("Validation error: {}", err.msg));
msg.push(format!(" on line {} in {}", line_number + 1, err.input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"));
}
msgs.join(SEPARATOR)
}
pub fn parsing(errs: Vec<Rich<'static, Token>>, src: &'static str, input: &'static str) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(src, *err.span());
let line = get_line(src, line_number);
let details = parsing_message(err);
msg.push(format!("Syntax error: {}", details));
msg.push(format!(" on line {} in {}", line_number + 1, input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"))
}
msgs.join(SEPARATOR)
}
fn parsing_message(err: Rich<'static, Token>) -> String {
let found = match err.found() {
Some(token) => token.show(),
None => "end of input".to_string(),
};
let expected = err.expected();
let mut expecteds = vec![];
for pattern in expected {
let shown = match pattern {
RichPattern::Token(t) => t.show(),
RichPattern::Label(s) => s.to_string(),
RichPattern::Identifier(s) => s.clone(),
RichPattern::Any => "any".to_string(),
RichPattern::SomethingElse => "something else".to_string(),
RichPattern::EndOfInput => "eof".to_string(),
};
expecteds.push(shown);
}
let expecteds = if expecteds.iter().any(|e| e == &"else".to_string()) {
vec!["else".to_string()]
} else {
expecteds
};
let expecteds = if expecteds.iter().any(|e| e == &"then".to_string()) {
vec!["then".to_string()]
} else {
expecteds
};
let expecteds = expecteds.join(" | ");
format!("Ludus did not expect to see: {found}\n expected: {expecteds}")
}
pub fn panic(panic: Panic) -> String {
// console_log!("Ludus panicked!: {panic}");
// panic.call_stack.last().unwrap().chunk().dissasemble();
// console_log!("{:?}", panic.call_stack.last().unwrap().chunk().spans);
let Panic {
msg,
scrutinee,
call_stack,
} = &panic;
match msg {
PanicMsg::Generic(msg) => {
let traceback = traceback(&panic);
format!("{msg}\n{traceback}")
}
PanicMsg::NoFnMatch => {
let name = call_stack.last().unwrap().function.as_fn().name();
let scrutinee = scrutinee.as_ref().unwrap();
let traceback = but_first_traceback(&panic);
let patterns = call_stack
.last()
.unwrap()
.function
.as_fn()
.patterns()
.stringify();
format!(
"no match calling fn `{name}` with `{scrutinee}`\n expected match with one of:\n {patterns}\n{traceback}",
)
}
PanicMsg::NoLetMatch => {
let scrutinee = scrutinee.as_ref().unwrap();
let traceback = traceback(&panic);
format!("no match in `let` form against `{scrutinee}`\n{traceback}")
}
PanicMsg::NoMatch => {
let scrutinee = scrutinee.as_ref().unwrap();
let traceback = traceback(&panic);
format!("no match in `match` form against `{scrutinee}`\n{traceback}")
}
}
}
fn but_first_traceback(panic: &Panic) -> String {
let mut traceback = vec![];
let mut stack = panic.call_stack.iter().rev();
stack.next();
for frame in stack {
traceback.push(frame_info(frame));
}
traceback.join("\n")
}
fn traceback(panic: &Panic) -> String {
let mut traceback = vec![];
for frame in panic.call_stack.iter().rev() {
traceback.push(frame_info(frame));
}
traceback.join("\n")
}
fn frame_info(frame: &CallFrame) -> String {
let span = frame.chunk().spans[if frame.ip == 0 { 0 } else { frame.ip - 1 }];
let line_number = line_number(frame.chunk().src, span);
// console_log!("ip: {} | span: {span} | line: {line_number}", frame.ip);
let line = get_line(frame.chunk().src, line_number);
let line = line.trim_start();
let name = frame.function.as_fn().name();
let input = frame.chunk().input;
format!(
" in {name} on line {} in {input}\n >>> {line}",
line_number + 1
)
}
/////// Some thoughts
// We're putting the information we need on the function and the chunk.
// In the compiler, on functions, build up a vec of strings that are the patterns the function can match against
// The pattern asts have a `show` method.
// And with the additional members on Chunk, we should have everything we need for a pretty fn no match message
// Let no match is no problem, either. We should have no concerns pulling the line with the span start and string
// We don't need to reproduce the pattern, since it will be right there in the code
// As for match forms, we'll just use "no match" and print the value

103
src/io.rs
View File

@ -1,103 +0,0 @@
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use crate::value::Value;
use crate::js::*;
use imbl::Vector;
use std::rc::Rc;
const OK: Value = Value::Keyword("ok");
const ERR: Value = Value::Keyword("err");
#[wasm_bindgen(module = "/pkg/worker.js")]
extern "C" {
#[wasm_bindgen(catch)]
async fn io (output: String) -> Result<JsValue, JsValue>;
}
type Url = Value; // expect a string
type Commands = Value; // expect a list of command tuples
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgOut {
Console(Value),
Commands(Commands),
Fetch(Url),
Complete(Value),
Error(String),
Ready
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgIn {
Input(String),
Fetch(String, f64, String),
Kill,
Keys(Vec<String>),
}
impl std::fmt::Display for MsgIn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MsgIn::Input(str) => write!(f, "Input: {str}"),
MsgIn::Kill => write!(f, "Kill"),
MsgIn::Fetch(url, code, text) => write!(f, "Fetch: {url} :: {code} ::\n{text}"),
MsgIn::Keys(keys) => write!(f, "Keys: {:?}", keys),
}
}
}
impl MsgIn {
pub fn into_value(self) -> Value {
match self {
MsgIn::Input(str) => Value::from_string(str),
MsgIn::Fetch(url, status_f64, string) => {
let url = Value::from_string(url);
let status = Value::from_f64(status_f64);
let text = Value::from_string(string);
let result_tuple = if status_f64 == 200.0 {
Value::tuple(vec![OK, text])
} else {
Value::tuple(vec![ERR, status])
};
Value::tuple(vec![url, result_tuple])
}
MsgIn::Kill => Value::Nothing,
MsgIn::Keys(downkeys) => {
let mut vector = Vector::new();
for key in downkeys {
vector.push_back(Value::String(Rc::new(key)));
}
Value::list(vector)
}
}
}
}
pub async fn send_err_to_ludus_console(msg: String) {
console_log!("{msg}");
do_io(vec![MsgOut::Ready, MsgOut::Error(msg)]).await;
}
pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
let json = serde_json::to_string(&msgs).unwrap();
let inbox = io(json).await;
let inbox = match inbox {
Ok(msgs) => msgs,
Err(errs) => {
// console_log!("error receiving messages in io; {:?}", errs);
return vec![];
}
};
let inbox = inbox.as_string().expect("response should be a string");
let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");
if !inbox.is_empty() {
// console_log!("ludus received messages");
for msg in inbox.iter() {
// console_log!("{}", msg);
}
}
inbox
}

View File

@ -1,19 +0,0 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
pub fn log(a: &str);
#[wasm_bindgen(js_namespace = Math)]
pub fn random() -> f64;
#[wasm_bindgen(js_namespace = Date)]
pub fn now() -> f64;
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
pub(crate) use console_log;

View File

@ -1,170 +0,0 @@
use crate::spans::*;
use chumsky::prelude::*;
use std::fmt;
#[derive(Clone, PartialEq, Debug)]
pub enum Token {
Nil,
Number(f64),
Word(&'static str),
Boolean(bool),
Keyword(&'static str),
String(&'static str),
// todo: hard code these types
Reserved(&'static str),
Punctuation(&'static str),
Method(&'static str),
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Token::Number(n) => write!(f, "[Number {}]", n),
Token::Word(w) => write!(f, "[Word {}]", w),
Token::Boolean(b) => write!(f, "[Boolean {}]", b),
Token::Keyword(k) => write!(f, "[Keyword :{}]", k),
Token::String(s) => write!(f, "[String {}]", s),
Token::Reserved(r) => write!(f, "[Reserved {}]", r),
Token::Nil => write!(f, "[nil]"),
Token::Punctuation(p) => write!(f, "[Punctuation {}]", p),
Token::Method(m) => write!(f, "[Method {m}]"),
}
}
}
impl Token {
pub fn show(&self) -> String {
match self {
Token::Number(n) => format!("{n}"),
Token::Boolean(b) => format!("{b}"),
Token::Keyword(k) => format!(":{k}"),
Token::Method(m) => format!("::{m}"),
Token::Nil => "nil".to_string(),
Token::String(s) => format!("\"{s}\""),
Token::Reserved(s) | Token::Word(s) => s.to_string(),
Token::Punctuation(s) => {
let out = if *s == "\n" { "newline" } else { s };
out.to_string()
}
}
}
}
pub fn lexer(
) -> impl Parser<'static, &'static str, Vec<(Token, Span)>, extra::Err<Rich<'static, char, Span>>> {
let number = just('-')
.or_not()
.then(text::int(10).then(just('.').then(text::digits(10)).or_not()))
.to_slice()
.from_str()
.unwrapped()
.map(Token::Number);
let word = any()
.filter(char::is_ascii_lowercase)
.then(
any()
.filter(char::is_ascii_alphanumeric)
.or(one_of("*/?!_"))
.repeated(),
)
.to_slice();
let reserved_or_word = word.map(|word: &str| match word {
"true" => Token::Boolean(true),
"false" => Token::Boolean(false),
"nil" => Token::Nil,
// todo: hard code these as type constructors
"as" | "box" | "do" | "else" | "fn" | "if" | "let" | "loop" | "match" | "panic!"
| "recur" | "repeat" | "then" | "when" | "with" | "or" | "and" | "receive" => {
Token::Reserved(word)
}
_ => Token::Word(word),
});
let method = just("::").ignore_then(word).map(Token::Method);
let keyword = just(':').ignore_then(word).map(Token::Keyword);
let escape = just('\\')
.then(choice((
just('\\').to('\\'),
just('n').to('\n'),
just('t').to('\t'),
just('r').to('\r'),
just('"').to('"'), // TODO: figure out why this isn't working
)))
.ignored();
let string = none_of('"')
.ignored()
.or(escape)
.repeated()
.to_slice()
.map(Token::String)
.delimited_by(just('"'), just('"'));
// todo: hard code these as type constructors
let punctuation = one_of(",=[]{}()>;\n_")
.to_slice()
.or(just("->"))
.or(just("..."))
.or(just("#{"))
.or(just("${"))
.map(Token::Punctuation);
let token = number
.or(reserved_or_word)
.or(keyword)
.or(method)
.or(string)
.or(punctuation);
let comment = just('&')
.ignore_then(any().and_is(just('\n').not()).repeated())
.repeated();
let ludus_ws = just(' ').or(just('\t')).repeated();
token
.map_with(|tok, e| (tok, e.span()))
.padded_by(ludus_ws)
.padded_by(comment)
.recover_with(skip_then_retry_until(any().ignored(), end()))
.repeated()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_lexes_nil() {
let spanned_toks = lexer().parse("nil").into_output_errors().0.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::Nil);
}
#[test]
fn it_lexes_strings() {
let spanned_toks = lexer()
.parse("\"foo bar baz\"")
.into_output_errors()
.0
.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::String("foo bar baz"));
}
#[test]
fn it_lexes_strings_w_escaped_quotes() {
let spanned_toks = lexer()
.parse("\"foo \\\"bar baz\"")
.into_output_errors()
.0
.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::String("foo \"bar baz"));
}
}

View File

@ -1,186 +0,0 @@
use chumsky::{input::Stream, prelude::*};
use imbl::HashMap;
use wasm_bindgen::prelude::*;
use std::rc::Rc;
use std::cell::RefCell;
const DEBUG_SCRIPT_COMPILE: bool = false;
const DEBUG_SCRIPT_RUN: bool = false;
const DEBUG_PRELUDE_COMPILE: bool = false;
const DEBUG_PRELUDE_RUN: bool = false;
mod io;
use io::send_err_to_ludus_console;
mod ast;
use crate::ast::Ast;
mod base;
mod world;
use crate::world::{World, Zoo};
mod spans;
use crate::spans::Spanned;
mod lexer;
use crate::lexer::lexer;
mod parser;
use crate::parser::parser;
mod validator;
use crate::validator::Validator;
mod errors;
use crate::errors::{lexing, parsing, validation};
mod panic;
mod js;
use crate::js::*;
mod chunk;
mod op;
mod compiler;
use crate::compiler::Compiler;
pub mod value;
use value::{Value, Key};
mod vm;
use vm::Creature;
const PRELUDE: &str = include_str!("../assets/prelude.ld");
fn prelude() -> HashMap<Key, Value> {
let tokens = lexer().parse(PRELUDE).into_output_errors().0.unwrap();
let (parsed, parse_errors) = parser()
.parse(Stream::from_iter(tokens).map((0..PRELUDE.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !parse_errors.is_empty() {
console_log!("ERROR PARSING PRELUDE:");
console_log!("{:?}", parse_errors);
panic!("parsing errors in prelude");
}
let parsed = parsed.unwrap();
let (ast, span) = &parsed;
let base = base::make_base();
let mut base_env = imbl::HashMap::new();
base_env.insert(Key::Keyword("base"), base.clone());
let mut validator = Validator::new(ast, span, "prelude", PRELUDE, base_env);
validator.validate();
if !validator.errors.is_empty() {
console_log!("VALIDATION ERRORS IN PRLUDE:");
// report_invalidation(validator.errors);
console_log!("{:?}", validator.errors);
panic!("validator errors in prelude");
}
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parsed));
let mut compiler = Compiler::new(
parsed,
"prelude",
PRELUDE,
0,
HashMap::new(),
DEBUG_PRELUDE_COMPILE,
);
compiler.emit_constant(base);
compiler.bind("base");
compiler.compile();
let chunk = compiler.chunk;
let stub_zoo = Rc::new(RefCell::new(Zoo::new()));
let mut prld_sync = Creature::new(chunk, stub_zoo, DEBUG_PRELUDE_RUN);
prld_sync.interpret();
let prelude = prld_sync.result.unwrap().unwrap();
match prelude {
Value::Dict(hashmap) => *hashmap,
_ => unreachable!(),
}
}
#[wasm_bindgen]
pub async fn ludus(src: String) {
// instrument wasm to report rust panics
console_error_panic_hook::set_once();
// leak the source so it lives FOREVER
let src = src.to_string().leak();
// lex the source
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {
send_err_to_ludus_console(lexing(lex_errs, src, "user script")).await;
return;
}
let tokens = tokens.unwrap();
let (parse_result, parse_errors) = parser()
.parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !parse_errors.is_empty() {
send_err_to_ludus_console(parsing(parse_errors, src, "user script")).await;
return;
}
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parse_result.unwrap()));
let prelude = prelude();
// let prelude = imbl::HashMap::new();
let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone());
validator.validate();
// TODO: validator should generate a string, not print to the console
if !validator.errors.is_empty() {
send_err_to_ludus_console(validation(validator.errors)).await;
return;
}
let mut compiler = Compiler::new(
parsed,
"user script",
src,
0,
prelude.clone(),
DEBUG_SCRIPT_COMPILE,
);
compiler.compile();
if DEBUG_SCRIPT_COMPILE {
console_log!("=== source code ===");
console_log!("{src}");
compiler.disassemble();
console_log!("\n\n")
}
if DEBUG_SCRIPT_RUN {
console_log!("=== vm run ===");
}
let vm_chunk = compiler.chunk;
let mut world = World::new(vm_chunk, prelude.clone(), DEBUG_SCRIPT_RUN);
world.run().await;
// TODO: actually do something useful on a panic
// match result {
// Some(Ok(val)) => val.show(),
// Some(Err(panic)) => format!("Ludus panicked! {panic}"),
// None => "Ludus run terminated by user".to_string()
// };
// if DEBUG_SCRIPT_RUN {
// // vm.print_stack();
// }
}

View File

@ -1,198 +0,0 @@
// use ariadne::{sources, Color, Label, Report, ReportKind};
use chumsky::prelude::*;
use std::fmt;
pub type Span = SimpleSpan<usize>;
#[derive(Clone, Debug, PartialEq)]
pub enum Token<'src> {
// atomic types
Boolean(bool),
Number(f64),
String(&'src str),
Word(&'src str),
Keyword(&'src str),
Pkgkeyword(&'src str),
Ignored(&'src str),
// reserved words
As,
Box,
Do,
Else,
Fn,
If,
Import,
Let,
Loop,
Match,
Nil,
Ns,
Panic,
Pkg,
Recur,
Repeat,
Test,
Then,
Use,
When,
With,
// punctuation
Arrow,
Comma,
Equals,
Lbrace,
Lbracket,
Lparen,
Newline,
Pipeline,
Placeholder,
Rbrace,
Rbracket,
Rparen,
Semi,
Splat,
Startdict,
Startset,
}
impl<'src> fmt::Display for Token<'src> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Token::Boolean(b) => write!(f, "{}", b),
Token::Number(n) => write!(f, "{}", n),
Token::String(s) => write!(f, "{}", s),
Token::Word(w) => write!(f, "{}", w),
Token::Keyword(k) => write!(f, ":{}", k),
Token::Ignored(i) => write!(f, "_{}", i),
Token::Pkgkeyword(k) => write!(f, ":{}", k),
Token::As => write!(f, "as"),
Token::Box => write!(f, "box"),
Token::Do => write!(f, "do"),
Token::Else => write!(f, "else"),
Token::Fn => write!(f, "fn"),
Token::If => write!(f, "if"),
Token::Import => write!(f, "import"),
Token::Let => write!(f, "let"),
Token::Loop => write!(f, "loop"),
Token::Match => write!(f, "match"),
Token::Nil => write!(f, "nil"),
Token::Ns => write!(f, "ns"),
Token::Panic => write!(f, "panic!"),
Token::Pkg => write!(f, "pkg"),
Token::Recur => write!(f, "recur"),
Token::Repeat => write!(f, "repeat"),
Token::Test => write!(f, "test"),
Token::Then => write!(f, "then"),
Token::Use => write!(f, "use"),
Token::When => write!(f, "when"),
Token::With => write!(f, "with"),
Token::Arrow => write!(f, "->"),
Token::Comma => write!(f, ","),
Token::Equals => write!(f, "="),
Token::Lbrace => write!(f, "{{"),
Token::Lbracket => write!(f, "["),
Token::Lparen => write!(f, "("),
Token::Newline => write!(f, "\\n"),
Token::Pipeline => write!(f, ">"),
Token::Placeholder => write!(f, "_"),
Token::Rbrace => write!(f, "}}"),
Token::Rbracket => write!(f, "]"),
Token::Rparen => write!(f, ")"),
Token::Semi => write!(f, ";"),
Token::Splat => write!(f, "..."),
Token::Startdict => write!(f, "#{{"),
Token::Startset => write!(f, "${{"),
}
}
}
pub fn lexer<'src>(
) -> impl Parser<'src, &'src str, Vec<(Token<'src>, Span)>, extra::Err<Rich<'src, char, Span>>> {
let string = just('"')
.ignore_then(none_of('"').repeated().to_slice())
.then_ignore(just('"'))
.map(Token::String);
let word = any()
.filter(char::is_ascii_lowercase)
.then(
any()
.filter(char::is_ascii_alphanumeric)
.or(one_of("*_/!?")),
)
.repeated()
.to_slice();
let keyword = just(':').ignore_then(word.clone()).map(Token::Keyword);
let number = just('-')
.or_not()
.then(text::int(10).then(just('.').then(text::digits(10)).or_not()))
.to_slice()
.from_str()
.unwrapped()
.map(Token::Number);
let reserved_or_word = word.map(|word: &str| match word {
"as" => Token::As,
"box" => Token::Box,
"do" => Token::Do,
"else" => Token::Else,
"false" => Token::Boolean(false),
"fn" => Token::Fn,
"if" => Token::If,
"import" => Token::Import,
"let" => Token::Let,
"loop" => Token::Loop,
"match" => Token::Match,
"nil" => Token::Nil,
"ns" => Token::Ns,
"panic!" => Token::Panic, // won't match until C-style ident -> Ludus word
"pkg" => Token::Pkg,
"recur" => Token::Recur,
"repeat" => Token::Repeat,
"test" => Token::Test,
"then" => Token::Then,
"true" => Token::Boolean(true),
"use" => Token::Use,
"when" => Token::When,
"with" => Token::With,
_ => Token::Word(word),
});
let arrow = just("->").to(Token::Arrow);
let comma = just(',').to(Token::Comma);
let semicolon = just(';').to(Token::Semi);
let placeholder = just('_').to(Token::Placeholder);
let control = arrow.or(comma).or(semicolon).or(placeholder);
let comment = just('&')
.then(any().and_is(just('\n').not()).repeated())
.padded();
let atom = number.or(string).or(keyword).or(reserved_or_word);
atom.or(control)
.map_with(|tok, e| (tok, e.span()))
.padded_by(comment.repeated())
.padded()
}
#[cfg(test)]
mod tests {
use crate::lexer;
use crate::Token;
use chumsky::{container::Seq, prelude::*};
#[test]
fn it_works() {
let toks = lexer().parse("42").unwrap();
let (tok, _) = toks[0].clone();
assert_eq!(tok, Token::Number(42.0));
}
}

View File

@ -1,7 +0,0 @@
use rudus::value::Value;
use std::env;
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
println!("Hello, world.")
}

View File

@ -1,58 +0,0 @@
use imbl::{HashMap, Vector};
use index_vec::Idx;
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
struct Word(&'static str);
struct Keyword(&'static str);
struct Interned(&'static str);
enum StringPart {
Word(&'static str),
Data(&'static str),
Inline(&'static str),
}
#[derive(Clone, Debug, PartialEq)]
struct LBox {
name: usize,
cell: RefCell<Value>,
}
#[derive(Clone, Debug, PartialEq)]
struct Fn {
name: &'static str,
body: Vec<String>,
//...etc
}
#[derive(Clone, Debug, PartialEq)]
enum Value {
Nil,
Placeholder,
Boolean(bool),
Keyword(usize),
Interned(usize),
FnDecl(usize),
String(Rc<String>),
Number(f64),
Tuple(Rc<Vec<Value>>),
List(Box<Vector<Value>>),
Dict(Box<HashMap<&'static str, Value>>),
Box(Rc<LBox>),
Fn(Rc<RefCell<Fn>>),
}
fn futz() {
let foo: &'static str = "foo";
let baz: Vec<u8> = vec![];
let bar: Range<usize> = 1..3;
let quux: Vector<u8> = Vector::new();
let fuzz = Rc::new(quux);
let blah = Box::new(foo);
let val = Value::Number(12.09);
let foo: f64 = 12.0;
}

View File

@ -1,212 +0,0 @@
// an implementation of Ludus
// curently left undone (and not adding for a while yet):
// * sets
// * interpolated strings & string patterns
// * pkgs, namespaces, imports, `use` forms
// * with forms
// * test forms
// * ignored words
// todo:
// * [x] rewrite fn parser to use chumsky::Recursive::declare/define
// - [x] do this to extract/simplify/DRY things like tuple patterns, fn clauses, etc.
// * [x] Work around chumsky::Stream::from_iter().spanned disappearing in most recent version
// * [x] investigate using labels (which is behind a compiler flag, somehow)
// * [ ] write parsing errors
// * [ ] wire up Ariadne parsing errors
// * [x] add stack traces and code locations to panics
// * [x] validation
// * [x] break this out into multiple files
// * [x] write a tree-walk VM
// - [x] learn how to deal with lifetimes
// - [x] with stack mechanics and refcounting
// - [ ] with tail-call optimization (nb: this may not be possible w/ a TW-VM)
// - [ ] with all the necessary forms for current Ludus
// * [x] guards in match clauses
// * [x] `as` patterns
// * [x] splat patterns in tuples, lists, dicts
// * [x] splats in list and dict literals
// * [x] `loop` and `recur`
// * [x] string patterns
// * [x] string interpolation
// * [x] docstrings
// * [x] write `base` in Rust
// * [ ] turn this into a library function
// * [ ] compile this into WASM
// * [ ] perf testing
use chumsky::{input::Stream, prelude::*};
use rust_embed::Embed;
mod spans;
mod lexer;
use crate::lexer::*;
mod value;
use crate::value::*;
mod parser;
use crate::parser::*;
mod base;
use crate::base::*;
mod validator;
use crate::validator::*;
mod process;
use crate::process::*;
mod errors;
use crate::errors::*;
mod byte_values;
mod compiler;
mod memory_sandbox;
#[derive(Embed)]
#[folder = "assets/"]
struct Asset;
pub fn prelude<'src>() -> (
Vec<(String, Value<'src>)>,
std::collections::HashMap<*const Ast, FnInfo>,
) {
let prelude = Asset::get("prelude.ld").unwrap().data.into_owned();
// we know for sure Prelude should live through the whole run of the program
let leaked = Box::leak(Box::new(prelude));
let prelude = std::str::from_utf8(leaked).unwrap();
let (ptoks, perrs) = lexer().parse(prelude).into_output_errors();
if !perrs.is_empty() {
println!("Errors lexing Prelude");
println!("{:?}", perrs);
panic!();
}
let ptoks = ptoks.unwrap();
let (p_ast, perrs) = parser()
.parse(Stream::from_iter(ptoks).map((0..prelude.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !perrs.is_empty() {
println!("Errors parsing Prelude");
println!("{:?}", perrs);
panic!();
}
let prelude_parsed = Box::leak(Box::new(p_ast.unwrap()));
let base_pkg = base();
let mut v6or = Validator::new(
&prelude_parsed.0,
prelude_parsed.1,
"prelude",
prelude,
&base_pkg,
);
v6or.validate();
if !v6or.errors.is_empty() {
report_invalidation(v6or.errors);
panic!("interal Ludus error: invalid prelude")
}
let mut base_ctx = Process::<'src> {
input: "prelude",
src: prelude,
locals: base_pkg.clone(),
ast: &prelude_parsed.0,
span: prelude_parsed.1,
prelude: vec![],
fn_info: v6or.fn_info,
};
let prelude = base_ctx.eval();
let mut p_ctx = vec![];
match prelude {
Ok(Value::Dict(p_dict)) => {
for (key, value) in p_dict.iter() {
p_ctx.push((key.to_string(), value.clone()))
}
}
Ok(_) => {
println!("Bad Prelude export");
panic!();
}
Err(LErr { msg, .. }) => {
println!("Error running Prelude");
println!("{:?}", msg);
panic!();
}
};
(p_ctx, base_ctx.fn_info)
}
pub fn run(src: &'static str) {
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {
println!("{:?}", lex_errs);
return;
}
let tokens = tokens.unwrap();
let to_parse = tokens.clone();
let (parse_result, parse_errors) = parser()
.parse(Stream::from_iter(to_parse).map((0..src.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !parse_errors.is_empty() {
println!("{:?}", parse_errors);
return;
}
let parsed = parse_result.unwrap();
let (prelude_ctx, mut prelude_fn_info) = prelude();
let mut v6or = Validator::new(&parsed.0, parsed.1, "script", src, &prelude_ctx);
v6or.validate();
if !v6or.errors.is_empty() {
report_invalidation(v6or.errors);
return;
}
prelude_fn_info.extend(&mut v6or.fn_info.into_iter());
let mut proc = Process {
input: "script",
src,
locals: vec![],
prelude: prelude_ctx,
ast: &parsed.0,
span: parsed.1,
fn_info: prelude_fn_info,
};
let result = proc.eval();
match result {
Ok(result) => println!("{}", result),
Err(err) => report_panic(err),
}
}
pub fn main() {
let src = "
loop (100000, 1) with {
(1, acc) -> acc
(n, acc) -> recur (dec (n), add (n, acc))
}
";
run(src);
// struct_scalpel::print_dissection_info::<value::Value>()
// struct_scalpel::print_dissection_info::<parser::Ast>();
// println!("{}", std::mem::size_of::<parser::Ast>())
}

View File

@ -1,193 +0,0 @@
use crate::base::*;
use crate::parser::*;
use crate::spans::*;
use imbl::*;
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use struct_scalpel::Dissectible;
#[derive(Clone, Debug)]
pub struct Fn<'src> {
pub name: String,
pub body: &'src Vec<Spanned<Ast>>,
pub doc: Option<String>,
pub enclosing: Vec<(String, Value<'src>)>,
pub has_run: bool,
pub input: &'static str,
pub src: &'static str,
}
#[derive(Debug, Dissectible)]
pub enum Value<'src> {
Nil,
Placeholder,
Boolean(bool),
Number(f64),
Keyword(&'static str),
InternedString(&'static str),
AllocatedString(Rc<String>),
// on the heap for now
Tuple(Rc<Vec<Self>>),
Args(Rc<Vec<Self>>),
List(Vector<Self>),
Dict(HashMap<&'static str, Self>),
Box(&'static str, Rc<RefCell<Self>>),
Fn(Rc<RefCell<Fn<'src>>>),
FnDecl(&'static str),
Base(BaseFn<'src>),
Recur(Vec<Self>),
// Set(HashSet<Self>),
// Sets are hard
// Sets require Eq
// Eq is not implemented on f64, because NaNs
// We could use ordered_float::NotNan
// Let's defer that
// We're not really using sets in Ludus
// Other things we're not implementing yet:
// pkgs, nses, tests
}
impl<'src> Clone for Value<'src> {
fn clone(&self) -> Value<'src> {
match self {
Value::Nil => Value::Nil,
Value::Boolean(b) => Value::Boolean(*b),
Value::InternedString(s) => Value::InternedString(s),
Value::AllocatedString(s) => Value::AllocatedString(s.clone()),
Value::Keyword(s) => Value::Keyword(s),
Value::Number(n) => Value::Number(*n),
Value::Tuple(t) => Value::Tuple(t.clone()),
Value::Args(a) => Value::Args(a.clone()),
Value::Fn(f) => Value::Fn(f.clone()),
Value::FnDecl(name) => Value::FnDecl(name),
Value::List(l) => Value::List(l.clone()),
Value::Dict(d) => Value::Dict(d.clone()),
Value::Box(name, b) => Value::Box(name, b.clone()),
Value::Placeholder => Value::Placeholder,
Value::Base(b) => Value::Base(b.clone()),
Value::Recur(..) => unreachable!(),
}
}
}
impl fmt::Display for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Value::Nil => write!(f, "nil"),
Value::Boolean(b) => write!(f, "{b}"),
Value::Number(n) => write!(f, "{n}"),
Value::Keyword(k) => write!(f, ":{k}"),
Value::InternedString(s) => write!(f, "\"{s}\""),
Value::AllocatedString(s) => write!(f, "\"{s}\""),
Value::Fn(fun) => write!(f, "fn {}", fun.borrow().name),
Value::FnDecl(name) => write!(f, "fn {name}"),
Value::Tuple(t) | Value::Args(t) => write!(
f,
"({})",
t.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
),
Value::List(l) => write!(
f,
"[{}]",
l.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
),
Value::Dict(d) => write!(
f,
"#{{{}}}",
d.iter()
.map(|(k, v)| format!(":{k} {v}"))
.collect::<Vec<_>>()
.join(", ")
),
Value::Box(name, value) => {
write!(
f,
"box {}: [{}]",
name,
&value.try_borrow().unwrap().to_string()
)
}
Value::Placeholder => write!(f, "_"),
Value::Base(..) => unreachable!(),
Value::Recur(..) => unreachable!(),
}
}
}
impl Value<'_> {
pub fn bool(&self) -> bool {
!matches!(self, Value::Nil | Value::Boolean(false))
}
}
impl<'src> PartialEq for Value<'src> {
fn eq(&self, other: &Value<'src>) -> bool {
match (self, other) {
// value equality types
(Value::Nil, Value::Nil) => true,
(Value::Boolean(x), Value::Boolean(y)) => x == y,
(Value::Number(x), Value::Number(y)) => x == y,
(Value::InternedString(x), Value::InternedString(y)) => x == y,
(Value::AllocatedString(x), Value::AllocatedString(y)) => x == y,
(Value::InternedString(x), Value::AllocatedString(y)) => *x == **y,
(Value::AllocatedString(x), Value::InternedString(y)) => **x == *y,
(Value::Keyword(x), Value::Keyword(y)) => x == y,
(Value::Tuple(x), Value::Tuple(y)) => x == y,
(Value::List(x), Value::List(y)) => x == y,
(Value::Dict(x), Value::Dict(y)) => x == y,
// reference equality types
(Value::Fn(x), Value::Fn(y)) => {
Rc::<RefCell<Fn<'_>>>::as_ptr(x) == Rc::<RefCell<Fn<'_>>>::as_ptr(y)
}
(Value::Box(_, x), Value::Box(_, y)) => {
Rc::<RefCell<Value<'_>>>::as_ptr(x) == Rc::<RefCell<Value<'_>>>::as_ptr(y)
}
_ => false,
}
}
}
impl Eq for Value<'_> {}
impl Value<'_> {
pub fn interpolate(&self) -> String {
match self {
Value::Nil => String::new(),
Value::Boolean(b) => format!("{b}"),
Value::Number(n) => format!("{n}"),
Value::Keyword(k) => format!(":{k}"),
Value::AllocatedString(s) => format!("{s}"),
Value::InternedString(s) => s.to_string(),
Value::Box(_, x) => x.borrow().interpolate(),
Value::Tuple(xs) => xs
.iter()
.map(|x| x.interpolate())
.collect::<Vec<_>>()
.join(", "),
Value::List(xs) => xs
.iter()
.map(|x| x.interpolate())
.collect::<Vec<_>>()
.join(", "),
Value::Dict(xs) => xs
.iter()
.map(|(k, v)| format!(":{} {}", k, v.interpolate()))
.collect::<Vec<_>>()
.join(", "),
Value::Fn(x) => format!("fn {}", x.borrow().name),
Value::FnDecl(name) => format!("fn {name}"),
Value::Placeholder => unreachable!(),
Value::Args(_) => unreachable!(),
Value::Recur(_) => unreachable!(),
Value::Base(_) => unreachable!(),
}
}
}

View File

@ -1,533 +0,0 @@
use crate::base::*;
use crate::parser::*;
use crate::value::*;
use imbl::HashMap;
use imbl::Vector;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug)]
pub struct LudusError {
pub msg: String,
}
// oy
// lifetimes are a mess
// I need 'src kind of everywhere
// But (maybe) using 'src in eval
// for ctx
// means I can't borrow it mutably
// I guess the question is how to get
// the branches for Ast::Block and Ast::If
// to work with a mutable borrow of ctx
// pub struct Ctx<'src> {
// pub locals: Vec<(&'src str, Value<'src>)>,
// // pub names: Vec<&'src str>,
// // pub values: Vec<Value<'src>>,
// }
// impl<'src> Ctx<'src> {
// pub fn resolve(&self, name: &'src str) -> Value {
// if let Some((_, val)) = self.locals.iter().rev().find(|(bound, _)| *bound == name) {
// val.clone()
// } else {
// unreachable!()
// }
// }
// pub fn store(&mut self, name: &'src str, value: Value<'src>) {
// self.locals.push((name, value));
// }
// }
type Context<'src> = Vec<(String, Value<'src>)>;
pub fn match_eq<T, U>(x: T, y: T, z: U) -> Option<U>
where
T: PartialEq,
{
if x == y {
Some(z)
} else {
None
}
}
pub fn match_pattern<'src, 'a>(
patt: &Pattern,
val: &Value<'src>,
ctx: &'a mut Context<'src>,
) -> Option<&'a mut Context<'src>> {
match (patt, val) {
(Pattern::Nil, Value::Nil) => Some(ctx),
(Pattern::Placeholder, _) => Some(ctx),
(Pattern::Number(x), Value::Number(y)) => match_eq(x, y, ctx),
(Pattern::Boolean(x), Value::Boolean(y)) => match_eq(x, y, ctx),
(Pattern::Keyword(x), Value::Keyword(y)) => match_eq(x, y, ctx),
(Pattern::String(x), Value::InternedString(y)) => match_eq(x, y, ctx),
(Pattern::String(x), Value::AllocatedString(y)) => match_eq(&x.to_string(), y, ctx),
(Pattern::Interpolated(_, StringMatcher(matcher)), Value::InternedString(y)) => {
match matcher(y.to_string()) {
Some(matches) => {
let mut matches = matches
.iter()
.map(|(word, string)| {
(
word.clone(),
Value::AllocatedString(Rc::new(string.clone())),
)
})
.collect::<Vec<_>>();
ctx.append(&mut matches);
Some(ctx)
}
None => None,
}
}
(Pattern::Word(w), val) => {
ctx.push((w.to_string(), val.clone()));
Some(ctx)
}
(Pattern::As(word, type_str), value) => {
let ludus_type = r#type(value);
let type_kw = Value::Keyword(type_str);
if type_kw == ludus_type {
ctx.push((word.to_string(), value.clone()));
Some(ctx)
} else {
None
}
}
// todo: add splats to these match clauses
(Pattern::Tuple(x), Value::Tuple(y)) => {
let has_splat = x
.iter()
.any(|patt| matches!(patt, (Pattern::Splattern(_), _)));
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
return None;
};
let to = ctx.len();
for i in 0..x.len() {
if let Pattern::Splattern(patt) = &x[i].0 {
let mut list = Vector::new();
for i in i..y.len() {
list.push_back(y[i].clone())
}
let list = Value::List(list);
match_pattern(&patt.0, &list, ctx);
} else if match_pattern(&x[i].0, &y[i], ctx).is_none() {
while ctx.len() > to {
ctx.pop();
}
return None;
}
}
Some(ctx)
}
(Pattern::List(x), Value::List(y)) => {
let has_splat = x
.iter()
.any(|patt| matches!(patt, (Pattern::Splattern(_), _)));
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
return None;
};
let to = ctx.len();
for (i, (patt, _)) in x.iter().enumerate() {
if let Pattern::Splattern(patt) = &patt {
let list = Value::List(y.skip(i));
match_pattern(&patt.0, &list, ctx);
} else if match_pattern(patt, y.get(i).unwrap(), ctx).is_none() {
while ctx.len() > to {
ctx.pop();
}
return None;
}
}
Some(ctx)
}
// TODO: optimize this on several levels
// - [ ] opportunistic mutation
// - [ ] get rid of all the pointer indirection in word splats
(Pattern::Dict(x), Value::Dict(y)) => {
let has_splat = x
.iter()
.any(|patt| matches!(patt, (Pattern::Splattern(_), _)));
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
return None;
};
let to = ctx.len();
let mut matched = vec![];
for (pattern, _) in x {
match pattern {
Pattern::Pair(key, patt) => {
if let Some(val) = y.get(key) {
if match_pattern(&patt.0, val, ctx).is_none() {
while ctx.len() > to {
ctx.pop();
}
return None;
} else {
matched.push(key);
}
} else {
return None;
};
}
Pattern::Splattern(pattern) => match pattern.0 {
Pattern::Word(w) => {
// TODO: find a way to take ownership
// this will ALWAYS make structural changes, because of this clone
// we want opportunistic mutation if possible
let mut unmatched = y.clone();
for key in matched.iter() {
unmatched.remove(*key);
}
ctx.push((w.to_string(), Value::Dict(unmatched)));
}
Pattern::Placeholder => (),
_ => unreachable!(),
},
_ => unreachable!(),
}
}
Some(ctx)
}
_ => None,
}
}
pub fn match_clauses<'src>(
value: &Value<'src>,
clauses: &'src [MatchClause],
ctx: &mut Context<'src>,
) -> Result<Value<'src>, LudusError> {
let to = ctx.len();
for MatchClause { patt, body, guard } in clauses.iter() {
if let Some(ctx) = match_pattern(&patt.0, value, ctx) {
let pass_guard = match guard {
None => true,
Some((ast, _)) => {
let guard_res = eval(ast, ctx);
match &guard_res {
Err(_) => return guard_res,
Ok(val) => val.bool(),
}
}
};
if !pass_guard {
while ctx.len() > to {
ctx.pop();
}
continue;
}
let res = eval(&body.0, ctx);
while ctx.len() > to {
ctx.pop();
}
return res;
}
}
Err(LudusError {
msg: "no match".to_string(),
})
}
pub fn apply<'src>(
callee: Value<'src>,
caller: Value<'src>,
ctx: &mut Context,
) -> Result<Value<'src>, LudusError> {
match (callee, caller) {
(Value::Keyword(kw), Value::Dict(dict)) => {
if let Some(val) = dict.get(kw) {
Ok(val.clone())
} else {
Ok(Value::Nil)
}
}
(Value::Dict(dict), Value::Keyword(kw)) => {
if let Some(val) = dict.get(kw) {
Ok(val.clone())
} else {
Ok(Value::Nil)
}
}
(Value::Fn(f), Value::Tuple(args)) => {
let args = Value::Tuple(args);
match_clauses(&args, f.body, ctx)
}
(Value::Fn(_f), Value::Args(_args)) => todo!(),
(_, Value::Keyword(_)) => Ok(Value::Nil),
(_, Value::Args(_)) => Err(LudusError {
msg: "you may only call a function".to_string(),
}),
(Value::Base(f), Value::Tuple(args)) => match f {
Base::Nullary(f) => {
if args.len() != 0 {
Err(LudusError {
msg: "wrong arity: expected 0 arguments".to_string(),
})
} else {
Ok(f())
}
}
Base::Unary(f) => {
if args.len() != 1 {
Err(LudusError {
msg: "wrong arity: expected 1 argument".to_string(),
})
} else {
Ok(f(&args[0]))
}
}
Base::Binary(r#fn) => {
if args.len() != 2 {
Err(LudusError {
msg: "wrong arity: expected 2 arguments".to_string(),
})
} else {
Ok(r#fn(&args[0], &args[1]))
}
}
Base::Ternary(f) => {
if args.len() != 3 {
Err(LudusError {
msg: "wrong arity: expected 3 arguments".to_string(),
})
} else {
Ok(f(&args[0], &args[1], &args[2]))
}
}
},
_ => unreachable!(),
}
}
pub fn eval<'src, 'a>(
ast: &'src Ast,
ctx: &'a mut Vec<(String, Value<'src>)>,
) -> Result<Value<'src>, LudusError> {
match ast {
Ast::Nil => Ok(Value::Nil),
Ast::Boolean(b) => Ok(Value::Boolean(*b)),
Ast::Number(n) => Ok(Value::Number(*n)),
Ast::Keyword(k) => Ok(Value::Keyword(k)),
Ast::String(s) => Ok(Value::InternedString(s)),
Ast::Interpolated(parts) => {
let mut interpolated = String::new();
for part in parts {
match &part.0 {
StringPart::Data(s) => interpolated.push_str(s.as_str()),
StringPart::Word(w) => {
let val = if let Some((_, value)) =
ctx.iter().rev().find(|(name, _)| w == name)
{
value.clone()
} else {
return Err(LudusError {
msg: format!("unbound name {w}"),
});
};
interpolated.push_str(val.interpolate().as_str())
}
StringPart::Inline(_) => unreachable!(),
}
}
Ok(Value::AllocatedString(Rc::new(interpolated)))
}
Ast::Block(exprs) => {
let to = ctx.len();
let mut result = Value::Nil;
for (expr, _) in exprs {
result = eval(expr, ctx)?;
}
while ctx.len() > to {
ctx.pop();
}
Ok(result)
}
Ast::If(cond, if_true, if_false) => {
let truthy = eval(&cond.0, ctx)?.bool();
if truthy {
eval(&if_true.0, ctx)
} else {
eval(&if_false.0, ctx)
}
}
Ast::List(members) => {
let mut vect = Vector::new();
for member in members {
if let Ast::Splat(_) = member.0 {
let to_splat = eval(&member.0, ctx)?;
match to_splat {
Value::List(list) => vect.append(list),
_ => {
return Err(LudusError {
msg: "only lists may be splatted into lists".to_string(),
})
}
}
} else {
vect.push_back(eval(&member.0, ctx)?)
}
}
Ok(Value::List(vect))
}
Ast::Tuple(members) => {
let mut vect = Vec::new();
for member in members {
vect.push(eval(&member.0, ctx)?);
}
Ok(Value::Tuple(Rc::new(vect)))
}
Ast::Word(w) | Ast::Splat(w) => {
let val = if let Some((_, value)) = ctx.iter().rev().find(|(name, _)| w == name) {
value.clone()
} else {
return Err(LudusError {
msg: format!("unbound name {w}"),
});
};
Ok(val)
}
Ast::Let(patt, expr) => {
let val = eval(&expr.0, ctx)?;
match match_pattern(&patt.0, &val, ctx) {
Some(_) => Ok(val),
None => Err(LudusError {
msg: "No match".to_string(),
}),
}
}
Ast::Placeholder => Ok(Value::Placeholder),
Ast::Error => unreachable!(),
Ast::Arguments(a) => {
let mut args = vec![];
for (arg, _) in a.iter() {
let arg = eval(arg, ctx)?;
args.push(arg);
}
if args.iter().any(|arg| matches!(arg, Value::Placeholder)) {
Ok(Value::Args(Rc::new(args)))
} else {
Ok(Value::Tuple(Rc::new(args)))
}
}
Ast::Dict(terms) => {
let mut dict = HashMap::new();
for term in terms {
let (term, _) = term;
match term {
Ast::Pair(key, value) => {
let value = eval(&value.0, ctx)?;
dict.insert(*key, value);
}
Ast::Splat(_) => {
let resolved = eval(term, ctx)?;
let Value::Dict(to_splat) = resolved else {
return Err(LudusError {
msg: "cannot splat non-dict into dict".to_string(),
});
};
dict = to_splat.union(dict);
}
_ => unreachable!(),
}
}
Ok(Value::Dict(dict))
}
Ast::Box(name, expr) => {
let val = eval(&expr.0, ctx)?;
let boxed = Value::Box(name, Rc::new(RefCell::new(val)));
ctx.push((name.to_string(), boxed.clone()));
Ok(boxed)
}
Ast::Synthetic(root, first, rest) => {
let root = eval(&root.0, ctx)?;
let first = eval(&first.0, ctx)?;
let mut curr = apply(root, first, ctx)?;
for term in rest.iter() {
let next = eval(&term.0, ctx)?;
curr = apply(curr, next, ctx)?;
}
Ok(curr)
}
Ast::When(clauses) => {
for clause in clauses.iter() {
let WhenClause { cond, body } = &clause.0;
if eval(&cond.0, ctx)?.bool() {
return eval(&body.0, ctx);
};
}
Err(LudusError {
msg: "no match".to_string(),
})
}
Ast::Match(value, clauses) => {
let value = eval(&value.0, ctx)?;
match_clauses(&value, clauses, ctx)
}
Ast::Fn(name, clauses, doc) => {
let doc = doc.map(|s| s.to_string());
let the_fn = Value::Fn::<'src>(Rc::new(Fn::<'src> {
name: name.to_string(),
body: clauses,
doc,
}));
ctx.push((name.to_string(), the_fn.clone()));
Ok(the_fn)
}
Ast::FnDeclaration(_name) => todo!(),
Ast::Panic(msg) => {
let msg = eval(&msg.0, ctx)?;
Err(LudusError {
msg: msg.to_string(),
})
}
Ast::Repeat(times, body) => {
let times_num = match eval(&times.0, ctx) {
Ok(Value::Number(n)) => n as usize,
_ => {
return Err(LudusError {
msg: "repeat may only take numbers".to_string(),
})
}
};
for _ in 0..times_num {
eval(&body.0, ctx)?;
}
Ok(Value::Nil)
}
Ast::Do(terms) => {
let mut result = eval(&terms[0].0, ctx)?;
for (term, _) in terms.iter().skip(1) {
let next = eval(term, ctx)?;
let arg = Value::Tuple(Rc::new(vec![result]));
result = apply(next, arg, ctx)?;
}
Ok(result)
}
Ast::Pair(..) => {
unreachable!()
}
Ast::Loop(init, clauses) => {
let mut args = eval(&init.0, ctx)?;
loop {
let result = match_clauses(&args, clauses, ctx)?;
if let Value::Recur(recur_args) = result {
args = Value::Tuple(Rc::new(recur_args));
} else {
return Ok(result);
}
}
}
Ast::Recur(args) => {
let mut vect = Vec::new();
for arg in args {
vect.push(eval(&arg.0, ctx)?);
}
Ok(Value::Recur(vect))
}
}
}

218
src/op.rs
View File

@ -1,218 +0,0 @@
use num_derive::{FromPrimitive, ToPrimitive};
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
pub enum Op {
Noop,
Nothing,
Nil,
True,
False,
Constant,
Jump,
JumpIfFalse,
JumpIfTrue,
Pop,
PopN,
PushBinding,
PushGlobal,
Store,
StoreN,
Stash,
Load,
LoadN,
ResetMatch,
UnconditionalMatch,
MatchNil,
MatchTrue,
MatchFalse,
MatchConstant,
MatchString,
PushStringMatches,
MatchType,
MatchTuple,
MatchSplattedTuple,
PushTuple,
LoadTuple,
LoadSplattedTuple,
MatchList,
MatchSplattedList,
LoadList,
LoadSplattedList,
PushList,
AppendList,
ConcatList,
PushDict,
AppendDict,
ConcatDict,
LoadDictValue,
MatchDict,
MatchSplattedDict,
DropDictEntry,
PushBox,
GetKey,
PanicWhenFallthrough,
JumpIfNoMatch,
JumpIfMatch,
PanicNoMatch,
PanicNoLetMatch,
PanicNoFnMatch,
TypeOf,
JumpBack,
JumpIfZero,
Duplicate,
Decrement,
ToInt,
MatchDepth,
Panic,
EmptyString,
ConcatStrings,
Stringify,
Call,
TailCall,
Return,
Partial,
Eq,
Add,
Sub,
Mult,
Div,
Unbox,
BoxStore,
Assert,
Get,
At,
// Inc,
// Dec,
// Gt,
// Gte,
// Lt,
// Lte,
// Mod,
// First,
// Rest
// Sqrt,
// Append,
Not,
Print,
SetUpvalue,
GetUpvalue,
Msg,
LoadMessage,
NextMessage,
MatchMessage,
ClearMessage,
SendMethod,
Spawn,
LoadScrutinee,
}
impl std::fmt::Display for Op {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Op::*;
let rep = match self {
Msg => "msg",
Noop => "noop",
Nothing => "nothing",
Nil => "nil",
True => "true",
False => "false",
Constant => "constant",
Jump => "jump",
JumpIfFalse => "jump_if_false",
JumpIfTrue => "jump_if_true",
Pop => "pop",
PopN => "pop_n",
PushBinding => "push_binding",
PushGlobal => "push_global",
Store => "store",
StoreN => "store_n",
Stash => "stash",
Load => "load",
LoadN => "load_n",
UnconditionalMatch => "match",
MatchNil => "match_nil",
MatchTrue => "match_true",
MatchFalse => "match_false",
ResetMatch => "reset_match",
MatchConstant => "match_constant",
MatchString => "match_string",
PushStringMatches => "push_string_matches",
MatchType => "match_type",
MatchTuple => "match_tuple",
MatchSplattedTuple => "match_splatted_tuple",
PushTuple => "push_tuple",
LoadTuple => "load_tuple",
LoadSplattedTuple => "load_splatted_tuple",
MatchList => "match_list",
MatchSplattedList => "match_splatted_list",
LoadList => "load_list",
LoadSplattedList => "load_splatted_list",
PushList => "push_list",
AppendList => "append_list",
ConcatList => "concat_list",
PushDict => "push_dict",
AppendDict => "append_dict",
ConcatDict => "concat_dict",
LoadDictValue => "load_dict_value",
MatchDict => "match_dict",
MatchSplattedDict => "match_splatted_dict",
DropDictEntry => "drop_dict_entry",
PushBox => "push_box",
GetKey => "get_key",
PanicWhenFallthrough => "panic_no_when",
JumpIfNoMatch => "jump_if_no_match",
JumpIfMatch => "jump_if_match",
PanicNoMatch => "panic_no_match",
PanicNoFnMatch => "panic_no_fn_match",
PanicNoLetMatch => "panic_no_let_match",
TypeOf => "type_of",
JumpBack => "jump_back",
JumpIfZero => "jump_if_zero",
Decrement => "decrement",
ToInt => "truncate",
Duplicate => "duplicate",
MatchDepth => "match_depth",
Panic => "panic",
EmptyString => "empty_string",
ConcatStrings => "concat_strings",
Stringify => "stringify",
Print => "print",
Eq => "eq",
Add => "add",
Sub => "sub",
Mult => "mult",
Div => "div",
Unbox => "unbox",
BoxStore => "box_store",
Assert => "assert",
Get => "get",
At => "at",
Not => "not",
Call => "call",
Return => "return",
Partial => "partial",
TailCall => "tail_call",
SetUpvalue => "set_upvalue",
GetUpvalue => "get_upvalue",
LoadMessage => "load_message",
NextMessage => "next_message",
MatchMessage => "match_message",
ClearMessage => "clear_message",
SendMethod => "send_method",
Spawn => "spawn",
LoadScrutinee => "load_scrutinee",
};
write!(f, "{rep}")
}
}

View File

@ -1,47 +0,0 @@
use crate::errors::panic;
use crate::value::Value;
use crate::vm::CallFrame;
#[derive(Debug, Clone, PartialEq)]
pub enum PanicMsg {
NoLetMatch,
NoFnMatch,
NoMatch,
Generic(String),
}
impl std::fmt::Display for PanicMsg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use PanicMsg::*;
match self {
NoLetMatch => write!(f, "no match in `let`"),
NoFnMatch => write!(f, "no match calling fn"),
NoMatch => write!(f, "no match in `match` form"),
Generic(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Panic {
pub msg: PanicMsg,
pub scrutinee: Option<Value>,
pub call_stack: Vec<CallFrame>,
}
fn frame_dump(frame: &CallFrame) -> String {
let dump = format!("stack name: {}\nspans: {:?}", frame, frame.chunk().spans);
dump
}
impl std::fmt::Display for Panic {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let stub_trace = self
.call_stack
.iter()
.map(frame_dump)
.collect::<Vec<_>>()
.join("\n");
write!(f, "Panic: {}\n{stub_trace}", self.msg)
}
}

View File

@ -1,688 +0,0 @@
// TODO: move AST to its own module
// TODO: remove StringMatcher cruft
// TODO: good error messages?
use crate::ast::{Ast, StringPart};
use crate::lexer::*;
use crate::spans::*;
use chumsky::{input::ValueInput, prelude::*, recursive::Recursive};
use std::fmt;
pub struct StringMatcher();
impl PartialEq for StringMatcher {
fn eq(&self, _other: &StringMatcher) -> bool {
true
}
}
impl Clone for StringMatcher {
fn clone(&self) -> StringMatcher {
unreachable!()
}
}
impl fmt::Display for StringMatcher {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "string matcher")
}
}
impl fmt::Debug for StringMatcher {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "string matcher")
}
}
fn is_word_char(c: char) -> bool {
if c.is_ascii_alphanumeric() {
return true;
};
matches!(c, '_' | '/' | '?' | '!')
}
fn parse_string(s: &'static str, span: SimpleSpan) -> Result<Vec<Spanned<StringPart>>, String> {
// println!("parsing string pattern: {s}");
let mut parts = vec![];
let mut current_part = String::new();
let mut start = span.start;
let mut is_word = false;
let mut chars = s.char_indices();
while let Some((i, char)) = chars.next() {
match char {
'{' => {
if is_word {
return Err("interpolations must only contain words".to_string());
};
match chars.next() {
None => return Err("unclosed brace".to_string()),
Some((_, '{')) => current_part.push('{'),
Some((i, c)) => {
if !current_part.is_empty() {
parts.push((
StringPart::Data(current_part),
SimpleSpan::new(span.context(), start..start + i),
));
};
current_part = String::new();
start = i;
is_word = true;
if c.is_ascii_lowercase() {
current_part.push(c);
} else {
return Err("interpolations must only contain words".to_string());
}
}
}
}
'}' => {
if is_word {
parts.push((
StringPart::Word(current_part.leak()),
SimpleSpan::new(span.context(), start..start + i),
));
current_part = String::new();
start = i;
is_word = false;
} else {
match chars.next() {
None => return Err("unclosed brace".to_string()),
Some((_, c)) => current_part.push(c),
}
}
}
_ => {
if is_word {
if is_word_char(char) {
current_part.push(char)
} else {
return Err("interpolations must only contain words".to_string());
}
} else {
current_part.push(char)
}
}
}
}
if current_part == s {
parts.push((
StringPart::Inline(current_part),
SimpleSpan::new(span.context(), start..span.end),
))
} else if !current_part.is_empty() {
let part_len = current_part.len();
parts.push((
StringPart::Data(current_part),
SimpleSpan::new(span.context(), start..part_len),
))
}
Ok(parts)
}
pub fn parser<I>(
) -> impl Parser<'static, I, Spanned<Ast>, extra::Err<Rich<'static, Token, Span>>> + Clone
where
I: ValueInput<'static, Token = Token, Span = Span>,
{
use Ast::*;
let mut expr = Recursive::declare();
let mut pattern = Recursive::declare();
let mut simple = Recursive::declare();
let mut nonbinding = Recursive::declare();
let separators = recursive(|separators| {
just(Token::Punctuation(","))
.or(just(Token::Punctuation("\n")))
.then(separators.clone().repeated())
})
.labelled("separator");
let terminators = recursive(|terminators| {
just(Token::Punctuation(";"))
.or(just(Token::Punctuation("\n")))
.then(terminators.clone().repeated())
})
.labelled("terminator");
let placeholder_pattern =
select! {Token::Punctuation("_") => PlaceholderPattern}.map_with(|p, e| (p, e.span()));
let word_pattern = select! { Token::Word(w) => WordPattern(w) }.map_with(|w, e| (w, e.span()));
let atom_pattern = select! {
Token::Nil => NilPattern,
Token::Boolean(b) => BooleanPattern(b),
Token::Number(n) => NumberPattern(n),
Token::Keyword(k) => KeywordPattern(k),
}
.map_with(|a, e| (a, e.span()));
let string_pattern = select! {Token::String(s) => s}.try_map_with(|s, e| {
let parsed = parse_string(s, e.span());
match parsed {
Ok(parts) => match parts[0] {
(StringPart::Inline(_), _) => Ok((StringPattern(s), e.span())),
_ => Ok((InterpolatedPattern(parts.clone()), e.span())),
},
Err(msg) => Err(Rich::custom(e.span(), msg)),
}
});
let bare_splat = just(Token::Punctuation("...")).map_with(|_, e| {
(
Splattern(Box::new((PlaceholderPattern, e.span()))),
e.span(),
)
});
let splattable = word_pattern.or(placeholder_pattern);
let patt_splat = just(Token::Punctuation("..."))
.ignore_then(splattable)
.map_with(|x, e| (Splattern(Box::new(x)), e.span()));
let splattern = patt_splat.or(bare_splat);
let tuple_pattern = pattern
.clone()
.or(splattern.clone())
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("(")), just(Token::Punctuation(")")))
.map_with(|tuple, e| (TuplePattern(tuple), e.span()))
.labelled("tuple pattern");
let list_pattern = pattern
.clone()
.or(splattern.clone())
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("[")), just(Token::Punctuation("]")))
.map_with(|list, e| (ListPattern(list), e.span()))
.labelled("list pattern");
let key_pair_pattern = select! {Token::Keyword(k) => k}
.then(pattern.clone())
.map_with(|(key, patt), e| (KeyPairPattern(key, Box::new(patt)), e.span()));
let shorthand_pattern = select! {Token::Word(w) => w}.map_with(|w, e| {
(
KeyPairPattern(w, Box::new((WordPattern(w), e.span()))),
e.span(),
)
});
let str_pair_pattern = select! {Token::String(s) => s}
.then(pattern.clone())
.map_with(|(key, patt), e| (StrPairPattern(key, Box::new(patt)), e.span()));
let dict_pattern = key_pair_pattern
.or(shorthand_pattern)
.or(str_pair_pattern)
.or(splattern.clone())
.labelled("pair pattern")
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(
just(Token::Punctuation("#{")),
just(Token::Punctuation("}")),
)
.map_with(|dict, e| (DictPattern(dict), e.span()));
let keyword = select! {Token::Keyword(k) => Keyword(k)}
.map_with(|k, e| (k, e.span()))
.labelled("keyword");
let as_pattern = select! {Token::Word(w) => w}
.then_ignore(just(Token::Reserved("as")))
.then(select! {Token::Keyword(k) => k})
.labelled("keyword")
.map_with(|(w, t), e| (AsPattern(w, t), e.span()));
pattern.define(
atom_pattern
.or(string_pattern)
.or(as_pattern)
.or(word_pattern)
.or(placeholder_pattern)
.or(tuple_pattern.clone())
.or(list_pattern)
.or(dict_pattern)
.labelled("pattern"),
);
let placeholder =
select! {Token::Punctuation("_") => Placeholder}.map_with(|p, e| (p, e.span()));
let word = select! { Token::Word(w) => Word(w) }
.map_with(|w, e| (w, e.span()))
.labelled("word");
let value = select! {
Token::Nil => Nil,
Token::Boolean(b) => Boolean(b),
Token::Number(n) => Number(n),
}
.map_with(|v, e| (v, e.span()));
let string = select! {Token::String(s) => s}.try_map_with(|s, e| {
let parsed = parse_string(s, e.span());
match parsed {
Ok(parts) => match parts[0] {
(StringPart::Inline(_), _) => Ok((String(s), e.span())),
_ => Ok((Interpolated(parts), e.span())),
},
Err(msg) => Err(Rich::custom(e.span(), msg)),
}
});
let tuple = simple
.clone()
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("(")), just(Token::Punctuation(")")))
.map_with(|tuple, e| (Tuple(tuple), e.span()))
.labelled("tuple");
let args = simple
.clone()
.or(placeholder)
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("(")), just(Token::Punctuation(")")))
.map_with(|args, e| (Arguments(args), e.span()))
.labelled("args");
let or = just(Token::Reserved("or")).map_with(|_, e| (Or, e.span()));
let and = just(Token::Reserved("and")).map_with(|_, e| (And, e.span()));
let method = select!(Token::Method(m) => m)
.then(tuple.clone())
.map_with(|(m, t), e| (Ast::Method(m, Box::new(t)), e.span()))
.labelled("method");
let synth_root = or.or(and).or(word).or(keyword);
let synth_term = keyword.or(args).or(method);
let synthetic = synth_root
.then(synth_term.clone())
.then(synth_term.clone().repeated().collect())
.map_with(|((root, first), rest), e| {
(Synthetic(Box::new(root), Box::new(first), rest), e.span())
});
let splat = just(Token::Punctuation("..."))
.ignore_then(word)
.map_with(|(w, _), e| {
(
Splat(if let Word(w) = w { w } else { unreachable!() }),
e.span(),
)
})
.labelled("...");
let list = simple
.clone()
.or(splat.clone())
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("[")), just(Token::Punctuation("]")))
.map_with(|list, e| (List(list), e.span()));
let key_pair = select! {Token::Keyword(k) => k}
.then(simple.clone())
.map_with(|(key, value), e| (KeywordPair(key, Box::new(value)), e.span()));
let shorthand = select! {Token::Word(w) => w}
.map_with(|w, e| (KeywordPair(w, Box::new((Word(w), e.span()))), e.span()));
let str_pair = select! {Token::String(s) => s}
.then(simple.clone())
.map_with(|(key, value), e| (StringPair(key, Box::new(value)), e.span()));
let dict = key_pair
.or(shorthand)
.or(str_pair)
.or(splat.clone())
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(
just(Token::Punctuation("#{")),
just(Token::Punctuation("}")),
)
.map_with(|dict, e| (Dict(dict), e.span()));
let recur = just(Token::Reserved("recur"))
.ignore_then(tuple.clone())
.map_with(|args, e| {
let (Tuple(args), _) = args else {
unreachable!()
};
(Recur(args), e.span())
});
let block = expr
.clone()
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}")))
.map_with(|block, e| (Block(block), e.span()))
.recover_with(via_parser(nested_delimiters(
Token::Punctuation("{"),
Token::Punctuation("}"),
[
(Token::Punctuation("("), Token::Punctuation(")")),
(Token::Punctuation("["), Token::Punctuation("]")),
],
|span| (Error, span),
)));
let r#if = just(Token::Reserved("if"))
.ignore_then(simple.clone())
.then_ignore(terminators.clone().or_not())
.then_ignore(just(Token::Reserved("then")))
.then(expr.clone())
.then_ignore(terminators.clone().or_not())
.then_ignore(just(Token::Reserved("else")))
.then(expr.clone())
.map_with(|((condition, then_branch), else_branch), e| {
(
If(
Box::new(condition),
Box::new(then_branch),
Box::new(else_branch),
),
e.span(),
)
});
let when_clause = simple
.clone()
.then_ignore(just(Token::Punctuation("->")))
.then(expr.clone())
.map_with(|(cond, body), e| (WhenClause(Box::new(cond), Box::new(body)), e.span()));
let when = just(Token::Reserved("when"))
.ignore_then(
when_clause
.separated_by(terminators.clone())
.allow_trailing()
.allow_leading()
.collect()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}"))),
)
.map_with(|clauses, e| (When(clauses), e.span()));
let guarded_clause = pattern
.clone()
.then_ignore(just(Token::Reserved("if")))
.then(simple.clone())
.then_ignore(just(Token::Punctuation("->")))
.then(expr.clone())
.map_with(|((patt, guard), body), e| {
(
MatchClause(Box::new(patt), Box::new(Some(guard)), Box::new(body)),
e.span(),
)
});
let match_clause = pattern
.clone()
.then_ignore(just(Token::Punctuation("->")))
.then(expr.clone())
.map_with(|(patt, body), e| {
(
MatchClause(Box::new(patt), Box::new(None), Box::new(body)),
e.span(),
)
});
let r#match = just(Token::Reserved("match"))
.ignore_then(simple.clone())
.then_ignore(just(Token::Reserved("with")))
.then(
match_clause
.clone()
.or(guarded_clause.clone())
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}"))),
)
.map_with(|(expr, clauses), e| (Match(Box::new(expr), clauses), e.span()));
let receive = just(Token::Reserved("receive"))
.ignore_then(
match_clause
.clone()
.or(guarded_clause)
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}"))),
)
.map_with(|clauses, e| (Receive(clauses), e.span()));
let conditional = when.or(r#if).or(r#match).or(receive);
let panic = just(Token::Reserved("panic!"))
.ignore_then(nonbinding.clone())
.map_with(|expr, e| (Panic(Box::new(expr)), e.span()));
let r#do = just(Token::Reserved("do"))
.ignore_then(
simple
.clone()
.separated_by(
just(Token::Punctuation(">")).then(just(Token::Punctuation("\n")).repeated()),
)
.collect(),
)
.map_with(|exprs, e| (Do(exprs), e.span()));
let repeat = just(Token::Reserved("repeat"))
.ignore_then(simple.clone())
.then(block.clone())
.map_with(|(count, body), e| (Repeat(Box::new(count), Box::new(body)), e.span()));
let spawn = just(Token::Reserved("spawn!"))
.ignore_then(nonbinding.clone())
.map_with(|body, e| (Spawn(Box::new(body)), e.span()));
let fn_guarded = tuple_pattern
.clone()
.then_ignore(just(Token::Reserved("if")))
.then(simple.clone())
.then_ignore(just(Token::Punctuation("->")))
.then(nonbinding.clone())
.map_with(|((patt, guard), body), e| {
(
MatchClause(Box::new(patt), Box::new(Some(guard)), Box::new(body)),
e.span(),
)
})
.labelled("function clause");
let fn_unguarded = tuple_pattern
.clone()
.then_ignore(just(Token::Punctuation("->")))
.then(nonbinding.clone())
.map_with(|(patt, body), e| {
(
MatchClause(Box::new(patt), Box::new(None), Box::new(body)),
e.span(),
)
})
.labelled("function clause");
let fn_clause = fn_guarded.clone().or(fn_unguarded.clone());
let lambda = just(Token::Reserved("fn"))
.ignore_then(fn_unguarded.clone())
.map_with(|clause, e| {
(
Fn("", Box::new((Ast::FnBody(vec![clause]), e.span())), None),
e.span(),
)
});
let fn_clauses = fn_clause
.clone()
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
.collect();
let loop_multiclause = fn_clauses
.clone()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}")));
let fn_single_clause = fn_clause.clone().map_with(|c, _| vec![c]);
let r#loop = just(Token::Reserved("loop"))
.ignore_then(tuple.clone())
.then_ignore(just(Token::Reserved("with")))
.then(loop_multiclause.clone().or(fn_single_clause.clone()))
.map_with(|(init, body), e| (Loop(Box::new(init), body), e.span()));
simple.define(
synthetic
.or(recur)
.or(word)
.or(keyword)
.or(value)
.or(tuple.clone())
.or(list)
.or(dict)
.or(panic)
.or(string)
.or(r#do)
.or(lambda.clone())
.labelled("simple expression"),
);
nonbinding.define(
simple
.clone()
.or(conditional)
.or(block)
.or(repeat)
.or(spawn)
.or(r#loop)
.labelled("nonbinding expression"),
);
let let_ = just(Token::Reserved("let"))
.ignore_then(pattern.clone())
.then_ignore(just(Token::Punctuation("=")))
.then(nonbinding.clone())
.map_with(|(pattern, expression), e| {
(Let(Box::new(pattern), Box::new(expression)), e.span())
});
let box_ = just(Token::Reserved("box"))
.ignore_then(word)
.then_ignore(just(Token::Punctuation("=")))
.then(nonbinding.clone())
.map_with(|(word, expr), e| {
let name = if let Word(w) = word.0 {
w
} else {
unreachable!()
};
(LBox(name, Box::new(expr)), e.span())
});
let fn_decl = just(Token::Reserved("fn"))
.ignore_then(word)
.map_with(|(word, _), e| {
let name = if let Word(w) = word {
w
} else {
unreachable!()
};
(FnDeclaration(name), e.span())
});
let fn_named = just(Token::Reserved("fn"))
.ignore_then(word)
.then(fn_unguarded.clone())
.map_with(|(word, clause), e| {
let name = if let Word(word) = word.0 {
word
} else {
unreachable!()
};
(
Fn(name, Box::new((Ast::FnBody(vec![clause]), e.span())), None),
e.span(),
)
});
let docstr = select! {Token::String(s) => s};
let fn_multiclause = separators
.clone()
.or_not()
.ignore_then(docstr.or_not())
.then(fn_clauses.clone())
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}")))
.map_with(|(docstr, clauses), e| (docstr, clauses, e.span()));
let fn_compound = just(Token::Reserved("fn"))
.ignore_then(word)
.then(fn_multiclause)
.map_with(|(word, (docstr, clauses, _)), e| {
let name = if let Word(word) = word.0 {
word
} else {
unreachable!()
};
(
Fn(name, Box::new((Ast::FnBody(clauses), e.span())), docstr),
e.span(),
)
});
let fn_ = fn_named.or(fn_compound).or(fn_decl);
let binding = let_.or(box_).or(fn_);
expr.define(binding.or(nonbinding));
let script = expr
.separated_by(terminators.clone())
.allow_trailing()
.allow_leading()
.collect()
.map_with(|exprs, e| (Block(exprs), e.span()));
script
}

View File

@ -1,4 +0,0 @@
use chumsky::prelude::*;
pub type Span = SimpleSpan<usize>;
pub type Spanned<T> = (T, Span);

View File

@ -1,677 +0,0 @@
// TODO:
// * [ ] ensure loops have fixed arity (no splats)
// * [ ] ensure fn pattern splats are always highest (and same) arity
use crate::ast::{Ast, StringPart};
use crate::spans::{Span, Spanned};
use crate::value::{Key, Value};
use std::cmp::max;
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq)]
pub struct VErr<'a> {
pub msg: String,
pub span: &'a Span,
pub input: &'static str,
pub src: &'static str,
}
impl<'a> VErr<'a> {
pub fn new(msg: String, span: &'a Span, input: &'static str, src: &'static str) -> VErr<'a> {
VErr {
msg,
span,
input,
src,
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct VStatus {
tail_position: bool,
in_loop: bool,
loop_arity: u8,
last_term: bool,
has_placeholder: bool,
used_bindings: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Arity {
Fixed(u8),
Splat(u8),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FnInfo {
Declared,
Defined(String, HashSet<Arity>, HashSet<String>),
Unknown,
}
fn match_arities(arities: &HashSet<Arity>, num_args: u8) -> bool {
arities.iter().any(|arity| match arity {
Arity::Fixed(n) => *n == num_args,
Arity::Splat(n) => *n <= num_args,
})
}
#[derive(Debug, PartialEq)]
pub struct Validator<'a> {
pub locals: Vec<(String, &'a Span, FnInfo)>,
pub prelude: imbl::HashMap<Key, Value>,
pub input: &'static str,
pub src: &'static str,
pub ast: &'a Ast,
pub span: &'a Span,
pub errors: Vec<VErr<'a>>,
pub fn_info: HashMap<*const Ast, FnInfo>,
status: VStatus,
}
impl<'a> Validator<'a> {
pub fn new(
ast: &'a Ast,
span: &'a Span,
input: &'static str,
src: &'static str,
prelude: imbl::HashMap<Key, Value>,
) -> Validator<'a> {
Validator {
input,
src,
locals: vec![],
prelude,
ast,
span,
fn_info: std::collections::HashMap::new(),
errors: vec![],
status: VStatus {
tail_position: false,
in_loop: false,
loop_arity: 0,
last_term: false,
has_placeholder: false,
used_bindings: vec![],
},
}
}
fn bind(&mut self, name: String) {
self.locals.push((name, self.span, FnInfo::Unknown));
}
fn declare_fn(&mut self, name: String) {
if name.is_empty() {
return;
}
self.locals.push((name, self.span, FnInfo::Declared));
}
fn define_fn(&mut self, name: String, info: FnInfo) {
if name.is_empty() {
return;
}
let i = self.locals.iter().position(|(n, ..)| *n == name).unwrap();
let new_binding = (name, self.locals[i].1, info);
self.locals[i] = new_binding;
}
fn resolved(&self, name: &'static str) -> bool {
self.locals.iter().any(|(bound, ..)| name == bound.as_str())
|| self
.prelude
.iter()
.any(|(bound, _)| Key::Keyword(name) == *bound)
}
fn bound(&self, name: &str) -> Option<&(String, &Span, FnInfo)> {
match self.locals.iter().rev().find(|(bound, ..)| name == bound) {
Some(binding) => Some(binding),
None => None,
}
}
fn err(&mut self, msg: String) {
self.errors
.push(VErr::new(msg, self.span, self.input, self.src))
}
fn use_name(&mut self, name: String) {
self.status.used_bindings.push(name);
}
fn arity(&mut self) -> Arity {
let Ast::MatchClause(pattern, ..) = self.ast else {
unreachable!("internal Ludus error")
};
let (Ast::TuplePattern(members), _) = pattern.as_ref() else {
unreachable!("internal Ludus error");
};
let last_member = members.last();
match last_member {
None => Arity::Fixed(0),
Some((Ast::Splattern(..), _)) => Arity::Splat(members.len() as u8),
Some(_) => Arity::Fixed(members.len() as u8),
}
}
fn visit(&mut self, node: &'a Spanned<Ast>) {
let (expr, span) = node;
self.ast = expr;
self.span = span;
self.validate();
}
pub fn validate(&mut self) {
use Ast::*;
let root = self.ast;
match root {
Error => unreachable!(),
Spawn(body) => self.visit(body),
Word(name) | Splat(name) => {
if !self.resolved(name) {
self.err(format!("unbound name `{name}`"))
} else {
self.use_name(name.to_string())
}
}
Interpolated(parts) => {
for part in parts {
if let (StringPart::Word(name), span) = part {
self.span = span;
if !self.resolved(name) {
self.err(format!("unbound name `{name}`"));
} else {
self.use_name(name.to_string());
}
}
}
}
// validate each line
// ensure it's not empty
// pass through tail position validation
// check if there are any declared but undefined functions
// pop all the bindings off the local stack
Block(block) => {
if block.is_empty() {
self.err("blocks must have at least one expression".to_string());
return;
}
let to = self.locals.len();
let tailpos = self.status.tail_position;
for line in block.iter().take(block.len() - 1) {
self.status.tail_position = false;
self.visit(line);
}
self.status.tail_position = tailpos;
self.visit(block.last().unwrap());
let block_bindings = self.locals.split_off(to);
for binding in block_bindings {
let (name, _, fn_info) = binding;
if matches!(fn_info, FnInfo::Declared) {
self.err(format!("fn `{name}` is declared but not defined"))
}
}
}
// if in tail position, pass through tail position validation
// no unbound names
If(cond, then, r#else) => {
let tailpos = self.status.tail_position;
self.status.tail_position = false;
self.visit(cond.as_ref());
// pass through tailpos only to then/else
self.status.tail_position = tailpos;
self.visit(then.as_ref());
self.visit(r#else.as_ref());
}
Tuple(members) => {
if members.is_empty() {
return;
}
if members.len() > 7 {
self.err("tuples may not have more than 7 members".to_string());
}
let tailpos = self.status.tail_position;
self.status.tail_position = false;
for member in members {
self.visit(member);
}
self.status.tail_position = tailpos;
}
// no more than one placeholder
Arguments(args) => {
if args.is_empty() {
return;
}
if args.len() > 7 {
self.err("tuples may not have more than 7 members".to_string());
}
let tailpos = self.status.tail_position;
self.status.tail_position = false;
for arg in args {
self.visit(arg);
}
self.status.has_placeholder = false;
self.status.tail_position = tailpos;
}
Placeholder => {
if self.status.has_placeholder {
self.err(
"you may only use one placeholder when partially applying functions"
.to_string(),
);
}
self.status.has_placeholder = true;
}
List(list) => {
if list.is_empty() {
return;
}
let tailpos = self.status.tail_position;
self.status.tail_position = false;
for member in list {
self.visit(member);
}
self.status.tail_position = tailpos;
}
KeywordPair(_, value) | StringPair(_, value) => self.visit(value.as_ref()),
Dict(dict) => {
if dict.is_empty() {
return;
}
let tailpos = self.status.tail_position;
self.status.tail_position = false;
for pair in dict {
self.visit(pair)
}
self.status.tail_position = tailpos;
}
// TODO!
// check arity against fn info if first term is word and second term is args
Synthetic(first, second, rest) => {
match (&first.0, &second.0) {
(Ast::Word(_), Ast::Method(_, args)) => {
self.visit(first.as_ref());
self.visit(args);
}
(Ast::Keyword(_), Ast::Method(_, args)) => {
self.visit(args);
}
(Ast::And, Ast::Arguments(_)) | (Ast::Or, Ast::Arguments(_)) => {
self.visit(second.as_ref())
}
(Ast::Word(_), Ast::Keyword(_)) => self.visit(first.as_ref()),
(Ast::Keyword(_), Ast::Arguments(args)) => {
if args.len() != 1 {
self.err("called keywords may only take one argument".to_string())
}
self.visit(second.as_ref());
}
(Ast::Word(name), Ast::Arguments(args)) => {
self.visit(first.as_ref());
self.visit(second.as_ref());
//TODO: check arities of prelude fns, too
let fn_binding = self.bound(name);
if let Some((_, _, FnInfo::Defined(_, arities, _))) = fn_binding {
let num_args = args.len();
if !match_arities(arities, num_args as u8) {
self.err(format!("arity mismatch: no clause in function `{name}` with {num_args} argument(s)"))
}
}
}
_ => unreachable!(
"malformed synthetic root with\nfirst: {}\nsecond: {}",
first.0, second.0
),
}
for term in rest {
self.visit(term);
}
}
WhenClause(cond, body) => {
let tailpos = self.status.tail_position;
self.status.tail_position = false;
self.visit(cond.as_ref());
//pass through tail position for when bodies
self.status.tail_position = tailpos;
self.visit(body.as_ref());
}
When(clauses) => {
for clause in clauses {
self.visit(clause);
}
}
// binding forms
// TODO: set up errors to include original binding
LBox(name, boxed) => {
if self.bound(name).is_some() {
self.err(format!("box name `{name}` is already bound"));
} else {
self.bind(name.to_string());
}
self.visit(boxed.as_ref());
}
Let(lhs, rhs) => {
self.visit(rhs.as_ref());
self.visit(lhs.as_ref());
}
MatchClause(pattern, guard, body) => {
let to = self.locals.len();
self.visit(pattern.as_ref());
if let Some(guard) = guard.as_ref() {
self.visit(guard);
}
self.visit(body.as_ref());
self.locals.truncate(to);
}
Match(scrutinee, clauses) => {
self.visit(scrutinee.as_ref());
for clause in clauses {
self.visit(clause);
}
}
Receive(clauses) => {
for clause in clauses {
self.visit(clause);
}
}
FnDeclaration(name) => {
let tailpos = self.status.tail_position;
self.status.tail_position = false;
if self.bound(name).is_some() {
self.err(format!("fn name `{name}` is already bound"));
return;
}
self.declare_fn(name.to_string());
self.status.tail_position = tailpos;
}
FnBody(..) => unreachable!(),
Fn(name, body, ..) => {
let mut is_declared = false;
match self.bound(name) {
Some((_, _, FnInfo::Declared)) => is_declared = true,
None => (),
_ => {
self.err(format!("name `{name}` is already bound"));
}
}
// TODO: devise a placeholder binding for recursive functions
if !is_declared {
self.declare_fn(name.to_string());
}
let from = self.status.used_bindings.len();
let mut arities = HashSet::new();
let (Ast::FnBody(clauses), _) = body.as_ref() else {
unreachable!()
};
for clause in clauses {
// we have to do this explicitly here because of arity checking
let (expr, span) = clause;
self.ast = expr;
self.span = span;
// add clause arity to arities
arities.insert(self.arity());
self.validate();
}
let mut max_arity = 0;
let mut has_splat = false;
for arity in arities.iter() {
match arity {
Arity::Fixed(n) => {
max_arity = max(*n, max_arity);
}
Arity::Splat(n) => {
max_arity = max(*n, max_arity);
has_splat = true;
}
}
}
for arity in arities.iter() {
match arity {
Arity::Fixed(n) => {
if *n == max_arity && has_splat {
self.err(
"splats must be the longest arity in function clauses"
.to_string(),
);
break;
}
}
Arity::Splat(n) => {
if *n > max_arity {
self.err(
"splats must be the longest arity in function clauses"
.to_string(),
);
break;
}
}
}
}
// collect info about what the function closes over
let mut closed_over = HashSet::new();
for binding in self.status.used_bindings.iter().skip(from) {
if self.bound(binding.as_str()).is_some() {
// println!("{name} closing over {binding}");
closed_over.insert(binding.clone());
}
}
let info = FnInfo::Defined(name.to_string(), arities, closed_over);
let root_ptr: *const Ast = root;
self.fn_info.insert(root_ptr, info.clone());
self.define_fn(name.to_string(), info);
}
Panic(msg) => {
let tailpos = self.status.tail_position;
self.status.tail_position = false;
self.visit(msg.as_ref());
self.status.tail_position = tailpos;
}
// TODO: fix the tail call here?
Do(terms) => {
if terms.len() < 2 {
return self.err("do expressions must have at least two terms".to_string());
}
for term in terms.iter().take(terms.len() - 1) {
self.visit(term);
}
let last = terms.last().unwrap();
self.visit(last);
if matches!(last.0, Ast::Recur(_)) {
self.err("`recur` may not be used in `do` forms".to_string());
}
}
Repeat(times, body) => {
self.status.tail_position = false;
self.visit(times.as_ref());
self.visit(body.as_ref());
}
Loop(with, body) => {
self.visit(with.as_ref());
let Ast::Tuple(input) = &with.0 else {
unreachable!()
};
// dbg!(&input);
let tailpos = self.status.tail_position;
self.status.tail_position = true;
let in_loop = self.status.in_loop;
let outer_arity = self.status.loop_arity;
self.status.in_loop = true;
let loop_arity = input.len() as u8;
self.status.loop_arity = loop_arity;
for clause in body {
let (expr, span) = clause;
self.ast = expr;
self.span = span;
let arity = self.arity();
// dbg!(&arity);
match arity {
Arity::Fixed(clause_arity) => {
if clause_arity != loop_arity {
self.err(format!("mismatched arity: expected {loop_arity} arguments in `loop` clause; got {clause_arity}"))
}
}
Arity::Splat(clause_arity) => {
if clause_arity > loop_arity {
self.err("loop clauses may not have splats".to_string())
}
}
};
self.validate();
}
self.status.tail_position = tailpos;
self.status.in_loop = in_loop;
self.status.loop_arity = outer_arity;
}
Recur(args) => {
if !self.status.in_loop {
self.err("you may only use `recur` in a `loop` form".to_string());
return;
}
if !self.status.tail_position {
self.err("you may only use `recur` in tail position".to_string());
}
if args.len() > 7 {
self.err("tuples may only have 7 members".to_string());
}
let num_args = args.len() as u8;
let loop_arity = self.status.loop_arity;
if num_args != loop_arity {
self.err(format!("loop arity mismatch: loop has arity of {loop_arity}; `recur` called with {num_args} arguments"))
}
self.status.tail_position = false;
for arg in args {
self.visit(arg);
}
}
WordPattern(name) => match self.bound(name) {
Some((name, _span, _)) => {
self.err(format!("name `{name}` is already bound"));
}
None => {
self.bind(name.to_string());
}
},
InterpolatedPattern(parts) => {
for (part, span) in parts {
if let StringPart::Word(name) = part {
self.span = span;
match self.bound(name) {
Some(_) => self.err(format!("name `{name}` is already bound")),
None => self.bind(name.to_string()),
}
}
}
}
AsPattern(name, r#type) => {
match self.bound(name) {
Some((name, _span, _)) => {
self.err(format!("name `{name}` is already bound"));
}
None => {
self.bind(name.to_string());
}
}
let as_type = *r#type;
match as_type {
"nil" | "bool" | "number" | "keyword" | "string" | "tuple" | "dict"
| "list" | "fn" | "box" => (),
_ => self.err(format!("unknown type `:{as_type}`")),
}
}
Splattern(splatted) => {
if !self.status.last_term {
self.err("splats in patterns must come last".to_string());
}
match splatted.as_ref() {
(PlaceholderPattern, _) => (),
(WordPattern(name), span) => match self.bound(name) {
Some(_) => {
self.span = span;
self.err(format!("name `{name}` is already bound"))
}
None => self.bind(name.to_string()),
},
_ => {
println!("internal Ludus error: unexpected splat pattern");
// dbg!(splatted);
unreachable!()
}
}
}
TuplePattern(terms) => {
if terms.is_empty() {
return;
}
if terms.len() > 7 {
self.err("tuples may only have 7 arguments".to_string())
}
for term in terms.iter().take(terms.len() - 1) {
self.visit(term);
}
self.status.last_term = true;
let last = terms.last().unwrap();
self.visit(last);
self.status.last_term = false;
}
ListPattern(terms) | DictPattern(terms) => {
if terms.is_empty() {
return;
}
for term in terms.iter().take(terms.len() - 1) {
self.visit(term);
}
self.status.last_term = true;
let last = terms.last().unwrap();
self.visit(last);
self.status.last_term = false;
}
KeyPairPattern(_, patt) | StrPairPattern(_, patt) => self.visit(patt.as_ref()),
// terminals can never be invalid
Nil | Boolean(_) | Number(_) | Keyword(_) | String(_) | And | Or | Method(..) => (),
// terminal patterns can never be invalid
NilPattern | BooleanPattern(..) | NumberPattern(..) | StringPattern(..)
| KeywordPattern(..) | PlaceholderPattern => (),
};
self.ast = root;
}
}

View File

@ -1,510 +0,0 @@
use crate::base::BaseFn;
use crate::chunk::Chunk;
use imbl::{HashMap, Vector};
use ordered_float::NotNan;
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug)]
pub enum LFn {
Declared {
name: &'static str,
},
Defined {
name: &'static str,
doc: Option<&'static str>,
arities: Vec<u8>,
chunks: Vec<Chunk>,
splat: u8,
closed: RefCell<Vec<Value>>,
patterns: Vec<String>,
},
}
impl LFn {
pub fn close(&self, value: Value) {
match self {
LFn::Declared { .. } => unreachable!(),
LFn::Defined { closed, .. } => {
let shown = value.show();
closed.borrow_mut().push(value);
let pos = closed.borrow().len();
if crate::DEBUG_SCRIPT_RUN {
println!("closing over in {} at {pos}: {shown}", self.name(),);
}
}
}
}
pub fn patterns(&self) -> Value {
match self {
LFn::Declared { .. } => unreachable!(),
LFn::Defined { patterns, .. } => Value::from_string(
patterns
.iter()
.map(|pattern| format!(" {pattern}"))
.collect::<Vec<_>>()
.join("\n"),
),
}
}
pub fn doc(&self) -> Value {
match self {
LFn::Declared { name } => {
Value::String(Rc::new(format!("fn {name}: undefined function")))
}
LFn::Defined { doc: Some(doc), .. } => Value::String(Rc::new(doc.to_string())),
LFn::Defined { .. } => Value::String(Rc::new("no documentation found".to_string())),
}
}
pub fn accepts(&self, arity: u8) -> bool {
match self {
LFn::Defined { arities, splat, .. } => {
if arities.contains(&arity) {
return true;
}
if *splat == 0 {
return false;
}
let max_arity = arities.iter().fold(0, |a, b| a.max(*b));
arity > max_arity
}
LFn::Declared { .. } => unreachable!(),
}
}
pub fn splat_arity(&self) -> u8 {
match self {
LFn::Defined { splat, .. } => *splat,
LFn::Declared { .. } => unreachable!(),
}
}
pub fn name(&self) -> &'static str {
match self {
LFn::Declared { name } | LFn::Defined { name, .. } => name,
}
}
pub fn chunk(&self, arity: u8) -> &Chunk {
match self {
LFn::Declared { .. } => unreachable!(),
LFn::Defined {
arities,
splat,
chunks,
..
} => {
let chunk_pos = arities.iter().position(|a| arity == *a);
match chunk_pos {
Some(pos) => &chunks[pos],
None => &chunks[*splat as usize],
}
}
}
}
pub fn upvalue(&self, idx: u8) -> Value {
match self {
LFn::Declared { .. } => unreachable!(),
LFn::Defined { closed, .. } => closed.borrow()[idx as usize].clone(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Partial {
pub args: Vec<Value>,
pub name: &'static str,
pub function: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Key {
Keyword(&'static str),
Interned(&'static str),
String(Rc<String>),
}
impl std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Key::Keyword(s) => write!(f, ":{s}"),
Key::Interned(s) => write!(f, "\"{s}\""),
Key::String(s) => write!(f, "\"{s}\""),
}
}
}
impl Serialize for Key {
fn serialize<S>(&self, srlzr: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Key::Keyword(s) => srlzr.serialize_str(s),
Key::Interned(s) => srlzr.serialize_str(s),
Key::String(s) => srlzr.serialize_str(s.as_str()),
}
}
}
impl Key {
pub fn to_value(&self) -> Value {
match self {
Key::Keyword(s) => Value::Keyword(s),
Key::Interned(s) => Value::Interned(s),
Key::String(s) => Value::String(s.clone()),
}
}
pub fn from_value(value: Value) -> Key {
match value {
Value::Keyword(s) => Key::Keyword(s),
Value::Interned(s) => Key::Interned(s),
Value::String(s) => Key::String(s.clone()),
_ => unreachable!("dict keys must be keywords or strings"),
}
}
}
#[derive(Clone, Debug)]
pub enum Value {
Nothing,
Nil,
True,
False,
Keyword(&'static str),
Interned(&'static str),
String(Rc<String>),
Number(NotNan<f64>),
Tuple(Rc<Vec<Value>>),
List(Box<Vector<Value>>),
Dict(Box<HashMap<Key, Value>>), // not hashable b/c why?
Box(Rc<RefCell<Value>>), // not hashable b/c refcell
Fn(Rc<LFn>), // not hashable b/c refcell
BaseFn(Box<BaseFn>),
Partial(Rc<Partial>),
Process,
}
impl PartialEq for Value {
fn eq(&self, other: &Value) -> bool {
use Value::*;
match (self, other) {
(Nothing, Nothing) | (Nil, Nil) | (True, True) | (False, False) => true,
(Keyword(str1), Keyword(str2)) | (Interned(str1), Interned(str2)) => str1 == str2,
(String(x), String(y)) => x == y,
(String(x), Interned(y)) => x.as_ref() == y,
(Interned(x), String(y)) => x == y.as_ref(),
(Number(x), Number(y)) => x == y,
(Tuple(x), Tuple(y)) => x == y,
(List(x), List(y)) => x == y,
(Dict(x), Dict(y)) => x == y,
(Box(x), Box(y)) => std::ptr::eq(x.as_ref().as_ptr(), y.as_ref().as_ptr()),
(Fn(x), Fn(y)) => std::ptr::eq(x, y),
(BaseFn(x), BaseFn(y)) => std::ptr::eq(x, y),
(Partial(x), Partial(y)) => x == y,
_ => false,
}
}
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Value::*;
match self {
Nothing => write!(f, "_"),
Nil => write!(f, "nil"),
True => write!(f, "true"),
False => write!(f, "false"),
Keyword(str) => write!(f, ":{str}"),
Interned(str) => write!(f, "\"{str}\""),
String(str) => write!(f, "\"{str}\""),
Number(n) => write!(f, "{n}"),
Process => write!(f, "Process"),
Tuple(members) => write!(
f,
"({})",
members
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
),
List(members) => write!(
f,
"[{}]",
members
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
),
Dict(members) => write!(
f,
"#{{{}}}",
members
.iter()
.map(|(k, v)| format!("{k} {v}"))
.collect::<Vec<_>>()
.join(", ")
),
Box(value) => write!(f, "box {{ {} }}", value.as_ref().borrow()),
Fn(lfn) => write!(f, "fn {}", lfn.name()),
BaseFn(inner) => {
let name = match **inner {
crate::base::BaseFn::Nullary(name, _)
| crate::base::BaseFn::Unary(name, _)
| crate::base::BaseFn::Binary(name, _)
| crate::base::BaseFn::Ternary(name, _) => name,
};
write!(f, "fn {name}/base")
}
Partial(partial) => write!(f, "fn {}/partial", partial.name),
}
}
}
impl Serialize for Value {
fn serialize<S>(&self, srlzr: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use Value::*;
match self {
Nil => srlzr.serialize_none(),
True => srlzr.serialize_bool(true),
False => srlzr.serialize_bool(false),
Number(n) => srlzr.serialize_f64(f64::from(*n)),
Interned(s) => srlzr.serialize_str(s),
Keyword(k) => srlzr.serialize_str(k),
String(s) => srlzr.serialize_str(s.as_str()),
Tuple(t) => {
let mut seq = srlzr.serialize_seq(Some(t.len()))?;
for e in t.iter() {
seq.serialize_element(e)?;
}
seq.end()
}
List(l) => {
let mut seq = srlzr.serialize_seq(Some(l.len()))?;
for e in l.iter() {
seq.serialize_element(e)?;
}
seq.end()
}
Dict(d) => {
let mut map = srlzr.serialize_map(Some(d.len()))?;
for (k, v) in d.iter() {
map.serialize_entry(k, v)?;
}
map.end()
}
Box(b) => {
let boxed = b.borrow();
(*boxed).serialize(srlzr)
}
Fn(..) | BaseFn(..) | Partial(..) => unreachable!(),
Process | Nothing => unreachable!(),
}
}
}
impl Value {
pub fn show(&self) -> String {
use Value::*;
let mut out = match &self {
Process => "Process".to_string(),
Nil => "nil".to_string(),
True => "true".to_string(),
False => "false".to_string(),
Number(n) => format!("{n}"),
Interned(str) => format!("\"{str}\""),
String(str) => format!("\"{str}\""),
Keyword(str) => format!(":{str}"),
Tuple(t) => {
let members = t.iter().map(|e| e.show()).collect::<Vec<_>>().join(", ");
format!("({members})")
}
List(l) => {
let members = l.iter().map(|e| e.show()).collect::<Vec<_>>().join(", ");
format!("[{members}]")
}
Dict(d) => {
let members = d
.iter()
.map(|(k, v)| {
let value_show = v.show();
format!("{k} {value_show}")
})
.collect::<Vec<_>>()
.join(", ");
format!("#{{{members}}}")
}
Box(x) => format!("box {{ {} }}", x.as_ref().borrow().show()),
Fn(lfn) => format!("fn {}", lfn.name()),
Partial(partial) => format!("fn {}/partial", partial.name),
BaseFn(_) => format!("{self}"),
Nothing => "_".to_string(),
};
if out.len() > 1000 {
out.truncate(997);
format!("{out}...")
} else {
out
}
}
pub fn stringify(&self) -> String {
use Value::*;
match &self {
Process => "process".to_string(),
Nil => "nil".to_string(),
True => "true".to_string(),
False => "false".to_string(),
Number(n) => format!("{n}"),
Interned(str) => str.to_string(),
Keyword(str) => format!(":{str}"),
Tuple(t) => {
let members = t
.iter()
.map(|e| e.stringify())
.collect::<Vec<_>>()
.join(", ");
format!("({members})")
}
List(l) => {
let members = l
.iter()
.map(|e| e.stringify())
.collect::<Vec<_>>()
.join(", ");
format!("[{members}]")
}
Dict(d) => {
let members = d
.iter()
.map(|(k, v)| {
let value_show = v.stringify();
format!("{k} {value_show}")
})
.collect::<Vec<_>>()
.join(", ");
format!("#{{{members}}}")
}
String(s) => s.as_ref().clone(),
Box(x) => x.as_ref().borrow().stringify(),
Fn(lfn) => format!("fn {}", lfn.name()),
Partial(partial) => format!("fn {}/partial", partial.name),
BaseFn(_) => format!("{self}"),
Nothing => unreachable!(),
}
}
pub fn type_of(&self) -> &'static str {
use Value::*;
match self {
Nothing => unreachable!(),
Nil => "nil",
True => "bool",
False => "bool",
Keyword(..) => "keyword",
Interned(..) => "string",
String(..) => "string",
Number(..) => "number",
Tuple(..) => "tuple",
List(..) => "list",
Dict(..) => "dict",
Box(..) => "box",
Fn(..) => "fn",
BaseFn(..) => "fn",
Partial(..) => "fn",
Process => "process",
}
}
pub fn as_fn(&self) -> &LFn {
match self {
Value::Fn(ref inner) => inner,
_ => unreachable!("expected value to be fn"),
}
}
pub fn as_list(&self) -> &Vector<Value> {
match self {
Value::List(ref inner) => inner,
_ => unreachable!("expected value to be list"),
}
}
pub fn as_box(&self) -> Rc<RefCell<Value>> {
match self {
Value::Box(inner) => inner.clone(),
_ => unreachable!("expected value to be a box"),
}
}
pub fn as_string(&self) -> Rc<String> {
match self {
Value::String(str) => str.clone(),
Value::Interned(str) => Rc::new(str.to_string()),
_ => unreachable!("expected value to be a string"),
}
}
pub fn as_tuple(&self) -> Rc<Vec<Value>> {
match self {
Value::Tuple(members) => members.clone(),
_ => unreachable!("expected value to be a tuple"),
}
}
pub fn from_string(str: String) -> Value {
Value::String(Rc::new(str))
}
pub fn list(list: Vector<Value>) -> Value {
Value::List(Box::new(list))
}
pub fn new_list() -> Value {
Value::list(Vector::new())
}
pub fn r#box(value: Value) -> Value {
Value::Box(Rc::new(RefCell::new(value)))
}
pub fn tuple(vec: Vec<Value>) -> Value {
Value::Tuple(Rc::new(vec))
}
pub fn from_f64(f: f64) -> Value {
Value::Number(NotNan::new(f).unwrap())
}
pub fn from_usize(n: usize) -> Value {
Value::Number(NotNan::new(n as f64).unwrap())
}
pub fn as_f64(&self) -> f64 {
match self {
Value::Number(n) => f64::from(*n),
_ => unreachable!("expected value to be a number"),
}
}
// pub fn get_shared_box(&self, name: &'static str) -> Value {
// match self {
// Value::Dict(dict) => dict
// .get(name)
// .expect("expected dict to have requested value")
// .clone(),
// _ => unreachable!("expected dict"),
// }
// }
}

1414
src/vm.rs

File diff suppressed because it is too large Load Diff

View File

@ -1,540 +0,0 @@
use chumsky::primitive::NoneOf;
use crate::chunk::Chunk;
use crate::value::{Value, Key};
use crate::vm::Creature;
use crate::panic::Panic;
use crate::errors::panic;
use crate::js::*;
use crate::io::{MsgOut, MsgIn, do_io};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::mem::swap;
use std::rc::Rc;
const ANIMALS: [&str; 32] = [
"tortoise",
"hare",
"squirrel",
"hawk",
"woodpecker",
"cardinal",
"coyote",
"raccoon",
"rat",
"axolotl",
"cormorant",
"duck",
"orca",
"humbpack",
"tern",
"quokka",
"koala",
"kangaroo",
"zebra",
"hyena",
"giraffe",
"hippopotamus",
"capybara",
"python",
"gopher",
"crab",
"trout",
"osprey",
"lemur",
"wobbegong",
"walrus",
"opossum",
];
#[derive(Debug, Clone, PartialEq)]
enum Status {
Empty,
Borrowed,
Nested(Creature),
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Status::Empty => write!(f, "empty"),
Status::Borrowed => write!(f, "borrowed"),
Status::Nested(creature) => write!(f, "nested {creature}"),
}
}
}
impl Status {
pub fn receive(&mut self, msg: Value) {
match self {
Status::Nested(creature) => creature.receive(msg),
Status::Borrowed => println!("sending a message to a borrowed process"),
Status::Empty => println!("sending a message to a dead process"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Zoo {
procs: Vec<Status>,
empty: Vec<usize>,
ids: HashMap<&'static str, usize>,
dead: HashSet<&'static str>,
kill_list: Vec<&'static str>,
sleeping: HashMap<&'static str, f64>,
active_idx: usize,
active_id: &'static str,
msgs: Vec<String>,
}
impl Zoo {
pub fn new() -> Zoo {
Zoo {
procs: vec![],
empty: vec![],
ids: HashMap::new(),
kill_list: vec![],
dead: HashSet::new(),
sleeping: HashMap::new(),
active_idx: 0,
active_id: "",
msgs: vec![],
}
}
fn random_id(&self) -> String {
let rand_idx = (random() * 32.0) as usize;
let idx = self.procs.len();
format!("{}_{idx}", ANIMALS[rand_idx])
}
fn new_id(&self) -> &'static str {
let mut new = self.random_id();
while self.dead.iter().any(|old| *old == new) {
new = self.random_id();
}
new.leak()
}
pub fn put(&mut self, mut proc: Creature) -> &'static str {
if self.empty.is_empty() {
let id = self.new_id();
let idx = self.procs.len();
proc.pid = id;
self.procs.push(Status::Nested(proc));
self.ids.insert(id, idx);
id
} else {
let idx = self.empty.pop().unwrap();
let rand = (random() * 32.0) as usize;
let id = format!("{}_{idx}", ANIMALS[rand]).leak();
proc.pid = id;
self.ids.insert(id, idx);
self.procs[idx] = Status::Nested(proc);
id
}
}
pub fn kill_linked(&mut self, id: &'static str) {
self.msgs.push(format!("process :{id} terminated by linked process"));
self.kill_list.push(id);
}
pub fn kill(&mut self, id: &'static str) {
self.kill_list.push(id);
}
pub fn sleep(&mut self, id: &'static str, ms: f64) {
self.sleeping
.insert(id, now() + ms);
}
pub fn is_alive(&self, id: &'static str) -> bool {
if self.kill_list.contains(&id) {
return false;
}
let idx = self.ids.get(id);
match idx {
Some(idx) => match self.procs.get(*idx) {
Some(proc) => match proc {
Status::Empty => false,
Status::Borrowed => true,
Status::Nested(_) => true,
},
None => false,
},
None => false,
}
}
pub fn clean_up(&mut self) {
while let Some(id) = self.kill_list.pop() {
if let Some(idx) = self.ids.get(id) {
console_log!("process :{id} terminated");
self.procs[*idx] = Status::Empty;
self.empty.push(*idx);
self.ids.remove(id);
self.dead.insert(id);
}
}
self.sleeping
.retain(|_, wakeup_time| now() < *wakeup_time);
// console_log!(
// "currently sleeping processes: {}",
// self.sleeping
// .keys()
// .map(|id| id.to_string())
// .collect::<Vec<_>>()
// .join(" | ")
// );
}
pub fn catch(&mut self, id: &'static str) -> Creature {
if let Some(idx) = self.ids.get(id) {
let mut proc = Status::Borrowed;
swap(&mut proc, &mut self.procs[*idx]);
let Status::Nested(proc) = proc else {
unreachable!("tried to borrow an empty or already-borrowed process {id}");
};
proc
} else {
unreachable!("tried to borrow a non-existent process {id}");
}
}
pub fn release(&mut self, proc: Creature) {
let id = proc.pid;
if let Some(idx) = self.ids.get(id) {
let mut proc = Status::Nested(proc);
swap(&mut proc, &mut self.procs[*idx]);
}
}
pub fn is_available(&self) -> bool {
match &self.procs[self.active_idx] {
Status::Empty => false,
Status::Borrowed => false,
Status::Nested(proc) => !self.sleeping.contains_key(proc.pid),
}
}
pub fn next(&mut self) -> &'static str {
self.clean_up();
let starting_idx = self.active_idx;
self.active_idx = (self.active_idx + 1) % self.procs.len();
while !self.is_available() {
// we've gone round the process queue already
// that means no process is active
// but we may have processes that are alive and asleep
// if nothing is active, yield back to the world's event loop
if self.active_idx == starting_idx {
return ""
}
self.active_idx = (self.active_idx + 1) % self.procs.len();
}
match &self.procs[self.active_idx] {
Status::Empty | Status::Borrowed => unreachable!(),
Status::Nested(proc) => proc.pid,
}
}
pub fn send_msg(&mut self, id: &'static str, msg: Value) {
let Some(idx) = self.ids.get(id) else {
return;
};
self.procs[*idx].receive(msg);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Buffers {
console: Value,
commands: Value,
fetch_out: Value,
fetch_in: Value,
input: Value,
keys_down: Value,
}
impl Buffers {
pub fn new (prelude: imbl::HashMap<Key, Value>) -> Buffers {
Buffers {
console: prelude.get(&Key::Keyword("console")).unwrap().clone(),
commands: prelude.get(&Key::Keyword("turtle_commands")).unwrap().clone(),
fetch_out: prelude.get(&Key::Keyword("fetch_outbox")).unwrap().clone(),
fetch_in: prelude.get(&Key::Keyword("fetch_inbox")).unwrap().clone(),
input: prelude.get(&Key::Keyword("input")).unwrap().clone(),
keys_down: prelude.get(&Key::Keyword("keys_down")).unwrap().clone(),
}
}
pub fn console (&self) -> Rc<RefCell<Value>> {
self.console.as_box()
}
pub fn input (&self) -> Rc<RefCell<Value>> {
self.input.as_box()
}
pub fn commands (&self) -> Rc<RefCell<Value>> {
self.commands.as_box()
}
pub fn fetch_out (&self) -> Rc<RefCell<Value>> {
self.fetch_out.as_box()
}
pub fn fetch_in (&self) -> Rc<RefCell<Value>> {
self.fetch_in.as_box()
}
pub fn keys_down (&self) -> Rc<RefCell<Value>> {
self.keys_down.as_box()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct World {
zoo: Rc<RefCell<Zoo>>,
active: Option<Creature>,
main: &'static str,
pub result: Option<Result<Value, Panic>>,
buffers: Buffers,
last_io: f64,
kill_signal: bool,
}
impl World {
pub fn new(chunk: Chunk, prelude: imbl::HashMap<Key, Value>, debug: bool) -> World {
let zoo = Rc::new(RefCell::new(Zoo::new()));
let main = Creature::new(chunk, zoo.clone(), debug);
let id = zoo.borrow_mut().put(main);
let buffers = Buffers::new(prelude);
World {
zoo,
active: None,
main: id,
result: None,
buffers,
last_io: 0.0,
kill_signal: false,
}
}
fn next(&mut self) {
let mut active = None;
swap(&mut active, &mut self.active);
let mut zoo = self.zoo.borrow_mut();
if let Some(active) = active {
zoo.release(active);
}
let new_active_id = zoo.next();
if new_active_id.is_empty() {
self.active = None;
return;
}
let mut new_active_proc = zoo.catch(new_active_id);
new_active_proc.reset_reductions();
let mut new_active_opt = Some(new_active_proc);
swap(&mut new_active_opt, &mut self.active);
}
fn activate_main(&mut self) {
let main = self.zoo.borrow_mut().catch(self.main);
self.active = Some(main);
}
fn active_id(&mut self) -> Option<&'static str> {
match &self.active {
Some(creature) => Some(creature.pid),
None => None,
}
}
fn kill_active(&mut self) {
if let Some(pid) = self.active_id() {
self.zoo.borrow_mut().kill(pid);
}
}
fn active_result(&mut self) -> &Option<Result<Value, Panic>> {
if self.active.is_none() { return &None; }
&self.active.as_ref().unwrap().result
}
fn flush_buffers(&mut self) -> Vec<MsgOut> {
let mut outbox = vec![];
if let Some(console) = self.flush_console() {
outbox.push(console);
}
if let Some(commands) = self.flush_commands() {
outbox.push(commands);
}
if let Some(fetch) = self.make_fetch_happen() {
outbox.push(fetch);
}
if let Some(msgs) = self.flush_zoo() {
outbox.push(msgs);
}
outbox
}
fn flush_zoo(&mut self) -> Option<MsgOut> {
let mut zoo_msgs = vec![];
let mut zoo = self.zoo.borrow_mut();
swap(&mut zoo_msgs, &mut zoo.msgs);
if zoo_msgs.is_empty() {
None
} else {
let inner = zoo_msgs.into_iter().map(Value::from_string).collect::<imbl::Vector<_>>();
Some(MsgOut::Console(Value::list(inner)))
}
}
fn make_fetch_happen(&self) -> Option<MsgOut> {
let out = self.buffers.fetch_out();
let working = RefCell::new(Value::Interned(""));
out.swap(&working);
let working = working.borrow();
if working.as_string().is_empty() {
None
} else {
Some(MsgOut::Fetch(working.clone()))
}
}
fn flush_console(&self) -> Option<MsgOut> {
let console = self.buffers.console();
let working_copy = RefCell::new(Value::new_list());
console.swap(&working_copy);
let working_value = working_copy.borrow();
if working_value.as_list().is_empty() {
None
} else {
Some(MsgOut::Console(working_value.clone()))
}
}
fn flush_commands(&self) -> Option<MsgOut> {
let commands = self.buffers.commands();
let working_copy = RefCell::new(Value::new_list());
commands.swap(&working_copy);
let commands = working_copy.borrow();
if commands.as_list().is_empty() {
None
} else {
Some(MsgOut::Commands(commands.clone()))
}
}
fn complete_main(&mut self) -> Vec<MsgOut> {
let mut outbox = self.flush_buffers();
// TODO: if we have a panic, actually add the panic message to the console
let result = self.active_result().clone().unwrap();
self.result = Some(result.clone());
match result {
Ok(value) => outbox.push(MsgOut::Complete(Value::from_string(value.show()))),
Err(p) => {
outbox.push(MsgOut::Console(Value::list(imbl::vector!(Value::from_string("Ludus panicked!".to_string())))));
outbox.push(MsgOut::Error(panic(p)))
}
}
outbox
}
fn interpret_active(&mut self) {
self.active.as_mut().unwrap().interpret();
}
async fn maybe_do_io(&mut self) {
if self.last_io + 10.0 < now() {
let outbox = self.flush_buffers();
let inbox = do_io(outbox).await;
self.fill_buffers(inbox);
self.last_io = now();
}
}
fn fill_input(&mut self, str: String) {
let value = Value::from_string(str);
let working = RefCell::new(value);
let input = self.buffers.input();
input.swap(&working);
}
fn fetch_reply(&mut self, reply: Value) {
let inbox_rc = self.buffers.fetch_in();
inbox_rc.replace(reply);
}
fn register_keys(&mut self, keys: Value) {
// console_log!("New keys: {keys}");
let keys_down_rc = self.buffers.keys_down();
keys_down_rc.replace(keys);
}
fn fill_buffers(&mut self, inbox: Vec<MsgIn>) {
for msg in inbox {
match msg {
MsgIn::Input(str) => self.fill_input(str),
MsgIn::Kill => self.kill_signal = true,
MsgIn::Fetch(..) => self.fetch_reply(msg.into_value()),
MsgIn::Keys(..) => self.register_keys(msg.into_value()),
}
}
}
async fn ready_io(&mut self) {
let inbox = do_io(vec![MsgOut::Ready]).await;
self.fill_buffers(inbox);
self.last_io = now();
}
fn send_ludus_msg(&mut self, msg: String) {
let console = self.buffers.console();
let mut console = console.as_ref().borrow_mut();
let Value::List(ref mut console) = *console else {unreachable!("expect console to be a list")};
console.push_back(Value::from_string(msg));
}
fn report_process_end(&mut self) {
let result = self.active_result().clone().unwrap();
if let Err(panic) = result {
let msg = format!("Process :{} panicked: {}", self.active_id().unwrap(), crate::errors::panic(panic));
self.send_ludus_msg(msg)
}
}
pub async fn run(&mut self) {
self.activate_main();
self.ready_io().await;
loop {
self.maybe_do_io().await;
if self.kill_signal {
let mut outbox = self.flush_buffers();
outbox.push(MsgOut::Error("Ludus stopped by user".to_string()));
do_io(outbox).await;
return;
}
if self.active.is_some() {
self.interpret_active();
}
if self.active_result().is_some() {
if self.active_id().unwrap() == self.main {
let outbox = self.complete_main();
do_io(outbox).await;
return;
} else {
self.report_process_end()
}
self.kill_active();
}
self.next();
}
}
}