~geb/gek

7cec5bb1c8036a1daa1d983d782028b1adab5200 — John Gebbie 2 years ago v0.1
initial commit
A  => LICENSE +674 -0
@@ 1,674 @@
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

A  => README.md +17 -0
@@ 1,17 @@
# The Gek Programming Language

Gek is a simple programming language for the simple life.

The website is https://geklang.org and there's a tutorial at https://geklang.org/tutorial.html

## Install From Source

On Unix, all you need is Git and the Go programming language compiler.<br>
Then run:
	`git clone https://git.sr.ht/~geb/gek && gek/install.sh`

## Chat

This repository hosts the initial implementation of the `gek` interpreter as well as user documentation.
The current focus is simplicity and the language design, greater efficiency will come later.
It does compile on Windows but it's not prioritized.

A  => doc/chat/description +13 -0
@@ 1,13 @@
The Gek language is useful for manipulating files and program output, and conducting programs.
Gek programs often perform an operation on each line of input, making them applicable to ongoing pipelines as well as input with a definite end.

Programs can be entered on the command line, or read from a file with the -f option.
Input is read from the list of files passed on the command line, or from stdin if no files are passed.

Gek programs consist of "words" that each do an action and string literals.
These are executed in order for each record of input, which is by default a line.
Words take their arguments from "the stack" and put their return values onto it.
String literals put themselves on the stack.
All values are strings but words might take their arguments to represent text, a number or regular expression.

There is a comprehensive tutorial at: https://geklang.org/tutorial.html

A  => doc/chat/notation +44 -0
@@ 1,44 @@
Each word listing has a pictorial showing the word's effect on the stack.
These "stack effects" follow the format: ( <arguments> -- <return values> )

The word `swap` has the listing:
	swap ( s1 s2 -- s2 s1 )
	Swaps the elements.
It shows it takes two strings and returns two strings.
All values are strings so the term element is used interchangeably when they don't represent a number or the like.
As always, the arguments are taken from the top of the stack, and the return values are put on the top of the stack.

The word `+` has the listing:
	+ ( n1 n2 -- n3 )
	Returns the two numbers added.
It shows it takes two numbers and returns a numbers.
As all values are strings, numbers are really characters representing numbers.

Some words like `READ?` use a | to show alternative outcomes:
	READ? ( s1 -- s2 1 | 0 )
	Returns the contents of the file at filepath s1 and 1, or just returns 0.
This shows `READ?` returns either (s2 and 1) or just 0.

Some words like `mapping` steal a token after them, which is denoted in angle brackets:
	mapping <name> ( -- )
	Create a new mapping.


All values are strings but words might take them their arguments to represent text, a number, or regular expression.
Symbols:
	s - a string
	n - a number, potentially fractional
	c - a the whole number, any fractional part is disregarded, a negative number will throw an error
	i - an integer number, any fractional part is disregarded
	f - a boolean flag number where non-zero is true and zero is false
	re - a regular expression
	s... - zero or more strings
	s... c - zero or more strings and a count of how many

Words determine the value of number arguments after skipping whitespace and giving up after a character that's not a sign, point or digit.

If integer arguments specify the <i>th something we call them indices.
Indices start from zero, so 0 for the first, 1 for the second, and so on.
If they're negative they go from the other end, so -1 for the last, -2 for the second last, and so on.

Boolean flags returned by a word will always be 0 or 1.

A  => doc/chat/snippets +12 -0
@@ 1,12 @@
Print the stack without touching it:
	Stack #$

Filter a list:
	"A" "b" "C" 3 foreach x x upper is if x then loop @

Skip unreadable files:
	FB fileless? if apologize nextfile then ;

Filter output through an external command:
	B withhold ;
	E withheld "sort | uniq" $shell ;

A  => doc/chat/topics +25 -0
@@ 1,25 @@
Chat
:description
:notation
:snippets

Word Categories
:general
:input
:stack
:printing
:string
:math
:events
:branching
:loops
:error
:args
:files
:system
:random
:state
:variables
:mappings
:stacks
:processes

A  => doc/words/args +38 -0
@@ 1,38 @@
The command-line arguments passed to gek are initially put on a special stack called Args.
Before each FB event, the bottom element is used as the next input file filepath and removed from the stack.



optparse <Map name> ( s... c s1 -- s... c )
Parses a list of strings for GNU-style command-line options (like -v and --version) with s1 specifying the allowed options.
Option results are assigned to the map and any positional arguments are returned.
s1 should be the option names separated by whitespace, each with a trailing ? or :.
? specifies a boolean flag option, the result will be 0 or 1.
: specifies an option that takes an argument, the result will be the argument.
One-character names specify short options like -v, multi-character names specify long options like --version.
Long boolean flags in the arguments can have an optional argument like --version=true, they may be: 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
Throws an error if it parses an unknown option or invalid boolean, or s1 is invalid.

optparse? <Map name> ( s... c s1 -- s... c 1 | 0 )
Same as `optparse` but returns whether successful rather than throwing an error if it parses an unknown option.


optclean ( s... c s1 -- s... c )
Tidies a list of GNU-style command-line options (like -v and --version) and positional arguments, with s1 specifying the allowed options.
s1 should be the option names separated by whitespace, each with a trailing ? or :.
? specifies a boolean flag option.
: specifies an option that takes an argument.
It returns each short option joined with its argument if it takes one, like "-fmyfile".
It returns each long option assigned to its argument or to true/false if it doesn't take one, like "--file=myfile" or "--version=true".
The options are returned in order, then a "--" and the positional arguments.
Long boolean flags in the arguments can have an optional argument like --version=true, they may be: 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
Throws an error if it parses an unknown option or invalid boolean, or s1 is invalid.

optclean? <Map name> ( s... c s1 -- s... c 1 | 0 )
Same as `optclean` but returns whether successful rather than throwing an error if it parses an unknown option.


varparse <Map name> ( s... c -- s... c )
Parses a list of strings for the form "some key=some value", assigning results to the map and returning non-matches.
They match the form be if they have an =.
If they match, the substring before the first = is used as the key, the substring after the first = is used as the value.

A  => doc/words/branching +44 -0
@@ 1,44 @@
If statements conditionally execute code.



<flag> if <code> then
If flag is true, code is executed.

<flag> if <code1> else <code2> then
If flag is true, code1 is executed, else code2 is executed.

<flag> if <code1> otherwise <flag2> if <code2> otherwise <flag3> if <code3> ... then
If flag is true, code1 is executed, otherwise if flag2 is true code2 is executed, otherwise if flag3 is true code3 is executed ...

<flag> if <code1> otherwise <flag2> if <code2> otherwise <flag3> if <code3> ... else <code4> then
Same as above but if none of the flags are true, code4 is executed.


if ( f -- )
Starts an if statement.
If f is false execution skips to the next `otherwise`, `else`, or `then` not nested in another if statement.

otherwise ( -- )
Used instead of `then` to chain if statements similar to "else if" in other languages.
If reached (not skipped by `if`), execution continues after the next `then` not nested in another if statement.

else ( -- )
Starts the optional clause in an if statement that executes if the first clause didn't.
If reached (not skipped by `if`), execution continues after the next `then` not nested by another if statement.

then ( -- )
Delimits an if statement.


and ( n1 n2 -- n1 | n2 )
Returns n2 if n1 is true else false.

#and ( n... c -- n1 )
Returns false if any are false else the last number.

or ( n1 n2 -- n1 | n2 )
Returns n1 if n1 is true else n2.

#or ( n... c -- n1 )
Returns the first true number else false.

A  => doc/words/error +17 -0
@@ 1,17 @@
Some words throw an error terminating the program if they are unsuccessful.
Some words return whether they were successful and set `apologize` to print why they were unsuccessful.



apology ( -- s )
Some words have this return an error message if they're unsuccessful or an empty string if they're successful.

apologize ( -- )
Some words have this print an error message to stderr if they're unsuccessful or do nothing if they're successful.

assert ( f -- )
Throws an error if f is false.
Useful for sanity checks and low effort error handling.

error ( s -- )
Throws an error with the message s.

A  => doc/words/events +64 -0
@@ 1,64 @@
Each event word lets you define code to run at it's specific event.
The event words are: B, E, FB, FE, PE, PRENUM, POSTNUM, TOKEN, and :.

All event words take this code from after themselves until a terminating `;`.
Any ; in string literals or commented by `#` or `#!` are ignored.
Occurrences of event word names increase nesting ignoring a future `;`.
So do occurrences of sort, another word that takes a `;` block.

Multiple instances of the same event word can append more code.
Each instance instance only executes on its first encounter, their code blocks are skipped on future encounters.
Event words in macros or evaluated by `eval` are taken to be unique instances and always execute.

Gek does a first pass where it evaluates the uninterrupted event words and `#!` and `#` comments at the start of each program text.
If this covers all the words and no E/FB/FE events were defined the program terminates.
This first pass is so events can be defined and `B` events executed before any input is read, and so lone `B` blocks terminate without reading input.


There is a special Mapping of the Macros called Macro.
Assignments to it assign Macros indirectly.



B <code> ; ( -- )
Defines code to run at the beginning of the program, before any input is read.
It must be at the start of the program as described above.

E <code> ; ( -- )
Defines code to run at the end of the program, unless the program is terminated with `EXIT` or by an operating system signal.

FB <code> ; ( -- )
Defines code to run before processing each input file.

FE <code> ; ( -- )
Defines code to run after processing each input file.

PE <code> ; ( -- )
Defines code to run at the end of the process, even if terminated by an error or by a non-forceful operating system signal.

PRENUM <code> ; ( -- )
Defines code to run before each number argument is taken from the stack.
Number arguments in `PRENUM` or `POSTNUM` events don't trigger more `PRENUM` events.

POSTNUM <code> ; ( -- )
Defines code to run after each number is put on the stack, including number literals.
Number arguments in `PRENUM` or `POSTNUM` events don't trigger more `POSTNUM` events.

TOKEN <code> ; ( -- )
Defines code to run on each token before they're evaluated.
In this case, tokens are each non-whitespace part or string literal of the program.
Before each token is evaluated, the token is put on the stack, `TOKEN` events execute, and the new top value is taken to be the token.
Tokens in the `TOKEN` code don't trigger `TOKEN` events.

: <name> <code> ; ( -- )
Defines a word that expands to code terminated by `;` but the first token is used for the word's name.
The name can't start or end with " or `.
We call a word defined by `:` a macro.


eval ( s -- <depends on s> )
Executes s as code.

yank ( -- s )
Steals the next token after the macro it is in.
In this case, tokens are each non-whitespace part of the program.

A  => doc/words/files +110 -0
@@ 1,110 @@
read ( s1 -- s2 )
Returns the contents of the file at filepath s1 having removed any trailing newline and anything after, or throws an error.

read? ( s1 -- s2 1 | 0 )
Tries to return the contents of the file at filepath s1 having removed any trailing newline and anything after.
Returns whether successful and sets `apologize`.

READ ( s1 -- s2 )
Returns the contents of the file at filepath s1, or throws an error.

READ? ( s1 -- s2 1 | 0 )
Returns the contents of the file at filepath s1 and 1, or just returns 0.
Sets `apologize`.


write ( s1 s2 -- )
Makes s1 plus a newline the contents of the file at filepath s2, creating it if necessary, or throws an error.

WRITE ( s1 s2 -- )
Same as `write` but doesn't add a newline.

write? ( s1 s2 -- f )
Tries to make s1 plus a newline the contents of the file at filepath s2, creating it if necessary.
Returns whether successful and sets `apologize`.

WRITE? ( s1 s2 -- f )
Same as `write?` but doesn't add a newline.

append ( s1 s2 -- )
Appends s1 plus a newline to the file at filepath s2, creating it if necessary, or throws an error.

APPEND ( s1 s2 -- )
Same as `append` but doesn't add a newline.

append? ( s1 s2 -- f )
Tries to append s1 plus a newline to the file at filepath s2, creating it if necessary.
Returns whether successful and sets `apologize`.

APPEND? ( s1 s2 -- f )
Same as `append?` but doesn't add a newline.



unlink ( s -- )
Removes the file or empty directory at filepath s, or throws an error but not if it didn't exist.

unlink? ( s -- f )
Tries to remove the file or empty directory s.
Returns 1 if it was removed or didn't exist, else 0.
Sets `apologize`.

UNLINK ( s -- )
Removes the file or directory and its children at filepath s, or throws an error but not if it didn't exist.

UNLINK? ( s -- f )
Tries to remove the file or directory and its children at filepath s.
Returns 1 if everything was removed or it didn't exist, else 0.
Sets `apologize`.

rename ( s1 s2 -- )
Renames/moves the file or directory at filepath s1 to s2, or throws an error.
It throws an error if s2 is a directory.

rename? ( s1 s2 -- f )
Tries to rename/move the file or directory at filepath s1 to s2, and returns whether successful.
It is unsuccessful if s2 is a directory.
Sets `apologize`.

mkdir ( s -- )
Creates a new directory at the filepath s, or throws an error.

mkdir? ( s -- f )
Tries to create a new directory at the filepath s, and returns whether successful.
Sets `apologize`.

entries ( s1 -- s... c )
Returns the children's filepaths of the directory at filepath s1, or throws error.
The filepaths will be prefixed with s1 and directories will have a trailing / (or \ on Windows).

entries? ( s1 -- s... c 1 | 0 )
Returns the list of entries of the directory s1 and 1, or just returns 0.
The filepaths will be prefixed with s1 and directories will have a trailing / (or \ on Windows).
Sets `apologize`.

ENTRIES ( s1 -- s... c )
Same as `entries` but doesn't prefix the filepaths with s1.

ENTRIES? ( s1 -- s... c 1 | 0 )
Same as `entries?` but doesn't prefix the filepaths with s1.


cleanpath ( s1 -- s2 )
Returns the shortest filepath equivalent to s1 by purely lexical processing.

relatepath ( s1 s2 -- s3 )
Returns the filepath s1 relative to the filepath s2 by purely lexical processing, or throws error.
(Returned paths are clean, not requiring `cleanpath`)

relatepath? ( s1 s2 -- s3 1 | 0 )
Returns the filepath s1 relative to the filepath s2 by purely lexical processing and 1, or just returns 0.
(Returned paths are clean, not requiring `cleanpath`)
Sets `apologize`.

resolvepath ( s1 -- s2 )
Returns the real filepath of s1 having followed any symbolic links, or throws error.

resolvepath? ( s1 -- s2 1 | 0 )
Returns the real filepath of s1 having followed any symbolic links and 1, or just returns 0.
(Returned paths are clean, not requiring `cleanpath`)
Sets `apologize`.

A  => doc/words/general +11 -0
@@ 1,11 @@
#! ( -- )
Skips the rest of line.
Good for shebang lines.

# ( -- )
Skips the rest of line.
Good for commenting code.

? ( -- )
Shows the stack's contents unambiguously for debugging.
It doesn't affect the value of `@`.

A  => doc/words/input +87 -0
@@ 1,87 @@
Gek programs execute for every record of the input files.
A record is a line by default.



fileless? ( -- f )
Returns whether the input file opened successfully (or false in `B`).
Useful in `FB` to skip bad files with `nextfile`.
Sets `apologize` regarding the opening.

file ( -- s )
Returns the filepath of the lastest input file whether it was opened successfully or not (or an empty string in `B`).


r ( -- s )
Returns the record.
Used after the end of an input file it returns the "not quite a record" that wasn't unterminated by a separator.

R ( s -- )
Sets the record to s, recalculating fields.

n ( -- c )
Returns the number of input file records read in.

N ( -- c )
Returns the number of input file records read in with the current input file.

f ( i -- s )
Returns the field at i, or an empty string.

F ( s i -- )
Sets the field at i to s.
If new territory, the in-between field delimiters each become a space, the in-between fields become empty strings.

fields ( -- s... c )
Returns the fields.

c ( -- c )
Returns the number of fields.

d ( i -- s )
Returns the field delimiter at i, or an empty string.

D ( s i -- )
Sets the field delimiter at i to s.
If new territory, the in-between field delimiters each become a space, the in-between fields become empty strings.

delims ( -- s... c )
Returns the field delimiters.

sep ( -- s )
Returns the record separator, which is either the text that terminated the record (using `separator`), or the text that was terminated by the record (using `record`).
Used after the end of an input file it returns the "not quite a separator" that wasn't terminated by a record.


record ( re -- )
Defines a record to be the regular expression re for future records.
The default is `"\n" separator`.

separator ( re -- )
Defines a record to be what's terminated by the regular expression re for future records.
The default is `"\n" separator`.

field ( re -- )
Defines a field to be the regular expression re for future records.
The default is `"\S+" field`.

delim ( re -- )
Defines a field to be what's terminated by the regular expression re for future records.
The default is `"\S+" field`.


