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