diff --git a/HsYAML.cabal b/HsYAML.cabal
new file mode 100644
--- /dev/null
+++ b/HsYAML.cabal
@@ -0,0 +1,94 @@
+cabal-version:       1.14
+name:                HsYAML
+version:             0.1.0.0
+
+synopsis:            Pure Haskell YAML 1.2 parser
+homepage:            https://github.com/hvr/HsYAML
+license:             GPL-3
+license-file:        LICENSE
+author:              Herbert Valerio Riedel
+maintainer:          hvr@gnu.org
+copyright:           2015-2018 Herbert Valerio Riedel
+                   , 2007-2008 Oren Ben-Kiki
+category:            Text
+build-type:          Simple
+tested-with:         GHC==8.4.3, GHC==8.2.2, GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2
+
+description:
+  @HsYAML@ is a [YAML 1.2](http://yaml.org/spec/1.2/spec.html) parser for Haskell.
+  .
+  Features include:
+  .
+   * Pure Haskell implementation with small dependency footprint and emphasis on strict compliance with the [YAML 1.2 specification](http://yaml.org/spec/1.2/spec.html)
+   * Low-level API access to lexical token-based scanner ("Data.YAML.Token")
+   * Event-based API resembling LibYAML's Event-based API ("Data.YAML.Event")
+   * Support for constructing custom YAML node graph representation (including support for cyclic YAML data structures)
+   * Direct decoding via typeclass-based API similiar to @aeson@ ("Data.YAML")
+   * Support for /Failsafe/, /JSON/ and /Core/ schemas as defined in the YAML 1.2 specification (including support for user-defined custom schemas)
+
+source-repository head
+  type: git
+  location: https://github.com/hvr/HsYAML.git
+
+flag exe
+  description: Enable @exe:yaml-test@ component
+  manual: True
+  default: False
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Data.YAML
+                     , Data.YAML.Event
+                     , Data.YAML.Token
+  other-modules:       Data.YAML.Loader
+                     , Data.YAML.Schema
+                     , Util
+
+  default-language:    Haskell2010
+  other-extensions:    FlexibleContexts
+                       FlexibleInstances
+                       FunctionalDependencies
+                       MultiParamTypeClasses
+                       OverloadedStrings
+                       PostfixOperators
+                       RecordWildCards
+                       RecursiveDo
+                       Safe
+                       ScopedTypeVariables
+                       Trustworthy
+                       TypeSynonymInstances
+
+  build-depends:       base         >=4.5   && <4.12
+                     , bytestring   >=0.9   && <0.11
+                     , dlist        >=0.8   && <0.9
+                     , containers   >=0.4.2 && <0.6
+                     , text         >=1.2.3 && <1.3
+                     , mtl          >=2.2.1 && <2.3
+                     , parsec       >=3.1.13.0 && < 3.2
+
+  ghc-options:         -Wall
+
+executable yaml-test
+  hs-source-dirs: src-test
+  main-is: Main.hs
+  other-modules: TML
+
+  default-language:    Haskell2010
+
+  if flag(exe)
+    build-depends: HsYAML
+                   -- inherited constraints
+                 , bytestring >= 0.10.8.0
+                 , base
+                 , text
+                 , containers
+                 , mtl
+                   -- non-inherited
+                 , megaparsec  >= 6.5.0 && < 6.6
+                 , microaeson  == 0.1.*
+                 , filepath    == 1.4.*
+                 , directory   >= 1.2 && < 1.4
+  else
+    buildable: False
+
+  ghc-options: -rtsopts
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+              GNU GENERAL PUBLIC LICENSE
+                Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The GNU 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 <http://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
+<http://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
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src-test/Main.hs b/src-test/Main.hs
new file mode 100644
--- /dev/null
+++ b/src-test/Main.hs
@@ -0,0 +1,331 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+module Main where
+
+import           Control.Monad
+import           Control.Monad.Identity
+import qualified Data.ByteString.Char8      as BS
+import qualified Data.ByteString.Lazy.Char8 as BS.L
+import           Data.Int                   (Int64)
+import           Data.Maybe
+import           System.Directory
+import           System.Environment
+import           System.Exit
+import           System.FilePath
+import           System.IO
+import           Text.Read
+
+import qualified Data.Aeson.Micro           as J
+import           Data.Map                   (Map)
+import qualified Data.Map                   as Map
+import           Data.Text                  (Text)
+import qualified Data.Text                  as T
+import qualified Data.Text.Encoding         as T
+import qualified Data.Text.IO               as T
+
+import           Data.YAML                  as Y
+import           Data.YAML.Event            as YE
+import qualified Data.YAML.Token            as YT
+
+import qualified TML
+
+main :: IO ()
+main = do
+  args <- getArgs
+
+  case args of
+    ("yaml2event":args')
+      | null args' -> cmdYaml2Event
+      | otherwise -> do
+          hPutStrLn stderr "unexpected arguments passed to yaml2event sub-command"
+          exitFailure
+
+    ("yaml2json":args')
+      | null args' -> cmdYaml2Json
+      | otherwise -> do
+          hPutStrLn stderr "unexpected arguments passed to yaml2json sub-command"
+          exitFailure
+
+    ("run-tml":args') -> cmdRunTml args'
+
+    ("testml-compiler":args') -> cmdTestmlCompiler args'
+
+    _ -> do
+      hPutStrLn stderr "usage: yaml-test <command> [<args>]"
+      hPutStrLn stderr ""
+      hPutStrLn stderr "Commands:"
+      hPutStrLn stderr ""
+      hPutStrLn stderr "  yaml2event       reads YAML stream from STDIN and dumps events to STDOUT"
+      hPutStrLn stderr "  yaml2json        reads YAML stream from STDIN and dumps JSON to STDOUT"
+      hPutStrLn stderr "  run-tml          run/validate YAML-specific .tml file(s)"
+      hPutStrLn stderr "  testml-compiler  emulate testml-compiler"
+
+      exitFailure
+
+
+cmdYaml2Event :: IO ()
+cmdYaml2Event = do
+  inYamlDat <- BS.L.getContents
+  forM_ (parseEvents inYamlDat) $ \ev -> case ev of
+    Left (ofs,msg) -> do
+      case msg of
+        "" -> hPutStrLn stderr ("parsing error near byte offset " ++ show ofs)
+        _  -> hPutStrLn stderr ("parsing error near byte offset " ++ show ofs ++ " (" ++ msg ++ ")")
+      exitFailure
+    Right event -> do
+      hPutStrLn stdout (ev2str event)
+      hFlush stdout
+
+-- | 'J.Value' look-alike
+data Value' = Object'  (Map Text Value')
+            | Array'   [Value']
+            | String'  !Text
+            | NumberD' !Double
+            | NumberI' !Integer
+            | Bool'    !Bool
+            | Null'
+            deriving Show
+
+toProperValue :: Value' -> J.Value
+toProperValue v = case v of
+  Null'      -> J.Null
+  String' t  -> J.String t
+  NumberD' x -> J.Number x
+  NumberI' x -> J.Number (fromInteger x)
+  Bool' b    -> J.Bool b
+  Array' xs  -> J.Array (map toProperValue xs)
+  Object' xs -> J.Object (fmap toProperValue xs)
+
+instance FromYAML Value' where
+  parseYAML (Y.Scalar s) = case s of
+    SNull        -> pure Null'
+    SBool b      -> pure (Bool' b)
+    SFloat x     -> pure (NumberD' x)
+    SInt x       -> pure (NumberI' x)
+    SStr t       -> pure (String' t)
+    SUnknown _ t -> pure (String' t) -- HACK
+
+  parseYAML (Y.Sequence _ xs) = Array' <$> mapM parseYAML xs
+
+  parseYAML (Y.Mapping _ m) = Object' . Map.fromList <$> mapM parseKV (Map.toList m)
+    where
+      parseKV :: (Y.Node,Y.Node) -> Parser (Text,Value')
+      parseKV (k,v) = (,) <$> parseK k <*> parseYAML v
+
+      -- for numbers and !!null we apply implicit conversions
+      parseK n = do
+        k <- parseYAML n
+        case k of
+          NumberI' t -> pure (T.pack (show t))
+          NumberD' t -> pure (T.pack (show t))
+          String' t  -> pure t
+          Null'      -> pure ""
+          _          -> fail ("dictionary entry had non-string key " ++ show k)
+
+decodeAeson :: BS.L.ByteString -> Either String [J.Value]
+decodeAeson = fmap (map toProperValue) . decode
+
+-- | Try to convert 'Double' into 'Int64', return 'Nothing' if not
+-- representable loss-free as integral 'Int64' value.
+doubleToInt64 :: Double -> Maybe Int64
+doubleToInt64 x
+  | fromInteger x' == x
+  , x' <= toInteger (maxBound :: Int64)
+  , x' >= toInteger (minBound :: Int64)
+    = Just (fromIntegral x')
+  | otherwise = Nothing
+  where
+    x' = round x
+
+
+decodeNumber :: T.Text -> Maybe Double
+decodeNumber = readMaybe . T.unpack -- fixme
+
+cmdYaml2Json :: IO ()
+cmdYaml2Json = do
+  inYamlDat <- BS.L.getContents
+
+  case decodeAeson inYamlDat of
+    Left e -> fail e
+    Right vs -> do
+      forM_ vs $ \v -> BS.L.putStrLn (J.encode v)
+
+  return ()
+
+unescapeSpcTab :: T.Text -> T.Text
+unescapeSpcTab = T.replace "<SPC>" " " . T.replace "<TAB>" "\t"
+
+cmdRunTml :: [FilePath] -> IO ()
+cmdRunTml args = do
+  results <- forM args $ \fn -> do
+    tml <- BS.readFile fn
+
+    hPutStr stdout (fn ++ " : ")
+    hFlush stdout
+
+    TML.Document _ blocks <- either (fail . T.unpack) pure $ TML.parse fn (T.decodeUtf8 tml)
+
+    forM blocks $ \(TML.Block label points) -> do
+
+      let dats = [ (k,v) | TML.PointStr k v <- points ]
+
+      let isErr = isJust (lookup "error" dats)
+
+          Just inYamlDat = BS.L.fromStrict . T.encodeUtf8 . unescapeSpcTab <$> lookup "in-yaml" dats
+          Just testEvDat = lines . T.unpack . unescapeSpcTab <$> lookup "test-event" dats
+
+          mInJsonDat :: Maybe [J.Value]
+          mInJsonDat = (maybe (error ("invalid JSON in " ++ show fn)) id . J.decodeStrictN . T.encodeUtf8) <$> lookup "in-json" dats
+
+      case sequence (parseEvents inYamlDat) of
+        Left err
+          | isErr -> do
+              putStrLn "OK! (error)"
+              pure True
+          | otherwise -> do
+              putStrLn "FAIL!"
+              putStrLn ""
+              putStrLn "----------------------------------------------------------------------------"
+              putStrLn' (T.unpack label)
+              putStrLn ""
+              putStrLn' (show err)
+              putStrLn ""
+              putStrLn' (show testEvDat)
+              putStrLn ""
+              BS.L.putStr inYamlDat
+              putStrLn ""
+              testParse inYamlDat
+              putStrLn ""
+              -- forM_ (parseEvents inYamlDat) (putStrLn' . show)
+              putStrLn ""
+              putStrLn "----------------------------------------------------------------------------"
+              putStrLn ""
+              pure False
+
+        Right evs' -> do
+          let evs'' = map ev2str evs'
+          if evs'' == testEvDat
+             then do
+
+               case mInJsonDat of
+                 Nothing -> do
+                   putStrLn "OK!"
+                   pure True
+                 Just inJsonDat -> do
+                   iutJson <- either fail pure $ decodeAeson inYamlDat
+
+                   if iutJson == inJsonDat
+                     then do
+                       putStrLn "OK! (+JSON)"
+                       pure True
+                     else do
+                       putStrLn "FAIL! (bad JSON)"
+
+                       putStrLn' ("ref = " ++ show inJsonDat)
+                       putStrLn' ("iut = " ++ show iutJson)
+
+                       pure False
+
+             else do
+               if isErr
+                 then putStrLn "FAIL! (unexpected parser success)"
+                 else putStrLn "FAIL!"
+
+               putStrLn ""
+               putStrLn "----------------------------------------------------------------------------"
+               putStrLn' (T.unpack label)
+               putStrLn ""
+               putStrLn' ("ref = " ++ show testEvDat)
+               putStrLn' ("iut = " ++ show evs'')
+               putStrLn ""
+               BS.L.putStr inYamlDat
+               putStrLn ""
+               testParse inYamlDat
+               putStrLn ""
+               -- forM_ (parseEvents inYamlDat) (putStrLn' . show)
+               putStrLn ""
+               putStrLn "----------------------------------------------------------------------------"
+               putStrLn ""
+               pure False
+
+  putStrLn ""
+
+  let ok = length (filter id results')
+      nok = length (filter not results')
+      results' = concat results
+
+  putStrLn ("done (passed: " ++ show ok ++ " / failed: " ++ show nok ++ ")")
+
+
+-- | Incomplete proof-of-concept 'testml-compiler' operation
+cmdTestmlCompiler :: [FilePath] -> IO ()
+cmdTestmlCompiler [fn0] = do
+  (fn,raw) <- case fn0 of
+    "-" -> (,) "<stdin>" <$> T.getContents
+    _   -> (,) fn0 <$> T.readFile fn0
+
+  case TML.parse fn raw of
+    Left e    -> T.hPutStrLn stderr e >> exitFailure
+    Right doc -> BS.putStrLn (J.encodeStrict doc)
+cmdTestmlCompiler _ = do
+  hPutStrLn stderr ("Usage: yaml-test testml-compiler [ <testml-file-name> | - ]")
+  exitFailure
+
+
+putStrLn' :: String -> IO ()
+putStrLn' msg = putStrLn ("  " ++ msg)
+
+
+ev2str :: Event -> String
+ev2str StreamStart           = "+STR"
+ev2str (DocumentStart True)  = "+DOC ---"
+ev2str (DocumentStart False) = "+DOC"
+ev2str MappingEnd            = "-MAP"
+ev2str (MappingStart manc mtag)    = "+MAP" ++ ancTagStr manc mtag
+ev2str SequenceEnd           = "-SEQ"
+ev2str (SequenceStart manc mtag)   = "+SEQ" ++ ancTagStr manc mtag
+ev2str (DocumentEnd True) = "-DOC ..."
+ev2str (DocumentEnd False) = "-DOC"
+ev2str StreamEnd             = "-STR"
+ev2str (Alias a)             = "=ALI *" ++ T.unpack a
+ev2str (YE.Scalar manc mtag sty v) = "=VAL" ++ ancTagStr manc mtag ++ v'
+  where
+    v' = case sty of
+           Plain        -> " :"  ++ quote2 v
+           DoubleQuoted -> " \"" ++ quote2 v
+           Literal      -> " |"  ++ quote2 v
+           Folded       -> " >"  ++ quote2 v
+           SingleQuoted -> " '"  ++ quote2 v
+
+ancTagStr manc mtag = anc' ++ tag'
+  where
+    anc' = case manc of
+             Nothing  -> ""
+             Just anc -> " &" ++ T.unpack anc
+
+    tag' = case tagToText mtag of
+             Nothing -> ""
+             Just t  -> " <" ++ T.unpack t ++ ">"
+
+
+quote2 :: T.Text -> String
+quote2 = concatMap go . T.unpack
+  where
+    go c | c == '\n' = "\\n"
+         | c == '\t' = "\\t"
+         | c == '\b' = "\\b"
+         | c == '\r' = "\\r"
+         | c == '\\' = "\\\\"
+         | otherwise = [c]
+
+
+
+testParse :: BS.L.ByteString -> IO ()
+testParse bs0 = mapM_  (putStrLn' . showT) $ YT.tokenize bs0 False
+  where
+    showT :: YT.Token -> String
+    showT t = replicate (YT.tLineChar t) ' ' ++ show (YT.tText t) ++ "  " ++ show (YT.tCode t)
diff --git a/src-test/TML.hs b/src-test/TML.hs
new file mode 100644
--- /dev/null
+++ b/src-test/TML.hs
@@ -0,0 +1,433 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+-- Incomplete TestML 0.3.0 parser
+module TML
+    ( TML.parse
+
+    , Document(..)
+    , Block(..)
+
+    , Point(..)
+    , PseudoId(..)
+
+    , Code(..)
+    , AssertOp(..)
+    , CodeExpr(..)
+    , CodeObject(..)
+    , FunCall(..)
+    ) where
+
+import           Data.ByteString            (ByteString)
+import qualified Data.ByteString.Char8      as B
+
+import           Control.Applicative        hiding (many, some)
+import           Control.Monad
+import qualified Data.Aeson.Micro           as J
+import qualified Data.ByteString            as BS
+import qualified Data.Char                  as C
+import           Data.List
+import qualified Data.Map                   as Map
+import           Data.Maybe
+import qualified Data.Text                  as T
+import qualified Data.Text.Encoding         as T
+import qualified Data.Text.IO               as T
+import           Data.Void
+import           System.Environment
+import           Text.Megaparsec
+import           Text.Megaparsec.Char
+import qualified Text.Megaparsec.Char.Lexer as L
+import           Text.Megaparsec.Expr
+
+type Parser = Parsec Void T.Text
+
+parse :: String -> T.Text -> Either T.Text Document
+parse fn raw = either (Left . T.pack . parseErrorPretty' raw)
+                      (Right .process_pseudo)
+                      (Text.Megaparsec.parse testml_document fn raw)
+
+
+----------------------------------------------------------------------------
+
+data Document = Document [Code] [Block]
+              deriving Show
+
+instance J.ToJSON Document where
+  toJSON (Document code dat)
+    = J.object [ "testml" J..= ("0.3.0" :: T.Text)
+               , "code" J..= code
+               , "data" J..= dat
+               ]
+
+data Block = Block !T.Text [Point]
+           deriving Show
+
+instance J.ToJSON Block where
+  toJSON (Block label points)
+    = J.object [ "label" J..= label
+               , "point" J..= J.object (map f points)
+               ]
+    where
+      f (PointStr k v)  = k J..= v
+      f (PointPseudo k) = (T.pack (show k)) J..= True
+      f (PointInt k v)  = k J..= v
+
+data Point = PointStr !T.Text !T.Text
+           | PointInt !T.Text !Integer
+           | PointPseudo !PseudoId
+           deriving Show
+
+instance J.ToJSON Code where
+  toJSON (CodeAssignmentStmt lhs rhs)
+    = J.Array [J.String "=", J.String lhs, J.toJSON rhs]
+  toJSON stmt@(CodeExpressionStmt lhs massert)
+    | pobjs@(_:_) <- pointObjsInExpr stmt
+      = J.Array [ J.String "%()"
+                , J.Array [ J.String ("*" `mappend` p) | p <- pobjs ]
+                , expr'
+                ]
+    | otherwise = expr'
+    where
+      expr' = case massert of
+                Just (op,rhs) -> J.toJSON (op,lhs,rhs)
+                Nothing       -> J.toJSON lhs
+
+data Code = CodeAssignmentStmt !T.Text !CodeExpr
+          | CodeExpressionStmt !CodeExpr !(Maybe (AssertOp,CodeExpr))
+          | CodeImportStmt [T.Text]
+          deriving Show
+
+instance J.ToJSON AssertOp where
+  toJSON AssertEq   = J.String "=="
+  toJSON AssertHas  = J.String "~~"
+  toJSON AssertLike = J.String "=~"
+
+data AssertOp = AssertEq | AssertHas | AssertLike
+              deriving Show
+
+instance J.ToJSON CodeExpr where
+  toJSON (CodeExpr obj [])  = J.toJSON obj
+  toJSON (CodeExpr obj fns) = J.Array $ [J.String ".", J.toJSON obj] ++ map J.toJSON fns
+
+data CodeExpr = CodeExpr !CodeObject [FunCall]
+  deriving Show
+
+instance J.ToJSON CodeObject where
+  toJSON (StrObj s)   = J.String s
+  toJSON (NumObj n)   = J.Number n
+  toJSON (PointObj j) = J.Array [J.String "*", J.String j]
+  toJSON (CallObj fn) = J.toJSON fn
+
+data CodeObject = StrObj !T.Text
+                | CallObj !FunCall
+                | NumObj !Double
+                | PointObj !T.Text
+                deriving Show
+
+instance J.ToJSON FunCall where
+  toJSON (FunCall fn args) = J.Array (J.String fn : map J.toJSON args)
+
+data FunCall = FunCall !T.Text [CodeExpr]
+  deriving Show
+
+
+
+pointObjsInExpr :: Code -> [T.Text]
+pointObjsInExpr co = nub $ case co of
+    CodeAssignmentStmt _ expr           -> goExpr expr
+    CodeExpressionStmt e1 Nothing       -> goExpr e1
+    CodeExpressionStmt e1 (Just (_,e2)) -> goExpr e1 ++ goExpr e2
+  where
+    goExpr (CodeExpr obj fns) = goObj obj ++ concatMap goFun fns
+
+    goFun (FunCall _ exprs) = concatMap goExpr exprs
+
+    goObj (PointObj j) = [j]
+    goObj (CallObj fn) = goFun fn
+    goObj (StrObj _)   = []
+    goObj (NumObj _)   = []
+
+
+testml_document :: Parser Document
+testml_document = Document <$> code_section <*> data_section <* eof
+
+pseudo_point_name  :: Parser PseudoId
+pseudo_point_name
+  = choice [ HEAD <$ string "HEAD"
+           , LAST <$ string "LAST"
+           , ONLY <$ string "ONLY"
+           , SKIP <$ string "SKIP"
+           , TODO <$ string "TODO"
+           , DIFF <$ string "DIFF"
+           ]
+
+data PseudoId = HEAD
+              | LAST
+              | ONLY
+              | SKIP
+              | TODO
+              | DIFF
+              deriving (Eq,Show)
+
+process_pseudo :: Document -> Document
+process_pseudo (Document code bs0) = Document code (go bs0)
+  where
+    go blocks
+      | Just b <- find isOnly blocks' = [b]
+      | Just bs <- goHead blocks'     = bs
+      | Just bs <- goLast [] blocks'  = bs
+      | otherwise = blocks'
+      where
+        blocks' = filter (not . isSkip) blocks
+
+        isOnly b = ONLY `elem` pseudos b
+        isSkip b = SKIP `elem` pseudos b
+        isHead b = HEAD `elem` pseudos b
+        isLast b = LAST `elem` pseudos b
+
+        pseudos (Block _ ps) = [ k | PointPseudo k <- ps ]
+
+        goHead [] = Nothing
+        goHead (b:bs)
+          | isHead b = Just (b:bs)
+          | otherwise = goHead bs
+
+        goLast acc [] = Nothing
+        goLast acc (b:bs)
+          | isLast b  = Just $ reverse (b:bs)
+          | otherwise = goLast (b:acc) bs
+
+code_section :: Parser [Code]
+code_section = do
+    xs <- many code_statement
+    pure (catMaybes xs)
+  where
+    code_statement = choice
+      [ Nothing <$ comment_lines
+      , Just <$> import_directive
+      , Just <$> assignment_statement
+      , Just <$> expression_statement
+      ]
+
+    import_directive = do
+      string "%Import"
+      ws
+      mods <- module_name `sepBy1` ws
+      ws0
+      eol
+      pure $! CodeImportStmt mods
+
+    module_name :: Parser T.Text
+    module_name = T.pack <$> some alphaNumChar
+
+    assignment_statement = do
+      v <- try $ do
+        v' <- identifier_name
+        ws
+        void (char '=') <|> void (string "||=") -- FIXME
+        ws
+        pure v'
+      e <- code_expression
+      eol
+      pure (CodeAssignmentStmt v e)
+
+    expression_statement = do
+      -- TODO: expression-label
+      -- optional (double_string >> char ':' >> ws0)
+
+      -- TODO: pick-expression
+
+      lhs <- code_expression
+      ws
+      op <- choice
+            [ AssertEq   <$ string "=="
+            , AssertHas  <$ string "~~"
+            , AssertLike <$ string "=~"
+            ]
+      ws
+      rhs <- code_expression
+
+      optional $ do
+        ws0
+        char ':'
+        double_string
+
+      eol
+
+      pure (CodeExpressionStmt lhs (Just (op,rhs)))
+
+
+code_expression :: Parser CodeExpr
+code_expression = CodeExpr <$> code_object <*> many function_call
+
+
+-- quoted string
+double_string :: Parser T.Text
+double_string = do
+    char '"'
+    str <- many (noneOf ("\n\"\\" :: [Char]) <|> (char '\\' >> (unesc <$> oneOf ("\\\"0nt" :: [Char]))))
+    char '"'
+    pure $! (T.pack str)
+  where
+    unesc '0' = '\0'
+    unesc 'n' = '\n'
+    unesc 't' = '\t'
+    unesc c   = c
+
+single_string :: Parser T.Text
+single_string = do
+    char '\''
+    str <- many (noneOf ("\n'\\" :: [Char]) <|> (char '\\' >> (oneOf ("\\'" :: [Char]))))
+    char '\''
+    pure $! (T.pack str)
+
+function_call :: Parser FunCall
+function_call = do
+  char '.'
+  call_object
+
+call_object :: Parser FunCall
+call_object = FunCall <$> identifier_name
+                      <*> optional' [] (between (char '(') (char ')') $ code_expression `sepBy1` (char ',' >> ws0))
+
+optional' def p = do
+  x <- optional p
+  case x of
+    Nothing -> pure def
+    Just y  -> pure y
+
+code_object :: Parser CodeObject
+code_object
+  = choice
+    [ mkPoint <$> char '*' <*> lowerChar <*> many (lowerChar <|> digitChar <|> char '-' <|> char '_')
+    , mkNum <$> optional (char '-') <*> some digitChar <*> optional (char '.' >> some digitChar)
+    , CallObj <$> call_object
+    , StrObj <$> single_string
+    , StrObj <$> double_string
+    ] <?> "code-object"
+  where
+    mkPoint _ c cs = PointObj $! (T.pack (c:cs))
+    mkNum msign ds1 mds2 = NumObj $! (read $ (maybe id (:) msign) ds1 ++ (maybe "" ('.':) mds2))
+
+data_section :: Parser [Block]
+data_section = many block_definition
+  where
+    block_definition = do
+      -- block_heading
+      string "===" *> ws
+      l <- T.pack <$> manyTill anyChar eol
+
+      -- TODO: user_defined
+      ps <- many point_definition
+      pure (Block l ps)
+
+    point_definition = do
+      string "---" *> ws
+
+      j <- eitherP identifier_user pseudo_point_name
+
+      filters <- maybe [] id <$> optional filter_spec
+
+      let single = do
+            _ <- char ':' *> ws
+            x <- T.pack <$> manyTill anyChar eol
+            -- consume and ignore any point_lines
+            _ <- point_lines
+            pure $! case j of
+                      Left j'  -> mkSinglePointVal j' (transformPoint True filters x)
+                      Right j' -> PointPseudo j' -- is this allowed?
+
+          multi = do
+            ws0 *> eol
+            x <- point_lines
+            pure $! case j of
+                      Left j'  -> PointStr j' (transformPoint False filters x)
+                      Right j' -> PointPseudo j'
+
+      single <|> multi
+
+    filter_spec = between (char '(') (char ')') $ many (oneOf ("<#+-~/@" :: [Char]))
+
+    mkSinglePointVal k v
+      | T.all C.isDigit v = PointInt k (read (T.unpack v))
+      | otherwise         = PointStr k v
+
+point_lines :: Parser T.Text
+point_lines = T.pack . unlines <$> go
+  where
+    go = many (notFollowedBy point_boundary *> manyTill anyChar eol)
+
+    point_boundary :: Parser ()
+    point_boundary = void (string "---") <|> void (string "===") <|> eof
+
+identifier_user :: Parser T.Text
+identifier_user = do
+  x  <- (:) <$> lowerChar <*> many alphaNumChar
+  xs <- many ((:) <$> char '-' <*> some alphaNumChar)
+
+  pure $! T.pack (concat (x:xs))
+
+identifier_name :: Parser T.Text
+identifier_name = do
+  x  <- (:) <$> letterChar <*> many alphaNumChar
+  xs <- many ((:) <$> char '-' <*> some alphaNumChar)
+
+  pure $! T.pack (concat (x:xs))
+
+
+ws :: Parser ()
+ws = void $ takeWhile1P (Just "BLANK") (\c -> c == ' ' || c == '\t')
+
+ws0 :: Parser ()
+ws0 = void $ takeWhileP (Just "BLANK") (\c -> c == ' ' || c == '\t')
+
+blank_line :: Parser ()
+blank_line = (try (ws0 <* eol) <|> try (ws <* eof)) <?> "blank-line"
+
+comment_line :: Parser ()
+comment_line = (char '#' *> takeWhileP Nothing (/= '\n') *> void eol) <?> "comment-line"
+
+comment_lines :: Parser ()
+comment_lines = void (some (comment_line <|> blank_line))
+
+stripTrailEols :: T.Text -> T.Text
+stripTrailEols = go
+  where
+    go t | T.isSuffixOf "\n\n" t     = go (T.init t)
+         | T.isSuffixOf "\r\n\r\n" t = go (T.init (T.init t))
+         | t == "\n"                 = ""
+         | otherwise = t
+
+-- 'undent'
+stripPrefixInd :: T.Text -> T.Text
+stripPrefixInd = T.unlines . map go . T.lines
+  where
+    go t | T.isPrefixOf "    " t = T.drop 4 t
+         | T.isPrefixOf "   "  t = T.drop 3 t
+         | T.isPrefixOf "  "   t = T.drop 2 t
+         | T.isPrefixOf " "    t = T.drop 1 t
+         | otherwise           = t
+
+stripComments :: T.Text -> T.Text
+stripComments = T.unlines . filter (not . T.isPrefixOf "#") . T.lines
+
+transformPoint :: Bool -> [Char] -> T.Text -> T.Text
+transformPoint single mods0 -- TODO: backslash
+  = go mods0 .
+    (if keepBlanks then id else stripTrailEols) .
+    (if keepComments then id else stripComments)
+  where
+    keepBlanks = single || ('+' `elem` mods0)
+    keepComments = single || ('#' `elem` mods0)
+
+    go []       = id
+    go ('<':xs)
+      | single = error "invalid filter for point-single"
+      | otherwise = go xs . stripPrefixInd
+    go ('+':xs) = go xs -- negative flag
+    go ('#':xs) = go xs -- negative flag
+    go ('-':xs) = go xs . T.dropWhileEnd C.isSpace
+    go (c:_)    = error ("unknown filter " ++ show c)
diff --git a/src/Data/YAML.hs b/src/Data/YAML.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/YAML.hs
@@ -0,0 +1,416 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE Trustworthy                #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+-- Document oriented YAML parsing API inspired by [aeson](http://hackage.haskell.org/package/aeson).
+--
+-- === Usage Example
+--
+-- Let's assume we want to decode a simple YAML document
+--
+-- > - name: Erik Weisz
+-- >   age: 52
+-- >   magic: True
+-- > - name: Mina Crandon
+-- >   age: 53
+--
+-- into a Haskell list of person records, i.e. a value of type @[Person]@.
+--
+-- The code below shows how to manually define a @Person@ record type together with a 'FromYAML' instance:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- >
+-- > import Data.YAML
+-- >
+-- > data Person = Person
+-- >     { name  :: Text
+-- >     , age   :: Int
+-- >     , magic :: Bool
+-- >     } deriving Show
+-- >
+-- > instance FromYAML Person where
+-- >    parseYAML = withMap "Person" $ \m -> Person
+-- >        <$> m .: "name"
+-- >        <*> m .: "age"
+-- >        <*> m .:? "magic" .!= False
+--
+-- And now we can 'decode' the YAML document like so:
+--
+-- >>> decode "- name: Erik Weisz\n  age: 52\n  magic: True\n- name: Mina Crandon\n  age: 53" :: Either String [[Person]]
+-- Right [[Person {name = "Erik Weisz", age = 52, magic = True},Person {name = "Mina Crandon", age = 53, magic = False}]]
+--
+--
+module Data.YAML
+    (
+      -- * Typeclass-based resolving/decoding
+      decode
+    , FromYAML(..)
+    , Parser
+    , parseEither
+
+      -- ** Accessors for YAML 'Mapping's
+    , Mapping
+    , (.:), (.:?), (.:!), (.!=)
+
+      -- ** Prism-style parsers
+    , withSeq
+    , withBool
+    , withFloat
+    , withInt
+    , withNull
+    , withStr
+    , withMap
+
+      -- * \"Concrete\" AST
+    , decodeNode
+    , decodeNode'
+    , Doc(..)
+    , Node(..)
+    , Scalar(..)
+
+      -- * YAML 1.2 Schema resolvers
+    , SchemaResolver(..)
+    , failsafeSchemaResolver
+    , jsonSchemaResolver
+    , coreSchemaResolver
+
+      -- * Generalised AST construction
+    , decodeLoader
+    , Loader(..)
+    , NodeId
+
+    ) where
+
+import qualified Data.ByteString.Lazy as BS.L
+import qualified Data.Map             as Map
+import qualified Data.Text            as T
+
+import           Data.YAML.Event      (Tag, isUntagged, tagToText)
+import           Data.YAML.Loader
+import           Data.YAML.Schema
+
+import           Util
+
+-- | YAML Document tree/graph
+newtype Doc n = Doc n deriving (Eq,Ord,Show)
+
+-- | YAML Document node
+data Node = Scalar   !Scalar
+          | Mapping  !Tag Mapping
+          | Sequence !Tag [Node]
+          | Anchor   !NodeId !Node
+          deriving (Eq,Ord,Show)
+
+-- | YAML mapping
+type Mapping = Map Node Node
+
+-- | Retrieve value in 'Mapping' indexed by a @!!str@ 'Text' key.
+--
+-- This parser fails if the key doesn't exist.
+(.:) :: FromYAML a => Mapping -> Text -> Parser a
+m .: k = maybe (fail $ "key " ++ show k ++ " not found") parseYAML (Map.lookup (Scalar (SStr k)) m)
+
+-- | Retrieve optional value in 'Mapping' indexed by a @!!str@ 'Text' key.
+--
+-- 'Nothing' is returned if the key is missing or points to a @tag:yaml.org,2002:null@ node.
+-- This combinator only fails if the key exists but cannot be converted to the required type.
+--
+-- See also '.:!'.
+(.:?) :: FromYAML a => Mapping -> Text -> Parser (Maybe a)
+m .:? k = maybe (pure Nothing) parseYAML (Map.lookup (Scalar (SStr k)) m)
+
+-- | Retrieve optional value in 'Mapping' indexed by a @!!str@ 'Text' key.
+--
+-- 'Nothing' is returned if the key is missing.
+-- This combinator only fails if the key exists but cannot be converted to the required type.
+--
+-- __NOTE__: This is a variant of '.:?' which doesn't map a @tag:yaml.org,2002:null@ node to 'Nothing'.
+(.:!) :: FromYAML a => Mapping -> Text -> Parser (Maybe a)
+m .:! k = maybe (pure Nothing) (fmap Just . parseYAML) (Map.lookup (Scalar (SStr k)) m)
+
+-- | Defaulting helper to be used with '.:?' or '.:!'.
+(.!=) :: Parser (Maybe a) -> a -> Parser a
+mv .!= def = fmap (maybe def id) mv
+
+
+-- | Parse and decode YAML document(s) into 'Node' graphs
+--
+-- This is a convenience wrapper over `decodeNode'`
+--
+-- > decodeNode = decodeNode' coreSchemaResolver False False
+--
+-- In other words,
+--
+-- * Use the YAML 1.2 Core schema for resolving
+-- * Don't create 'Anchor' nodes
+-- * Disallow cyclic anchor references
+--
+decodeNode :: BS.L.ByteString -> Either String [Doc Node]
+decodeNode = decodeNode' coreSchemaResolver False False
+
+
+-- | Customizable variant of 'decodeNode'
+--
+decodeNode' :: SchemaResolver  -- ^ YAML Schema resolver to use
+            -> Bool            -- ^ Whether to emit anchor nodes
+            -> Bool            -- ^ Whether to allow cyclic references
+            -> BS.L.ByteString -- ^ YAML document to parse
+            -> Either String [Doc Node]
+decodeNode' SchemaResolver{..} anchorNodes allowCycles bs0
+  = map Doc <$> runIdentity (decodeLoader failsafeLoader bs0)
+  where
+    failsafeLoader = Loader { yScalar   = \t s v -> pure $ fmap Scalar (schemaResolverScalar t s v)
+                            , ySequence = \t vs  -> pure $ schemaResolverSequence t >>= \t' -> Right (Sequence t' vs)
+                            , yMapping  = \t kvs -> pure $ schemaResolverMapping  t >>= \t' -> Right (Mapping t' (Map.fromList kvs))
+                            , yAlias    = if allowCycles
+                                          then \_ _ n -> pure $ Right n
+                                          else \_ c n -> pure $ if c then Left "cycle detected" else Right n
+                            , yAnchor   = if anchorNodes
+                                          then \j n   -> pure $ Right (Anchor j n)
+                                          else \_ n   -> pure $ Right n
+                            }
+
+
+----------------------------------------------------------------------------
+
+-- | YAML Parser 'Monad' used by 'FromYAML'
+--
+-- See also 'parseEither' or 'decode'
+newtype Parser a = P { unP :: Either String a }
+                 deriving (Functor,Applicative)
+
+-- TODO: MonadFail
+instance Monad Parser where
+  return = pure
+  P m >>= k = P (m >>= unP . k)
+  (>>) = (*>)
+  fail = P . Left
+
+-- | Run 'Parser'
+--
+-- A common use-case is 'parseEither' 'parseYAML'.
+parseEither :: Parser a -> Either String a
+parseEither = unP
+
+-- helper
+failTypeMismatch :: String -> Node -> Parser a
+failTypeMismatch expected node = fail ("expected " ++ expected ++ " instead of " ++ got)
+  where
+    got = case node of
+            Scalar (SBool _)             -> "!!bool"
+            Scalar (SInt _)              -> "!!int"
+            Scalar  SNull                -> "!!null"
+            Scalar (SStr _)              -> "!!str"
+            Scalar (SFloat _)            -> "!!float"
+            Scalar (SUnknown t v)
+              | isUntagged t             -> tagged t ++ show v
+              | otherwise                -> "(unsupported) " ++ tagged t ++ "scalar"
+            (Anchor _ _)                 -> "anchor"
+            (Mapping t _)                -> tagged t ++ " mapping"
+            (Sequence t _)               -> tagged t ++ " sequence"
+
+    tagged t0 = case tagToText t0 of
+               Nothing -> "non-specifically ? tagged (i.e. unresolved) "
+               Just t  -> T.unpack t ++ " tagged"
+
+-- | A type into which YAML nodes can be converted/deserialized
+class FromYAML a where
+  parseYAML :: Node -> Parser a
+
+-- | Operate on @tag:yaml.org,2002:null@ node (or fail)
+withNull :: String -> Parser a -> Node -> Parser a
+withNull _        f (Scalar SNull) = f
+withNull expected _ v              = failTypeMismatch expected v
+
+
+-- | Trivial instance
+instance FromYAML Node where
+  parseYAML = pure
+
+instance FromYAML Bool where
+  parseYAML = withBool "!!bool" pure
+
+-- | Operate on @tag:yaml.org,2002:bool@ node (or fail)
+withBool :: String -> (Bool -> Parser a) -> Node -> Parser a
+withBool _        f (Scalar (SBool b)) = f b
+withBool expected _ v                  = failTypeMismatch expected v
+
+instance FromYAML Text where
+  parseYAML = withStr "!!str" pure
+
+-- | Operate on @tag:yaml.org,2002:str@ node (or fail)
+withStr :: String -> (Text -> Parser a) -> Node -> Parser a
+withStr _        f (Scalar (SStr b)) = f b
+withStr expected _ v                 = failTypeMismatch expected v
+
+instance FromYAML Integer where
+  parseYAML = withInt "!!int" pure
+
+-- | Operate on @tag:yaml.org,2002:int@ node (or fail)
+withInt :: String -> (Integer -> Parser a) -> Node -> Parser a
+withInt _        f (Scalar (SInt b)) = f b
+withInt expected _ v                 = failTypeMismatch expected v
+
+instance FromYAML Double where
+  parseYAML = withFloat "!!float" pure
+
+-- | Operate on @tag:yaml.org,2002:float@ node (or fail)
+withFloat :: String -> (Double -> Parser a) -> Node -> Parser a
+withFloat _        f (Scalar (SFloat b)) = f b
+withFloat expected _ v                   = failTypeMismatch expected v
+
+-- signed fixed-width integers
+
+instance FromYAML Int where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Int'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Int8 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Int8'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Int16 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Int16'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Int32 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Int32'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Int64 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Int64'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+
+instance FromYAML Word where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Word'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Word8 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Word8'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Word16 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Word16'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Word32 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Word32'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance FromYAML Word64 where
+  parseYAML (Scalar (SInt b)) = maybe (fail $ "!!int " ++ show b ++ " out of range for 'Word64'") pure $ fromIntegerMaybe b
+  parseYAML n                    = failTypeMismatch "!!int" n
+
+instance (Ord k, FromYAML k, FromYAML v) => FromYAML (Map k v) where
+  parseYAML = withMap "!!map" $ \xs -> Map.fromList <$> mapM (\(a,b) -> (,) <$> parseYAML a <*> parseYAML b) (Map.toList xs)
+
+-- | Operate on @tag:yaml.org,2002:seq@ node (or fail)
+withMap :: String -> (Mapping -> Parser a) -> Node -> Parser a
+withMap _        f (Mapping tag xs)
+  | tag == tagMap    = f xs
+withMap expected _ v = failTypeMismatch expected v
+
+instance FromYAML v => FromYAML [v] where
+  parseYAML = withSeq "!!seq" (mapM parseYAML)
+
+-- | Operate on @tag:yaml.org,2002:seq@ node (or fail)
+withSeq :: String -> ([Node] -> Parser a) -> Node -> Parser a
+withSeq _        f (Sequence tag xs)
+  | tag == tagSeq    = f xs
+withSeq expected _ v = failTypeMismatch expected v
+
+instance FromYAML a => FromYAML (Maybe a) where
+  parseYAML (Scalar SNull) = pure Nothing
+  parseYAML j              = Just <$> parseYAML j
+
+----------------------------------------------------------------------------
+
+instance (FromYAML a, FromYAML b) => FromYAML (a,b) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b] -> (,) <$> parseYAML a
+                                          <*> parseYAML b
+                             _     -> fail ("expected 2-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+instance (FromYAML a, FromYAML b, FromYAML c) => FromYAML (a,b,c) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b,c] -> (,,) <$> parseYAML a
+                                             <*> parseYAML b
+                                             <*> parseYAML c
+                             _     -> fail ("expected 3-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+
+instance (FromYAML a, FromYAML b, FromYAML c, FromYAML d) => FromYAML (a,b,c,d) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b,c,d] -> (,,,) <$> parseYAML a
+                                                <*> parseYAML b
+                                                <*> parseYAML c
+                                                <*> parseYAML d
+                             _     -> fail ("expected 4-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+
+instance (FromYAML a, FromYAML b, FromYAML c, FromYAML d, FromYAML e) => FromYAML (a,b,c,d,e) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b,c,d,e] -> (,,,,) <$> parseYAML a
+                                                   <*> parseYAML b
+                                                   <*> parseYAML c
+                                                   <*> parseYAML d
+                                                   <*> parseYAML e
+                             _     -> fail ("expected 5-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+
+instance (FromYAML a, FromYAML b, FromYAML c, FromYAML d, FromYAML e, FromYAML f) => FromYAML (a,b,c,d,e,f) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b,c,d,e,f] -> (,,,,,) <$> parseYAML a
+                                                      <*> parseYAML b
+                                                      <*> parseYAML c
+                                                      <*> parseYAML d
+                                                      <*> parseYAML e
+                                                      <*> parseYAML f
+                             _     -> fail ("expected 6-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+
+instance (FromYAML a, FromYAML b, FromYAML c, FromYAML d, FromYAML e, FromYAML f, FromYAML g) => FromYAML (a,b,c,d,e,f,g) where
+  parseYAML = withSeq "!!seq" $ \xs ->
+                           case xs of
+                             [a,b,c,d,e,f,g] -> (,,,,,,) <$> parseYAML a
+                                                         <*> parseYAML b
+                                                         <*> parseYAML c
+                                                         <*> parseYAML d
+                                                         <*> parseYAML e
+                                                         <*> parseYAML f
+                                                         <*> parseYAML g
+                             _     -> fail ("expected 7-sequence but got " ++ show (length xs) ++ "-sequence instead")
+
+
+-- | Decode YAML document(s) using the YAML 1.2 Core schema
+--
+-- Each document contained in the YAML stream produce one element of
+-- the response list. Here's an example of decoding two concatenated
+-- YAML documents:
+--
+-- >>> decode "Foo\n---\nBar" :: Either String [Text]
+-- Right ["Foo","Bar"]
+--
+-- Note that an empty stream doesn't contain any (non-comment)
+-- document nodes, and therefore results in an empty result list:
+--
+-- >>> decode "# just a comment" :: Either String [Text]
+-- Right []
+--
+-- 'decode' uses the same settings as 'decodeNode' for tag-resolving. If
+-- you need a different custom parsing configuration, you need to
+-- combine 'parseEither' and `decodeNode'` yourself.
+decode :: FromYAML v => BS.L.ByteString -> Either String [v]
+decode bs0 = decodeNode bs0 >>= mapM (parseEither . parseYAML . (\(Doc x) -> x))
+
diff --git a/src/Data/YAML/Event.hs b/src/Data/YAML/Event.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/YAML/Event.hs
@@ -0,0 +1,415 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE Safe            #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+-- Event-stream oriented YAML parsing API
+module Data.YAML.Event
+    ( parseEvents
+    , EvStream
+    , Event(..)
+    , Style(..)
+    , Tag, untagged, isUntagged, tagToText, mkTag
+    , Anchor
+    , Pos(..)
+    ) where
+
+import qualified Data.ByteString.Lazy as BS.L
+import           Data.Char
+import qualified Data.Map             as Map
+import qualified Data.Text            as T
+import qualified Data.YAML.Token      as Y
+import           Numeric              (readHex)
+
+import           Util
+
+-- TODO: consider also non-essential attributes
+
+-- | YAML Event Types
+--
+-- The events correspond to the ones from [LibYAML](http://pyyaml.org/wiki/LibYAML)
+--
+-- The grammar below defines well-formed streams of 'Event's:
+--
+-- @
+-- stream   ::= 'StreamStart' document* 'StreamEnd'
+-- document ::= 'DocumentStart' node 'DocumentEnd'
+-- node     ::= 'Alias'
+--            | 'Scalar'
+--            | sequence
+--            | mapping
+-- sequence ::= 'SequenceStart' node* 'SequenceEnd'
+-- mapping  ::= 'MappingStart' (node node)* 'MappingEnd'
+-- @
+data Event
+    = StreamStart
+    | StreamEnd
+    | DocumentStart  !Bool
+    | DocumentEnd    !Bool
+    | Alias          !Anchor
+    | Scalar         !(Maybe Anchor)  !Tag  !Style  !Text
+    | SequenceStart  !(Maybe Anchor)  !Tag
+    | SequenceEnd
+    | MappingStart   !(Maybe Anchor)  !Tag
+    | MappingEnd
+    deriving (Show, Eq)
+
+-- | YAML Anchor identifiers
+type Anchor = Text
+
+-- | YAML Tags
+newtype Tag = Tag (Maybe Text)
+            deriving (Eq,Ord)
+
+instance Show Tag where
+  show (Tag x) = show x
+
+-- | Convert 'Tag' to its string representation
+--
+-- Returns 'Nothing' for 'untagged'
+tagToText :: Tag -> Maybe T.Text
+tagToText (Tag x) = x
+
+-- | An \"untagged\" YAML tag
+untagged :: Tag
+untagged = Tag Nothing
+
+-- | Equivalent to @(== 'untagged')@
+isUntagged :: Tag -> Bool
+isUntagged (Tag Nothing) = True
+isUntagged _             = False
+
+-- | Construct YAML tag
+mkTag :: String -> Tag
+mkTag "" = error "mkTag"
+mkTag "!" = Tag (Just $! T.pack "!")
+mkTag s   = Tag (Just $! tagUnescape s)
+  where
+    tagUnescape = T.pack . go
+      where
+        go [] = []
+        go ('%':h:l:cs)
+          | Just c <- decodeL1 [h,l] = c : go cs
+        go (c:cs) = c : go cs
+
+
+mkTag' :: String -> Tag
+mkTag' "" = error "mkTag'"
+mkTag' s  = Tag (Just $! T.pack s)
+
+mkTag'' :: String -> Tag
+mkTag'' "" = error "mkTag''"
+mkTag'' s  = Tag (Just $! T.pack ("tag:yaml.org,2002:" ++ s))
+
+
+
+-- | Event stream produced by 'parseEvents'
+--
+-- A 'Left' value denotes parsing errors. The event stream ends
+-- immediately once a 'Left' value is returned.
+type EvStream = [Either (Pos,String) Event]
+
+-- | Position in parsed YAML source
+data Pos = Pos
+    { posByteOffset :: !Int -- ^ 0-based byte offset
+    , posCharOffset :: !Int -- ^ 0-based character (Unicode code-point) offset
+    , posLine       :: !Int -- ^ 1-based line number
+    , posColumn     :: !Int -- ^ 0-based character (Unicode code-point) column number
+    } deriving Show
+
+tok2pos :: Y.Token -> Pos
+tok2pos Y.Token { Y.tByteOffset = posByteOffset, Y.tCharOffset = posCharOffset, Y.tLine = posLine, Y.tLineChar = posColumn } = Pos {..}
+
+-- | 'Scalar' node style
+data Style = Plain
+           | SingleQuoted
+           | DoubleQuoted
+           | Literal
+           | Folded
+           deriving (Eq,Ord,Show)
+
+-- internal
+type TagHandle = Text
+type Props = (Maybe Text,Tag)
+
+getHandle :: [Y.Token] -> Maybe (TagHandle,[Y.Token])
+getHandle toks0 = do
+  Y.Token { Y.tCode = Y.BeginHandle } : toks1 <- Just toks0
+  (hs,Y.Token { Y.tCode = Y.EndHandle } : toks2) <- Just $ span (\Y.Token { Y.tCode = c } -> c `elem` [Y.Indicator,Y.Meta]) toks1
+  pure (T.pack $ concatMap Y.tText hs, toks2)
+
+getUriTag :: [Y.Token] -> Maybe (Text,[Y.Token])
+getUriTag toks0 = do
+  Y.Token { Y.tCode = Y.BeginTag } : toks1 <- Just toks0
+  (hs,Y.Token { Y.tCode = Y.EndTag } : toks2) <- Just $ span (\Y.Token { Y.tCode = c } -> c `elem` [Y.Indicator,Y.Meta]) toks1
+  pure (T.pack $ concatMap Y.tText hs, toks2)
+
+{- WARNING: the code that follows will make you cry; a safety pig is provided below for your benefit.
+
+                         _
+ _._ _..._ .-',     _.._(`))
+'-. `     '  /-._.-'    ',/
+   )         \            '.
+  / _    _    |             \
+ |  a    a    /              |
+ \   .-.                     ;
+  '-('' ).-'       ,'       ;
+     '-;           |      .'
+        \           \    /
+        | 7  .__  _.-\   \
+        | |  |  ``/  /`  /
+       /,_|  |   /,_/   /
+          /,_/      '`-'
+
+-}
+
+-- | Parse YAML 'Event's from a lazy 'BS.L.ByteString'.
+parseEvents :: BS.L.ByteString -> EvStream
+parseEvents = \bs0 -> Right StreamStart : (go0 mempty $ stripComments $ filter (not . isWhite) (Y.tokenize bs0 False))
+  where
+    isTCode tc = (== tc) . Y.tCode
+    skipPast tc (t : ts)
+      | isTCode tc t = ts
+      | otherwise = skipPast tc ts
+    skipPast _ [] = error "the impossible happened"
+
+
+    isWhite :: Y.Token -> Bool
+    isWhite (Y.Token { Y.tCode = Y.White })  = True
+    isWhite (Y.Token { Y.tCode = Y.Indent }) = True
+    isWhite (Y.Token { Y.tCode = Y.Break })  = True
+    isWhite _                                = False
+
+    goDir :: Map TagHandle Text -> [Y.Token] -> EvStream
+    goDir m (Y.Token { Y.tCode = Y.Indicator, Y.tText = "%" } :
+             Y.Token { Y.tCode = Y.Meta, Y.tText = "YAML" } :
+             Y.Token { Y.tCode = Y.Meta } :
+             Y.Token { Y.tCode = Y.EndDirective } :
+             rest) = go0 m rest
+
+    goDir m (Y.Token { Y.tCode = Y.Indicator, Y.tText = "%" } :
+             Y.Token { Y.tCode = Y.Meta, Y.tText = "TAG" } :
+             rest)
+      | Just (h, rest') <- getHandle rest
+      , Just (t, rest'') <- getUriTag rest' = go0 (Map.insert h t m) (skipPast Y.EndDirective rest'')
+
+    goDir m (Y.Token { Y.tCode = Y.Indicator, Y.tText = "%" } :
+             Y.Token { Y.tCode = Y.Meta, Y.tText = l } :
+             rest) | l `notElem` ["TAG","YAML"] = go0 m (skipPast Y.EndDirective rest)
+    goDir _ xs                                            = err xs
+
+    go0 :: Map.Map TagHandle Text -> Tok2EvStream
+    go0 _ [] = [Right StreamEnd]
+    go0 _ (Y.Token { Y.tCode = Y.White } : _) = error "the impossible happened"
+    go0 m (Y.Token { Y.tCode = Y.Indicator } : rest) = go0 m rest -- ignore indicators here
+    go0 m (Y.Token { Y.tCode = Y.DirectivesEnd } : rest) = go0 m rest
+    go0 m (Y.Token { Y.tCode = Y.BeginDocument } : Y.Token { Y.tCode = Y.DirectivesEnd } : rest) = Right (DocumentStart True) : go0 m rest -- hack
+    go0 m (Y.Token { Y.tCode = Y.BeginDocument } : rest@(Y.Token { Y.tCode = Y.BeginDirective } : _)) = Right (DocumentStart True) : go0 m rest -- hack
+    go0 m (Y.Token { Y.tCode = Y.BeginDocument } : rest) = Right (DocumentStart False) : go0 m rest
+    go0 _ (Y.Token { Y.tCode = Y.EndDocument } : Y.Token { Y.tCode = Y.DocumentEnd } : rest) = Right (DocumentEnd True) : go0 mempty rest
+    go0 _ (Y.Token { Y.tCode = Y.EndDocument } : rest) = Right (DocumentEnd False) : go0 mempty rest
+    go0 m (Y.Token { Y.tCode = Y.DocumentEnd } : rest) = go0 m rest -- should not occur
+    go0 m (Y.Token { Y.tCode = Y.BeginNode } : rest) = goNode0 m rest (go0 m)
+    go0 m (Y.Token { Y.tCode = Y.BeginDirective } : rest) = goDir m rest
+    go0 _ xs = err xs
+
+err :: Tok2EvStream
+err (tok@Y.Token { Y.tCode = Y.Error, Y.tText = msg } : _) = [Left (tok2pos tok, msg)]
+err (tok@Y.Token { Y.tCode = Y.Unparsed, Y.tText = txt } : _) = [Left (tok2pos tok, ("Lexical error near " ++ show txt))]
+err (tok@Y.Token { Y.tCode = code } : _) = [Left (tok2pos tok, ("Parse failure near " ++ show code ++ " token"))]
+err [] = [Left ((Pos (-1) (-1) (-1) (-1)), "Unexpected end of token stream")]
+
+goNode0 :: Map TagHandle Text -> Tok2EvStreamCont
+goNode0 tagmap = goNode
+  where
+    goNode :: Tok2EvStreamCont
+    goNode (Y.Token { Y.tCode = Y.BeginScalar }   : rest) cont = goScalar (mempty,untagged) rest (flip goNodeEnd cont)
+    goNode (Y.Token { Y.tCode = Y.BeginSequence } : rest) cont = Right (SequenceStart Nothing untagged) : goSeq rest (flip goNodeEnd cont)
+    goNode (Y.Token { Y.tCode = Y.BeginMapping }  : rest) cont = Right (MappingStart Nothing untagged) : goMap rest (flip goNodeEnd cont)
+    goNode (Y.Token { Y.tCode = Y.BeginProperties } : rest) cont = goProp (mempty,untagged) rest (\p rest' -> goNode' p rest' cont)
+    goNode (Y.Token { Y.tCode = Y.BeginAlias } :
+            Y.Token { Y.tCode = Y.Indicator } :
+            Y.Token { Y.tCode = Y.Meta, Y.tText = anchor } :
+            Y.Token { Y.tCode = Y.EndAlias } :
+            Y.Token { Y.tCode = Y.EndNode } :
+            rest) cont = Right (Alias (T.pack anchor)) : cont rest
+    goNode xs _cont = err xs
+
+    goNode' :: Props -> Tok2EvStreamCont
+    goNode' props (Y.Token { Y.tCode = Y.BeginScalar }   : rest) cont   = goScalar props rest (flip goNodeEnd cont)
+    goNode' (manchor,mtag) (Y.Token { Y.tCode = Y.BeginSequence } : rest) cont = Right (SequenceStart manchor mtag) : goSeq rest (flip goNodeEnd cont)
+    goNode' (manchor,mtag) (Y.Token { Y.tCode = Y.BeginMapping }  : rest) cont = Right (MappingStart manchor mtag) : goMap rest (flip goNodeEnd cont)
+    goNode' _ xs                                            _cont = err xs
+
+    goNodeEnd :: Tok2EvStreamCont
+    goNodeEnd (Y.Token { Y.tCode = Y.EndNode } : rest) cont = cont rest
+    goNodeEnd xs                                      _cont = err xs
+
+    goProp :: Props -> [Y.Token] -> (Props -> [Y.Token] -> EvStream) -> EvStream
+    goProp props (Y.Token { Y.tCode = Y.EndProperties } : rest) cont = cont props rest
+    goProp props (Y.Token { Y.tCode = Y.BeginAnchor } : rest) cont = goAnchor props rest (\x y -> goProp x y cont)
+    goProp props (Y.Token { Y.tCode = Y.BeginTag } : rest) cont = goTag props rest (\x y -> goProp x y cont)
+    goProp _props xs                                     _cont = err xs
+
+    goAnchor :: Props -> [Y.Token] -> (Props -> [Y.Token] -> EvStream) -> EvStream
+    goAnchor props (Y.Token { Y.tCode = Y.Indicator } : rest) cont = goAnchor props rest cont
+    goAnchor (_,tag) (Y.Token { Y.tCode = Y.Meta, Y.tText = anchor } : rest) cont = goAnchor (Just $! T.pack anchor,tag) rest cont
+    goAnchor props (Y.Token { Y.tCode = Y.EndAnchor } : rest) cont = cont props rest
+    goAnchor _ xs _ = err xs
+
+    goTag :: Props -> [Y.Token] -> (Props -> [Y.Token] -> EvStream) -> EvStream
+
+    goTag (anchor,_) (Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.EndTag } : rest)
+          cont = cont (anchor,mkTag' "!") rest
+
+    goTag (anchor,_) (Y.Token { Y.tCode = Y.BeginHandle } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.EndHandle } :
+                      Y.Token { Y.tCode = Y.Meta, Y.tText = tag } :
+                      Y.Token { Y.tCode = Y.EndTag } : rest)
+          cont
+            | Just t' <- Map.lookup (T.pack ("!!")) tagmap
+              = cont (anchor,mkTag (T.unpack t' ++ tag)) rest
+            | otherwise = cont (anchor,mkTag'' tag) rest
+
+    goTag (anchor,_) (Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "<" } :
+                      Y.Token { Y.tCode = Y.Meta, Y.tText = tag } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = ">" } :
+                      Y.Token { Y.tCode = Y.EndTag } : rest)
+          cont = cont (anchor,mkTag tag) rest
+
+    goTag (anchor,_) xs@(Y.Token { Y.tCode = Y.BeginHandle } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.Meta, Y.tText = h } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.EndHandle } :
+                      Y.Token { Y.tCode = Y.Meta, Y.tText = tag } :
+                      Y.Token { Y.tCode = Y.EndTag } : rest)
+          cont
+            | Just t' <- Map.lookup (T.pack ("!" ++ h ++ "!")) tagmap
+              = cont (anchor,mkTag (T.unpack t' ++ tag)) rest
+            | otherwise = err xs
+
+    goTag (anchor,_) (Y.Token { Y.tCode = Y.BeginHandle } :
+                      Y.Token { Y.tCode = Y.Indicator, Y.tText = "!" } :
+                      Y.Token { Y.tCode = Y.EndHandle } :
+                      Y.Token { Y.tCode = Y.Meta, Y.tText = tag } :
+                      Y.Token { Y.tCode = Y.EndTag } : rest)
+          cont
+            | Just t' <- Map.lookup (T.pack ("!")) tagmap
+              = cont (anchor,mkTag (T.unpack t' ++ tag)) rest
+            | otherwise = cont (anchor,mkTag' ('!' : tag)) rest -- unresolved
+    goTag _ xs _ = err xs
+
+    goScalar :: Props -> Tok2EvStreamCont
+    goScalar (manchor,tag) toks0 cont = go' "" Plain toks0
+      where
+        go' acc sty (Y.Token { Y.tCode = Y.Text, Y.tText = t } : rest) = go'  (acc ++ t) sty rest
+        go' acc sty (Y.Token { Y.tCode = Y.LineFold } : rest) = go'  (acc ++ " ") sty rest
+        go' acc sty (Y.Token { Y.tCode = Y.LineFeed } : rest) = go'  (acc ++ "\n") sty rest
+
+        go' acc sty (Y.Token { Y.tCode = Y.BeginEscape } :
+                     Y.Token { Y.tCode = Y.Indicator, Y.tText = "'" } :
+                     Y.Token { Y.tCode = Y.Meta, Y.tText = "'" } :
+                     Y.Token { Y.tCode = Y.EndEscape } :
+                     rest) = go'  (acc ++ "'") sty rest
+
+        go' acc sty (Y.Token { Y.tCode = Y.BeginEscape } :
+                     Y.Token { Y.tCode = Y.Indicator, Y.tText = "\\" } :
+--                     Y.Token { Y.tCode = Y.Break } :
+                     Y.Token { Y.tCode = Y.EndEscape } :
+                     rest) = go' acc sty rest -- end-line
+
+        go' acc sty (Y.Token { Y.tCode = Y.BeginEscape } :
+                     Y.Token { Y.tCode = Y.Indicator, Y.tText = "\\" } :
+                     Y.Token { Y.tCode = Y.Meta, Y.tText = t } :
+                     Y.Token { Y.tCode = Y.EndEscape } :
+                     rest)
+          | t == "n"  = go'  (acc ++ "\n") sty rest
+          | t == "r"  = go'  (acc ++ "\r") sty rest
+          | t == "t"  = go'  (acc ++ "\t") sty rest
+          | t == "b"  = go'  (acc ++ "\b") sty rest
+          | t == "/"  = go'  (acc ++ t) sty rest
+          | t == " "  = go'  (acc ++ t) sty rest
+          | t == "\\"  = go'  (acc ++ t) sty rest
+          | t == "\"" = go'  (acc ++ t) sty rest
+
+        go' acc sty (Y.Token { Y.tCode = Y.BeginEscape } :
+                     Y.Token { Y.tCode = Y.Indicator, Y.tText = "\\" } :
+                     Y.Token { Y.tCode = Y.Indicator, Y.tText = pfx } :
+                     Y.Token { Y.tCode = Y.Meta, Y.tText = ucode } :
+                     Y.Token { Y.tCode = Y.EndEscape } :
+                     rest)
+          | pfx == "u", Just c <- decodeCP ucode = go' (acc ++ [c]) sty rest
+          | pfx == "x", Just c <- decodeL1 ucode = go' (acc ++ [c]) sty rest
+
+        go' acc sty (Y.Token { Y.tCode = Y.Indicator, Y.tText = ind } : rest)
+          | "'"  <- ind = go' acc SingleQuoted rest
+          | "\"" <- ind = go' acc DoubleQuoted rest
+          | "|"  <- ind = go' acc Literal rest
+          | ">"  <- ind = go' acc Folded rest
+          | otherwise   = go' acc sty rest
+        go' acc sty (Y.Token { Y.tCode = Y.EndScalar } : rest) = Right (Scalar manchor tag sty (T.pack acc)) : cont rest
+        go' _ _ xs | False = error (show xs)
+        go' _ _ xs = err xs
+
+    goSeq :: Tok2EvStreamCont
+    goSeq (Y.Token { Y.tCode = Y.EndSequence } : rest) cont = Right SequenceEnd : cont rest
+    goSeq (Y.Token { Y.tCode = Y.BeginNode } : rest) cont = goNode rest (flip goSeq cont)
+    goSeq (Y.Token { Y.tCode = Y.BeginMapping } : rest) cont = Right (MappingStart Nothing untagged) : goMap rest (flip goSeq cont)
+    goSeq (Y.Token { Y.tCode = Y.Indicator } : rest) cont = goSeq rest cont
+--    goSeq xs _cont = error (show xs)
+    goSeq xs _cont = err xs
+
+    goMap :: Tok2EvStreamCont
+    goMap (Y.Token { Y.tCode = Y.EndMapping } : rest) cont = Right MappingEnd : cont rest
+    goMap (Y.Token { Y.tCode = Y.BeginPair } : rest) cont = goPair1 rest (flip goMap cont)
+    goMap (Y.Token { Y.tCode = Y.Indicator } : rest) cont = goMap rest cont
+    goMap xs _cont = err xs
+
+    goPair1 (Y.Token { Y.tCode = Y.BeginNode } : rest) cont = goNode rest (flip goPair2 cont)
+    goPair1 (Y.Token { Y.tCode = Y.Indicator } : rest) cont = goPair1 rest cont
+    goPair1 xs _cont = err xs
+
+    goPair2 (Y.Token { Y.tCode = Y.BeginNode } : rest) cont = goNode rest (flip goPairEnd cont)
+    goPair2 (Y.Token { Y.tCode = Y.Indicator } : rest) cont = goPair2 rest cont
+    goPair2 xs _cont                                        = err xs
+
+    goPairEnd (Y.Token { Y.tCode = Y.EndPair } : rest) cont = cont rest
+    goPairEnd xs _cont                                      = err xs
+
+
+stripComments :: [Y.Token] -> [Y.Token]
+stripComments (Y.Token { Y.tCode = Y.BeginComment } : rest) = skip rest
+  where
+    skip (Y.Token { Y.tCode = Y.EndComment } : rest') = stripComments rest'
+    skip (_                                  : rest') = skip rest'
+    skip [] = error "the impossible happened"
+stripComments (t : rest) = t : stripComments rest
+stripComments [] = []
+
+type Tok2EvStream = [Y.Token] -> EvStream
+
+type Tok2EvStreamCont = [Y.Token] -> Cont EvStream [Y.Token]
+
+type Cont r a = (a -> r) -> r
+
+
+-- decode 4-hex-digit unicode code-point
+decodeCP :: String -> Maybe Char
+decodeCP s = case s of
+               [_,_,_,_] | all isHexDigit s
+                         , [(j, "")] <- readHex s -> Just (chr (fromInteger j))
+               _ -> Nothing
+
+-- decode 2-hex-digit latin1 code-point
+decodeL1 :: String -> Maybe Char
+decodeL1 s = case s of
+               [_,_] | all isHexDigit s
+                     , [(j, "")] <- readHex s -> Just (chr (fromInteger j))
+               _ -> Nothing
diff --git a/src/Data/YAML/Loader.hs b/src/Data/YAML/Loader.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/YAML/Loader.hs
@@ -0,0 +1,206 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE RecursiveDo                #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE Trustworthy                #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+module Data.YAML.Loader
+    ( decodeLoader
+    , Loader(..)
+    , NodeId
+    ) where
+
+import           Control.Monad.Except
+import           Control.Monad.State
+import qualified Data.ByteString.Lazy as BS.L
+import qualified Data.Map             as Map
+import qualified Data.Set             as Set
+
+import           Data.YAML.Event      (Tag)
+import qualified Data.YAML.Event      as YE
+import           Util
+
+-- | Unique identifier for identifying nodes
+--
+-- This is allows to observe the alias/anchor-reference structure
+type NodeId = Word
+
+-- | Structure defining how to construct a document tree/graph
+--
+data Loader m n = Loader
+  { yScalar   :: Tag -> YE.Style -> Text -> m (Either String n)
+  , ySequence :: Tag -> [n]              -> m (Either String n)
+  , yMapping  :: Tag -> [(n,n)]          -> m (Either String n)
+  , yAlias    :: NodeId -> Bool -> n     -> m (Either String n)
+  , yAnchor   :: NodeId -> n             -> m (Either String n)
+  }
+
+-- | Generalised document tree/graph construction
+--
+-- This doesn't yet perform any tag resolution (thus all scalars are
+-- represented as 'Text' values). See also 'decodeNode' for a more
+-- convenient interface.
+{-# INLINEABLE decodeLoader #-}
+decodeLoader :: forall n m . MonadFix m => Loader m n -> BS.L.ByteString -> m (Either String [n])
+decodeLoader Loader{..} bs0 = do
+    case sequence . YE.parseEvents $ bs0 of
+      Left (pos,err)
+        | YE.posCharOffset pos < 0 -> return (Left err)
+        | otherwise                -> return (Left $ ":" ++ show (YE.posLine pos) ++ ":" ++ show (YE.posColumn pos) ++ ": " ++ err)
+      Right evs                    -> runParserT goStream evs
+  where
+    goStream :: PT n m [n]
+    goStream = do
+      _ <- satisfy (== YE.StreamStart)
+      ds <- manyUnless (== YE.StreamEnd) goDoc
+      eof
+      return ds
+
+    goDoc :: PT n m n
+    goDoc = do
+      _ <- satisfy isDocStart
+      modify $ \s0 -> s0 { sDict = mempty, sCycle = mempty }
+      n <- goNode
+      _ <- satisfy isDocEnd
+      return n
+
+    getNewNid :: PT n m Word
+    getNewNid = state $ \s0 -> let i0 = sIdCnt s0
+                               in (i0, s0 { sIdCnt = i0+1 })
+
+    returnNode :: (Maybe YE.Anchor) -> Either String n -> PT n m n
+    returnNode _ (Left err) = throwError err
+    returnNode Nothing (Right node) = return node
+    returnNode (Just a) (Right node) = do
+      nid <- getNewNid
+      node0 <- lift $ yAnchor nid node
+      node' <- liftEither node0
+      modify $ \s0 -> s0 { sDict = Map.insert a (nid,node') (sDict s0) }
+      return node'
+
+    registerAnchor :: Maybe YE.Anchor -> PT n m n -> PT n m n
+    registerAnchor Nothing  pn = pn
+    registerAnchor (Just a) pn = do
+      modify $ \s0 -> s0 { sCycle = Set.insert a (sCycle s0) }
+      nid <- getNewNid
+
+      mdo
+        modify $ \s0 -> s0 { sDict = Map.insert a (nid,n) (sDict s0) }
+        n0 <- pn
+        n1 <- lift $ yAnchor nid n0
+        n <-  liftEither n1
+        return n
+
+    exitAnchor :: Maybe YE.Anchor -> PT n m ()
+    exitAnchor Nothing = return ()
+    exitAnchor (Just a) = modify $ \s0 -> s0 { sCycle = Set.delete a (sCycle s0) }
+
+    goNode :: PT n m n
+    goNode = do
+      n <- satisfy (const True)
+      case n of
+        YE.Scalar manc tag sty val -> do
+          exitAnchor manc
+          n' <- lift $ yScalar tag sty val
+          returnNode manc $! n'
+
+        YE.SequenceStart manc tag -> registerAnchor manc $ do
+          ns <- manyUnless (== YE.SequenceEnd) goNode
+          exitAnchor manc
+          liftEither =<< (lift $ ySequence tag ns)
+
+        YE.MappingStart manc tag -> registerAnchor manc $ do
+          kvs <- manyUnless (== YE.MappingEnd) (liftM2 (,) goNode goNode)
+          exitAnchor manc
+          liftEither =<< (lift $ yMapping tag kvs)
+
+        YE.Alias a -> do
+          d <- gets sDict
+          cy <- gets sCycle
+          case Map.lookup a d of
+            Nothing -> throwError ("anchor not found: " ++ show a)
+            Just (nid,n') -> liftEither =<< (lift $ yAlias nid (Set.member a cy) n')
+
+        _ -> throwError "goNode: unexpected event"
+
+
+----------------------------------------------------------------------------
+-- small parser framework
+
+
+data S n = S { sEvs   :: [YE.Event]
+             , sDict  :: Map YE.Anchor (Word,n)
+             , sCycle :: Set YE.Anchor
+             , sIdCnt :: !Word
+             }
+
+newtype PT n m a = PT (StateT (S n) (ExceptT String m) a)
+                 deriving ( Functor
+                          , Applicative
+                          , Monad
+                          , MonadState (S n)
+                          , MonadError String
+                          , MonadFix
+                          )
+
+instance MonadTrans (PT n) where
+  lift = PT . lift . lift
+
+runParserT :: Monad m => PT n m a -> [YE.Event] -> m (Either String a)
+runParserT (PT act) s0 = runExceptT $ evalStateT act (S s0 mempty mempty 0)
+
+satisfy :: Monad m => (YE.Event -> Bool) -> PT n m YE.Event
+satisfy p = do
+  s0 <- get
+  case sEvs s0 of
+    [] -> throwError "satisfy: premature eof"
+    (ev:rest)
+       | p ev -> do put (s0 { sEvs = rest})
+                    return ev
+       | otherwise -> throwError ("satisfy: predicate failed " ++ show ev)
+
+peek :: Monad m => PT n m (Maybe YE.Event)
+peek = do
+  s0 <- get
+  case sEvs s0 of
+    []     -> return Nothing
+    (ev:_) -> return (Just ev)
+
+peek1 :: Monad m => PT n m YE.Event
+peek1 = maybe (throwError "peek1: premature eof") return =<< peek
+
+anyEv :: Monad m => PT n m YE.Event
+anyEv = satisfy (const True)
+
+eof :: Monad m => PT n m ()
+eof = do
+  s0 <- get
+  case sEvs s0 of
+    [] -> return ()
+    _  -> throwError "eof expected"
+
+-- NB: consumes the end-event
+manyUnless :: Monad m => (YE.Event -> Bool) -> PT n m a -> PT n m [a]
+manyUnless p act = do
+  t0 <- peek1
+  if p t0
+    then anyEv >> return []
+    else liftM2 (:) act (manyUnless p act)
+
+{-
+tryError :: MonadError e m => m a -> m (Either e a)
+tryError act = catchError (Right <$> act) (pure . Left)
+-}
+
+isDocStart :: YE.Event -> Bool
+isDocStart (YE.DocumentStart _) = True
+isDocStart _                    = False
+
+isDocEnd :: YE.Event -> Bool
+isDocEnd (YE.DocumentEnd _) = True
+isDocEnd _                  = False
diff --git a/src/Data/YAML/Schema.hs b/src/Data/YAML/Schema.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/YAML/Schema.hs
@@ -0,0 +1,345 @@
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE RecordWildCards     #-}
+{-# LANGUAGE Safe                #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+-- YAML 1.2 Schema resolvers
+--
+module Data.YAML.Schema
+    ( SchemaResolver(..)
+    , failsafeSchemaResolver
+    , jsonSchemaResolver
+    , coreSchemaResolver
+    , Scalar(..)
+
+    , tagNull, tagBool, tagStr, tagInt, tagFloat, tagSeq, tagMap
+    ) where
+
+import           Control.Monad.Except
+import qualified Data.Char            as C
+import qualified Data.Map             as Map
+import qualified Data.Set             as Set
+import qualified Data.Text            as T
+import           Numeric              (readHex, readOct)
+import           Text.Parsec          as P
+import           Text.Parsec.Text
+
+import           Data.YAML.Event      (Tag, isUntagged, mkTag, untagged)
+import qualified Data.YAML.Event      as YE
+
+import           Util
+
+-- | Primitive scalar types as defined in YAML 1.2
+data Scalar = SNull            -- ^ @tag:yaml.org,2002:null@
+            | SBool   !Bool    -- ^ @tag:yaml.org,2002:bool@
+            | SFloat  !Double  -- ^ @tag:yaml.org,2002:float@
+            | SInt    !Integer -- ^ @tag:yaml.org,2002:int@
+            | SStr    !Text    -- ^ @tag:yaml.org,2002:str@
+
+            | SUnknown !Tag !Text -- ^ unknown/unsupported tag or untagged (thus unresolved) scalar
+            deriving (Eq,Ord,Show)
+
+
+-- | Definition of a [YAML 1.2 Schema](http://yaml.org/spec/1.2/spec.html#Schema)
+--
+-- A YAML schema defines how implicit tags are resolved to concrete tags and how data is represented textually in YAML.
+data SchemaResolver = SchemaResolver
+     { schemaResolverScalar   :: Tag -> YE.Style -> T.Text -> Either String Scalar
+     , schemaResolverSequence :: Tag -> Either String Tag
+     , schemaResolverMapping  :: Tag -> Either String Tag
+     }
+
+
+data ScalarTag = ScalarBangTag   -- ^ non-specific ! tag
+               | ScalarQMarkTag  -- ^ non-specific ? tag
+               | ScalarTag !Tag  -- ^ specific tag
+
+-- common logic for 'schemaResolverScalar'
+scalarTag :: (ScalarTag -> T.Text -> Either String Scalar)
+             -> Tag -> YE.Style -> T.Text -> Either String Scalar
+scalarTag f tag sty val = f tag' val
+  where
+    tag' = case sty of
+             YE.Plain
+               | tag == untagged -> ScalarQMarkTag -- implicit ? tag
+
+             _ | tag == untagged -> ScalarBangTag -- implicit ! tag
+               | tag == tagBang  -> ScalarBangTag -- explicit ! tag
+               | otherwise       -> ScalarTag tag
+
+
+-- | \"Failsafe\" schema resolver as specified
+-- in [YAML 1.2 / 10.1.2. Tag Resolution](http://yaml.org/spec/1.2/spec.html#id2803036)
+failsafeSchemaResolver :: SchemaResolver
+failsafeSchemaResolver = SchemaResolver{..}
+  where
+    -- scalars
+    schemaResolverScalar = scalarTag go
+      where
+        go ScalarBangTag  v = Right (SStr v)
+        go (ScalarTag t)  v
+          | t == tagStr     = Right (SStr v)
+          | otherwise       = Right (SUnknown t v)
+        go ScalarQMarkTag v = Right (SUnknown untagged v) -- leave unresolved
+
+    -- mappings
+    schemaResolverMapping t
+      | t == tagBang = Right tagMap
+      | otherwise    = Right t
+
+    -- sequences
+    schemaResolverSequence t
+      | t == tagBang = Right tagSeq
+      | otherwise    = Right t
+
+-- | Strict JSON schema resolver as specified
+-- in [YAML 1.2 / 10.2.2. Tag Resolution](http://yaml.org/spec/1.2/spec.html#id2804356)
+jsonSchemaResolver :: SchemaResolver
+jsonSchemaResolver = SchemaResolver{..}
+  where
+    -- scalars
+    schemaResolverScalar = scalarTag go
+      where
+        go ScalarBangTag  v = Right (SStr v)
+        go (ScalarTag t)  v
+          | t == tagStr     = Right (SStr v)
+          | t == tagNull  = if isNullLiteral v then Right SNull else Left ("invalid !!null " ++ show v)
+          | t == tagInt   = maybe (Left $ "invalid !!int " ++ show v)   (Right . SInt)   $ jsonDecodeInt   v
+          | t == tagFloat = maybe (Left $ "invalid !!float " ++ show v) (Right . SFloat) $ jsonDecodeFloat v
+          | t == tagBool  = maybe (Left $ "invalid !!bool " ++ show v)  (Right . SBool)  $ jsonDecodeBool  v
+          | otherwise       = Right (SUnknown t v) -- unknown specific tag
+        go ScalarQMarkTag v
+          | isNullLiteral v             = Right SNull
+          | Just b <- jsonDecodeBool  v = Right $! SBool b
+          | Just i <- jsonDecodeInt   v = Right $! SInt i
+          | Just f <- jsonDecodeFloat v = Right $! SFloat f
+          | otherwise = Right (SUnknown untagged v) -- leave unresolved  -- FIXME: YAML 1.2 spec requires an error here
+
+    isNullLiteral = (== "null")
+
+    -- mappings
+    schemaResolverMapping t
+      | t == tagBang = Right tagMap
+      | isUntagged t = Right tagMap
+      | otherwise    = Right t
+
+    -- sequences
+    schemaResolverSequence t
+      | t == tagBang = Right tagSeq
+      | isUntagged t = Right tagSeq
+      | otherwise    = Right t
+
+-- | Core JSON schema resolver as specified
+-- in [YAML 1.2 / 10.3.2. Tag Resolution](http://yaml.org/spec/1.2/spec.html#id2805071)
+coreSchemaResolver :: SchemaResolver
+coreSchemaResolver = SchemaResolver{..}
+  where
+    -- scalars
+    schemaResolverScalar = scalarTag go
+      where
+        go ScalarBangTag  v = Right (SStr v)
+        go (ScalarTag t)  v
+          | t == tagStr     = Right (SStr v)
+          | t == tagNull  = if isNullLiteral v then Right SNull else Left ("invalid !!null " ++ show v)
+          | t == tagInt   = maybe (Left $ "invalid !!int " ++ show v)   (Right . SInt)   $ coreDecodeInt   v
+          | t == tagFloat = maybe (Left $ "invalid !!float " ++ show v) (Right . SFloat) $ coreDecodeFloat v
+          | t == tagBool  = maybe (Left $ "invalid !!bool " ++ show v)  (Right . SBool)  $ coreDecodeBool  v
+          | otherwise       = Right (SUnknown t v) -- unknown specific tag
+        go ScalarQMarkTag v
+          | isNullLiteral v             = Right SNull
+          | Just b <- coreDecodeBool  v = Right $! SBool b
+          | Just i <- coreDecodeInt   v = Right $! SInt i
+          | Just f <- coreDecodeFloat v = Right $! SFloat f
+          | otherwise = Right (SStr v) -- map to !!str by default
+
+    isNullLiteral = flip Set.member (Set.fromList [ "", "null", "NULL", "Null", "~" ])
+
+    -- mappings
+    schemaResolverMapping t
+      | t == tagBang = Right tagMap
+      | isUntagged t = Right tagMap
+      | otherwise    = Right t
+
+    -- sequences
+    schemaResolverSequence t
+      | t == tagBang = Right tagSeq
+      | isUntagged t = Right tagSeq
+      | otherwise    = Right t
+
+
+-- | @tag:yaml.org,2002:bool@ (JSON Schema)
+jsonDecodeBool :: T.Text -> Maybe Bool
+jsonDecodeBool "false" = Just False
+jsonDecodeBool "true"  = Just True
+jsonDecodeBool _       = Nothing
+
+-- | @tag:yaml.org,2002:bool@ (Core Schema)
+coreDecodeBool :: T.Text -> Maybe Bool
+coreDecodeBool = flip Map.lookup $
+  Map.fromList [ ("true", True)
+               , ("True", True)
+               , ("TRUE", True)
+               , ("false", False)
+               , ("False", False)
+               , ("FALSE", False)
+               ]
+
+-- | @tag:yaml.org,2002:int@ according to JSON Schema
+--
+-- > 0 | -? [1-9] [0-9]*
+jsonDecodeInt  :: T.Text -> Maybe Integer
+jsonDecodeInt t | T.null t = Nothing
+jsonDecodeInt "0" = Just 0
+jsonDecodeInt t = do
+  -- [-]? [1-9] [0-9]*
+  let tabs | T.isPrefixOf "-" t = T.tail t
+           | otherwise          = t
+
+  guard (not (T.null tabs))
+  guard (T.head tabs /= '0')
+  guard (T.all C.isDigit tabs)
+
+  readMaybe (T.unpack t)
+
+-- | @tag:yaml.org,2002:int@ according to Core Schema
+--
+-- > [-+]? [0-9]+         (Base 10)
+-- > 0o [0-7]+            (Base 8)
+-- > 0x [0-9a-fA-F]+      (Base 16)
+--
+coreDecodeInt :: T.Text -> Maybe Integer
+coreDecodeInt t
+  | T.null t = Nothing
+
+  -- > 0x [0-9a-fA-F]+      (Base 16)
+  | Just rest <- T.stripPrefix "0x" t
+  , T.all C.isHexDigit rest
+  , [(j,"")] <- readHex (T.unpack rest)
+  = Just $! j
+
+  -- 0o [0-7]+            (Base 8)
+  | Just rest <- T.stripPrefix "0o" t
+  , T.all C.isOctDigit rest
+  , [(j,"")] <- readOct (T.unpack rest)
+  = Just $! j
+
+  -- [-+]? [0-9]+         (Base 10)
+  | T.all C.isDigit t
+  = Just $! read (T.unpack t)
+
+  | Just rest <- T.stripPrefix "+" t
+  , not (T.null rest)
+  , T.all C.isDigit rest
+  = Just $! read (T.unpack rest)
+
+  | Just rest <- T.stripPrefix "-" t
+  , not (T.null rest)
+  , T.all C.isDigit rest
+  = Just $! read (T.unpack t)
+
+  | otherwise = Nothing
+
+
+-- | @tag:yaml.org,2002:float@ according to JSON Schema
+--
+-- > -? ( 0 | [1-9] [0-9]* ) ( \. [0-9]* )? ( [eE] [-+]? [0-9]+ )?
+--
+jsonDecodeFloat :: T.Text -> Maybe Double
+jsonDecodeFloat = either (const Nothing) Just . parse float ""
+  where
+    float :: Parser Double
+    float = do
+      -- -?
+      p0 <- option "" ("-" <$ char '-')
+
+      -- ( 0 | [1-9] [0-9]* )
+      p1 <- do
+        d <- digit
+        if (d /= '0')
+          then (d:) <$> P.many digit
+          else pure [d]
+
+      -- ( \. [0-9]* )?
+      p2 <- option "" $ (:) <$> char '.' <*> option "0" (many1 digit)
+
+      -- ( [eE] [-+]? [0-9]+ )?
+      p3 <- option "" $ do
+        void (char 'e' P.<|> char 'E')
+        s <- option "" (("-" <$ char '-') P.<|> ("" <$ char '+'))
+        d <- P.many digit
+
+        pure ("e" ++ s ++ d)
+
+      eof
+
+      let t' = p0++p1++p2++p3
+      pure $! read t'
+
+-- | @tag:yaml.org,2002:float@ according to Core Schema
+--
+-- > [-+]? ( \. [0-9]+ | [0-9]+ ( \. [0-9]* )? ) ( [eE] [-+]? [0-9]+ )?
+--
+coreDecodeFloat :: T.Text -> Maybe Double
+coreDecodeFloat t
+  | Just j <- Map.lookup t literals = Just j -- short-cut
+  | otherwise = either (const Nothing) Just . parse float "" $ t
+  where
+    float :: Parser Double
+    float = do
+      -- [-+]?
+      p0 <- option "" (("-" <$ char '-') P.<|> "" <$ char '+')
+
+      -- ( \. [0-9]+ | [0-9]+ ( \. [0-9]* )? )
+      p1 <- (char '.' *> (("0."++) <$> many1 digit))
+            P.<|> do d1  <- many1 digit
+                     d2  <- option "" $ (:) <$> char '.' <*> option "0" (many1 digit)
+                     pure (d1++d2)
+
+      -- ( [eE] [-+]? [0-9]+ )?
+      p2 <- option "" $ do
+        void (char 'e' P.<|> char 'E')
+        s <- option "" (("-" <$ char '-') P.<|> ("" <$ char '+'))
+        d <- P.many digit
+
+        pure ("e" ++ s ++ d)
+
+      eof
+
+      let t' = p0++p1++p2
+
+      pure $! read t'
+
+    literals = Map.fromList
+      [ ("0"   , 0)
+
+      , (".nan", (0/0))
+      , (".NaN", (0/0))
+      , (".NAN", (0/0))
+
+      , (".inf", (1/0))
+      , (".Inf", (1/0))
+      , (".INF", (1/0))
+
+      , ("+.inf", (1/0))
+      , ("+.Inf", (1/0))
+      , ("+.INF", (1/0))
+
+      , ("-.inf", (-1/0))
+      , ("-.Inf", (-1/0))
+      , ("-.INF", (-1/0))
+      ]
+
+
+tagNull, tagBool, tagStr, tagInt, tagFloat, tagSeq, tagMap, tagBang :: Tag
+tagNull  = mkTag "tag:yaml.org,2002:null"
+tagStr   = mkTag "tag:yaml.org,2002:str"
+tagInt   = mkTag "tag:yaml.org,2002:int"
+tagFloat = mkTag "tag:yaml.org,2002:float"
+tagBool  = mkTag "tag:yaml.org,2002:bool"
+tagSeq   = mkTag "tag:yaml.org,2002:seq"
+tagMap   = mkTag "tag:yaml.org,2002:map"
+tagBang  = mkTag "!"
diff --git a/src/Data/YAML/Token.hs b/src/Data/YAML/Token.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/YAML/Token.hs
@@ -0,0 +1,1958 @@
+{-# LANGUAGE FlexibleContexts       #-}
+{-# LANGUAGE FlexibleInstances      #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE MultiParamTypeClasses  #-}
+{-# LANGUAGE PostfixOperators       #-}
+{-# LANGUAGE Safe                   #-}
+{-# LANGUAGE TypeSynonymInstances   #-}
+
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
+
+{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} -- FIXME
+
+-- |
+-- Copyright: © Oren Ben-Kiki 2007,
+--            © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+-- Tokenizer for the YAML 1.2 syntax as defined in <http://yaml.org/spec/1.2/spec.html>.
+--
+module Data.YAML.Token
+  ( tokenize
+  , Token(..)
+  , Code(..)
+  ) where
+
+import qualified Data.ByteString.Lazy.Char8 as BLC
+import           Data.Char                  (chr, ord)
+import qualified Data.DList                 as D
+import           Prelude                    hiding ((*), (+), (-), (/), (^))
+import qualified Prelude
+
+import           Util                       hiding (empty)
+
+-- * Generic operators
+--
+-- ** Numeric operators
+--
+-- We rename the four numerical operators @+@ @-@ @*@ @\/@ to start with @.@
+-- (@.+@, @.-@, @.*@, @.\/@). This allows us to use the originals for BNF
+-- notation (we also hijack the @^@ operator). This is not a generally
+-- recommended practice. It is justified in this case since we have very little
+-- arithmetic operations, and a lot of BNF rules which this makes extremely
+-- readable.
+
+infixl 6 .+
+-- | \".+\" is the numeric addition (we use \"+\" for postfix \"one or more\").
+(.+) :: Int -> Int -> Int
+(.+) = (Prelude.+)
+
+infixl 6 .-
+-- | \".-\" is the numeric subtraction (we use \"-\" for infix \"and not\").
+(.-) :: Int -> Int -> Int
+(.-) = (Prelude.-)
+
+infixl 7 .*
+-- | \".*\" is the numeric multiplication (we use \"*\" for postfix \"zero or
+-- more\").
+(.*) :: Int -> Int -> Int
+(.*) = (Prelude.*)
+
+-- ** Record field access
+--
+-- We also define @|>@ for record access for increased readability.
+
+infixl 9 |>
+-- | @record |> field@ is the same as @field record@,  but is more readable.
+(|>) :: record -> (record -> value) -> value
+record |> field = field record
+
+-- * UTF decoding
+--
+-- This really should be factored out to the standard libraries. Since it isn't
+-- there, we get to tailor it exactly to our needs. We use lazy byte strings as
+-- input, which should give reasonable I\/O performance when reading large
+-- files. The output is a normal 'Char' list which is easy to work with and
+-- should be efficient enough as long as the 'Parser' does its job right.
+
+-- | Recognized Unicode encodings. As of YAML 1.2 UTF-32 is also required.
+data Encoding = UTF8    -- ^ UTF-8 encoding (or ASCII)
+              | UTF16LE -- ^ UTF-16 little endian
+              | UTF16BE -- ^ UTF-16 big endian
+              | UTF32LE -- ^ UTF-32 little endian
+              | UTF32BE -- ^ UTF-32 big endian
+
+-- | @show encoding@ converts an 'Encoding' to the encoding name (with a "-")
+-- as used by most programs.
+instance Show Encoding where
+    show UTF8    = "UTF-8"
+    show UTF16LE = "UTF-16LE"
+    show UTF16BE = "UTF-16BE"
+    show UTF32LE = "UTF-32LE"
+    show UTF32BE = "UTF-32BE"
+
+-- | @decode bytes@ automatically detects the 'Encoding' used and converts the
+-- /bytes/ to Unicode characters, with byte offsets. Note the offset is for
+-- past end of the character, not its beginning.
+decode :: BLC.ByteString -> (Encoding, [(Int, Char)])
+decode text = (encoding, undoEncoding encoding text)
+  where encoding = detectEncoding $ BLC.unpack $ BLC.take 4 text
+
+-- | @detectEncoding text@ examines the first few chars (bytes) of the /text/
+-- to deduce the Unicode encoding used according to the YAML spec.
+detectEncoding :: [Char] -> Encoding
+detectEncoding text =
+  case text of
+    '\x00' : '\x00' : '\xFE' : '\xFF' : _ -> UTF32BE
+    '\x00' : '\x00' : '\x00' : _      : _ -> UTF32BE
+    '\xFF' : '\xFE' : '\x00' : '\x00' : _ -> UTF32LE
+    _      : '\x00' : '\x00' : '\x00' : _ -> UTF32LE
+    '\xFE' : '\xFF' : _                   -> UTF16BE
+    '\x00' : _      : _                   -> UTF16BE
+    '\xFF' : '\xFE' : _                   -> UTF16LE
+    _      : '\x00' : _                   -> UTF16LE
+    '\xEF' : '\xBB' : '\xBF' : _          -> UTF8
+    _                                     -> UTF8
+
+-- | @undoEncoding encoding bytes@ converts a /bytes/ stream to Unicode
+-- characters according to the /encoding/.
+undoEncoding :: Encoding -> BLC.ByteString -> [(Int, Char)]
+undoEncoding encoding bytes =
+  case encoding of
+    UTF8    -> undoUTF8 bytes 0
+    UTF16LE -> combinePairs $ undoUTF16LE bytes 0
+    UTF16BE -> combinePairs $ undoUTF16BE bytes 0
+    UTF32LE -> combinePairs $ undoUTF32LE bytes 0
+    UTF32BE -> combinePairs $ undoUTF32BE bytes 0
+
+-- ** UTF-32 decoding
+
+-- | @hasFewerThan bytes n@ checks whether there are fewer than /n/ /bytes/
+-- left to read.
+hasFewerThan :: Int -> BLC.ByteString -> Bool
+hasFewerThan n bytes
+  | n == 1 = BLC.null bytes
+  | n  > 1 = BLC.null bytes || hasFewerThan (n .- 1) (BLC.tail bytes)
+  | otherwise = False
+
+-- | @undoUTF32LE bytes offset@ decoded a UTF-32LE /bytes/ stream to Unicode
+-- chars.
+undoUTF32LE :: BLC.ByteString -> Int -> [(Int, Char)]
+undoUTF32LE bytes offset
+  | BLC.null bytes = []
+  | hasFewerThan 4 bytes = error "UTF-32LE input contains invalid number of bytes"
+  | otherwise = let first    = BLC.head bytes
+                    bytes'   = BLC.tail bytes
+                    second   = BLC.head bytes'
+                    bytes''  = BLC.tail bytes'
+                    third    = BLC.head bytes''
+                    bytes''' = BLC.tail bytes''
+                    fourth   = BLC.head bytes'''
+                    rest     = BLC.tail bytes'''
+                in (offset .+ 4,
+                    chr $ (ord first)
+                        .+ 256 .* ((ord second)
+                        .+ 256 .* ((ord third)
+                        .+ 256 .* ((ord fourth))))):(undoUTF32LE rest $ offset .+ 4)
+
+-- | @undoUTF32BE bytes offset@ decoded a UTF-32BE /bytes/ stream to Unicode
+-- chars.
+undoUTF32BE :: BLC.ByteString -> Int -> [(Int, Char)]
+undoUTF32BE bytes offset
+  | BLC.null bytes = []
+  | hasFewerThan 4 bytes = error "UTF-32BE input contains invalid number of bytes"
+  | otherwise = let first    = BLC.head bytes
+                    bytes'   = BLC.tail bytes
+                    second   = BLC.head bytes'
+                    bytes''  = BLC.tail bytes'
+                    third    = BLC.head bytes''
+                    bytes''' = BLC.tail bytes''
+                    fourth   = BLC.head bytes'''
+                    rest     = BLC.tail bytes'''
+                in (offset .+ 4,
+                    chr $ (ord fourth)
+                        .+ 256 .* ((ord third)
+                        .+ 256 .* ((ord second)
+                        .+ 256 .* ((ord first))))):(undoUTF32BE rest $ offset .+ 4)
+
+-- ** UTF-16 decoding
+
+-- | @combinePairs chars@ converts each pair of UTF-16 surrogate characters to a
+-- single Unicode character.
+combinePairs :: [(Int, Char)] -> [(Int, Char)]
+combinePairs []                          = []
+combinePairs (head@(_, head_char):tail)
+  | '\xD800' <= head_char && head_char <= '\xDBFF' = combineLead head tail
+  | '\xDC00' <= head_char && head_char <= '\xDFFF' = error "UTF-16 contains trail surrogate without lead surrogate"
+  | otherwise                                      = head:(combinePairs tail)
+
+-- | @combineLead lead rest@ combines the /lead/ surrogate with the head of the
+-- /rest/ of the input chars, assumed to be a /trail/ surrogate, and continues
+-- combining surrogate pairs.
+combineLead :: (Int, Char) -> [(Int, Char)] -> [(Int, Char)]
+combineLead _lead []                                 = error "UTF-16 contains lead surrogate as final character"
+combineLead (_, lead_char) ((trail_offset, trail_char):rest)
+  | '\xDC00' <= trail_char && trail_char <= '\xDFFF' = (trail_offset, combineSurrogates lead_char trail_char):combinePairs rest
+  | otherwise                                        = error "UTF-16 contains lead surrogate without trail surrogate"
+
+-- | @surrogateOffset@ is copied from the Unicode FAQs.
+surrogateOffset :: Int
+surrogateOffset = 0x10000 .- (0xD800 .* 1024) .- 0xDC00
+
+-- | @combineSurrogates lead trail@ combines two UTF-16 surrogates into a single
+-- Unicode character.
+combineSurrogates :: Char -> Char -> Char
+combineSurrogates lead trail = chr $ (ord lead) .* 1024 .+ (ord trail) .+ surrogateOffset
+
+-- | @undoUTF18LE bytes offset@ decoded a UTF-16LE /bytes/ stream to Unicode
+-- chars.
+undoUTF16LE :: BLC.ByteString -> Int -> [(Int, Char)]
+undoUTF16LE bytes offset
+  | BLC.null bytes = []
+  | hasFewerThan 2 bytes = error "UTF-16LE input contains odd number of bytes"
+  | otherwise = let low    = BLC.head bytes
+                    bytes' = BLC.tail bytes
+                    high   = BLC.head bytes'
+                    rest   = BLC.tail bytes'
+                in (offset .+ 2, chr $ (ord low) .+ (ord high) .* 256):(undoUTF16LE rest $ offset .+ 2)
+
+-- | @undoUTF18BE bytes offset@ decoded a UTF-16BE /bytes/ stream to Unicode
+-- chars.
+undoUTF16BE :: BLC.ByteString -> Int -> [(Int, Char)]
+undoUTF16BE bytes offset
+  | BLC.null bytes = []
+  | hasFewerThan 2 bytes = error "UTF-16BE input contains odd number of bytes"
+  | otherwise = let high   = BLC.head bytes
+                    bytes' = BLC.tail bytes
+                    low    = BLC.head bytes'
+                    rest   = BLC.tail bytes'
+                in (offset .+ 2, chr $ (ord low) .+ (ord high) .* 256):(undoUTF16BE rest $ offset .+ 2)
+
+-- ** UTF-8 decoding
+
+-- | @undoUTF8 bytes offset@ decoded a UTF-8 /bytes/ stream to Unicode chars.
+undoUTF8 :: BLC.ByteString -> Int -> [(Int, Char)]
+undoUTF8 bytes offset
+  | BLC.null bytes = []
+  | otherwise = let first = BLC.head bytes
+                    rest  = BLC.tail bytes
+                in case () of
+                      _ | first < '\x80' -> (offset .+ 1, first):(undoUTF8 rest $ offset .+ 1)
+                        | first < '\xC0' -> error $ "UTF-8 input contains invalid first byte"
+                        | first < '\xE0' -> decodeTwoUTF8 first offset rest
+                        | first < '\xF0' -> decodeThreeUTF8 first offset rest
+                        | first < '\xF8' -> decodeFourUTF8 first offset rest
+                        | otherwise      -> error $ "UTF-8 input contains invalid first byte"
+
+-- | @decodeTwoUTF8 first offset bytes@ decodes a two-byte UTF-8 character,
+-- where the /first/ byte is already available and the second is the head of
+-- the /bytes/, and then continues to undo the UTF-8 encoding.
+decodeTwoUTF8 :: Char -> Int -> BLC.ByteString -> [(Int, Char)]
+decodeTwoUTF8 first offset bytes
+  | BLC.null bytes = error "UTF-8 double byte char is missing second byte at eof"
+  | otherwise = let second = BLC.head bytes
+                    rest   = BLC.tail bytes
+                in case () of
+                      _ | second < '\x80' || '\xBF' < second -> error $ "UTF-8 double byte char has invalid second byte"
+                        | otherwise                          -> (offset .+ 2, combineTwoUTF8 first second):(undoUTF8 rest $ offset .+ 2)
+
+-- | @combineTwoUTF8 first second@ combines the /first/ and /second/ bytes of a
+-- two-byte UTF-8 char into a single Unicode char.
+combineTwoUTF8 :: Char -> Char -> Char
+combineTwoUTF8 first second = chr(((ord first) .- 0xC0) .* 64
+                               .+ ((ord second) .- 0x80))
+
+-- | @decodeThreeUTF8 first offset bytes@ decodes a three-byte UTF-8 character,
+-- where the /first/ byte is already available and the second and third are the
+-- head of the /bytes/, and then continues to undo the UTF-8 encoding.
+decodeThreeUTF8 :: Char -> Int -> BLC.ByteString -> [(Int, Char)]
+decodeThreeUTF8 first offset bytes
+  | hasFewerThan 2 bytes = error "UTF-8 triple byte char is missing bytes at eof"
+  | otherwise = let second = BLC.head bytes
+                    bytes' = BLC.tail bytes
+                    third  = BLC.head bytes'
+                    rest   = BLC.tail bytes'
+                in case () of
+                      _ | second < '\x80' || '\xBF' < second -> error "UTF-8 triple byte char has invalid second byte"
+                        | third < '\x80' || '\xBF' < third   -> error "UTF-8 triple byte char has invalid third byte"
+                        | otherwise                          -> (offset .+ 3, combineThreeUTF8 first second third):(undoUTF8 rest $ offset .+ 3)
+
+-- | @combineThreeUTF8 first second@ combines the /first/, /second/ and /third/
+-- bytes of a three-byte UTF-8 char into a single Unicode char.
+combineThreeUTF8 :: Char -> Char -> Char -> Char
+combineThreeUTF8 first second third = chr(((ord first) .- 0xE0) .* 4096
+                                       .+ ((ord second) .- 0x80) .* 64
+                                       .+ ((ord third)  .- 0x80))
+
+-- | @decodeFourUTF8 first offset bytes@ decodes a four-byte UTF-8 character,
+-- where the /first/ byte is already available and the second, third and fourth
+-- are the head of the /bytes/, and then continues to undo the UTF-8 encoding.
+decodeFourUTF8 :: Char -> Int -> BLC.ByteString -> [(Int, Char)]
+decodeFourUTF8 first offset bytes
+  | hasFewerThan 3 bytes = error "UTF-8 quad byte char is missing bytes at eof"
+  | otherwise = let second  = BLC.head bytes
+                    bytes'  = BLC.tail bytes
+                    third   = BLC.head bytes'
+                    bytes'' = BLC.tail bytes'
+                    fourth  = BLC.head bytes''
+                    rest    = BLC.tail bytes''
+                in case () of
+                      _ | second < '\x80' || '\xBF' < second -> error "UTF-8 quad byte char has invalid second byte"
+                        | third < '\x80' || '\xBF' < third   -> error "UTF-8 quad byte char has invalid third byte"
+                        | third < '\x80' || '\xBF' < third   -> error "UTF-8 quad byte char has invalid fourth byte"
+                        | otherwise                          -> (offset .+ 4, combineFourUTF8 first second third fourth):(undoUTF8 rest $ offset .+ 4)
+
+-- | @combineFourUTF8 first second@ combines the /first/, /second/ and /third/
+-- bytes of a three-byte UTF-8 char into a single Unicode char.
+combineFourUTF8 :: Char -> Char -> Char -> Char -> Char
+combineFourUTF8 first second third fourth = chr(((ord first)  .- 0xF0) .* 262144
+                                             .+ ((ord second) .- 0x80) .* 4096
+                                             .+ ((ord third)  .- 0x80) .* 64
+                                             .+ ((ord fourth) .- 0x80))
+
+-- * Result tokens
+--
+-- The parsing result is a stream of tokens rather than a parse tree. The idea
+-- is to convert the YAML input into \"byte codes\". These byte codes are
+-- intended to be written into a byte codes file (or more likely a UNIX pipe)
+-- for further processing.
+
+-- | 'Token' codes.
+data Code = Bom             -- ^ BOM, contains \"@TF8@\", \"@TF16LE@\", \"@TF32BE@\", etc.
+          | Text            -- ^ Content text characters.
+          | Meta            -- ^ Non-content (meta) text characters.
+          | Break           -- ^ Separation line break.
+          | LineFeed        -- ^ Line break normalized to content line feed.
+          | LineFold        -- ^ Line break folded to content space.
+          | Indicator       -- ^ Character indicating structure.
+          | White           -- ^ Separation white space.
+          | Indent          -- ^ Indentation spaces.
+          | DirectivesEnd   -- ^ Document start marker.
+          | DocumentEnd     -- ^ Document end marker.
+          | BeginEscape     -- ^ Begins escape sequence.
+          | EndEscape       -- ^ Ends escape sequence.
+          | BeginComment    -- ^ Begins comment.
+          | EndComment      -- ^ Ends comment.
+          | BeginDirective  -- ^ Begins directive.
+          | EndDirective    -- ^ Ends directive.
+          | BeginTag        -- ^ Begins tag.
+          | EndTag          -- ^ Ends tag.
+          | BeginHandle     -- ^ Begins tag handle.
+          | EndHandle       -- ^ Ends tag handle.
+          | BeginAnchor     -- ^ Begins anchor.
+          | EndAnchor       -- ^ Ends anchor.
+          | BeginProperties -- ^ Begins node properties.
+          | EndProperties   -- ^ Ends node properties.
+          | BeginAlias      -- ^ Begins alias.
+          | EndAlias        -- ^ Ends alias.
+          | BeginScalar     -- ^ Begins scalar content.
+          | EndScalar       -- ^ Ends scalar content.
+          | BeginSequence   -- ^ Begins sequence content.
+          | EndSequence     -- ^ Ends sequence content.
+          | BeginMapping    -- ^ Begins mapping content.
+          | EndMapping      -- ^ Ends mapping content.
+          | BeginPair       -- ^ Begins mapping key:value pair.
+          | EndPair         -- ^ Ends mapping key:value pair.
+          | BeginNode       -- ^ Begins complete node.
+          | EndNode         -- ^ Ends complete node.
+          | BeginDocument   -- ^ Begins document.
+          | EndDocument     -- ^ Ends document.
+          | BeginStream     -- ^ Begins YAML stream.
+          | EndStream       -- ^ Ends YAML stream.
+          | Error           -- ^ Parsing error at this point.
+          | Unparsed        -- ^ Unparsed due to errors (or at end of test).
+          | Detected        -- ^ Detected parameter (for testing).
+  deriving (Show,Eq)
+
+{-
+-- | @show code@ converts a 'Code' to the one-character YEAST token code char.
+-- The list of byte codes is also documented in the @yaml2yeast@ program.
+instance Show Code where
+  show code = case code of
+                   Bom             -> "U"
+                   Text            -> "T"
+                   Meta            -> "t"
+                   Break           -> "b"
+                   LineFeed        -> "L"
+                   LineFold        -> "l"
+                   Indicator       -> "I"
+                   White           -> "w"
+                   Indent          -> "i"
+                   DirectivesEnd   -> "K"
+                   DocumentEnd     -> "k"
+                   BeginEscape     -> "E"
+                   EndEscape       -> "e"
+                   BeginComment    -> "C"
+                   EndComment      -> "c"
+                   BeginDirective  -> "D"
+                   EndDirective    -> "d"
+                   BeginTag        -> "G"
+                   EndTag          -> "g"
+                   BeginHandle     -> "H"
+                   EndHandle       -> "h"
+                   BeginAnchor     -> "A"
+                   EndAnchor       -> "a"
+                   BeginProperties -> "P"
+                   EndProperties   -> "p"
+                   BeginAlias      -> "R"
+                   EndAlias        -> "r"
+                   BeginScalar     -> "S"
+                   EndScalar       -> "s"
+                   BeginSequence   -> "Q"
+                   EndSequence     -> "q"
+                   BeginMapping    -> "M"
+                   EndMapping      -> "m"
+                   BeginNode       -> "N"
+                   EndNode         -> "n"
+                   BeginPair       -> "X"
+                   EndPair         -> "x"
+                   BeginDocument   -> "O"
+                   EndDocument     -> "o"
+                   Error           -> "!"
+                   Unparsed        -> "-"
+                   Detected        -> "$"
+-}
+
+-- | Parsed token.
+data Token = Token {
+    tByteOffset :: Int,   -- ^ 0-base byte offset in stream.
+    tCharOffset :: Int,   -- ^ 0-base character offset in stream.
+    tLine       :: Int,   -- ^ 1-based line number.
+    tLineChar   :: Int,   -- ^ 0-based character in line.
+    tCode       :: Code,  -- ^ Specific token 'Code'.
+    tText       :: String -- ^ Contained input chars, if any.
+  } deriving Show
+
+
+-- * Parsing framework
+--
+-- Haskell has no shortage of parsing frameworks. We use our own because:
+--
+--  * Most available frameworks are inappropriate because of their focus on
+--    building a parse tree, and completing all of it before any of it is
+--    accessible to the caller. We return a stream of tokens, and would like
+--    its head to be accessible as soon as possible to allow for streaming. To
+--    do this with bounded memory usage we use a combination of continuation
+--    passing style and difference lists for the collected tokens.
+--
+--  * Haskell makes it so easy to roll your own parsing framework. We need some
+--    specialized machinery (limited lookahead, forbidden patterns). It is
+--    possible to build these on top of existing frameworks but the end result
+--    isn't much shorter than rolling our own.
+--
+-- Since we roll our own framework we don't bother with making it generalized,
+-- so we maintain a single 'State' type rather than having a generic one that
+-- contains a polymorphic \"UserState\" field etc.
+
+-- | A 'Parser' is basically a function computing a 'Reply'.
+data Parser result = Parser (State -> Reply result)
+
+-- | The 'Result' of each invocation is either an error, the actual result, or
+-- a continuation for computing the actual result.
+data Result result = Failed String        -- ^ Parsing aborted with a failure.
+                   | Result result        -- ^ Parsing completed with a result.
+                   | More (Parser result) -- ^ Parsing is ongoing with a continuation.
+
+-- Showing a 'Result' is only used in debugging.
+instance (Show result) => Show (Result result) where
+  show result = case result of
+                     Failed message -> "Failed " ++ message
+                     Result result  -> "Result " ++ (show result)
+                     More _         -> "More"
+
+-- | Each invication of a 'Parser' yields a 'Reply'. The 'Result' is only one
+-- part of the 'Reply'.
+data Reply result = Reply {
+    rResult :: !(Result result), -- ^ Parsing result.
+    rTokens :: !(D.DList Token), -- ^ Tokens generated by the parser.
+    rCommit :: !(Maybe String),  -- ^ Commitment to a decision point.
+    rState  :: !State            -- ^ The updated parser state.
+  }
+
+-- Showing a 'State' is only used in debugging.
+instance (Show result) => Show (Reply result) where
+  show reply = "Result: "    ++ (show $ reply|>rResult)
+            ++ ", Tokens: "  ++ (show $ D.toList $ reply|>rTokens)
+            ++ ", Commit: "  ++ (show $ reply|>rCommit)
+            ++ ", State: { " ++ (show $ reply|>rState) ++ "}"
+
+-- A 'Pattern' is a parser that doesn't have an (interesting) result.
+type Pattern = Parser ()
+
+-- ** Parsing state
+
+-- | The internal parser state. We don't bother with parameterising it with a
+-- \"UserState\", we just bundle the generic and specific fields together (not
+-- that it is that easy to draw the line - is @sLine@ generic or specific?).
+data State = State {
+    sEncoding        :: !Encoding,        -- ^ The input UTF encoding.
+    sDecision        :: !String,          -- ^ Current decision name.
+    sLimit           :: !Int,             -- ^ Lookahead characters limit.
+    sForbidden       :: !(Maybe Pattern), -- ^ Pattern we must not enter into.
+    sIsPeek          :: !Bool,            -- ^ Disables token generation.
+    sIsSol           :: !Bool,            -- ^ Is at start of line?
+    sChars           :: ![Char],          -- ^ (Reversed) characters collected for a token.
+    sCharsByteOffset :: !Int,             -- ^ Byte offset of first collected character.
+    sCharsCharOffset :: !Int,             -- ^ Char offset of first collected character.
+    sCharsLine       :: !Int,             -- ^ Line of first collected character.
+    sCharsLineChar   :: !Int,             -- ^ Character in line of first collected character.
+    sByteOffset      :: !Int,             -- ^ Offset in bytes in the input.
+    sCharOffset      :: !Int,             -- ^ Offset in characters in the input.
+    sLine            :: !Int,             -- ^ Builds on YAML's line break definition.
+    sLineChar        :: !Int,             -- ^ Character number in line.
+    sCode            :: !Code,            -- ^ Of token we are collecting chars for.
+    sLast            :: !Char,            -- ^ Last matched character.
+    sInput           :: ![(Int, Char)]    -- ^ The decoded input characters.
+  }
+
+-- Showing a 'State' is only used in debugging. Note that forcing dump of
+-- @sInput@ will disable streaming it.
+instance Show State where
+  show state = "Encoding: "          ++ (show $ state|>sEncoding)
+            ++ ", Decision: "        ++ (show $ state|>sDecision)
+            ++ ", Limit: "           ++ (show $ state|>sLimit)
+            ++ ", IsPeek: "          ++ (show $ state|>sIsPeek)
+            ++ ", IsSol: "           ++ (show $ state|>sIsSol)
+            ++ ", Chars: >>>"        ++ (reverse $ state|>sChars) ++ "<<<"
+            ++ ", CharsByteOffset: " ++ (show $ state|>sCharsByteOffset)
+            ++ ", CharsCharOffset: " ++ (show $ state|>sCharsCharOffset)
+            ++ ", CharsLine: "       ++ (show $ state|>sCharsLine)
+            ++ ", CharsLineChar: "   ++ (show $ state|>sCharsLineChar)
+            ++ ", ByteOffset: "      ++ (show $ state|>sByteOffset)
+            ++ ", CharOffset: "      ++ (show $ state|>sCharOffset)
+            ++ ", Line: "            ++ (show $ state|>sLine)
+            ++ ", LineChar: "        ++ (show $ state|>sLineChar)
+            ++ ", Code: "            ++ (show $ state|>sCode)
+            ++ ", Last: "            ++ (show $ state|>sLast)
+--          ++ ", Input: >>>"        ++ (show $ state|>sInput) ++ "<<<"
+
+-- | @initialState name input@ returns an initial 'State' for parsing the
+-- /input/ (with /name/ for error messages).
+initialState :: BLC.ByteString -> State
+initialState input
+  = State { sEncoding        = encoding
+          , sDecision        = ""
+          , sLimit           = -1
+          , sForbidden       = Nothing
+          , sIsPeek          = False
+          , sIsSol           = True
+          , sChars           = []
+          , sCharsByteOffset = -1
+          , sCharsCharOffset = -1
+          , sCharsLine       = -1
+          , sCharsLineChar   = -1
+          , sByteOffset      = 0
+          , sCharOffset      = 0
+          , sLine            = 1
+          , sLineChar        = 0
+          , sCode            = Unparsed
+          , sLast            = ' '
+          , sInput           = decoded
+          }
+  where
+    (encoding, decoded) = decode input
+
+-- *** Setters
+--
+-- We need four setter functions to pass them around as arguments. For some
+-- reason, Haskell only generates getter functions.
+
+-- | @setLimit limit state@ sets the @sLimit@ field to /limit/.
+setLimit :: Int -> State -> State
+setLimit limit state = state { sLimit = limit }
+
+-- | @setForbidden forbidden state@ sets the @sForbidden@ field to /forbidden/.
+setForbidden :: Maybe Pattern -> State -> State
+setForbidden forbidden state = state { sForbidden = forbidden }
+
+-- | @setCode code state@ sets the @sCode@ field to /code/.
+setCode :: Code -> State -> State
+setCode code state = state { sCode = code }
+
+-- ** Implicit parsers
+--
+-- It is tedious to have to wrap each expected character (or character range)
+-- in an explicit 'Parse' constructor. We let Haskell do that for us using a
+-- 'Match' class.
+
+-- | @Match parameter result@ specifies that we can convert the /parameter/ to
+-- a 'Parser' returning the /result/.
+class Match parameter result | parameter -> result where
+    match :: parameter -> Parser result
+
+-- | We don't need to convert a 'Parser', it already is one.
+instance Match (Parser result) result where
+    match = id
+
+-- | We convert 'Char' to a parser for a character (that returns nothing).
+instance Match Char () where
+    match code = nextIf (== code)
+
+-- | We convert a 'Char' tuple to a parser for a character range (that returns
+-- nothing).
+instance Match (Char, Char) () where
+    match (low, high) = nextIf $ \ code -> low <= code && code <= high
+
+-- | We convert 'String' to a parser for a sequence of characters (that returns
+-- nothing).
+instance Match String () where
+    match = foldr (&) empty
+
+-- ** Reply constructors
+
+-- | @returnReply state result@ prepares a 'Reply' with the specified /state/
+-- and /result/.
+returnReply :: State -> result -> Reply result
+returnReply state result = Reply { rResult = Result result,
+                                   rTokens = D.empty,
+                                   rCommit = Nothing,
+                                   rState  = state }
+
+-- | @tokenReply state token@ returns a 'Reply' containing the /state/ and
+-- /token/. Any collected characters are cleared (either there are none, or we
+-- put them in this token, or we don't want them).
+tokenReply :: State -> Token -> Reply ()
+tokenReply state token = Reply { rResult = Result (),
+                                 rTokens = D.singleton token,
+                                 rCommit = Nothing,
+                                 rState  = state { sCharsByteOffset = -1,
+                                                   sCharsCharOffset = -1,
+                                                   sCharsLine       = -1,
+                                                   sCharsLineChar   = -1,
+                                                   sChars           = [] } }
+
+-- | @failReply state message@ prepares a 'Reply' with the specified /state/
+-- and error /message/.
+failReply :: State -> String -> Reply result
+failReply state message = Reply { rResult = Failed message,
+                                  rTokens = D.empty,
+                                  rCommit = Nothing,
+                                  rState  = state }
+
+-- | @unexpectedReply state@ returns a @failReply@ for an unexpected character.
+unexpectedReply :: State -> Reply result
+unexpectedReply state = case state|>sInput of
+                             ((_, char):_) -> failReply state $ "Unexpected '" ++ [char] ++ "'"
+                             []            -> failReply state "Unexpected end of input"
+
+
+instance Functor Parser where
+  fmap = liftM
+
+instance Applicative Parser where
+  pure = return
+  (<*>) = ap
+
+-- | Allow using the @do@ notation for our parsers, which makes for short and
+-- sweet @do@ syntax when we want to examine the results (we typically don't).
+instance Monad Parser where
+
+  -- @return result@ does just that - return a /result/.
+  return result = Parser $ \ state -> returnReply state result
+
+  -- @left >>= right@ applies the /left/ parser, and if it didn't fail
+  -- applies the /right/ one (well, the one /right/ returns).
+  left >>= right = bindParser left right
+                   where bindParser (Parser left) right = Parser $ \ state ->
+                           let reply = left state
+                           in case reply|>rResult of
+                                   Failed message -> reply { rResult = Failed message }
+                                   Result value   -> reply { rResult = More $ right value }
+                                   More parser    -> reply { rResult = More $ bindParser parser right }
+
+  -- @fail message@ does just that - fails with a /message/.
+  fail message = Parser $ \ state -> failReply state message
+
+-- ** Parsing operators
+--
+-- Here we reap the benefits of renaming the numerical operators. The Operator
+-- precedence, in decreasing strength:
+--
+-- @repeated % n@, @repeated <% n@, @match - rejected@, @match ! decision@,
+-- @match ?! decision@, @choice ^ (first \/ second)@.
+--
+-- @match - first - second@ is @(match - first) - second@.
+--
+-- @first & second & third@ is @first & (second & third)@. Note that @first -
+-- rejected & second@ is @(first - rejected) & second@, etc.
+--
+-- @match \/ alternative \/ otherwise@ is @match \/ (alternative \/
+-- otherwise)@. Note that @first & second \/ third@ is @(first & second) \/
+-- third@.
+--
+-- @( match *)@, @(match +)@, @(match ?)@, @(match <?)@, @(match >?)@, @(match
+-- >!)@, @(match <!)@ are the weakest and require the surrounding @()@.
+
+infix  3 ^
+infix  3 %
+infix  3 <%
+infix  3 !
+infix  3 ?!
+infixl 3 -
+infixr 2 &
+infixr 1 /
+infix  0 ?
+infix  0 *
+infix  0 +
+infix  0 <?
+infix  0 >?
+infix  0 >!
+
+-- | @parser % n@ repeats /parser/ exactly /n/ times.
+(%) :: (Match match result) => match -> Int -> Pattern
+parser % n
+  | n <= 0 = empty
+  | n > 0  = parser & parser % n .- 1
+
+-- | @parser <% n@ matches fewer than /n/ occurrences of /parser/.
+(<%) :: (Match match result) => match -> Int -> Pattern
+parser <% n
+  | n < 1 = fail "Fewer than 0 repetitions"
+  | n == 1 = reject parser Nothing
+  | n > 1  = "<%" ^ ( parser ! "<%" & parser <% n .- 1 / empty )
+
+-- | @decision ^ (option \/ option \/ ...)@ provides a /decision/ name to the
+-- choice about to be made, to allow to @commit@ to it.
+(^) :: (Match match result) => String -> match -> Parser result
+decision ^ parser = choice decision $ match parser
+
+-- | @parser ! decision@ commits to /decision/ (in an option) after
+-- successfully matching the /parser/.
+(!) :: (Match match result) => match -> String -> Pattern
+parser ! decision = parser & commit decision
+
+-- | @parser ?! decision@ commits to /decision/ (in an option) if the current
+-- position matches /parser/, without consuming any characters.
+(?!) :: (Match match result) => match -> String -> Pattern
+parser ?! decision = peek parser & commit decision
+
+-- | @lookbehind <?@ matches the current point without consuming any
+-- characters, if the previous character matches the lookbehind parser (single
+-- character positive lookbehind)
+(<?) :: (Match match result) => match -> Parser result
+(<?) lookbehind = prev lookbehind
+
+-- | @lookahead >?@ matches the current point without consuming any characters
+-- if it matches the lookahead parser (positive lookahead)
+(>?) :: (Match match result) => match -> Parser result
+(>?) lookahead = peek lookahead
+
+-- | @lookahead >?@ matches the current point without consuming any characters
+-- if it matches the lookahead parser (negative lookahead)
+(>!) :: (Match match result) => match -> Pattern
+(>!) lookahead = reject lookahead Nothing
+
+-- | @parser - rejected@ matches /parser/, except if /rejected/ matches at this
+-- point.
+(-) :: (Match match1 result1, Match match2 result2) => match1 -> match2 -> Parser result1
+parser - rejected = reject rejected Nothing & parser
+
+-- | @before & after@ parses /before/ and, if it succeeds, parses /after/. This
+-- basically invokes the monad's @>>=@ (bind) method.
+(&) :: (Match match1 result1, Match match2 result2) => match1 -> match2 -> Parser result2
+before & after = (match before) >> (match after)
+
+-- | @first \/ second@ tries to parse /first/, and failing that parses
+-- /second/, unless /first/ has committed in which case is fails immediately.
+(/) :: (Match match1 result, Match match2 result) => match1 -> match2 -> Parser result
+first / second = Parser $ \ state ->
+  let Parser parser = decide (match first) (match second)
+  in parser state
+
+-- | @(optional ?)@ tries to match /parser/, otherwise does nothing.
+(?) :: (Match match result) => match -> Pattern
+(?) optional = (optional & empty) / empty
+
+-- | @(parser *)@ matches zero or more occurrences of /repeat/, as long as each
+-- one actually consumes input characters.
+(*) :: (Match match result) => match -> Pattern
+(*) parser = "*" ^ zomParser
+  where zomParser = (parser ! "*" & zomParser) / empty
+
+-- | @(parser +)@ matches one or more occurrences of /parser/, as long as each
+-- one actually consumed input characters.
+(+) :: (Match match result) => match -> Pattern
+(+) parser = parser & (parser *)
+
+-- ** Basic parsers
+
+-- | @decide first second@ tries to parse /first/, and failing that parses
+-- /second/, unless /first/ has committed in which case is fails immediately.
+decide :: Parser result -> Parser result -> Parser result
+decide left right = Parser $ \ state ->
+  let Parser parser = decideParser state D.empty left right
+  in parser state
+  where decideParser point tokens (Parser left) right = Parser $ \state ->
+          let reply = left state
+              tokens' reply = D.append tokens $ reply|>rTokens
+          in case (reply|>rResult, reply|>rCommit) of
+                  (Failed _,    _)      -> Reply { rState  = point,
+                                                   rTokens = D.empty,
+                                                   rResult = More right,
+                                                   rCommit = Nothing }
+                  (Result _,   _)       -> reply { rTokens = tokens' reply }
+                  (More left', Just _)  -> reply { rTokens = tokens' reply,
+                                                   rResult = More left' }
+                  (More left', Nothing) -> let Parser parser = decideParser point (tokens' reply) left' right
+                                           in parser $ reply|>rState
+
+-- | @choice decision parser@ provides a /decision/ name to the choice about to
+-- be made in /parser/, to allow to @commit@ to it.
+choice :: String -> Parser result -> Parser result
+choice decision parser = Parser $ \ state ->
+  let Parser parser' = choiceParser (state|>sDecision) decision parser
+  in parser' state { sDecision = decision }
+  where choiceParser parentDecision makingDecision (Parser parser) = Parser $ \ state ->
+          let reply   = parser state
+              commit' = case reply|>rCommit of
+                             Nothing                                    -> Nothing
+                             Just decision | decision == makingDecision -> Nothing
+                                           | otherwise                  -> reply|>rCommit
+              reply'  = case reply|>rResult of
+                             More parser' -> reply { rCommit = commit',
+                                                     rResult = More $ choiceParser parentDecision makingDecision parser' }
+                             _            -> reply { rCommit = commit',
+                                                     rState = (reply|>rState) { sDecision = parentDecision } }
+          in reply'
+
+-- | @parser ``recovery`` pattern@ parses the specified /parser/; if it fails,
+-- it continues to the /recovery/ parser to recover.
+recovery :: (Match match1 result, Match match2 result) => match1 -> match2 -> Parser result
+recovery pattern recover =
+  Parser $ \ state ->
+    let (Parser parser) = match pattern
+        reply = parser state
+    in if state|>sIsPeek
+          then reply
+          else case reply|>rResult of
+                    Result _       -> reply
+                    More more      -> reply { rResult = More $ more `recovery` recover }
+                    Failed message -> reply { rResult = More $ fake Error message & unparsed & recover }
+    where unparsed = let (Parser parser) = match finishToken
+                     in Parser $ \ state -> parser $ state { sCode = Unparsed }
+
+-- | @prev parser@ succeeds if /parser/ matches at the previous character. It
+-- does not consume any input.
+prev :: (Match match result) => match -> Parser result
+prev parser = Parser $ \ state ->
+  prevParser state (match parser) state { sIsPeek = True, sInput = (-1, state|>sLast) : state|>sInput }
+  where prevParser point (Parser parser) state =
+          let reply = parser state
+          in case reply|>rResult of
+                  Failed message -> failReply point message
+                  Result value   -> returnReply point value
+                  More parser'   -> prevParser point parser' $ reply|>rState
+
+-- | @peek parser@ succeeds if /parser/ matches at this point, but does not
+-- consume any input.
+peek :: (Match match result) => match -> Parser result
+peek parser = Parser $ \ state ->
+  peekParser state (match parser) state { sIsPeek = True }
+  where peekParser point (Parser parser) state =
+          let reply = parser state
+          in case reply|>rResult of
+                  Failed message -> failReply point message
+                  Result value   -> returnReply point value
+                  More parser'   -> peekParser point parser' $ reply|>rState
+
+-- | @reject parser name@ fails if /parser/ matches at this point, and does
+-- nothing otherwise. If /name/ is provided, it is used in the error message,
+-- otherwise the messages uses the current character.
+reject :: (Match match result) => match -> Maybe String -> Pattern
+reject parser name = Parser $ \ state ->
+  rejectParser state name (match parser) state { sIsPeek = True }
+  where rejectParser point name (Parser parser) state =
+          let reply = parser state
+          in case reply|>rResult of
+                  Failed _message -> returnReply point ()
+                  Result _value   -> case name of
+                                         Nothing   -> unexpectedReply point
+                                         Just text -> failReply point $ "Unexpected " ++ text
+                  More parser'    -> rejectParser point name parser' $ reply|>rState
+
+-- | @upto parser@ consumes all the character up to and not including the next
+-- point where the specified parser is a match.
+upto :: Pattern -> Pattern
+
+upto parser = ( ( parser >!) & nextIf (const True) *)
+
+-- | @nonEmpty parser@ succeeds if /parser/ matches some non-empty input
+-- characters at this point.
+nonEmpty :: (Match match result) => match -> Parser result
+nonEmpty parser = Parser $ \ state ->
+  let Parser parser' = nonEmptyParser (state|>sCharOffset) (match parser)
+  in parser' state
+  where nonEmptyParser offset (Parser parser) = Parser $ \ state ->
+          let reply = parser state
+              state' = reply|>rState
+          in case reply|>rResult of
+                  Failed _message -> reply
+                  Result _value   -> if state'|>sCharOffset > offset
+                                       then reply
+                                       else failReply state' "Matched empty pattern"
+                  More parser'    -> reply { rResult = More $ nonEmptyParser offset parser' }
+
+-- | @empty@ always matches without consuming any input.
+empty :: Pattern
+empty = return ()
+
+-- | @eof@ matches the end of the input.
+eof :: Pattern
+eof = Parser $ \ state ->
+  if state|>sInput == []
+     then returnReply state ()
+     else unexpectedReply state
+
+-- | @sol@ matches the start of a line.
+sol :: Pattern
+sol = Parser $ \ state ->
+  if state|>sIsSol
+     then returnReply state ()
+     else failReply state "Expected start of line"
+
+-- ** State manipulation pseudo-parsers
+
+-- | @commit decision@ commits the parser to all the decisions up to the most
+-- recent parent /decision/. This makes all tokens generated in this parsing
+-- path immediately available to the caller.
+commit :: String -> Pattern
+commit decision = Parser $ \ state ->
+  Reply { rState  = state,
+          rTokens = D.empty,
+          rResult = Result (),
+          rCommit = Just decision }
+
+-- | @nextLine@ increments @sLine@ counter and resets @sLineChar@.
+nextLine :: Pattern
+nextLine = Parser $ \ state ->
+  returnReply state { sIsSol    = True,
+                      sLine     = state|>sLine .+ 1,
+                      sLineChar = 0 }
+              ()
+
+-- | @with setField getField value parser@ invokes the specified /parser/ with
+-- the value of the specified field set to /value/ for the duration of the
+-- invocation, using the /setField/ and /getField/ functions to manipulate it.
+with :: (value -> State -> State) -> (State -> value) -> value -> Parser result -> Parser result
+with setField getField value parser = Parser $ \ state ->
+  let value' = getField state
+      Parser parser' = value' `seq` withParser value' parser
+  in parser' $ setField value state
+  where withParser parentValue (Parser parser) = Parser $ \ state ->
+          let reply = parser state
+          in case reply|>rResult of
+                  Failed _     -> reply { rState = setField parentValue $ reply|>rState }
+                  Result _     -> reply { rState = setField parentValue $ reply|>rState }
+                  More parser' -> reply { rResult = More $ withParser parentValue parser' }
+
+-- | @parser ``forbidding`` pattern@ parses the specified /parser/ ensuring
+-- that it does not contain anything matching the /forbidden/ parser.
+forbidding :: (Match match1 result1, Match match2 result2) => match1 -> match2 -> Parser result1
+forbidding parser forbidden = with setForbidden sForbidden (Just $ forbidden & empty) (match parser)
+
+-- | @parser ``limitedTo`` limit@ parses the specified /parser/
+-- ensuring that it does not consume more than the /limit/ input chars.
+limitedTo :: (Match match result) => match -> Int -> Parser result
+limitedTo parser limit = with setLimit sLimit limit (match parser)
+
+-- ** Consuming input characters
+
+-- | @nextIf test@ fails if the current position matches the 'State' forbidden
+-- pattern or if the 'State' lookahead limit is reached. Otherwise it consumes
+-- (and buffers) the next input char if it satisfies /test/.
+nextIf :: (Char -> Bool) -> Pattern
+nextIf test = Parser $ \ state ->
+  case state|>sForbidden of
+       Nothing     -> limitedNextIf state
+       Just parser -> let Parser parser' = reject parser $ Just "forbidden pattern"
+                          reply = parser' state { sForbidden = Nothing }
+                      in case reply|>rResult of
+                              Failed _ -> reply
+                              Result _ -> limitedNextIf state
+  where limitedNextIf state =
+          case state|>sLimit of
+               -1     -> consumeNextIf state
+               0      -> failReply state "Lookahead limit reached"
+               _limit -> consumeNextIf state { sLimit = state|>sLimit .- 1 }
+        consumeNextIf state =
+          case state|>sInput of
+               ((offset, char):rest) | test char -> let chars = if state|>sIsPeek
+                                                                   then []
+                                                                   else char:(state|>sChars)
+                                                        byte_offset = charsOf sByteOffset sCharsByteOffset
+                                                        char_offset = charsOf sCharOffset sCharsCharOffset
+                                                        line        = charsOf sLine       sCharsLine
+                                                        line_char   = charsOf sLineChar   sCharsLineChar
+                                                        is_sol = if char == '\xFEFF'
+                                                                    then state|>sIsSol
+                                                                    else False
+                                                        state' = state { sInput           = rest,
+                                                                         sLast            = char,
+                                                                         sChars           = chars,
+                                                                         sCharsByteOffset = byte_offset,
+                                                                         sCharsCharOffset = char_offset,
+                                                                         sCharsLine       = line,
+                                                                         sCharsLineChar   = line_char,
+                                                                         sIsSol           = is_sol,
+                                                                         sByteOffset      = offset,
+                                                                         sCharOffset      = state|>sCharOffset .+ 1,
+                                                                         sLineChar        = state|>sLineChar .+ 1 }
+                                                    in returnReply state' ()
+                           | otherwise -> unexpectedReply state
+               []                      -> unexpectedReply state
+          where charsOf field charsField = if state|>sIsPeek
+                                              then -1
+                                              else if state|>sChars == []
+                                                      then state|>field
+                                                      else state|>charsField
+
+-- ** Producing tokens
+
+-- | @finishToken@ places all collected text into a new token and begins a new
+-- one, or does nothing if there are no collected characters.
+finishToken :: Pattern
+finishToken = Parser $ \ state ->
+  let state' = state { sChars           = [],
+                       sCharsByteOffset = -1,
+                       sCharsCharOffset = -1,
+                       sCharsLine       = -1,
+                       sCharsLineChar   = -1 }
+  in if state|>sIsPeek
+        then returnReply state' ()
+        else case state|>sChars of
+                  []          -> returnReply state' ()
+                  chars@(_:_) -> tokenReply state' Token { tByteOffset = state|>sCharsByteOffset,
+                                                           tCharOffset = state|>sCharsCharOffset,
+                                                           tLine       = state|>sCharsLine,
+                                                           tLineChar   = state|>sCharsLineChar,
+                                                           tCode       = state|>sCode,
+                                                           tText       = reverse chars }
+
+-- | @wrap parser@ invokes the /parser/, ensures any unclaimed input characters
+-- are wrapped into a token (only happens when testing productions), ensures no
+-- input is left unparsed, and returns the parser's result.
+wrap :: (Match match result) => match -> Parser result
+wrap parser = do result <- match parser
+                 finishToken
+                 eof
+                 return result
+
+-- | @token code parser@ places all text matched by /parser/ into a 'Token' with
+-- the specified /code/ (unless it is empty). Note it collects the text even if
+-- there is an error.
+token :: (Match match result) => Code -> match -> Pattern
+token code parser = finishToken & with setCode sCode code (parser & finishToken)
+
+-- | @fake code text@ creates a token with the specified /code/ and \"fake\"
+-- /text/ characters, instead of whatever characters are collected so far.
+fake :: Code -> String -> Pattern
+fake code text = Parser $ \ state ->
+  if state|>sIsPeek
+     then returnReply state ()
+     else tokenReply state Token { tByteOffset = value state sByteOffset sCharsByteOffset,
+                                   tCharOffset = value state sCharOffset sCharsCharOffset,
+                                   tLine       = value state sLine sCharsLine,
+                                   tLineChar   = value state sLineChar sCharsLineChar,
+                                   tCode       = code,
+                                   tText       = text }
+    where value state field1 field2 =
+            if field2 state == -1
+               then field1 state
+               else field2 state
+
+-- | @meta parser@ collects the text matched by the specified /parser/ into a
+-- | @Meta@ token.
+meta :: (Match match result) => match -> Pattern
+meta parser = token Meta parser
+
+-- | @indicator code@ collects the text matched by the specified /parser/ into an
+-- @Indicator@ token.
+indicator :: (Match match result) => match -> Pattern
+indicator parser = token Indicator $ parser
+
+-- | @text parser@  collects the text matched by the specified /parser/ into a
+-- @Text@ token.
+text :: (Match match result) => match -> Pattern
+text parser = token Text parser
+
+-- | @emptyToken code@ returns an empty token.
+emptyToken :: Code -> Pattern
+emptyToken code = finishToken & parser code
+  where parser code = Parser $ \ state ->
+          if state|>sIsPeek
+             then returnReply state ()
+             else tokenReply state Token { tByteOffset = state|>sByteOffset,
+                                           tCharOffset = state|>sCharOffset,
+                                           tLine       = state|>sLine,
+                                           tLineChar   = state|>sLineChar,
+                                           tCode       = code,
+                                           tText       = "" }
+
+-- | @wrapTokens beginCode endCode parser@ wraps the specified /parser/ with
+-- matching /beginCode/ and /endCode/ tokens.
+wrapTokens :: Code -> Code -> Pattern -> Pattern
+wrapTokens beginCode endCode pattern = emptyToken beginCode
+                                      & prefixErrorWith pattern (emptyToken endCode)
+                                      & emptyToken endCode
+
+-- | @prefixErrorWith pattern prefix@ will invoke the @prefix@ parser if an
+-- error is detected during the @pattern@ parser, and then return the error.
+prefixErrorWith :: (Match match result) => match -> Pattern -> Parser result
+prefixErrorWith pattern prefix =
+  Parser $ \ state ->
+    let (Parser parser) = match pattern
+        reply = parser state
+    in case reply|>rResult of
+            Result _       -> reply
+            More more      -> reply { rResult = More $ prefixErrorWith more prefix }
+            Failed message -> reply { rResult = More $ prefix & (fail message :: Parser result) }
+
+-- * Production parameters
+
+-- | Production context.
+data Context = BlockOut     -- ^ Outside block sequence.
+             | BlockIn      -- ^ Inside block sequence.
+             | FlowOut      -- ^ Outside flow collection.
+             | FlowIn       -- ^ Inside flow collection.
+             | BlockKey     -- ^ Implicit block key.
+             | FlowKey      -- ^ Implicit flow key.
+
+-- | @show context@ converts a 'Context' to a 'String'.
+instance Show Context where
+  show context = case context of
+                      BlockOut -> "block-out"
+                      BlockIn  -> "block-in"
+                      FlowOut  -> "flow-out"
+                      FlowIn   -> "flow-in"
+                      BlockKey -> "block-key"
+                      FlowKey  -> "flow-key"
+
+-- | @read context@ converts a 'String' to a 'Context'. We trust our callers to
+-- convert any @-@ characters into @_@ to allow the built-in @lex@ function to
+-- handle the names as single identifiers.
+instance Read Context where
+  readsPrec _ text = [ ((r word), tail) | (word, tail) <- lex text ]
+    where r word = case word of
+                        "block_out" -> BlockOut
+                        "block_in"  -> BlockIn
+                        "flow_out"  -> FlowOut
+                        "flow_in"   -> FlowIn
+                        "block_key" -> BlockKey
+                        "flow_key"  -> FlowKey
+                        _           -> error $ "unknown context: " ++ word
+
+-- | Chomp method.
+data Chomp = Strip -- ^ Remove all trailing line breaks.
+           | Clip  -- ^ Keep first trailing line break.
+           | Keep  -- ^ Keep all trailing line breaks.
+
+-- | @show chomp@ converts a 'Chomp' to a 'String'.
+instance Show Chomp where
+  show chomp = case chomp of
+                    Strip -> "strip"
+                    Clip  -> "clip"
+                    Keep  -> "keep"
+
+-- | @read chomp@ converts a 'String' to a 'Chomp'.
+instance Read Chomp where
+  readsPrec _ text = [ ((r word), tail) | (word, tail) <- lex text ]
+    where r word = case word of
+                        "strip" -> Strip
+                        "clip"  -> Clip
+                        "keep"  -> Keep
+                        _       -> error $ "unknown chomp: " ++ word
+
+-- * Tokenizers
+--
+-- We encapsulate the 'Parser' inside a 'Tokenizer'. This allows us to hide the
+-- implementation details from our callers.
+
+-- | 'Tokenizer' converts a input text into a list of 'Token'. Errors
+-- are reported as tokens with the @Error@ 'Code', and the unparsed text
+-- following an error may be attached as a final token (if the @Bool@ is
+-- @True@). Note that tokens are available \"immediately\", allowing for
+-- streaming of large YAML files with memory requirements depending only on the
+-- YAML nesting level.
+type Tokenizer = BLC.ByteString -> Bool -> [Token]
+
+-- | @patternTokenizer pattern@ converts the /pattern/ to a simple 'Tokenizer'.
+patternTokenizer :: Pattern -> Tokenizer
+patternTokenizer pattern input withFollowing =
+  D.toList $ patternParser (wrap pattern) (initialState input)
+  where patternParser (Parser parser) state =
+          let reply = parser state
+              tokens = commitBugs reply
+              state' = reply|>rState
+          in case reply|>rResult of
+                  Failed message -> errorTokens tokens state' message withFollowing
+                  Result _       -> tokens
+                  More parser'   -> D.append tokens $ patternParser parser' state'
+
+-- | @errorTokens tokens state message withFollowing@ appends an @Error@ token
+-- with the specified /message/ at the end of /tokens/, and if /withFollowing/
+-- also appends the unparsed text following the error as a final @Unparsed@
+-- token.
+errorTokens :: D.DList Token -> State -> String -> Bool -> D.DList Token
+errorTokens tokens state message withFollowing =
+    let tokens' = D.append tokens $ D.singleton Token { tByteOffset = state|>sByteOffset,
+                                                        tCharOffset = state|>sCharOffset,
+                                                        tLine       = state|>sLine,
+                                                        tLineChar   = state|>sLineChar,
+                                                        tCode       = Error,
+                                                        tText       = message }
+    in if withFollowing && state|>sInput /= []
+       then D.append tokens' $ D.singleton Token { tByteOffset = state|>sByteOffset,
+                                                   tCharOffset = state|>sCharOffset,
+                                                   tLine       = state|>sLine,
+                                                   tLineChar   = state|>sLineChar,
+                                                   tCode       = Unparsed,
+                                                   tText       = map snd $ state|>sInput }
+       else tokens'
+
+-- | @commitBugs reply@ inserts an error token if a commit was made outside a
+-- named choice. This should never happen outside tests.
+commitBugs :: Reply result -> D.DList Token
+commitBugs reply =
+  let tokens = reply|>rTokens
+      state = reply|>rState
+  in case reply|>rCommit of
+          Nothing     -> tokens
+          Just commit -> D.append tokens $ D.singleton Token { tByteOffset = state|>sByteOffset,
+                                                               tCharOffset = state|>sCharOffset,
+                                                               tLine       = state|>sLine,
+                                                               tLineChar   = state|>sLineChar,
+                                                               tCode       = Error,
+                                                               tText       = "Commit to '" ++ commit ++ "' was made outside it" }
+
+-- | @'tokenize' input emit_unparsed@
+-- converts the Unicode /input/ to a
+-- list of 'Token' according to the YAML 1.2 specification.
+--
+-- Errors are reported as tokens with @'Error' :: 'Code'@, and the
+-- unparsed text following an error may be attached as a final 'Unparsed' token
+-- (if the /emit_unparsed/ argument is @True@). Note that tokens are available
+-- \"immediately\", allowing for streaming of large YAML files with
+-- memory requirements depending only on the YAML nesting level.
+tokenize :: BLC.ByteString -> Bool -> [Token]
+tokenize = patternTokenizer l_yaml_stream
+
+-- * Productions
+
+-- ** BNF compatibility helpers
+
+-- | @detect_utf_encoding@ doesn't actually detect the encoding, we just call it
+-- this way to make the productions compatible with the spec. Instead it simply
+-- reports the encoding (which was already detected when we started parsing).
+bom :: Match match1 result1 => match1 -> Parser ()
+bom code = code
+         & (Parser $ \ state -> let text = case state|>sEncoding of
+                                                UTF8    -> "TF-8"
+                                                UTF16LE -> "TF-16LE"
+                                                UTF16BE -> "TF-16BE"
+                                                UTF32LE -> "TF-32LE"
+                                                UTF32BE -> "TF-32BE"
+                                    Parser parser = fake Bom text
+                                in parser state)
+
+-- | @na@ is the \"non-applicable\" indentation value. We use Haskell's laziness
+-- to verify it really is never used.
+na :: Int
+na = error "Accessing non-applicable indentation"
+
+-- | @asInteger@ returns the last consumed character, which is assumed to be a
+-- decimal digit, as an integer.
+asInteger :: Parser Int
+asInteger = Parser $ \ state -> returnReply state $ ord (state|>sLast) .- 48
+
+-- | @result value@ is the same as /return value/ except that we give the
+-- Haskell type deduction the additional boost it needs to figure out this is
+-- wrapped in a 'Parser'.
+result :: result -> Parser result
+result = return
+
+----------------------------------------------------------------------------
+
+-- ** Spec productions
+--
+-- These are copied directly from the spec, with the sprinkling of
+-- additional token and decision point directives.
+
+-- 5.1 Character Set
+
+c_printable {- 1 -} = '\x9' / '\xA' / '\xD' / ('\x20', '\x7E')
+                    / '\x85' / ('\xA0', '\xD7FF') / ('\xE000', '\xFFFD')
+                    / ('\x10000', '\x10FFFF')
+
+nb_json {- 2 -} = '\x9' / ('\x20', '\x10FFFF')
+
+-- 5.2 Character Encodings
+
+c_byte_order_mark {- 3 -} = bom '\xFEFF'
+
+-- 5.3 Indicator Characters
+
+c_sequence_entry {-  4 -} = indicator '-'
+c_mapping_key    {-  5 -} = indicator '?'
+c_mapping_value  {-  6 -} = indicator ':'
+
+c_collect_entry  {-  7 -} = indicator ','
+c_sequence_start {-  8 -} = indicator '['
+c_sequence_end   {-  9 -} = indicator ']'
+c_mapping_start  {- 10 -} = indicator '{'
+c_mapping_end    {- 11 -} = indicator '}'
+
+c_comment        {- 12 -} = indicator '#'
+
+c_anchor         {- 13 -} = indicator '&'
+c_alias          {- 14 -} = indicator '*'
+c_tag            {- 15 -} = indicator '!'
+
+c_literal        {- 16 -} = indicator '|'
+c_folded         {- 17 -} = indicator '>'
+
+c_single_quote   {- 18 -} = indicator '\''
+c_double_quote   {- 19 -} = indicator '"'
+
+c_directive      {- 20 -} = indicator '%'
+
+c_reserved       {- 21 -} = indicator ( '@' /  '`' )
+
+c_indicator {- 22 -} = c_sequence_entry / c_mapping_key / c_mapping_value / c_collect_entry
+                     / c_sequence_start / c_sequence_end / c_mapping_start / c_mapping_end
+                     / c_comment / c_anchor / c_alias / c_tag
+                     / c_literal / c_folded / c_single_quote / c_double_quote
+                     / c_directive / c_reserved
+
+c_flow_indicator {- 23 -} = c_collect_entry / c_sequence_start / c_sequence_end / c_mapping_start / c_mapping_end
+
+-- 5.4 Line Break Characters
+
+b_line_feed       {- 24 -} = '\xA'
+b_carriage_return {- 25 -} = '\xD'
+b_char            {- 26 -} = b_line_feed / b_carriage_return
+
+nb_char {- 27 -} = c_printable - b_char - c_byte_order_mark
+
+b_break {- 28 -} = ( b_carriage_return & b_line_feed
+                   / b_carriage_return
+                   / b_line_feed )
+                 & nextLine
+
+b_as_line_feed {- 29 -} = token LineFeed b_break
+
+b_non_content {- 30 -} = token Break b_break
+
+-- 5.5 White Space Characters
+
+s_space {- 31 -} = '\x20'
+s_tab   {- 32 -} = '\x9'
+s_white {- 33 -} = s_space / s_tab
+
+ns_char {- 34 -} = nb_char - s_white
+
+-- 5.6 Miscellaneous Characters
+
+ns_dec_digit {- 35 -} = ('\x30', '\x39')
+
+ns_hex_digit {- 36 -} = ns_dec_digit
+                      / ('\x41', '\x46') / ('\x61', '\x66')
+
+ns_ascii_letter {- 37 -} = ('\x41', '\x5A') / ('\x61', '\x7A')
+
+ns_word_char {- 38 -} = ns_dec_digit / ns_ascii_letter / '-'
+
+ns_uri_char {- 39 -} = "escape"
+                     ^ ( '%' ! "escape" & ns_hex_digit & ns_hex_digit / ns_word_char / '#'
+                       / ';' / '/' / '?' / ':' / '@' / '&'  / '=' / '+' / '$' / ','
+                       / '_' / '.' / '!' / '~' / '*' / '\'' / '(' / ')' / '[' / ']' )
+
+ns_tag_char {- 40 -} = ns_uri_char - c_tag - c_flow_indicator
+
+-- 5.7 Escaped Characters
+
+c_escape {- 41 -} = indicator '\\'
+
+ns_esc_null                {- 42 -} = meta '0'
+ns_esc_bell                {- 43 -} = meta 'a'
+ns_esc_backspace           {- 44 -} = meta 'b'
+ns_esc_horizontal_tab      {- 45 -} = meta ( 't' / '\x9' )
+ns_esc_line_feed           {- 46 -} = meta 'n'
+ns_esc_vertical_tab        {- 47 -} = meta 'v'
+ns_esc_form_feed           {- 48 -} = meta 'f'
+ns_esc_carriage_return     {- 49 -} = meta 'r'
+ns_esc_escape              {- 50 -} = meta 'e'
+ns_esc_space               {- 51 -} = meta '\x20'
+ns_esc_double_quote        {- 52 -} = meta '"'
+ns_esc_slash               {- 53 -} = meta '/'
+ns_esc_backslash           {- 54 -} = meta '\\'
+ns_esc_next_line           {- 55 -} = meta 'N'
+ns_esc_non_breaking_space  {- 56 -} = meta '_'
+ns_esc_line_separator      {- 57 -} = meta 'L'
+ns_esc_paragraph_separator {- 58 -} = meta 'P'
+ns_esc_8_bit               {- 59 -} = indicator 'x' ! "escaped" & meta ( ns_hex_digit % 2 )
+ns_esc_16_bit              {- 60 -} = indicator 'u' ! "escaped" & meta ( ns_hex_digit % 4 )
+ns_esc_32_bit              {- 61 -} = indicator 'U' ! "escaped" & meta ( ns_hex_digit % 8 )
+
+c_ns_esc_char {- 62 -} = wrapTokens BeginEscape EndEscape
+                       $ c_escape ! "escape"
+                       & "escaped"
+                       ^ ( ns_esc_null / ns_esc_bell / ns_esc_backspace
+                         / ns_esc_horizontal_tab / ns_esc_line_feed
+                         / ns_esc_vertical_tab / ns_esc_form_feed
+                         / ns_esc_carriage_return / ns_esc_escape / ns_esc_space
+                         / ns_esc_double_quote / ns_esc_slash / ns_esc_backslash
+                         / ns_esc_next_line / ns_esc_non_breaking_space
+                         / ns_esc_line_separator / ns_esc_paragraph_separator
+                         / ns_esc_8_bit / ns_esc_16_bit / ns_esc_32_bit )
+
+-- 6.1 Indentation Spaces
+
+s_indent    n {- 63 -} = token Indent ( s_space % n )
+
+s_indent_lt n {- 64 -} = token Indent ( s_space <% n )
+s_indent_le n {- 65 -} = token Indent ( s_space <% (n .+ 1) )
+
+-- 6.2 Separation Spaces
+
+s_separate_in_line {- 66 -} = token White ( s_white +) / sol
+
+-- 6.3 Line Prefixes
+
+s_line_prefix n c {- 67 -} = case c of
+                                  BlockOut -> s_block_line_prefix n
+                                  BlockIn  -> s_block_line_prefix n
+                                  FlowOut  -> s_flow_line_prefix n
+                                  FlowIn   -> s_flow_line_prefix n
+
+s_block_line_prefix n {- 68 -} = s_indent n
+s_flow_line_prefix  n {- 69 -} = s_indent n & ( s_separate_in_line ?)
+
+-- 6.4 Empty Lines
+
+l_empty n c {- 70 -} = ( s_line_prefix n c / s_indent_lt n )
+                     & b_as_line_feed
+
+-- 6.5 Line Folding
+
+b_l_trimmed  n c {- 71 -} = b_non_content & ( l_empty n c +)
+
+b_as_space {- 72 -} = token LineFold b_break
+
+b_l_folded n c  {- 73 -} = b_l_trimmed n c / b_as_space
+
+s_flow_folded n {- 74 -} = ( s_separate_in_line ?) & b_l_folded n FlowIn
+                         & s_flow_line_prefix n
+
+-- 6.6 Comments
+
+c_nb_comment_text {- 75 -} = wrapTokens BeginComment EndComment
+                           $ c_comment & meta ( nb_char *)
+
+b_comment {- 76 -} = b_non_content / eof
+
+s_b_comment {- 77 -} = ( s_separate_in_line & ( c_nb_comment_text ?) ?)
+                     & b_comment
+
+l_comment {- 78 -} = s_separate_in_line & ( c_nb_comment_text ?) & b_comment
+
+s_l_comments {- 79 -} = ( s_b_comment / sol )
+                      & ( nonEmpty l_comment *)
+
+-- 6.7 Separation Lines
+
+s_separate n c      {- 80 -} = case c of
+                                    BlockOut -> s_separate_lines n
+                                    BlockIn  -> s_separate_lines n
+                                    FlowOut  -> s_separate_lines n
+                                    FlowIn   -> s_separate_lines n
+                                    BlockKey -> s_separate_in_line
+                                    FlowKey  -> s_separate_in_line
+s_separate_lines n {- 81 -} = s_l_comments & s_flow_line_prefix n
+                            / s_separate_in_line
+
+-- 6.8 Directives
+
+l_directive {- 82 -} = ( wrapTokens BeginDirective EndDirective
+                       $ c_directive ! "doc"
+                       & "directive"
+                       ^ ( ns_yaml_directive
+                         / ns_tag_directive
+                         / ns_reserved_directive ) )
+                     & s_l_comments
+
+ns_reserved_directive  {- 83 -} = ns_directive_name
+                                & ( s_separate_in_line & ns_directive_parameter *)
+ns_directive_name      {- 84 -} = meta ( ns_char +)
+ns_directive_parameter {- 85 -} = meta ( ns_char +)
+
+-- 6.8.1 Yaml Directives
+
+ns_yaml_directive {- 86 -} = meta [ 'Y', 'A', 'M', 'L' ] ! "directive"
+                           & s_separate_in_line & ns_yaml_version
+ns_yaml_version   {- 87 -} = meta ( ( ns_dec_digit +) & '.' & ( ns_dec_digit +) )
+
+-- 6.8.2 Tag Directives
+
+ns_tag_directive {- 88 -} = meta [ 'T', 'A', 'G' ] ! "directive"
+                          & s_separate_in_line & c_tag_handle
+                          & s_separate_in_line & ns_tag_prefix
+
+-- 6.8.2.1 Tag Handles
+
+c_tag_handle {- 89 -} = c_named_tag_handle
+                      / c_secondary_tag_handle
+                      / c_primary_tag_handle
+
+c_primary_tag_handle   {- 90 -} = wrapTokens BeginHandle EndHandle
+                                $ c_tag
+
+c_secondary_tag_handle {- 91 -} = wrapTokens BeginHandle EndHandle
+                                $ c_tag & c_tag
+
+c_named_tag_handle     {- 92 -} = wrapTokens BeginHandle EndHandle
+                                $ c_tag & meta ( ns_word_char +) & c_tag
+
+-- 6.8.2.2 Tag Prefixes
+
+ns_tag_prefix {- 93 -} = wrapTokens BeginTag EndTag
+                       $ ( c_ns_local_tag_prefix / ns_global_tag_prefix )
+
+c_ns_local_tag_prefix {- 94 -} = c_tag & meta ( ns_uri_char *)
+
+ns_global_tag_prefix  {- 95 -} = meta ( ns_tag_char & ( ns_uri_char *) )
+
+-- 6.9 Node Properties
+
+c_ns_properties n c {- 96 -} = wrapTokens BeginProperties EndProperties
+                             $ ( c_ns_tag_property
+                               & ( s_separate n c & c_ns_anchor_property ?) )
+                             / ( c_ns_anchor_property
+                               & ( s_separate n c & c_ns_tag_property ?) )
+
+-- 6.9.1 Node Tags
+
+c_ns_tag_property {- 97 -} = wrapTokens BeginTag EndTag
+                           $ c_verbatim_tag
+                           / c_ns_shorthand_tag
+                           / c_non_specific_tag
+
+c_verbatim_tag     {- 98 -} = c_tag & indicator '<' & meta ( ns_uri_char +) & indicator '>'
+
+c_ns_shorthand_tag {- 99 -} = c_tag_handle & meta ( ns_tag_char +)
+
+c_non_specific_tag {- 100 -} = c_tag
+
+-- 6.9.2 Node Anchors
+
+c_ns_anchor_property {- 101 -} = wrapTokens BeginAnchor EndAnchor
+                               $ c_anchor & ns_anchor_name
+
+ns_anchor_char {- 102 -} = ns_char - c_flow_indicator
+ns_anchor_name {- 103 -} = meta ( ns_anchor_char +)
+
+-- 7.1 Alias Nodes
+
+c_ns_alias_node {- 104 -} = wrapTokens BeginAlias EndAlias
+                          $ c_alias ! "node" & ns_anchor_name
+
+-- 7.2 Empty Nodes
+
+e_scalar {- 105 -} = wrapTokens BeginScalar EndScalar empty
+
+e_node {- 106 -} = wrapTokens BeginNode EndNode e_scalar
+
+-- 7.3.1 Double Quoted Style
+
+nb_double_char {- 107 -} = "escape" ^ ( c_ns_esc_char / ( nb_json - c_escape - c_double_quote ) )
+ns_double_char {- 108 -} = nb_double_char - s_white
+
+c_double_quoted n c {- 109 -} = wrapTokens BeginScalar EndScalar
+                              $ c_double_quote ! "node" & text ( nb_double_text n c ) & c_double_quote
+nb_double_text n c  {- 110 -} = case c of
+                                     FlowOut  -> nb_double_multi_line n
+                                     FlowIn   -> nb_double_multi_line n
+                                     BlockKey -> nb_double_one_line
+                                     FlowKey  -> nb_double_one_line
+nb_double_one_line  {- 111 -} = ( nb_double_char *)
+
+s_double_escaped n {- 112 -} = ( s_white *)
+                             & wrapTokens BeginEscape EndEscape ( c_escape ! "escape" & b_non_content )
+                             & ( l_empty n FlowIn *)
+                             & s_flow_line_prefix n
+s_double_break n   {- 113 -} = "escape" ^ ( s_double_escaped n / s_flow_folded n )
+
+nb_ns_double_in_line    {- 114 -} = ( ( s_white *) & ns_double_char *)
+s_double_next_line n {- 115 -} = s_double_break n
+                               & ( ns_double_char & nb_ns_double_in_line
+                                 & ( s_double_next_line n / ( s_white *) ) ?)
+nb_double_multi_line n  {- 116 -} = nb_ns_double_in_line
+                                  & ( s_double_next_line n / ( s_white *) )
+
+-- 7.3.2 Single Quoted Style
+
+c_quoted_quote {- 117 -} = wrapTokens BeginEscape EndEscape
+                         $ c_single_quote ! "escape" & meta '\''
+nb_single_char {- 118 -} = "escape" ^ ( c_quoted_quote / ( nb_json - c_single_quote ) )
+ns_single_char {- 119 -} = nb_single_char - s_white
+
+c_single_quoted  n c {- 120 -} = wrapTokens BeginScalar EndScalar
+                               $ c_single_quote ! "node" & text ( nb_single_text n c ) & c_single_quote
+nb_single_text n c {- 121 -} = case c of
+                                    FlowOut  -> nb_single_multi_line n
+                                    FlowIn   -> nb_single_multi_line n
+                                    BlockKey -> nb_single_one_line
+                                    FlowKey  -> nb_single_one_line
+nb_single_one_line {- 122 -} = ( nb_single_char *)
+
+nb_ns_single_in_line    {- 123 -} = ( ( s_white *) & ns_single_char *)
+s_single_next_line n {- 124 -} = s_flow_folded n
+                                 & ( ns_single_char & nb_ns_single_in_line
+                                    & ( s_single_next_line n / ( s_white *) ) ?)
+nb_single_multi_line n  {- 125 -} = nb_ns_single_in_line
+                                  & ( s_single_next_line n / ( s_white *) )
+
+-- 7.3.3 Plain Style
+
+ns_plain_first _c  {- 126 -} = ns_char - c_indicator
+                            / ( ':' / '?' / '-' ) & ( ns_char >?)
+
+ns_plain_safe c   {- 127 -} = case c of
+                                   FlowOut  -> ns_plain_safe_out
+                                   FlowIn   -> ns_plain_safe_in
+                                   BlockKey -> ns_plain_safe_out
+                                   FlowKey  -> ns_plain_safe_in
+ns_plain_safe_out {- 128 -} = ns_char - c_mapping_value - c_comment
+ns_plain_safe_in  {- 129 -} = ns_plain_safe_out - c_flow_indicator
+ns_plain_char c   {- 130 -} = ns_plain_safe c
+                            / ( ns_char <?) & '#'
+                            / ':' & ( ns_char >?)
+
+ns_plain n c          {- 131 -} = wrapTokens BeginScalar EndScalar
+                                $ text (case c of
+                                             FlowOut  -> ns_plain_multi_line n c
+                                             FlowIn   -> ns_plain_multi_line n c
+                                             BlockKey -> ns_plain_one_line c
+                                             FlowKey  -> ns_plain_one_line c)
+nb_ns_plain_in_line c {- 132 -} = ( ( s_white *) & ns_plain_char c *)
+ns_plain_one_line c   {- 133 -} = ns_plain_first c ! "node" & nb_ns_plain_in_line c
+
+s_ns_plain_next_line n c {- 134 -} = s_flow_folded n
+                                   & ns_plain_char c & nb_ns_plain_in_line c
+ns_plain_multi_line n c  {- 135 -} = ns_plain_one_line c
+                                   & ( s_ns_plain_next_line n c *)
+
+-- 7.4 Flow Collection Styles
+
+in_flow c {- 136 -} = case c of
+                           FlowOut  -> FlowIn
+                           FlowIn   -> FlowIn
+                           BlockKey -> FlowKey
+                           FlowKey  -> FlowKey
+
+-- 7.4.1 Flow Sequences
+
+c_flow_sequence n c {- 137 -} = wrapTokens BeginSequence EndSequence
+                              $ c_sequence_start ! "node" & ( s_separate n c ?)
+                              & ( ns_s_flow_seq_entries n (in_flow c) ?) & c_sequence_end
+
+ns_s_flow_seq_entries n c {- 138 -} = ns_flow_seq_entry n c & ( s_separate n c ?)
+                                    & ( c_collect_entry & ( s_separate n c ?)
+                                      & ( ns_s_flow_seq_entries n c ?) ?)
+
+ns_flow_seq_entry n c {- 139 -} = "pair" ^ ( ns_flow_pair n c / "node" ^ ns_flow_node n c )
+
+-- 7.4.2 Flow Mappings
+
+c_flow_mapping n c        {- 140 -} = wrapTokens BeginMapping EndMapping
+                                    $ c_mapping_start ! "node" & ( s_separate n c ?)
+                                    & ( ns_s_flow_map_entries n (in_flow c) ?) & c_mapping_end
+ns_s_flow_map_entries n c {- 141 -} = ns_flow_map_entry n c & ( s_separate n c ?)
+                                    & ( c_collect_entry & ( s_separate n c ?)
+                                      & ( ns_s_flow_map_entries n c ?) ?)
+
+ns_flow_map_entry n c {- 142 -}          = wrapTokens BeginPair EndPair
+                                         $ "key" ^ ( ( c_mapping_key ! "key" & s_separate n c
+                                                     & ns_flow_map_explicit_entry n c )
+                                                   / ns_flow_map_implicit_entry n c )
+ns_flow_map_explicit_entry n c {- 143 -} = ns_flow_map_implicit_entry n c
+                                         / ( e_node
+                                           & e_node )
+
+ns_flow_map_implicit_entry n c {- 144 -}    = "pair"
+                                            ^ ( ns_flow_map_yaml_key_entry n c
+                                              / c_ns_flow_map_empty_key_entry n c
+                                              / c_ns_flow_map_json_key_entry n c )
+ns_flow_map_yaml_key_entry n c {- 145 -}    = ( "node" ^ ns_flow_yaml_node n c ) ! "pair"
+                                            & ( ( ( s_separate n c ?)
+                                                & c_ns_flow_map_separate_value n c )
+                                              / e_node )
+c_ns_flow_map_empty_key_entry n c {- 146 -} = e_node
+                                            & c_ns_flow_map_separate_value n c
+
+c_ns_flow_map_separate_value n c {- 147 -}  = c_mapping_value & ( ns_char >!) ! "pair"
+                                            & ( ( s_separate n c & ns_flow_node n c )
+                                              / e_node )
+
+c_ns_flow_map_json_key_entry n c {- 148 -} = ( "node" ^ c_flow_json_node n c ) ! "pair"
+                                           & ( ( ( s_separate n c ?)
+                                               & c_ns_flow_map_adjacent_value n c )
+                                             / e_node )
+c_ns_flow_map_adjacent_value n c {- 149 -} = c_mapping_value ! "pair"
+                                           & ( ( ( s_separate n c ?)
+                                               & ns_flow_node n c )
+                                               / e_node )
+
+ns_flow_pair n c {- 150 -} = wrapTokens BeginMapping EndMapping
+                           $ wrapTokens BeginPair EndPair
+                           $ ( ( c_mapping_key ! "pair" & s_separate n c
+                               & ns_flow_map_explicit_entry n c )
+                             / ns_flow_pair_entry n c )
+
+ns_flow_pair_entry n c            {- 151 -} = ( ns_flow_pair_yaml_key_entry n c
+                                              / c_ns_flow_map_empty_key_entry n c
+                                              / c_ns_flow_pair_json_key_entry n c )
+ns_flow_pair_yaml_key_entry n c   {- 152 -} = ns_s_implicit_yaml_key FlowKey
+                                            & c_ns_flow_map_separate_value n c
+c_ns_flow_pair_json_key_entry n c {- 153 -} = c_s_implicit_json_key FlowKey
+                                            & c_ns_flow_map_adjacent_value n c
+ns_s_implicit_yaml_key c          {- 154 -} = ( "node" ^ ( ns_flow_yaml_node na c ) & ( s_separate_in_line ?) )
+                                            `limitedTo` 1024
+c_s_implicit_json_key c           {- 155 -} = ( "node" ^ ( c_flow_json_node  na c ) & ( s_separate_in_line ?) )
+                                            `limitedTo` 1024
+
+-- 7.5 Flow Nodes
+
+ns_flow_yaml_content n c {- 156 -} = ns_plain n c
+c_flow_json_content n c  {- 157 -} = c_flow_sequence n c / c_flow_mapping n c
+                                   / c_single_quoted n c / c_double_quoted n c
+ns_flow_content n c      {- 158 -} = ns_flow_yaml_content n c / c_flow_json_content n c
+
+ns_flow_yaml_node n c {- 159 -} = wrapTokens BeginNode EndNode
+                                $ c_ns_alias_node
+                                / ns_flow_yaml_content n c
+                                / ( c_ns_properties n c
+                                  & ( ( s_separate n c & ns_flow_yaml_content n c )
+                                    / e_scalar ) )
+c_flow_json_node n c  {- 160 -} = wrapTokens BeginNode EndNode
+                                $ ( c_ns_properties n c & s_separate n c ?)
+                                & c_flow_json_content n c
+ns_flow_node n c      {- 161 -} = wrapTokens BeginNode EndNode
+                                $ c_ns_alias_node
+                                / ns_flow_content n c
+                                / ( c_ns_properties n c
+                                  & ( ( s_separate n c & ns_flow_content n c )
+                                    / e_scalar ) )
+
+-- 8.1.1 Block Scalar Headers
+
+c_b_block_header n {- 162 -} = "header"
+                             ^ ( do m <- c_indentation_indicator n
+                                    t <- c_chomping_indicator
+                                    ( s_white / b_char ) ?! "header"
+                                    s_b_comment
+                                    result (m, t)
+                               / do t <- c_chomping_indicator
+                                    m <- c_indentation_indicator n
+                                    s_b_comment
+                                    result (m, t) )
+
+-- 8.1.1.1 Block Indentation Indicator
+
+c_indentation_indicator n {- 163 -} = indicator ( ns_dec_digit - '0' ) & asInteger
+                                    / detect_scalar_indentation n
+
+detect_scalar_indentation n = peek $ ( nb_char *)
+                                   & ( b_non_content & ( l_empty n BlockIn *) ?)
+                                   & count_spaces (-n)
+
+count_spaces n  = (s_space & count_spaces (n .+ 1))
+                / result (max 1 n)
+
+-- 8.1.1.2 Chomping Indicator
+
+c_chomping_indicator {- 164 -} = indicator '-' & result Strip
+                               / indicator '+' & result Keep
+                               / result Clip
+
+end_block_scalar t = case t of
+                          Strip -> emptyToken EndScalar
+                          Clip  -> emptyToken EndScalar
+                          Keep  -> empty
+
+b_chomped_last t {- 165 -} = case t of
+                                  Strip -> emptyToken EndScalar & b_non_content
+                                  Clip  -> b_as_line_feed & emptyToken EndScalar
+                                  Keep  -> b_as_line_feed
+
+l_chomped_empty n t {- 166 -} = case t of
+                                     Strip -> l_strip_empty n
+                                     Clip  -> l_strip_empty n
+                                     Keep  -> l_keep_empty n
+l_strip_empty n     {- 167 -} = ( s_indent_le n & b_non_content *)
+                              & ( l_trail_comments n ?)
+l_keep_empty n      {- 168 -} = ( l_empty n BlockIn *)
+                              & emptyToken EndScalar
+                              & ( l_trail_comments n ?)
+
+l_trail_comments n {- 169 -} = s_indent_lt n & c_nb_comment_text & b_comment
+                             & ( nonEmpty l_comment *)
+
+-- 8.1.2 Literal Style
+
+c_l__literal n {- 170 -} = do emptyToken BeginScalar
+                              c_literal ! "node"
+                              (m, t) <- c_b_block_header n `prefixErrorWith` emptyToken EndScalar
+                              text ( l_literal_content (n .+ m) t )
+
+l_nb_literal_text n   {- 171 -} = ( l_empty n BlockIn *)
+                                & s_indent n & ( nb_char +)
+b_nb_literal_next n   {- 172 -} = b_as_line_feed
+                                & l_nb_literal_text n
+l_literal_content n t {- 173 -} = ( ( l_nb_literal_text n & ( b_nb_literal_next n *) & b_chomped_last t )
+                                  / end_block_scalar t )
+                                & l_chomped_empty n t
+
+-- 8.1.3 Folded Style
+
+c_l__folded n {- 174 -} = do emptyToken BeginScalar
+                             c_folded ! "node"
+                             (m, t) <- c_b_block_header n `prefixErrorWith` emptyToken EndScalar
+                             text ( l_folded_content (n .+ m) t )
+
+s_nb_folded_text n  {- 175 -} = s_indent n & ns_char ! "fold" & ( nb_char *)
+l_nb_folded_lines n {- 176 -} = s_nb_folded_text n
+                              & ( b_l_folded n BlockIn & s_nb_folded_text n *)
+
+s_nb_spaced_text n  {- 177 -} = s_indent n & s_white ! "fold" & ( nb_char *)
+b_l_spaced        n {- 178 -} = b_as_line_feed
+                              & ( l_empty n BlockIn *)
+l_nb_spaced_lines n {- 179 -} = s_nb_spaced_text n
+                              & ( b_l_spaced n & s_nb_spaced_text n *)
+
+l_nb_same_lines n {- 180 -} = ( l_empty n BlockIn *)
+                            & "fold" ^ ( l_nb_folded_lines n / l_nb_spaced_lines n )
+
+l_nb_diff_lines n {- 181 -} = l_nb_same_lines n
+                            & ( b_as_line_feed & l_nb_same_lines n *)
+
+l_folded_content n t {- 182 -} = ( ( l_nb_diff_lines n & b_chomped_last t )
+                                 / end_block_scalar t )
+                               & l_chomped_empty n t
+
+-- 8.2.1 Block Sequences
+
+detect_collection_indentation n = peek $ ( nonEmpty l_comment* ) & count_spaces (-n)
+detect_inline_indentation       = peek $ count_spaces 0
+
+l__block_sequence n   {- 183 -} = do m  <- detect_collection_indentation n
+                                     wrapTokens BeginSequence EndSequence $ ( s_indent (n .+ m) & c_l_block_seq_entry (n .+ m) +)
+c_l_block_seq_entry n {- 184 -} = c_sequence_entry & ( ns_char >!) ! "node"
+                                & s_l__block_indented n BlockIn
+
+s_l__block_indented n c {- 185 -} = do m <- detect_inline_indentation
+                                       "node" ^ ( ( s_indent m
+                                                  & ( ns_l_in_line_sequence (n .+ 1 .+ m)
+                                                    / ns_l_in_line_mapping (n .+ 1 .+ m) ) )
+                                                / s_l__block_node n c
+                                                / ( e_node & ( s_l_comments ?) & unparsed (n .+ 1) ) ) `recovery` unparsed (n .+ 1)
+ns_l_in_line_sequence n {- 186 -} = wrapTokens BeginNode EndNode
+                                  $ wrapTokens BeginSequence EndSequence
+                                  $ c_l_block_seq_entry n
+                                  & ( s_indent n & c_l_block_seq_entry n *)
+
+-- 8.2.2 Block Mappings
+
+l__block_mapping n = {- 187 -} do m <- detect_collection_indentation n
+                                  wrapTokens BeginMapping EndMapping $ ( s_indent (n .+ m) & ns_l_block_map_entry (n .+ m) +)
+
+ns_l_block_map_entry n {- 188 -} = wrapTokens BeginPair EndPair
+                                 $ c_l_block_map_explicit_entry n
+                                 / ns_l_block_map_implicit_entry n
+c_l_block_map_explicit_entry n {- 189 -} = c_l_block_map_explicit_key n
+                                         & ( l_block_map_explicit_value n
+                                         / e_node )
+c_l_block_map_explicit_key n   {- 190 -} = c_mapping_key ! "node" & s_l__block_indented n BlockOut
+l_block_map_explicit_value n   {- 191 -} = s_indent n & c_mapping_value & s_l__block_indented n BlockOut
+
+ns_l_block_map_implicit_entry n {- 192 -} = ( ns_s_block_map_implicit_key
+                                            / e_node )
+                                          & c_l_block_map_implicit_value n
+ns_s_block_map_implicit_key     {- 193 -} = c_s_implicit_json_key BlockKey
+                                          / ns_s_implicit_yaml_key BlockKey
+
+c_l_block_map_implicit_value n  {- 194 -} = c_mapping_value ! "node"
+                                          & ( ( s_l__block_node n BlockOut
+                                              / ( e_node & ( s_l_comments ?) & unparsed (n .+ 1) ) ) `recovery` unparsed (n .+ 1) )
+
+ns_l_in_line_mapping n {- 195 -} = wrapTokens BeginNode EndNode
+                                 $ wrapTokens BeginMapping EndMapping
+                                 $ ns_l_block_map_entry n
+                                 & ( s_indent n & ns_l_block_map_entry n *)
+
+-- 8.2.3 Block Nodes
+
+unparsed n = ( sol / unparsed_text & unparsed_break )
+           & ( nonEmpty ( unparsed_indent n & unparsed_text & unparsed_break ) *)
+unparsed_indent n = token Unparsed ( s_space % n )
+unparsed_text = token Unparsed ( upto ( eof / c_forbidden / b_break ) )
+unparsed_break = eof / peek c_forbidden / token Unparsed b_break / empty
+
+s_l__block_node n c  {- 196 -} = s_l__block_in_block n c / s_l__flow_in_block n
+s_l__flow_in_block n {- 197 -} = s_separate (n .+ 1) FlowOut
+                               & ns_flow_node (n .+ 1) FlowOut & s_l_comments
+
+s_l__block_in_block n c {- 198 -} = wrapTokens BeginNode EndNode
+                                  $ ( s_l__block_scalar n c / s_l__block_collection n c )
+s_l__block_scalar n c   {- 199 -} = s_separate (n .+ 1) c
+                                  & ( c_ns_properties (n .+ 1) c & s_separate (n .+ 1) c ?)
+                                  & ( c_l__literal n / c_l__folded n )
+
+s_l__block_collection n c {- 200 -} = ( s_separate (n .+ 1) c & c_ns_properties (n .+ 1) c & ( s_l_comments >?) ?)
+                                    & s_l_comments
+                                    & ( l__block_sequence (seq_spaces n c)
+                                      / l__block_mapping n )
+seq_spaces n c            {- 201 -} = case c of
+                                           BlockOut -> n .- 1
+                                           BlockIn  -> n
+
+-- 9.1.1 Document Prefix
+
+l_document_prefix {- 202 -} = ( c_byte_order_mark ?) & ( nonEmpty l_comment *)
+
+-- 9.1.2 Document Markers
+
+c_directives_end  {- 203 -} = token DirectivesEnd [ '-', '-', '-' ]
+c_document_end    {- 204 -} = token DocumentEnd [ '.', '.', '.' ]
+l_document_suffix {- 205 -} = c_document_end & s_l_comments
+c_forbidden       {- 206 -} = sol
+                            & ( c_directives_end / c_document_end )
+                            & ( b_char / s_white / eof )
+
+-- 9.1.3 Explicit Documents
+
+l_bare_document {- 207 -} = "node" ^ s_l__block_node (-1) BlockIn
+                            `forbidding` c_forbidden
+
+-- 9.1.4 Explicit Documents
+
+l_explicit_document {- 208 -} = c_directives_end ! "doc"
+                              & ( ( l_bare_document
+                                  / e_node & ( s_l_comments ?) & unparsed 0 ) `recovery` unparsed 0 )
+
+-- 9.1.5 Directives Documents
+
+l_directives_document {- 209 -} = ( l_directive +)
+                                & l_explicit_document
+
+-- 9.2 Streams:
+
+l_any_document   {- 210 -} = wrapTokens BeginDocument EndDocument
+                           $ "doc" ^ ( l_directives_document
+                                     / l_explicit_document
+                                     / l_bare_document ) `recovery` unparsed 0
+
+l_yaml_stream {- 211 -} = ( nonEmpty l_document_prefix *)
+                        & ( eof / ( c_document_end & ( b_char / s_white / eof ) >?) / l_any_document )
+                        & ( nonEmpty ( "more" ^ ( ( l_document_suffix ! "more" +) & ( nonEmpty l_document_prefix *) & ( eof / l_any_document )
+                                                / ( nonEmpty l_document_prefix *) & "doc" ^ ( wrapTokens BeginDocument EndDocument l_explicit_document ?) ) ) *)
diff --git a/src/Util.hs b/src/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Util.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE CPP                 #-}
+{-# LANGUAGE Safe                #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+
+-- |
+-- Copyright: © Herbert Valerio Riedel 2015-2018
+-- SPDX-License-Identifier: GPL-3.0
+--
+module Util
+    ( liftEither
+    , readMaybe
+    , readEither
+    , fromIntegerMaybe
+
+    , module X
+    ) where
+
+import           Control.Applicative          as X
+import           Control.Monad                as X
+import           Data.Int                     as X
+import           Data.Word                    as X
+
+import           Control.Monad.Except
+import           Control.Monad.Identity       as X
+
+import           Data.Map                     as X (Map)
+import           Data.Monoid                  as X (Monoid (mappend, mempty))
+import           Data.Set                     as X (Set)
+import           Data.Text                    as X (Text)
+
+import           Text.ParserCombinators.ReadP as P
+import           Text.Read
+
+#if !MIN_VERSION_mtl(2,2,2)
+liftEither :: MonadError e m => Either e a -> m a
+liftEither = either throwError return
+#endif
+
+#if !MIN_VERSION_base(4,6,0)
+readMaybe :: Read a => String -> Maybe a
+readMaybe = either (const Nothing) id . readEither
+
+readEither :: Read a => String -> Either String a
+readEither s = case [ x | (x,"") <- readPrec_to_S read' minPrec s ] of
+                 [x] -> Right x
+                 []  -> Left "Prelude.read: no parse"
+                 _   -> Left "Prelude.read: ambiguous parse"
+ where
+  read' = do x <- readPrec
+             Text.Read.lift P.skipSpaces
+             return x
+#endif
+
+
+fromIntegerMaybe :: forall n . (Integral n, Bounded n) => Integer -> Maybe n
+fromIntegerMaybe j
+  | l <= j, j <= u  = Just (fromInteger j)
+  | otherwise       = Nothing
+  where
+    u = toInteger (maxBound :: n)
+    l = toInteger (minBound :: n)
+