bump ( -- )
Tries to prematurely read in the next record of the input file or of the next input file if fileless.
If there wasn't one, the record will be an empty string or the "not quite a record" that wasn't terminated by a separator.

bump? ( -- f )
Same as `bump` but returns whether it read in a record.

next ( -- )
Stops executing the current program cycle or event.
Useful for finishing with a record early and going on to the next record.

nextfile ( -- )
Stops executing the current program cycle or event, and stops processing the input file if any.
Unless used in FB, FE will still be triggered.
Useful for finishing with an input file early and going on to the next input file.

A  => doc/words/loops +56 -0
@@ 1,56 @@
for ( n -- )
Starts a for loop that terminates when the index, that starts at zero, equals or crosses the limit n.

#for ( n1 n2 -- )
Starts a for loop that terminates when the index, that starts at n2, equals or crosses the limit n1.

foreach ( s... c -- )
Starts a foreach loop that terminates when each list element has had it's turn.

forever ( -- )
Starts a forever loop that loops indefinitely.

loop ( -- )
Increments the innermost nested loop's index by one and conditionally jumps back to the start of it.
If a for loop: it jumps back if the index hasn't equaled or crossed the limit.
If a foreach loop: if there's another element, it proceeds onto it and jumps back.
If a forever loop: it unconditionally jumps back.

#loop ( n -- )
Same as `loop` but increments the index by n.


i ( -- n )
Returns the index of the innermost loop.

j ( -- n )
Returns the index of the second innermost loop.

x ( -- s )
Returns the element of the innermost foreach loop.

X ( -- s )
Has the innermost foreach loop proceed onto its next element or an empty string if out of elements, and returns it.

y ( -- s )
Returns the element of the second innermost foreach loop.

Y ( -- s )
Has the second innermost foreach loop proceed onto its next element or an empty string if out of elements, and returns it.

remnant ( -- n )
If in a for loop: returns limit - index - 1, which is how many iterations remain if incrementing by one.
If in a foreach loop: returns how many elements remain.


break ( -- )
Unconditionally breaks out of the innermost nested loop.
Execution continues after the next `loop` or `#loop` not nested by another loop.

while ( f -- )
Breaks out of a loop if f is false.
Equivalent to `0 = if break then`.

until ( f -- )
Breaks out of a loop if f is true.
Equivalent to `if break then`.

A  => doc/words/mappings +44 -0
@@ 1,44 @@
Mappings are collections of keys and corresponding values.



mapping <name> ( -- )
Create a new mapping.

map <Mapping name> ( s1 s2 -- )
Assigns the value s1 to the key s2 of the mapping.

+map <Mapping name> ( n s -- )
Increments by n the value of the key s of the mapping.
The value will be n if the key was unassigned.

-map <Mapping name> ( n s -- )
Decrements by n the value of the key s of the mapping.
The value will be -n if the key was unassigned.

unmap <Mapping name> ( s -- )
Unassigns the key s of the mapping if it is assigned.

<Mapping name> ( s1 -- s2 )
Returns the corresponding value of the key s1 of the mapping, or an empty string if it's not assigned.

of <Mapping name> ( s1 -- s2 )
Returns the corresponding value of the key s1 of the mapping, or throws an error if it's not assigned.

of? <Mapping name> ( s1 -- s2 1 | 0 )
Returns the corresponding value of the key s1 of the mapping and 1, or just 0 if it's not assigned.

tot <Mapping name> ( -- c )
Returns the number of keys the mapping has.

in <Mapping name> ( s -- f )
Returns whether the mapping has a key s.

keysof <Mapping name> ( -- s... c )
Returns the keys of the mapping.

valuesof <Mapping name> ( -- s... c )
Returns the values of the mapping.

itemsof <Mapping name> ( -- s... c )
Returns key value pairs of the mapping.

A  => doc/words/math +142 -0
@@ 1,142 @@
+ ( n1 n2 -- n3 )
Returns the two numbers added.
- ( n1 n2 -- n3 )
Returns n1 subtracted by n2.
* ( n1 n2 -- n3 )
Returns the two numbers multiplied.
/ ( n1 n2 -- n3 )
Returns n1 divided by n2.

#+ ( n... c n... c -- n... )
Does addition on the corresponding numbers of the lists, padding the shorter list with zeroes.
#- ( n... c n... c -- n... )
Does subtraction on the corresponding numbers of the lists, padding the shorter list with zeroes.
#* ( n... c n... c -- n... )
Does multiplication on the corresponding numbers of the lists, padding the shorter list with zeroes.
#/ ( n... c n... c -- n... )
Does division on the corresponding numbers of the lists, padding the shorter list with zeroes.

<  ( n1 n2 -- f )
Returns whether n1 is less than n2.
<= ( n1 n2 -- f )
Returns whether n1 is less than or equal to n2.
>  ( n1 n2 -- f )
Returns whether n1 is greater than n2.
>= ( n1 n2 -- f )
Returns whether n1 is greater than or equal to n2.
=  ( n1 n2 -- f )
Returns whether the numbers are equal.
!= ( n1 n2 -- f )
Returns whether the numbers are unequal.


mod ( n1 n2 -- n3 )
Returns the modulo of the numbers.

`10 3 mod` returns 1 because the best you can fill up 10 in chunks of 3 is 9 with a remainder of 1.
`9 3 mod` returns 0 because you can perfectly fill up 9 in chunks of 3.

It lets you wrap numbers into a given range.
`<number> 12 mod` wraps the number in the range [0,12).
`17 12 mod` returns 5, like how 17:00 is five o'clock.
`-2 12 mod` returns 10, like you're turning a clock back.

A number line of `<number> 3 mod`:
-7 -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7
 2  0  1  2  0  1  2  0  1  2  0  1  2  0  1


MOD ( n1 n2 -- n3 )
Returns the remainder of the numbers.
This differs from `mod` when it comes to negative arguments.

A number line of `<number> 3 MOD`:
-7 -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7
-1  0 -2 -1  0 -2 -1  0  1  2  0  1  2  0  1


sum ( n... c -- n )
Returns the sum of the list.

mean ( n... c -- n )
Returns the mean of the list.

min ( n1 n2 -- n1 | n2 )
Returns the lesser of the two numbers.

#min ( n... c -- n )
Returns the least of a list of numbers.

max ( n1 n2 -- n1 | n2 )
Returns the greater of the two numbers.

#max ( n... c -- n )
Returns the greatest of a list of numbers.

abs ( n1 -- n2 )
Returns the absolute value the of the number (the number with the sign dropped).

floor ( n1 -- n2 )
Returns the greatest integer less than or equal to the number.

ceil ( n1 -- n2 )
Returns the least integer greater than or equal to the number.

round ( n1 -- n2 )
Returns the nearest integer to the number.

pow ( n1 n2 -- n3 )
Returns n1 to the power of n2.

sqrt ( n1 -- n2 )
Returns the square root of the number.

hypot ( n1 n2 -- n3 )
Returns the hypotenuse of a right triangle given the other two sides, Sqrt(n1*n1 + n2*n2).

pi ( -- n )
Returns the mathematical constant pi.

sin ( n1 -- n2 )
Returns the sine of the radians.

cos ( n1 -- n2 )
Returns the cosine of the radians.

tan ( n1 -- n2 )
Returns the tangent of the radians.

asin ( n1 -- n2 )
Returns the arcsine (inverse sine) in radians of the number.

acos ( n1 -- n2 )
Returns the arccosine (inverse cosine) in radians of the number.

atan ( n1 -- n2 )
Returns the arctangent (inverse tangent) in radians of the number.

direction ( nx ny -- n )
Returns the angle of the coordinate from the positive x-axis.
Like atan2(y,x) in other languages, with swapped arguments.

degrees ( n1 -- n2 )
Returns the number of radians converted to degrees.

radians ( n1 -- n2 )
Returns the number of degrees converted to radians.

log ( n1 -- n2 )
Returns the natural logarithm of the number.

exp ( n1 -- n2 )
Returns e (the mathematical constant) raised to the power of the number.


que ( n1 n2 -- n3 )
Returns the bitwise AND of the absolute values of n1 and n2, negated if both are negative.

vel ( n1 n2 -- n3 )
Returns the bitwise OR of the absolute values of n1 and n2, negated if either or both are negative.

xor ( n1 n2 -- n3 )
Returns the bitwise XOR of the absolute values of n1 and n2, negated if either but not both are negative.

A  => doc/words/printing +53 -0
@@ 1,53 @@
! ( s -- )
Prints the string.

. ( s -- )
Prints the string and a space.

$ ( s -- )
Prints the string and a newline.

#$ ( s... c -- )
Prints the list of strings each with a newline.

?. ( s f -- )
If f is true, prints the string and a space.

?$ ( s f -- )
If f, prints the string and a newline.

inform ( s -- )
Prints the string and a newline to stderr, unless the string is empty.


to ( s -- )
Has future prints append to the file at filepath s, creating it if necessary, or throws an error.

to? ( s -- f )
Tries to have future prints append to the file at filepath s, creating it if necessary.
Returns whether successful, setting `apologize`.

fret ( -- )
Redirects future prints to stderr.

chill ( -- )
Redirects future prints to stdout (the default).


withhold ( -- )
Retains future prints until a corresponding `withheld`.

withheld ( -- s )
Returns a string of the prints since the last `withhold`.
Useful for sorting output, giving it to an external program, modifying a file in place, or building a string from printing words.

buffer ( -- )
Has future prints buffered.
See also the -b option.

unbuffer ( -- )
Has future prints unbuffered (the default).

flush ( -- )
Flushes any buffering prints.
Does nothing if printing is unbuffered which is the default.

A  => doc/words/processes +53 -0
@@ 1,53 @@
Processes let you run external commands in the background.
The stdout is continuously stored and retained even after the command terminates.
The stderr is printed to stderr.



process <name> ( s -- )
Creates a Process of the command-line s.
The shell is invoked immediately with s piped in.

take <Process name> ( -- s )
Returns the next line of the Process' stdout, or an empty string.
This can result in a deadlock if the command does not flush its output.

take? <Process name> ( -- s f )
Returns the next line of the Process' stdout and 1, or just 0 if none or unreadable.
Sets `apologize` regarding readability.

#take <Process name> ( c -- s... c )
Returns the Process' next c lines of stdout, or as many as there are if it's finished outputting and there's fewer lines.

send <Process name> ( s -- )
Writes s plus a newline to the Process' stdin and doesn't do anything if it's unsuccessful.

send? <Process name> ( s -- f )
Tries to write s plus a newline to the Process' stdin and returns whether successful.
Sets `apologize`.

SEND <Process name> ( s -- )
Writes s to the Process' stdin and doesn't do anything if it's unsuccessful.

SEND? <Process name> ( s -- f )
Tries to write s to the Process' stdin and returns whether successful.
Sets `apologize`.

shut <Process name> ( -- )
Closes the Process' stdin.
Useful for programs that don't output until all the input is read, like the Unix sort.

await <Process name> ( -- )
Waits for the Process' command to terminate if not already.
`rc` will give the return code.

conclude <Process name> ( -- s )
Closes the Process' stdin, waits for the command to terminate if not already, and returns the remaining stdout.
Sets `apologize` regarding readability.


<Process name> ( -- f )
Returns whether the Process' command has terminated.

tot <Process name> ( -- c )
Returns how many lines of stdout the Process has.

A  => doc/words/random +12 -0
@@ 1,12 @@
seed ( c -- )
Sets the seed for the pseudo-random words, which determines their sequence of results.
The default seed is different every time.

random ( -- c )
Returns a pseudo-random positive whole number.

RANDOM ( -- n )
Returns a pseudo-random real number between 0.0 and 1.0 (but never 1.0).

shuffle ( s... c -- s... )
Returns the elements in a pseudo-random order.

A  => doc/words/stack +109 -0
@@ 1,109 @@
drop ( s -- )
Discards the element.

#drop ( s... c -- )
Discards the list.

nip ( s1 s2 -- s2 )
Discards s1.

swap ( s1 s2 -- s2 s1 )
Swaps the two elements.

dup ( s -- s s )
Duplicates the element.

#dup ( s... c -- s... s... )
Duplicates the elements.

over ( s1 s2 -- s1 s2 s1 )
Duplicates s1 over s2.

rot ( s1 s2 s3 -- s2 s3 s1 )
Cycles the three elements.

#rot ( s... c i -- s... )
Cycles a list i places, with `3 1 #rot` the same as `rot`.
Cycles the other way if i is negative.

more ( s c -- s... )
Returns c+1 instances of s.


relist ( s... c -- s... c s... c )
Duplicates the list.

add ( s... c s... c -- s... c )
Joins the two lists.

exchange ( s... c s... c -- s... c s... c )
Swaps the two lists.

reverse ( s... c -- s... )
Reverses the order of the elements.

keep ( s... c i -- s... )
Returns at most i of the elements.
If i is positive, it returns elements from the end, else from the start.

KEEP ( s... c i -- s... )
Returns all but the elements `keep` would return.

kill ( s... c i -- s... )
Returns the elements except the element at i if there even is one.

#kill ( s... c i... c -- s... )
Returns the elements of the first list except those at indices in the second list.
Out of range indices do nothing.

spare ( s... c i -- s | <nothing> )
Returns the element at i if any.

compose ( s... c i... c -- s... )
Returns the elements of the first list ordered by the indices of the second list, with repeated indices repeating elements.
Out of range indices do nothing.

pad ( s... c i -- s... )
Returns the list and empty strings to make it up to i elements.
If i is positive, the empty strings are added to the end, else to the start

$pad ( s... c i s2 -- s... )
Same as `pad` but pads with s2 instead of empty strings.

lace ( s... c s... c -- s... )
Returns the two lists interlaced, using empty strings where one is longer.

unlace ( s... c -- s... c s... c )
Returns a list of the even-indexed elements and a list of the odd-indexed elements.

tailor ( s... c s... c -- s... c s... c )
Returns the two lists with the second truncated to the number of elements of the first.


within ( s1 s... c -- f )
Returns whether s1 occurs in the list.
If it does, `where` will give the position of the first occurrence, else -1.
See also `in` <Stack name>.

#within ( s1 s... c i -- f )
Returns whether s1 occurs enough in the list to refer to an occurrence by i.
If it does, `where` will give the position of that occurrence, else -1.

among ( s1 s... c -- n )
Returns the number of occurrences of s1 in the list.


depth ( -- c )
returns the number of elements on the stack.

@ ( -- i )
Returns the number of elements the last builtin word returned, or after `loop` or `#loop` the change in stack depth the loop caused.

clear ( -- )
Discards all elements on the stack.


sort ... ; ( s... c -- s... c )
Compares the elements using the code block terminated by `;` and orders them accordingly.
The code should take two elements and return a flag.
If the flag is true, the first element will come before the second.

A  => doc/words/stacks +53 -0
@@ 1,53 @@
Additional Stacks are useful to collect values and to hold values to work through.



stack <name> ( -- )
Creates a new Stack.

as <Stack name> ( s... c -- )
Sets the Stack to the elements.

<Stack name> ( -- s... c )
Returns copies of the elements of the Stack.

of <Stack name> ( i -- s )
Returns the element at i of the Stack, or throws an error if none.

of? <Stack name> ( i -- s 1 | 0 )
Returns the element at i of the Stack and 1, or just 0 if none.


push <Stack name> ( s -- )
Sends s to top of the Stack.

#push <Stack name> ( s... c -- ) ( -- s... )
Sends the elements to top of the Stack.


pop <Stack name> ( -- s )
Returns an element taken from the top of the Stack, or an empty string if none.

pop? <Stack name> ( -- s 1 | 0 )
Returns an element taken from the top of the Stack and 1, or just 0 if none.

#pop <Stack name> ( c -- s... c ) ( s... -- )
Takes c elements from the top of the Stack, or as many as there are.


POP <Stack name> ( -- s )
Returns an element taken from the bottom of the Stack, or an empty string if none.

POP? <Stack name> ( -- s 1 | 0 )
Returns an element taken from the bottom of the Stack and 1, or just 0 if none.

#POP <Stack name> ( c -- s... c )
Takes c elements from the bottom of the Stack, or as many as there are.


tot <Stack name> ( -- c )
Returns the number of elements in the Stack.

in <Stack name> ( s -- f )
Returns whether the Stack has an element of s.
If it does, `where` will give the position of the first occurrence, else -1.

A  => doc/words/state +31 -0
@@ 1,31 @@
There is a special Mapping called GekValue of values used by some builtin words.
It lets you define macros that set `apologize` and the like.

The keys are:
	"@"
	"apology"
	"match"
	"where"
	"subbed"
	"rc"



match ( -- s )
Returns the text matched by `meets`.

where ( -- i )
Returns the index set by `says`, `meets` and `within`.

rc ( -- n )
Returns the return code set by one the of the shell words or `await`.


