From 101eebbfed2feb907bc8c5f33d79465f90196960 Mon Sep 17 00:00:00 2001 From: "Simon A. Nielsen Knights" Date: Mon, 15 May 2023 00:34:37 +0200 Subject: [PATCH] switch to zangle 0.3.0 --- CONTRIBUTING.md | 47 +- CONTRIBUTORS.md | 3 - LICENSE | 682 ++++++- README.md | 3708 +------------------------------------- assets/css/custom.css | 105 -- assets/css/normalize.css | 349 ---- assets/css/skeleton.css | 418 ----- build.zig | 56 +- build.zig.zon | 11 + lib/Instruction.zig | 55 - lib/Interpreter.zig | 392 ---- lib/Linker.zig | 306 ---- lib/Parser.zig | 991 ---------- lib/TangleStep.zig | 163 -- lib/Tokenizer.zig | 181 -- lib/context.zig | 27 - lib/lib.zig | 15 - lib/wasm.zig | 73 - out/.keep | 0 src/FindContext.zig | 82 - src/GraphContext.zig | 201 --- src/main.zig | 577 ------ zangle.sh | 33 - 23 files changed, 760 insertions(+), 7715 deletions(-) delete mode 100644 CONTRIBUTORS.md delete mode 100644 assets/css/custom.css delete mode 100644 assets/css/normalize.css delete mode 100644 assets/css/skeleton.css create mode 100644 build.zig.zon delete mode 100644 lib/Instruction.zig delete mode 100644 lib/Interpreter.zig delete mode 100644 lib/Linker.zig delete mode 100644 lib/Parser.zig delete mode 100644 lib/TangleStep.zig delete mode 100644 lib/Tokenizer.zig delete mode 100644 lib/context.zig delete mode 100644 lib/lib.zig delete mode 100644 lib/wasm.zig delete mode 100644 out/.keep delete mode 100644 src/FindContext.zig delete mode 100644 src/GraphContext.zig delete mode 100644 src/main.zig delete mode 100644 zangle.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8a2cb4..af3e2b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,48 +1,3 @@ # Contributing to zangle 101 -1. Fork the master branch -2. Open a draft pull request and link related issues -3. Commit changes while following the commit style guide -4. Mark as ready and await review -5. Upon requested changes: discuss, apply, and so on -6. ???? -7. Merged - -## Commits - -Commits fall into the following categories: - -| prefix | meaning | -| -- | -- | -| `fix:` | This commit fixes a bug in zangle | -| `doc:` | This commit improves documentation and involves no functional changes | -| `ci/cd:` | This commit only concerns the CI/CD pipeline | -| `chore:` | This commit contains structural and no functional changes (file rename, ect) | -| `feature:` | This commit adds a new feature or command to zangle | -| `workflow:` | This commit updates or adds workflow scripts for working with zangle | - -Where functional changes should be kept separate from documentation even if -that requires a greater number of commits to document the change. - -Each commit should start with it's category prefix (e.g `doc: add description -of opcodes`) and be relatively short message after. More information can be -written after a blank line but usually such is better if it can go in the -document itself. - -## Branch / Pull Request names - -Branches should have the prefix of their intended change with a prefix -similar to those for commits but with a `-` dash instead of a `:` colon with -the rest of the name using `'` dashes in-place of what would be spaces (e.g -`fix-rendering-indent-in-nested-blocks`). - -## License - -All code submitted must be under the MIT license of the same version as in -LICENSE and will be taken as such unless specified otherwise. Code under -any other license will be rejected. - -## Remember - -Append your name and link your github profile in CONTRIBUTORS.md if this -is your first contribution to the project. +This project has moved to https://git.sr.ht/~tauoverpi/levy, all contributions should be directed there. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index b56d668..0000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributors - -- [Simon A. Nielsen Knights](https://github.com/tauoverpi) diff --git a/LICENSE b/LICENSE index 2920813..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -MIT License - -Copyright (c) 2021 Simon A. Nielsen Knights - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/README.md b/README.md index 155a773..1fa39f2 100644 --- a/README.md +++ b/README.md @@ -6,3672 +6,118 @@ Zangle is a literate programming tool for extracting code fragments from markdown and other types of text documents into separate files ready for compilation. -NOTE: Currently zangle only supports markdown with a special header on -indented code blocks. +NOTE: Currently zangle only supports markdown. +NOTE: This project has moved to https://git.sr.ht/~tauoverpi/levy however this remains the official issue tracker. -### Examples +### Building + +``` +$ zig build -Drelease +``` + +### Invocation + +Let `book/` be a directory of markdown files. Tangle all files within a document - $ zangle tangle README.md + $ zangle tangle book/ List all files in a document - $ zangle ls README.md + $ zangle ls book/ Render the content of a tag to stdout - $ zangle call README.md --tag='interpreter step' + $ zangle call book/ --tag='interpreter step' Render a graph representing document structure - $ zangle graph README.md | dot -Tpng -o grpah.png + $ zangle graph book/ | dot -Tpng -o grpah.png Render a graph representing the structure of a single file output - $ zangle graph README.md --file=lib/Linker.zig | dot -Tpng -o grpah.png + $ zangle graph book/ --file=lib/Linker.zig | dot -Tpng -o grpah.png -Find where given tags reside within output files +Find where given tags reside within output files (TODO) $ zangle find README.md --tag='parser codegen' --tag='command-line parser' -Create a new literate document from existing files +Create a new literate document from existing files (TODO) $ find src lib -name '*.zig' | zangle init build.zig --stdin > Zangle.md -## As a library - - lang: zig esc: none file: lib/lib.zig - ------------------------------------- - - pub const Tokenizer = @import("Tokenizer.zig"); - pub const Parser = @import("Parser.zig"); - pub const Linker = @import("Linker.zig"); - pub const Instruction = @import("Instruction.zig"); - pub const Interpreter = @import("Interpreter.zig"); - pub const context = @import("context.zig"); - pub const TangleStep = @import("TangleStep.zig"); - - test { - _ = Tokenizer; - _ = Parser; - _ = Linker; - _ = Instruction; - _ = Interpreter; - } - -## As a stand-alone application - - lang: zig esc: [[]] file: src/main.zig - -------------------------------------- +### Example - const std = @import("std"); - const lib = @import("lib"); - const mem = std.mem; - const assert = std.debug.assert; - const testing = std.testing; - const meta = std.meta; - const fs = std.fs; - const fmt = std.fmt; - const io = std.io; - const os = std.os; - const math = std.math; - const stdout = io.getStdOut().writer(); - const stdin = io.getStdIn().reader(); +This project fetches the real package from sr.ht using the new zig package manager however most options are the same as +the `init-exe` template with a few minor changes. The general structure follows: - const Allocator = std.mem.Allocator; - const ArrayList = std.ArrayListUnmanaged; - const HashMap = std.AutoArrayHashMapUnmanaged; - const MultiArrayList = std.MultiArrayList; - const Tokenizer = lib.Tokenizer; - const Parser = lib.Parser; - const Linker = lib.Linker; - const Instruction = lib.Instruction; - const Interpreter = lib.Interpreter; - const GraphContext = @import("GraphContext.zig"); - const FindContext = @import("FindContext.zig"); - const BufferedWriter = io.BufferedWriter(4096, fs.File.Writer); - const FileContext = lib.context.StreamContext(BufferedWriter.Writer); +``` zig file: build.zig +const std = @import("std"); - pub const log_level = .info; - - const Options = struct { - allow_absolute_paths: bool = false, - omit_trailing_newline: bool = false, - list_files: bool = false, - list_tags: bool = false, - calls: []const FileOrTag = &.{}, - graph_text_colour: u24 = 0x000000, - graph_background_colour: u24 = 0xffffff, - graph_border_colour: u24 = 0x92abc9, - graph_inherit_line_colour: bool = false, - graph_line_gradient: u8 = 5, - graph_colours: []const u24 = &.{ - 0xdf4d77, - 0x2288ed, - 0x94bd76, - 0xc678dd, - 0x61aeee, - 0xe3bd79, +pub fn build(b: *std.Build) void { + [[declare release and target options]] + [[import zangle from the dependency list, set target parameters, and install the artifact]] + [[setup a run command such that it can be tested without having write the path to the binary in zig-out]] +} +``` + +For the target options, `.ReleaseSafe` was chosen such that the program would panic as soon as it invokes [safety-checked +undefined behaviour](https://ziglang.org/documentation/0.10.1/#Undefined-Behavior). + +``` zig tag: declare release and target options +const target = b.standardTargetOptions(.{}); +const optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .ReleaseSafe, +}); +``` + +The package is hosted on sr.ht as a sub project of a game project which uses zangle to document every design choice made +for all code included in the final game. + +``` zig file: build.zig.zon +.{ + .name = "zangle", + .version = "0.3.0", + + .dependencies = .{ + .zangle = .{ + .url = "https://git.sr.ht/~tauoverpi/levy/archive/935578e5c70bc44e056e673a8b9ef3f0388cc961.tar.gz", + .hash = "1220ad55840aeaa62b01057f8838fa187bb1463ffbd2476b5ad2a4b2332b9e6f778e", }, - command: Command, - files: []const []const u8 = &.{}, - - pub const FileOrTag = union(enum) { - file: []const u8, - tag: []const u8, - }; - }; - - [[command-line parser]] - - pub fn main() void { - run() catch |err| { - log.err("{s}", .{@errorName(err)}); - os.exit(1); - }; - } - - pub fn run() !void { - var vm: Interpreter = .{}; - var instance = std.heap.GeneralPurposeAllocator(.{}){}; - const gpa = instance.allocator(); - - var options = (try parseCli(gpa, &vm.linker.objects)) orelse return; - - const n_objects = vm.linker.objects.items.len; - const plural: []const u8 = if (n_objects == 1) "object" else "objects"; - - log.info("linking {d} {s}...", .{ n_objects, plural }); - - try vm.linker.link(gpa); - - log.debug("processing command {s}", .{@tagName(options.command)}); - - switch (options.command) { - .help => unreachable, // handled in parseCli - - .ls => { - var buffered = io.bufferedWriter(stdout); - const writer = buffered.writer(); - - if (!options.list_files) options.list_files = !options.list_tags; - - if (options.list_tags) for (vm.linker.procedures.keys()) |path| { - try writer.writeAll(path); - try writer.writeByte('\n'); - }; - - if (options.list_files) for (vm.linker.files.keys()) |path| { - try writer.writeAll(path); - try writer.writeByte('\n'); - }; - - try buffered.flush(); - }, - - .call => { - var buffered: BufferedWriter = .{ .unbuffered_writer = stdout }; - var context = FileContext.init(buffered.writer()); - - for (options.calls) |call| switch (call) { - .file => |file| { - log.debug("calling file {s}", .{file}); - try vm.callFile(gpa, file, *FileContext, &context); - if (!options.omit_trailing_newline) try context.stream.writeByte('\n'); - }, - .tag => |tag| { - log.debug("calling tag {s}", .{tag}); - try vm.call(gpa, tag, *FileContext, &context); - }, - }; - - try buffered.flush(); - }, - - .find => for (options.calls) |call| switch (call) { - .file => unreachable, // not an option for find - .tag => |tag| { - log.debug("finding paths to tag {s}", .{tag}); - for (vm.linker.files.keys()) |file| { - var context = FindContext.init(gpa, file, tag, stdout); - try vm.callFile(gpa, file, *FindContext, &context); - try context.stream.flush(); - } - }, - }, - - .graph => { - var context = GraphContext.init(gpa, stdout); - - try context.begin(.{ - .border = options.graph_border_colour, - .background = options.graph_background_colour, - .text = options.graph_text_colour, - .colours = options.graph_colours, - .inherit = options.graph_inherit_line_colour, - .gradient = options.graph_line_gradient, - }); - - if (options.calls.len != 0) { - for (options.calls) |call| switch (call) { - .tag => unreachable, // not an option for graph - .file => |file| { - log.debug("rendering graph for file {s}", .{file}); - try vm.callFile(gpa, file, *GraphContext, &context); - }, - }; - } else { - for (vm.linker.files.keys()) |path| { - try vm.callFile(gpa, path, *GraphContext, &context); - } - - for (vm.linker.procedures.keys()) |proc| { - if (!context.target.contains(proc.ptr)) { - try vm.call(gpa, proc, *GraphContext, &context); - } - } - } - - try context.end(); - }, - - .tangle => for (vm.linker.files.keys()) |path| { - const file = try createFile(path, options); - defer file.close(); - - var buffered: BufferedWriter = .{ .unbuffered_writer = file.writer() }; - var context = FileContext.init(buffered.writer()); - - try vm.callFile(gpa, path, *FileContext, &context); - if (!options.omit_trailing_newline) try context.stream.writeByte('\n'); - try buffered.flush(); - }, - - .init => for (options.files) |path, index| { - try import(path, stdout); - if (index + 1 != options.files.len) try stdout.writeByte('\n'); - }, - } - } - - [[create file with path wrapper]] - [[render text indented by four spaces]] - [[method for import a file and emit a code block targeting it]] - -# Command-line interface - - lang: zig esc: none tag: #command-line parser - --------------------------------------------- - - const Command = enum { - help, - tangle, - ls, - call, - graph, - find, - init, - - pub const map = std.ComptimeStringMap(Command, .{ - .{ "help", .help }, - .{ "tangle", .tangle }, - .{ "ls", .ls }, - .{ "call", .call }, - .{ "graph", .graph }, - .{ "find", .find }, - .{ "init", .init }, - }); - }; - - const Flag = enum { - allow_absolute_paths, - omit_trailing_newline, - file, - tag, - list_tags, - list_files, - graph_border_colour, - graph_inherit_line_colour, - graph_colours, - graph_background_colour, - graph_line_gradient, - graph_text_colour, - @"--", - stdin, - - pub const map = std.ComptimeStringMap(Flag, .{ - .{ "--allow-absolute-paths", .allow_absolute_paths }, - .{ "--omit-trailing-newline", .omit_trailing_newline }, - .{ "--file=", .file }, - .{ "--tag=", .tag }, - .{ "--list-tags", .list_tags }, - .{ "--list-files", .list_files }, - .{ "--graph-border-colour=", .graph_border_colour }, - .{ "--graph-colours=", .graph_colours }, - .{ "--graph-background-colour=", .graph_background_colour }, - .{ "--graph-text-colour=", .graph_text_colour }, - .{ "--graph-inherit-line-colour", .graph_inherit_line_colour }, - .{ "--graph-line-gradient=", .graph_line_gradient }, - .{ "--", .@"--" }, - .{ "--stdin", .stdin }, - }); - }; - - const tangle_help = - \\Usage: zangle tangle [options] [files] - \\ - \\ --allow-absolute-paths Allow writing file blocks with absolute paths - \\ --omit-trailing-newline Do not print a trailing newline at the end of a file block - ; - - const ls_help = - \\Usage: zangle ls [files] - \\ - \\ --list-files (default) List all file output paths in the document - \\ --list-tags List all tags in the document - ; - - const call_help = - \\Usage: zangle call [options] [files] - \\ - \\ --file=[filepath] Render file block to stdout - \\ --tag=[tagname] Render tag block to stdout - ; - - const find_help = - \\Usage: zangle find [options] [files] - \\ - \\ --tag=[tagname] Find the location of the given tag in the literate document and output files - ; - - const graph_help = - \\Usage: zangle graph [files] - \\ - \\ --file=[filepath] Render the graph for the given file - \\ --graph-border=[#rrggbb] Set item border colour - \\ --graph-colours=[#rrggbb,...] Set spline colours - \\ --graph-background-colour=[#rrggbb] Set the background colour of the graph - \\ --graph-text-colour=[#rrggbb] Set node label text colour - \\ --graph-inherit-line-colour Borders inherit their colour from the choden line colour - \\ --graph-line-gradient=[number] Set the gradient level - ; - - const init_help = - \\Usage: zangle init [files] - \\ --stdin Read file names from stdin - ; - - const log = std.log; - - fn helpGeneric() void { - log.info( - \\{s} - \\ - \\{s} - \\ - \\{s} - \\ - \\{s} - \\ - \\{s} - , .{ - tangle_help, - ls_help, - call_help, - graph_help, - init_help, - }); - } - - fn help(com: ?Command, name: ?[]const u8) void { - const command = com orelse { - helpGeneric(); - log.err("I don't know how to handle the given command '{s}'", .{name.?}); - return; - }; - - switch (command) { - .help => helpGeneric(), - .tangle => log.info(tangle_help, .{}), - .ls => log.info(ls_help, .{}), - .call => log.info(call_help, .{}), - .graph => log.info(graph_help, .{}), - .find => log.info(find_help, .{}), - .init => log.info(init_help, .{}), - } - } - - fn parseCli(gpa: Allocator, objects: *Linker.Object.List) !?Options { - var options: Options = .{ .command = undefined }; - const args = os.argv; - - if (args.len < 2) { - help(.help, null); - return error.@"Missing command name"; - } - - const command_name = mem.sliceTo(args[1], 0); - const command = Command.map.get(command_name); - - if (args.len < 3 or command == null or command.? == .help) { - help(command, command_name); - if (command) |com| { - switch (com) { - .help => return null, - else => return error.@"Not enough arguments", - } - } else { - return error.@"Invalid command"; - } - } - - var interpret_flags_as_files: bool = false; - var calls = std.ArrayList(Options.FileOrTag).init(gpa); - var files = std.ArrayList([]const u8).init(gpa); - var graph_colours = std.ArrayList(u24).init(gpa); - var graph_colours_set = false; - var files_on_stdin = false; - - options.command = command.?; - - for (args[2..]) |arg0| { - const arg = mem.sliceTo(arg0, 0); - if (arg.len == 0) return error.@"Zero length argument"; - - if (arg[0] == '-' and !interpret_flags_as_files) { - errdefer log.err("I don't know how to parse the given option '{s}'", .{arg}); - - log.debug("processing {s} flag '{s}'", .{ @tagName(options.command), arg }); - - const split = (mem.indexOfScalar(u8, arg, '=') orelse (arg.len - 1)) + 1; - const flag = Flag.map.get(arg[0..split]) orelse { - return error.@"Unknown option"; - }; - - switch (options.command) { - .help => unreachable, - - .ls => switch (flag) { - .list_files => options.list_files = true, - .list_tags => options.list_tags = true, - else => return error.@"Unknown command-line flag", - }, - - .call => switch (flag) { - .file => try calls.append(.{ .file = arg[split..] }), - .tag => try calls.append(.{ .tag = arg[split..] }), - else => return error.@"Unknown command-line flag", - }, - - .find => switch (flag) { - .tag => try calls.append(.{ .tag = arg[split..] }), - else => return error.@"Unknown command-line flag", - }, - - .graph => switch (flag) { - .file => try calls.append(.{ .file = arg[split..] }), - .graph_border_colour => options.graph_border_colour = try parseColour(arg[split..]), - .graph_background_colour => options.graph_background_colour = try parseColour(arg[split..]), - .graph_text_colour => options.graph_text_colour = try parseColour(arg[split..]), - .graph_inherit_line_colour => options.graph_inherit_line_colour = true, - .graph_line_gradient => options.graph_line_gradient = fmt.parseInt(u8, arg[split..], 10) catch { - return error.@"Invalid value specified, expected a number between 0-255 (inclusive)"; - }, - - .graph_colours => { - var it = mem.tokenize(u8, arg[split..], ","); - - while (it.next()) |item| { - try graph_colours.append(try parseColour(item)); - } - - graph_colours_set = true; - }, - - else => return error.@"Unknown command-line flag", - }, - - .tangle => switch (flag) { - .allow_absolute_paths => options.allow_absolute_paths = true, - .omit_trailing_newline => options.omit_trailing_newline = true, - .@"--" => interpret_flags_as_files = true, - else => return error.@"Unknown command-line flag", - }, - - .init => switch (flag) { - .stdin => files_on_stdin = true, - else => return error.@"Unknown command-line flag", - }, - } - } else if (options.command != .init) { - std.log.info("compiling {s}", .{arg}); - const text = try fs.cwd().readFileAlloc(gpa, arg, 0x7fff_ffff); - - var p: Parser = .{ .it = .{ .bytes = text } }; - - while (p.step(gpa)) |working| { - if (!working) break; - } else |err| { - const location = p.it.locationFrom(.{}); - log.err("line {d} col {d}: {s}", .{ - location.line, - location.column, - @errorName(err), - }); - - os.exit(1); - } - - const object = p.object(arg); - - objects.append(gpa, object) catch return error.@"Exhausted memory"; - } else { - files.append(arg) catch return error.@"Exhausted memory"; - } - } - - if (files_on_stdin) { - const err = error.@"Exhausted memory"; - while (stdin.readUntilDelimiterOrEofAlloc(gpa, '\n', fs.MAX_PATH_BYTES) catch return err) |path| { - files.append(path) catch return error.@"Exhausted memory"; - } - } - - if (options.command == .init and files.items.len == 0) { - return error.@"No files to import specified"; - } - - options.calls = calls.toOwnedSlice(); - options.files = files.toOwnedSlice(); - if (graph_colours_set) { - options.graph_colours = graph_colours.toOwnedSlice(); - } - return options; - } - - fn parseColour(text: []const u8) !u24 { - if (text.len == 7) { - if (text[0] != '#') return error.@"Invalid colour spexification, expected '#'"; - return fmt.parseInt(u24, text[1..], 16) catch error.@"Colour specification is not a valid 24-bit hex number"; - } else { - return error.@"Invalid hex colour specification length; expecting a 6 hex digit colour prefixed with a '#'"; - } - } - -## Loading files - - lang: zig esc: none tag: #create file with path wrapper - ------------------------------------------------------- - - fn createFile(path: []const u8, options: Options) !fs.File { - var tmp: [fs.MAX_PATH_BYTES]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&tmp); - var filename = path; - var absolute = false; - - if (filename.len > 2 and mem.eql(u8, filename[0..2], "~/")) { - filename = try fs.path.join(fba.allocator(), &.{ - os.getenv("HOME") orelse return error.@"unable to find ~/", - filename[2..], - }); - } - - if (path[0] == '/' or path[0] == '~') { - if (!options.allow_absolute_paths) { - return error.@"Absolute paths disabled; use --allow-absolute-paths to enable them."; - } else { - absolute = true; - } - } - - if (fs.path.dirname(filename)) |dir| fs.cwd().makePath(dir) catch {}; - - if (absolute) { - log.warn("writing file with absolute path: {s}", .{filename}); - } else { - log.info("writing file: {s}", .{filename}); - } - return try fs.cwd().createFile(filename, .{ .truncate = true }); - } - -## Setting up existing projects - - lang: zig esc: none tag: #method for import a file and emit a code block targeting it - ------------------------------------------------------------------------------------- - - fn import(path: []const u8, writer: anytype) !void { - var file = try fs.cwd().openFile(path, .{}); - defer file.close(); - - const last = mem.lastIndexOfScalar(u8, path, '/') orelse 0; - const lang = if (mem.lastIndexOfScalar(u8, path[last..], '.')) |index| - path[last + index + 1 ..] - else - "unknown"; - - var buffered = io.bufferedReader(file.reader()); - var counting = io.countingWriter(writer); - try writer.writeByteNTimes('#', math.clamp(mem.count(u8, path[1..], "/"), 0, 5) + 1); - try writer.writeByte(' '); - try writer.writeAll(path); - try writer.writeAll(" \n\n "); - try counting.writer().print("lang: {s} esc: none file: {s}", .{ lang, path }); - try writer.writeByte('\n'); - try writer.writeAll(" "); - try writer.writeByteNTimes('-', counting.bytes_written); - try writer.writeAll("\n\n"); - try indent(buffered.reader(), writer); - } - - - - lang: zig esc: none tag: #render text indented by four spaces - ------------------------------------------------------------- - - fn indent(reader: anytype, writer: anytype) !void { - var buffer: [1 << 12]u8 = undefined; - var nl = true; - - while (true) { - const len = try reader.read(&buffer); - if (len == 0) return; - const slice = buffer[0..len]; - var last: usize = 0; - while (mem.indexOfScalarPos(u8, slice, last, '\n')) |index| { - if (nl) try writer.writeAll(" "); - try writer.writeAll(slice[last..index]); - try writer.writeByte('\n'); - nl = true; - last = index + 1; - } else if (slice[last..].len != 0) { - if (nl) try writer.writeAll(" "); - try writer.writeAll(slice[last..]); - nl = false; - } - } - } - - test "indent text block" { - const source = - \\pub fn main() !void { - \\ return; - \\} - ; - var buffer: [1024 * 4]u8 = undefined; - var in = io.fixedBufferStream(source); - var out = io.fixedBufferStream(&buffer); - - try indent(in.reader(), out.writer()); - - try testing.expectEqualStrings( - \\ pub fn main() !void { - \\ return; - \\ } - , out.getWritten()); - } - -# Build step - - lang: zig esc: none file: lib/TangleStep.zig - -------------------------------------------- - - const std = @import("std"); - const lib = @import("lib.zig"); - const fs = std.fs; - const mem = std.mem; - const io = std.io; - - const TangleStep = @This(); - const Allocator = std.mem.Allocator; - const Builder = std.build.Builder; - const Step = std.build.Step; - const Parser = lib.Parser; - const Interpreter = lib.Interpreter; - const SourceList = std.TailQueue(Source); - const FileSource = std.build.FileSource; - const GeneratedFile = std.build.GeneratedFile; - const BufferedWriter = io.BufferedWriter(4096, fs.File.Writer); - const FileContext = lib.context.StreamContext(BufferedWriter.Writer); - - pub const FileList = std.ArrayListUnmanaged([]const u8); - - pub const Source = struct { - source: GeneratedFile, - path: []const u8, - }; - - const log = std.log.scoped(.tangle_step); - - vm: Interpreter = .{}, - output_dir: ?[]const u8 = null, - builder: *Builder, - files: FileList = .{}, - sources: SourceList = .{}, - step: Step, - - pub fn create(b: *Builder) *TangleStep { - const self = b.allocator.create(TangleStep) catch @panic("Out of memory"); - self.* = .{ - .builder = b, - .step = Step.init(.custom, "tangle", b.allocator, make), - }; - return self; - } - - pub fn addFile(self: *TangleStep, path: []const u8) void { - self.files.append(self.builder.allocator, self.builder.dupe(path)) catch @panic( - \\Out of memory - ); - } - - pub fn getFileSource(self: *TangleStep, path: []const u8) FileSource { - var it = self.sources.first; - while (it) |node| : (it = node.next) { - if (std.mem.eql(u8, node.data.path, path)) - return FileSource{ .generated = &node.data.source }; - } - - const node = self.builder.allocator.create(SourceList.Node) catch @panic( - \\Out of memory - ); - node.* = .{ - .data = .{ - .source = .{ .step = &self.step }, - .path = self.builder.dupe(path), - }, - }; - - self.sources.append(node); - - return FileSource{ .generated = &node.data.source }; - } - - fn make(step: *Step) anyerror!void { - const self = @fieldParentPtr(TangleStep, "step", step); - - var hash = std.crypto.hash.blake2.Blake2b384.init(.{}); - - for (self.files.items) |path| { - const text = try fs.cwd().readFileAlloc(self.builder.allocator, path, 0x7fff_ffff); - var p: Parser = .{ .it = .{ .bytes = text } }; - while (p.step(self.builder.allocator)) |working| { - if (!working) break; - } else |err| { - const location = p.it.locationFrom(.{}); - log.err("line {d} col {d}: {s}", .{ - location.line, - location.column, - @errorName(err), - }); - - @panic("Failed parsing module"); - } - - hash.update(path); - hash.update(text); - - const object = p.object(path); - try self.vm.linker.objects.append(self.builder.allocator, object); - } - - try self.vm.linker.link(self.builder.allocator); - - var digest: [48]u8 = undefined; - hash.final(&digest); - - var basename: [64]u8 = undefined; - _ = std.fs.base64_encoder.encode(&basename, &digest); - - if (self.output_dir == null) { - self.output_dir = try fs.path.join(self.builder.allocator, &.{ - self.builder.cache_root, - "o", - &basename, - }); - } - - try fs.cwd().makePath(self.output_dir.?); - - var dir = try fs.cwd().openDir(self.output_dir.?, .{}); - defer dir.close(); - - for (self.vm.linker.files.keys()) |path| { - if (path.len > 2 and mem.eql(u8, path[0..2], "~/")) { - return error.@"Absolute paths are not allowed"; - } else if (mem.indexOf(u8, path, "../") != null) { - return error.@"paths containing ../ are not allowed"; - } - - if (fs.path.dirname(path)) |sub| try dir.makePath(sub); - - const file = try dir.createFile(path, .{ .truncate = true }); - defer file.close(); - - var buffered: BufferedWriter = .{ .unbuffered_writer = file.writer() }; - const writer = buffered.writer(); - var context = FileContext.init(writer); - try self.vm.callFile(self.builder.allocator, path, *FileContext, &context); - try context.stream.writeByte('\n'); - try buffered.flush(); - - var it = self.sources.first; - while (it) |node| : (it = node.next) { - if (mem.eql(u8, node.data.path, path)) { - self.sources.remove(node); - node.data.source.path = try fs.path.join( - self.builder.allocator, - &.{ self.output_dir.?, node.data.path }, - ); - break; - } - } - } - - if (self.sources.first) |node| { - log.err("file not found: {s}", .{node.data.path}); - var it = node.next; - - while (it) |next| { - log.err("file not found: {s}", .{next.data.path}); - } - - @panic("Files not found"); - } - } - -# Machine - -Zangle represents documents as bytecode programs consisting mostly of `write` -instructions to render code line-by-line with respect to the tag's indentation -along with block writes for weaving literate source documents. Other -instructions handle the order in which to tangle blocks of code such as -`call` which embeds one block in another and `jmp` which threads adjacent -blocks (by tag name) together into one. - -## Instructions - -Each instruction consists of an 8-bit opcode along with a 64-bit data argument. - - lang: zig esc: [[]] file: lib/Instruction.zig - --------------------------------------------- - - const std = @import("std"); - const assert = std.debug.assert; - - const Instruction = @This(); - - opcode: Opcode, - data: Data, - - pub const List = std.MultiArrayList(Instruction); - pub const Opcode = enum(u8) { - ret, - call, - jmp, - shell, - write, - }; - - pub const Data = extern union { - ret: Ret, - jmp: Jmp, - call: Call, - shell: Shell, - write: Write, - - [[instruction list]] - }; - - comptime { - assert(@sizeOf(Data) == 8); - } - -### Ret - -Pops the location and module in which the matching `call` instruction -originated from. If any filters have been registered in the calling context -then this instruction marks the end of the context and executes the action -bound. The payload includes the index and length of the current procedure -name which is provided as a parameter to rendering contexts. - - lang: zig esc: none tag: #instruction list - ------------------------------------------ - - pub const Ret = extern struct { - start: u32, - len: u16, - pad: u16 = 0, - }; - - - - lang: zig esc: none tag: #parser codegen - ---------------------------------------- - - fn emitRet( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Ret, - ) !void { - log.debug("emitting ret", .{}); - try p.program.append(gpa, .{ - .opcode = .ret, - .data = .{ .ret = params }, - }); - } - -Execution of the `ret` instruction. - -`ret` will invoke the `ret` method of the render context upon returning from -a normal procedure and `terminate` upon reaching the end of the program. Of -the parameters, `module` is that of the caller and `ip` points to the next -instruction to be run which a rendering context can use to calculate the -entry-point of the procedure. - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - fn execRet(vm: *Interpreter, comptime T: type, data: Instruction.Data.Ret, eval: T) Child(T).Error!bool { - const name = vm.linker.objects.items[vm.module - 1] - .text[data.start .. data.start + data.len]; - - if (vm.stack.popOrNull()) |location| { - const mod = vm.module; - const ip = vm.ip; - - vm.ip = location.value.ip; - vm.module = location.value.module; - vm.indent -= location.value.indent; - - if (@hasDecl(Child(T), "ret")) try eval.ret( - vm, - name, - ); - log.debug("[mod {d} ip {x:0>8}] ret(mod {d}, ip {x:0>8}, indent {d}, identifier '{s}')", .{ - mod, - ip, - vm.module, - vm.ip, - vm.indent, - name, - }); - - return true; - } - - if (@hasDecl(Child(T), "terminate")) try eval.terminate(vm, name); - log.debug("[mod {d} ip {x:0>8}] terminate(identifier '{s}')", .{ - vm.module, - vm.ip, - name, - }); - - return false; - } - -### Jmp - -Jumps to the specified address of the given module without pushing to the -return stack. This instruction is primarily used to thread blocks with the -same tag together across files in the order in which they occur within the -literate source. If the target module is 0 then it's interpreted as being -local to the current module. - - - lang: zig esc: none tag: #instruction list - ------------------------------------------ - - pub const Jmp = extern struct { - address: u32, - module: u16, - generation: u16 = 0, - }; - - - - lang: zig esc: none tag: #parser codegen - ---------------------------------------- - - fn writeJmp( - p: *Parser, - location: u32, - params: Instruction.Data.Jmp, - ) !void { - log.debug("writing jmp over {x:0>8} to {x:0>8}", .{ - location, - params.address, - }); - p.program.set(location, .{ - .opcode = .jmp, - .data = .{ .jmp = params }, - }); - } - - - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - fn execJmp(vm: *Interpreter, comptime T: type, data: Instruction.Data.Jmp, eval: T) Child(T).Error!void { - const mod = vm.module; - const ip = vm.ip; - - if (data.module != 0) { - vm.module = data.module; - } - - vm.ip = data.address; - - if (@hasDecl(Child(T), "jmp")) try eval.jmp(vm, data.address); - if (@hasDecl(Child(T), "write")) try eval.write(vm, "\n", 0); - - log.debug("[mod {d} ip {x:0>8}] jmp(mod {d}, address {x:0>8})", .{ - mod, - ip, - vm.module, - vm.ip, - }); - - vm.last_is_newline = true; - } - -### Call - -Saves the module context, instruction pointer, and calling context on the -return stack before jumping to the specified address within the given module. -If the target module is 0 then it's interpreted as being local to the -current module. - - lang: zig esc: none tag: #instruction list - ------------------------------------------ - - pub const Call = extern struct { - address: u32, - module: u16, - indent: u16, - }; - - - - lang: zig esc: none tag: #parser codegen - ---------------------------------------- - - fn emitCall( - p: *Parser, - gpa: Allocator, - tag: []const u8, - params: Instruction.Data.Call, - ) !void { - log.debug("emitting call to {s}", .{tag}); - const result = try p.symbols.getOrPut(gpa, tag); - if (!result.found_existing) { - result.value_ptr.* = .{}; - } - - try result.value_ptr.append(gpa, @intCast(u32, p.program.len)); - - try p.program.append(gpa, .{ - .opcode = .call, - .data = .{ .call = params }, - }); - } - - - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - pub const CallError = error{ - @"Cyclic reference detected", - OutOfMemory, - }; - - fn execCall( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Call, - gpa: Allocator, - eval: T, - ) (CallError || Child(T).Error)!void { - if (vm.stack.contains(vm.ip)) { - return error.@"Cyclic reference detected"; - } - - const mod = vm.module; - const ip = vm.ip; - - try vm.stack.put(gpa, vm.ip, .{ - .ip = vm.ip, - .indent = data.indent, - .module = vm.module, - }); - - vm.indent += data.indent; - vm.ip = data.address; - - if (data.module != 0) { - vm.module = data.module; - } - - if (@hasDecl(Child(T), "call")) try eval.call(vm); - log.debug("[mod {d} ip {x:0>8}] call(mod {d}, ip {x:0>8})", .{ - mod, - ip - 1, - vm.module, - vm.ip, - }); - } - -### Shell - -Appends a calling context to the next `call` instruction with a shell command -for filtering rendered content within the given block. - - lang: zig esc: none tag: #instruction list - ------------------------------------------ - - pub const Shell = extern struct { - command: u32, - module: u16, - len: u8, - pad: u8, - }; - - - - - lang: zig esc: none tag: #parser codegen - ---------------------------------------- - - fn emitShell( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Shell, - ) !void { - log.debug("emitting shell command", .{}); - try p.program.append(gpa, .{ - .opcode = .shell, - .data = .{ .shell = params }, - }); - } - - - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - fn execShell( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Shell, - text: []const u8, - eval: T, - ) void { - if (@hasDecl(Child(T), "shell")) try eval.shell(vm); - _ = vm; - _ = data; - _ = text; - @panic("TODO: implement shell"); - } - -### Write - -Writes lines of text from the current module to the output stream. If a -calling context is present then the output is written to a buffer instead. -A trail of newline characters is emitted after the text as specified in the -`nl` field of the 64-bit data block. - - lang: zig esc: none tag: #instruction list - ------------------------------------------ - - pub const Write = extern struct { - start: u32, - len: u16, - nl: u16, - }; - - - - lang: zig esc: none tag: #parser codegen - ---------------------------------------- - - fn emitWrite( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Write, - ) !void { - log.debug("emitting write {x:0>8} len {d} nl {d}", .{ - params.start, - params.len, - params.nl, - }); - try p.program.append(gpa, .{ - .opcode = .write, - .data = .{ .write = params }, - }); - } - - - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - fn execWrite( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Write, - text: []const u8, - eval: T, - ) Child(T).Error!void { - if (vm.should_indent and vm.last_is_newline) { - if (@hasDecl(Child(T), "indent")) try eval.indent(vm); - log.debug("[mod {d} ip {x:0>8}] indent(len {d})", .{ - vm.module, - vm.ip, - vm.indent, - }); - } else { - vm.should_indent = true; - } - - if (@hasDecl(Child(T), "write")) try eval.write( - vm, - text[data.start .. data.start + data.len], - data.nl, - ); - - log.debug("[mod {d} ip {x:0>8}] write(text {*}, index {x:0>8}, len {d}, nl {d}): {s}", .{ - vm.module, - vm.ip, - text, - data.start, - data.len, - data.nl, - text[data.start .. data.start + data.len], - }); - - vm.last_is_newline = data.nl != 0; - } - -## Interpreter contexts - -Rendering is handled by passing a context in which to run the program. - -### Test context - - lang: zig esc: none tag: #interpreter step - ------------------------------------------ - - const Test = struct { - stream: Stream, - - pub const Error = Stream.WriteError; - - pub const Stream = std.io.FixedBufferStream([]u8); - - pub fn write(self: *Test, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - const writer = self.stream.writer(); - try writer.writeAll(text); - try writer.writeByteNTimes('\n', nl); - } - - pub fn indent(self: *Test, vm: *Interpreter) !void { - _ = vm; - const writer = self.stream.writer(); - try writer.writeByteNTimes(' ', vm.indent); - } - - pub fn expect(self: *Test, expected: []const u8) !void { - try testing.expectEqualStrings(expected, self.stream.getWritten()); - } - }; - -### Stream context - - lang: zig esc: none file: lib/context.zig - ----------------------------------------- - - const lib = @import("lib.zig"); - - const Interpreter = lib.Interpreter; - - pub fn StreamContext(comptime Writer: type) type { - return struct { - stream: Writer, - - const Self = @This(); - - pub const Error = Writer.Error; - - pub fn init(writer: Writer) Self { - return .{ .stream = writer }; - } - - pub fn write(self: *Self, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - try self.stream.writeAll(text); - try self.stream.writeByteNTimes('\n', nl); - } - - pub fn indent(self: *Self, vm: *Interpreter) !void { - try self.stream.writeByteNTimes(' ', vm.indent); - } - }; - } - -### Find context - - lang: zig esc: none file: src/FindContext.zig - --------------------------------------------- - - const std = @import("std"); - const lib = @import("lib"); - const io = std.io; - const fs = std.fs; - const mem = std.mem; - - const ArrayList = std.ArrayListUnmanaged; - const Allocator = std.mem.Allocator; - const Interpreter = lib.Interpreter; - const FindContext = @This(); - - stream: Stream, - line: u32 = 1, - column: u32 = 1, - stack: Stack = .{}, - filename: []const u8, - tag: []const u8, - gpa: Allocator, - - const log = std.log.scoped(.find_context); - - pub const Error = error{OutOfMemory} || std.os.WriteError; - - pub const Stream = io.BufferedWriter(1024, std.fs.File.Writer); - - pub const Stack = ArrayList(Location); + }, +} +``` - pub const Location = struct { - line: u32, - column: u32, - }; +In `build.zig`, the real zangle is loaded as a dependency and set to follow the local target and optimization +configuration. - pub fn init(gpa: Allocator, file: []const u8, tag: []const u8, writer: fs.File.Writer) FindContext { - return .{ - .stream = .{ .unbuffered_writer = writer }, - .filename = file, - .tag = tag, - .gpa = gpa, - }; - } +``` zig tag: import zangle from the dependency list, set target parameters, and install the artifact +const dep = b.dependency("zangle", .{}); +const zangle = dep.artifact("zangle"); +zangle.target = target; +zangle.optimize = optimize; +``` - pub fn write(self: *FindContext, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - if (nl == 0) { - self.column += @intCast(u32, text.len); - } else { - self.line += @intCast(u32, nl); - self.column = @intCast(u32, text.len + 1); - } - } +Then installed with `b.installArtifact()` which also ensures that the executable is built upon invoking `zig build`. - pub fn call(self: *FindContext, vm: *Interpreter) !void { - _ = vm; +``` zig tag: import zangle from the dependency list, set target parameters, and install the artifact +b.installArtifact(zangle); +``` - try self.stack.append(self.gpa, .{ - .line = self.line, - .column = self.column, - }); - } +Finally, testing out zangle should require no more than `zig build run` to invoke it. - pub fn ret(self: *FindContext, vm: *Interpreter, name: []const u8) !void { - _ = name; +``` zig tag: setup a run command such that it can be tested without having write the path to the binary in zig-out +const run_cmd = b.addRunArtifact(zangle); - const writer = self.stream.writer(); - const location = self.stack.pop(); - const procedure = vm.linker.procedures.get(name).?; - const obj = vm.linker.objects.items[procedure.module - 1]; - - if (mem.eql(u8, self.tag, name)) try writer.print( - \\{s}: line {d} column {d} '{s}' -> line {d} column {d} '{s}' ({d} lines) - \\ - , .{ - self.tag, - procedure.location.line, - procedure.location.column, - obj.name, - location.line, - location.column, - self.filename, - self.line - location.line, - }); - } - -### Graph context - - lang: zig esc: [[]] file: src/GraphContext.zig - ---------------------------------------------- - - const std = @import("std"); - const lib = @import("lib"); - const io = std.io; - const fs = std.fs; - const assert = std.debug.assert; - - const Allocator = std.mem.Allocator; - const ArrayList = std.ArrayListUnmanaged; - const HashMap = std.AutoHashMapUnmanaged; - const Interpreter = lib.Interpreter; - const GraphContext = @This(); - - stream: Stream, - stack: Stack = .{}, - omit: Omit = .{}, - gpa: Allocator, - colour: u8 = 0, - target: Target = .{}, - text_colour: u24 = 0, - inherit: bool = false, - colours: []const u24 = &.{}, - gradient: u8 = 5, - - pub const Error = error{OutOfMemory} || std.os.WriteError; - - pub const Stack = ArrayList(Layer); - pub const Layer = struct { - list: ArrayList([]const u8) = .{}, - }; - - pub const Target = HashMap([*]const u8, u8); - - pub const Omit = HashMap(Pair, void); - pub const Pair = struct { - from: [*]const u8, - to: [*]const u8, - }; - - pub const Stream = io.BufferedWriter(1024, std.fs.File.Writer); - - pub fn init(gpa: Allocator, writer: fs.File.Writer) GraphContext { - return .{ - .stream = .{ .unbuffered_writer = writer }, - .gpa = gpa, - }; - } - - pub const GraphOptions = struct { - border: u24 = 0, - background: u24 = 0, - text: u24 = 0, - colours: []const u24 = &.{}, - inherit: bool = false, - gradient: u8 = 0, - }; - - pub fn begin(self: *GraphContext, options: GraphOptions) !void { - try self.stream.writer().print( - \\graph G {{ - \\ bgcolor = "#{[background]x:0>6}"; - \\ overlap = false; - \\ rankdir = LR; - \\ concentrate = true; - \\ node[shape = rectangle, color = "#{[border]x:0>6}"]; - \\ - , .{ - .background = options.background, - .border = options.border, - }); - - try self.stack.append(self.gpa, .{}); - - self.colours = options.colours; - self.text_colour = options.text; - self.inherit = options.inherit; - self.gradient = options.gradient; - } - - pub fn end(self: *GraphContext) !void { - try self.stream.writer().writeAll("}\n"); - try self.stream.flush(); - } - - pub fn call(self: *GraphContext, vm: *Interpreter) !void { - _ = vm; - try self.stack.append(self.gpa, .{}); - } - - pub fn ret(self: *GraphContext, vm: *Interpreter, name: []const u8) !void { - _ = vm; - - try self.render(name); - - var old = self.stack.pop(); - old.list.deinit(self.gpa); - - try self.stack.items[self.stack.items.len - 1].list.append(self.gpa, name); - } - - pub fn terminate(self: *GraphContext, vm: *Interpreter, name: []const u8) !void { - _ = vm; - try self.render(name); - - self.stack.items[0].list.clearRetainingCapacity(); - - assert(self.stack.items.len == 1); - } - - [[graph context render node]] - - - - lang: zig esc: [[]] tag: #graph context render node - --------------------------------------------------- - - fn render(self: *GraphContext, name: []const u8) !void { - const writer = self.stream.writer(); - const sub_nodes = self.stack.items[self.stack.items.len - 1].list.items; - - var valid: usize = 0; - for (sub_nodes) |sub| { - if (!self.omit.contains(.{ .from = name.ptr, .to = sub.ptr })) { - valid += 1; - } - } - - const theme = try self.target.getOrPut(self.gpa, name.ptr); - if (!theme.found_existing) { - theme.value_ptr.* = self.colour; - defer self.colour +%= 1; - - const selected = if (self.colours.len == 0) - self.colour - else - self.colours[self.colour % self.colours.len]; - - if (self.inherit) { - try writer.print( - \\ "{[name]s}"[fontcolor = "#{[colour]x:0>6}", color = "#{[inherit]x:0>6}"]; - \\ - , .{ - .name = name, - .colour = self.text_colour, - .inherit = selected, - }); - } else { - try writer.print( - \\ "{[name]s}"[fontcolor = "#{[colour]x:0>6}"]; - \\ - , .{ - .name = name, - .colour = self.text_colour, - }); - } - } - - for (sub_nodes) |sub| { - const entry = try self.omit.getOrPut(self.gpa, .{ - .from = name.ptr, - .to = sub.ptr, - }); - - if (!entry.found_existing) { - const to = self.target.get(sub.ptr).?; - const from = self.target.get(name.ptr).?; - - const selected: struct { from: u24, to: u24 } = if (self.colours.len == 0) .{ - .from = 0, - .to = 0, - } else .{ - .from = self.colours[from % self.colours.len], - .to = self.colours[to % self.colours.len], - }; - - try writer.print( - \\ "{s}" -- "{s}" [color = " - , .{ name, sub }); - - [[graph context gradient]] - - try writer.print( - \\#{x:0>6}"]; - \\ - , .{selected.to}); - } - } - } - -#### Gradients - -Node graphs can be rather difficult when they result in a directed acyclic -graph instead of the usual tree structure that most projects will have; for -those situations, targeted colour may not be enough to trace paths correctly. -However, gradients offer enough variation to avoid the problem in most cases -and they tend to look good most of the time making them a good default. - -The implementation interpolates between the parent and child node colours -and inserts the results in the node's `color` list to create the effect. - - lang: zig esc: none tag: #graph context gradient - ------------------------------------------------ - - if (self.gradient != 0) { - var i: i24 = 0; - const r: i32 = @truncate(u8, selected.from >> 16); - const g: i32 = @truncate(u8, selected.from >> 8); - const b: i32 = @truncate(u8, selected.from); - - const x: i32 = @truncate(u8, selected.to >> 16); - const y: i32 = @truncate(u8, selected.to >> 8); - const z: i32 = @truncate(u8, selected.to); - - const dx = @divTrunc(x - r, self.gradient); - const gy = @divTrunc(y - g, self.gradient); - const bz = @divTrunc(z - b, self.gradient); - - while (i < self.gradient) : (i += 1) { - const red = r + dx * i; - const green = g + gy * i; - const blue = b + bz * i; - const rgb = @bitCast(u24, @truncate(i24, red << 16 | (green << 8) | (blue & 0xff))); - try writer.print("#{x:0>6};{d}:", .{ rgb, 1.0 / @intToFloat(f64, self.gradient) }); - } - } - -## Interpreter main - - lang: zig esc: [[]] file: lib/Interpreter.zig - --------------------------------------------- - - const std = @import("std"); - const lib = @import("lib.zig"); - const meta = std.meta; - const testing = std.testing; - - const Linker = lib.Linker; - const Parser = lib.Parser; - const Instruction = lib.Instruction; - const HashMap = std.AutoArrayHashMapUnmanaged; - const Allocator = std.mem.Allocator; - const Interpreter = @This(); - - linker: Linker = .{}, - module: u16 = 1, - ip: u32 = 0, - stack: Stack = .{}, - indent: u16 = 0, - should_indent: bool = false, - last_is_newline: bool = true, - - const Stack = HashMap(u32, StackFrame); - - const StackFrame = struct { - module: u16, - ip: u32, - indent: u16, - }; - - const log = std.log.scoped(.vm); - - pub fn step(vm: *Interpreter, gpa: Allocator, comptime T: type, eval: T) !bool { - const object = vm.linker.objects.items[vm.module - 1]; - const opcode = object.program.items(.opcode); - const data = object.program.items(.data); - const index = vm.ip; - - vm.ip += 1; - - switch (opcode[index]) { - .ret => return try vm.execRet(T, data[index].ret, eval), - .jmp => try vm.execJmp(T, data[index].jmp, eval), - .call => try vm.execCall(T, data[index].call, gpa, eval), - .shell => vm.execShell(T, data[index].shell, object.text, eval), - .write => try vm.execWrite(T, data[index].write, object.text, eval), - } - - return true; - } - - [[interpreter step]] - - pub fn deinit(vm: *Interpreter, gpa: Allocator) void { - vm.linker.deinit(gpa); - vm.stack.deinit(gpa); - } - - fn Child(comptime T: type) type { - switch (@typeInfo(T)) { - .Pointer => |info| return info.child, - else => return T, - } - } - - pub fn call(vm: *Interpreter, gpa: Allocator, symbol: []const u8, comptime T: type, eval: T) !void { - if (vm.linker.procedures.get(symbol)) |sym| { - vm.ip = sym.entry; - vm.module = sym.module; - vm.indent = 0; - log.debug("calling {s} address {x:0>8} module {d}", .{ symbol, vm.ip, vm.module }); - while (try vm.step(gpa, T, eval)) {} - } else return error.@"Unknown procedure"; - } - - pub fn callFile(vm: *Interpreter, gpa: Allocator, symbol: []const u8, comptime T: type, eval: T) !void { - if (vm.linker.files.get(symbol)) |sym| { - vm.ip = sym.entry; - vm.module = sym.module; - vm.indent = 0; - log.debug("calling {s} address {x:0>8} module {d}", .{ symbol, vm.ip, vm.module }); - while (try vm.step(gpa, T, eval)) {} - } else return error.@"Unknown procedure"; - } - - [[interpreter tests]] - -# Linker - - lang: zig esc: [[]] file: lib/Linker.zig - ---------------------------------------- - - const std = @import("std"); - const lib = @import("lib.zig"); - const testing = std.testing; - const assert = std.debug.assert; - - const Parser = lib.Parser; - const Instruction = lib.Instruction; - const ArrayList = std.ArrayListUnmanaged; - const Allocator = std.mem.Allocator; - const StringMap = std.StringArrayHashMapUnmanaged; - const Tokenizer = lib.Tokenizer; - const Linker = @This(); - - objects: Object.List = .{}, - generation: u16 = 1, - procedures: ProcedureMap = .{}, - files: FileMap = .{}, - - const ProcedureMap = StringMap(Procedure); - const FileMap = StringMap(Procedure); - const Procedure = struct { - entry: u32, - module: u16, - location: Tokenizer.Location, - }; - - const log = std.log.scoped(.linker); - - pub fn deinit(l: *Linker, gpa: Allocator) void { - for (l.objects.items) |*obj| obj.deinit(gpa); - l.objects.deinit(gpa); - l.procedures.deinit(gpa); - l.files.deinit(gpa); - l.generation = undefined; - } - - pub const Object = struct { - name: []const u8, - text: []const u8, - program: Instruction.List = .{}, - symbols: SymbolMap = .{}, - adjacent: AdjacentMap = .{}, - files: Object.FileMap = .{}, - - pub const List = ArrayList(Object); - pub const SymbolMap = StringMap(SymbolList); - pub const FileMap = StringMap(File); - pub const SymbolList = ArrayList(u32); - pub const AdjacentMap = StringMap(Adjacent); - - pub const File = struct { - entry: u32, - location: Tokenizer.Location, - }; - - pub const Adjacent = struct { - entry: u32, - exit: u32, - location: Tokenizer.Location, - }; - - pub fn deinit(self: *Object, gpa: Allocator) void { - self.program.deinit(gpa); - - for (self.symbols.values()) |*entry| entry.deinit(gpa); - self.symbols.deinit(gpa); - self.adjacent.deinit(gpa); - self.files.deinit(gpa); - } - }; - - [[linker merge adjacent blocks method]] - - [[linker build procedure table method]] - - [[linker update procedure calls method]] - - [[linker build file table method]] - - [[linker link method]] - -### Merge adjacent blocks - -TODO: short-circuit on non local module end - - lang: zig esc: none tag: #linker merge adjacent blocks method - ------------------------------------------------------------- - - fn mergeAdjacent(l: *Linker) void { - for (l.objects.items) |*obj, module| { - log.debug("processing module {d}", .{module + 1}); - const values = obj.adjacent.values(); - for (obj.adjacent.keys()) |key, i| { - const opcodes = obj.program.items(.opcode); - const data = obj.program.items(.data); - const exit = values[i].exit; - log.debug("opcode {}", .{opcodes[exit]}); - - switch (opcodes[exit]) { - .ret, .jmp => { - if (opcodes[exit] == .jmp and data[exit].jmp.generation == l.generation) continue; - var last_adj = values[i]; - var last_obj = obj; - - for (l.objects.items[module + 1 ..]) |*next, offset| { - if (next.adjacent.get(key)) |current| { - const op = last_obj.program.items(.opcode)[last_adj.exit]; - assert(op == .jmp or op == .ret); - - const destination = @intCast(u16, module + offset) + 2; - log.debug("updating jump location to address 0x{x:0>8} in module {d}", .{ - current.entry, - destination, - }); - - last_obj.program.items(.opcode)[last_adj.exit] = .jmp; - last_obj.program.items(.data)[last_adj.exit] = .{ .jmp = .{ - .generation = l.generation, - .address = current.entry, - .module = destination, - } }; - last_adj = current; - last_obj = next; - } - } - }, - - else => unreachable, - } - } - } - } - - test "merge" { - var obj_a = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - \\ - \\ lang: zig esc: none tag: #b - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var obj_b = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var obj_c = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #b - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var l: Linker = .{}; - defer l.deinit(testing.allocator); - - try l.objects.appendSlice(testing.allocator, &.{ - obj_a, - obj_b, - obj_c, - }); - - l.mergeAdjacent(); - - try testing.expectEqualSlices(Instruction.Opcode, &.{ .write, .jmp, .write, .jmp }, obj_a.program.items(.opcode)); - - try testing.expectEqual( - Instruction.Data.Jmp{ - .module = 2, - .address = 0, - .generation = 1, - }, - obj_a.program.items(.data)[1].jmp, - ); - - try testing.expectEqual( - Instruction.Data.Jmp{ - .module = 3, - .address = 0, - .generation = 1, - }, - obj_a.program.items(.data)[3].jmp, - ); - } - -### Register procedures - - lang: zig esc: none tag: #linker build procedure table method - ------------------------------------------------------------- - - fn buildProcedureTable(l: *Linker, gpa: Allocator) !void { - log.debug("building procedure table", .{}); - for (l.objects.items) |obj, module| { - log.debug("processing module {d} with {d} procedures", .{ module + 1, obj.adjacent.keys().len }); - for (obj.adjacent.keys()) |key, i| { - const entry = try l.procedures.getOrPut(gpa, key); - if (!entry.found_existing) { - const adjacent = obj.adjacent.values()[i]; - log.debug("registering new procedure '{s}' address {x:0>8} module {d}", .{ - key, - adjacent.entry, - module + 1, - }); - - entry.value_ptr.* = .{ - .module = @intCast(u16, module) + 1, - .entry = @intCast(u32, adjacent.entry), - .location = adjacent.location, - }; - } - } - } - log.debug("registered {d} procedures", .{l.procedures.count()}); - } - -### Update procedure calls - - lang: zig esc: none tag: #linker update procedure calls method - -------------------------------------------------------------- - - fn updateProcedureCalls(l: *Linker) void { - log.debug("updating procedure calls", .{}); - for (l.procedures.keys()) |key, i| { - const proc = l.procedures.values()[i]; - for (l.objects.items) |*obj| if (obj.symbols.get(key)) |sym| { - log.debug("updating locations {any}", .{sym.items}); - for (sym.items) |location| { - assert(obj.program.items(.opcode)[location] == .call); - const call = &obj.program.items(.data)[location].call; - call.address = proc.entry; - call.module = proc.module; - } - }; - } - } - -### Check for file conflicts and build a file table - - lang: zig esc: none tag: #linker build file table method - -------------------------------------------------------- - - fn buildFileTable(l: *Linker, gpa: Allocator) !void { - for (l.objects.items) |obj, module| { - for (obj.files.keys()) |key, i| { - const file = try l.files.getOrPut(gpa, key); - const record = obj.files.values()[i]; - if (file.found_existing) return error.@"Multiple files with the same name"; - file.value_ptr.module = @intCast(u16, module) + 1; - file.value_ptr.entry = record.entry; - file.value_ptr.location = record.location; - } - } - } - -### Link - - lang: zig esc: none tag: #linker link method - -------------------------------------------- - - pub fn link(l: *Linker, gpa: Allocator) !void { - l.procedures.clearRetainingCapacity(); - l.files.clearRetainingCapacity(); - - try l.buildProcedureTable(gpa); - try l.buildFileTable(gpa); - - l.mergeAdjacent(); - l.updateProcedureCalls(); - - var failure = false; - for (l.objects.items) |obj| { - for (obj.symbols.keys()) |key| { - if (!l.procedures.contains(key)) { - failure = true; - log.err("unknown symbol '{s}'", .{key}); - } - } - } - - if (failure) return error.@"Unknown symbol"; - } - - test "call" { - var obj = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - \\ - \\ lang: zig esc: [[]] tag: #b - \\ --------------------------- - \\ - \\ [[a]] - \\ - \\end - ); - - var l: Linker = .{}; - defer l.deinit(testing.allocator); - - try l.objects.append(testing.allocator, obj); - try l.link(testing.allocator); - - try testing.expectEqualSlices( - Instruction.Opcode, - &.{ .write, .ret, .call, .ret }, - obj.program.items(.opcode), - ); - - try testing.expectEqual( - Instruction.Data.Call{ - .address = 0, - .module = 1, - .indent = 0, - }, - obj.program.items(.data)[2].call, - ); - } - -# Parser - - lang: zig esc: [[]] file: lib/Parser.zig - ---------------------------------------- - - const std = @import("std"); - const lib = @import("lib.zig"); - const mem = std.mem; - const testing = std.testing; - const assert = std.debug.assert; - - const Tokenizer = lib.Tokenizer; - const Linker = lib.Linker; - const Allocator = std.mem.Allocator; - const Instruction = lib.Instruction; - const Location = Tokenizer.Location; - const Parser = @This(); - - it: Tokenizer, - program: Instruction.List = .{}, - symbols: Linker.Object.SymbolMap = .{}, - adjacent: Linker.Object.AdjacentMap = .{}, - files: Linker.Object.FileMap = .{}, - location: Location = .{}, - - const Token = Tokenizer.Token; - const log = std.log.scoped(.parser); - - pub fn deinit(p: *Parser, gpa: Allocator) void { - p.program.deinit(gpa); - for (p.symbols.values()) |*entry| entry.deinit(gpa); - p.symbols.deinit(gpa); - p.adjacent.deinit(gpa); - p.files.deinit(gpa); - p.* = undefined; - } - - [[zangle parser primitives]] - [[zangle parser]] - -## Token - - lang: zig esc: none tag: #zangle tokenizer token - ------------------------------------------------ - - pub const Token = struct { - tag: Tag, - start: usize, - end: usize, - - pub const Tag = enum(u8) { - eof, - - nl = '\n', - space = ' ', - - word, - line = '-', - hash = '#', - pipe = '|', - colon = ':', - - l_angle = '<', - l_brace = '{', - l_bracket = '[', - l_paren = '(', - - r_angle = '>', - r_brace = '}', - r_bracket = ']', - r_paren = ')', - - unknown, - }; - - pub fn slice(t: Token, bytes: []const u8) []const u8 { - return bytes[t.start..t.end]; - } - - pub fn len(t: Token) usize { - return t.end - t.start; - } - }; - -## Tokenizer - -![Zangle tokenizer state transition graph](out/tokenizer-state-transitions.png) - -\hidden{ - - lang: dot esc: {{}} file: graphs/tokenizer-state-transitions.dot - ---------------------------------------------------------------- - - digraph G { - node [shape = doublecircle] start; - node [shape = rectangle]; - rankdir = LR; - layout = "dot"; - - {{tokenizer state transition}} - } - -} - - - - lang: zig esc: [[]] file: lib/Tokenizer.zig - ------------------------------------------- - - const std = @import("std"); - const mem = std.mem; - const testing = std.testing; - const assert = std.debug.assert; - - const Tokenizer = @This(); - - bytes: []const u8, - index: usize = 0, - - pub const Location = struct { - line: usize = 1, - column: usize = 1, - }; - - const log = std.log.scoped(.tokenizer); - - [[zangle tokenizer token]] - - pub fn locationFrom(self: Tokenizer, from: Location) Location { - assert(from.line != 0); - assert(from.column != 0); - - var loc = from; - const start = from.line * from.column - 1; - - for (self.bytes[start..self.index]) |byte| { - if (byte == '\n') { - loc.line += 1; - loc.column = 1; - } else { - loc.column += 1; - } - } - - return loc; - } - - pub fn next(self: *Tokenizer) Token { - var token: Token = .{ - .tag = .eof, - .start = self.index, - .end = undefined, - }; - - defer log.debug("{s: >10} {d: >3} | {s}", .{ - @tagName(token.tag), - token.len(), - token.slice(self.bytes), - }); - - const State = enum { start, trivial, unknown, word }; - var state: State = .start; - var trivial: u8 = 0; - - while (self.index < self.bytes.len) : (self.index += 1) { - const c = self.bytes[self.index]; - switch (state) { - .start => switch (c) { - [[zangle tokenizer start transitions]] - }, - - [[zangle tokenizer state transitions]] - } - } - - token.end = self.index; - return token; - } - - [[zangle tokenizer tests]] - -#### Trivial - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - trivial -> trivial [label = "same byte"]; - trivial -> end; - -} - - lang: zig esc: none tag: #zangle tokenizer state transitions - ------------------------------------------------------------ - - .trivial => if (c != trivial) break, - -#### Whitespace - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - start -> trivial [label = "space, nl"]; - -} - -Whitespace of the same type is consumed as a single token. - - lang: zig esc: none tag: #zangle tokenizer start transitions - ------------------------------------------------------------ - - ' ', '\n' => { - token.tag = @intToEnum(Token.Tag, c); - trivial = c; - state = .trivial; - }, - - - - lang: zig esc: none tag: #zangle tokenizer tests - ------------------------------------------------ - - test "tokenize whitespace" { - try testTokenize("\n", &.{.nl}); - try testTokenize(" ", &.{.space}); - try testTokenize("\n\n\n\n\n", &.{.nl}); - try testTokenize("\n\n \n\n\n", &.{ .nl, .space, .nl }); - } - -#### Header - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - start -> trivial [label = "line"]; - start -> word [label = "a...z"]; - start -> end [label = "hash, colon"]; - -} - - lang: zig esc: none tag: #zangle tokenizer start transitions - ------------------------------------------------------------ - - '-' => { - token.tag = .line; - trivial = '-'; - state = .trivial; - }, - - 'a'...'z' => { - token.tag = .word; - state = .word; - }, - - '#', ':' => { - token.tag = @intToEnum(Token.Tag, c); - self.index += 1; - break; - }, - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - word -> word [label = "a...z, A...Z, #, +, -, \\, _"]; - word -> end; - -} - - lang: zig esc: none tag: #zangle tokenizer state transitions - ------------------------------------------------------------ - - .word => switch (c) { - 'a'...'z', 'A'...'Z', '#', '+', '-', '\'', '_' => {}, - else => break, - }, - - - - lang: zig esc: none tag: #zangle tokenizer tests - ------------------------------------------------ - - test "tokenize header" { - try testTokenize("-", &.{.line}); - try testTokenize("#", &.{.hash}); - try testTokenize(":", &.{.colon}); - try testTokenize("-----------------", &.{.line}); - try testTokenize("###", &.{ .hash, .hash, .hash }); - try testTokenize(":::", &.{ .colon, .colon, .colon }); - } - -#### Include - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - start -> trivial [label = "<, {, [, (, ), ], }, >"]; +run_cmd.step.dependOn(b.getInstallStep()); +if (b.args) |args| { + run_cmd.addArgs(args); } - lang: zig esc: none tag: #zangle tokenizer start transitions - ------------------------------------------------------------ - - '<', '{', '[', '(', ')', ']', '}', '>' => { - token.tag = @intToEnum(Token.Tag, c); - trivial = c; - state = .trivial; - }, - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - start -> end [label = "pipe"]; - -} - - lang: zig esc: none tag: #zangle tokenizer start transitions - ------------------------------------------------------------ - - '|' => { - token.tag = .pipe; - self.index += 1; - break; - }, - - - - lang: zig esc: none tag: #zangle tokenizer tests - ------------------------------------------------ - - test "tokenize include" { - try testTokenize("|", &.{.pipe}); - try testTokenize("|||", &.{ .pipe, .pipe, .pipe }); - } - -#### Unknown - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - start -> unknown [label = "unknown"]; - -} - - lang: zig esc: none tag: #zangle tokenizer start transitions - ------------------------------------------------------------ - - else => { - token.tag = .unknown; - state = .unknown; - }, - -\hidden{ - - lang: dot esc: none tag: #tokenizer state transition - ---------------------------------------------------- - - unknown -> unknown; - unknown -> end [label = "nl, <, {, [, (, ), ], }, >, colon, pipe"]; - -} - - lang: zig esc: none tag: #zangle tokenizer state transitions - ------------------------------------------------------------ - - .unknown => if (mem.indexOfScalar(u8, "\n <{[()]}>:|", c)) |_| { - break; - }, - - - - lang: zig esc: none tag: #zangle tokenizer tests - ------------------------------------------------ - - test "tokenize unknown" { - try testTokenize("/file.example/path/../__", &.{.unknown}); - } - -## Header - -Each code block starts with a header specifying the language used, delimiters -for code block imports, and either a file in which the content should be -written to or a tag which may be referenced with import statements. - -TODO: record statistisc on the number of tag occurences so it's possible to -display `Tag: example tag (5/16)`. - -TODO: link same tags together so they form a chain and a loop back to the -start at the end. This allows the user to click through to the next block. - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - const Header = struct { - language: []const u8, - delimiter: ?[]const u8, - resource: Slice, - type: Type, - - pub const Slice = struct { - start: u32, - len: u16, - - pub fn slice(self: Slice, text: []const u8) []const u8 { - return text[self.start .. self.start + self.len]; - } - }; - - pub const Type = enum { file, tag }; - }; - - const ParseHeaderError = error{ - @"Expected a space between 'lang:' and the language name", - @"Expected a space after the language name", - @"Expected a space between 'esc:' and the delimiter specification", - @"Expected open delimiter", - @"Expected closing delimiter", - @"Expected matching closing angle bracket '>'", - @"Expected matching closing brace '}'", - @"Expected matching closing bracket ']'", - @"Expected matching closing paren ')'", - @"Expected opening and closing delimiter lengths to match", - @"Expected a space after delimiter specification", - @"Expected 'tag:' or 'file:' following delimiter specification", - @"Expected a space after 'file:'", - @"Expected a space after 'tag:'", - @"Expected a newline after the header", - @"Expected the dividing line to be indented by 4 spaces", - @"Expected a dividing line of '-' of the same length as the header", - @"Expected the division line to be of the same length as the header", - @"Expected at least one blank line after the division line", - @"Expected there to be only one space but more were given", - - @"Missing language specification", - @"Missing ':' after 'lang'", - @"Missing language name", - @"Missing 'esc:' delimiter specification", - @"Missing ':' after 'esc'", - @"Missing ':' after 'file'", - @"Missing ':' after 'tag'", - @"Missing '#' after 'tag: '", - @"Missing file name", - @"Missing tag name", - - @"Invalid delimiter, expected one of '<', '{', '[', '('", - @"Invalid delimiter, expected one of '>', '}', ']', ')'", - @"Invalid option given, expected 'tag:' or 'file:'", - @"Invalid file path, parent directory references '../' and '..\\' are not allowed within output paths", - @"Invalid file path, current directory references './' and '.\\' are not allowed within output paths", - }; - - fn parseHeaderLine(p: *Parser) ParseHeaderError!Header { - var header: Header = undefined; - - const header_start = p.it.index; - p.match(.word, "lang") orelse return error.@"Missing language specification"; - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'lang'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space between 'lang:' and the language name"; - } - - header.language = p.eat(.word, @src()) orelse return error.@"Missing language name"; - p.expect(.space, @src()) orelse return error.@"Expected a space after the language name"; - p.match(.word, "esc") orelse return error.@"Missing 'esc:' delimiter specification"; - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'esc'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space between 'esc:' and the delimiter specification"; - } - - if (p.match(.word, "none") == null) { - const start = p.it.index; - const open = p.next() orelse return error.@"Expected open delimiter"; - - switch (open.tag) { - .l_angle, .l_brace, .l_bracket, .l_paren => {}, - else => return error.@"Invalid delimiter, expected one of '<', '{', '[', '('", - } - - const closed = p.next() orelse return error.@"Expected closing delimiter"; - switch (closed.tag) { - .r_angle, .r_brace, .r_bracket, .r_paren => {}, - else => return error.@"Invalid delimiter, expected one of '>', '}', ']', ')'", - } - - if (open.tag == .l_angle and closed.tag != .r_angle) { - return error.@"Expected matching closing angle bracket '>'"; - } else if (open.tag == .l_brace and closed.tag != .r_brace) { - return error.@"Expected matching closing brace '}'"; - } else if (open.tag == .l_bracket and closed.tag != .r_bracket) { - return error.@"Expected matching closing bracket ']'"; - } else if (open.tag == .l_paren and closed.tag != .r_paren) { - return error.@"Expected matching closing paren ')'"; - } - - if (open.len() != closed.len()) { - return error.@"Expected opening and closing delimiter lengths to match"; - } - - header.delimiter = p.slice(start, p.it.index); - } else { - header.delimiter = null; - } - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space after delimiter specification"; - } - - var start: usize = undefined; - const tag = p.eat(.word, @src()) orelse { - return error.@"Expected 'tag:' or 'file:' following delimiter specification"; - }; - - if (mem.eql(u8, tag, "file")) { - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'file'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else return error.@"Expected a space after 'file:'"; - - header.type = .file; - start = p.it.index; - } else if (mem.eql(u8, tag, "tag")) { - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'tag'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else return error.@"Expected a space after 'tag:'"; - - p.expect(.hash, @src()) orelse return error.@"Missing '#' after 'tag: '"; - header.type = .tag; - start = p.it.index; - } else { - return error.@"Invalid option given, expected 'tag:' or 'file:'"; - } - - const nl = p.scan(.nl) orelse { - return error.@"Expected a newline after the header"; - }; - - header.resource = .{ - .start = @intCast(u32, start), - .len = @intCast(u16, nl.start - start), - }; - -File paths may specify `../` and `./` which are valid paths but the former -allows reaching outside of the project root while the other is confusing as -it always references the current directory. Since zangle shouldn't be able -to reach outside of the directory unless explicit permission is given via a -command-line flag and the other makes little sense, both options are treated -as parser errors. - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - const resource = header.resource.slice(p.it.bytes); - - if (header.type == .file) for (&[_][]const u8{ "../", "..\\" }) |invalid| { - if (mem.indexOf(u8, resource, invalid)) |index| { - if (index == 0 or resource[index - 1] != '.') { - return error.@"Invalid file path, parent directory references '../' and '..\\' are not allowed within output paths"; - } - } - }; - - if (header.type == .file) for (&[_][]const u8{ "./", ".\\" }) |invalid| { - if (mem.indexOf(u8, resource, invalid)) |index| { - if (index == 0 or resource[index - 1] != '.') { - return error.@"Invalid file path, current directory references './' and '.\\' are not allowed within output paths"; - } - } - }; - - - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - if (header.resource.len == 0) { - switch (header.type) { - .file => return error.@"Missing file name", - .tag => return error.@"Missing tag name", - } - } - - const len = (p.it.index - 1) - header_start; - - if ((p.eat(.space, @src()) orelse "").len != 4) { - return error.@"Expected the dividing line to be indented by 4 spaces"; - } - - const line = p.eat(.line, @src()) orelse { - return error.@"Expected a dividing line of '-' of the same length as the header"; - }; - - if (line.len != len) { - log.debug("header {d} line {d}", .{ len, line.len }); - return error.@"Expected the division line to be of the same length as the header"; - } - - if ((p.eat(.nl, @src()) orelse "").len < 2) { - return error.@"Expected at least one blank line after the division line"; - } - - return header; - } - -## Body - -TODO: link tags to their definition - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - fn parseBody(p: *Parser, gpa: Allocator, header: Header) !void { - log.debug("begin parsing body", .{}); - defer log.debug("end parsing body", .{}); - - const entry_point = @intCast(u32, p.program.len); - const location = p.it.locationFrom(p.location); - p.location = location; // avoid RLS - - var nl: usize = 0; - loop: while (p.eat(.space, @src())) |space| { - if (space.len < 4) break; - nl = 0; - - var sol = p.it.index - (space.len - 4); - while (true) { - const token = p.it.next(); - switch (token.tag) { - .eof => { - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = 0, - }); - break :loop; - }, - - .nl => { - nl = token.len(); - - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = @intCast(u16, nl), - }); - break; - }, - - .l_angle, - .l_brace, - .l_bracket, - .l_paren, - => if (header.delimiter) |delim| { - if (delim[0] != @enumToInt(token.tag)) { - log.debug("dilimiter doesn't match, skipping", .{}); - continue; - } - - if (delim.len != token.len() * 2) { - log.debug("dilimiter length doesn't match, skipping", .{}); - continue; - } - - if (token.start - sol > 0) { - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = 0, - }); - } - - try p.parseDelimiter(gpa, delim, token.start - sol); - sol = p.it.index; - }, - - else => {}, - } - } - } - - const len = p.program.len; - if (len != 0) { - const item = &p.program.items(.data)[len - 1].write; - item.nl = 0; - if (item.len == 0) p.program.len -= 1; - } - - if (nl < 2 and p.it.index < p.it.bytes.len) { - return error.@"Expected a blank line after the end of the code block"; - } - - switch (header.type) { - .tag => { - const adj = try p.adjacent.getOrPut(gpa, header.resource.slice(p.it.bytes)); - if (adj.found_existing) { - try p.writeJmp(adj.value_ptr.exit, .{ - .address = entry_point, - .module = 0, - }); - } else { - adj.value_ptr.entry = entry_point; - adj.value_ptr.location = location; - } - - adj.value_ptr.exit = @intCast(u32, p.program.len); - }, - - .file => { - const file = try p.files.getOrPut(gpa, header.resource.slice(p.it.bytes)); - if (file.found_existing) return error.@"Multiple file outputs with the same name"; - file.value_ptr.* = .{ - .entry = entry_point, - .location = location, - }; - }, - } - - try p.emitRet(gpa, .{ - .start = header.resource.start, - .len = header.resource.len, - }); - } - -#### Delimiters - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - fn parseDelimiter( - p: *Parser, - gpa: Allocator, - delim: []const u8, - indent: usize, - ) !void { - log.debug("parsing call", .{}); - - var pipe = false; - var colon = false; - var reached_end = false; - - const tag = blk: { - const start = p.it.index; - while (p.next()) |sub| switch (sub.tag) { - .nl => return error.@"Unexpected newline", - .pipe => { - pipe = true; - break :blk p.it.bytes[start..sub.start]; - }, - .colon => { - colon = true; - break :blk p.it.bytes[start..sub.start]; - }, - - .r_angle, - .r_brace, - .r_bracket, - .r_paren, - => if (@enumToInt(sub.tag) == delim[delim.len - 1]) { - if (delim.len != sub.len() * 2) { - return error.@"Expected a closing delimiter of equal length"; - } - reached_end = true; - break :blk p.it.bytes[start..sub.start]; - }, - - else => {}, - }; - - return error.@"Unexpected end of file"; - }; - - -Type casts must be given if the imported code block has a different type -than the target code block. - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - if (colon) { - const ty = p.eat(.word, @src()) orelse return error.@"Missing 'from' following ':'"; - if (!mem.eql(u8, ty, "from")) return error.@"Unknown type operation"; - p.expect(.l_paren, @src()) orelse return error.@"Expected '(' following 'from'"; - p.expect(.word, @src()) orelse return error.@"Expected type name"; - p.expect(.r_paren, @src()) orelse return error.@"Expected ')' following type name"; - } - -Pipes pass code blocks through external programs. - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - if (pipe or p.eat(.pipe, @src()) != null) { - const index = @intCast(u32, p.it.index); - const shell = p.eat(.word, @src()) orelse { - return error.@"Missing command following '|'"; - }; - - if (shell.len > 255) return error.@"Shell command name too long"; - try p.emitShell(gpa, .{ - .command = index, - .module = 0xffff, - .len = @intCast(u8, shell.len), - .pad = 0, - }); - } - - try p.emitCall(gpa, tag, .{ - .address = undefined, - .module = undefined, - .indent = @intCast(u16, indent), - }); - - if (!reached_end) { - const last = p.next() orelse return error.@"Expected closing delimiter"; - - if (last.len() * 2 != delim.len) { - return error.@"Expected closing delimiter length to match"; - } - - if (@enumToInt(last.tag) != delim[delim.len - 1]) { - return error.@"Invalid closing delimiter"; - } - } - } - - test "parse body" { - const text = - \\ <> - \\ [[a b c | : a}} - \\ <> - \\ <> - \\ <<<|:>> - \\ <|:> - \\ - \\text - ; - - var p: Parser = .{ .it = .{ .bytes = text } }; - defer p.deinit(testing.allocator); - try p.parseBody(testing.allocator, .{ - .language = "", - .delimiter = "<<>>", - .resource = .{ .start = 0, .len = 0 }, - .type = .tag, - }); - } - - test "compile single tag" { - const text = - \\ <> - \\ <<. . .:from(zig)>> - \\ <<1 2 3|com>> - \\ - \\end - ; - - var p: Parser = .{ .it = .{ .bytes = text } }; - defer p.deinit(testing.allocator); - try p.parseBody(testing.allocator, .{ - .language = "", - .delimiter = "<<>>", - .resource = .{ .start = 0, .len = 0 }, - .type = .tag, - }); - - try testing.expect(p.symbols.contains("a b c")); - try testing.expect(p.symbols.contains("1 2 3")); - try testing.expect(p.symbols.contains(". . .")); - } - - pub fn parse(gpa: Allocator, name: []const u8, text: []const u8) !Linker.Object { - var p: Parser = .{ .it = .{ .bytes = text } }; - errdefer p.deinit(gpa); - - while (try p.step(gpa)) {} - - return Linker.Object{ - .name = name, - .text = text, - .program = p.program, - .symbols = p.symbols, - .adjacent = p.adjacent, - .files = p.files, - }; - } - - pub fn object(p: *Parser, name: []const u8) Linker.Object { - return Linker.Object{ - .name = name, - .text = p.it.bytes, - .program = p.program, - .symbols = p.symbols, - .adjacent = p.adjacent, - .files = p.files, - }; - } - - pub fn step(p: *Parser, gpa: Allocator) !bool { - while (p.next()) |token| if (token.tag == .nl and token.len() >= 2) { - const space = p.eat(.space, @src()) orelse continue; - if (space.len != 4) continue; - - if (p.parseHeaderLine()) |header| { - try p.parseBody(gpa, header); - } else |e| switch (e) { - error.@"Missing language specification" => { - log.debug("begin indented block", .{}); - defer log.debug("end indented block", .{}); - - while (p.scan(.nl)) |nl| if (nl.len() >= 2) { - const tmp = p.next() orelse return false; - if (tmp.tag != .space) return true; - if (tmp.len() < 4) return true; - }; - }, - - else => |err| return err, - } - }; - - return false; - } - -# Appendix. Parser tests - - lang: zig esc: none tag: #zangle parser - --------------------------------------- - - test "parse header line" { - const complete_header = "lang: zig esc: {{}} tag: #hash\n ------------------------------\n\n"; - const common: Header = .{ - .language = "zig", - .delimiter = "{{}}", - .resource = .{ - .start = @intCast(u32, mem.indexOf(u8, complete_header, "hash").?), - .len = 4, - }, - .type = .tag, - }; - - try testing.expectError( - error.@"Expected a space between 'lang:' and the language name", - testParseHeader("lang:zig", common), - ); - - try testing.expectError( - error.@"Missing 'esc:' delimiter specification", - testParseHeader("lang: zig ", common), - ); - - try testing.expectError( - error.@"Missing ':' after 'esc'", - testParseHeader("lang: zig esc", common), - ); - - try testing.expectError( - error.@"Expected a space between 'esc:' and the delimiter specification", - testParseHeader("lang: zig esc:", common), - ); - - try testing.expectError( - error.@"Expected closing delimiter", - testParseHeader("lang: zig esc: {", common), - ); - - try testing.expectError( - error.@"Expected matching closing angle bracket '>'", - testParseHeader("lang: zig esc: <}", common), - ); - - try testing.expectError( - error.@"Expected matching closing brace '}'", - testParseHeader("lang: zig esc: {>", common), - ); - - try testing.expectError( - error.@"Expected matching closing bracket ']'", - testParseHeader("lang: zig esc: [>", common), - ); - - try testing.expectError( - error.@"Expected matching closing paren ')'", - testParseHeader("lang: zig esc: (>", common), - ); - - try testing.expectError( - error.@"Invalid delimiter, expected one of '<', '{', '[', '('", - testParseHeader("lang: zig esc: foo", common), - ); - - try testing.expectError( - error.@"Invalid delimiter, expected one of '>', '}', ']', ')'", - testParseHeader("lang: zig esc: > tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ - \\end - , .{ - .program = &.{ .call, .ret }, - .symbols = &.{"example"}, - .exports = &.{"here"}, - }); - } - - test "compile block with jump threadding" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ - \\then - \\ - \\ lang: zig esc: none tag: #here - \\ ------------------------------ - \\ - \\ more - \\ - \\end - , .{ - .program = &.{ .call, .jmp, .write, .ret }, - .symbols = &.{"example"}, - .exports = &.{"here"}, - }); - } - - test "compile block multiple call" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ <> - \\ <> - \\ - \\end - , .{ - .program = &.{ .call, .write, .call, .write, .call, .ret }, - .symbols = &.{ "one", "two", "three" }, - .exports = &.{"here"}, - }); - } - - test "compile block inline" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <><> - \\ - \\end - , .{ - .program = &.{ .call, .call, .ret }, - .symbols = &.{ "one", "two" }, - .exports = &.{"here"}, - }); - } - - test "compile block inline indent" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ one<> - \\ - \\end - , .{ - .program = &.{ .write, .call, .ret }, - .symbols = &.{"two"}, - .exports = &.{"here"}, - }); - } - - test "compile indented" { - try testCompile( - \\begin - \\ - \\ normal code block - \\ - \\end - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <><> - \\ - \\end - , .{ - .program = &.{ .call, .call, .ret }, - .symbols = &.{ "one", "two" }, - .exports = &.{"here"}, - }); - } - - - - lang: zig esc: none tag: #zangle tokenizer tests - ------------------------------------------------ - - fn testTokenize(text: []const u8, expected: []const Token.Tag) !void { - var it: Tokenizer = .{ .bytes = text }; - - for (expected) |tag| { - const token = it.next(); - try testing.expectEqual(tag, token.tag); - } - - const token = it.next(); - try testing.expectEqual(Token.Tag.eof, token.tag); - try testing.expectEqual(text.len, token.end); - } - -# Appendix. Interpreter tests - - lang: zig esc: none tag: #interpreter tests - ------------------------------------------- - - const TestTangleOutput = struct { - name: []const u8, - text: []const u8, - }; - - fn testTangle(source: []const []const u8, output: []const TestTangleOutput) !void { - var owned = true; - var l: Linker = .{}; - defer if (owned) l.deinit(testing.allocator); - - for (source) |src| { - const obj = try Parser.parse(testing.allocator, "", src); - try l.objects.append(testing.allocator, obj); - } - - try l.link(testing.allocator); - - var vm: Interpreter = .{ .linker = l }; - defer vm.deinit(testing.allocator); - owned = false; - - errdefer for (l.objects.items) |obj, i| { - log.debug("module {d}", .{i + 1}); - for (obj.program.items(.opcode)) |op| { - log.debug("{}", .{op}); - } - }; - - for (output) |out| { - log.debug("evaluating {s}", .{out.name}); - var buffer: [4096]u8 = undefined; - var context: Test = .{ .stream = .{ .buffer = &buffer, .pos = 0 } }; - try vm.call(testing.allocator, out.name, *Test, &context); - try context.expect(out.text); - } - } - - test "run simple no calls" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: none tag: #foo - \\ ----------------------------- - \\ - \\ abc - \\ - \\end - }, &.{ - .{ .name = "foo", .text = "abc" }, - }); - } - - test "run multiple outputs no calls" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: none tag: #foo - \\ ----------------------------- - \\ - \\ abc - \\ - \\then - \\ - \\ lang: zig esc: none tag: #bar - \\ ----------------------------- - \\ - \\ 123 - \\ - \\end - }, &.{ - .{ .name = "foo", .text = "abc" }, - .{ .name = "bar", .text = "123" }, - }); - } - - test "run multiple outputs common call" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: [[]] tag: #foo - \\ ----------------------------- - \\ - \\ [[baz]] - \\ - \\then - \\ - \\ lang: zig esc: [[]] tag: #bar - \\ ----------------------------- - \\ - \\ [[baz]][[baz]] - \\ - \\then - \\ - \\ lang: zig esc: none tag: #baz - \\ ----------------------------- - \\ - \\ abc - }, &.{ - .{ .name = "baz", .text = "abc" }, - .{ .name = "bar", .text = "abcabc" }, - .{ .name = "foo", .text = "abc" }, - }); - } - - test "run multiple outputs multiple inputs" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: [[]] tag: #foo - \\ ----------------------------- - \\ - \\ [[baz]] - \\ - \\end - , - \\begin - \\ - \\ lang: zig esc: [[]] tag: #bar - \\ ----------------------------- - \\ - \\ [[baz]][[baz]] - \\ - \\begin - , - \\end - \\ - \\ lang: zig esc: none tag: #baz - \\ ----------------------------- - \\ - \\ abc - \\ - \\end - }, &.{ - .{ .name = "baz", .text = "abc" }, - .{ .name = "bar", .text = "abcabc" }, - .{ .name = "foo", .text = "abc" }, - }); - } - -# Appendix. Parser primitives - - lang: zig esc: [[]] tag: #zangle parser primitives - -------------------------------------------------- - - [[parser codegen]] - - const Loc = std.builtin.SourceLocation; - - pub fn eat(p: *Parser, tag: Token.Tag, loc: Loc) ?[]const u8 { - const state = p.it; - const token = p.it.next(); - if (token.tag == tag) { - return token.slice(p.it.bytes); - } else { - log.debug("I'm starving for a '{s}' but this is a '{s}' ({s} {d}:{d})", .{ - @tagName(tag), - @tagName(token.tag), - loc.fn_name, - loc.line, - loc.column, - }); - p.it = state; - return null; - } - } - - pub fn next(p: *Parser) ?Token { - const token = p.it.next(); - if (token.tag != .eof) { - return token; - } else { - return null; - } - } - - pub fn scan(p: *Parser, tag: Token.Tag) ?Token { - while (p.next()) |token| if (token.tag == tag) { - return token; - }; - - return null; - } - - pub fn expect(p: *Parser, tag: Token.Tag, loc: Loc) ?void { - _ = p.eat(tag, loc) orelse { - log.debug("Wanted a {s}, but got nothing captain ({s} {d}:{d})", .{ - @tagName(tag), - loc.fn_name, - loc.line, - loc.column, - }); - return null; - }; - } - - pub fn slice(p: *Parser, from: usize, to: usize) []const u8 { - assert(from <= to); - return p.it.bytes[from..to]; - } - - pub fn match(p: *Parser, tag: Token.Tag, text: []const u8) ?void { - const state = p.it; - const token = p.it.next(); - if (token.tag == tag and mem.eql(u8, token.slice(p.it.bytes), text)) { - return; - } else { - p.it = state; - return null; - } - } - -# Appendix. Wasm interface - - lang: zig esc: none file: lib/wasm.zig - -------------------------------------- - - const std = @import("std"); - const lib = @import("lib.zig"); - - const Interpreter = lib.Interpreter; - const Parser = lib.Parser; - const ArrayList = std.ArrayList; - - var vm: Interpreter = .{}; - var instance = std.heap.GeneralPurposeAllocator(.{}){}; - var output: ArrayList(u8) = undefined; - const gpa = instance.allocator(); - - pub export fn init() void { - output = ArrayList(u8).init(gpa); - } - - pub export fn add(text: [*]const u8, len: usize) i32 { - const slice = text[0..len]; - return addInternal(slice) catch -1; - } - - fn addInternal(text: []const u8) !i32 { - var obj = try Parser.parse(gpa, "", text); - errdefer obj.deinit(gpa); - try vm.linker.objects.append(gpa, obj); - return @intCast(i32, vm.linker.objects.items.len - 1); - } - - pub export fn update(id: u32, text: [*]const u8, len: usize) i32 { - const slice = text[0..len]; - updateInternal(id, slice) catch return -1; - return 0; - } - - fn updateInternal(id: u32, text: []const u8) !void { - if (id >= vm.linker.objects.items.len) return error.@"Id out of range"; - const obj = try Parser.parse(gpa, "", text); - gpa.free(vm.linker.objects.items[id].text); - vm.linker.objects.items[id].deinit(gpa); - vm.linker.objects.items[id] = obj; - } - - pub export fn link() i32 { - vm.linker.link(gpa) catch return -1; - return 0; - } - - pub export fn call(name: [*]const u8, len: usize) i32 { - vm.call(gpa, name[0..len], Render, .{}) catch return -1; - return 0; - } - - pub export fn reset() void { - for (vm.linker.objects.items) |obj| gpa.free(obj.text); - vm.deinit(gpa); - vm = .{}; - } - - const Render = struct { - pub const Error = @TypeOf(output).Writer.Error; - - pub fn write(_: Render, v: *Interpreter, text: []const u8, nl: u16) !void { - _ = v; - const writer = output.writer(); - try writer.writeAll(text); - try writer.writeByteNTimes('\n', nl); - } - - pub fn indent(_: Render, v: *Interpreter) !void { - const writer = output.writer(); - try writer.writeByteNTimes(' ', v.indent); - } - }; - - - +const run_step = b.step("run", "Run the app"); +run_step.dependOn(&run_cmd.step); +``` +This concludes the example zangle document with two files written where one included other tags. diff --git a/assets/css/custom.css b/assets/css/custom.css deleted file mode 100644 index 7fcc7df..0000000 --- a/assets/css/custom.css +++ /dev/null @@ -1,105 +0,0 @@ -.section { - width: 100%; -} - -pre { margin: 0em; } - -/* NAVBAR */ -.nav-logo {} - - -.logo { - display: flex; - justify-content: center; -} - -.logo > svg { - width: 5em; - height: 5em; - fill: #5168a4; -} - - -.intro { - top: 0em; - width: 100%; - position: absolute; - background-color: #f6f5ee; -} - -.zangle { color: #5168a4; } -.main { - display: block; - height: 100%; - width: 100%; - padding-top: 10em; -} -.code-block { - margin-bottom: 2em; - margin-top: 2em; - padding-top: 1em; - padding-bottom: 1em; - width: 100vw; - border: none; - color: #eeeeee; - background-color: #1d2021; -} - -.code-block > .container > pre { - margin: 0em; -} - -.code { - margin: 0em; - padding: 0em; - border-radius: 0; - border: none; - color: #eeeeee; - background-color: #1d2021; -} - -.inlinecode { - margin: 0.5em; - padding: 0.5em; - border: none; - color: #000000; - background-color: #f6f5ee; -}.type-error { - color: #cc241d; -} -.type-safe-header { - color: #664270; - text-align: center; -}.filters-header { - color: #2bb37c; - text-align: center; -}.bi-header { - color: #d85229; - text-align: center; -}.tryit-header { - color: #007bb6; - text-align: center; -} -.tryit-block { - border: none; - width: 100%; - height: 30em; - background-color: #f6f5ee; -}.footer { - padding-top: 2em; - height: 20em; - width: 100vw; - background-color: #ccd1c8; -} - -body { - background-color: #ecf0e7; -} - -p { font-size: 1.5em; } - -.center.horizontal { - display: block; - margin-left: auto; - margin-right: auto; -} \ No newline at end of file diff --git a/assets/css/normalize.css b/assets/css/normalize.css deleted file mode 100644 index 192eb9c..0000000 --- a/assets/css/normalize.css +++ /dev/null @@ -1,349 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Render the `main` element consistently in IE. - */ - -main { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} diff --git a/assets/css/skeleton.css b/assets/css/skeleton.css deleted file mode 100644 index f28bf6c..0000000 --- a/assets/css/skeleton.css +++ /dev/null @@ -1,418 +0,0 @@ -/* -* Skeleton V2.0.4 -* Copyright 2014, Dave Gamache -* www.getskeleton.com -* Free to use under the MIT license. -* http://www.opensource.org/licenses/mit-license.php -* 12/29/2014 -*/ - - -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; } -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; } - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; } -} - -/* For devices larger than 550px */ -@media (min-width: 550px) { - .container { - width: 80%; } - .column, - .columns { - margin-left: 4%; } - .column:first-child, - .columns:first-child { - margin-left: 0; } - - .one.column, - .one.columns { width: 4.66666666667%; } - .two.columns { width: 13.3333333333%; } - .three.columns { width: 22%; } - .four.columns { width: 30.6666666667%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 48%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 65.3333333333%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.3333333333%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } - .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } - .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } - .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } - .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } - .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } - .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } - .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } - .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } - .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } - .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } - - .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } - - .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } - -} - - -/* Base Styles -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE -html is set to 62.5% so that all the REM measurements throughout Skeleton -are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; } -body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #222; } - - -/* Typography -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 2rem; - font-weight: 300; } -h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } -h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } -h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } -h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } - -/* Larger than phablet */ -@media (min-width: 550px) { - h1 { font-size: 5.0rem; } - h2 { font-size: 4.2rem; } - h3 { font-size: 3.6rem; } - h4 { font-size: 3.0rem; } - h5 { font-size: 2.4rem; } - h6 { font-size: 1.5rem; } -} - -p { - margin-top: 0; } - - -/* Links -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; } -a:hover { - color: #0FA0CE; } - - -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; } -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; } -.button.button-primary, -button.button-primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } -.button.button-primary:hover, -button.button-primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } - - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; } -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; } -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; } -label, -legend { - display: block; - margin-bottom: .5rem; - font-weight: 600; } -fieldset { - padding: 0; - border-width: 0; } -input[type="checkbox"], -input[type="radio"] { - display: inline; } -label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; } -ol { - list-style: decimal inside; } -ol, ul { - padding-left: 0; - margin-top: 0; } -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } -li { - margin-bottom: 1rem; } - - -/* Code -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -code { - padding: .2rem .5rem; - margin: 0 .2rem; - font-size: 90%; - white-space: nowrap; - background: #F1F1F1; - border: 1px solid #E1E1E1; - border-radius: 4px; } -pre > code { - display: block; - padding: 1rem 1.5rem; - white-space: pre; } - - -/* Tables -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -th, -td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } -th:first-child, -td:first-child { - padding-left: 0; } -th:last-child, -td:last-child { - padding-right: 0; } - - -/* Spacing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 1rem; } -input, -textarea, -select, -fieldset { - margin-bottom: 1.5rem; } -pre, -blockquote, -dl, -figure, -table, -p, -ul, -ol, -form { - margin-bottom: 2.5rem; } - - -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; } -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; } -.u-pull-right { - float: right; } -.u-pull-left { - float: left; } - - -/* Misc -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - -/* Clearing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; } - - -/* Media Queries -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* -Note: The best way to structure the use of media queries is to create the queries -near the relevant code. For example, if you wanted to change the styles for buttons -on small devices, paste the mobile query code up in the buttons section and style it -there. -*/ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} diff --git a/build.zig b/build.zig index c790399..3ca6974 100644 --- a/build.zig +++ b/build.zig @@ -1,60 +1,24 @@ const std = @import("std"); -const lib = @import("lib/lib.zig"); -const TangleStep = lib.TangleStep; -pub fn build(b: *std.build.Builder) !void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. +pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - - // Standard release options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. - b.setPreferredReleaseMode(.ReleaseSafe); - const mode = b.standardReleaseOptions(); - const exe = b.addExecutable("zangle", "src/main.zig"); - - const fmt_check_step = &b.addSystemCommand(&.{ "zig", "fmt", "--check", "--ast-check", "src", "lib" }).step; - - exe.addPackagePath("lib", "lib/lib.zig"); - exe.step.dependOn(fmt_check_step); - exe.setTarget(target); - exe.setBuildMode(mode); - exe.install(); - - const wa = b.addSharedLibrary("zangle", "lib/wasm.zig", .unversioned); - wa.step.dependOn(fmt_check_step); - wa.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); - wa.setBuildMode(mode); - wa.install(); - - const tangle = TangleStep.create(b); - tangle.addFile("README.md"); - const t_exe = b.addExecutableSource("zangle", tangle.getFileSource("src/main.zig")); - t_exe.addPackage(.{ - .name = "lib", - .path = tangle.getFileSource("lib/lib.zig"), + const optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .ReleaseSafe, }); + const dep = b.dependency("zangle", .{}); + const zangle = dep.artifact("zangle"); + zangle.target = target; + zangle.optimize = optimize; - const tangle_test_step = b.step("test-step", "Test compiling zangle using the build step"); - tangle_test_step.dependOn(&tangle.step); - tangle_test_step.dependOn(&b.addInstallArtifact(t_exe).step); + b.installArtifact(zangle); + const run_cmd = b.addRunArtifact(zangle); - const run_cmd = exe.run(); run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - - const test_cmd = b.addTest("lib/lib.zig"); - const test_main_cmd = b.addTest("src/main.zig"); - - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(fmt_check_step); - test_step.dependOn(&test_cmd.step); - test_step.dependOn(&test_main_cmd.step); } diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..4421e0f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "zangle", + .version = "0.3.0", + + .dependencies = .{ + .zangle = .{ + .url = "https://git.sr.ht/~tauoverpi/levy/archive/935578e5c70bc44e056e673a8b9ef3f0388cc961.tar.gz", + .hash = "1220ad55840aeaa62b01057f8838fa187bb1463ffbd2476b5ad2a4b2332b9e6f778e", + }, + }, +} diff --git a/lib/Instruction.zig b/lib/Instruction.zig deleted file mode 100644 index be91b20..0000000 --- a/lib/Instruction.zig +++ /dev/null @@ -1,55 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; - -const Instruction = @This(); - -opcode: Opcode, -data: Data, - -pub const List = std.MultiArrayList(Instruction); -pub const Opcode = enum(u8) { - ret, - call, - jmp, - shell, - write, -}; - -pub const Data = extern union { - ret: Ret, - jmp: Jmp, - call: Call, - shell: Shell, - write: Write, - - pub const Ret = extern struct { - start: u32, - len: u16, - pad: u16 = 0, - }; - pub const Jmp = extern struct { - address: u32, - module: u16, - generation: u16 = 0, - }; - pub const Call = extern struct { - address: u32, - module: u16, - indent: u16, - }; - pub const Shell = extern struct { - command: u32, - module: u16, - len: u8, - pad: u8, - }; - pub const Write = extern struct { - start: u32, - len: u16, - nl: u16, - }; -}; - -comptime { - assert(@sizeOf(Data) == 8); -} diff --git a/lib/Interpreter.zig b/lib/Interpreter.zig deleted file mode 100644 index c374476..0000000 --- a/lib/Interpreter.zig +++ /dev/null @@ -1,392 +0,0 @@ -const std = @import("std"); -const lib = @import("lib.zig"); -const meta = std.meta; -const testing = std.testing; - -const Linker = lib.Linker; -const Parser = lib.Parser; -const Instruction = lib.Instruction; -const HashMap = std.AutoArrayHashMapUnmanaged; -const Allocator = std.mem.Allocator; -const Interpreter = @This(); - -linker: Linker = .{}, -module: u16 = 1, -ip: u32 = 0, -stack: Stack = .{}, -indent: u16 = 0, -should_indent: bool = false, -last_is_newline: bool = true, - -const Stack = HashMap(u32, StackFrame); - -const StackFrame = struct { - module: u16, - ip: u32, - indent: u16, -}; - -const log = std.log.scoped(.vm); - -pub fn step(vm: *Interpreter, gpa: Allocator, comptime T: type, eval: T) !bool { - const object = vm.linker.objects.items[vm.module - 1]; - const opcode = object.program.items(.opcode); - const data = object.program.items(.data); - const index = vm.ip; - - vm.ip += 1; - - switch (opcode[index]) { - .ret => return try vm.execRet(T, data[index].ret, eval), - .jmp => try vm.execJmp(T, data[index].jmp, eval), - .call => try vm.execCall(T, data[index].call, gpa, eval), - .shell => vm.execShell(T, data[index].shell, object.text, eval), - .write => try vm.execWrite(T, data[index].write, object.text, eval), - } - - return true; -} - -fn execRet(vm: *Interpreter, comptime T: type, data: Instruction.Data.Ret, eval: T) Child(T).Error!bool { - const name = vm.linker.objects.items[vm.module - 1] - .text[data.start .. data.start + data.len]; - - if (vm.stack.popOrNull()) |location| { - const mod = vm.module; - const ip = vm.ip; - - vm.ip = location.value.ip; - vm.module = location.value.module; - vm.indent -= location.value.indent; - - if (@hasDecl(Child(T), "ret")) try eval.ret( - vm, - name, - ); - log.debug("[mod {d} ip {x:0>8}] ret(mod {d}, ip {x:0>8}, indent {d}, identifier '{s}')", .{ - mod, - ip, - vm.module, - vm.ip, - vm.indent, - name, - }); - - return true; - } - - if (@hasDecl(Child(T), "terminate")) try eval.terminate(vm, name); - log.debug("[mod {d} ip {x:0>8}] terminate(identifier '{s}')", .{ - vm.module, - vm.ip, - name, - }); - - return false; -} -fn execJmp(vm: *Interpreter, comptime T: type, data: Instruction.Data.Jmp, eval: T) Child(T).Error!void { - const mod = vm.module; - const ip = vm.ip; - - if (data.module != 0) { - vm.module = data.module; - } - - vm.ip = data.address; - - if (@hasDecl(Child(T), "jmp")) try eval.jmp(vm, data.address); - if (@hasDecl(Child(T), "write")) try eval.write(vm, "\n", 0); - - log.debug("[mod {d} ip {x:0>8}] jmp(mod {d}, address {x:0>8})", .{ - mod, - ip, - vm.module, - vm.ip, - }); - - vm.last_is_newline = true; -} -pub const CallError = error{ - @"Cyclic reference detected", - OutOfMemory, -}; - -fn execCall( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Call, - gpa: Allocator, - eval: T, -) (CallError || Child(T).Error)!void { - if (vm.stack.contains(vm.ip)) { - return error.@"Cyclic reference detected"; - } - - const mod = vm.module; - const ip = vm.ip; - - try vm.stack.put(gpa, vm.ip, .{ - .ip = vm.ip, - .indent = data.indent, - .module = vm.module, - }); - - vm.indent += data.indent; - vm.ip = data.address; - - if (data.module != 0) { - vm.module = data.module; - } - - if (@hasDecl(Child(T), "call")) try eval.call(vm); - log.debug("[mod {d} ip {x:0>8}] call(mod {d}, ip {x:0>8})", .{ - mod, - ip - 1, - vm.module, - vm.ip, - }); -} -fn execShell( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Shell, - text: []const u8, - eval: T, -) void { - if (@hasDecl(Child(T), "shell")) try eval.shell(vm); - _ = vm; - _ = data; - _ = text; - @panic("TODO: implement shell"); -} -fn execWrite( - vm: *Interpreter, - comptime T: type, - data: Instruction.Data.Write, - text: []const u8, - eval: T, -) Child(T).Error!void { - if (vm.should_indent and vm.last_is_newline) { - if (@hasDecl(Child(T), "indent")) try eval.indent(vm); - log.debug("[mod {d} ip {x:0>8}] indent(len {d})", .{ - vm.module, - vm.ip, - vm.indent, - }); - } else { - vm.should_indent = true; - } - - if (@hasDecl(Child(T), "write")) try eval.write( - vm, - text[data.start .. data.start + data.len], - data.nl, - ); - - log.debug("[mod {d} ip {x:0>8}] write(text {*}, index {x:0>8}, len {d}, nl {d}): {s}", .{ - vm.module, - vm.ip, - text, - data.start, - data.len, - data.nl, - text[data.start .. data.start + data.len], - }); - - vm.last_is_newline = data.nl != 0; -} -const Test = struct { - stream: Stream, - - pub const Error = Stream.WriteError; - - pub const Stream = std.io.FixedBufferStream([]u8); - - pub fn write(self: *Test, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - const writer = self.stream.writer(); - try writer.writeAll(text); - try writer.writeByteNTimes('\n', nl); - } - - pub fn indent(self: *Test, vm: *Interpreter) !void { - _ = vm; - const writer = self.stream.writer(); - try writer.writeByteNTimes(' ', vm.indent); - } - - pub fn expect(self: *Test, expected: []const u8) !void { - try testing.expectEqualStrings(expected, self.stream.getWritten()); - } -}; - -pub fn deinit(vm: *Interpreter, gpa: Allocator) void { - vm.linker.deinit(gpa); - vm.stack.deinit(gpa); -} - -fn Child(comptime T: type) type { - switch (@typeInfo(T)) { - .Pointer => |info| return info.child, - else => return T, - } -} - -pub fn call(vm: *Interpreter, gpa: Allocator, symbol: []const u8, comptime T: type, eval: T) !void { - if (vm.linker.procedures.get(symbol)) |sym| { - vm.ip = sym.entry; - vm.module = sym.module; - vm.indent = 0; - log.debug("calling {s} address {x:0>8} module {d}", .{ symbol, vm.ip, vm.module }); - while (try vm.step(gpa, T, eval)) {} - } else return error.@"Unknown procedure"; -} - -pub fn callFile(vm: *Interpreter, gpa: Allocator, symbol: []const u8, comptime T: type, eval: T) !void { - if (vm.linker.files.get(symbol)) |sym| { - vm.ip = sym.entry; - vm.module = sym.module; - vm.indent = 0; - log.debug("calling {s} address {x:0>8} module {d}", .{ symbol, vm.ip, vm.module }); - while (try vm.step(gpa, T, eval)) {} - } else return error.@"Unknown procedure"; -} - -const TestTangleOutput = struct { - name: []const u8, - text: []const u8, -}; - -fn testTangle(source: []const []const u8, output: []const TestTangleOutput) !void { - var owned = true; - var l: Linker = .{}; - defer if (owned) l.deinit(testing.allocator); - - for (source) |src| { - const obj = try Parser.parse(testing.allocator, "", src); - try l.objects.append(testing.allocator, obj); - } - - try l.link(testing.allocator); - - var vm: Interpreter = .{ .linker = l }; - defer vm.deinit(testing.allocator); - owned = false; - - errdefer for (l.objects.items) |obj, i| { - log.debug("module {d}", .{i + 1}); - for (obj.program.items(.opcode)) |op| { - log.debug("{}", .{op}); - } - }; - - for (output) |out| { - log.debug("evaluating {s}", .{out.name}); - var buffer: [4096]u8 = undefined; - var context: Test = .{ .stream = .{ .buffer = &buffer, .pos = 0 } }; - try vm.call(testing.allocator, out.name, *Test, &context); - try context.expect(out.text); - } -} - -test "run simple no calls" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: none tag: #foo - \\ ----------------------------- - \\ - \\ abc - \\ - \\end - }, &.{ - .{ .name = "foo", .text = "abc" }, - }); -} - -test "run multiple outputs no calls" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: none tag: #foo - \\ ----------------------------- - \\ - \\ abc - \\ - \\then - \\ - \\ lang: zig esc: none tag: #bar - \\ ----------------------------- - \\ - \\ 123 - \\ - \\end - }, &.{ - .{ .name = "foo", .text = "abc" }, - .{ .name = "bar", .text = "123" }, - }); -} - -test "run multiple outputs common call" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: [[]] tag: #foo - \\ ----------------------------- - \\ - \\ [[baz]] - \\ - \\then - \\ - \\ lang: zig esc: [[]] tag: #bar - \\ ----------------------------- - \\ - \\ [[baz]][[baz]] - \\ - \\then - \\ - \\ lang: zig esc: none tag: #baz - \\ ----------------------------- - \\ - \\ abc - }, &.{ - .{ .name = "baz", .text = "abc" }, - .{ .name = "bar", .text = "abcabc" }, - .{ .name = "foo", .text = "abc" }, - }); -} - -test "run multiple outputs multiple inputs" { - try testTangle(&.{ - \\begin - \\ - \\ lang: zig esc: [[]] tag: #foo - \\ ----------------------------- - \\ - \\ [[baz]] - \\ - \\end - , - \\begin - \\ - \\ lang: zig esc: [[]] tag: #bar - \\ ----------------------------- - \\ - \\ [[baz]][[baz]] - \\ - \\begin - , - \\end - \\ - \\ lang: zig esc: none tag: #baz - \\ ----------------------------- - \\ - \\ abc - \\ - \\end - }, &.{ - .{ .name = "baz", .text = "abc" }, - .{ .name = "bar", .text = "abcabc" }, - .{ .name = "foo", .text = "abc" }, - }); -} diff --git a/lib/Linker.zig b/lib/Linker.zig deleted file mode 100644 index 63201ab..0000000 --- a/lib/Linker.zig +++ /dev/null @@ -1,306 +0,0 @@ -const std = @import("std"); -const lib = @import("lib.zig"); -const testing = std.testing; -const assert = std.debug.assert; - -const Parser = lib.Parser; -const Instruction = lib.Instruction; -const ArrayList = std.ArrayListUnmanaged; -const Allocator = std.mem.Allocator; -const StringMap = std.StringArrayHashMapUnmanaged; -const Tokenizer = lib.Tokenizer; -const Linker = @This(); - -objects: Object.List = .{}, -generation: u16 = 1, -procedures: ProcedureMap = .{}, -files: FileMap = .{}, - -const ProcedureMap = StringMap(Procedure); -const FileMap = StringMap(Procedure); -const Procedure = struct { - entry: u32, - module: u16, - location: Tokenizer.Location, -}; - -const log = std.log.scoped(.linker); - -pub fn deinit(l: *Linker, gpa: Allocator) void { - for (l.objects.items) |*obj| obj.deinit(gpa); - l.objects.deinit(gpa); - l.procedures.deinit(gpa); - l.files.deinit(gpa); - l.generation = undefined; -} - -pub const Object = struct { - name: []const u8, - text: []const u8, - program: Instruction.List = .{}, - symbols: SymbolMap = .{}, - adjacent: AdjacentMap = .{}, - files: Object.FileMap = .{}, - - pub const List = ArrayList(Object); - pub const SymbolMap = StringMap(SymbolList); - pub const FileMap = StringMap(File); - pub const SymbolList = ArrayList(u32); - pub const AdjacentMap = StringMap(Adjacent); - - pub const File = struct { - entry: u32, - location: Tokenizer.Location, - }; - - pub const Adjacent = struct { - entry: u32, - exit: u32, - location: Tokenizer.Location, - }; - - pub fn deinit(self: *Object, gpa: Allocator) void { - self.program.deinit(gpa); - - for (self.symbols.values()) |*entry| entry.deinit(gpa); - self.symbols.deinit(gpa); - self.adjacent.deinit(gpa); - self.files.deinit(gpa); - } -}; - -fn mergeAdjacent(l: *Linker) void { - for (l.objects.items) |*obj, module| { - log.debug("processing module {d}", .{module + 1}); - const values = obj.adjacent.values(); - for (obj.adjacent.keys()) |key, i| { - const opcodes = obj.program.items(.opcode); - const data = obj.program.items(.data); - const exit = values[i].exit; - log.debug("opcode {}", .{opcodes[exit]}); - - switch (opcodes[exit]) { - .ret, .jmp => { - if (opcodes[exit] == .jmp and data[exit].jmp.generation == l.generation) continue; - var last_adj = values[i]; - var last_obj = obj; - - for (l.objects.items[module + 1 ..]) |*next, offset| { - if (next.adjacent.get(key)) |current| { - const op = last_obj.program.items(.opcode)[last_adj.exit]; - assert(op == .jmp or op == .ret); - - const destination = @intCast(u16, module + offset) + 2; - log.debug("updating jump location to address 0x{x:0>8} in module {d}", .{ - current.entry, - destination, - }); - - last_obj.program.items(.opcode)[last_adj.exit] = .jmp; - last_obj.program.items(.data)[last_adj.exit] = .{ .jmp = .{ - .generation = l.generation, - .address = current.entry, - .module = destination, - } }; - last_adj = current; - last_obj = next; - } - } - }, - - else => unreachable, - } - } - } -} - -test "merge" { - var obj_a = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - \\ - \\ lang: zig esc: none tag: #b - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var obj_b = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var obj_c = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #b - \\ --------------------------- - \\ - \\ abc - \\ - \\end - ); - - var l: Linker = .{}; - defer l.deinit(testing.allocator); - - try l.objects.appendSlice(testing.allocator, &.{ - obj_a, - obj_b, - obj_c, - }); - - l.mergeAdjacent(); - - try testing.expectEqualSlices(Instruction.Opcode, &.{ .write, .jmp, .write, .jmp }, obj_a.program.items(.opcode)); - - try testing.expectEqual( - Instruction.Data.Jmp{ - .module = 2, - .address = 0, - .generation = 1, - }, - obj_a.program.items(.data)[1].jmp, - ); - - try testing.expectEqual( - Instruction.Data.Jmp{ - .module = 3, - .address = 0, - .generation = 1, - }, - obj_a.program.items(.data)[3].jmp, - ); -} - -fn buildProcedureTable(l: *Linker, gpa: Allocator) !void { - log.debug("building procedure table", .{}); - for (l.objects.items) |obj, module| { - log.debug("processing module {d} with {d} procedures", .{ module + 1, obj.adjacent.keys().len }); - for (obj.adjacent.keys()) |key, i| { - const entry = try l.procedures.getOrPut(gpa, key); - if (!entry.found_existing) { - const adjacent = obj.adjacent.values()[i]; - log.debug("registering new procedure '{s}' address {x:0>8} module {d}", .{ - key, - adjacent.entry, - module + 1, - }); - - entry.value_ptr.* = .{ - .module = @intCast(u16, module) + 1, - .entry = @intCast(u32, adjacent.entry), - .location = adjacent.location, - }; - } - } - } - log.debug("registered {d} procedures", .{l.procedures.count()}); -} - -fn updateProcedureCalls(l: *Linker) void { - log.debug("updating procedure calls", .{}); - for (l.procedures.keys()) |key, i| { - const proc = l.procedures.values()[i]; - for (l.objects.items) |*obj| if (obj.symbols.get(key)) |sym| { - log.debug("updating locations {any}", .{sym.items}); - for (sym.items) |location| { - assert(obj.program.items(.opcode)[location] == .call); - const call = &obj.program.items(.data)[location].call; - call.address = proc.entry; - call.module = proc.module; - } - }; - } -} - -fn buildFileTable(l: *Linker, gpa: Allocator) !void { - for (l.objects.items) |obj, module| { - for (obj.files.keys()) |key, i| { - const file = try l.files.getOrPut(gpa, key); - const record = obj.files.values()[i]; - if (file.found_existing) return error.@"Multiple files with the same name"; - file.value_ptr.module = @intCast(u16, module) + 1; - file.value_ptr.entry = record.entry; - file.value_ptr.location = record.location; - } - } -} - -pub fn link(l: *Linker, gpa: Allocator) !void { - l.procedures.clearRetainingCapacity(); - l.files.clearRetainingCapacity(); - - try l.buildProcedureTable(gpa); - try l.buildFileTable(gpa); - - l.mergeAdjacent(); - l.updateProcedureCalls(); - - var failure = false; - for (l.objects.items) |obj| { - for (obj.symbols.keys()) |key| { - if (!l.procedures.contains(key)) { - failure = true; - log.err("unknown symbol '{s}'", .{key}); - } - } - } - - if (failure) return error.@"Unknown symbol"; -} - -test "call" { - var obj = try Parser.parse(testing.allocator, "", - \\ - \\ - \\ lang: zig esc: none tag: #a - \\ --------------------------- - \\ - \\ abc - \\ - \\end - \\ - \\ lang: zig esc: [[]] tag: #b - \\ --------------------------- - \\ - \\ [[a]] - \\ - \\end - ); - - var l: Linker = .{}; - defer l.deinit(testing.allocator); - - try l.objects.append(testing.allocator, obj); - try l.link(testing.allocator); - - try testing.expectEqualSlices( - Instruction.Opcode, - &.{ .write, .ret, .call, .ret }, - obj.program.items(.opcode), - ); - - try testing.expectEqual( - Instruction.Data.Call{ - .address = 0, - .module = 1, - .indent = 0, - }, - obj.program.items(.data)[2].call, - ); -} diff --git a/lib/Parser.zig b/lib/Parser.zig deleted file mode 100644 index 84004be..0000000 --- a/lib/Parser.zig +++ /dev/null @@ -1,991 +0,0 @@ -const std = @import("std"); -const lib = @import("lib.zig"); -const mem = std.mem; -const testing = std.testing; -const assert = std.debug.assert; - -const Tokenizer = lib.Tokenizer; -const Linker = lib.Linker; -const Allocator = std.mem.Allocator; -const Instruction = lib.Instruction; -const Location = Tokenizer.Location; -const Parser = @This(); - -it: Tokenizer, -program: Instruction.List = .{}, -symbols: Linker.Object.SymbolMap = .{}, -adjacent: Linker.Object.AdjacentMap = .{}, -files: Linker.Object.FileMap = .{}, -location: Location = .{}, - -const Token = Tokenizer.Token; -const log = std.log.scoped(.parser); - -pub fn deinit(p: *Parser, gpa: Allocator) void { - p.program.deinit(gpa); - for (p.symbols.values()) |*entry| entry.deinit(gpa); - p.symbols.deinit(gpa); - p.adjacent.deinit(gpa); - p.files.deinit(gpa); - p.* = undefined; -} - -fn emitRet( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Ret, -) !void { - log.debug("emitting ret", .{}); - try p.program.append(gpa, .{ - .opcode = .ret, - .data = .{ .ret = params }, - }); -} -fn writeJmp( - p: *Parser, - location: u32, - params: Instruction.Data.Jmp, -) !void { - log.debug("writing jmp over {x:0>8} to {x:0>8}", .{ - location, - params.address, - }); - p.program.set(location, .{ - .opcode = .jmp, - .data = .{ .jmp = params }, - }); -} -fn emitCall( - p: *Parser, - gpa: Allocator, - tag: []const u8, - params: Instruction.Data.Call, -) !void { - log.debug("emitting call to {s}", .{tag}); - const result = try p.symbols.getOrPut(gpa, tag); - if (!result.found_existing) { - result.value_ptr.* = .{}; - } - - try result.value_ptr.append(gpa, @intCast(u32, p.program.len)); - - try p.program.append(gpa, .{ - .opcode = .call, - .data = .{ .call = params }, - }); -} -fn emitShell( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Shell, -) !void { - log.debug("emitting shell command", .{}); - try p.program.append(gpa, .{ - .opcode = .shell, - .data = .{ .shell = params }, - }); -} -fn emitWrite( - p: *Parser, - gpa: Allocator, - params: Instruction.Data.Write, -) !void { - log.debug("emitting write {x:0>8} len {d} nl {d}", .{ - params.start, - params.len, - params.nl, - }); - try p.program.append(gpa, .{ - .opcode = .write, - .data = .{ .write = params }, - }); -} - -const Loc = std.builtin.SourceLocation; - -pub fn eat(p: *Parser, tag: Token.Tag, loc: Loc) ?[]const u8 { - const state = p.it; - const token = p.it.next(); - if (token.tag == tag) { - return token.slice(p.it.bytes); - } else { - log.debug("I'm starving for a '{s}' but this is a '{s}' ({s} {d}:{d})", .{ - @tagName(tag), - @tagName(token.tag), - loc.fn_name, - loc.line, - loc.column, - }); - p.it = state; - return null; - } -} - -pub fn next(p: *Parser) ?Token { - const token = p.it.next(); - if (token.tag != .eof) { - return token; - } else { - return null; - } -} - -pub fn scan(p: *Parser, tag: Token.Tag) ?Token { - while (p.next()) |token| if (token.tag == tag) { - return token; - }; - - return null; -} - -pub fn expect(p: *Parser, tag: Token.Tag, loc: Loc) ?void { - _ = p.eat(tag, loc) orelse { - log.debug("Wanted a {s}, but got nothing captain ({s} {d}:{d})", .{ - @tagName(tag), - loc.fn_name, - loc.line, - loc.column, - }); - return null; - }; -} - -pub fn slice(p: *Parser, from: usize, to: usize) []const u8 { - assert(from <= to); - return p.it.bytes[from..to]; -} - -pub fn match(p: *Parser, tag: Token.Tag, text: []const u8) ?void { - const state = p.it; - const token = p.it.next(); - if (token.tag == tag and mem.eql(u8, token.slice(p.it.bytes), text)) { - return; - } else { - p.it = state; - return null; - } -} -const Header = struct { - language: []const u8, - delimiter: ?[]const u8, - resource: Slice, - type: Type, - - pub const Slice = struct { - start: u32, - len: u16, - - pub fn slice(self: Slice, text: []const u8) []const u8 { - return text[self.start .. self.start + self.len]; - } - }; - - pub const Type = enum { file, tag }; -}; - -const ParseHeaderError = error{ - @"Expected a space between 'lang:' and the language name", - @"Expected a space after the language name", - @"Expected a space between 'esc:' and the delimiter specification", - @"Expected open delimiter", - @"Expected closing delimiter", - @"Expected matching closing angle bracket '>'", - @"Expected matching closing brace '}'", - @"Expected matching closing bracket ']'", - @"Expected matching closing paren ')'", - @"Expected opening and closing delimiter lengths to match", - @"Expected a space after delimiter specification", - @"Expected 'tag:' or 'file:' following delimiter specification", - @"Expected a space after 'file:'", - @"Expected a space after 'tag:'", - @"Expected a newline after the header", - @"Expected the dividing line to be indented by 4 spaces", - @"Expected a dividing line of '-' of the same length as the header", - @"Expected the division line to be of the same length as the header", - @"Expected at least one blank line after the division line", - @"Expected there to be only one space but more were given", - - @"Missing language specification", - @"Missing ':' after 'lang'", - @"Missing language name", - @"Missing 'esc:' delimiter specification", - @"Missing ':' after 'esc'", - @"Missing ':' after 'file'", - @"Missing ':' after 'tag'", - @"Missing '#' after 'tag: '", - @"Missing file name", - @"Missing tag name", - - @"Invalid delimiter, expected one of '<', '{', '[', '('", - @"Invalid delimiter, expected one of '>', '}', ']', ')'", - @"Invalid option given, expected 'tag:' or 'file:'", - @"Invalid file path, parent directory references '../' and '..\\' are not allowed within output paths", - @"Invalid file path, current directory references './' and '.\\' are not allowed within output paths", -}; - -fn parseHeaderLine(p: *Parser) ParseHeaderError!Header { - var header: Header = undefined; - - const header_start = p.it.index; - p.match(.word, "lang") orelse return error.@"Missing language specification"; - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'lang'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space between 'lang:' and the language name"; - } - - header.language = p.eat(.word, @src()) orelse return error.@"Missing language name"; - p.expect(.space, @src()) orelse return error.@"Expected a space after the language name"; - p.match(.word, "esc") orelse return error.@"Missing 'esc:' delimiter specification"; - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'esc'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space between 'esc:' and the delimiter specification"; - } - - if (p.match(.word, "none") == null) { - const start = p.it.index; - const open = p.next() orelse return error.@"Expected open delimiter"; - - switch (open.tag) { - .l_angle, .l_brace, .l_bracket, .l_paren => {}, - else => return error.@"Invalid delimiter, expected one of '<', '{', '[', '('", - } - - const closed = p.next() orelse return error.@"Expected closing delimiter"; - switch (closed.tag) { - .r_angle, .r_brace, .r_bracket, .r_paren => {}, - else => return error.@"Invalid delimiter, expected one of '>', '}', ']', ')'", - } - - if (open.tag == .l_angle and closed.tag != .r_angle) { - return error.@"Expected matching closing angle bracket '>'"; - } else if (open.tag == .l_brace and closed.tag != .r_brace) { - return error.@"Expected matching closing brace '}'"; - } else if (open.tag == .l_bracket and closed.tag != .r_bracket) { - return error.@"Expected matching closing bracket ']'"; - } else if (open.tag == .l_paren and closed.tag != .r_paren) { - return error.@"Expected matching closing paren ')'"; - } - - if (open.len() != closed.len()) { - return error.@"Expected opening and closing delimiter lengths to match"; - } - - header.delimiter = p.slice(start, p.it.index); - } else { - header.delimiter = null; - } - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else { - return error.@"Expected a space after delimiter specification"; - } - - var start: usize = undefined; - const tag = p.eat(.word, @src()) orelse { - return error.@"Expected 'tag:' or 'file:' following delimiter specification"; - }; - - if (mem.eql(u8, tag, "file")) { - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'file'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else return error.@"Expected a space after 'file:'"; - - header.type = .file; - start = p.it.index; - } else if (mem.eql(u8, tag, "tag")) { - p.expect(.colon, @src()) orelse return error.@"Missing ':' after 'tag'"; - - if (p.eat(.space, @src())) |space| { - if (space.len != 1) return error.@"Expected there to be only one space but more were given"; - } else return error.@"Expected a space after 'tag:'"; - - p.expect(.hash, @src()) orelse return error.@"Missing '#' after 'tag: '"; - header.type = .tag; - start = p.it.index; - } else { - return error.@"Invalid option given, expected 'tag:' or 'file:'"; - } - - const nl = p.scan(.nl) orelse { - return error.@"Expected a newline after the header"; - }; - - header.resource = .{ - .start = @intCast(u32, start), - .len = @intCast(u16, nl.start - start), - }; - const resource = header.resource.slice(p.it.bytes); - - if (header.type == .file) for (&[_][]const u8{ "../", "..\\" }) |invalid| { - if (mem.indexOf(u8, resource, invalid)) |index| { - if (index == 0 or resource[index - 1] != '.') { - return error.@"Invalid file path, parent directory references '../' and '..\\' are not allowed within output paths"; - } - } - }; - - if (header.type == .file) for (&[_][]const u8{ "./", ".\\" }) |invalid| { - if (mem.indexOf(u8, resource, invalid)) |index| { - if (index == 0 or resource[index - 1] != '.') { - return error.@"Invalid file path, current directory references './' and '.\\' are not allowed within output paths"; - } - } - }; - if (header.resource.len == 0) { - switch (header.type) { - .file => return error.@"Missing file name", - .tag => return error.@"Missing tag name", - } - } - - const len = (p.it.index - 1) - header_start; - - if ((p.eat(.space, @src()) orelse "").len != 4) { - return error.@"Expected the dividing line to be indented by 4 spaces"; - } - - const line = p.eat(.line, @src()) orelse { - return error.@"Expected a dividing line of '-' of the same length as the header"; - }; - - if (line.len != len) { - log.debug("header {d} line {d}", .{ len, line.len }); - return error.@"Expected the division line to be of the same length as the header"; - } - - if ((p.eat(.nl, @src()) orelse "").len < 2) { - return error.@"Expected at least one blank line after the division line"; - } - - return header; -} -fn parseBody(p: *Parser, gpa: Allocator, header: Header) !void { - log.debug("begin parsing body", .{}); - defer log.debug("end parsing body", .{}); - - const entry_point = @intCast(u32, p.program.len); - const location = p.it.locationFrom(p.location); - p.location = location; // avoid RLS - - var nl: usize = 0; - loop: while (p.eat(.space, @src())) |space| { - if (space.len < 4) break; - nl = 0; - - var sol = p.it.index - (space.len - 4); - while (true) { - const token = p.it.next(); - switch (token.tag) { - .eof => { - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = 0, - }); - break :loop; - }, - - .nl => { - nl = token.len(); - - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = @intCast(u16, nl), - }); - break; - }, - - .l_angle, - .l_brace, - .l_bracket, - .l_paren, - => if (header.delimiter) |delim| { - if (delim[0] != @enumToInt(token.tag)) { - log.debug("dilimiter doesn't match, skipping", .{}); - continue; - } - - if (delim.len != token.len() * 2) { - log.debug("dilimiter length doesn't match, skipping", .{}); - continue; - } - - if (token.start - sol > 0) { - try p.emitWrite(gpa, .{ - .start = @intCast(u32, sol), - .len = @intCast(u16, token.start - sol), - .nl = 0, - }); - } - - try p.parseDelimiter(gpa, delim, token.start - sol); - sol = p.it.index; - }, - - else => {}, - } - } - } - - const len = p.program.len; - if (len != 0) { - const item = &p.program.items(.data)[len - 1].write; - item.nl = 0; - if (item.len == 0) p.program.len -= 1; - } - - if (nl < 2 and p.it.index < p.it.bytes.len) { - return error.@"Expected a blank line after the end of the code block"; - } - - switch (header.type) { - .tag => { - const adj = try p.adjacent.getOrPut(gpa, header.resource.slice(p.it.bytes)); - if (adj.found_existing) { - try p.writeJmp(adj.value_ptr.exit, .{ - .address = entry_point, - .module = 0, - }); - } else { - adj.value_ptr.entry = entry_point; - adj.value_ptr.location = location; - } - - adj.value_ptr.exit = @intCast(u32, p.program.len); - }, - - .file => { - const file = try p.files.getOrPut(gpa, header.resource.slice(p.it.bytes)); - if (file.found_existing) return error.@"Multiple file outputs with the same name"; - file.value_ptr.* = .{ - .entry = entry_point, - .location = location, - }; - }, - } - - try p.emitRet(gpa, .{ - .start = header.resource.start, - .len = header.resource.len, - }); -} -fn parseDelimiter( - p: *Parser, - gpa: Allocator, - delim: []const u8, - indent: usize, -) !void { - log.debug("parsing call", .{}); - - var pipe = false; - var colon = false; - var reached_end = false; - - const tag = blk: { - const start = p.it.index; - while (p.next()) |sub| switch (sub.tag) { - .nl => return error.@"Unexpected newline", - .pipe => { - pipe = true; - break :blk p.it.bytes[start..sub.start]; - }, - .colon => { - colon = true; - break :blk p.it.bytes[start..sub.start]; - }, - - .r_angle, - .r_brace, - .r_bracket, - .r_paren, - => if (@enumToInt(sub.tag) == delim[delim.len - 1]) { - if (delim.len != sub.len() * 2) { - return error.@"Expected a closing delimiter of equal length"; - } - reached_end = true; - break :blk p.it.bytes[start..sub.start]; - }, - - else => {}, - }; - - return error.@"Unexpected end of file"; - }; - if (colon) { - const ty = p.eat(.word, @src()) orelse return error.@"Missing 'from' following ':'"; - if (!mem.eql(u8, ty, "from")) return error.@"Unknown type operation"; - p.expect(.l_paren, @src()) orelse return error.@"Expected '(' following 'from'"; - p.expect(.word, @src()) orelse return error.@"Expected type name"; - p.expect(.r_paren, @src()) orelse return error.@"Expected ')' following type name"; - } - if (pipe or p.eat(.pipe, @src()) != null) { - const index = @intCast(u32, p.it.index); - const shell = p.eat(.word, @src()) orelse { - return error.@"Missing command following '|'"; - }; - - if (shell.len > 255) return error.@"Shell command name too long"; - try p.emitShell(gpa, .{ - .command = index, - .module = 0xffff, - .len = @intCast(u8, shell.len), - .pad = 0, - }); - } - - try p.emitCall(gpa, tag, .{ - .address = undefined, - .module = undefined, - .indent = @intCast(u16, indent), - }); - - if (!reached_end) { - const last = p.next() orelse return error.@"Expected closing delimiter"; - - if (last.len() * 2 != delim.len) { - return error.@"Expected closing delimiter length to match"; - } - - if (@enumToInt(last.tag) != delim[delim.len - 1]) { - return error.@"Invalid closing delimiter"; - } - } -} - -test "parse body" { - const text = - \\ <> - \\ [[a b c | : a}} - \\ <> - \\ <> - \\ <<<|:>> - \\ <|:> - \\ - \\text - ; - - var p: Parser = .{ .it = .{ .bytes = text } }; - defer p.deinit(testing.allocator); - try p.parseBody(testing.allocator, .{ - .language = "", - .delimiter = "<<>>", - .resource = .{ .start = 0, .len = 0 }, - .type = .tag, - }); -} - -test "compile single tag" { - const text = - \\ <> - \\ <<. . .:from(zig)>> - \\ <<1 2 3|com>> - \\ - \\end - ; - - var p: Parser = .{ .it = .{ .bytes = text } }; - defer p.deinit(testing.allocator); - try p.parseBody(testing.allocator, .{ - .language = "", - .delimiter = "<<>>", - .resource = .{ .start = 0, .len = 0 }, - .type = .tag, - }); - - try testing.expect(p.symbols.contains("a b c")); - try testing.expect(p.symbols.contains("1 2 3")); - try testing.expect(p.symbols.contains(". . .")); -} - -pub fn parse(gpa: Allocator, name: []const u8, text: []const u8) !Linker.Object { - var p: Parser = .{ .it = .{ .bytes = text } }; - errdefer p.deinit(gpa); - - while (try p.step(gpa)) {} - - return Linker.Object{ - .name = name, - .text = text, - .program = p.program, - .symbols = p.symbols, - .adjacent = p.adjacent, - .files = p.files, - }; -} - -pub fn object(p: *Parser, name: []const u8) Linker.Object { - return Linker.Object{ - .name = name, - .text = p.it.bytes, - .program = p.program, - .symbols = p.symbols, - .adjacent = p.adjacent, - .files = p.files, - }; -} - -pub fn step(p: *Parser, gpa: Allocator) !bool { - while (p.next()) |token| if (token.tag == .nl and token.len() >= 2) { - const space = p.eat(.space, @src()) orelse continue; - if (space.len != 4) continue; - - if (p.parseHeaderLine()) |header| { - try p.parseBody(gpa, header); - } else |e| switch (e) { - error.@"Missing language specification" => { - log.debug("begin indented block", .{}); - defer log.debug("end indented block", .{}); - - while (p.scan(.nl)) |nl| if (nl.len() >= 2) { - const tmp = p.next() orelse return false; - if (tmp.tag != .space) return true; - if (tmp.len() < 4) return true; - }; - }, - - else => |err| return err, - } - }; - - return false; -} -test "parse header line" { - const complete_header = "lang: zig esc: {{}} tag: #hash\n ------------------------------\n\n"; - const common: Header = .{ - .language = "zig", - .delimiter = "{{}}", - .resource = .{ - .start = @intCast(u32, mem.indexOf(u8, complete_header, "hash").?), - .len = 4, - }, - .type = .tag, - }; - - try testing.expectError( - error.@"Expected a space between 'lang:' and the language name", - testParseHeader("lang:zig", common), - ); - - try testing.expectError( - error.@"Missing 'esc:' delimiter specification", - testParseHeader("lang: zig ", common), - ); - - try testing.expectError( - error.@"Missing ':' after 'esc'", - testParseHeader("lang: zig esc", common), - ); - - try testing.expectError( - error.@"Expected a space between 'esc:' and the delimiter specification", - testParseHeader("lang: zig esc:", common), - ); - - try testing.expectError( - error.@"Expected closing delimiter", - testParseHeader("lang: zig esc: {", common), - ); - - try testing.expectError( - error.@"Expected matching closing angle bracket '>'", - testParseHeader("lang: zig esc: <}", common), - ); - - try testing.expectError( - error.@"Expected matching closing brace '}'", - testParseHeader("lang: zig esc: {>", common), - ); - - try testing.expectError( - error.@"Expected matching closing bracket ']'", - testParseHeader("lang: zig esc: [>", common), - ); - - try testing.expectError( - error.@"Expected matching closing paren ')'", - testParseHeader("lang: zig esc: (>", common), - ); - - try testing.expectError( - error.@"Invalid delimiter, expected one of '<', '{', '[', '('", - testParseHeader("lang: zig esc: foo", common), - ); - - try testing.expectError( - error.@"Invalid delimiter, expected one of '>', '}', ']', ')'", - testParseHeader("lang: zig esc: > tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ - \\end - , .{ - .program = &.{ .call, .ret }, - .symbols = &.{"example"}, - .exports = &.{"here"}, - }); -} - -test "compile block with jump threadding" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ - \\then - \\ - \\ lang: zig esc: none tag: #here - \\ ------------------------------ - \\ - \\ more - \\ - \\end - , .{ - .program = &.{ .call, .jmp, .write, .ret }, - .symbols = &.{"example"}, - .exports = &.{"here"}, - }); -} - -test "compile block multiple call" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <> - \\ <> - \\ <> - \\ - \\end - , .{ - .program = &.{ .call, .write, .call, .write, .call, .ret }, - .symbols = &.{ "one", "two", "three" }, - .exports = &.{"here"}, - }); -} - -test "compile block inline" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <><> - \\ - \\end - , .{ - .program = &.{ .call, .call, .ret }, - .symbols = &.{ "one", "two" }, - .exports = &.{"here"}, - }); -} - -test "compile block inline indent" { - try testCompile( - \\begin - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ one<> - \\ - \\end - , .{ - .program = &.{ .write, .call, .ret }, - .symbols = &.{"two"}, - .exports = &.{"here"}, - }); -} - -test "compile indented" { - try testCompile( - \\begin - \\ - \\ normal code block - \\ - \\end - \\ - \\ lang: zig esc: <<>> tag: #here - \\ ------------------------------ - \\ - \\ <><> - \\ - \\end - , .{ - .program = &.{ .call, .call, .ret }, - .symbols = &.{ "one", "two" }, - .exports = &.{"here"}, - }); -} diff --git a/lib/TangleStep.zig b/lib/TangleStep.zig deleted file mode 100644 index 434b4e0..0000000 --- a/lib/TangleStep.zig +++ /dev/null @@ -1,163 +0,0 @@ -const std = @import("std"); -const lib = @import("lib.zig"); -const fs = std.fs; -const mem = std.mem; -const io = std.io; - -const TangleStep = @This(); -const Allocator = std.mem.Allocator; -const Builder = std.build.Builder; -const Step = std.build.Step; -const Parser = lib.Parser; -const Interpreter = lib.Interpreter; -const SourceList = std.TailQueue(Source); -const FileSource = std.build.FileSource; -const GeneratedFile = std.build.GeneratedFile; -const BufferedWriter = io.BufferedWriter(4096, fs.File.Writer); -const FileContext = lib.context.StreamContext(BufferedWriter.Writer); - -pub const FileList = std.ArrayListUnmanaged([]const u8); - -pub const Source = struct { - source: GeneratedFile, - path: []const u8, -}; - -const log = std.log.scoped(.tangle_step); - -vm: Interpreter = .{}, -output_dir: ?[]const u8 = null, -builder: *Builder, -files: FileList = .{}, -sources: SourceList = .{}, -step: Step, - -pub fn create(b: *Builder) *TangleStep { - const self = b.allocator.create(TangleStep) catch @panic("Out of memory"); - self.* = .{ - .builder = b, - .step = Step.init(.custom, "tangle", b.allocator, make), - }; - return self; -} - -pub fn addFile(self: *TangleStep, path: []const u8) void { - self.files.append(self.builder.allocator, self.builder.dupe(path)) catch @panic( - \\Out of memory - ); -} - -pub fn getFileSource(self: *TangleStep, path: []const u8) FileSource { - var it = self.sources.first; - while (it) |node| : (it = node.next) { - if (std.mem.eql(u8, node.data.path, path)) - return FileSource{ .generated = &node.data.source }; - } - - const node = self.builder.allocator.create(SourceList.Node) catch @panic( - \\Out of memory - ); - node.* = .{ - .data = .{ - .source = .{ .step = &self.step }, - .path = self.builder.dupe(path), - }, - }; - - self.sources.append(node); - - return FileSource{ .generated = &node.data.source }; -} - -fn make(step: *Step) anyerror!void { - const self = @fieldParentPtr(TangleStep, "step", step); - - var hash = std.crypto.hash.blake2.Blake2b384.init(.{}); - - for (self.files.items) |path| { - const text = try fs.cwd().readFileAlloc(self.builder.allocator, path, 0x7fff_ffff); - var p: Parser = .{ .it = .{ .bytes = text } }; - while (p.step(self.builder.allocator)) |working| { - if (!working) break; - } else |err| { - const location = p.it.locationFrom(.{}); - log.err("line {d} col {d}: {s}", .{ - location.line, - location.column, - @errorName(err), - }); - - @panic("Failed parsing module"); - } - - hash.update(path); - hash.update(text); - - const object = p.object(path); - try self.vm.linker.objects.append(self.builder.allocator, object); - } - - try self.vm.linker.link(self.builder.allocator); - - var digest: [48]u8 = undefined; - hash.final(&digest); - - var basename: [64]u8 = undefined; - _ = std.fs.base64_encoder.encode(&basename, &digest); - - if (self.output_dir == null) { - self.output_dir = try fs.path.join(self.builder.allocator, &.{ - self.builder.cache_root, - "o", - &basename, - }); - } - - try fs.cwd().makePath(self.output_dir.?); - - var dir = try fs.cwd().openDir(self.output_dir.?, .{}); - defer dir.close(); - - for (self.vm.linker.files.keys()) |path| { - if (path.len > 2 and mem.eql(u8, path[0..2], "~/")) { - return error.@"Absolute paths are not allowed"; - } else if (mem.indexOf(u8, path, "../") != null) { - return error.@"paths containing ../ are not allowed"; - } - - if (fs.path.dirname(path)) |sub| try dir.makePath(sub); - - const file = try dir.createFile(path, .{ .truncate = true }); - defer file.close(); - - var buffered: BufferedWriter = .{ .unbuffered_writer = file.writer() }; - const writer = buffered.writer(); - var context = FileContext.init(writer); - try self.vm.callFile(self.builder.allocator, path, *FileContext, &context); - try context.stream.writeByte('\n'); - try buffered.flush(); - - var it = self.sources.first; - while (it) |node| : (it = node.next) { - if (mem.eql(u8, node.data.path, path)) { - self.sources.remove(node); - node.data.source.path = try fs.path.join( - self.builder.allocator, - &.{ self.output_dir.?, node.data.path }, - ); - break; - } - } - } - - if (self.sources.first) |node| { - log.err("file not found: {s}", .{node.data.path}); - var it = node.next; - - while (it) |next| { - log.err("file not found: {s}", .{next.data.path}); - } - - @panic("Files not found"); - } -} diff --git a/lib/Tokenizer.zig b/lib/Tokenizer.zig deleted file mode 100644 index 8369667..0000000 --- a/lib/Tokenizer.zig +++ /dev/null @@ -1,181 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const testing = std.testing; -const assert = std.debug.assert; - -const Tokenizer = @This(); - -bytes: []const u8, -index: usize = 0, - -pub const Location = struct { - line: usize = 1, - column: usize = 1, -}; - -const log = std.log.scoped(.tokenizer); - -pub const Token = struct { - tag: Tag, - start: usize, - end: usize, - - pub const Tag = enum(u8) { - eof, - - nl = '\n', - space = ' ', - - word, - line = '-', - hash = '#', - pipe = '|', - colon = ':', - - l_angle = '<', - l_brace = '{', - l_bracket = '[', - l_paren = '(', - - r_angle = '>', - r_brace = '}', - r_bracket = ']', - r_paren = ')', - - unknown, - }; - - pub fn slice(t: Token, bytes: []const u8) []const u8 { - return bytes[t.start..t.end]; - } - - pub fn len(t: Token) usize { - return t.end - t.start; - } -}; - -pub fn locationFrom(self: Tokenizer, from: Location) Location { - assert(from.line != 0); - assert(from.column != 0); - - var loc = from; - const start = from.line * from.column - 1; - - for (self.bytes[start..self.index]) |byte| { - if (byte == '\n') { - loc.line += 1; - loc.column = 1; - } else { - loc.column += 1; - } - } - - return loc; -} - -pub fn next(self: *Tokenizer) Token { - var token: Token = .{ - .tag = .eof, - .start = self.index, - .end = undefined, - }; - - defer log.debug("{s: >10} {d: >3} | {s}", .{ - @tagName(token.tag), - token.len(), - token.slice(self.bytes), - }); - - const State = enum { start, trivial, unknown, word }; - var state: State = .start; - var trivial: u8 = 0; - - while (self.index < self.bytes.len) : (self.index += 1) { - const c = self.bytes[self.index]; - switch (state) { - .start => switch (c) { - ' ', '\n' => { - token.tag = @intToEnum(Token.Tag, c); - trivial = c; - state = .trivial; - }, - '-' => { - token.tag = .line; - trivial = '-'; - state = .trivial; - }, - - 'a'...'z' => { - token.tag = .word; - state = .word; - }, - - '#', ':' => { - token.tag = @intToEnum(Token.Tag, c); - self.index += 1; - break; - }, - '<', '{', '[', '(', ')', ']', '}', '>' => { - token.tag = @intToEnum(Token.Tag, c); - trivial = c; - state = .trivial; - }, - '|' => { - token.tag = .pipe; - self.index += 1; - break; - }, - else => { - token.tag = .unknown; - state = .unknown; - }, - }, - - .trivial => if (c != trivial) break, - .word => switch (c) { - 'a'...'z', 'A'...'Z', '#', '+', '-', '\'', '_' => {}, - else => break, - }, - .unknown => if (mem.indexOfScalar(u8, "\n <{[()]}>:|", c)) |_| { - break; - }, - } - } - - token.end = self.index; - return token; -} - -test "tokenize whitespace" { - try testTokenize("\n", &.{.nl}); - try testTokenize(" ", &.{.space}); - try testTokenize("\n\n\n\n\n", &.{.nl}); - try testTokenize("\n\n \n\n\n", &.{ .nl, .space, .nl }); -} -test "tokenize header" { - try testTokenize("-", &.{.line}); - try testTokenize("#", &.{.hash}); - try testTokenize(":", &.{.colon}); - try testTokenize("-----------------", &.{.line}); - try testTokenize("###", &.{ .hash, .hash, .hash }); - try testTokenize(":::", &.{ .colon, .colon, .colon }); -} -test "tokenize include" { - try testTokenize("|", &.{.pipe}); - try testTokenize("|||", &.{ .pipe, .pipe, .pipe }); -} -test "tokenize unknown" { - try testTokenize("/file.example/path/../__", &.{.unknown}); -} -fn testTokenize(text: []const u8, expected: []const Token.Tag) !void { - var it: Tokenizer = .{ .bytes = text }; - - for (expected) |tag| { - const token = it.next(); - try testing.expectEqual(tag, token.tag); - } - - const token = it.next(); - try testing.expectEqual(Token.Tag.eof, token.tag); - try testing.expectEqual(text.len, token.end); -} diff --git a/lib/context.zig b/lib/context.zig deleted file mode 100644 index 4a51759..0000000 --- a/lib/context.zig +++ /dev/null @@ -1,27 +0,0 @@ -const lib = @import("lib.zig"); - -const Interpreter = lib.Interpreter; - -pub fn StreamContext(comptime Writer: type) type { - return struct { - stream: Writer, - - const Self = @This(); - - pub const Error = Writer.Error; - - pub fn init(writer: Writer) Self { - return .{ .stream = writer }; - } - - pub fn write(self: *Self, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - try self.stream.writeAll(text); - try self.stream.writeByteNTimes('\n', nl); - } - - pub fn indent(self: *Self, vm: *Interpreter) !void { - try self.stream.writeByteNTimes(' ', vm.indent); - } - }; -} diff --git a/lib/lib.zig b/lib/lib.zig deleted file mode 100644 index 5f47106..0000000 --- a/lib/lib.zig +++ /dev/null @@ -1,15 +0,0 @@ -pub const Tokenizer = @import("Tokenizer.zig"); -pub const Parser = @import("Parser.zig"); -pub const Linker = @import("Linker.zig"); -pub const Instruction = @import("Instruction.zig"); -pub const Interpreter = @import("Interpreter.zig"); -pub const context = @import("context.zig"); -pub const TangleStep = @import("TangleStep.zig"); - -test { - _ = Tokenizer; - _ = Parser; - _ = Linker; - _ = Instruction; - _ = Interpreter; -} diff --git a/lib/wasm.zig b/lib/wasm.zig deleted file mode 100644 index 6bede11..0000000 --- a/lib/wasm.zig +++ /dev/null @@ -1,73 +0,0 @@ -const std = @import("std"); -const lib = @import("lib.zig"); - -const Interpreter = lib.Interpreter; -const Parser = lib.Parser; -const ArrayList = std.ArrayList; - -var vm: Interpreter = .{}; -var instance = std.heap.GeneralPurposeAllocator(.{}){}; -var output: ArrayList(u8) = undefined; -const gpa = instance.allocator(); - -pub export fn init() void { - output = ArrayList(u8).init(gpa); -} - -pub export fn add(text: [*]const u8, len: usize) i32 { - const slice = text[0..len]; - return addInternal(slice) catch -1; -} - -fn addInternal(text: []const u8) !i32 { - var obj = try Parser.parse(gpa, "", text); - errdefer obj.deinit(gpa); - try vm.linker.objects.append(gpa, obj); - return @intCast(i32, vm.linker.objects.items.len - 1); -} - -pub export fn update(id: u32, text: [*]const u8, len: usize) i32 { - const slice = text[0..len]; - updateInternal(id, slice) catch return -1; - return 0; -} - -fn updateInternal(id: u32, text: []const u8) !void { - if (id >= vm.linker.objects.items.len) return error.@"Id out of range"; - const obj = try Parser.parse(gpa, "", text); - gpa.free(vm.linker.objects.items[id].text); - vm.linker.objects.items[id].deinit(gpa); - vm.linker.objects.items[id] = obj; -} - -pub export fn link() i32 { - vm.linker.link(gpa) catch return -1; - return 0; -} - -pub export fn call(name: [*]const u8, len: usize) i32 { - vm.call(gpa, name[0..len], Render, .{}) catch return -1; - return 0; -} - -pub export fn reset() void { - for (vm.linker.objects.items) |obj| gpa.free(obj.text); - vm.deinit(gpa); - vm = .{}; -} - -const Render = struct { - pub const Error = @TypeOf(output).Writer.Error; - - pub fn write(_: Render, v: *Interpreter, text: []const u8, nl: u16) !void { - _ = v; - const writer = output.writer(); - try writer.writeAll(text); - try writer.writeByteNTimes('\n', nl); - } - - pub fn indent(_: Render, v: *Interpreter) !void { - const writer = output.writer(); - try writer.writeByteNTimes(' ', v.indent); - } -}; diff --git a/out/.keep b/out/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/FindContext.zig b/src/FindContext.zig deleted file mode 100644 index f49e4e0..0000000 --- a/src/FindContext.zig +++ /dev/null @@ -1,82 +0,0 @@ -const std = @import("std"); -const lib = @import("lib"); -const io = std.io; -const fs = std.fs; -const mem = std.mem; - -const ArrayList = std.ArrayListUnmanaged; -const Allocator = std.mem.Allocator; -const Interpreter = lib.Interpreter; -const FindContext = @This(); - -stream: Stream, -line: u32 = 1, -column: u32 = 1, -stack: Stack = .{}, -filename: []const u8, -tag: []const u8, -gpa: Allocator, - -const log = std.log.scoped(.find_context); - -pub const Error = error{OutOfMemory} || std.os.WriteError; - -pub const Stream = io.BufferedWriter(1024, std.fs.File.Writer); - -pub const Stack = ArrayList(Location); - -pub const Location = struct { - line: u32, - column: u32, -}; - -pub fn init(gpa: Allocator, file: []const u8, tag: []const u8, writer: fs.File.Writer) FindContext { - return .{ - .stream = .{ .unbuffered_writer = writer }, - .filename = file, - .tag = tag, - .gpa = gpa, - }; -} - -pub fn write(self: *FindContext, vm: *Interpreter, text: []const u8, nl: u16) !void { - _ = vm; - if (nl == 0) { - self.column += @intCast(u32, text.len); - } else { - self.line += @intCast(u32, nl); - self.column = @intCast(u32, text.len + 1); - } -} - -pub fn call(self: *FindContext, vm: *Interpreter) !void { - _ = vm; - - try self.stack.append(self.gpa, .{ - .line = self.line, - .column = self.column, - }); -} - -pub fn ret(self: *FindContext, vm: *Interpreter, name: []const u8) !void { - _ = name; - - const writer = self.stream.writer(); - const location = self.stack.pop(); - const procedure = vm.linker.procedures.get(name).?; - const obj = vm.linker.objects.items[procedure.module - 1]; - - if (mem.eql(u8, self.tag, name)) try writer.print( - \\{s}: line {d} column {d} '{s}' -> line {d} column {d} '{s}' ({d} lines) - \\ - , .{ - self.tag, - procedure.location.line, - procedure.location.column, - obj.name, - location.line, - location.column, - self.filename, - self.line - location.line, - }); -} diff --git a/src/GraphContext.zig b/src/GraphContext.zig deleted file mode 100644 index 70d08fd..0000000 --- a/src/GraphContext.zig +++ /dev/null @@ -1,201 +0,0 @@ -const std = @import("std"); -const lib = @import("lib"); -const io = std.io; -const fs = std.fs; -const assert = std.debug.assert; - -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayListUnmanaged; -const HashMap = std.AutoHashMapUnmanaged; -const Interpreter = lib.Interpreter; -const GraphContext = @This(); - -stream: Stream, -stack: Stack = .{}, -omit: Omit = .{}, -gpa: Allocator, -colour: u8 = 0, -target: Target = .{}, -text_colour: u24 = 0, -inherit: bool = false, -colours: []const u24 = &.{}, -gradient: u8 = 5, - -pub const Error = error{OutOfMemory} || std.os.WriteError; - -pub const Stack = ArrayList(Layer); -pub const Layer = struct { - list: ArrayList([]const u8) = .{}, -}; - -pub const Target = HashMap([*]const u8, u8); - -pub const Omit = HashMap(Pair, void); -pub const Pair = struct { - from: [*]const u8, - to: [*]const u8, -}; - -pub const Stream = io.BufferedWriter(1024, std.fs.File.Writer); - -pub fn init(gpa: Allocator, writer: fs.File.Writer) GraphContext { - return .{ - .stream = .{ .unbuffered_writer = writer }, - .gpa = gpa, - }; -} - -pub const GraphOptions = struct { - border: u24 = 0, - background: u24 = 0, - text: u24 = 0, - colours: []const u24 = &.{}, - inherit: bool = false, - gradient: u8 = 0, -}; - -pub fn begin(self: *GraphContext, options: GraphOptions) !void { - try self.stream.writer().print( - \\graph G {{ - \\ bgcolor = "#{[background]x:0>6}"; - \\ overlap = false; - \\ rankdir = LR; - \\ concentrate = true; - \\ node[shape = rectangle, color = "#{[border]x:0>6}"]; - \\ - , .{ - .background = options.background, - .border = options.border, - }); - - try self.stack.append(self.gpa, .{}); - - self.colours = options.colours; - self.text_colour = options.text; - self.inherit = options.inherit; - self.gradient = options.gradient; -} - -pub fn end(self: *GraphContext) !void { - try self.stream.writer().writeAll("}\n"); - try self.stream.flush(); -} - -pub fn call(self: *GraphContext, vm: *Interpreter) !void { - _ = vm; - try self.stack.append(self.gpa, .{}); -} - -pub fn ret(self: *GraphContext, vm: *Interpreter, name: []const u8) !void { - _ = vm; - - try self.render(name); - - var old = self.stack.pop(); - old.list.deinit(self.gpa); - - try self.stack.items[self.stack.items.len - 1].list.append(self.gpa, name); -} - -pub fn terminate(self: *GraphContext, vm: *Interpreter, name: []const u8) !void { - _ = vm; - try self.render(name); - - self.stack.items[0].list.clearRetainingCapacity(); - - assert(self.stack.items.len == 1); -} - -fn render(self: *GraphContext, name: []const u8) !void { - const writer = self.stream.writer(); - const sub_nodes = self.stack.items[self.stack.items.len - 1].list.items; - - var valid: usize = 0; - for (sub_nodes) |sub| { - if (!self.omit.contains(.{ .from = name.ptr, .to = sub.ptr })) { - valid += 1; - } - } - - const theme = try self.target.getOrPut(self.gpa, name.ptr); - if (!theme.found_existing) { - theme.value_ptr.* = self.colour; - defer self.colour +%= 1; - - const selected = if (self.colours.len == 0) - self.colour - else - self.colours[self.colour % self.colours.len]; - - if (self.inherit) { - try writer.print( - \\ "{[name]s}"[fontcolor = "#{[colour]x:0>6}", color = "#{[inherit]x:0>6}"]; - \\ - , .{ - .name = name, - .colour = self.text_colour, - .inherit = selected, - }); - } else { - try writer.print( - \\ "{[name]s}"[fontcolor = "#{[colour]x:0>6}"]; - \\ - , .{ - .name = name, - .colour = self.text_colour, - }); - } - } - - for (sub_nodes) |sub| { - const entry = try self.omit.getOrPut(self.gpa, .{ - .from = name.ptr, - .to = sub.ptr, - }); - - if (!entry.found_existing) { - const to = self.target.get(sub.ptr).?; - const from = self.target.get(name.ptr).?; - - const selected: struct { from: u24, to: u24 } = if (self.colours.len == 0) .{ - .from = 0, - .to = 0, - } else .{ - .from = self.colours[from % self.colours.len], - .to = self.colours[to % self.colours.len], - }; - - try writer.print( - \\ "{s}" -- "{s}" [color = " - , .{ name, sub }); - - if (self.gradient != 0) { - var i: i24 = 0; - const r: i32 = @truncate(u8, selected.from >> 16); - const g: i32 = @truncate(u8, selected.from >> 8); - const b: i32 = @truncate(u8, selected.from); - - const x: i32 = @truncate(u8, selected.to >> 16); - const y: i32 = @truncate(u8, selected.to >> 8); - const z: i32 = @truncate(u8, selected.to); - - const dx = @divTrunc(x - r, self.gradient); - const gy = @divTrunc(y - g, self.gradient); - const bz = @divTrunc(z - b, self.gradient); - - while (i < self.gradient) : (i += 1) { - const red = r + dx * i; - const green = g + gy * i; - const blue = b + bz * i; - const rgb = @bitCast(u24, @truncate(i24, red << 16 | (green << 8) | (blue & 0xff))); - try writer.print("#{x:0>6};{d}:", .{ rgb, 1.0 / @intToFloat(f64, self.gradient) }); - } - } - - try writer.print( - \\#{x:0>6}"]; - \\ - , .{selected.to}); - } - } -} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 7b0af11..0000000 --- a/src/main.zig +++ /dev/null @@ -1,577 +0,0 @@ -const std = @import("std"); -const lib = @import("lib"); -const mem = std.mem; -const assert = std.debug.assert; -const testing = std.testing; -const meta = std.meta; -const fs = std.fs; -const fmt = std.fmt; -const io = std.io; -const os = std.os; -const math = std.math; -const stdout = io.getStdOut().writer(); -const stdin = io.getStdIn().reader(); - -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayListUnmanaged; -const HashMap = std.AutoArrayHashMapUnmanaged; -const MultiArrayList = std.MultiArrayList; -const Tokenizer = lib.Tokenizer; -const Parser = lib.Parser; -const Linker = lib.Linker; -const Instruction = lib.Instruction; -const Interpreter = lib.Interpreter; -const GraphContext = @import("GraphContext.zig"); -const FindContext = @import("FindContext.zig"); -const BufferedWriter = io.BufferedWriter(4096, fs.File.Writer); -const FileContext = lib.context.StreamContext(BufferedWriter.Writer); - -pub const log_level = .info; - -const Options = struct { - allow_absolute_paths: bool = false, - omit_trailing_newline: bool = false, - list_files: bool = false, - list_tags: bool = false, - calls: []const FileOrTag = &.{}, - graph_text_colour: u24 = 0x000000, - graph_background_colour: u24 = 0xffffff, - graph_border_colour: u24 = 0x92abc9, - graph_inherit_line_colour: bool = false, - graph_line_gradient: u8 = 5, - graph_colours: []const u24 = &.{ - 0xdf4d77, - 0x2288ed, - 0x94bd76, - 0xc678dd, - 0x61aeee, - 0xe3bd79, - }, - command: Command, - files: []const []const u8 = &.{}, - - pub const FileOrTag = union(enum) { - file: []const u8, - tag: []const u8, - }; -}; - -const Command = enum { - help, - tangle, - ls, - call, - graph, - find, - init, - - pub const map = std.ComptimeStringMap(Command, .{ - .{ "help", .help }, - .{ "tangle", .tangle }, - .{ "ls", .ls }, - .{ "call", .call }, - .{ "graph", .graph }, - .{ "find", .find }, - .{ "init", .init }, - }); -}; - -const Flag = enum { - allow_absolute_paths, - omit_trailing_newline, - file, - tag, - list_tags, - list_files, - graph_border_colour, - graph_inherit_line_colour, - graph_colours, - graph_background_colour, - graph_line_gradient, - graph_text_colour, - @"--", - stdin, - - pub const map = std.ComptimeStringMap(Flag, .{ - .{ "--allow-absolute-paths", .allow_absolute_paths }, - .{ "--omit-trailing-newline", .omit_trailing_newline }, - .{ "--file=", .file }, - .{ "--tag=", .tag }, - .{ "--list-tags", .list_tags }, - .{ "--list-files", .list_files }, - .{ "--graph-border-colour=", .graph_border_colour }, - .{ "--graph-colours=", .graph_colours }, - .{ "--graph-background-colour=", .graph_background_colour }, - .{ "--graph-text-colour=", .graph_text_colour }, - .{ "--graph-inherit-line-colour", .graph_inherit_line_colour }, - .{ "--graph-line-gradient=", .graph_line_gradient }, - .{ "--", .@"--" }, - .{ "--stdin", .stdin }, - }); -}; - -const tangle_help = - \\Usage: zangle tangle [options] [files] - \\ - \\ --allow-absolute-paths Allow writing file blocks with absolute paths - \\ --omit-trailing-newline Do not print a trailing newline at the end of a file block -; - -const ls_help = - \\Usage: zangle ls [files] - \\ - \\ --list-files (default) List all file output paths in the document - \\ --list-tags List all tags in the document -; - -const call_help = - \\Usage: zangle call [options] [files] - \\ - \\ --file=[filepath] Render file block to stdout - \\ --tag=[tagname] Render tag block to stdout -; - -const find_help = - \\Usage: zangle find [options] [files] - \\ - \\ --tag=[tagname] Find the location of the given tag in the literate document and output files -; - -const graph_help = - \\Usage: zangle graph [files] - \\ - \\ --file=[filepath] Render the graph for the given file - \\ --graph-border=[#rrggbb] Set item border colour - \\ --graph-colours=[#rrggbb,...] Set spline colours - \\ --graph-background-colour=[#rrggbb] Set the background colour of the graph - \\ --graph-text-colour=[#rrggbb] Set node label text colour - \\ --graph-inherit-line-colour Borders inherit their colour from the choden line colour - \\ --graph-line-gradient=[number] Set the gradient level -; - -const init_help = - \\Usage: zangle init [files] - \\ --stdin Read file names from stdin -; - -const log = std.log; - -fn helpGeneric() void { - log.info( - \\{s} - \\ - \\{s} - \\ - \\{s} - \\ - \\{s} - \\ - \\{s} - , .{ - tangle_help, - ls_help, - call_help, - graph_help, - init_help, - }); -} - -fn help(com: ?Command, name: ?[]const u8) void { - const command = com orelse { - helpGeneric(); - log.err("I don't know how to handle the given command '{s}'", .{name.?}); - return; - }; - - switch (command) { - .help => helpGeneric(), - .tangle => log.info(tangle_help, .{}), - .ls => log.info(ls_help, .{}), - .call => log.info(call_help, .{}), - .graph => log.info(graph_help, .{}), - .find => log.info(find_help, .{}), - .init => log.info(init_help, .{}), - } -} - -fn parseCli(gpa: Allocator, objects: *Linker.Object.List) !?Options { - var options: Options = .{ .command = undefined }; - const args = os.argv; - - if (args.len < 2) { - help(.help, null); - return error.@"Missing command name"; - } - - const command_name = mem.sliceTo(args[1], 0); - const command = Command.map.get(command_name); - - if (args.len < 3 or command == null or command.? == .help) { - help(command, command_name); - if (command) |com| { - switch (com) { - .help => return null, - else => return error.@"Not enough arguments", - } - } else { - return error.@"Invalid command"; - } - } - - var interpret_flags_as_files: bool = false; - var calls = std.ArrayList(Options.FileOrTag).init(gpa); - var files = std.ArrayList([]const u8).init(gpa); - var graph_colours = std.ArrayList(u24).init(gpa); - var graph_colours_set = false; - var files_on_stdin = false; - - options.command = command.?; - - for (args[2..]) |arg0| { - const arg = mem.sliceTo(arg0, 0); - if (arg.len == 0) return error.@"Zero length argument"; - - if (arg[0] == '-' and !interpret_flags_as_files) { - errdefer log.err("I don't know how to parse the given option '{s}'", .{arg}); - - log.debug("processing {s} flag '{s}'", .{ @tagName(options.command), arg }); - - const split = (mem.indexOfScalar(u8, arg, '=') orelse (arg.len - 1)) + 1; - const flag = Flag.map.get(arg[0..split]) orelse { - return error.@"Unknown option"; - }; - - switch (options.command) { - .help => unreachable, - - .ls => switch (flag) { - .list_files => options.list_files = true, - .list_tags => options.list_tags = true, - else => return error.@"Unknown command-line flag", - }, - - .call => switch (flag) { - .file => try calls.append(.{ .file = arg[split..] }), - .tag => try calls.append(.{ .tag = arg[split..] }), - else => return error.@"Unknown command-line flag", - }, - - .find => switch (flag) { - .tag => try calls.append(.{ .tag = arg[split..] }), - else => return error.@"Unknown command-line flag", - }, - - .graph => switch (flag) { - .file => try calls.append(.{ .file = arg[split..] }), - .graph_border_colour => options.graph_border_colour = try parseColour(arg[split..]), - .graph_background_colour => options.graph_background_colour = try parseColour(arg[split..]), - .graph_text_colour => options.graph_text_colour = try parseColour(arg[split..]), - .graph_inherit_line_colour => options.graph_inherit_line_colour = true, - .graph_line_gradient => options.graph_line_gradient = fmt.parseInt(u8, arg[split..], 10) catch { - return error.@"Invalid value specified, expected a number between 0-255 (inclusive)"; - }, - - .graph_colours => { - var it = mem.tokenize(u8, arg[split..], ","); - - while (it.next()) |item| { - try graph_colours.append(try parseColour(item)); - } - - graph_colours_set = true; - }, - - else => return error.@"Unknown command-line flag", - }, - - .tangle => switch (flag) { - .allow_absolute_paths => options.allow_absolute_paths = true, - .omit_trailing_newline => options.omit_trailing_newline = true, - .@"--" => interpret_flags_as_files = true, - else => return error.@"Unknown command-line flag", - }, - - .init => switch (flag) { - .stdin => files_on_stdin = true, - else => return error.@"Unknown command-line flag", - }, - } - } else if (options.command != .init) { - std.log.info("compiling {s}", .{arg}); - const text = try fs.cwd().readFileAlloc(gpa, arg, 0x7fff_ffff); - - var p: Parser = .{ .it = .{ .bytes = text } }; - - while (p.step(gpa)) |working| { - if (!working) break; - } else |err| { - const location = p.it.locationFrom(.{}); - log.err("line {d} col {d}: {s}", .{ - location.line, - location.column, - @errorName(err), - }); - - os.exit(1); - } - - const object = p.object(arg); - - objects.append(gpa, object) catch return error.@"Exhausted memory"; - } else { - files.append(arg) catch return error.@"Exhausted memory"; - } - } - - if (files_on_stdin) { - const err = error.@"Exhausted memory"; - while (stdin.readUntilDelimiterOrEofAlloc(gpa, '\n', fs.MAX_PATH_BYTES) catch return err) |path| { - files.append(path) catch return error.@"Exhausted memory"; - } - } - - if (options.command == .init and files.items.len == 0) { - return error.@"No files to import specified"; - } - - options.calls = calls.toOwnedSlice(); - options.files = files.toOwnedSlice(); - if (graph_colours_set) { - options.graph_colours = graph_colours.toOwnedSlice(); - } - return options; -} - -fn parseColour(text: []const u8) !u24 { - if (text.len == 7) { - if (text[0] != '#') return error.@"Invalid colour spexification, expected '#'"; - return fmt.parseInt(u24, text[1..], 16) catch error.@"Colour specification is not a valid 24-bit hex number"; - } else { - return error.@"Invalid hex colour specification length; expecting a 6 hex digit colour prefixed with a '#'"; - } -} - -pub fn main() void { - run() catch |err| { - log.err("{s}", .{@errorName(err)}); - os.exit(1); - }; -} - -pub fn run() !void { - var vm: Interpreter = .{}; - var instance = std.heap.GeneralPurposeAllocator(.{}){}; - const gpa = instance.allocator(); - - var options = (try parseCli(gpa, &vm.linker.objects)) orelse return; - - const n_objects = vm.linker.objects.items.len; - const plural: []const u8 = if (n_objects == 1) "object" else "objects"; - - log.info("linking {d} {s}...", .{ n_objects, plural }); - - try vm.linker.link(gpa); - - log.debug("processing command {s}", .{@tagName(options.command)}); - - switch (options.command) { - .help => unreachable, // handled in parseCli - - .ls => { - var buffered = io.bufferedWriter(stdout); - const writer = buffered.writer(); - - if (!options.list_files) options.list_files = !options.list_tags; - - if (options.list_tags) for (vm.linker.procedures.keys()) |path| { - try writer.writeAll(path); - try writer.writeByte('\n'); - }; - - if (options.list_files) for (vm.linker.files.keys()) |path| { - try writer.writeAll(path); - try writer.writeByte('\n'); - }; - - try buffered.flush(); - }, - - .call => { - var buffered: BufferedWriter = .{ .unbuffered_writer = stdout }; - var context = FileContext.init(buffered.writer()); - - for (options.calls) |call| switch (call) { - .file => |file| { - log.debug("calling file {s}", .{file}); - try vm.callFile(gpa, file, *FileContext, &context); - if (!options.omit_trailing_newline) try context.stream.writeByte('\n'); - }, - .tag => |tag| { - log.debug("calling tag {s}", .{tag}); - try vm.call(gpa, tag, *FileContext, &context); - }, - }; - - try buffered.flush(); - }, - - .find => for (options.calls) |call| switch (call) { - .file => unreachable, // not an option for find - .tag => |tag| { - log.debug("finding paths to tag {s}", .{tag}); - for (vm.linker.files.keys()) |file| { - var context = FindContext.init(gpa, file, tag, stdout); - try vm.callFile(gpa, file, *FindContext, &context); - try context.stream.flush(); - } - }, - }, - - .graph => { - var context = GraphContext.init(gpa, stdout); - - try context.begin(.{ - .border = options.graph_border_colour, - .background = options.graph_background_colour, - .text = options.graph_text_colour, - .colours = options.graph_colours, - .inherit = options.graph_inherit_line_colour, - .gradient = options.graph_line_gradient, - }); - - if (options.calls.len != 0) { - for (options.calls) |call| switch (call) { - .tag => unreachable, // not an option for graph - .file => |file| { - log.debug("rendering graph for file {s}", .{file}); - try vm.callFile(gpa, file, *GraphContext, &context); - }, - }; - } else { - for (vm.linker.files.keys()) |path| { - try vm.callFile(gpa, path, *GraphContext, &context); - } - - for (vm.linker.procedures.keys()) |proc| { - if (!context.target.contains(proc.ptr)) { - try vm.call(gpa, proc, *GraphContext, &context); - } - } - } - - try context.end(); - }, - - .tangle => for (vm.linker.files.keys()) |path| { - const file = try createFile(path, options); - defer file.close(); - - var buffered: BufferedWriter = .{ .unbuffered_writer = file.writer() }; - var context = FileContext.init(buffered.writer()); - - try vm.callFile(gpa, path, *FileContext, &context); - if (!options.omit_trailing_newline) try context.stream.writeByte('\n'); - try buffered.flush(); - }, - - .init => for (options.files) |path, index| { - try import(path, stdout); - if (index + 1 != options.files.len) try stdout.writeByte('\n'); - }, - } -} - -fn createFile(path: []const u8, options: Options) !fs.File { - var tmp: [fs.MAX_PATH_BYTES]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&tmp); - var filename = path; - var absolute = false; - - if (filename.len > 2 and mem.eql(u8, filename[0..2], "~/")) { - filename = try fs.path.join(fba.allocator(), &.{ - os.getenv("HOME") orelse return error.@"unable to find ~/", - filename[2..], - }); - } - - if (path[0] == '/' or path[0] == '~') { - if (!options.allow_absolute_paths) { - return error.@"Absolute paths disabled; use --allow-absolute-paths to enable them."; - } else { - absolute = true; - } - } - - if (fs.path.dirname(filename)) |dir| fs.cwd().makePath(dir) catch {}; - - if (absolute) { - log.warn("writing file with absolute path: {s}", .{filename}); - } else { - log.info("writing file: {s}", .{filename}); - } - return try fs.cwd().createFile(filename, .{ .truncate = true }); -} -fn indent(reader: anytype, writer: anytype) !void { - var buffer: [1 << 12]u8 = undefined; - var nl = true; - - while (true) { - const len = try reader.read(&buffer); - if (len == 0) return; - const slice = buffer[0..len]; - var last: usize = 0; - while (mem.indexOfScalarPos(u8, slice, last, '\n')) |index| { - if (nl) try writer.writeAll(" "); - try writer.writeAll(slice[last..index]); - try writer.writeByte('\n'); - nl = true; - last = index + 1; - } else if (slice[last..].len != 0) { - if (nl) try writer.writeAll(" "); - try writer.writeAll(slice[last..]); - nl = false; - } - } -} - -test "indent text block" { - const source = - \\pub fn main() !void { - \\ return; - \\} - ; - var buffer: [1024 * 4]u8 = undefined; - var in = io.fixedBufferStream(source); - var out = io.fixedBufferStream(&buffer); - - try indent(in.reader(), out.writer()); - - try testing.expectEqualStrings( - \\ pub fn main() !void { - \\ return; - \\ } - , out.getWritten()); -} -fn import(path: []const u8, writer: anytype) !void { - var file = try fs.cwd().openFile(path, .{}); - defer file.close(); - - const last = mem.lastIndexOfScalar(u8, path, '/') orelse 0; - const lang = if (mem.lastIndexOfScalar(u8, path[last..], '.')) |index| - path[last + index + 1 ..] - else - "unknown"; - - var buffered = io.bufferedReader(file.reader()); - var counting = io.countingWriter(writer); - try writer.writeByteNTimes('#', math.clamp(mem.count(u8, path[1..], "/"), 0, 5) + 1); - try writer.writeByte(' '); - try writer.writeAll(path); - try writer.writeAll(" \n\n "); - try counting.writer().print("lang: {s} esc: none file: {s}", .{ lang, path }); - try writer.writeByte('\n'); - try writer.writeAll(" "); - try writer.writeByteNTimes('-', counting.bytes_written); - try writer.writeAll("\n\n"); - try indent(buffered.reader(), writer); -} diff --git a/zangle.sh b/zangle.sh deleted file mode 100644 index 58aa0df..0000000 --- a/zangle.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -while true -do - echo README.md \ - | SHELL=/bin/sh entr -s '\ - zangle tangle README.md; \ - ls graphs/ | grep ".dot" | sed "s/.dot\$//" | xargs -I "{}" dot -Tpng -o out/{}.png graphs/{}.dot; \ - ls graphs/ | grep ".uml" | sed "s/.uml\$//" | xargs -I "{}" plantuml -o ../out/ graphs/{}.uml; \ - pandoc README.md -o /tmp/out.pdf \ - --standalone \ - --toc \ - --file-scope \ - --pdf-engine=xelatex \ - --highlight-style=misc/syntax.theme \ - --syntax-definition=misc/syntax.xml \ - --metadata-file misc/metadata.yml \ - --indented-code-classes=zig & \ - zig build; \ - zig-out/bin/zangle graph README.md \ - --graph-background-colour="#000000" \ - --graph-inherit-line-colour \ - --graph-text-colour="#ffffff" \ - --graph-border-colour="#444444" | dot -Tpng -o /tmp/zangle.png; \ - zig build test \ - || zig fmt --check --ast-check src lib \ - | while read l; do \ - cp $l /tmp/tmp.zig; \ - zig fmt /tmp/tmp.zig; \ - diff $l /tmp/tmp.zig; \ - done' - sleep 5 -done