builtins ( -- s... c )
Returns the names of the builtin words.

mappings ( -- s... c )
Returns the names of the Mappings, including the special Mappings.

processes ( -- s... c )
Returns the names of the Processes.

A  => doc/words/string +290 -0
@@ 1,290 @@
All values in Gek are strings but these words do string operations in particular.
Strings are sequences of bytes often taken to represent UTF-8 text.



nl ( -- s )
Returns a string that's just a newline.

tab ( -- s )
Returns a string that's just a tab.


is ( s1 s2 -- f )
Returns whether s1 and s2 are the same.

aint ( s1 s2 -- f )
Returns whether s1 and s2 are different.

starts ( s1 s2 -- f )
Returns whether s1 starts with s2.

ends ( s1 s2 -- f )
Returns whether s1 ends with s2.

cmp ( s1 s2 -- -1 | 0 | 1 )
Returns a number comparing the two strings lexigraphically.
Returns 0 if s1 = s2, -1 if s1 < s2, 1 if s1 > s2.

insensitively ( s1 s2 -- s3 s4 )
Returns s1 and s2 casefolded for case-insensitive comparison.


says ( s1 s2 -- f )
Returns whether s1 contains an occurrence of s2.
If it does, `where` will give the position of the first occurrence, else -1.

#says ( s1 s2 i -- f )
Returns whether s1 contains enough occurrences of s2 to refer to an occurrence by i.
If it does, `where` will give the position of that occurrence, else -1.

sayings ( s1 s2 -- c )
Returns the number of occurrences of s2 in s1.

meets ( s re -- f )
Returns whether s matches an occurrence of the regular expression re.
If it does, `match` will give the matched text of the first occurrence, and `where` its position.
If it doesn't, `match` will give an empty string, and `where` -1.

#meets ( s re i -- f )
Returns whether s matches enough occurrences of the regular expression re to refer to an occurrence by i.
If it does, `match` will give the matched text of that occurrence, and `where` its position.
If it doesn't, `match` will give an empty string, and `where` -1.

meetings ( s re -- c )
Returns the number of matches of the regular expression re in s.



len ( s -- c )
Returns the number of characters in the string.
Or more correctly, it returns the number of Unicode code points in the string.

lower ( s -- s )
Returns the string lowercase.

upper ( s -- s )
Returns the string uppercase.

title ( s -- s )
Returns the string lowercase with the first letter of each word uppercase.
Or more correctly, the first letter of each word mapped to their Unicode title case.

scale ( s i -- s )
Returns a string repeated i times and reversed if i is negative.

unprefix ( s1 s2 -- s3 )
Returns s1 with the prefix s2 removed if present.

unsuffix ( s1 s2 -- s3 )
Returns s1 with the suffix s2 removed if present.

strip ( s1 -- s2 )
Returns s1 with leading and trailing whitespace removed.

lstrip ( s1 -- s2 )
Returns s1 with leading whitespace removed.

rstrip ( s1 -- s2 )
Returns s1 with trailing whitespace removed.

$strip ( s1 s2 -- s3 )
Returns s1 with any/all leading and trailing characters of s2 removed.

$lstrip ( s1 s2 -- s3 )
Returns s1 with any/all leading characters of s2 removed.

$rstrip ( s1 s2 -- s3 )
Returns s1 with any/all trailing characters of s2 removed.

justify ( s i -- s )
Returns a right/left-justified string padded with spaces to length i.
If i is positive it's right-justified else left-justified.

$justify ( s1 i s2 -- s3 )
Same as `justify` except padding is done with s2 repeated.

center ( s i -- s )
Returns a centered string padded with spaces to length i.
If i is positive the left side may be padded one less than the right side, else the other way round.

$center ( s1 i s2 -- s3 )
Same as `center` except padding is done with s2 repeated.



cut ( s1 i -- s2 )
Returns s1 with i characters removed, or an empty string.
If i is positive the characters are removed from the start else from the end.

CUT ( s1 i -- s2 )
Returns s1 with all but i characters removed, or an empty string.
If i is positive the characters remain from the start else from the end.

chip ( s1 i -- s2 s3 )
Returns s1 split after i characters, from the start if i is positive else from the end.
Splits off an empty string if i is out of range.

snap ( s1 -- s2 s3 )
Returns s1 split at the first occurrence of whitespace.
If there is no occurrence, it returns s1 and an empty string.

snap? ( s1 -- s2 s3 1 | s1 0 )
Returns s1 split at the first occurrence of whitespace and 1, or s1 and 0 if there is no occurrence.

$snap ( s1 s2 -- s3 s4 )
Returns s1 split at the first occurrence of s2.
If there is no occurrence, it returns s1 and an empty string.

rsnap ( s1 -- s2 s3 )
Returns s1 split at the last occurrence of whitespace.
If there is no occurrence, it returns an empty string and s1.

$snap? ( s1 s2 -- s3 s4 1 | s1 0 )
Combination of `$snap` and `snap?`.
rsnap? ( s1 -- s2 s3 1 | s1 0 )
Combination of `rsnap` and `snap?`.
$rsnap ( s1 s2 -- s3 s4 )
Combination of `rsnap` and `$snap`.
$rsnap? ( s1 s2 -- s3 s4 1 | s1 0 )
Combination of `rsnap`, `$snap` and `snap?`.



, ( s1 s2 -- s3 )
Returns the two strings joined together.

#, ( s... c -- s1 )
Returns the strings of the list joined together.

join ( s... c -- s )
Returns the strings of the list joined together with a space between each.

$join ( s... i s1 -- s2 )
Returns the strings of the list joined together with s1 between each.
If the list count i is negative, s1 is not inserted between the first and second.

split ( s1 -- s... c )
Returns a list of s1 split at whitespace.
If s1 is just whitespace, it returns 0.

#split ( s1 i -- s... c )
Returns a list of s1 split at whitespace, maxing out at i strings.
If i is positive splits are done starting from the start of the string else from the end of the string.
If s1 is just whitespace, it returns 0.

$split ( s1 s2 -- s... c )
Returns a list of s1 split at occurrences of s2.
If s2 is empty, it splits between each character.
If there are no occurrences, it returns s1 and 1.

#$split ( s1 i s2 -- s... c )
Returns a list of s1 split at occurrences of s2, maxing out at i strings.
If i is positive splits are done starting from the start of the string else from the end of the string.
If s2 is empty, it splits between each character.
If there are no occurrences and i is non-zero, it returns s1 and 1.

quotejoin ( s... i -- s )
Returns the strings surrounded with " and joined together with a space between each.
Any instances of " in the strings are escaped with a backslash.
If the list count i is negative, the first isn't quoted but a space is still inserted between the first and second.

quotesplit ( s1 -- s... c )
Returns a list of the substrings that are surrounded with ".
Instances of " escaped with a backslash are ignored.


extract ( s1 re -- s... c )
Returns the matches of the regular expression re in s1.
If re has capturing groups, just their matches are returned.

#extract ( s1 re c -- s... c )
Same as `extract` but returns at most c strings.

punch ( s1 re -- s... c )
Returns the non-matches of the regular expression re in s1.
If re has capturing groups, their matches are also returned.

#punch ( s1 re c -- s... c )
Same as `punch` but makes at most c punches.
That is, ignoring the capturing group matches, it splits s1 at most c times.


sub ( s1 re s2 -- s3 )
Returns s1 with the first match of the regular expression re replaced using s2.
In the replacement text s2, the sequence ${n}, where n is a number, denotes the text that matched the nth (unnamed) capturing group.
${0} denotes the entire match text.
${name} denotes the text that matched the named capturing group with the (?P<name>...) syntax.
$$ denotes a literal $.
The braces are not mandatory but names are taken to be as long as possible: $1x is equivalent to ${1x} not ${1}x, and $10 is equivalent to ${10}, not ${1}0.
Names are non-empty sequences of letters, digits, and underscores.

gsub ( s1 re s2 -- s3 )
Same as `sub` but replaces all matches.

#sub ( s1 re s2 i -- s3 )
Same as `sub` but replaces the match specified by the index i if there is one.

SUB ( s1 s2 s3 -- s3 )
Returns s1 with the first occurrence of s2 replaced with s3.

GSUB ( s1 s2 s3 -- s3 )
Same as `SUB` but replaces all matches.

#SUB ( s1 s2 s3 n -- s3 )
Same as `sub` but replaces the occurrence specified by the index i if there is one.

subbed ( -- n )
Returns the number of replacements performed by the last `sub`/`gsub`/`#sub`/`SUB`/`GSUB`/`#SUB`.

rephrase ( s1 s... c -- s )
Returns s1 having replaced occurrences of even-indexed strings of the list with the string after them.
Said another way, the list is pairs of substrings to replace and their replacement.

reword ( s1 s... c -- s )
Same as `rephrase` but occurrences must be whole words.



valid? ( re -- f )
Returns whether the regular expression is valid, setting `apologize`.

escape ( s -- re )
Returns s with all regular expression metacharacters escaped.
Said another way, it returns a regular expression to match the literal text s.



es ( s1 -- s2 )
Returns s1 with backslash escape sequences substituted.
Throws an error if there's an invalid or unrecognized sequence after a backslash.

There are four ways to encode arbitrary values:
\x followed by two hexadecimal digits.
\ followed by three octal digits, representing no more than 255.
\u followed by four hexadecimal digits, representing a Unicode code point.
\U followed by eight hexadecimal digits, representing a Unicode code point.

Some one-character escapes represent special values:
\a  U+0007 alert or bell
\b  U+0008 backspace
\f  U+000C form feed
\n  U+000A line feed or newline
\r  U+000D carriage return
\t  U+0009 horizontal tab
\v  U+000B vertical tab
\\  U+005C backslash


es? ( s1 -- s2 1 | 0 )
Same as `es` but returns whether successful instead of throwing an error.
Sets `apologize`.

ord ( s -- c )
Returns the Unicode code point value for the first Unicode code point of the string.
So pretty much the number that maps to the first character of the string.
Throws an error if s is empty.

chr ( c -- s )
Returns a string of the Unicode code point.
So pretty much the character the number maps to.

A  => doc/words/system +86 -0
@@ 1,86 @@
The environment variable GEKSHELL sets the shell, defaulting to /bin/sh.

There is a special Mapping for environment variables called Env.
Assignments to it change the environment for gek and programs spawned by shell words and Processes.



shell ( s -- )
Executes the command-line s through the shell and waits for it to finish.
Any stdout or stderr output prints while it runs.
`rc` will give the return code.

$shell ( s1 s2 -- )
Same as `shell` but giving s1 plus a newline as input.

shellout ( s1 -- s2 )
Executes the command-line s through the shell, waits for it to finish, and returns the stdout output having removed any trailing newline and anything after.
Any stderr output prints while it runs.
`rc` will give the return code.

$shellout ( s1 s2 -- s3 )
Same as `shellout` but giving s1 plus a newline as input.

SHELLOUT ( s1 -- s2 )
Same as `shellout` but returns the stdout output as is.

$SHELLOUT ( s1 s2 -- s3 )
Same as `SHELLOUT` but giving s1 plus a newline as input.


shelljoin ( s... i -- s )
Returns the strings shell-escaped and joined together with a space between each.
If the list count i is negative, the first string is not shell-escaped.
Only suitable for Unix shells like sh.

shellsplit ( s1 -- s... c )
Returns s1 split using shell syntax.
Only suitable for Unix shells like sh.


sleep ( n -- )
Pauses the program for n seconds.

code ( i -- )
Sets the return code the program will terminate with.
Conventionally, zero indicates success, non-zero indicates an error.
Between 0 and 125 inclusive is recommended for portability.

exit ( -- )
Stops executing the current event or program cycle, and stops processing input files for good.
So it pretty much runs any E events and terminates the program.

EXIT ( -- )
Terminates the program immediately.


pwd ( -- s )
Returns the current working directory.

cd ( s -- )
Changes the current working directory to the filepath s, or throws an error.

cd? ( s -- f )
Tries to change the current working directory to the filepath s.
Returns whether successful, setting `apologize` if not.


stdin ( -- s )
Returns the filepath of stdin.

stdout ( -- s )
Returns the filepath of stdout.

stderr ( -- s )
Returns the filepath of stderr.

os ( -- s )
Returns the name of the Operating System in lowercase.
Perhaps "linux" or "windows".

pathsep ( -- s )
Returns the path separator the Operating System uses, "/" or "\".

pathlistsep ( -- s )
Returns the path list separator the Operating System uses.
Perhaps ":" or ";".

A  => doc/words/variables +21 -0
@@ 1,21 @@
Variables let you assign a value to a name by which you can retrieve copies of it.

There is a special Mapping of the Variables called Variable.
Assignments to it assign Variables indirectly.



set <name> ( s -- )
Assigns the value s to the Variable.
If the Variable doesn't exist it is created.

+set <name> ( n -- )
Increments the value of the Variable by n.
If the Variable doesn't exist it is created with the value n.

-set <name> ( n -- )
Decrement the value of the Variable by n.
If the Variable doesn't exist it is created with the value n.

<Variable name> ( -- s )
Returns the value of the Variable.

A  => gek.1 +74 -0
@@ 1,74 @@
.TH GEK 1
.SH NAME
gek \- stack-based stream editor and scripting language
.SH SYNOPSIS
.B gek
[\fB\-b\fR]
[\fB\-i\fR]
[ [\fB-e\fR] \fI'prog'\fR | \fB\-f\fR \fIprogfile\fR | \fB\-F\fR \fIprogfile\fR ]
.\" [\fB\-\-bits\fR \fIBITS\fR]
.IR file ...
.SH DESCRIPTION
\fBgek\fR is an interpreter for the Gek programming language.
The Gek language is useful for manipulating files and program output, and conducting programs.
Gek programs often perform an operation on each line of input, making them applicable to ongoing pipelines as well as input with a definite end.

Programs can be entered on the command line, or read from a file with the \fB\-f\fR option.
Input is read from the list of files passed on the command line, or from stdin if no files are passed.

Gek programs consist of "words" that each do an action and string literals.
These are executed in order for each record of input, which is by default a line.
Words take their arguments from "the stack" and put their return values onto it.
String literals put themselves on the stack.
All values are strings but words might take their arguments to represent text, a number or regular expression.

There is a comprehensive tutorial at: https://geklang.org/tutorial.html
\fBgek\fR also comes with \fBgekdoc\fR that provides documentation on words and topics.
.SH OPTIONS
.TP
.B \-b
Buffer the output.
.TP
.BI \-e " program-text"
Read the program text from \fIprogram-text\fR, instead of from the first command line argument.
Multiple \fB-e\fR options may be used.
.TP
.BI \-f " program-file"
Read the program text from \fIprogram-file\fR, instead of from the first command line argument.
Multiple \fB-f\fR options may be used.
.TP
.BI \-F " program-file"
Same as \fB-f\fR but following arguments are treated as positional arguments.
Useful for shebang lines to stop the user passing options or if your program has it's own options.
.TP
.B \-i
Edit the input files in place.
.P
If the options \fB-e\fR, \fB-f\fR or \fB-F\fR are used together or multiple times, the program text is the concatenation of them with newlines in between.
.SH EXAMPLES
.nf
Sum each row of a file:
.RS
gek 'fields sum $' myfile
.RE

Join lines of stdin which end with a backslash:
.RS
gek -b 'forever r "\\" ends while r -1 cut ! bump loop r $'
.RE

Reinvent the tail command:
.RS
#! /bin/gek -F
B
   mapping Opt
   10 "n" map Opt
   Args "n:" optparse Opt as Args
;
r depth "n" Opt keep
FE depth #$ ;
.RE
.fi
.SH SEE ALSO
.BR sed (1),
.BR awk (1)

A  => gekdoc +55 -0
@@ 1,55 @@
#! /usr/bin/env -S gek -F

: usage
	"Usage: gekdoc [-h] [--help] ['word'] [':topic'] ...

gekdoc provides documentation on words and topics of the Gek programming language.
You can pass a word as an argument like so:
	gekdoc upper
Or a topic preceded by a colon like so:
	gekdoc :printing

The topics are:" $
	"chat/topics" READ $
	EXIT
;

B
	"/usr/share/doc/gek" cd

	mapping Opt
	Args "h? help?" optparse? Opt 0 = if apologize 1 code usage then as Args
	tot Args 0 = "h" Opt or "help" Opt or if usage then

	stack Patterns
	Args foreach
		 "chat/" x ":" $snap nip , READ? if
			$
		otherwise "words/" x ":" $snap nip , READ? if
			$
		otherwise x ":" starts  x ":" aint and if
			fret x . "is not a topic" $ chill
		otherwise x len if
			"(?i)^#?\$?" x escape "\??\s.*\(.*--.*\)$" 3 #, push Patterns
		then
	loop
	POP? Patterns if set Pat else EXIT then

	stack WordFiles  "words" entries as WordFiles  WordFiles as Args

	withhold
;

# If the current line meets the pattern, print lines until the next stack effect, and repeat.
forever r Pat meets while
	forever r $  bump? while  r "\(.*--.*\)$" meets if nl ! break then loop
loop

FE
	# If finished the files, go through them again looking for the next pattern.
	tot Args 1 = if
		POP? Patterns if set Pat  WordFiles as Args then
	then
;

E withheld "\n{2,}" nl nl , gsub ! ;

A  => go.mod +9 -0
@@ 1,9 @@
module git.sr.ht/~geb/gek

go 1.18

require (
	git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
	golang.org/x/text v0.3.7
)

A  => go.sum +6 -0
@@ 1,6 @@
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1 h1:bmje0IdPzrY5nX6fAx8KuHP5G8EP4XMedMFcrssfJXc=
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1/go.mod h1:T5QFtG9s8i/kW5pDVCke6Mt2WmElJCIfTL1HMdpP7Rk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

A  => install.sh +11 -0
@@ 1,11 @@
#! /bin/sh
# Compiles gek to /usr/local/bin which is usually in $PATH.

cd "$(dirname "$0")" || exit
! go version > /dev/null && echo "you need to install go (the Go programming language)" && exit 1
go mod tidy && go build -o /dev/stdout ./src | sudo tee /usr/local/bin/gek > /dev/null || exit
sudo chmod +x /usr/local/bin/gek || exit
sudo cp gekdoc /usr/local/bin/gekdoc
sudo cp gek.1 /usr/share/man/man1/gek.1
sudo rm -rf /usr/share/doc/gek
sudo cp -r doc /usr/share/doc/gek

A  => src/FILES +18 -0
@@ 1,18 @@
test - runs tests

setup.go - parsing the command-line arguments
builtins.go - big map of the words
main.go - global variables and the mainloop

flow.go - control flow stack
stack.go - the stack

parse.go - tokenizing, and parsing literals
execute.go - executing tokens
trace.go - execution stack

errors.go - printing tracebacks
input.go - object for going through the input files
output.go - object for printing/buffering/redirecting user output

Most of the rest implement words for builtins.go

A  => src/builtins.go +1444 -0
@@ 1,1444 @@
package main

import (
	"errors"
	"fmt"
	"math"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
	"unicode"
	"os"
    "golang.org/x/text/cases"
    "golang.org/x/text/language"
)

var PrivateValues = struct {
	n int
	N int
	code int
}{}
var PublicValues = map[string]Element{
		"@": Num(0),
		"apology": Str(""),
		"match": Str(""),
		"where": Num(-1),
		"subbed": Num(0),
		"rc": Num(0),
}
const (
	VariableMapName = "Variable"
	MacroMapName = "Macro"
	ValueMapName = "GekValue"
	EnvMapName = "Env"
)
var Variables = map[string]Element{}
var Mappings = map[string]map[string]Element{
	VariableMapName: Variables,
	ValueMapName: PublicValues,
}
var Processes = map[string]*Process{}
var Macros = map[string][][]string{}

func forget(word string) {
	delete(Stacks, word)
	delete(Variables, word)
	delete(Mappings, word)
	delete(Macros, word)
	delete(Processes, word)
}


func excuse(err error) {
	if err == nil {
		PublicValues["apology"] = Str("")
	} else {
		PublicValues["apology"] = Str(err.Error())
	}
}

func yankProcess() *Process {
	if !next() { SyntaxFatal("expected process name") }
	p, in := Processes[tok()]
	if !in { TypeFatal(tok() + " is not a process name") }
	return p
}

func expectStackName() {
	if !next() { SyntaxFatal("expected stack name") }
	_, in := Stacks[tok()]
	if !in { TypeFatal(tok() + " is not a stack name") }
}

func MappingAssign(mapname, key string, value Element) {
	if m := Mappings[mapname]; m != nil {
		m[key] = value
	} else if mapname == EnvMapName {
		err := os.Setenv(key, string(value.Str()))
		if err != nil { Fatal(err.Error()) }
	} else if mapname == MacroMapName {
		Macros[key] = TokenizeString(string(value.Str()))
	} else {
		TypeFatal(mapname + " is not a mapping name")
	}
}
func MappingUnassign(mapname, key string) {
	if m := Mappings[mapname]; m != nil {
		delete(m, key)
	} else if mapname == EnvMapName {
		err := os.Unsetenv(key)
		if err != nil { Fatal(err.Error()) }
	} else if mapname == MacroMapName {
		delete(Macros, key)
	} else {
		TypeFatal(mapname + " is not a mapping name")
	}
}
func MappingExists(mapname string) bool {
	return Mappings[mapname] != nil || mapname == EnvMapName || mapname == MacroMapName
}
func MappingGet(mapname, key string) (Element, bool) {
	if m := Mappings[mapname]; m != nil {
		e, in := m[key]
		if !in { e = Str("") }
		return e, in
	} else if mapname == EnvMapName {
		s, in := os.LookupEnv(key)
		return Str(s), in
	} else if mapname == MacroMapName {
		tokenlines, in := Macros[key]
		return Str(Untokenize(tokenlines)), in
	}
	panic("unreachable")
}


var Builtins map[string]func()
func init() {
	Builtins = map[string]func() {
		"#!": func() { endline() },
		"#": func() { endline() },
		"?": func() {
			fmt.Fprintf(os.Stderr, "[%d]", len(Stack()))
			for _, e := range Stack() { fmt.Fprint(os.Stderr, " " + quote(string(e.Str()))) }
			fmt.Fprint(os.Stderr, "\n")
			PushCount = int(PublicValues["@"].Num())
		},
		"fileless?": func() {
			excuse(In.Err())
			pushBool(In.File() == nil)
		},
		"file": func() { pushString(In.Name()) },
		"r": func() { pushString(In.Record()) },
		"R": func() { In.SetRecord(popString()) },
		"n": func() { pushInt(PrivateValues.n) },
		"N": func() { pushInt(PrivateValues.N) },
		"f": func() { pushString(In.Field(popInt())) },
		"F": func() { i := popInt(); In.SetField(i, popString()) },
		"fields": func() { pushStringList(In.Fields()) },
		"c": func() { pushInt(len(In.Fields())) },
		"d": func() { pushString(In.Delim(popInt())) },
		"D": func() { i := popInt(); In.SetDelim(i, popString()) },
		"delims": func() { pushStringList(In.Delims()) },
		"sep": func() { pushString(In.Sep()) },
		"record": func() { In.RecordPattern(popString()) },
		"separator": func() { In.SeparatorPattern(popString()) },
		"field": func() { In.FieldPattern(popString()) },
		"delim": func() { In.DelimPattern(popString()) },
		"bump": func() {
			if In.File() == nil { In.NextFile() }
			In.Scan()
		},
		"bump?": func() {
			if In.File() == nil { In.NextFile() }
			pushBool(In.Scan())
		},
		"next": func() {
			Trace = []TraceFrame{}
			ClearFlowStack()
		},
		"nextfile": func() {
			Trace = []TraceFrame{}
			ClearFlowStack()
			RedoFB = true
			In.Bail()
		},
		"drop": func() { pop() },
		"#drop": func() { popList() },
		"nip": func() { a := pop(); pop(); push(a) },
		"swap": func() { b := pop(); a := pop(); push(b); push(a) },
		"dup": func() { a := pop(); push(a); push(a) },
		"#dup": func() { elems := popList(); pushElements(elems); pushElements(elems) },
		"over": func() { b := pop(); a := pop(); push(a); push(b); push(a) },
		"rot": func() { c := pop(); b := pop(); a := pop(); push(b); push(c); push(a) },
		"#rot": func() {
			i := popInt()
			elems := popList()
			i = mod(i, len(elems))
			pushElements(elems[i:])
			pushElements(elems[:i])
		},
		"more": func() {
			n := popCount()
			if n > 0 {
				e := pop()
				push(e)
				for ; n > 0; n-- { push(e) }
			}
		},
		"relist": func() { elems := popList(); pushList(elems); pushList(elems) },
		"add": func() {
			l2 := popList()
			l1 := popList()
			pushElements(l1)
			pushElements(l2)
			pushInt(len(l1) + len(l2))
		},
		"exchange": func() {
			l2 := popList()
			l1 := popList()
			pushList(l2)
			pushList(l1)
		},
		"reverse": func() {
			elems := popList()
			for i := len(elems)-1; i >= 0; i-- { push(elems[i]) }
		},
		"keep": func() {
			n := popInt()
			elems := popList()
			if n > 0 {
				pushElements(elems[max(0, len(elems)-n):])
			} else {
				pushElements(elems[:min(len(elems), -n)])
			}
		},
		"KEEP": func() {
			n := popInt()
			elems := popList()
			if n > 0 {
				pushElements(elems[:max(0, len(elems)-n)])
			} else {
				pushElements(elems[min(len(elems), -n):])
			}
		},
		"kill": func() {
			i := popInt()
			elems := popList()
			if i < 0 { i += len(elems) }
			if i >= 0 && i < len(elems) {
				pushElements(elems[:len(elems)-1-i])
				pushElements(elems[len(elems)-i:])
			} else {
				pushElements(elems)
			}
		},
		"#kill": func() {
			indices := popIntList()
			pushElements(kill(popList(), indices))
		},
		"spare": func() {
			i := popInt()
			elems := popList()
			if i < 0 { i += len(elems) }
			if i >= 0 && i < len(elems) {
				push(elems[len(elems)-1-i])
			} else {
				pushString("")
			}
		},
		"compose": func() {
			indices := popIntList()
			pushElements(compose(popList(), indices))
		},
		"pad": func() {
			goal := popInt()
			n := popCount()
			if goal >= 0 {
				for i := 0; i < goal-n; i++ { pushString("") }
			} else {
				elems := popElements(n)
				for i := 0; i < -goal-n; i++ { pushString("") }
				pushElements(elems)
			}
		},
		"$pad": func() {
			s := popString()
			goal := popInt()
			n := popCount()
			if goal >= 0 {
				for i := 0; i < goal-n; i++ { pushString(s) }
			} else {
				elems := popElements(n)
				for i := 0; i < -goal-n; i++ { pushString(s) }
				pushElements(elems)
			}
		},
		"lace": func() {
			l2 := popList()
			l1 := popList()
			for i := 0; i < max(len(l1), len(l2)); i++ {
				if i < len(l1) { push(l1[i]) } else { pushString("") }
				if i < len(l2) { push(l2[i]) } else { pushString("") }
			}
		},
		"unlace": func() {
			n := popCount()
			if n % 2 != 0 { ValueFatal("unlace expects an even count but received an odd count") }
			elems := popElements(n)
			for i := 0; i < n; i += 2 { push(elems[i]) }
			for i := 1; i < n; i += 2 { push(elems[i]) }
		},
		"tailor": func() {
			l2 := popList(); l1 := popList()
			pushList(l1)
			pushList(l2[:min(len(l1), len(l2))])
		},
		"within": func() {
			elems := popStringList()
			needle := popString()
			index := -1
			for i, e := range elems {
				if e == needle { index = i; break }
			}
			PublicValues["where"] = Num(index)
			pushBool(index != -1)
		},
		"#within": func() {
			hash := popInt()
			elems := popStringList()
			needle := popString()
			where := -1
			if hash >= 0 {
				for i := 0; i < len(elems); i++ {
					if elems[i] == needle {
						hash--
						if hash == -1 { where = i; break }
					}
				}
			} else {
				for i := len(elems)-1; i >= 0; i-- {
					if elems[i] == needle {
						hash++
						if hash == 0 { where = i; break }
					}
				}
			}
			PublicValues["where"] = Num(where)
			pushBool(where != -1)
		},
		"among": func() {
			elems := popStringList()
			needle := popString()
			count := 0
			for _, e := range elems {
				if e == needle { count++ }
			}
			pushInt(count)
		},
		"depth": func() { pushInt(len(Stack())) },
		"@": func() { push(PublicValues["@"]) },
		"clear": func() { ClearStack() },
		"sort": func() {
			elems := popList()
			by := getBlock()
			Sort(elems, by)
			pushElements(elems)
			// The execution of by messes with PushCount.
			// And it's undefined behavior if by doesn't just return one element.
			PushCount = len(elems)
		},
		"!": func() { Out.Print(popString()) },
		".": func() { Out.Print(popString() + " ") },
		"$": func() { Out.Println(popString()) },
		"#$": func() { for _, s := range popStringList() { Out.Println(s) } },
		"?.": func() { if popBool() { Out.Print(popString() + " ") } else { popString() } },
		"?$": func() { if popBool() { Out.Println(popString()) } else { popString() } },
		"inform": func() {
			s := popString()
			if len(s) != 0 { fmt.Fprintln(os.Stderr, s) }
		},
		"to": func() { throw(Out.To(popString())) },
		"to?": func() {
			err := Out.To(popString())
			excuse(err)
			pushBool(err == nil)
		},
		"fret": func() { Out.ToStderr() },
		"chill": func() { Out.ToStdout() },
		"withhold": func() { Out.Withhold() },
		"withheld": func() {
			s, ok := Out.PopWithheld()
			if !ok || (Setup.Inplace && Out.WithheldNum() == 0) {
				Fatal("withheld without corresponding withhold")
			}
			pushString(s)
		},
		"buffer": func() { Out.Buffer() },
		"unbuffer": func() { Out.Unbuffer() },
		"flush": func() { Out.Flush() },
		"nl": func() { pushString("\n") },
		"tab": func() { pushString("\t") },
		"is": func() { pushBool(popString() == popString()) },
		"aint": func() { pushBool(popString() != popString()) },
		"starts": func() { p := popString(); pushBool(strings.HasPrefix(popString(), p)) },
		"ends": func() { s := popString(); pushBool(strings.HasSuffix(popString(), s)) },
		"cmp": func() { pushInt(-strings.Compare(popString(), popString())) },
		"insensitively": func() {
			c := cases.Fold()
			s2 := popString()
			s1 := popString()
			pushString(c.String(s1))
			pushString(c.String(s2))
		},
		"says": func() {
			needle := popString()
			s := popString()
			i := strings.Index(s, needle)
			PublicValues["where"] = Num(i)
			pushBool(i != -1)
		},
		"#says": func() {
			at := popInt()
			needle := popString()
			s := popString()
			if at < 0 { at += strings.Count(s, needle) }
			if at < 0 {
				PublicValues["where"] = Num(-1)
				pushInt(0)
				return
			}

			s2 := s
			for ; at >= 0; at-- {
				i := strings.Index(s2, needle)
				if i == -1 { pushInt(0); return }
				s2 = s2[i+len(needle):]
			}
			PublicValues["where"] = Num(len(s) - len(s2) - len(needle))
			pushInt(1)
		},
		"sayings": func() {
			s2 := popString(); s1 := popString()
			pushInt(strings.Count(s1, s2))
		},
		"meets": func() {
			pat := popString()
			s := popString()
			match, i := Meets(s, pat)
			PublicValues["match"] = Str(match)
			PublicValues["where"] = Num(i)
			pushBool(i != -1)
		},
		"#meets": func() {
			at := popInt()
			pat := popString()
			s := popString()
			match, i := Nmeets(s, pat, at)
			PublicValues["match"] = Str(match)
			PublicValues["where"] = Num(i)
			pushBool(i != -1)
		},
		"meetings": func() {
			pat := popString(); s := popString()
			pushInt(len(mustCompile(pat).FindAllString(s, -1)))
		},
		"len": func() { pushInt(len([]rune(popString()))) },
		"lower": func() { pushString(strings.ToLower(popString())) },
		"upper": func() { pushString(strings.ToUpper(popString())) },
		"title": func() { pushString(cases.Title(language.Und).String(popString())) },
		"scale": func() { n := popInt(); s := popString(); pushString(Scale(s, n)) },
		"unprefix": func() { prefix := popString(); pushString(strings.TrimPrefix(popString(), prefix)) },
		"unsuffix": func() { suffix := popString(); pushString(strings.TrimSuffix(popString(), suffix)) },
		"strip": func() { pushString(strings.TrimSpace(popString())) },
		"lstrip": func() { pushString(strings_TrimLeftSpace(popString())) },
		"rstrip": func() { pushString(strings_TrimRightSpace(popString())) },
		"$strip": func() {
			cutset := popString()
			pushString(strings.Trim(popString(), cutset))
		},
		"$lstrip": func() {
			cutset := popString()
			pushString(strings.TrimLeft(popString(), cutset))
		},
		"$rstrip": func() {
			cutset := popString()
			pushString(strings.TrimRight(popString(), cutset))
		},
		"justify": func() {
			n := popInt()
			s := popString()
			if n >= 0 {
				pushString(strings.Repeat(" ", max(0, n-len(s))) + s)
			} else {
				pushString(s + strings.Repeat(" ", max(0, n-len(s))))
			}
		},
		"$justify": func() {
			p := popString()
			n := popInt()
			s := popString()
			margin := max(0, abs(n)-len(s))
			padding := strings.Repeat(p, margin/len(p)) + p[:margin%len(p)]
			if n >= 0 {
				pushString(padding + s)
			} else {
				pushString(s + reverse(padding))
			}
		},
		"center": func() {
			n := popInt()
			s := popString()
			margin := max(0, abs(n) - len(s))
			right := margin/2 + (margin & abs(n) & 1)
			left := margin - right
			if n < 0 { left, right = right, left }
			pushString(strings.Repeat(" ", left) + s + strings.Repeat(" ", right))
		},
		"$center": func() {
			p := popString()
			n := popInt()
			s := popString()
			margin := max(0, abs(n) - len(s))
			right := margin/2 + (margin & abs(n) & 1)
			left := margin - right
			if n < 0 { left, right = right, left }
			pushString(fill(p, left) + s + fill(p, right))
		},
		"cut": func() {
			n := popInt()
			s := popString()
			if n >= 0 {
				pushString(s[min(len(s), n):])
			} else {
				pushString(s[:max(0, len(s)+n)])
			}
		},
		"CUT": func() {
			n := popInt()
			s := popString()
			if n >= 0 {
				pushString(s[:min(len(s), n)])
			} else {
				pushString(s[max(0, len(s)+n):])
			}
		},
		"chip": func() {
			n := popInt()
			s := popString()
			if n < 0 { n += len(s) }
			if n >= len(s) {
				pushString(s)
				pushString("")
			} else if n < 0 {
				pushString("")
				pushString(s)
			} else {
				pushString(s[:n])
				pushString(s[n:])
			}
		},
		"snap": func() {
			left, right, _ := strings_CutFunc(popString(), unicode.IsSpace)
			pushString(left)
			pushString(right)
		},
		"snap?": func() {
			left, right, found := strings_CutFunc(popString(), unicode.IsSpace)
			if found {
				pushString(left)
				pushString(right)
				pushInt(1)
			} else {
				pushString(left)
				pushInt(0)
			}
		},
		"$snap": func() {
			sep := popString()
			s := popString()
			left, right, _ := strings.Cut(s, sep)
			pushString(left)
			pushString(right)
		},
		"rsnap": func() {
			left, right, _ := strings_CutRightFunc(popString(), unicode.IsSpace)
			pushString(left)
			pushString(right)
		},
		"$snap?": func() {
			sep := popString()
			s := popString()
			left, right, found := strings.Cut(s, sep)
			if found {
				pushString(left)
				pushString(right)
				pushInt(1)
			} else {
				pushString(left)
				pushInt(0)
			}
		},
		"rsnap?": func() {
			left, right, found := strings_CutRightFunc(popString(), unicode.IsSpace)
			if found {
				pushString(left)
				pushString(right)
				pushInt(1)
			} else {
				pushString(right)
				pushInt(0)
			}
		},
		"$rsnap": func() {
			sep := popString()
			s := popString()
			left, right, _ := strings_CutRight(s, sep)
			pushString(left)
			pushString(right)
		},
		"$rsnap?": func() {
			sep := popString()
			s := popString()
			left, right, found := strings_CutRight(s, sep)
			if found {
				pushString(left)
				pushString(right)
				pushInt(1)
			} else {
				pushString(right)
				pushInt(0)
			}
		},
		",": func() { b := popString(); pushString(popString() + b) },
		"#,": func() {
			n := popCount()
			pushString(strings.Join(popStrings(n), ""))
		},
		"join": func() {
			pushString(strings.Join(popStringList(), " "))
		},
		"$join": func() {
			sep := popString()
			n := popInt()
			if n >= 0 {
				pushString(strings.Join(popStrings(n), sep))
			} else {
				strs := popStrings(-n)
				pushString(strs[0] + strings.Join(strs[1:], sep))
			}
		},
		"split": func() { strs := strings.Fields(popString()); for _, s := range strs { pushString(s) }; pushInt(len(strs)) },
		"#split": func() { hash := popInt(); s := popString(); pushStringList(HashSplitSpace(s, hash)) },
		"$split": func() { sep := popString(); pushStringList(strings.Split(popString(), sep)) },
		"#$split": func() {  sep := popString(); hash := popInt(); s := popString(); pushStringList(HashSplit(s, sep, hash)) },
		"quotejoin": func() {
			n := popInt()
			if n >= 0 {
				pushString(QuoteJoin(popStrings(n)))
			} else {
				strs := popStrings(-n)
				pushString(QuoteAppend(strs))
			}
		},
		"quotesplit": func() { pushStringList(QuoteSplit(popString())) },
		"extract": func() {
			pat := popString(); s := popString()
			pushStringList(Extract(s, pat, -1))
		},
		"#extract": func() {
			hash := popCount(); pat := popString(); s := popString()
			pushStringList(Extract(s, pat, hash))
		},
		"punch": func() {
			pat := popString(); s := popString()
			pushStringList(Punch(s, pat, -1))
		},
		"#punch": func() {
			hash := popCount(); pat := popString(); s := popString()
			pushStringList(Punch(s, pat, hash))
		},
		"sub": func() {
			repl := popString(); pat := popString(); s := popString()
			s2, did := Sub(s, pat, repl)
			if did { PublicValues["subbed"] = Num(1) } else { PublicValues["subbed"] = Num(0) }
			pushString(s2)
		},
		"gsub": func() {
			repl := popString(); pat := popString(); s := popString()
			s2, subcount := Gsub(s, pat, repl)
			PublicValues["subbed"] = Num(subcount)
			pushString(s2)
		},
		"#sub": func() {
			hash := popInt(); repl := popString(); pat := popString(); s := popString()
			s2, did := Nsub(s, pat, repl, hash)
			if did { PublicValues["subbed"] = Num(1) } else { PublicValues["subbed"] = Num(0) }
			pushString(s2)
		},
		"SUB": func() {
			that := popString(); this := popString(); s := popString()
			after := strings.Replace(s, this, that, 1)
			if after == s {
				PublicValues["subbed"] = Num(0)
			} else {
				PublicValues["subbed"] = Num(1)
			}
			pushString(after)
		},
		"GSUB": func() {
			that := popString(); this := popString(); s := popString()
			PublicValues["subbed"] = Num(strings.Count(s, this))
			pushString(strings.ReplaceAll(s, this, that))
		},
		"#SUB": func() {
			i := popInt(); that := popString(); this := popString(); s := popString()
			if i < 0 { i += strings.Count(s, this) }
			if i >= 0 {
				parts := strings.SplitN(s, this, i+2)
				if len(parts) == i+2 {
					PublicValues["subbed"] = Num(1)
					pushString(strings.Join(parts[:i+1], this) + that + parts[i+1])
					return
				}
			}
			PublicValues["subbed"] = Num(0)
			pushString(s)
		},
		"subbed": func() { push(PublicValues["subbed"]) },
		"rephrase": func() { oldnew := popStringList(); pushString(Rephrase(popString(), oldnew)) },
		"reword": func() { oldnew := popStringList(); pushString(Reword(popString(), oldnew)) },
		"valid?": func() {
			err := IsValidRegex(popString())
			excuse(err)
			pushBool(err == nil)
		},
		"escape": func() { pushString(EscapeRegex(popString())) },
		"es": func() {
			s, err := InterpretEscapes(popString())
			if err != nil { ValueFatal(err.Error()) }
			pushString(s)
		},
		"es?": func() {
			s, err := InterpretEscapes(popString())
			excuse(err)
			if err == nil { pushString(s) }
			pushBool(err == nil)
		},
		"ord": func() {
			s := popString()
			if len(s) == 0 { TypeFatal("ord received an empty string") }
			pushInt(int(s[0]))
		},
		"chr": func() { pushString(string(popInt())) },
		"+": func() { pushFloat(popFloat() + popFloat()) },
		"-": func() { b := popFloat(); pushFloat(popFloat() - b) },
		"*": func() { pushFloat(popFloat() * popFloat()) },
		"/": func() {
			b := popFloat()
			if b == 0 { ArithmeticFatal("division by zero attempted") }
			pushFloat(popFloat() / b)
		},
		"#+": func() {
			s2 := popFloatList(); s1 := popFloatList()
			pushFloats(elementwise(s1, s2, func(a, b float64) float64 { return a + b }))
		},
		"#-": func() {
			s2 := popFloatList(); s1 := popFloatList()
			pushFloats(elementwise(s1, s2, func(a, b float64) float64 { return a - b }))
		},
		"#*": func() {
			s2 := popFloatList(); s1 := popFloatList()
			pushFloats(elementwise(s1, s2, func(a, b float64) float64 { return a * b }))
		},
		"#/": func() {
			s2 := popFloatList(); s1 := popFloatList()
			pushFloats(elementwise(s1, s2, func(a, b float64) float64 {
				if b == 0 { ArithmeticFatal("division by zero") }
				return a / b
			}))
		},
		"<": func() { pushBool(popFloat() > popFloat()) },
		"<=": func() { pushBool(popFloat() >= popFloat()) },
		">": func() { pushBool(popFloat() < popFloat()) },
		">=": func() { pushBool(popFloat() <= popFloat()) },
		"=": func() { pushBool(popFloat() == popFloat()) },
		"!=": func() { pushBool(popFloat() != popFloat()) },
		"mod": func() { b := popFloat(); pushFloat(Modulo(popFloat(), b)) },
		"MOD": func() { b := popFloat(); pushFloat(math.Mod(popFloat(), b)) },
		"sum": func() { pushFloat(Sum(popFloatList())) },
		"mean": func() { pushFloat(Mean(popFloatList())) },
		"min": func() { pushFloat(math.Min(popFloat(), popFloat())) },
		"#min": func() { pushFloat(SliceMin(popFloatList())) },
		"max": func() { pushFloat(math.Max(popFloat(), popFloat())) },
		"#max": func() { pushFloat(SliceMax(popFloatList())) },
		"abs": func() { pushFloat(math.Abs(popFloat())) },
		"floor": func() { pushFloat(math.Floor(popFloat())) },
		"ceil": func() { pushFloat(math.Ceil(popFloat())) },
		"round": func() { pushFloat(math.Round(popFloat())) },
		"pow": func() { exp := popFloat(); pushFloat(Pow(popFloat(), exp)) },
		"sqrt": func() { pushFloat(Sqrt(popFloat())) },
		"hypot": func() { pushFloat(math.Hypot(popFloat(), popFloat())) },
		"pi": func() { pushFloat(math.Pi) },
		"sin": func() { pushFloat(math.Sin(popFloat())) },
		"cos": func() { pushFloat(math.Cos(popFloat())) },
		"tan": func() { pushFloat(math.Tan(popFloat())) },
		"asin": func() { pushFloat(Asin(popFloat())) },
		"acos": func() { pushFloat(Acos(popFloat())) },
		"atan": func() { pushFloat(math.Atan(popFloat())) },
		"direction": func() { pushFloat(math.Atan2(popFloat(), popFloat())) },
		"degrees": func() { pushFloat(popFloat() * 180 / math.Pi) },
		"radians": func() { pushFloat(popFloat() * math.Pi / 180) },
		"log": func() { pushFloat(Log(popFloat())) },
		"exp": func() { pushFloat(Exp(popFloat())) },
		"que": func() { pushInt(BitwiseAnd(popInt(), popInt())) },
		"vel": func() { pushInt(BitwiseOr(popInt(), popInt())) },
		"xor": func() { pushInt(BitwiseXor(popInt(), popInt())) },
		"B": func() { SyntaxFatal("unexpected B") },
		"E": func() { EventDefinition(&Events.E) },
		"FB": func() { EventDefinition(&Events.FB) },
		"FE": func() { EventDefinition(&Events.FE) },
		"PE": func() { EventDefinition(&Events.PE) },
		"PRENUM": func() { EventDefinition(&Events.PRENUM) },
		"POSTNUM": func() { EventDefinition(&Events.POSTNUM) },
		"TOKEN": func() { EventDefinition(&Events.TOKEN) },
		":": func() {
			if !next() { SyntaxFatal("expected name for macro") }
			if []rune(tok())[0] == '"' { SyntaxFatal(`macro names can't start with "`) }
			if []rune(tok())[0] == '`' { SyntaxFatal("macro names can't start with `") }
			forget(tok())
			Macros[tok()] = getBlock()
		},
		"eval": func() { next(); pushframe("eval", TokenizeString(popString())); pushframe("", [][]string{ { "" } }) },
		"yank": func() {
			if AtToplevel() { SyntaxFatal("unexpected yank at the toplevel (not in a macro etc.)") }
			var top TraceFrame
			Trace, top = Trace[:len(Trace)-1], Trace[len(Trace)-1]
			if exhausted() {
				Trace = append(Trace, top)
				SyntaxFatal("expected token after " + top.Name())
			}
			pushString(tok())
			next()
			Trace = append(Trace, top)
		},
		"if": func() { If() },
		"otherwise": func() { Otherwise() },
		"else": func() { Else() },
		"then": func() { Then() },
		"and": func() { push(LogicalAnd(popElements(2))) },
		"#and": func() { push(LogicalAnd(popList())) },
		"or": func() { push(LogicalOr(popElements(2))) },
		"#or": func() { push(LogicalOr(popList())) },
		"for": func() {
			limit := popFloat()
			if limit == 0 { SkipLoop(); return }
			PushFor(0, limit, len(Stack()))
		},
		"#for": func() {
			start := popFloat()
			limit := popFloat()
			if start == limit { SkipLoop(); return }
			PushFor(start, limit, len(Stack()))
		},
		"foreach": func() {
			elems := popList()
			if len(elems) == 0 { SkipLoop(); return }
			PushForeach(elems, len(Stack()))
		},
		"forever": func() { PushForever(len(Stack())) },
		"loop": func() {
			startingDepth, ok := ConditionallyLoop(1)
			if !ok { SyntaxFatal("unexpected loop") }
			PushCount = len(Stack()) - startingDepth
		},
		"#loop": func() {
			startingDepth, ok := ConditionallyLoop(popFloat())
			if !ok { SyntaxFatal("unexpected #loop") }
			PushCount = len(Stack()) - startingDepth
		},
		"i": func() { lp := GetLoop(0); if lp == nil { SyntaxFatal("i not in loop") }; pushFloat(lp.Index()) },
		"j": func() { lp := GetLoop(1); if lp == nil { SyntaxFatal("j not in loop") }; pushFloat(lp.Index()) },
		"x": func() {
			lp := GetForeach(0)
			if lp == nil { SyntaxFatal("unexpected x not in foreach loop") }
			push((*lp).Element())
		},
		"X": func() {
			lp := GetForeach(0)
			if lp == nil { SyntaxFatal("unexpected X not in foreach loop") }
			lp.Next()
			push((*lp).Element())
		},
		"y": func() {
			lp := GetForeach(1)
			if lp == nil { SyntaxFatal("unexpected y not in foreach loop") }
			push((*lp).Element())
		},
		"Y": func() {
			lp := GetForeach(1)
			if lp == nil { SyntaxFatal("unexpected Y not in foreach loop") }
			lp.Next()
			push((*lp).Element())
		},
		"remnant": func() { lp := GetLoop(0); if lp == nil { SyntaxFatal("unexpected remnant not in loop") }; pushFloat(lp.Remnant()) },
		"break": func() {
			Break("break")
		},
		"while": func() {
			if popBool() {
				if GetLoop(0) == nil { SyntaxFatal("unexpected while not in loop") }
			} else {
				Break("while")
			}
		},
		"until": func() {
			if popBool() {
				Break("until")
			} else {
				if GetLoop(0) == nil { SyntaxFatal("unexpected until not in loop") }
			}
		},
		"apology": func() { push(PublicValues["apology"]) },
		"apologize": func() {
			s := string(PublicValues["apology"].Str())
			if len(s) != 0 { fmt.Fprintln(os.Stderr, s) }
		},
		"assert": func() {
			if popInt() == 0 { Fatal("assertion error") }
		},
		"error": func() { Fatal(popString()) },
		"optparse": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			optstring := popString()
			strs := popStringList()
			opts, positionals, err := optparse(optstring, strs)
			throw(err)
			for k, v := range opts { MappingAssign(tok(), k, v) }
			pushStringList(positionals)
		},
		"optparse?": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			optstring := popString()
			strs := popStringList()
			opts, positionals, err := optparse(optstring, strs)
			if _, ok := err.(*OptstringError); ok { throw(err) }
			excuse(err)
			if err == nil {
				for k, v := range opts { MappingAssign(tok(), k, v) }
				pushStringList(positionals)
				pushInt(1)
			} else {
				pushInt(0)
			}
		},
		"optclean": func() {
			optstring := popString()
			before := popStringList()
			after, err := optclean(optstring, before)
			throw(err)
			pushStringList(after)
		},
		"optclean?": func() {
			optstring := popString()
			before := popStringList()
			after, err := optclean(optstring, before)
			if _, ok := err.(*OptstringError); ok { throw(err) }
			excuse(err)
			pushStringList(after)
		},
		"varparse": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			strs := popStringList()
			n := len(strs)
			for _, s := range strs {
				if parts := strings.SplitN(s, "=", 2); len(parts) == 2 {
					MappingAssign(tok(), parts[0], Str(parts[1]))
					n--
				} else {
					pushString(s)
				}
			}
			pushInt(n)
		},
		"read": func() {
			data, err := os.ReadFile(popString())
			throw(err)
			pushString(stripNewline(string(data)))
		},
		"read?": func() {
			data, err := os.ReadFile(popString())
			excuse(err)
			if err == nil {
				pushString(stripNewline(string(data)))
			}
			pushBool(err == nil)
		},
		"READ": func() {
			data, err := os.ReadFile(popString())
			throw(err)
			pushString(string(data))
		},
		"READ?": func() {
			data, err := os.ReadFile(popString())
			excuse(err)
			if err == nil { pushString(string(data)) }
			pushBool(err == nil)
		},
		"write": func() { throw(WriteToFile(popString(), popString() + "\n")) },
		"WRITE": func() { throw(WriteToFile(popString(), popString())) },
		"write?": func() { err := WriteToFile(popString(), popString() + "\n"); excuse(err); pushBool(err == nil) },
		"WRITE?": func() { err := WriteToFile(popString(), popString()); excuse(err); pushBool(err == nil) },
		"append": func() { throw(AppendToFile(popString(), popString() + "\n")) },
		"APPEND": func() { throw(AppendToFile(popString(), popString())) },
		"append?": func() { err := AppendToFile(popString(), popString() + "\n"); excuse(err); pushBool(err == nil) },
		"APPEND?": func() { err := AppendToFile(popString(), popString()); excuse(err); pushBool(err == nil) },
		"unlink": func() {
			err := os.Remove(popString())
			if !errors.Is(err, os.ErrNotExist) { throw(err) }
		},
		"unlink?": func() {
			err := os.Remove(popString())
			excuse(err)
			pushBool(err == nil || errors.Is(err, os.ErrNotExist))
		},
		"UNLINK": func() { throw(os.RemoveAll(popString())) },
		"UNLINK?": func() {
			err := os.RemoveAll(popString())
			excuse(err)
			pushBool(err == nil)
		},
		"rename": func() {
			n := popString()
			o := popString()
			throw(os.Rename(o, n))
		},
		"rename?": func() {
			n := popString()
			o := popString()
			err := os.Rename(o, n)
			excuse(err)
			pushBool(err == nil)
		},
		"mkdir": func() {
			throw(os.MkdirAll(popString(), 0755))
		},
		"mkdir?": func() {
			err := os.MkdirAll(popString(), 0755)
			excuse(err)
			pushBool(err == nil)
		},
		"entries": func() {
			paths, err := Entries(popString(), true)
			throw(err)
			pushStringList(paths)
		},
		"entries?": func() {
			paths, err := Entries(popString(), true)
			excuse(err)
			if err == nil { pushStringList(paths) }
			pushBool(err == nil)
		},
		"ENTRIES": func() {
			paths, err := Entries(popString(), false)
			throw(err)
			pushStringList(paths)
		},
		"ENTRIES?": func() {
			paths, err := Entries(popString(), false)
			excuse(err)
			if err == nil { pushStringList(paths) }
			pushBool(err == nil)
		},
		"cleanpath": func() { pushString(filepath.Clean(popString())) },
		"relatepath": func() {
			from := popString()
			p := popString()
			rp, err := filepath.Rel(from, p)
			throw(err)
			pushString(rp)
		},
		"relatepath?": func() {
			from := popString()
			p := popString()
			rp, err := filepath.Rel(from, p)
			excuse(err)
			if err == nil { pushString(rp) }
			pushBool(err == nil)
		},
		"resolvepath": func() {
			rp, err := filepath.EvalSymlinks(popString())
			throw(err)
			pushString(rp)
		},
		"resolvepath?": func() {
			rp, err := filepath.EvalSymlinks(popString())
			excuse(err)
			if err == nil { pushString(rp) }
			pushBool(err == nil)
		},
		"shell": func() { PublicValues["rc"] = Num(shell(Out.to, popString(), "", false)) },
		"$shell": func() { PublicValues["rc"] = Num(shell(Out.to, popString(), popString() + "\n", true)) },
		"shellout": func() { o, rc := shellout(popString(), "", false); PublicValues["rc"] = Num(rc); pushString(stripNewline(o)) },
		"$shellout": func() { o, rc := shellout(popString(), popString() + "\n", true); PublicValues["rc"] = Num(rc); pushString(stripNewline(o)) },
		"SHELLOUT": func() { o, rc := shellout(popString(), "", false); PublicValues["rc"] = Num(rc); pushString(o) },
		"$SHELLOUT": func() { o, rc := shellout(popString(), popString() + "\n", true); PublicValues["rc"] = Num(rc); pushString(o) },
		"shelljoin": func() {
			n := popInt()
			if n >= 0 {
				pushString(Shelljoin(popStrings(n)))
			} else {
				pushString(ShellAppend(popStrings(-n)))
			}
		},
		"shellsplit": func() { pushStringList(Shellsplit(popString())) },
		"sleep": func() {
			n := popFloat()
			time.Sleep(time.Duration(n*float64(time.Second)))
		},
		"code": func() { PrivateValues.code = popInt() },
		"exit": func() { flowStack = nil; Finish() },
		"EXIT": func() { flowStack = nil; PrematurelyFinish() },
		"pwd": func() {
			wd, err := os.Getwd()
			throw(err)
			pushString(wd)
		},
		"cd": func() { throw(os.Chdir(popString())) },
		"cd?": func() {
			err := os.Chdir(popString())
			excuse(err)
			pushBool(err == nil)
		},
		"stdin": func() { pushString(os.Stdin.Name()) },
		"stdout": func() { pushString(os.Stdout.Name()) },
		"stderr": func() { pushString(os.Stderr.Name()) },
		"os": func() { pushString(runtime.GOOS) },
		"pathsep": func() { pushString(string(os.PathSeparator)) },
		"pathlistsep": func() { pushString(string(os.PathListSeparator)) },
		"seed": func() { RandomSeed(int64(popInt())) },
		"random": func() { pushInt(int(RandomInt())) },
		"RANDOM": func() { pushFloat(RandomFloat()) },
		"shuffle": func() { elems := popList(); Shuffle(elems); pushElements(elems) },
		"match": func() { push(PublicValues["match"]) },
		"where": func() { push(PublicValues["where"]) },
		"rc": func() { push(PublicValues["rc"]) },
		"builtins": func() {
			names := make([]string, 0, len(Builtins))
			for k := range Builtins { names = append(names, k) }
			sort.Strings(names)
			pushStringList(names)
		},
		"mappings": func() {
			names := make([]string, 0, len(Mappings) + 2)
			names = append(names, "Env")
			names = append(names, "Macro")
			for k := range Mappings { names = append(names, k) }
			sort.Strings(names)
			pushStringList(names)
		},
		"processes": func() {
			names := make([]string, 0, len(Processes))
			for k := range Processes { names = append(names, k) }
			sort.Strings(names)
			pushStringList(names)
		},
		"set": func() { if !next() { SyntaxFatal("expected variable name") }; forget(tok()); Variables[tok()] = pop() },
		"+set": func() {
			if !next() { SyntaxFatal("expected variable name") }
			inc := popFloat()
			var f float64
			if value := Variables[tok()]; value != nil { f = float64(value.Num()) }
			forget(tok())
			Variables[tok()] = Num(f + inc)
		},
		"-set": func() {
			if !next() { SyntaxFatal("expected variable name") }
			dec := popFloat()
			var f float64
			if value := Variables[tok()]; value != nil { f = float64(value.Num()) }
			forget(tok())
			Variables[tok()] = Num(f - dec)
		},
		"mapping": func() {
			if !next() { SyntaxFatal("expected name for mapping name") }
			forget(tok())
			Mappings[tok()] = map[string]Element{}
		},
		"map": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			key := popString()
			value := pop()
			MappingAssign(tok(), key, value)
		},
		"+map": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			key := popString()
			inc := popInt()
			if MappingExists(tok()) {
				v, _ := MappingGet(tok(), key)
				MappingAssign(tok(), key, Num(int(v.Num()) + inc))
			}
		},
		"-map": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			key := popString()
			dec := popInt()
			if MappingExists(tok()) {
				v, _ := MappingGet(tok(), key)
				MappingAssign(tok(), key, Num(int(v.Num()) - dec))
			}
		},
		"unmap": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			key := popString()
			MappingUnassign(tok(), key)
		},
		"of": func() {
			if !next() { SyntaxFatal("expected mapping name or stack name") }
			if _, is := Stacks[tok()]; is {
				i := popInt()
				if i < 0 { i += len(Stacks[tok()]) }
				if i < 0 || i >= len(Stacks[tok()]) { KeyFatal("out of range index") }
				push(Stacks[tok()][i])
			} else if MappingExists(tok()) {
				e, _ := MappingGet(tok(), popString())
				push(e)
			} else {
				TypeFatal(tok() + " is not a mapping name or stack name")
			}
		},
		"of?": func() {
			if !next() { SyntaxFatal("expected mapping name or stack name") }
			if _, is := Stacks[tok()]; is {
				i := popInt()
				if i < 0 { i += len(Stacks[tok()]) }
				if i >= 0 && i < len(Stacks[tok()]) {
					push(Stacks[tok()][i])
					pushInt(1)
				} else {
					pushInt(0)
				}
			} else if MappingExists(tok()) {
				e, in := MappingGet(tok(), popString())
				push(e)
				pushBool(in)
			} else {
				TypeFatal(tok() + " is not a mapping name or stack name")
			}
		},
		"tot": func() {
			if !next() { SyntaxFatal("expected mapping name or stack name or process name") }
			if m, in := Mappings[tok()]; in {
				pushInt(len(m))
			} else if s, in := Stacks[tok()]; in {
				pushInt(len(s))
			} else if p, in := Processes[tok()]; in {
				pushInt(p.Len())
			} else {
				TypeFatal(tok() + " is not a mapping name or stack name or process name")
			}
		},
		"in": func() {
			if !next() { SyntaxFatal("expected mapping name or stack name") }
			key := popString()
			var in bool
			if m := Mappings[tok()]; m != nil {
				_, in = m[key]
			} else if _, is := Stacks[tok()]; is {
				PublicValues["where"] = Num(-1)
				for i, e := range Stacks[tok()] {
					if e.Str() == Str(key) {
						in = true
						PublicValues["where"] = Num(i)
						break
					}
				}
			} else if tok() == EnvMapName {
				_, in = os.LookupEnv(key)
			} else if tok() == MacroMapName {
				_, in = Macros[key]
			} else {
				TypeFatal(tok() + " is not a mapping name or stack name")
			}
			pushBool(in)
		},
		"keysof": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			if m := Mappings[tok()]; m != nil {
				for k := range m { pushString(k) }
				pushInt(len(m))
			} else if tok() == EnvMapName {
				for _, s := range os.Environ() { pushString(strings.SplitN(s, "=", 1)[0]) }
				pushInt(len(os.Environ()))
			} else if tok() == MacroMapName {
				for k := range Macros { pushString(k) }
				pushInt(len(Macros))
			} else {
				TypeFatal(tok() + " is not a mapping name")
			}
		},
		"valuesof": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			if m := Mappings[tok()]; m != nil {
				for _, v := range m { push(v) }
				pushInt(len(m))
			} else if tok() == EnvMapName {
				for _, s := range os.Environ() { pushString(strings.SplitN(s, "=", 1)[1]) }
				pushInt(len(os.Environ()))
			} else if tok() == MacroMapName {
				for _, tokens := range Macros { pushString(Untokenize(tokens)) }
				pushInt(len(Macros))
			} else {
				TypeFatal(tok() + " is not a mapping name")
			}
		},
		"itemsof": func() {
			if !next() { SyntaxFatal("expected mapping name") }
			if m := Mappings[tok()]; m != nil {
				for k, v := range m { pushString(k); push(v) }
				pushInt(len(m) * 2)
			} else if tok() == EnvMapName {
				for _, s := range os.Environ() { pushStringList(strings.SplitN(s, "=", 1)) }
				pushInt(len(os.Environ()) * 2)
			} else if tok() == MacroMapName {
				for k, tokens := range Macros { pushString(k); pushString(Untokenize(tokens)) }
				pushInt(len(Macros) * 2)
			} else {
				TypeFatal(tok() + " is not a mapping name")
			}
		},
		"stack": func() {
			if !next() { SyntaxFatal("expected name for stack name") }
			name := tok()
			forget(name)
			Stacks[name] = nil
		},
		"as": func() {
			expectStackName()
			Stacks[tok()] = nil
			pushElementsToStack(tok(), popList())
		},
		"push": func() {
			expectStackName()
			pushToStack(tok(), pop())
		},
		"#push": func() {
			expectStackName()
			pushElementsToStack(tok(), popList())
		},
		"pop": func() {
			expectStackName()
			if len(Stacks[tok()]) == 0 {
				pushString("")
			} else {
				push(popFromStack(tok()))
			}
		},
		"pop?": func() {
			expectStackName()
			if len(Stacks[tok()]) == 0 {
				pushInt(0)
			} else {
				push(popFromStack(tok()))
				pushInt(1)
			}
		},
		"#pop": func() {
			expectStackName()
			n := popCount()
			elems := Stacks[tok()][max(0, len(Stacks[tok()]) - n):]
			Stacks[tok()] = Stacks[tok()][:len(Stacks[tok()]) - len(elems)]
			pushList(elems)
		},
		"POP": func() {
			expectStackName()
			if len(Stacks[tok()]) == 0 {
				pushString("")
			} else {
				push(Stacks[tok()][0])
				Stacks[tok()] = Stacks[tok()][1:]
			}
		},
		"POP?": func() {
			expectStackName()
			if len(Stacks[tok()]) == 0 {
				pushInt(0)
			} else {
				push(Stacks[tok()][0])
				Stacks[tok()] = Stacks[tok()][1:]
				pushInt(1)
			}
		},
		"#POP": func() {
			expectStackName()
			n := popCount()
			elems := Stacks[tok()][:min(len(Stacks[tok()]), n)]
			Stacks[tok()] = Stacks[tok()][len(elems):]
			pushList(elems)
		},
		"process": func() {
			if !next() { SyntaxFatal("expected processor word name") }
			forget(tok())
			Processes[tok()] = NewProcess(popString())
		},
		"take": func() {
			s, _, _ := yankProcess().Next()
			pushString(s)
		},
		"take?": func() {
			s, err, was := yankProcess().Next()
			excuse(err)
			if was { pushString(s) }
			pushBool(was)
		},
		"#take": func() {
			p := yankProcess()
			n := popCount()
			var i int
			for ; i < n; i++ {
				s, _, was := p.Next()
				if !was { break }
				pushString(s)
			}
			pushInt(i)
		},
		"send": func() {
			p := yankProcess()
			_ = p.Send(popString() + "\n")
		},
		"send?": func() {
			p := yankProcess()
			err := p.Send(popString() + "\n")
			excuse(err)
			pushBool(err == nil)
		},
		"SEND": func() {
			p := yankProcess()
			_ = p.Send(popString())
		},
		"SEND?": func() {
			p := yankProcess()
			err := p.Send(popString())
			excuse(err)
			pushBool(err == nil)
		},
		"shut": func() {
			p := yankProcess()
			p.Close()
		},
		"await": func() {
			p := yankProcess()
			PublicValues["rc"] = Num(p.Await())
		},
		"conclude": func() {
			p := yankProcess()
			rc, output, err := p.Conclude()
			excuse(err)
			PublicValues["rc"] = Num(rc)
			pushString(output)
		},
	}
}

A  => src/devscripts/documented +13 -0
@@ 1,13 @@
#! /usr/bin/env -S gek -f
# Prints the documented word names in topic order.

B
	"../doc" cd
	"chat/topics" read "(?s).*Word Categories" "" sub
	"(?m)^:(.+)" extract
	foreach "words/" x , loop @ as Args
;

r "^(\S+)\s.*\(.*--.*\)$" extract #$

E depth 0 = assert ;

A  => src/devscripts/implemented +11 -0
@@ 1,11 @@
#! /usr/bin/env -S gek -f
# Prints the implemented word names.

B
	"builtins.go" 1 as Args
	forever r "Builtins = map[string]func() {" says until bump? while loop
;

r `"(.*)": func\(\)` extract drop

E depth #$ ;

A  => src/devscripts/sorted +19 -0
@@ 1,19 @@
#! /usr/bin/env -S gek -f
# Prints the builtins.go map sorted by the documentation order.

B
	stack Order  "./devscripts/documented" shellout nl $split as Order
	"builtins.go" read ".*: func\(\)(?s:.)+?\}," extract

	# ( s -- i )
	: position
		`"(.*?)": func\(\)` 1 #extract if
			in Order drop where
		else
			-1
		then
	;

	sort position swap position > ;
	depth #$
;

A  => src/errors.go +45 -0
@@ 1,45 @@
package main

import (
	"fmt"
	"os"
	"strings"
)

const (
	ansiReset = "\033[0m"
	ansiRed = "\033[31m"
)

func tracebackLine(f *TraceFrame) string {
	tl := f.tokens[f.li]
	line := strings.Join(tl[:f.i], "") + ansiRed + tl[f.i] + ansiReset + strings.Join(tl[f.i+1:], "")
	return fmt.Sprintf("line %d of %s: %s\n", f.li+1, f.Name(), line)
}
func printTraceback() {
	for _, frame := range Trace {
		fmt.Fprint(os.Stderr, tracebackLine(&frame))
	}
}

func FatalWithoutTraceback(message string) {
	fmt.Fprintln(os.Stderr, message)
	PrivateValues.code = 1
	PrematurelyFinish()
}
func Fatal(s string) {
	printTraceback()
	FatalWithoutTraceback(s)
}
func ArithmeticFatal(s string) { Fatal(s) }
func KeyFatal(s string) { Fatal(s) }
func SyntaxFatal(s string) { Fatal("syntax error: " + s) }
func TypeFatal(s string) { Fatal(s) }
func ValueFatal(s string) { Fatal(s) }
func StackUnderflow() { Fatal("stack underflow") }

func throw(err error) {
	if err != nil {
		Fatal(err.Error())
	}
}

A  => src/events.go +58 -0
@@ 1,58 @@
package main

var Events = struct {
	// B blocks are instantly run
	E [][]string
	FB [][]string
	FE [][]string
	PE [][]string
	PRENUM [][]string
	POSTNUM [][]string
	TOKEN [][]string
}{}

var inNumEvent = false
func Prenum() {
	if !inNumEvent {
		inNumEvent = true
		ExecuteBlock("PRENUM", Events.PRENUM)
		inNumEvent = false
	}
}
func Postnum() {
	if !inNumEvent {
		inNumEvent = true
		ExecuteBlock("POSTNUM", Events.POSTNUM)
		inNumEvent = false
	}
}

var inTokenEvent = false
func TokenEvent(token string) string {
	if Events.TOKEN == nil { return token }
	if !inTokenEvent {
		inTokenEvent = true
		pushString(token)
		ExecuteBlock("TOKEN", Events.TOKEN)
		inTokenEvent = false
		return popString()
	}
	return token
}


type toplevelTokenPosition struct {
	li int
	i int
}
var exhaustedEvents []toplevelTokenPosition
func EventDefinition(event *[][]string) {
	pos := toplevelTokenPosition{ Frame().li, Frame().i }
	if AtToplevel() {
		for i := range exhaustedEvents {
			if pos == exhaustedEvents[i] { getBlock(); return }
		}
	}
	*event = append(*event, getBlock()...)
	exhaustedEvents = append(exhaustedEvents, pos)
}

A  => src/execute.go +122 -0
@@ 1,122 @@
package main

import (
	"fmt"
	"os"
	"strings"
)

// doVocab executes the next token if it's in vocab (or whitespace) and returns whether it was.
func doVocab(token string, vocab map[string]func()) bool {
	if len(strings.TrimSpace(token)) == 0  { return true }
	fn, in := vocab[token]
	if in {
		PushCount = 0
		fn()
		PublicValues["@"] = Num(PushCount)
	}
	return in
}

// DoNextWord skips string literals and ; terminated blocks and executes the next token if it's in vocab (or whitespace) and returns whether it was.
func DoNextWord(vocab map[string]func()) bool {
	for {
		if len(getStringLiteral(_tok())) != 0 { next(); continue }
		if IsBlockName(tok()) { getBlock(); continue }
		break
	}
	return doVocab(tok(), vocab)
}

// doLiteral executes the next token if it's a string literal or number literal and returns whether it was.
func doLiteral(token string) bool {
	if s := getStringLiteral(token); len(s) != 0 {
		s = strings_TrimRightSpace(s)
		pushString(s[1:len(s)-1])
		return true
	}
	if f, ok := ParseNumberLiteral(token); ok {
		pushFloat(f)
		return true
	}
	return false
}

// Do executes the next token.
// If it's a macro it will push tokens.
// Leaves Trace on the now exhausted token or a pushed guff token.
// The token is an argument so it can be manipulated beforehand (used to implement TOKEN events).
func Do(fulltoken string, vocab map[string]func()) bool {
	token := strings_TrimRightSpace(fulltoken)
	if e, in := Variables[token]; in {
		push(e)
	} else if m, in := Macros[token]; in {
		name := token
		next()
		pushframe(name, m)
		pushframe("", [][]string{ { "" } })
	} else if s, in := Stacks[token]; in {
		pushList(s)
	} else if p, in := Processes[token]; in {
		pushBool(p.Running())
	} else if MappingExists(token) {
		e, _ := MappingGet(token, popString())
		push(e)
	} else {
		return doVocab(token, vocab) || doLiteral(fulltoken)
	}
	return true
}

// Execute executes and exhausts Trace.
func Execute() {
	for !exhausted() {
		token := _tok()
		if token[0] == '"' || token[0] == '`' {
			token = getStringLiteral(token)
		}
		token = strings_TrimRightSpace(token)
		token = TokenEvent(token)
		if len(token) != 0 {
			if !Do(token, Builtins) { Fatal("undefined word: " + token) }
		}
		next()
	}
}

// ExecuteBlock executes the passed tokenlines.
func ExecuteBlock(name string, tokenlines [][]string) {
	old := Trace
	setTraceToBlock(name, tokenlines)
	Execute()
	Trace = old
}


func assertNoControlFlow() {
	if len(flowStack) != 0 {
		// printTracebackLine(getFlowFrame(flowStack[0]))
		fmt.Fprint(os.Stderr, tracebackLine(getFlowFrame(flowStack[0])))
		// This function is used in PrematurelyFinish() so using fatal() would loop.
		fmt.Fprintln(os.Stderr, "syntax error: unterminated control flow")
		PrivateValues.code = 1
	}
}
func AssertNoControlFlow() {
	assertNoControlFlow()
	if len(flowStack) != 0 { PrematurelyFinish() }
}

func PrematurelyFinish() {
	flowStack = nil
	ExecuteBlock("PE", Events.PE)
	assertNoControlFlow()
	Out.Flush()
	os.Exit(PrivateValues.code)
}

func Finish() {
	ExecuteBlock("E", Events.E)
	AssertNoControlFlow()
	PrematurelyFinish()
}

A  => src/filewords.go +29 -0
@@ 1,29 @@
package main

import "os"

func WriteToFile(filepath, s string) error {
	return os.WriteFile(filepath, []byte(s), 0644)
}
func AppendToFile(filepath, s string) error {
	f, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err == nil { _, err = f.WriteString(s) }
	f.Close()
	return err
}

func Entries(filepath string, prefix bool) ([]string, error) {
	names, err := os.ReadDir(filepath)
	if err != nil { return nil, err }
	s := make([]string, len(names))
	p := ""
	if prefix { p = filepath + string(os.PathSeparator) }
	for i := range names {
		if names[i].IsDir() {
			s[i] = p + names[i].Name() + string(os.PathSeparator)
		} else {
			s[i] = p + names[i].Name()
		}
	}
	return s, nil
}

A  => src/flow.go +290 -0
@@ 1,290 @@
package main

import (
	"fmt"
	"os"
)

type ifClause struct {
	trace []TraceFrame
}
type elseClause struct {
	trace []TraceFrame
}

type loop interface {
	Loop(float64) bool
	StartingDepth() int
	Trace() []TraceFrame
	Index() float64
	Remnant() float64
}

type loopcommon struct {
	startingDepth int
	trace []TraceFrame
	index float64
}
func (lp loopcommon) StartingDepth() int { return lp.startingDepth }
func (lp loopcommon) Trace() []TraceFrame { return lp.trace }
func (lp loopcommon) Index() float64 { return lp.index }

type forloop struct {
	loopcommon
	limit float64
}
func (lp *forloop) Loop(increment float64) bool {
	if lp.index < lp.limit {
		lp.index += increment
		return lp.index < lp.limit
	} else {
		lp.index += increment
		return lp.index > lp.limit
	}
}
func (lp *forloop) Remnant() float64 {
	return lp.limit - lp.index - 1
}

type foreachloop struct {
	loopcommon
	elements []Element
}
func (lp *foreachloop) Loop(increment float64) bool {
	lp.index += increment
	if len(lp.elements) == 0 { return false }
	lp.elements = lp.elements[1:]
	return len(lp.elements) != 0
}
func (lp *foreachloop) Next() {
	if len(lp.elements) != 0 { lp.elements = lp.elements[1:] }
}
func (lp *foreachloop) Element() Element {
	if len(lp.elements) == 0 { return Str("") } else { return lp.elements[0] }
}
func (lp *foreachloop) Remnant() float64 {
	return float64(len(lp.elements) - 1)
}

type foreverloop struct {
	loopcommon
}
func (lp *foreverloop) Loop(increment float64) bool {
	lp.index += increment
	return true
}
func (lp *foreverloop) Remnant() float64 {
	SyntaxFatal("unexpected remnant in forever loop")
	panic("unreachable")
}


var flowStack []interface{}

func getTraceCopy() []TraceFrame {
	t := make([]TraceFrame, len(Trace))
	copy(t, Trace)
	return t
}

func PushIf() { flowStack = append(flowStack, ifClause{ getTraceCopy() }) }
func PushElse() { flowStack = append(flowStack, elseClause{ getTraceCopy() }) }

func PushFor(start, limit float64, depth int) {
	flowStack = append(flowStack, &forloop{ loopcommon{ depth, getTraceCopy(), start }, limit })
}
func PushForeach(elems []Element, depth int) {
	flowStack = append(flowStack, &foreachloop{ loopcommon{ depth, getTraceCopy(), 0 }, elems })
}
func PushForever(depth int) {
	flowStack = append(flowStack, &foreverloop{ loopcommon{ depth, getTraceCopy(), 0 } })
}

func InIf() bool {
	if len(flowStack) == 0 { return false }
	_, is := flowStack[len(flowStack)-1].(ifClause)
	return is
}
func InElse() bool {
	if len(flowStack) == 0 { return false }
	_, is := flowStack[len(flowStack)-1].(elseClause)
	return is
}
func getLoop(depth int) int {
	for i := len(flowStack)-1; i >= 0; i-- {
		if _, is := flowStack[i].(loop); is {
			if depth == 0 { return i }
			depth--
		}
	}
	return -1
}

func getForeach(depth int) int {
	for i := len(flowStack)-1; i >= 0; i-- {
		if _, is := flowStack[i].(*foreachloop); is {
			if depth == 0 { return i }
			depth--
		}
	}
	return -1
}

func GetLoop(depth int) loop {
	fsi := getLoop(depth)
	if fsi == -1 { return nil }
	return flowStack[fsi].(loop)
}
func GetForeach(depth int) *foreachloop {
	fsi := getForeach(depth)
	if fsi == -1 { return nil }
	return flowStack[fsi].(*foreachloop)
}

func conditionallyLoop(increment float64) (int, bool) {
	fsi := getLoop(0)
	if fsi == -1 { return 0, false }
	lp := flowStack[fsi].(loop)
	if lp.Loop(increment) {
		Trace = make([]TraceFrame, len(lp.Trace()))
		copy(Trace, lp.Trace())
	} else {
		flowStack = flowStack[:fsi]
	}
	return lp.StartingDepth(), true
}
func ConditionallyLoop(increment float64) (int, bool) {
	select {
	case <-InterruptChannel:
		PrematurelyFinish()
	default:
		return conditionallyLoop(increment)
	}
	panic("unreachable")
}

func ClearFlowStack() { flowStack = nil }
func DropFlow() { flowStack = flowStack[:len(flowStack)-1] }
func DropLoop() bool {
	fsi := getLoop(0)
	if fsi == -1 { return false }
	flowStack = flowStack[:fsi]
	return true
}

func getFlowFrame(flow any) *TraceFrame {
	switch f := flow.(type) {
		case ifClause:
			trace := f.trace
			return &trace[len(trace)-1]
		case elseClause:
			trace := f.trace
			return &trace[len(trace)-1]
		case loop:
			trace := f.Trace()
			return &trace[len(trace)-1]
	}
	panic("unreachable")
}


func unterminatedControlFlow(traceback string) {
	fmt.Fprint(os.Stderr, traceback)
	Fatal("syntax error: unterminated control flow")
}

func skipIfClause() {
	emsg := tracebackLine(Frame())
	nesting := 1
	vocab := map[string]func() {
		"else": func() { if nesting == 1 { PushElse(); nesting-- } },
		"otherwise": func() { nesting-- },
		"then": func() { nesting-- },
		"if": func() { nesting++ },
	}
	for nesting != 0 && next() { DoNextWord(vocab) }
	if nesting != 0 { unterminatedControlFlow(emsg) }
}

func skipIfStatementOrChain() {
	emsg := tracebackLine(Frame())
	done := false
	vocab := map[string]func() {
		"else": func() { skipElse(); done = true },
		"otherwise": func() { skipOtherwises(); done = true },
		"then": func() { done = true },
		"if": func() { skipIfStatementOrChain() },
	}
	for !done && next() { DoNextWord(vocab) }
	if !done { unterminatedControlFlow(emsg) }
}

func skipElse() {
	emsg := tracebackLine(Frame())
	nesting := 1
	vocab := map[string]func() {
		"else": func() { SyntaxFatal("unexpected else") },
		"otherwise": func() { SyntaxFatal("unexpected otherwise") },
		"then": func() { nesting-- },
		"if": func() { skipIfStatementOrChain() },
	}
	for nesting != 0 && next() { DoNextWord(vocab) }
	if nesting != 0 { unterminatedControlFlow(emsg) }
}

func skipOtherwises() {
	emsg := tracebackLine(Frame())
	done := false
	vocab := map[string]func() {
		"if": func() { skipIfStatementOrChain(); done = true },
		"otherwise": func() {
			SyntaxFatal("unexpected otherwise after otherwise")
		},
		"then": func() {
			SyntaxFatal("unexpected then after otherwise")
		},
	}
	for !done && next() { DoNextWord(vocab) }
	if !done { unterminatedControlFlow(emsg) }
}

func If() {
	if pop().Num() == 0 {
		skipIfClause()
	} else {
		PushIf()
	}
}
func Else() {
	if !InIf() { SyntaxFatal("unexpected else") }
	skipElse()
	DropFlow()
}
func Otherwise() {
	if !InIf() { SyntaxFatal("unexpected otherwise") }
	skipOtherwises()
	DropFlow()
}
func Then() {
	if !InIf() && !InElse() { SyntaxFatal("unexpected then") }
	DropFlow()
}

func SkipLoop() {
	emsg := tracebackLine(Frame())
	nesting := 1
	vocab := map[string]func() {
		"for": func() { nesting++ },
		"#for": func() { nesting++ },
		"foreach": func() { nesting++ },
		"forever": func() { nesting++ },
		"loop": func() { nesting-- },
		"#loop": func() { nesting-- },
	}
	for nesting != 0 && next() { DoNextWord(vocab) }
	if nesting != 0 { unterminatedControlFlow(emsg) }
}
func Break(word string) {
	if ok := DropLoop(); !ok { SyntaxFatal("unexpected " + word + " outside of loop") }
	SkipLoop()
}

A  => src/helpers.go +68 -0
@@ 1,68 @@
package main

import (
	"errors"
	"os"
	"strings"
	"unicode"
)

func abs(n int) int {
	if n > 0 { return n } else { return -n }
}
func max(a, b int) int {
	if a > b { return a } else { return b }
}
func min(a, b int) int {
	if a < b { return a } else { return b }
}
func mod(a, b int) int {
	return (a % b + b) % b
}

func get[T any](s []T, index int, fallback T) T {
	if index < 0 { index += len(s) }
	if index < 0 || index >= len(s) { return fallback }
	return s[index]
}
func getElement(s []Element, index int, fallback Element) Element {
	if index < 0 { index += len(s) }
	if index < 0 || index >= len(s) { return fallback }
	return s[index]
}

func openRegularFile(name string) (*os.File, error) {
	f, err := os.Open(name)
	if info, err := f.Stat(); err == nil && info.IsDir() {
		return nil, errors.New("open " + name + ": is a directory")
	}
	return f, err
}

func stripNewline(s string) string {
	i := strings.LastIndex(s, "\n")
	if i != -1 { return s[:i] }
	return s
}

func strings_TrimLeftSpace(s string) string {
	return strings.TrimLeftFunc(s, unicode.IsSpace) 
}
func strings_TrimRightSpace(s string) string {
	return strings.TrimRightFunc(s, unicode.IsSpace) 
}
func strings_CutRight(s string, sep string) (string, string, bool) {
	i := strings.LastIndex(s, sep)
	if i == -1 { return "", s, false }
	return s[:i], s[i+1:], true
}
func strings_CutFunc(s string, f func(rune) bool) (string, string, bool) {
	i := strings.IndexFunc(s, f)
	if i == -1 { return s, "", false }
	return s[:i], s[i+1:], true
}
func strings_CutRightFunc(s string, f func(rune) bool) (string, string, bool) {
	i := strings.LastIndexFunc(s, f)
	if i == -1 { return "", s, false }
	return s[:i], s[i+1:], true
}

A  => src/input.go +213 -0
@@ 1,213 @@
package main

import (
	"bufio"
	"os"
	"regexp"
	"strings"
)

type Input struct {
	pop func() (string, bool)  // function to pop filepath

	started bool

	file *os.File
	name string  // rn path
	err error
	scanner *MyScanner

	usingSeperator bool
	recordSplitfunc bufio.SplitFunc
	fieldPattern *regexp.Regexp
	delimPattern *regexp.Regexp

	sep []byte

	// whether the record is no longer the scanner text
	// and to use the interlacement of .fields and .delims
	overridden bool

	fields []string
	delims []string
}

func NewInput(popFilepath func() (string, bool)) *Input {
	ip := Input{ pop: popFilepath, fieldPattern: regexp.MustCompile(`\S+`) }
	ip.SeparatorPattern(`\n`)
	return &ip
}

func (ip *Input) File() *os.File { return ip.file }
func (ip *Input) Name() string { return ip.name }
func (ip *Input) Err() error { return ip.err }
func (ip *Input) Sep() string { return string(ip.sep) }

func (ip *Input) label(record string) {
	if ip.fieldPattern != nil {
		ip.fields = ip.fieldPattern.FindAllString(record, -1)
		d := ip.fieldPattern.Split(record, -1)
		ip.delims = d[min(len(d)-1, 1):len(d)-1]
	} else {
		ip.delims = ip.delimPattern.FindAllString(record, -1)
		ip.fields = ip.delimPattern.Split(record, -1)
	}
}

func (ip *Input) unlabel() {
	ip.overridden = false
	ip.fields = nil
	ip.delims = nil
}

func (ip *Input) RecordPattern(pat string) {
	ip.usingSeperator = false
	fn := ScanRegex(pat, &ip.sep)
	ip.recordSplitfunc = fn
	if ip.scanner != nil {
		ip.scanner = NewMyScanner(ip.file)
		ip.scanner.Split(ip.recordSplitfunc)
	}
}
func (ip *Input) SeparatorPattern(pat string) {
	ip.usingSeperator = true
	fn := ScanRegexSeparated(pat, &ip.sep)
	ip.recordSplitfunc = fn
	if ip.scanner != nil {
		ip.scanner = NewMyScanner(ip.file)
		ip.scanner.Split(ip.recordSplitfunc)
	}
}

func (ip *Input) FieldPattern(pat string) {
	re := mustCompile(pat)
	ip.fieldPattern = re
	ip.delimPattern = nil
	ip.label(ip.Record())
}
func (ip *Input) DelimPattern(pat string) {
	re := mustCompile(pat)
	ip.delimPattern = re
	ip.fieldPattern = nil
	ip.label(ip.Record())
}

// Tries to take an arg from the Args stack and open it as the new input file.
// Returns true if there was an arg to take.
// Err() will give any error opening the file.
func (ip *Input) NextFile() bool {
	ip.overridden = false
	ip.err = nil
	ip.file.Close()
	if path, ok := ip.pop(); ok {
		ip.name = path
		ip.file, ip.err = openRegularFile(path)
		if ip.err != nil { return true }
	} else {
		if ip.started { return false }
		ip.file = os.Stdin
		ip.name = os.Stdin.Name()
	}
	ip.started = true
	ip.scanner = NewMyScanner(ip.file)
	ip.scanner.Split(ip.recordSplitfunc)
	return true
}

func (ip *Input) Scan() bool {
	ip.overridden = false
	ip.unlabel()
	if ip.scanner == nil { return false }
	if ip.scanner.Scan() { return true }
	if ip.scanner.Err() != nil { FatalWithoutTraceback(ip.scanner.Err().Error()) }
	ip.file = nil
	if ip.usingSeperator {
		// Make the record the skipped text that wasn't terminated by a separator
		ip.label(string(ip.sep))
		ip.overridden = true
		ip.sep = nil
	}
	return false
}

func (ip *Input) joinedParts() string {
	if len(ip.fields) == 0 { return "" }
	var sb strings.Builder
	for i := 0; i < len(ip.delims); i++ {
		sb.WriteString(ip.fields[i])
		sb.WriteString(ip.delims[i])
	}
	sb.WriteString(ip.fields[len(ip.fields)-1])
	return sb.String()
}
func (ip *Input) Record() string {
	if ip.overridden { return ip.joinedParts() }
	if ip.scanner == nil { return "" }
	return ip.scanner.Text()
}
func (ip *Input) SetRecord(s string) {
	ip.overridden = true
	ip.label(s)
}

func rightpadslice(s []string, n int, padding string) []string {
	for ; n > 0; n-- { s = append(s, padding) }
	return s
}
func leftpadslice(s []string, n int, padding string) []string {
	for ; n > 0; n-- { s = append([]string{ padding }, s...) }
	return s
}
func (ip *Input) SetField(i int, s string) {
	if ip.fields == nil { ip.label(ip.Record()) }
	if i < 0 { i += len(ip.fields) }
	if i >= 0 {
		ip.fields = rightpadslice(ip.fields, i-len(ip.delims), "")
		ip.delims = rightpadslice(ip.delims, i-len(ip.delims), " ")
		ip.fields[i] = s
	} else {
		ip.fields = leftpadslice(ip.fields, -i, "")
		ip.delims = leftpadslice(ip.delims, -i, " ")
		ip.fields[0] = s
	}
	ip.overridden = true
}
func (ip *Input) SetDelim(i int, s string) {
	if ip.delims == nil { ip.label(ip.Record()) }
	if i < 0 { i += len(ip.delims) }
	if i >= 0 {
		ip.fields = rightpadslice(ip.fields, i-len(ip.delims)+1, "")
		ip.delims = rightpadslice(ip.delims, i-len(ip.delims)+1, " ")
		ip.delims[i] = s
	} else {
		ip.fields = leftpadslice(ip.fields, -i+1, "")
		ip.delims = leftpadslice(ip.delims, -i+1, " ")
		ip.delims[0] = s
	}
	ip.overridden = true
}

func (ip *Input) Fields() []string {
	if ip.fields == nil { ip.label(ip.Record()) }
	return ip.fields
}
func (ip *Input) Field(i int) string {
	if i < 0 { i += len(ip.Fields()) }
	if i < 0 || i >= len(ip.Fields()) { return "" }
	return ip.Fields()[i]
}
func (ip *Input) Delims() []string {
	if ip.delims == nil { ip.label(ip.Record()) }
	return ip.delims
}
func (ip *Input) Delim(i int) string {
	if i < 0 { i += len(ip.Delims()) }
	if i < 0 || i >= len(ip.Delims()) { return "" }
	return ip.Delims()[i]
}

func (ip *Input) Bail() {
	ip.unlabel()
	ip.file = nil
	ip.scanner = nil
}

A  => src/main.go +121 -0
@@ 1,121 @@
package main

import (
	"os"
	"os/signal"
	"syscall"
)

var Tokens [][]string
var Markers []int
var MarkerNames map[int]string
var Setup Settings
var In *Input
var Out *Output


func init() {
	var args []string
	Tokens, Markers, MarkerNames, args, Setup = ParseCommand()

	a := "Args"
	Stacks[a] = make([]Element, len(args))
	for i := range args { Stacks[a][i] = Str(args[i]) }

	In = NewInput(func() (string, bool) {
		if len(Stacks[a]) == 0 { return "", false }
		var path Element
		path, Stacks[a] = Stacks[a][0], Stacks[a][1:]
		return string(path.Str()), true
	})

	Out = NewOutput()
	if Setup.Buffered {
		Out.Buffer()
	}
	if Setup.Inplace {
		Out.Withhold()
	}
}


var InterruptChannel = make(chan os.Signal, 1)
var RedoFB bool

func main() {

	var body TraceFrame  // the program after the first pass
	// First pass
	{
		words := map[string]func() {
			"#!": Builtins["#!"],
			"#": Builtins["#"],
			":": Builtins[":"],
			"B": func() { ExecuteBlock("B", getBlock()) },
			"E": Builtins["E"],
			"FB": Builtins["FB"],
			"FE": Builtins["FE"],
			"PE": Builtins["PE"],
			"PRENUM": Builtins["PRENUM"],
			"POSTNUM": Builtins["POSTNUM"],
			"TOKEN": Builtins["TOKEN"],
		}
		setTraceToBlock("", Tokens)
		for _, m := range Markers {
			if Frame().li > m { continue }
			if Frame().li < m { Frame().li = m }
			for !exhausted() && doVocab(tok(), words) { next() }
		}

		AssertNoControlFlow()

		if exhausted() && len(Events.E) == 0 && len(Events.FB) == 0 && len(Events.FE) == 0 {
			PrematurelyFinish()
		}

		if !exhausted() { body = *Frame() }
	}

	signal.Notify(InterruptChannel, os.Interrupt, syscall.SIGTERM)
	MAINLOOP:
	for {
		select {
		case <-InterruptChannel:
			PrematurelyFinish()
		default:
			if In.File() == nil {
				if In.started {
					ExecuteBlock("FE", Events.FE)
					if Setup.Inplace && Out.WithheldNum() == 1 {
						s, _ := Out.PopWithheld()
						if err := os.WriteFile(In.Name(), []byte(s), 0644); err != nil {
							FatalWithoutTraceback(err.Error())
						}
						Out.Withhold()
					}
				}

				PrivateValues.N = 0
				for {
					if !In.NextFile() { break MAINLOOP }
					RedoFB = false
					ExecuteBlock("FB", Events.FB)
					if !RedoFB { break }
				}
				if In.File() == nil {
					FatalWithoutTraceback(In.Err().Error())
				}
			}

			if In.Scan() {
				Trace = []TraceFrame{ body }
				PrivateValues.n++
				PrivateValues.N++
				Execute()
				AssertNoControlFlow()
			}
		}
	}

	Finish()
}

A  => src/manipwords.go +38 -0
@@ 1,38 @@
package main

import "sort"

func kill(elems []Element, indicies []int) []Element {
	for i := range indicies {
		if indicies[i] < 0 { indicies[i] = len(elems)+indicies[i] }
	}
	var s []Element
	OUTER:
	for ei := range elems {
		for _, index := range indicies {
			if index == len(elems)-1 - ei { continue OUTER }
		}
		s = append(s, elems[ei])
	}
    return s
}

func compose(elems []Element, indicies []int) []Element {
	s := make([]Element, len(indicies))
	for i := range indicies {
		s[i] = getElement(elems, indicies[i], Str(""))
	}
    return s
}

func Sort(elems []Element, by [][]string) {
	traceBackup := Trace
	sort.SliceStable(elems, func(i, j int) bool {
		setTraceToBlock("sort", by)
		push(elems[i])
		push(elems[j])
		Execute()
		return popBool()
	})
	Trace = traceBackup
}

A  => src/mathwords.go +98 -0
@@ 1,98 @@
package main

import (
	"math"
)

func SliceMin(floats []float64) float64 {
	if len(floats) == 0 { return 0.0 }
	r := floats[0]
	for _, f := range floats { r = math.Min(r, f) }
	return r
}
func SliceMax(floats []float64) float64 {
	if len(floats) == 0 { return 0.0 }
	r := floats[0]
	for _, f := range floats { r = math.Max(r, f) }
	return r
}

func Sum(floats []float64) float64 {
	if len(floats) == 0 { return 0.0 }  // why?
	r := 0.0
	for _, f := range floats { r += f }
	return r
}
func Mean(floats []float64) float64 {
	return Sum(floats) / float64(len(floats))
}

func LogicalOr(elems []Element) Element {
	for _, e := range elems { if e.Num() != 0 { return e } }
	return Num(0)
}
func LogicalAnd(elems []Element) Element {
	for _, e := range elems { if e.Num() == 0 { return Num(0) } }
	return elems[len(elems)-1]
}

// TODO maybe do euclidean modulo
func Modulo(a, b float64) float64 {
	if b == 0 { ArithmeticFatal("division by zero") }
	return a - b * math.Floor(a / b)
}

func Pow(a, b float64) float64 {
	if a < 0 && b != math.Floor(b) { ArithmeticFatal("pow received negative number to fractional power") }
	if a == 0 && b < 0 { ArithmeticFatal("pow received zero to negative power") }
	return math.Pow(a, b)
}
func Sqrt(n float64) float64 {
	if n < 0 { ArithmeticFatal("sqrt received negative number") }
	return math.Sqrt(n)
}
func Asin(n float64) float64 {
	if n < -1 || n > 1 { ArithmeticFatal("asin received out of range number") }
	return math.Asin(n)
}
func Acos(n float64) float64 {
	if n < -1 || n > 1 { ArithmeticFatal("acos received out of range number") }
	return math.Acos(n)
}
func Log(n float64) float64 { 
	if n == 0 { ArithmeticFatal("log received zero") }
	if n < 0 { ArithmeticFatal("log received negative argument") }
	return math.Log(n)
}
func Exp(n float64) float64 { 
	r := math.Exp(n)
	if math.IsInf(r, 1) { r = math.MaxFloat64 }
	return r
}

func BitwiseAnd(a, b int) int {
	if (a < 0 && b > 0) { return -a & b }
	if (b < 0 && a > 0) { return a & -b }
	if (a < 0 && b < 0) { return -(-a & -b) }
	return a & b
}
func BitwiseOr(a, b int) int {
	if (a < 0 && b > 0) { return -(-a | b) }
	if (b < 0 && a > 0) { return -(a | -b) }
	if (a < 0 && b < 0) { return -(-a | -b) }
	return a | b
}
func BitwiseXor(a, b int) int {
	if (a < 0 && b > 0) { return -(-a | b) }
	if (b < 0 && a > 0) { return -(a | -b) }
	if (a < 0 && b < 0) { return -a | -b }
	return a | b
}

func elementwise(s1, s2 []float64, op func(float64,float64) float64) []float64 {
	s := make([]float64, max(len(s1), len(s2)))
	for i := range s {
		s[i] = op(get(s1, i, 0), get(s2, i, 0))
	}
	return s
}

A  => src/myscanner.go +28 -0
@@ 1,28 @@
package main

import (
	"bufio"
	"io"
)

type MyScanner struct {
	*bufio.Scanner
}

func NewMyScanner(r io.Reader) *MyScanner {
	return &MyScanner{ bufio.NewScanner(r) }
}

func (sc *MyScanner) Scan() bool {
	c := make(chan bool, 1)
	go func(){
		c <- sc.Scanner.Scan()
	}()
	select {
	case <-InterruptChannel:
		PrematurelyFinish()
	case result := <-c:
		return result
	}
	panic("unreachable")
}

A  => src/optparse.go +84 -0
@@ 1,84 @@
package main

import (
	"strings"
	"git.sr.ht/~geb/opt"
)

type OptstringError struct { message string }
func (e *OptstringError) Error() string { return e.message }

func optparse(optstring string, strs []string) (map[string]Element, []string, error) {
	o := opt.NewOptionSet()
	m := map[string]Element{}
	// TODO don't fatal on repeats (redefinitions)
	for _, field := range strings.Fields(optstring) {
		name, symbol := field[:len(field)-1], field[len(field)-1]
		if symbol == '?' {
			o.BoolFunc(name, func(b bool) error {
				if b { m[name] = Num(1) } else { m[name] = Num(0) }
				return nil
			})
		} else if symbol == ':' {
			o.Func(name, func(s string) error {
				m[name] = Str(s)
				return nil
			})
		} else {
			return nil, nil, &OptstringError{"optstring field missing argument specifier: " + field}
		}
	}

	if err := o.Parse(true, strs); err != nil {
		return nil, nil, err
	}

	return m, o.Args(), nil
}

func optclean(optstring string, arguments []string) ([]string, error) {