aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYaroslav de la Peña Smirnov <yps@yaroslavps.com>2022-03-24 01:04:02 +0300
committerYaroslav de la Peña Smirnov <yps@yaroslavps.com>2022-03-24 01:04:02 +0300
commit5d66c96a190a396a1535c89bed4e33c2a005fe8d (patch)
tree36a681d8cf226cf30f06b2764c008077d9655dc7
downloadroscha-5d66c96a190a396a1535c89bed4e33c2a005fe8d.tar.gz
roscha-5d66c96a190a396a1535c89bed4e33c2a005fe8d.zip
Initial commit
Basically it works, just needs some polishing and maybe a couple of features that I could actually use. Also probably better docs. Not sure if it will be of use to anybody besides me.
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules3
-rw-r--r--COPYING7
-rw-r--r--LICENSE458
-rw-r--r--Makefile47
-rw-r--r--README.md37
-rw-r--r--include/ast.h221
-rw-r--r--include/hmap.h72
-rw-r--r--include/lexer.h33
-rw-r--r--include/object.h171
-rw-r--r--include/parser.h54
-rw-r--r--include/roscha.h54
-rw-r--r--include/slice.h37
-rw-r--r--include/tests/tests.h45
-rw-r--r--include/token.h84
-rw-r--r--include/vector.h29
m---------sds0
-rw-r--r--src/ast.c322
-rw-r--r--src/hmap.c222
-rw-r--r--src/lexer.c264
-rw-r--r--src/object.c242
-rw-r--r--src/parser.c700
-rw-r--r--src/roscha.c632
-rw-r--r--src/slice.c63
-rw-r--r--src/tests/lexer.c246
-rw-r--r--src/tests/parser.c542
-rw-r--r--src/tests/roscha.c189
-rw-r--r--src/tests/slice.c41
-rw-r--r--src/token.c144
-rw-r--r--src/vector.c49
30 files changed, 5011 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b0b5a06
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.cache
+obj/
+build/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..d12d74d
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "sds"]
+ path = sds
+ url = https://github.com/antirez/sds.git
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..85810d6
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,7 @@
+Copyright (c) 2021 Yaroslav de la Peña Smirnov
+
+This is free software, which you can redistribute and/or modify under the terms
+of the GNU Lesser General Public License, version 2.1 only, unless otherwise
+noted inside of individual source files. It is provided AS IS and WITHOUT ANY
+WARRANTY whatsoever. For more information read the LICENSE file and/or the
+individual source file.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..23632ca
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,458 @@
+GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+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 this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser 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 Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "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
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY 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
+LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2e8c567
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+VERSION=0.1.0
+CC?=gcc
+XFLAGS=
+CFLAGS?=-std=c11 -O2 -Wall $(XFLAGS)
+
+LIBS:=
+IDIRS:=$(addprefix -iquote,include ./)
+
+BUILDIR?=build/release
+
+ifdef DEBUG
+BUILDIR:=build/debug
+CFLAGS:=-std=c11 -O0 -DDEBUG $(XFLAGS) -g
+endif
+ifdef ASAN
+CFLAGS+= -fsanitize=address -fno-omit-frame-pointer
+endif
+
+OBJDIR=$(BUILDIR)/obj
+
+ROSCHA_SRCS:=$(shell find . -name '*.c' -not -path '*/tests/*')
+ROSCHA_OBJS:=$(ROSCHA_SRCS:%.c=$(OBJDIR)/%.o)
+ALL_OBJS:=$(ROSCHA_OBJS)
+TEST_OBJS:=$(filter-out $(OBJDIR)/src/roscha.o,$(ALL_OBJS))
+
+all: roscha
+
+test: tests/slice tests/lexer tests/parser tests/roscha
+
+tests/%: $(OBJDIR)/src/tests/%.o $(TEST_OBJS)
+ mkdir -p $(BUILDIR)/$(@D)
+ $(CC) -o $(BUILDIR)/$@ $^ $(IDIRS) $(LIBS) $(CFLAGS)
+
+$(OBJDIR)/%.o: %.c
+ mkdir -p $(@D)
+ $(CC) -c $(IDIRS) -o $@ $< $(LIBS) $(CFLAGS)
+
+roscha: $(ALL_OBJS)
+ mkdir -p $(@D)
+ $(CC) -o $(BUILDIR)/$@ $^ $(LIBS) $(CFLAGS)
+
+clean:
+ rm -r build
+
+.PHONY: clean all test
+
+.PRECIOUS: $(OBJDIR)/src/tests/%.o
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..21ecb18
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# roscha
+
+A jinja inspired template engine written in C11. Why would I do something like
+this? Because I needed a template engine I could use in C programs, and for the
+lulz, of course.
+
+Probably not the best in terms of performance and especially features, but it
+gets the job done.
+
+## How to use
+
+All the functions and structures that are needed to use roscha are in
+`include/roscha.h`, `include/object.h`, `include/hmap.h`, `include/vector.h`,
+and `include/slice.h`.
+
+Basically you initialize roscha with `roscha_init()`, then create a new
+environment where all the templates and variables will be with
+`roscha_env_new()` add some templates, e.g. you can load them from a dir with
+the `roscha_env_load_dir(env, dir)` function, add some variables to `env->vars`
+hashmap and render a template running `roscha_env_render(env, template_name)`.
+
+All variables used inside roscha are wrapped around a reference counted
+structure called `roscha_object` that also contains the type information needed
+by roscha. You should increment and decrement the reference count appropriately
+using the functions `roscha_object_ref(object)` and
+`roscha_object_unref(object)` accordingly.
+
+After using roscha you should free everything related to roscha by decrementing
+the reference counts, destroying the `struct roscha_env *` environment, and
+calling `roscha_deinit()`.
+
+## TODO
+
+* Better document this... or not if nobody else uses?
+* Probably fix some bugs that are currently hidden.
+* k, v arguments in for...in loops over hashmaps
+* Other stuff like space trimming
diff --git a/include/ast.h b/include/ast.h
new file mode 100644
index 0000000..fe722bf
--- /dev/null
+++ b/include/ast.h
@@ -0,0 +1,221 @@
+#ifndef ROSCHA_AST_H
+#define ROSCHA_AST_H
+
+#include "hmap.h"
+#include "token.h"
+#include "vector.h"
+
+#include "sds/sds.h"
+
+/* AST node structures */
+
+enum block_type {
+ BLOCK_CONTENT,
+ BLOCK_VARIABLE,
+ BLOCK_TAG,
+};
+
+enum tag_type {
+ TAG_IF,
+ TAG_FOR,
+ TAG_BLOCK,
+ TAG_EXTENDS,
+ /* keyword-only tags */
+ TAG_BREAK,
+ TAG_CLOSE,
+};
+
+enum expression_type {
+ EXPRESSION_IDENT,
+ EXPRESSION_INT,
+ EXPRESSION_BOOL,
+ EXPRESSION_STRING,
+ EXPRESSION_PREFIX,
+ EXPRESSION_INFIX,
+ EXPRESSION_MAPKEY,
+ EXPRESSION_INDEX,
+};
+
+struct ident {
+ struct token token;
+};
+
+struct integer {
+ struct token token;
+ int64_t value;
+};
+
+struct boolean {
+ struct token token;
+ bool value;
+};
+
+struct string {
+ struct token token;
+ struct slice value;
+};
+
+struct prefix {
+ struct token token;
+ struct slice operator;
+ struct expression *right;
+};
+
+struct infix {
+ struct token token;
+ struct slice operator;
+ struct expression *left;
+ struct expression *right;
+};
+
+/* Either a map key (map.k) or an array/vector index (arr[i]) */
+struct indexkey {
+ struct token token;
+ struct expression *left;
+ struct expression *key;
+};
+
+struct expression {
+ enum expression_type type;
+ union {
+ struct token token;
+ struct ident ident;
+ struct integer integer;
+ struct boolean boolean;
+ struct string string;
+ struct prefix prefix;
+ struct infix infix;
+ struct indexkey indexkey;
+ };
+};
+
+/* if, elif, else branch */
+struct branch {
+ struct token token;
+ /* if condition is null it means it is an else branch */
+ struct expression *condition;
+ struct vector *subblocks;
+ /* elif or else */
+ struct branch *next;
+};
+
+/* start of if, elif, else */
+struct cond {
+ struct token token;
+ struct branch *root;
+};
+
+/* for loop */
+struct loop {
+ struct token token;
+ struct ident item;
+ struct expression *seq;
+ struct vector *subblocks;
+};
+
+/* template block {% block ... %} */
+struct tblock {
+ struct token token;
+ struct ident name;
+ struct vector *subblocks;
+};
+
+/* {% extends ... %} */
+struct parent {
+ struct token token;
+ struct string *name;
+};
+
+/* {% ... %} blocks */
+struct tag {
+ union{
+ struct token token;
+ struct cond cond;
+ struct loop loop;
+ struct tblock tblock;
+ struct parent parent;
+ };
+ enum tag_type type;
+};
+
+/* {{ ... }} blocks */
+struct variable {
+ struct token token;
+ struct expression *expression;
+};
+
+/* blocks with content that doesn't need evaluation */
+struct content {
+ struct token token;
+};
+
+/*
+ * The template is divided into blocks or chunks which are either plain text
+ * content, {% %} tags or {{ }} variables. Not to be confused with
+ * {% block ... %} tags.
+ */
+struct block {
+ union {
+ struct token token;
+ struct content content;
+ struct tag tag;
+ struct variable variable;
+ };
+ enum block_type type;
+};
+
+/* Root of the AST */
+struct template {
+ /*
+ * The name of the template, might be a file name; used to identifiy the
+ * template in error messages. Will be free'd by template_destroy function
+ * so a copy should be made if it is needed after destroying the AST.
+ */
+ char *name;
+ /*
+ * The source text of the template before parsing. Should be free'd manually
+ * by the caller of roscha_env_render.
+ */
+ char *source;
+ /*
+ * struct that holds references to {% block ... %} tags, for easier/faster
+ * access to said blocks.
+ */
+ struct hmap *tblocks;
+ /*
+ * Holds a child template if there is one. Populated during evaluation,
+ * NULL'ed after evaluation, since a parent template can have different
+ * children depending on the context.
+ */
+ struct template *child;
+ /* vector of blocks */
+ struct vector *blocks;
+};
+
+/* Concatenate to an SDS string a human friendly representation of the node */
+
+sds expression_string(struct expression *, sds str);
+
+sds tag_string(struct tag *, sds str);
+
+sds variable_string(struct variable *, sds str);
+
+sds content_string(struct content *, sds str);
+
+sds block_string(struct block *, sds str);
+
+sds template_string(struct template *, sds str);
+
+/* Free all memory related with the objects */
+
+void branch_destroy(struct branch *);
+
+void tag_destroy(struct tag *);
+
+void expression_destroy(struct expression *);
+
+void block_destroy(struct block *);
+
+void template_destroy(struct template *);
+
+#endif
diff --git a/include/hmap.h b/include/hmap.h
new file mode 100644
index 0000000..fa24982
--- /dev/null
+++ b/include/hmap.h
@@ -0,0 +1,72 @@
+#ifndef ROSCHA_HASHMAP_H
+#define ROSCHA_HASHMAP_H
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "slice.h"
+
+#ifndef HASHMAP_CAP
+#define HASHMAP_CAP 32
+#endif
+
+typedef void (hmap_cb)(const struct slice *key, void *value);
+
+struct hmap {
+ struct hnode **buckets;
+ size_t cap;
+ size_t size;
+};
+
+struct hmap_iter;
+
+/* allocate a new hmap */
+struct hmap *hmap_new_with_cap(size_t cap);
+
+#define hmap_new() hmap_new_with_cap(HASHMAP_CAP)
+
+/*
+ * Inserts a key-value pair into the map. Returns NULL if map did not have key,
+ * old value if it did.
+ */
+void *hmap_sets(struct hmap *hm, struct slice key, void *value);
+
+/* Same as hmap_sets but pass a C string instead */
+#define hmap_set(h, k, v) hmap_sets(h, slice_whole(k), v)
+
+/* Returns a pointer to the value corresponding to the key. */
+void *hmap_gets(struct hmap *hm, const struct slice *key);
+
+/* Same as hmap_gets but pass a C string instead */
+void *hmap_get(struct hmap *hm, const char *key);
+
+/*
+ * Removes a key from the map, returning the value at the key if the key was
+ * previously in the map.
+ */
+void *hmap_removes(struct hmap *hm, const struct slice *key);
+
+/* Same as hmap_removes but pass a C string instead */
+void *hmap_remove(struct hmap *hm, const char *key);
+
+/* Iterate over keys in the hmap */
+void hmap_walk(struct hmap *hm, hmap_cb);
+
+/* Allocate a new hmap iterator */
+struct hmap_iter *hmap_iter_new(struct hmap *);
+
+/* Get the next key, value */
+bool hmap_iter_next(struct hmap_iter *iter, const struct slice **key,
+ void **value);
+
+#define hmap_iter_foreach(it, k, v) while (hmap_iter_next(it, k, v))
+
+/* Free a hmap iterator */
+void hmap_iter_free(struct hmap_iter *iter);
+
+/* free hmap related memory calling a function before freeing each node */
+void hmap_destroy(struct hmap *hm, hmap_cb cb);
+
+/* free hmap related memory */
+void hmap_free(struct hmap *hm);
+
+#endif
diff --git a/include/lexer.h b/include/lexer.h
new file mode 100644
index 0000000..8491c5a
--- /dev/null
+++ b/include/lexer.h
@@ -0,0 +1,33 @@
+#ifndef ROSCHA_LEXER_H
+#define ROSCHA_LEXER_H
+
+#include "slice.h"
+#include "token.h"
+
+#include <sys/types.h>
+#include <stdbool.h>
+
+/* The lexer */
+struct lexer {
+ /* Source input */
+ const char *input;
+ /* Length of input */
+ size_t len;
+ /* The current slice of the input string that will be tokenized */
+ struct slice word;
+ /* The current character belongs to content and should not be tokenized */
+ bool in_content;
+ size_t line;
+ size_t column;
+};
+
+/* Allocate a new lexer with input as the source */
+struct lexer *lexer_new(const char *input);
+
+/* Get the next token from the lexer */
+struct token lexer_next_token(struct lexer *);
+
+/* Free all memory related to the lexer */
+void lexer_destroy(struct lexer *);
+
+#endif
diff --git a/include/object.h b/include/object.h
new file mode 100644
index 0000000..157ac0a
--- /dev/null
+++ b/include/object.h
@@ -0,0 +1,171 @@
+#ifndef ROSCHA_OBJECT_H
+#define ROSCHA_OBJECT_H
+
+#include "hmap.h"
+#include "slice.h"
+#include "vector.h"
+
+#include "sds/sds.h"
+
+#include <stdbool.h>
+#include <sys/types.h>
+
+/* Types of roscha objects */
+enum roscha_type {
+ /* Only used internally; a variable that hasn't been set or defined. Does
+ * not have a correspoding field in the union.
+ */
+ ROSCHA_NULL,
+ /* an integer number. */
+ ROSCHA_INT,
+ /* Only used internally; a boolean value. */
+ ROSCHA_BOOL,
+ /* A text string */
+ ROSCHA_STRING,
+ /* A slice of a string; basically functions the same as a string */
+ ROSCHA_SLICE,
+ /* A vector of roscha objects */
+ ROSCHA_VECTOR,
+ /* A hashmap of roscha objects */
+ ROSCHA_HMAP,
+};
+
+/* A reference counted object for use in the environment */
+struct roscha_object {
+ enum roscha_type type;
+ size_t refcount;
+ union {
+ /* booleans are only used internally */
+ bool boolean;
+ /* integer numbers */
+ int64_t integer;
+ /* A dynamic string using the sds library */
+ sds string;
+ /* String slice */
+ struct slice slice;
+ /* vector of roscha_objects */
+ struct vector *vector;
+ /* hashmap of roscha_objects */
+ struct hmap *hmap;
+ };
+};
+
+/* Concatenate the textual representation of the object to an sds string */
+sds roscha_object_string(const struct roscha_object *, sds str);
+
+/* Return the textual representation of the type */
+inline const char *roscha_type_print(enum roscha_type);
+
+/* Create a new roscha object based on its type */
+struct roscha_object *roscha_object_new_int(int64_t val);
+struct roscha_object *roscha_object_new_string(sds str);
+struct roscha_object *roscha_object_new_slice(struct slice);
+struct roscha_object *roscha_object_new_vector(struct vector *);
+struct roscha_object *roscha_object_new_hmap(struct hmap *);
+
+#define roscha_object_new(v) _Generic((v), \
+ int: roscha_object_new_int, \
+ int64_t: roscha_object_new_int, \
+ sds: roscha_object_new_string, \
+ struct slice: roscha_object_new_slice, \
+ struct vector *: roscha_object_new_vector, \
+ struct hmap *: roscha_object_new_hmap \
+ )(v)
+
+/* Increment reference count of object */
+void roscha_object_ref(struct roscha_object *);
+
+/* Decrement reference count of object */
+void roscha_object_unref(struct roscha_object *);
+
+/*
+ * Helper macro to create a roscha object wrapper and push to the vector in one
+ * line.
+ */
+#define roscha_vector_push_new(vec, val) \
+ vector_push(vec->vector, roscha_object_new(val))
+
+/*
+ * Helper function to push a value to a reference counted vector; increments the
+ * count after adding the value to it.
+ */
+void roscha_vector_push(struct roscha_object *vec, struct roscha_object *val);
+
+/*
+ * Removes and returns the last value from a reference counted vector; doesn't
+ * decrement the reference count since the value is returned.
+ */
+struct roscha_object *roscha_vector_pop(struct roscha_object *vec);
+
+/*
+ * Helper macro to create a roscha object wrapper and insert it to the hmap in
+ * one line.
+ */
+#define roscha_hmap_set_new(h, k, v) hmap_set(h->hmap, k, roscha_object_new(v))
+
+/*
+ * Helper function to add a value to reference counted hmap; increments the
+ * count after adding the value; returns the old value if it was present in the
+ * hmap.
+ */
+struct roscha_object *roscha_hmap_sets(struct roscha_object *hmap,
+ struct slice key, struct roscha_object *value);
+
+/* Same as roscha_hmap_sets but use a C string instead */
+struct roscha_object *roscha_hmap_setstr(struct roscha_object *hmap,
+ const char *key, struct roscha_object *value);
+
+#define roscha_hmap_set(h, k, v) _Generic((k), \
+ char *: roscha_hmap_setstr, \
+ struct slice: roscha_hmap_sets \
+ )(h, k, v)
+
+/*
+ * Get a value from a reference counted hmap; the value's reference count is not
+ * incremented, should be incremented by the receiver if needed.
+ */
+struct roscha_object *roscha_hmap_gets(struct roscha_object *hmap,
+ const struct slice *key);
+
+/* Same as roscha_hmap_gets but use a C string instead */
+struct roscha_object *roscha_hmap_getstr(struct roscha_object *hmap,
+ const char *key);
+
+#define roscha_hmap_get(h, k) _Generic((k), \
+ char *: roscha_hmap_getstr, \
+ struct slice *: roscha_hmap_gets \
+ )(h, k)
+
+/*
+ * Remove a value from a reference counted hmap; the value's reference count is
+ * not decremented, since the value is returned.
+ */
+struct roscha_object *roscha_hmap_pops(struct roscha_object *hmap,
+ const struct slice *key);
+
+/* Same as roscha_hmap_pops but use a C string instead */
+struct roscha_object *roscha_hmap_popstr(struct roscha_object *hmap,
+ const char *key);
+
+#define roscha_hmap_pop(h, k) _Generic((k), \
+ char *: roscha_hmap_popstr, \
+ struct slice *: roscha_hmap_pops \
+ )(h, k)
+
+/*
+ * Remove a value from a reference counted hmap; the value's reference count is
+ * decremented.
+ */
+void roscha_hmap_unsets(struct roscha_object *hmap,
+ const struct slice *key);
+
+/* Same as roscha_hmap_unsets but use a C string instead */
+void roscha_hmap_unsetstr(struct roscha_object *hmap,
+ const char *key);
+
+#define roscha_hmap_unset(h, k) _Generic((k), \
+ char *: roscha_hmap_unsetstr, \
+ struct slice *: roscha_hmap_unsets \
+ )(h, k)
+
+#endif
diff --git a/include/parser.h b/include/parser.h
new file mode 100644
index 0000000..bdf052f
--- /dev/null
+++ b/include/parser.h
@@ -0,0 +1,54 @@
+#ifndef ROSCHA_PARSER_H
+#define ROSCHA_PARSER_H
+
+#include "ast.h"
+#include "hmap.h"
+#include "lexer.h"
+#include "token.h"
+#include "vector.h"
+#include "sds/sds.h"
+
+struct parser {
+ /* The name of the template; transfered to resulting template AST */
+ char *name;
+ /* The lexer that is ought to tokenize our input */
+ struct lexer *lexer;
+ /* Current token */
+ struct token cur_token;
+ /* Next token */
+ struct token peek_token;
+ /*
+ * Temporary field that holds {% block ... %} tags, for easier/faster
+ * access to said blocks without having to traverse all the AST, in case the
+ * template is a child template. This hashmap will be transfered to the
+ * resulting AST upon finishing parsing.
+ */
+ struct hmap *tblocks;
+ /* vector of sds */
+ struct vector *errors;
+};
+
+typedef struct expression *(*prefix_parse_f)(struct parser *);
+typedef struct expression *(*infix_parse_f)(struct parser *, struct expression *);
+
+/* Allocate a new parser */
+struct parser *parser_new(char *name, char *input);
+
+/* Parse template into an AST */
+struct template *parser_parse_template(struct parser *);
+
+/* Free all memory asociated with the parser */
+void parser_destroy(struct parser *);
+
+/*
+ * Initialize variables needed for parsing; may be used by several parsers.
+ */
+void parser_init(void);
+
+/*
+ * Free all static memory related to parsing; called when parsing/evaluation is
+ * no longer needed
+ */
+void parser_deinit(void);
+
+#endif
diff --git a/include/roscha.h b/include/roscha.h
new file mode 100644
index 0000000..82a2062
--- /dev/null
+++ b/include/roscha.h
@@ -0,0 +1,54 @@
+#ifndef ROSCHA_H
+#define ROSCHA_H
+
+#include "object.h"
+
+/* The environment for evaluation templates */
+struct roscha_env {
+ /* Template variables; reference counted hmap of roscha objects */
+ struct roscha_object *vars;
+ /* vector of sds with error messages */
+ struct vector *errors;
+ /* internal */
+ struct roscha_ *internal;
+};
+
+/*
+ * Initialize variables needed for parsing; may be used by several roscha
+ * parsers.
+ */
+void roscha_init(void);
+
+/*
+ * Free all static memory related to roscha; called when parsing/evaluation is
+ * no longer needed
+ */
+void roscha_deinit(void);
+
+/* Allocate a new environment */
+struct roscha_env *roscha_env_new(void);
+
+/*
+ * Parse and add a template to the environment. Returns false upon encountering
+ * a parsing error.
+ */
+bool roscha_env_add_template(struct roscha_env *, char *name, char *body);
+
+/*
+ * Load and parse templates from dir (non-recursively). All non-dir files are
+ * read and parsed. Returns false if an error occurred.
+ */
+bool roscha_env_load_dir(struct roscha_env *, const char *path);
+
+/* Render/evaluate the template */
+sds roscha_env_render(struct roscha_env *, const char *name);
+
+struct vector *roscha_env_check_errors(struct roscha_env *env);
+
+/*
+ * Free all memory associated with the environment, including parsed templates,
+ * and reducing reference counts of objects.
+ */
+void roscha_env_destroy(struct roscha_env *);
+
+#endif
diff --git a/include/slice.h b/include/slice.h
new file mode 100644
index 0000000..add82d5
--- /dev/null
+++ b/include/slice.h
@@ -0,0 +1,37 @@
+#ifndef CMONKEY_SLICE_H
+#define CMONKEY_SLICE_H
+
+#include "sds/sds.h"
+
+#include <string.h>
+#include <sys/types.h>
+
+/* A slice of a C string */
+struct slice {
+ const char *str;
+ size_t start;
+ size_t end;
+};
+
+/* Create a new slice from an existing string indicating its bounds */
+struct slice slice_new(const char *str, size_t start, size_t end);
+
+/* Create a new slice from a string literal */
+#define slice_whole(s) (struct slice){ s, 0, strlen(s), }
+
+/* Set a slice to a new string and bounds */
+void slice_set(struct slice *, const char *str, size_t start, size_t end);
+
+/* Get the length of the slice */
+size_t slice_len(const struct slice *);
+
+/* Returns 0 if equal, 1 if a > b, -1 if a < b */
+int slice_cmp(const struct slice *restrict a, const struct slice *restrict b);
+
+/* Copy the slice from src to dst; dst should already be allocated */
+void slice_cpy(struct slice *dst, const struct slice *src);
+
+/* Concatenate the slice to an SDS string */
+sds slice_string(const struct slice *, sds str);
+
+#endif
diff --git a/include/tests/tests.h b/include/tests/tests.h
new file mode 100644
index 0000000..8c89fcc
--- /dev/null
+++ b/include/tests/tests.h
@@ -0,0 +1,45 @@
+#ifndef TESTS_H
+#define TESTS_H
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifndef NOCOLOR
+#define TBLD "\033[1m"
+#define TRED "\033[31m"
+#define TGRN "\033[32m"
+#define TBLU "\033[34m"
+#define TRST "\033[0m"
+#else
+#define TBLD ""
+#define TRED ""
+#define TGRN ""
+#define TBLU ""
+#define TRST ""
+#endif
+
+#define RUN_TEST(test_func) \
+ printf("%s:\t", #test_func); \
+ fflush(stdout); \
+ test_func(); \
+ printf(TGRN "OK!\n" TRST)
+
+#define INIT_TESTS() \
+ printf(TBLD "running %s tests\n" TRST, __FILE__)
+
+#define FAIL_TEST(reason) \
+ printf(TBLD TRED "FAIL!\n" TRST); \
+ printf("%s:%d: %s: ", __FILE__, __LINE__, __func__); \
+ printf(reason); \
+ abort()
+
+#define asserteq(a, b) \
+ if (a != b) { \
+ FAIL_TEST("assertion " TBLD TBLU #a " == " #b TRST " failed\n"); \
+ }
+
+#define assertneq(a, b) \
+ if (a == b) { \
+ FAIL_TEST("assertion " TBLD TBLU #a " != " #b TRST " failed\n"); \
+ }
+
+#endif
diff --git a/include/token.h b/include/token.h
new file mode 100644
index 0000000..edff1d2
--- /dev/null
+++ b/include/token.h
@@ -0,0 +1,84 @@
+#ifndef ROSCHA_TOKEN_H
+#define ROSCHA_TOKEN_H
+
+#include "slice.h"
+
+#include <stdbool.h>
+
+enum token_type {
+ TOKEN_ILLEGAL,
+ TOKEN_EOF,
+ /* Identifiers/Literals */
+ TOKEN_IDENT,
+ TOKEN_INT,
+ TOKEN_STRING,
+ /* Operators */
+ TOKEN_ASSIGN,
+ TOKEN_PLUS,
+ TOKEN_MINUS,
+ TOKEN_BANG,
+ TOKEN_ASTERISK,
+ TOKEN_SLASH,
+ TOKEN_LT,
+ TOKEN_GT,
+ TOKEN_LTE,
+ TOKEN_GTE,
+ TOKEN_EQ,
+ TOKEN_NOTEQ,
+ /* Keyword-like operators */
+ TOKEN_AND,
+ TOKEN_OR,
+ TOKEN_NOT,
+ /* Delimiters */
+ TOKEN_DOT,
+ TOKEN_COMMA,
+ TOKEN_LPAREN,
+ TOKEN_RPAREN,
+ TOKEN_LBRACE,
+ TOKEN_RBRACE,
+ TOKEN_LBRACKET,
+ TOKEN_RBRACKET,
+ TOKEN_POUND,
+ TOKEN_PERCENT,
+ /* Keywords */
+ TOKEN_FOR,
+ TOKEN_IN,
+ TOKEN_BREAK,
+ TOKEN_ENDFOR,
+ TOKEN_TRUE,
+ TOKEN_FALSE,
+ TOKEN_IF,
+ TOKEN_ELIF,
+ TOKEN_ELSE,
+ TOKEN_ENDIF,
+ TOKEN_EXTENDS,
+ TOKEN_BLOCK,
+ TOKEN_ENDBLOCK,
+ /* The document content */
+ TOKEN_CONTENT,
+};
+
+/* A token in our template */
+struct token {
+ enum token_type type;
+ struct slice literal;
+ size_t line;
+ size_t column;
+};
+
+/* Intialize our keywords hashmap */
+void token_init_keywords(void);
+
+/* Get the token type for a keyword, if it is a registered keyword. */
+enum token_type token_lookup_ident(const struct slice *ident);
+
+/* Return a C string with the token type name */
+inline const char *token_type_print(enum token_type);
+
+/* Concatenate this token to a sds string */
+sds token_string(struct token *, sds str);
+
+/* Free memory allocated by the keywords hashmap */
+void token_free_keywords(void);
+
+#endif
diff --git a/include/vector.h b/include/vector.h
new file mode 100644
index 0000000..fc0eb4c
--- /dev/null
+++ b/include/vector.h
@@ -0,0 +1,29 @@
+#ifndef VECTOR_H
+#define VECTOR_H
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#define VEC_CAP 32
+
+struct vector {
+ size_t cap;
+ size_t len;
+ void **values;
+};
+
+struct vector *vector_new_with_cap(size_t cap);
+
+#define vector_new() vector_new_with_cap(VEC_CAP)
+
+ssize_t vector_push(struct vector *, void *val);
+
+void *vector_pop(struct vector *);
+
+void vector_free(struct vector *);
+
+#define vector_foreach(vec, i, val) \
+ for (i = 0, val = vec->values[i]; i < vec->len; i++, val = vec->values[i])
+
+#endif
diff --git a/sds b/sds
new file mode 160000
+Subproject fb463145c9c245636feb28b5aac0fc897e16f67
diff --git a/src/ast.c b/src/ast.c
new file mode 100644
index 0000000..2de718a
--- /dev/null
+++ b/src/ast.c
@@ -0,0 +1,322 @@
+#include "ast.h"
+#include "slice.h"
+#include "vector.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static inline sds
+subblocks_string(struct vector *subblocks, sds str)
+{
+ size_t i;
+ struct block *subblk;
+ vector_foreach(subblocks, i, subblk) {
+ str = block_string(subblk, str);
+ str = sdscat(str, "\n");
+ }
+
+ return str;
+}
+
+static inline sds
+ident_string(struct ident *ident, sds str)
+{
+ return slice_string(&ident->token.literal, str);
+}
+
+static inline sds
+integer_string(struct integer *i, sds str)
+{
+ return sdscatfmt(str, "%I", i->value);
+}
+
+static inline sds
+boolean_string(struct boolean *b, sds str)
+{
+ char *sval = b->value ? "true" : "false";
+ return sdscatfmt(str, "%s", sval);
+}
+
+static inline sds
+string_string(struct string *s, sds str)
+{
+ return slice_string(&s->value, str);
+}
+
+static inline sds
+prefix_string(struct prefix *pref, sds str)
+{
+ str = sdscat(str, "(");
+ str = slice_string(&pref->operator, str);
+ str = expression_string(pref->right, str);
+ str = sdscat(str, ")");
+ return str;
+}
+
+static inline sds
+infix_string(struct infix *inf, sds str)
+{
+ str = sdscat(str, "(");
+ str = expression_string(inf->left, str);
+ str = sdscat(str, " ");
+ str = slice_string(&inf->operator, str);
+ str = sdscat(str, " ");
+ str = expression_string(inf->right, str);
+ str = sdscat(str, ")");
+ return str;
+}
+
+static inline sds
+mapkey_string(struct indexkey *map, sds str)
+{
+ str = expression_string(map->left, str);
+ str = sdscat(str, ".");
+ str = ident_string(&map->key->ident, str);
+
+ return str;
+}
+
+static inline sds
+index_string(struct indexkey *index, sds str)
+{
+ str = expression_string(index->left, str);
+ str = sdscat(str, "[");
+ str = expression_string(index->key, str);
+ str = sdscat(str, "]");
+
+ return str;
+}
+
+sds
+expression_string(struct expression *expr, sds str)
+{
+ switch (expr->type) {
+ case EXPRESSION_IDENT:
+ return ident_string(&expr->ident, str);
+ case EXPRESSION_INT:
+ return integer_string(&expr->integer, str);
+ case EXPRESSION_BOOL:
+ return boolean_string(&expr->boolean, str);
+ case EXPRESSION_STRING:
+ return string_string(&expr->string, str);
+ case EXPRESSION_PREFIX:
+ return prefix_string(&expr->prefix, str);
+ case EXPRESSION_INFIX:
+ return infix_string(&expr->infix, str);
+ case EXPRESSION_MAPKEY:
+ return mapkey_string(&expr->indexkey, str);
+ case EXPRESSION_INDEX:
+ return index_string(&expr->indexkey, str);
+ }
+ return str;
+}
+
+static inline sds
+branch_string(struct branch *brnch, sds str)
+{
+ str = sdscat(str, "{% ");
+ str = slice_string(&brnch->token.literal, str);
+ str = sdscat(str, " ");
+ str = expression_string(brnch->condition, str);
+ str = sdscat(str, " ");
+ str = sdscat(str, " %}\n");
+ str = subblocks_string(brnch->subblocks, str);
+ if (brnch->next) {
+ str = branch_string(brnch->next, str);
+ }
+ return str;
+}
+
+static inline sds
+cond_string(struct cond *cond, sds str)
+{
+ str = branch_string(cond->root, str);
+ str = sdscat(str, "{% endif %}");
+ return str;
+}
+
+static sds
+loop_string(struct loop *loop, sds str)
+{
+ str = sdscat(str, "{% for ");
+ str = ident_string(&loop->item, str);
+ str = sdscat(str, " in ");
+ str = expression_string(loop->seq, str);
+ str = sdscat(str, " %}\n");
+ str = subblocks_string(loop->subblocks, str);
+ str = sdscat(str, "\n{% endfor %}");
+ return str;
+}
+
+static inline sds
+tblock_string(struct tblock *blk, sds str)
+{
+ str = sdscat(str, "{% block ");
+ str = ident_string(&blk->name, str);
+ str = subblocks_string(blk->subblocks, str);
+ str = sdscat(str, " %}\n");
+ str = sdscat(str, "\n{% endblock %}");
+ return str;
+}
+
+static inline sds
+parent_string(struct parent *prnt, sds str)
+{
+ str = sdscat(str, "{% extends ");
+ str = string_string(prnt->name, str);
+ str = sdscat(str, " %}");
+ return str;
+}
+
+sds
+tag_string(struct tag *tag, sds str)
+{
+ switch (tag->type) {
+ case TAG_IF:
+ return cond_string(&tag->cond, str);
+ case TAG_FOR:
+ return loop_string(&tag->loop, str);
+ case TAG_BLOCK:
+ return tblock_string(&tag->tblock, str);
+ case TAG_EXTENDS:
+ return parent_string(&tag->parent, str);
+ case TAG_BREAK:
+ str = sdscat(str, "{% ");
+ str = slice_string(&tag->token.literal, str);
+ return sdscat(str, " %}");
+ default:
+ break;
+ }
+ return str;
+}
+
+sds
+variable_string(struct variable *var, sds str)
+{
+ str = sdscat(str, "{{ ");
+ str = expression_string(var->expression, str);
+ str = sdscat(str, " }}");
+ return str;
+}
+
+sds
+content_string(struct content *cnt, sds str)
+{
+ return slice_string(&cnt->token.literal, str);
+}
+
+sds
+block_string(struct block *blk, sds str)
+{
+ switch (blk->type) {
+ case BLOCK_CONTENT:
+ return content_string(&blk->content, str);
+ case BLOCK_VARIABLE:
+ return variable_string(&blk->variable, str);
+ case BLOCK_TAG:
+ return tag_string(&blk->tag, str);
+ default:
+ break;
+ }
+ return str;
+}
+
+sds
+template_string(struct template *tmpl, sds str)
+{
+ return subblocks_string(tmpl->blocks, str);
+}
+
+void
+expression_destroy(struct expression *expr)
+{
+ switch (expr->type) {
+ case EXPRESSION_PREFIX:
+ expression_destroy(expr->prefix.right);
+ break;
+ case EXPRESSION_INFIX:
+ expression_destroy(expr->infix.left);
+ expression_destroy(expr->infix.right);
+ break;
+ case EXPRESSION_INDEX:
+ case EXPRESSION_MAPKEY:
+ expression_destroy(expr->indexkey.left);
+ expression_destroy(expr->indexkey.key);
+ case EXPRESSION_IDENT:
+ case EXPRESSION_INT:
+ case EXPRESSION_BOOL:
+ case EXPRESSION_STRING:
+ default:
+ break;
+ }
+ free(expr);
+}
+
+static inline void
+subblocks_destroy(struct vector *subblks)
+{
+ size_t i;
+ struct block *blk;
+ vector_foreach(subblks, i, blk){
+ block_destroy(blk);
+ }
+ vector_free(subblks);
+}
+
+void
+branch_destroy(struct branch *brnch)
+{
+ if (brnch->condition) expression_destroy(brnch->condition);
+ subblocks_destroy(brnch->subblocks);
+ if (brnch->next) branch_destroy(brnch->next);
+ free(brnch);
+}
+
+void
+tag_destroy(struct tag *tag)
+{
+ switch (tag->type) {
+ case TAG_IF:
+ branch_destroy(tag->cond.root);
+ break;
+ case TAG_FOR:
+ expression_destroy(tag->loop.seq);
+ subblocks_destroy(tag->loop.subblocks);
+ break;
+ case TAG_BLOCK:
+ subblocks_destroy(tag->tblock.subblocks);
+ break;
+ case TAG_EXTENDS:
+ free(tag->parent.name);
+ break;
+ case TAG_BREAK:
+ default:
+ break;
+ }
+}
+
+void
+block_destroy(struct block *blk)
+{
+ switch (blk->type) {
+ case BLOCK_VARIABLE:
+ expression_destroy(blk->variable.expression);
+ break;
+ case BLOCK_TAG:
+ tag_destroy(&blk->tag);
+ break;
+ case BLOCK_CONTENT:
+ default:
+ break;
+ }
+ free(blk);
+}
+
+void
+template_destroy(struct template *tmpl)
+{
+ free(tmpl->name);
+ subblocks_destroy(tmpl->blocks);
+ hmap_free(tmpl->tblocks);
+ free(tmpl);
+}
diff --git a/src/hmap.c b/src/hmap.c
new file mode 100644
index 0000000..fb58a88
--- /dev/null
+++ b/src/hmap.c
@@ -0,0 +1,222 @@
+#include "hmap.h"
+#include "slice.h"
+
+#include <inttypes.h>
+#include <string.h>
+#include <stdlib.h>
+#include <err.h>
+
+#if SIZE_MAX == 0xFFFFFFFF
+/* If size_t is 32bit */
+static const size_t fnv_prime = 16777619u;
+static const size_t fnv_offsetb = 2166136261u;
+#elif SIZE_MAX == 0xFFFFFFFFFFFFFFFF
+/* If size_t is 64bit */
+static const size_t fnv_prime = 1099511628211u;
+static const size_t fnv_offsetb = 14695981039346656037u;
+#else
+/* If size_t is 128bit. Maybe this is overdoing it? */
+static const size_t fnv_prime = 309485009821345068724781371u;
+static const size_t fnv_offsetb = 144066263297769815596495629667062367629u;
+#endif
+
+struct hnode {
+ struct slice key;
+ void *value;
+ struct hnode *next;
+};
+
+struct hmap_iter {
+ struct hmap *map;
+ size_t index;
+ size_t count;
+ struct hnode *cur;
+};
+
+/* FNV1a */
+static size_t
+hash_slice(const struct slice *slice)
+{
+ size_t hash = fnv_offsetb;
+ size_t i = slice->start;
+ while (i < slice->end) {
+ hash ^= slice->str[i];
+ hash *= fnv_prime;
+ i++;
+ }
+
+ return hash;
+}
+
+struct hmap *
+hmap_new_with_cap(size_t cap)
+{
+ struct hmap *hm = malloc(sizeof *hm);
+ hm->cap = cap;
+ hm->size = 0;
+ if (hm == NULL) return NULL;
+ hm->buckets = calloc(cap, sizeof hm->buckets);
+ if (hm->buckets == NULL) {
+ free(hm);
+ return NULL;
+ }
+
+ return hm;
+}
+
+void *
+hmap_sets(struct hmap *hm, struct slice key, void *value)
+{
+ int pos = hash_slice(&key) % hm->cap;
+ struct hnode *head = hm->buckets[pos];
+ struct hnode *node = head;
+ void *old_value = NULL;
+
+ while (node) {
+ if (slice_cmp(&node->key, &key) == 0) {
+ old_value = node->value;
+ node->value = value;
+ return old_value;
+ }
+ node = node->next;
+ }
+
+ node = malloc(sizeof *node);
+ node->key = key;
+ node->value = value;
+ node->next = head;
+ hm->buckets[pos] = node;
+ hm->size++;
+ return old_value;
+}
+
+void *
+hmap_gets(struct hmap *hm, const struct slice *key)
+{
+ size_t pos = hash_slice(key) % hm->cap;
+ struct hnode *node = hm->buckets[pos];
+ while (node != NULL) {
+ if (slice_cmp(&node->key, key) == 0) {
+ return node->value;
+ }
+
+ node = node->next;
+ }
+
+ return NULL;
+}
+
+void *
+hmap_get(struct hmap *hm, const char *k)
+{
+ struct slice key = slice_whole(k);
+ return hmap_gets(hm, &key);
+}
+
+void *
+hmap_removes(struct hmap *hm, const struct slice *key)
+{
+ int pos = hash_slice(key) % hm->cap;
+ struct hnode *node = hm->buckets[pos];
+ struct hnode *prev = NULL;
+ void *old_value;
+
+ while (node) {
+ if (slice_cmp(&node->key, key) == 0) {
+ if (prev) {
+ prev->next = node->next;
+ } else {
+ hm->buckets[pos] = node->next;
+ }
+ old_value = node->value;
+ free(node);
+ hm->size--;
+ return old_value;
+ }
+
+ prev = node;
+ node = node->next;
+ }
+
+ return NULL;
+}
+
+void *
+hmap_remove(struct hmap *hm, const char *k)
+{
+ struct slice key = slice_whole(k);
+ return hmap_removes(hm, &key);
+}
+
+#define HMAP_WALK(hm, ...) \
+ struct hnode *node; \
+ struct hnode *next; \
+ for (size_t i = 0; i < hm->cap; i++) { \
+ node = hm->buckets[i]; \
+ while (node) { \
+ next = node->next; \
+ __VA_ARGS__; \
+ node = next; \
+ } \
+ }
+
+void
+hmap_walk(struct hmap *hm, hmap_cb cb)
+{
+ HMAP_WALK(hm, cb(&node->key, node->value));
+}
+
+struct hmap_iter *
+hmap_iter_new(struct hmap *hm)
+{
+ struct hmap_iter *iter = malloc(sizeof(*iter));
+ iter->map = hm;
+ iter->index = 0;
+ iter->count = hm->size;
+ iter->cur = NULL;
+
+ return iter;
+}
+
+bool
+hmap_iter_next(struct hmap_iter *iter, const struct slice **key, void **value)
+{
+ if (iter->count < 1) return false;
+
+ if (!iter->cur || !iter->cur->next) {
+ do {
+ iter->cur = iter->map->buckets[iter->index++];
+ } while (!iter->cur);
+ } else {
+ iter->cur = iter->cur->next;
+ }
+ *key = &iter->cur->key;
+ *value = iter->cur->value;
+ iter->count--;
+
+ return true;
+}
+
+void
+hmap_iter_free(struct hmap_iter *iter)
+{
+ free(iter);
+}
+
+void
+hmap_destroy(struct hmap *hm, hmap_cb cb)
+{
+ HMAP_WALK(hm, cb(&node->key, node->value), free(node));
+
+ free(hm->buckets);
+ free(hm);
+}
+
+void
+hmap_free(struct hmap *hm)
+{
+ HMAP_WALK(hm, free(node));
+
+ free(hm->buckets);
+ free(hm);
+}
diff --git a/src/lexer.c b/src/lexer.c
new file mode 100644
index 0000000..1ba9912
--- /dev/null
+++ b/src/lexer.c
@@ -0,0 +1,264 @@
+#include "lexer.h"
+#include "token.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+
+static bool
+isidentc(char c)
+{
+ return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '_';
+}
+
+static void
+set_token(struct token *token, enum token_type t, const struct slice *s)
+{
+ token->type = t;
+ if (s == NULL) {
+ token->literal.str = "";
+ token->literal.start = 0;
+ token->literal.end = 0;
+ } else {
+ slice_cpy(&token->literal, s);
+ }
+}
+
+static char
+lexer_peek_prev_char(struct lexer *lexer)
+{
+ if (lexer->word.start <= 1) {
+ return 0;
+ }
+ return lexer->input[lexer->word.start - 1];
+}
+
+static char
+lexer_peek_char(struct lexer *lexer)
+{
+ if (lexer->word.start >= lexer->len) {
+ return 0;
+ }
+ return lexer->input[lexer->word.start + 1];
+}
+
+static inline void
+lexer_read_char(struct lexer *lexer)
+{
+ lexer->word.start = lexer->word.end;
+ if (lexer->word.end > lexer->len) {
+ lexer->word.end = 0;
+ return;
+ }
+ char prevc = lexer_peek_prev_char(lexer);
+ if (prevc == '\n') {
+ lexer->line++;
+ lexer->column = 0;
+ }
+ lexer->column++;
+ lexer->word.end++;
+}
+
+static void
+lexer_read_ident(struct lexer *lexer, struct token *token)
+{
+ size_t start = lexer->word.start;
+ token->literal.str = lexer->input;
+ while (isidentc(lexer->input[lexer->word.start])
+ || isdigit(lexer->input[lexer->word.start])) {
+ lexer_read_char(lexer);
+ }
+ token->literal.start = start;
+ token->literal.end = lexer->word.start;
+}
+
+static void
+lexer_read_num(struct lexer *lexer, struct token *token)
+{
+ size_t start = lexer->word.start;
+ token->literal.str = lexer->input;
+ while (isdigit(lexer->input[lexer->word.start])) {
+ lexer_read_char(lexer);
+ }
+ token->literal.start = start;
+ token->literal.end = lexer->word.start;
+}
+
+static void
+lexer_read_string(struct lexer *lexer, struct token *token)
+{
+ size_t start = lexer->word.start;
+ token->literal.str = lexer->input;
+ lexer_read_char(lexer);
+ while(lexer->input[lexer->word.start] != '"' &&
+ lexer->input[lexer->word.start] != '\0') {
+ lexer_read_char(lexer);
+ }
+ lexer_read_char(lexer);
+ token->literal.start = start;
+ token->literal.end = lexer->word.start;
+}
+
+static void
+lexer_read_content(struct lexer *lexer, struct token *token)
+{
+ size_t start = lexer->word.start;
+ token->literal.str = lexer->input;
+ while(lexer->input[lexer->word.start] != '{' &&
+ lexer->input[lexer->word.start] != '\0') {
+ lexer_read_char(lexer);
+ }
+ token->literal.start = start;
+ token->literal.end = lexer->word.start;
+}
+
+static void
+lexer_eatspace(struct lexer *lexer)
+{
+ while(isspace(lexer->input[lexer->word.start])) {
+ lexer_read_char(lexer);
+ }
+}
+
+struct lexer *
+lexer_new(const char *input)
+{
+ struct lexer *lexer = malloc(sizeof(*lexer));
+ lexer->input = input;
+ lexer->len = strlen(lexer->input);
+ lexer->word.str = lexer->input;
+ lexer->word.start = 0;
+ lexer->word.end = 0;
+ lexer->in_content = true;
+ lexer->line = 1;
+ lexer->column = 0;
+ lexer_read_char(lexer);
+
+ return lexer;
+}
+
+struct token
+lexer_next_token(struct lexer *lexer)
+{
+ struct token token = { .line = lexer->line, .column = lexer->column };
+ char c = lexer->input[lexer->word.start];
+
+ if (c == '\0') {
+ set_token(&token, TOKEN_EOF, NULL);
+ return token;
+ }
+
+ if (lexer->in_content && c != '{') {
+ lexer_read_content(lexer, &token);
+ token.type = TOKEN_CONTENT;
+ return token;
+ }
+
+ lexer_eatspace(lexer);
+ c = lexer->input[lexer->word.start];
+ switch (c) {
+ case '=':
+ if (lexer_peek_char(lexer) == '=') {
+ lexer->word.end++;
+ set_token(&token, TOKEN_EQ, &lexer->word);
+ } else {
+ set_token(&token, TOKEN_ILLEGAL, &lexer->word);
+ }
+ break;
+ case '+':
+ set_token(&token, TOKEN_PLUS, &lexer->word);
+ break;
+ case '-':
+ set_token(&token, TOKEN_MINUS, &lexer->word);
+ break;
+ case '!':
+ if (lexer_peek_char(lexer) == '=') {
+ lexer->word.end++;
+ set_token(&token, TOKEN_NOTEQ, &lexer->word);
+ } else {
+ set_token(&token, TOKEN_BANG, &lexer->word);
+ }
+ break;
+ case '/':
+ set_token(&token, TOKEN_SLASH, &lexer->word);
+ break;
+ case '*':
+ set_token(&token, TOKEN_ASTERISK, &lexer->word);
+ break;
+ case '<':
+ if (lexer_peek_char(lexer) == '=') {
+ lexer->word.end++;
+ set_token(&token, TOKEN_LTE, &lexer->word);
+ } else {
+ set_token(&token, TOKEN_LT, &lexer->word);
+ }
+ break;
+ case '>':
+ if (lexer_peek_char(lexer) == '=') {
+ lexer->word.end++;
+ set_token(&token, TOKEN_GTE, &lexer->word);
+ } else {
+ set_token(&token, TOKEN_GT, &lexer->word);
+ }
+ break;
+ case '(':
+ set_token(&token, TOKEN_LPAREN, &lexer->word);
+ break;
+ case ')':
+ set_token(&token, TOKEN_RPAREN, &lexer->word);
+ break;
+ case '.':
+ set_token(&token, TOKEN_DOT, &lexer->word);
+ break;
+ case ',':
+ set_token(&token, TOKEN_COMMA, &lexer->word);
+ break;
+ case '[':
+ set_token(&token, TOKEN_LBRACKET, &lexer->word);
+ break;
+ case ']':
+ set_token(&token, TOKEN_RBRACKET, &lexer->word);
+ break;
+ case '{':
+ lexer->in_content = false;
+ set_token(&token, TOKEN_LBRACE, &lexer->word);
+ break;
+ case '}':{
+ char prevc = lexer_peek_prev_char(lexer);
+ if (prevc == '}' || prevc == '%') {
+ lexer->in_content = true;
+ }
+ set_token(&token, TOKEN_RBRACE, &lexer->word);
+ break;
+ }
+ case '%':
+ set_token(&token, TOKEN_PERCENT, &lexer->word);
+ break;
+ default:
+ if (c == '"') {
+ lexer_read_string(lexer, &token);
+ token.type = TOKEN_STRING;
+ return token;
+ } else if (isidentc(c)) {
+ lexer_read_ident(lexer, &token);
+ token.type = token_lookup_ident(&token.literal);
+ return token;
+ } else if (isdigit(c)) {
+ lexer_read_num(lexer, &token);
+ token.type = TOKEN_INT;
+ return token;
+ }
+ set_token(&token, TOKEN_ILLEGAL, &lexer->word);
+ }
+
+ lexer_read_char(lexer);
+
+ return token;
+}
+
+void
+lexer_destroy(struct lexer *lexer)
+{
+ free(lexer);
+}
diff --git a/src/object.c b/src/object.c
new file mode 100644
index 0000000..e7311b5
--- /dev/null
+++ b/src/object.c
@@ -0,0 +1,242 @@
+#include "object.h"
+
+static const char *roscha_types[] = {
+ "null",
+ "int",
+ "bool",
+ "string",
+ "vector",
+ "hashmap",
+};
+
+static void
+roscha_object_destroy_hmap_cb(const struct slice *key, void *val)
+{
+ struct roscha_object *obj = val;
+ roscha_object_unref(obj);
+}
+
+extern inline const char *
+roscha_type_print(enum roscha_type type)
+{
+ return roscha_types[type];
+}
+
+static inline sds
+bool_string(bool val, sds str)
+{
+ sdscat(str, val ? "true" : "false");
+ return str;
+}
+
+static inline sds
+vector_string(struct vector *vec, sds str)
+{
+ size_t i;
+ struct roscha_object *obj;
+ str = sdscat(str, "[ ");
+ vector_foreach(vec, i, obj) {
+ str = roscha_object_string(obj, str);
+ str = sdscat(str, ", ");
+ }
+ str = sdscat(str, "]");
+ return str;
+}
+
+static inline sds
+hmap_string(struct hmap *map, sds str)
+{
+ str = sdscat(str, "{ ");
+ const struct slice *key;
+ void *val;
+ struct hmap_iter *iter = hmap_iter_new(map);
+ hmap_iter_foreach(iter, &key, &val) {
+ str = sdscatfmt(str, "\"%s\": ", key->str);
+ const struct roscha_object *obj = val;
+ str = roscha_object_string(obj, str);
+ str = sdscat(str, ", ");
+ }
+ str = sdscat(str, "}");
+
+ return str;
+}
+
+sds
+roscha_object_string(const struct roscha_object *obj, sds str)
+{
+ switch (obj->type) {
+ case ROSCHA_NULL:
+ return sdscat(str, "null");
+ case ROSCHA_BOOL:
+ return bool_string(obj->boolean, str);
+ case ROSCHA_INT:
+ return sdscatfmt(str, "%I", obj->integer);
+ case ROSCHA_STRING:
+ return sdscat(str, obj->string);
+ case ROSCHA_SLICE:
+ return slice_string(&obj->slice, str);
+ case ROSCHA_VECTOR:
+ return vector_string(obj->vector, str);
+ case ROSCHA_HMAP:
+ return hmap_string(obj->hmap, str);
+ }
+ return str;
+}
+
+struct roscha_object *
+roscha_object_new_int(int64_t val)
+{
+ struct roscha_object *obj = malloc(sizeof(*obj));
+ obj->type = ROSCHA_INT;
+ obj->refcount = 1;
+ obj->integer = val;
+ return obj;
+}
+
+struct roscha_object *
+roscha_object_new_slice(struct slice s)
+{
+ struct roscha_object *obj = malloc(sizeof(*obj));
+ obj->type = ROSCHA_SLICE;
+ obj->refcount = 1;
+ obj->slice = s;
+ return obj;
+}
+
+struct roscha_object *
+roscha_object_new_string(sds str)
+{
+ struct roscha_object *obj = malloc(sizeof(*obj));
+ obj->type = ROSCHA_STRING;
+ obj->refcount = 1;
+ obj->string = str;
+ return obj;
+}
+
+struct roscha_object *
+roscha_object_new_vector(struct vector *vec)
+{
+ struct roscha_object *obj = malloc(sizeof(*obj));
+ obj->type = ROSCHA_VECTOR;
+ obj->refcount = 1;
+ obj->vector = vec;
+ return obj;
+}
+
+struct roscha_object *
+roscha_object_new_hmap(struct hmap *map)
+{
+ struct roscha_object *obj = malloc(sizeof(*obj));
+ obj->type = ROSCHA_HMAP;
+ obj->refcount = 1;
+ obj->hmap = map;
+ return obj;
+}
+
+static inline void
+roscha_object_vector_destroy(struct roscha_object *obj)
+{
+ size_t i;
+ struct roscha_object *subobj;
+ vector_foreach(obj->vector, i, subobj) {
+ roscha_object_unref(subobj);
+ }
+ vector_free(obj->vector);
+}
+
+inline void
+roscha_object_ref(struct roscha_object *obj)
+{
+ obj->refcount++;
+}
+
+void
+roscha_object_unref(struct roscha_object *obj)
+{
+ if (obj == NULL) return;
+ if (obj->type == ROSCHA_NULL || obj->type == ROSCHA_BOOL) return;
+ if (--obj->refcount < 1) {
+ switch (obj->type) {
+ case ROSCHA_STRING:
+ sdsfree(obj->string);
+ break;
+ case ROSCHA_VECTOR:
+ roscha_object_vector_destroy(obj);
+ break;
+ case ROSCHA_HMAP:
+ hmap_destroy(obj->hmap, roscha_object_destroy_hmap_cb);
+ break;
+ default:
+ break;
+ }
+ free(obj);
+ }
+}
+
+void
+roscha_vector_push(struct roscha_object *vec, struct roscha_object *val)
+{
+ roscha_object_ref(val);
+ vector_push(vec->vector, val);
+}
+
+struct roscha_object *
+roscha_vector_pop(struct roscha_object *vec)
+{
+ return (struct roscha_object *)vector_pop(vec->vector);
+}
+
+struct roscha_object *
+roscha_hmap_sets(struct roscha_object *hmap, struct slice key,
+ struct roscha_object *value)
+{
+ roscha_object_ref(value);
+ return hmap_sets(hmap->hmap, key, value);
+}
+
+struct roscha_object *
+roscha_hmap_setstr(struct roscha_object *hmap, const char *key,
+ struct roscha_object *value)
+{
+ roscha_object_ref(value);
+ return hmap_set(hmap->hmap, key, value);
+}
+
+struct roscha_object *
+roscha_hmap_gets(struct roscha_object *hmap, const struct slice *key)
+{
+ return (struct roscha_object *)hmap_gets(hmap->hmap, key);
+}
+
+struct roscha_object *
+roscha_hmap_getstr(struct roscha_object *hmap, const char *key)
+{
+ return (struct roscha_object *)hmap_get(hmap->hmap, key);
+}
+
+struct roscha_object *
+roscha_hmap_pops(struct roscha_object *hmap, const struct slice *key)
+{
+ return (struct roscha_object *)hmap_removes(hmap->hmap, key);
+}
+
+struct roscha_object *
+roscha_hmap_popstr(struct roscha_object *hmap,
+ const char *key)
+{
+ return (struct roscha_object *)hmap_remove(hmap->hmap, key);
+}
+
+void
+roscha_hmap_unsets(struct roscha_object *hmap, const struct slice *key)
+{
+ struct roscha_object *obj = hmap_removes(hmap->hmap, key);
+ if (obj) roscha_object_unref(obj);
+}
+
+void
+roscha_hmap_unsetstr(struct roscha_object *hmap, const char *key)
+{
+ struct roscha_object *obj = hmap_remove(hmap->hmap, key);
+ if (obj) roscha_object_unref(obj);
+}
diff --git a/src/parser.c b/src/parser.c
new file mode 100644
index 0000000..4ccba1b
--- /dev/null
+++ b/src/parser.c
@@ -0,0 +1,700 @@
+#include "parser.h"
+#include "ast.h"
+#include "token.h"
+#include "vector.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+enum precedence {
+ PRE_LOWEST = 1,
+ PRE_EQUALS,
+ PRE_LG,
+ PRE_SUM,
+ PRE_PROD,
+ PRE_PREFIX,
+ PRE_CALL,
+ PRE_INDEX,
+};
+
+static enum precedence precedence_values[] = {
+ 0,
+ PRE_LOWEST,
+ PRE_EQUALS,
+ PRE_LG,
+ PRE_SUM,
+ PRE_PROD,
+ PRE_PREFIX,
+ PRE_CALL,
+ PRE_INDEX,
+};
+
+static struct hmap *prefix_fns = NULL;
+static struct hmap *infix_fns = NULL;
+static struct hmap *precedences = NULL;
+
+static struct block *parser_parse_block(struct parser *, struct block *opening);
+
+static inline void
+parser_register_prefix(enum token_type t, prefix_parse_f fn)
+{
+ hmap_set(prefix_fns, token_type_print(t), fn);
+}
+
+static inline void
+parser_register_infix(enum token_type t, infix_parse_f fn)
+{
+ hmap_set(infix_fns, token_type_print(t), fn);
+}
+
+static inline void
+parser_register_precedence(enum token_type t, enum precedence pre)
+{
+ hmap_set(precedences, token_type_print(t), &precedence_values[pre]);
+}
+
+static inline prefix_parse_f
+parser_get_prefix(struct parser *parser, enum token_type t)
+{
+ return hmap_get(prefix_fns, token_type_print(t));
+}
+
+static inline infix_parse_f
+parser_get_infix(struct parser *parser, enum token_type t)
+{
+ return hmap_get(infix_fns, token_type_print(t));
+}
+
+static inline enum precedence
+parser_get_precedence(struct parser *parser, enum token_type t)
+{
+ enum precedence *pre = hmap_get(precedences, token_type_print(t));
+ if (!pre) return PRE_LOWEST;
+ return *pre;
+}
+
+static inline void
+parser_next_token(struct parser *parser)
+{
+ parser->cur_token = parser->peek_token;
+ parser->peek_token = lexer_next_token(parser->lexer);
+}
+
+static inline bool
+parser_cur_token_is(struct parser *parser, enum token_type t)
+{
+ return parser->cur_token.type == t;
+}
+
+static inline bool
+parser_peek_token_is(struct parser *parser, enum token_type t)
+{
+ return parser->peek_token.type == t;
+}
+
+static inline enum precedence
+parser_peek_precedence(struct parser *parser)
+{
+ return parser_get_precedence(parser, parser->peek_token.type);
+}
+
+static inline enum precedence
+parser_cur_precedence(struct parser *parser)
+{
+ return parser_get_precedence(parser, parser->cur_token.type);
+}
+
+#define parser_error(p, t, fmt, ...) \
+ sds err = sdscatfmt(sdsempty(), "%s:%U:%U: "fmt, parser->name, \
+ t.line, t.column, __VA_ARGS__); \
+ vector_push(p->errors, err)
+
+static inline void
+parser_peek_error(struct parser *parser, enum token_type t)
+{
+ parser_error(parser, parser->peek_token, "expected token %s, got %s",
+ token_type_print(t), token_type_print(parser->peek_token.type));
+}
+
+static inline bool
+parser_expect_peek(struct parser *parser, enum token_type t)
+{
+ if (parser_peek_token_is(parser, t)) {
+ parser_next_token(parser);
+ return true;
+ }
+ parser_peek_error(parser, t);
+ return false;
+}
+
+static inline void
+parser_no_prefix_fn_error(struct parser *parser, enum token_type t)
+{
+ parser_error(parser, parser->cur_token, "%s not recognized as prefix",
+ token_type_print(t));
+}
+
+static struct expression *
+parser_parse_expression(struct parser *parser, enum precedence pre)
+{
+ prefix_parse_f prefix = parser_get_prefix(parser, parser->cur_token.type);
+ if (!prefix) {
+ parser_no_prefix_fn_error(parser, parser->cur_token.type);
+ return NULL;
+ }
+ struct expression *lexpr = prefix(parser);
+
+ while (pre < parser_peek_precedence(parser)
+ && !parser_peek_token_is(parser, TOKEN_PERCENT)
+ && !parser_peek_token_is(parser, TOKEN_RBRACE)) {
+ infix_parse_f infix = parser_get_infix(parser, parser->peek_token.type);
+ if (!infix) { return lexpr; }
+
+ parser_next_token(parser);
+ lexpr = infix(parser, lexpr);
+ }
+
+ return lexpr;
+}
+
+static struct expression *
+parser_parse_identifier(struct parser *parser)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_IDENT;
+ expr->token = parser->cur_token;
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_integer(struct parser *parser)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_INT;
+ expr->token = parser->cur_token;
+
+ char *end;
+ expr->integer.value =
+ strtol(expr->token.literal.str + expr->token.literal.start, &end, 0);
+ if (*end != '\0'
+ && (expr->token.literal.str + expr->token.literal.end) < end) {
+ sds istr = slice_string(&expr->token.literal, sdsempty());
+ parser_error(parser, parser->cur_token, "%s is not a valid integer",
+ istr);
+ sdsfree(istr);
+ free(expr);
+ return NULL;
+ }
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_boolean(struct parser *parser)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_BOOL;
+ expr->token = parser->cur_token;
+ expr->boolean.value = expr->token.type == TOKEN_TRUE;
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_string(struct parser *parser)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_STRING;
+ expr->token = parser->cur_token;
+ expr->string.value = parser->cur_token.literal;
+ expr->string.value.start++;
+ expr->string.value.end--;
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_grouped(struct parser *parser)
+{
+ parser_next_token(parser);
+ struct expression *expr = parser_parse_expression(parser, PRE_LOWEST);
+ if (!parser_expect_peek(parser, TOKEN_RPAREN)) { return NULL; }
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_prefix(struct parser *parser)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_PREFIX;
+ expr->token = parser->cur_token;
+ expr->prefix.operator= parser->cur_token.literal;
+
+ parser_next_token(parser);
+ expr->prefix.right = parser_parse_expression(parser, PRE_PREFIX);
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_infix(struct parser *parser, struct expression *lexpr)
+{
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_INFIX;
+ expr->token = parser->cur_token;
+ expr->infix.operator= parser->cur_token.literal;
+ expr->infix.left = lexpr;
+
+ enum precedence pre = parser_cur_precedence(parser);
+ parser_next_token(parser);
+ expr->infix.right = parser_parse_expression(parser, pre);
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_mapkey(struct parser *parser, struct expression *lexpr)
+{
+ if (lexpr->type != EXPRESSION_IDENT
+ && lexpr->type != EXPRESSION_MAPKEY
+ && lexpr->type != EXPRESSION_INDEX) {
+ sds got = expression_string(lexpr, sdsempty());
+ parser_error(parser, parser->cur_token,
+ "expected a map identifier, key or index; got %s", got);
+ sdsfree(got);
+ return NULL;
+ }
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_MAPKEY;
+ expr->token = parser->cur_token;
+ expr->indexkey.left = lexpr;
+
+ parser_next_token(parser);
+ expr->indexkey.key = parser_parse_expression(parser, PRE_INDEX);
+ if (expr->indexkey.key->type != EXPRESSION_IDENT) {
+ sds got = expression_string(expr->indexkey.key, sdsempty());
+ parser_error(parser, parser->cur_token,
+ "expected a map key identifier, got %s", got);
+ sdsfree(got);
+ expression_destroy(expr);
+ return NULL;
+ }
+
+ return expr;
+}
+
+static struct expression *
+parser_parse_index(struct parser *parser, struct expression *lexpr)
+{
+ if (lexpr->type != EXPRESSION_IDENT
+ && lexpr->type != EXPRESSION_MAPKEY
+ && lexpr->type != EXPRESSION_INDEX) {
+ sds got = expression_string(lexpr, sdsempty());
+ parser_error(parser, parser->cur_token,
+ "expected a vector identifier, key or index; got %s", got);
+ sdsfree(got);
+ return NULL;
+ }
+ struct expression *expr = malloc(sizeof(*expr));
+ expr->type = EXPRESSION_INDEX;
+ expr->token = parser->cur_token;
+ expr->indexkey.left = lexpr;
+
+ parser_next_token(parser);
+ expr->indexkey.key = parser_parse_expression(parser, PRE_LOWEST);
+
+ if (!parser_expect_peek(parser, TOKEN_RBRACKET)) {
+ expression_destroy(expr);
+ return NULL;
+ }
+
+ return expr;
+}
+
+static inline bool
+parser_parse_loop(struct parser *parser, struct block *blk)
+{
+ blk->tag.type = TAG_FOR;
+
+ if (!parser_expect_peek(parser, TOKEN_IDENT)) return false;
+ blk->tag.loop.item.token = parser->cur_token;
+ if (!parser_expect_peek(parser, TOKEN_IN)) return false;
+ if (!parser_expect_peek(parser, TOKEN_IDENT)) return false;
+ blk->tag.loop.seq = parser_parse_expression(parser, PRE_LOWEST);
+
+ if (!parser_expect_peek(parser, TOKEN_PERCENT)) return false;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) return false;
+
+ parser_next_token(parser);
+ blk->tag.loop.subblocks = vector_new();
+ while (!parser_cur_token_is(parser, TOKEN_EOF)) {
+ struct block *subblk = parser_parse_block(parser, blk);
+ if (subblk == NULL) {
+ return false;
+ }
+ vector_push(blk->tag.loop.subblocks, subblk);
+ parser_next_token(parser);
+ if (subblk->type == BLOCK_TAG && subblk->tag.type == TAG_CLOSE) {
+ break;
+ }
+ }
+
+ return true;
+}
+
+static inline struct branch *
+parser_parse_branch(struct parser *parser, struct block *opening)
+{
+ struct branch *brnch = calloc(1, sizeof(*brnch));
+ brnch->token = parser->cur_token;
+
+ if (brnch->token.type == TOKEN_IF || brnch->token.type == TOKEN_ELIF) {
+ parser_next_token(parser);
+ brnch->condition = parser_parse_expression(parser, PRE_LOWEST);
+ }
+
+ if (!parser_expect_peek(parser, TOKEN_PERCENT)) return false;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) return false;
+
+ parser_next_token(parser);
+ brnch->subblocks = vector_new();
+ while (!parser_cur_token_is(parser, TOKEN_EOF)) {
+ struct block *subblk = parser_parse_block(parser, opening);
+ if (subblk == NULL) {
+ branch_destroy(brnch);
+ return NULL;
+ }
+ if (subblk->type == BLOCK_TAG && subblk->tag.type == TAG_IF) {
+ brnch->next = subblk->tag.cond.root;
+ free(subblk);
+ break;
+ }
+ vector_push(brnch->subblocks, subblk);
+ parser_next_token(parser);
+ if (subblk->type == BLOCK_TAG && subblk->tag.type == TAG_CLOSE) {
+ break;
+ }
+ }
+
+ return brnch;
+}
+
+static inline bool
+parser_parse_cond(struct parser *parser, struct block *blk)
+{
+ blk->tag.type = TAG_IF;
+ blk->tag.cond.root = parser_parse_branch(parser, blk);
+ if (!blk->tag.cond.root) {
+ return false;
+ }
+
+ return true;
+}
+
+static inline bool
+parser_parse_cond_alt(struct parser *parser, struct block *blk,
+ struct block *opening)
+{
+ if (opening == NULL || opening->type != BLOCK_TAG
+ || opening->tag.type != TAG_IF) {
+ parser_error(parser, parser->cur_token, "unexpected token %s",
+ token_type_print(parser->cur_token.type));
+ return false;
+ }
+ blk->tag.type = TAG_IF;
+ blk->tag.cond.root = parser_parse_branch(parser, blk);
+ if (!blk->tag.cond.root) {
+ return false;
+ }
+
+ return true;
+}
+
+static inline bool
+parser_parse_parent(struct parser *parser, struct block *blk)
+{
+ blk->tag.type = TAG_EXTENDS;
+ if (!parser_expect_peek(parser, TOKEN_STRING)) return false;
+
+ blk->tag.parent.name = malloc(sizeof(*blk->tag.parent.name));
+ blk->tag.parent.name->token = parser->cur_token;
+ blk->tag.parent.name->value = parser->cur_token.literal;
+ blk->tag.parent.name->value.start++;
+ blk->tag.parent.name->value.end--;
+
+ if (!parser_expect_peek(parser, TOKEN_PERCENT)) return false;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) return false;
+
+ return true;
+}
+
+static inline bool
+parser_parse_tblock(struct parser *parser, struct block *blk)
+{
+ blk->tag.type = TAG_BLOCK;
+
+ if (!parser_expect_peek(parser, TOKEN_IDENT)) return false;
+ blk->tag.tblock.name.token = parser->cur_token;
+
+ if (!parser_expect_peek(parser, TOKEN_PERCENT)) return false;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) return false;
+
+ parser_next_token(parser);
+ blk->tag.tblock.subblocks = vector_new();
+ while (!parser_cur_token_is(parser, TOKEN_EOF)) {
+ struct block *subblk = parser_parse_block(parser, blk);
+ if (subblk == NULL) {
+ return false;
+ }
+ vector_push(blk->tag.tblock.subblocks, subblk);
+ if (subblk->type == BLOCK_TAG && subblk->tag.type == TAG_CLOSE) {
+ break;
+ }
+ parser_next_token(parser);
+ }
+
+ hmap_sets(parser->tblocks, blk->tag.tblock.name.token.literal, blk);
+ return true;
+}
+
+static inline struct block *
+parser_parse_tag(struct parser *parser, struct block *opening)
+{
+ struct block *blk = malloc(sizeof(*blk));
+ blk->type = BLOCK_TAG;
+
+ parser_next_token(parser);
+ parser_next_token(parser);
+
+ blk->token = parser->cur_token;
+
+ bool res = true;
+ switch (parser->cur_token.type) {
+ case TOKEN_FOR:
+ res = parser_parse_loop(parser, blk);
+ break;
+ case TOKEN_BREAK:
+ blk->tag.type = TAG_BREAK;
+ goto onetoken;
+ case TOKEN_IF:
+ res = parser_parse_cond(parser, blk);
+ break;
+ case TOKEN_ELIF:
+ case TOKEN_ELSE:
+ res = parser_parse_cond_alt(parser, blk, opening);
+ break;
+ case TOKEN_EXTENDS:
+ res = parser_parse_parent(parser, blk);
+ break;
+ case TOKEN_BLOCK:
+ res = parser_parse_tblock(parser, blk);
+ break;
+ case TOKEN_ENDFOR:
+ if (opening == NULL) goto noopening;
+ if (opening->tag.type != TAG_FOR) goto noopening;
+ goto closing;
+ case TOKEN_ENDIF:
+ if (opening == NULL) goto noopening;
+ if (opening->tag.type != TAG_IF) goto noopening;
+ goto closing;
+ case TOKEN_ENDBLOCK:
+ if (opening == NULL) goto noopening;
+ if (opening->tag.type != TAG_BLOCK) goto noopening;
+ goto closing;
+ default:;
+ parser_error(parser, parser->cur_token, "expected keyword, got %s",
+ token_type_print(parser->cur_token.type));
+ return NULL;
+ }
+
+ if (!res) {
+ free(blk);
+ return NULL;
+ }
+
+ return blk;
+closing:
+ blk->tag.type = TAG_CLOSE;
+onetoken:
+ if (!parser_expect_peek(parser, TOKEN_PERCENT)) goto fail;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) goto fail;
+ return blk;
+noopening:;
+ parser_error(parser, parser->cur_token, "unexpected closing tag %s",
+ token_type_print(parser->cur_token.type));
+fail:
+ free(blk);
+ return NULL;
+}
+
+static inline struct block *
+parser_parse_variable(struct parser *parser)
+{
+ struct block *blk = malloc(sizeof(*blk));
+ blk->type = BLOCK_VARIABLE;
+ blk->token = parser->peek_token;
+
+ parser_next_token(parser);
+ parser_next_token(parser);
+
+ blk->variable.expression = parser_parse_expression(parser, PRE_LOWEST);
+ if (!blk->variable.expression) goto fail;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) goto fail;
+ if (!parser_expect_peek(parser, TOKEN_RBRACE)) goto fail;
+
+ return blk;
+fail:
+ block_destroy(blk);
+ return NULL;
+}
+
+static inline struct block *
+parser_parse_content(struct parser *parser)
+{
+ struct block *blk = malloc(sizeof(*blk));
+ blk->type = BLOCK_CONTENT;
+ blk->token = parser->cur_token;
+
+ return blk;
+}
+
+static struct block *
+parser_parse_block(struct parser *parser, struct block *opening)
+{
+ switch (parser->cur_token.type) {
+ case TOKEN_LBRACE:
+ switch (parser->peek_token.type) {
+ case TOKEN_LBRACE:
+ return parser_parse_variable(parser);
+ case TOKEN_PERCENT:
+ return parser_parse_tag(parser, opening);
+ default:{
+ parser_error(parser, parser->cur_token,
+ "expected token %s or %s, got %s",
+ token_type_print(TOKEN_LBRACE),
+ token_type_print(TOKEN_PERCENT),
+ token_type_print(parser->peek_token.type));
+ return NULL;
+ }
+ }
+ case TOKEN_CONTENT:
+ default:
+ return parser_parse_content(parser);
+ }
+}
+
+struct parser *
+parser_new(char *name, char *input)
+{
+ struct parser *parser = calloc(1, sizeof(*parser));
+ parser->name = name;
+
+ struct lexer *lex = lexer_new(input);
+ parser->lexer = lex;
+
+ parser->errors = vector_new();
+
+ parser_next_token(parser);
+ parser_next_token(parser);
+
+ return parser;
+}
+
+struct template *
+parser_parse_template(struct parser *parser)
+{
+ struct template *tmpl = malloc(sizeof(*tmpl));
+ tmpl->name = parser->name;
+ tmpl->source = (char *)parser->lexer->input;
+ parser->tblocks = hmap_new();
+ tmpl->child = NULL;
+ tmpl->blocks = vector_new();
+
+ while (!parser_cur_token_is(parser, TOKEN_EOF)) {
+ struct block *blk = parser_parse_block(parser, NULL);
+ if (blk == NULL) {
+ break;
+ }
+ vector_push(tmpl->blocks, blk);
+
+ parser_next_token(parser);
+ }
+
+ tmpl->tblocks = parser->tblocks;
+ return tmpl;
+}
+
+void
+parser_destroy(struct parser *parser)
+{
+ size_t i;
+ char *val;
+ vector_foreach(parser->errors, i, val) {
+ sdsfree(val);
+ }
+ vector_free(parser->errors);
+ lexer_destroy(parser->lexer);
+ free(parser);
+}
+
+void
+parser_init(void)
+{
+ token_init_keywords();
+
+ prefix_fns = hmap_new();
+ parser_register_prefix(TOKEN_IDENT, parser_parse_identifier);
+ parser_register_prefix(TOKEN_INT, parser_parse_integer);
+ parser_register_prefix(TOKEN_BANG, parser_parse_prefix);
+ parser_register_prefix(TOKEN_MINUS, parser_parse_prefix);
+ parser_register_prefix(TOKEN_NOT, parser_parse_prefix);
+ parser_register_prefix(TOKEN_TRUE, parser_parse_boolean);
+ parser_register_prefix(TOKEN_FALSE, parser_parse_boolean);
+ parser_register_prefix(TOKEN_STRING, parser_parse_string);
+ parser_register_prefix(TOKEN_LPAREN, parser_parse_grouped);
+ parser_register_prefix(TOKEN_RPAREN, parser_parse_grouped);
+
+ infix_fns = hmap_new();
+ parser_register_infix(TOKEN_PLUS, parser_parse_infix);
+ parser_register_infix(TOKEN_MINUS, parser_parse_infix);
+ parser_register_infix(TOKEN_SLASH, parser_parse_infix);
+ parser_register_infix(TOKEN_ASTERISK, parser_parse_infix);
+ parser_register_infix(TOKEN_EQ, parser_parse_infix);
+ parser_register_infix(TOKEN_NOTEQ, parser_parse_infix);
+ parser_register_infix(TOKEN_LT, parser_parse_infix);
+ parser_register_infix(TOKEN_GT, parser_parse_infix);
+ parser_register_infix(TOKEN_LTE, parser_parse_infix);
+ parser_register_infix(TOKEN_GTE, parser_parse_infix);
+ parser_register_infix(TOKEN_AND, parser_parse_infix);
+ parser_register_infix(TOKEN_OR, parser_parse_infix);
+
+ parser_register_infix(TOKEN_DOT, parser_parse_mapkey);
+ parser_register_infix(TOKEN_LBRACKET, parser_parse_index);
+
+ precedences = hmap_new();
+ parser_register_precedence(TOKEN_EQ, PRE_EQUALS);
+ parser_register_precedence(TOKEN_NOTEQ, PRE_EQUALS);
+ parser_register_precedence(TOKEN_LT, PRE_LG);
+ parser_register_precedence(TOKEN_GT, PRE_LG);
+ parser_register_precedence(TOKEN_LTE, PRE_LG);
+ parser_register_precedence(TOKEN_GTE, PRE_LG);
+ parser_register_precedence(TOKEN_AND, PRE_LG);
+ parser_register_precedence(TOKEN_OR, PRE_LG);
+ parser_register_precedence(TOKEN_PLUS, PRE_SUM);
+ parser_register_precedence(TOKEN_MINUS, PRE_SUM);
+ parser_register_precedence(TOKEN_ASTERISK, PRE_PROD);
+ parser_register_precedence(TOKEN_SLASH, PRE_PROD);
+ parser_register_precedence(TOKEN_DOT, PRE_INDEX);
+ parser_register_precedence(TOKEN_LBRACKET, PRE_INDEX);
+}
+
+void
+parser_deinit(void)
+{
+ token_free_keywords();
+ hmap_free(infix_fns);
+ hmap_free(prefix_fns);
+ hmap_free(precedences);
+}
diff --git a/src/roscha.c b/src/roscha.c
new file mode 100644
index 0000000..3f315ce
--- /dev/null
+++ b/src/roscha.c
@@ -0,0 +1,632 @@
+#include "roscha.h"
+
+#include "ast.h"
+#include "hmap.h"
+#include "vector.h"
+#include "parser.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define BUFSIZE 8912
+
+struct roscha_ {
+ /* hmap of template */
+ struct hmap *templates;
+ /* template currently being evaluated */
+ const struct template *eval_tmpl;
+ /* Set when a break tag was encountered */
+ bool brk;
+};
+
+static struct roscha_object obj_null = {
+ ROSCHA_NULL,
+ 0,
+ .boolean = false,
+};
+static struct roscha_object obj_true = {
+ ROSCHA_BOOL,
+ 0,
+ .boolean = true,
+};
+static struct roscha_object obj_false = {
+ ROSCHA_BOOL,
+ 0,
+ .boolean = false,
+};
+
+static inline struct roscha_object *
+get_bool_object(bool val)
+{
+ struct roscha_object *obj = val ? &obj_true : &obj_false;
+ return obj;
+}
+
+static void
+roscha_env_destroy_templates_cb(const struct slice *key, void *val)
+{
+ struct template *tmpl = val;
+ template_destroy(tmpl);
+}
+
+#define eval_error(e, t, fmt, ...) \
+ sds err = sdscatfmt(sdsempty(), "%s:%U:%U: "fmt, \
+ e->internal->eval_tmpl->name, t.line, t.column, __VA_ARGS__); \
+ vector_push(e->errors, err)
+
+#define THERES_ERRORS env->errors->len > 0
+
+static inline struct roscha_object *eval_expression(struct roscha_env *,
+ struct expression *);
+
+static inline struct roscha_object *
+eval_prefix(struct roscha_env *env, struct prefix *pref)
+{
+ struct roscha_object *right = eval_expression(env, pref->right);
+ if (!right) {
+ return NULL;
+ }
+ struct roscha_object *res = NULL;
+ switch (pref->token.type) {
+ case TOKEN_BANG:
+ case TOKEN_NOT:
+ res = get_bool_object(!right->boolean);
+ break;
+ case TOKEN_MINUS:
+ if (right->type != ROSCHA_INT) {
+ eval_error(env, pref->token,
+ "operator '%s' can only be used with integer types",
+ token_type_print(pref->token.type));
+ } else {
+ res = roscha_object_new(-right->integer);
+ }
+ break;
+ default:{
+ eval_error(env, pref->token, "invalid prefix operator '%s'",
+ token_type_print(pref->token.type));
+ res = NULL;
+ }
+ }
+ roscha_object_unref(right);
+
+ return res;
+}
+
+static inline struct roscha_object *
+eval_boolean_infix(struct roscha_env *env, struct token *op,
+ struct roscha_object *left, struct roscha_object *right)
+{
+ struct roscha_object *res = NULL;
+ switch (op->type) {
+ case TOKEN_LT:
+ res = get_bool_object(left->boolean < right->boolean);
+ break;
+ case TOKEN_GT:
+ res = get_bool_object(left->boolean > right->boolean);
+ break;
+ case TOKEN_LTE:
+ res = get_bool_object(left->boolean <= right->boolean);
+ break;
+ case TOKEN_GTE:
+ res = get_bool_object(left->boolean >= right->boolean);
+ break;
+ case TOKEN_EQ:
+ res = get_bool_object(left->boolean == right->boolean);
+ break;
+ case TOKEN_NOTEQ:
+ res = get_bool_object(left->boolean != right->boolean);
+ break;
+ case TOKEN_AND:
+ res = get_bool_object(left->boolean && right->boolean);
+ break;
+ case TOKEN_OR:
+ res = get_bool_object(left->boolean || right->boolean);
+ break;
+ default:
+ if (left->type != right->type) {
+ eval_error(env, (*op), "types mismatch: %s %s %s",
+ roscha_type_print(left->type), token_type_print(op->type),
+ roscha_type_print(right->type));
+ break;
+ }
+ eval_error(env, (*op), "bad operator: %s %s %s",
+ roscha_type_print(left->type), token_type_print(op->type),
+ roscha_type_print(right->type));
+ break;
+ }
+ roscha_object_unref(left);
+ roscha_object_unref(right);
+
+ return res;
+}
+
+static inline struct roscha_object *
+eval_integer_infix(struct roscha_env *env, struct token *op,
+ struct roscha_object *left, struct roscha_object *right)
+{
+ struct roscha_object *res;
+ switch (op->type) {
+ case TOKEN_PLUS:
+ res = roscha_object_new(left->integer + right->integer);
+ break;
+ case TOKEN_MINUS:
+ res = roscha_object_new(left->integer - right->integer);
+ break;
+ case TOKEN_ASTERISK:
+ res = roscha_object_new(left->integer * right->integer);
+ break;
+ case TOKEN_SLASH:
+ res = roscha_object_new(left->integer / right->integer);
+ break;
+ default:
+ return eval_boolean_infix(env, op, left, right);
+ }
+ roscha_object_unref(left);
+ roscha_object_unref(right);
+
+ return res;
+}
+
+static inline struct roscha_object *
+eval_infix(struct roscha_env *env, struct infix *inf)
+{
+ struct roscha_object *left = eval_expression(env, inf->left);
+ if (!left) {
+ return NULL;
+ }
+ struct roscha_object *right = eval_expression(env, inf->right);
+ if (!right) {
+ roscha_object_unref(left);
+ return NULL;
+ }
+ if (left->type == ROSCHA_INT && right->type == ROSCHA_INT)
+ {
+ return eval_integer_infix(env, &inf->token, left, right);
+ }
+
+ return eval_boolean_infix(env, &inf->token, left, right);
+}
+
+static inline struct roscha_object *
+eval_mapkey(struct roscha_env *env, struct indexkey *mkey)
+{
+ struct roscha_object *res = NULL;
+ struct roscha_object *map = eval_expression(env, mkey->left);
+ if (!map) return NULL;
+ if (map->type != ROSCHA_HMAP) {
+ eval_error(env, mkey->token, "expected %s type got %s",
+ roscha_type_print(ROSCHA_HMAP),
+ roscha_type_print(map->type));
+ goto out;
+ }
+ if (mkey->key->type != EXPRESSION_IDENT) {
+ eval_error(env, mkey->key->token, "bad map key '%s'",
+ token_type_print(mkey->key->token.type));
+ goto out;
+ }
+ res = hmap_gets(map->hmap, &mkey->key->token.literal);
+ if (!res) {
+ res = &obj_null;
+ } else {
+ roscha_object_ref(res);
+ }
+
+out:
+ roscha_object_unref(map);
+ return res;
+}
+
+static inline struct roscha_object *
+eval_index(struct roscha_env *env, struct indexkey *index)
+{
+ struct roscha_object *res = NULL;
+ struct roscha_object *vec = eval_expression(env, index->left);
+ if (!vec) return NULL;
+ if (vec->type != ROSCHA_VECTOR) {
+ eval_error(env, index->token, "expected %s type got %s",
+ roscha_type_print(ROSCHA_VECTOR),
+ roscha_type_print(vec->type));
+ goto out;
+ }
+ struct roscha_object *i = eval_expression(env, index->key);
+ if (i->type != ROSCHA_INT) {
+ eval_error(env, index->key->token, "bad vector key type %s",
+ roscha_type_print(ROSCHA_INT));
+ goto out2;
+ }
+ if (i->integer > (vec->vector->len - 1)) {
+ res = &obj_null;
+ } else {
+ res = vec->vector->values[i->integer];
+ roscha_object_ref(res);
+ }
+
+out2:
+ roscha_object_unref(i);
+out:
+ roscha_object_unref(vec);
+ return res;
+}
+
+static inline struct roscha_object *
+eval_expression(struct roscha_env *env, struct expression *expr)
+{
+ struct roscha_object *obj = NULL;
+ switch (expr->type) {
+ case EXPRESSION_IDENT:
+ obj = roscha_hmap_get(env->vars, &expr->ident.token.literal);
+ if (!obj) {
+ obj = &obj_null;
+ } else {
+ roscha_object_ref(obj);
+ }
+ break;
+ case EXPRESSION_INT:
+ obj = roscha_object_new(expr->integer.value);
+ break;
+ case EXPRESSION_BOOL:
+ obj = get_bool_object(expr->boolean.value);
+ break;
+ case EXPRESSION_STRING:
+ obj = roscha_object_new(expr->string.value);
+ break;
+ case EXPRESSION_PREFIX:
+ obj = eval_prefix(env, &expr->prefix);
+ break;
+ case EXPRESSION_INFIX:
+ obj = eval_infix(env, &expr->infix);
+ break;
+ case EXPRESSION_MAPKEY:
+ obj = eval_mapkey(env, &expr->indexkey);
+ break;
+ case EXPRESSION_INDEX:
+ obj = eval_index(env, &expr->indexkey);
+ break;
+ }
+
+ return obj;
+}
+
+static inline sds
+eval_variable(struct roscha_env *env, sds r, struct variable *var)
+{
+ struct roscha_object *obj = eval_expression(env, var->expression);
+ if (!obj) {
+ return r;
+ }
+ r = roscha_object_string(obj, r);
+ roscha_object_unref(obj);
+
+ return r;
+}
+
+static inline sds eval_subblocks(struct roscha_env *, sds r,
+ struct vector *blks);
+
+static inline sds
+eval_branch(struct roscha_env *env, sds r, struct branch *br)
+{
+ if (br->condition) {
+ struct roscha_object *cond = eval_expression(env, br->condition);
+ if (cond->boolean) {
+ r = eval_subblocks(env, r, br->subblocks);
+ } else if (br->next) {
+ r = eval_branch(env, r, br->next);
+ }
+ roscha_object_unref(cond);
+ } else {
+ r = eval_subblocks(env, r, br->subblocks);
+ }
+ return r;
+}
+
+static inline sds
+eval_loop(struct roscha_env *env, sds r, struct loop *loop)
+{
+ struct roscha_object *loopv = roscha_object_new(hmap_new());
+ struct roscha_object *indexv = roscha_object_new(0);
+ roscha_hmap_set(loopv, "index", indexv);
+ struct slice loopk = slice_whole("loop");
+ struct roscha_object *outerloop = roscha_hmap_set(env->vars, "loop", loopv);
+ struct slice it = loop->item.token.literal;
+ struct roscha_object *outeritem = roscha_hmap_get(env->vars, &it);
+ struct roscha_object *seq = eval_expression(env, loop->seq);
+ if (!seq) return r;
+ if (seq->type == ROSCHA_VECTOR) {
+ struct roscha_object *item;
+ vector_foreach(seq->vector, indexv->integer, item) {
+ roscha_hmap_set(env->vars, it, item);
+ r = eval_subblocks(env, r, loop->subblocks);
+ roscha_hmap_unset(env->vars, &it);
+ if (THERES_ERRORS) break;
+ if (env->internal->brk) {
+ env->internal->brk = false;
+ break;
+ }
+ }
+ } else if(seq->type == ROSCHA_HMAP) {
+ struct hmap_iter *iter = hmap_iter_new(seq->hmap);
+ const struct slice *key;
+ void *val;
+ hmap_iter_foreach(iter, &key, &val) {
+ struct roscha_object *item = val;
+ indexv->integer++;
+ roscha_hmap_set(env->vars, it, item);
+ r = eval_subblocks(env, r, loop->subblocks);
+ roscha_hmap_unset(env->vars, &it);
+ if (THERES_ERRORS) break;
+ if (env->internal->brk) {
+ env->internal->brk = false;
+ break;
+ }
+ }
+ } else {
+ eval_error(env, loop->seq->token,
+ "sequence should be of type %s or %s, got %s",
+ roscha_type_print(ROSCHA_VECTOR),
+ roscha_type_print(ROSCHA_HMAP), roscha_type_print(seq->type));
+ }
+
+ if (outerloop) {
+ roscha_hmap_set(env->vars, loopk, outerloop);
+ roscha_object_unref(outerloop);
+ roscha_object_unref(loopv);
+ } else {
+ roscha_hmap_unset(env->vars, &loopk);
+ }
+ if (outeritem) {
+ roscha_hmap_set(env->vars, it, outeritem);
+ roscha_object_unref(outeritem);
+ }
+ roscha_object_unref(indexv);
+ roscha_object_unref(loopv);
+ roscha_object_unref(seq);
+
+ return r;
+}
+
+static inline struct tblock *
+get_child_tblock(struct roscha_env *env, struct slice *name,
+ const struct template *tmpl)
+{
+ struct tblock *tblk = NULL;
+ if (tmpl->child) {
+ tblk = get_child_tblock(env, name, tmpl->child);
+ }
+ if (!tblk) {
+ return hmap_gets(tmpl->tblocks, name);
+ }
+
+ return tblk;
+}
+
+static inline sds
+eval_tblock(struct roscha_env *env, sds r, struct tblock *tblk)
+{
+ struct tblock *child = get_child_tblock(env, &tblk->name.token.literal,
+ env->internal->eval_tmpl);
+ if (child) {
+ tblk = child;
+ }
+
+ return eval_subblocks(env, r, tblk->subblocks);
+}
+
+static inline sds
+eval_tag(struct roscha_env *env, sds r, struct tag *tag)
+{
+ switch (tag->type) {
+ case TAG_IF:
+ return eval_branch(env, r, tag->cond.root);
+ case TAG_FOR:
+ return eval_loop(env, r, &tag->loop);
+ case TAG_BLOCK:
+ return eval_tblock(env, r, &tag->tblock);
+ case TAG_EXTENDS:{
+ eval_error(env, tag->token, "extends tag can only be the first tag",
+ tag->token);
+ break;
+ }
+ case TAG_BREAK:
+ env->internal->brk = true;
+ break;
+ default:
+ break;
+ }
+
+ return r;
+}
+
+static inline sds
+eval_block(struct roscha_env *env, sds r, struct block *blk)
+{
+ switch (blk->type) {
+ case BLOCK_CONTENT:
+ return slice_string(&blk->token.literal, r);
+ case BLOCK_VARIABLE:
+ return eval_variable(env, r, &blk->variable);
+ case BLOCK_TAG:
+ return eval_tag(env, r, &blk->tag);
+ }
+}
+
+static inline sds
+eval_subblocks(struct roscha_env *env, sds r, struct vector *blks)
+{
+ size_t i;
+ struct block *blk;
+ vector_foreach (blks, i, blk) {
+ r = eval_block(env, r, blk);
+ if (THERES_ERRORS) return r;
+ if (env->internal->brk) return r;
+ }
+
+ return r;
+}
+
+static inline sds
+eval_template(struct roscha_env *env, const struct slice *name,
+ struct template *child)
+{
+ struct template *tmpl = hmap_gets(env->internal->templates, name);
+ if (!tmpl) {
+ sds errmsg = sdscat(sdsempty(), "template \"");
+ errmsg = slice_string(name, errmsg);
+ errmsg = sdscat(errmsg, "\" not found");
+ vector_push(env->errors, errmsg);
+ return NULL;
+ }
+
+ tmpl->child = child;
+ env->internal->eval_tmpl = tmpl;
+
+ struct block *blk = tmpl->blocks->values[0];
+ if (blk->type == BLOCK_TAG && blk->tag.type == TAG_EXTENDS) {
+ struct slice name = blk->tag.parent.name->value;
+ return eval_template(env, &name, tmpl);
+ }
+
+ sds r = sdsempty();
+ r = eval_subblocks(env, r, tmpl->blocks);
+
+ env->internal->eval_tmpl = NULL;
+
+ return r;
+}
+
+void
+roscha_init(void)
+{
+ parser_init();
+}
+
+void
+roscha_deinit(void)
+{
+ parser_deinit();
+}
+
+struct roscha_env *
+roscha_env_new(void)
+{
+ struct roscha_env *env = calloc(1, sizeof(*env));
+ env->internal = calloc(1, sizeof(*env->internal));
+ env->vars = roscha_object_new(hmap_new());
+ env->internal->templates = hmap_new();
+ env->errors = vector_new();
+
+ return env;
+}
+
+bool
+roscha_env_add_template(struct roscha_env *env, char *name, char *body)
+{
+ struct parser *parser = parser_new(name, body);
+ struct template *tmpl = parser_parse_template(parser);
+ if (parser->errors->len > 0) {
+ sds errmsg = NULL;
+ while ((errmsg = vector_pop(parser->errors)) != NULL) {
+ vector_push(env->errors, errmsg);
+ }
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ return false;
+ }
+ parser_destroy(parser);
+ hmap_sets(env->internal->templates, slice_whole(name), tmpl);
+ return true;
+}
+
+bool
+roscha_env_load_dir(struct roscha_env *env, const char *path)
+{
+ DIR *dir = opendir(path);
+ if (!dir) {
+ sds errmsg = sdscatfmt(sdsempty(), "unable to open dir %s, error %s",
+ path, strerror(errno));
+ vector_push(env->errors, errmsg);
+ return false;
+ }
+ struct dirent *ent;
+ while ((ent = readdir(dir))) {
+ if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
+ continue;
+ }
+
+ struct stat fstats;
+ sds fpath = sdscatfmt(sdsempty(), "%s/%s", path, ent->d_name);
+ if (stat(fpath, &fstats)) {
+ sds errmsg = sdscatfmt(sdsempty(),
+ "unable to stat file %s, error %s", fpath, strerror(errno));
+ vector_push(env->errors, errmsg);
+ closedir(dir);
+ return false;
+ }
+ if (S_ISDIR(fstats.st_mode)) continue;
+
+ char *name = malloc(strlen(ent->d_name) + 1);
+ strcpy(name, ent->d_name);
+
+ int fd = open(fpath, O_RDONLY);
+ char buf[BUFSIZE];
+ sds body = sdsempty();
+ size_t nread;
+ while ((nread = read(fd, buf, BUFSIZE)) > 0) {
+ buf[nread] = '\0';
+ body = sdscat(body, buf);
+ }
+ close(fd);
+ if (nread < 0) {
+ sds errmsg = sdscatfmt(sdsempty(),
+ "unable to read file %s, error %s", fpath, strerror(errno));
+ vector_push(env->errors, errmsg);
+ goto fileerr;
+ }
+ if (!roscha_env_add_template(env, name, body)) {
+ goto fileerr;
+ }
+ continue;
+fileerr:
+ closedir(dir);
+ sdsfree(body);
+ return false;
+ }
+
+ closedir(dir);
+ return true;
+}
+
+sds
+roscha_env_render(struct roscha_env *env, const char *name)
+{
+ struct slice sname = slice_whole(name);
+ return eval_template(env, &sname, NULL);
+}
+
+struct vector *
+roscha_env_check_errors(struct roscha_env *env)
+{
+ if (env->errors->len > 0) return env->errors;
+ return NULL;
+}
+
+void
+roscha_env_destroy(struct roscha_env *env)
+{
+ size_t i;
+ sds errmsg;
+ vector_foreach (env->errors, i, errmsg) {
+ sdsfree(errmsg);
+ }
+ vector_free(env->errors);
+ roscha_object_unref(env->vars);
+ hmap_destroy(env->internal->templates, roscha_env_destroy_templates_cb);
+ free(env->internal);
+ free(env);
+}
diff --git a/src/slice.c b/src/slice.c
new file mode 100644
index 0000000..5d89d9e
--- /dev/null
+++ b/src/slice.c
@@ -0,0 +1,63 @@
+#include "slice.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+struct slice
+slice_new(const char *str, size_t start, size_t end)
+{
+ struct slice slice = {
+ .str = str,
+ .start = start,
+ .end = end,
+ };
+
+ return slice;
+}
+
+void
+slice_set(struct slice *slice, const char *str, size_t start, size_t end)
+{
+ slice->str = str;
+ slice->start = start;
+ slice->end = end;
+}
+
+size_t
+slice_len(const struct slice *slice)
+{
+ return slice->end - slice->start;
+}
+
+int
+slice_cmp(const struct slice *restrict a, const struct slice *restrict b)
+{
+ size_t lena = slice_len(a), lenb = slice_len(b);
+ int lencmp = (lena > lenb) - (lena < lenb);
+ if (lencmp) {
+ return lencmp;
+ }
+
+ for (size_t i = 0; i < lena; i++) {
+ char ca = a->str[a->start + i], cb = b->str[b->start + i];
+ int cmp = (ca > cb) - (ca < cb);
+ if (cmp) return cmp;
+ }
+
+ return 0;
+}
+
+void
+slice_cpy(struct slice *dst, const struct slice *src)
+{
+ dst->str = src->str;
+ dst->start = src->start;
+ dst->end = src->end;
+}
+
+sds
+slice_string(const struct slice *slice, sds str)
+{
+ size_t len = slice->end - slice->start;
+ return sdscatlen(str, slice->str + slice->start, len);
+}
diff --git a/src/tests/lexer.c b/src/tests/lexer.c
new file mode 100644
index 0000000..a9cb142
--- /dev/null
+++ b/src/tests/lexer.c
@@ -0,0 +1,246 @@
+#include "tests/tests.h"
+#include "lexer.h"
+
+#include <string.h>
+
+#include "slice.h"
+#include "token.h"
+
+static void
+test_next_token(void)
+{
+ char *input = "{% extends \"template\" %}\n"
+ "{% block rooster %}\n"
+ "{% if true %}\n"
+ "some content\n"
+ "{% elif not false %}\n"
+ "other content\n"
+ "{% else %}\n"
+ "yet something else\n"
+ "{% endif %}\n"
+ "{% for v in list %}\n"
+ "{{ v+(1-2)*4/5 }}\n"
+ "{% break %}\n"
+ "{% endfor %}\n"
+ "{% endblock %}\n"
+ "{{ list[1] }}\n"
+ "{{ map.value }}\n"
+ "{{ 5 < 10 }}\n"
+ "{{ 10 > 5 }}\n"
+ "{{ 5 <= 10 }}\n"
+ "{{ 10 >= 5 }}\n"
+ "{{ 10 != 5 }}\n"
+ "{{ 5 == 5 }}\n"
+ "{{ 5 and 5 }}\n"
+ "{{ 5 or 5 }}\n";
+
+ token_init_keywords();
+ struct lexer *lexer = lexer_new(input);
+ struct token expected[] = {
+ { TOKEN_LBRACE, slice_whole("{"), 1, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 1, 2 },
+ { TOKEN_EXTENDS, slice_whole("extends"), 1, 4 },
+ { TOKEN_STRING, slice_whole("\"template\""), 1, 12 },
+ { TOKEN_PERCENT, slice_whole("%"), 1, 23 },
+ { TOKEN_RBRACE, slice_whole("}"), 1, 24 },
+ { TOKEN_CONTENT, slice_whole("\n"), 1, 25 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 2, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 2, 2 },
+ { TOKEN_BLOCK, slice_whole("block"), 2, 4 },
+ { TOKEN_IDENT, slice_whole("rooster"), 2, 10 },
+ { TOKEN_PERCENT, slice_whole("%"), 2, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 2, 19 },
+ { TOKEN_CONTENT, slice_whole("\n"), 2, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 3, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 3, 2 },
+ { TOKEN_IF, slice_whole("if"), 3, 4 },
+ { TOKEN_TRUE, slice_whole("true"), 3, 7 },
+ { TOKEN_PERCENT, slice_whole("%"), 3, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 3, 13 },
+ { TOKEN_CONTENT, slice_whole("\nsome content\n"), 3, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 5, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 5, 2 },
+ { TOKEN_ELIF, slice_whole("elif"), 5, 4 },
+ { TOKEN_NOT, slice_whole("not"), 5, 9 },
+ { TOKEN_FALSE, slice_whole("false"), 5, 12 },
+ { TOKEN_PERCENT, slice_whole("%"), 5, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 5, 19 },
+ { TOKEN_CONTENT, slice_whole("\nother content\n"), 5, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 7, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 7, 2 },
+ { TOKEN_ELSE, slice_whole("else"), 7, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 7, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 7, 10 },
+ { TOKEN_CONTENT, slice_whole("\nyet something else\n"), 7, 11 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 9, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 9, 2 },
+ { TOKEN_ENDIF, slice_whole("endif"), 9, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 9, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 9, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 9, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 10, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 10, 2 },
+ { TOKEN_FOR, slice_whole("for"), 10, 4 },
+ { TOKEN_IDENT, slice_whole("v"), 10, 8 },
+ { TOKEN_IN, slice_whole("in"), 10, 10 },
+ { TOKEN_IDENT, slice_whole("list"), 10, 13 },
+ { TOKEN_PERCENT, slice_whole("%"), 10, 18 },
+ { TOKEN_RBRACE, slice_whole("}"), 10, 19 },
+ { TOKEN_CONTENT, slice_whole("\n"), 10, 20 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 11, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 11, 2 },
+ { TOKEN_IDENT, slice_whole("v"), 11, 4 },
+ { TOKEN_PLUS, slice_whole("+"), 11, 5 },
+ { TOKEN_LPAREN, slice_whole("("), 11, 6 },
+ { TOKEN_INT, slice_whole("1"), 11, 7 },
+ { TOKEN_MINUS, slice_whole("-"), 11, 8 },
+ { TOKEN_INT, slice_whole("2"), 11, 9 },
+ { TOKEN_RPAREN, slice_whole(")"), 11, 10 },
+ { TOKEN_ASTERISK, slice_whole("*"), 11, 11 },
+ { TOKEN_INT, slice_whole("4"), 11, 12 },
+ { TOKEN_SLASH, slice_whole("/"), 11, 13 },
+ { TOKEN_INT, slice_whole("5"), 11, 14 },
+ { TOKEN_RBRACE, slice_whole("}"), 11, 16 },
+ { TOKEN_RBRACE, slice_whole("}"), 11, 17 },
+ { TOKEN_CONTENT, slice_whole("\n"), 11, 18 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 12, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 12, 2 },
+ { TOKEN_BREAK, slice_whole("break"), 12, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 12, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 12, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 12, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 13, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 13, 2 },
+ { TOKEN_ENDFOR, slice_whole("endfor"), 13, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 13, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 13, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 13, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 14, 1 },
+ { TOKEN_PERCENT, slice_whole("%"), 14, 2 },
+ { TOKEN_ENDBLOCK, slice_whole("endblock"), 14, 4 },
+ { TOKEN_PERCENT, slice_whole("%"), 14, 13 },
+ { TOKEN_RBRACE, slice_whole("}"), 14, 14 },
+ { TOKEN_CONTENT, slice_whole("\n"), 14, 15 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 15, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 15, 2 },
+ { TOKEN_IDENT, slice_whole("list"), 15, 4 },
+ { TOKEN_LBRACKET, slice_whole("["), 15, 8 },
+ { TOKEN_INT, slice_whole("1"), 15, 9 },
+ { TOKEN_RBRACKET, slice_whole("]"), 15, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 15, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 15, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 15, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 16, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 16, 2 },
+ { TOKEN_IDENT, slice_whole("map"), 16, 4 },
+ { TOKEN_DOT, slice_whole("."), 16, 7 },
+ { TOKEN_IDENT, slice_whole("value"), 16, 8 },
+ { TOKEN_RBRACE, slice_whole("}"), 16, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 16, 11 },
+ { TOKEN_CONTENT, slice_whole("\n"), 16, 12 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 17, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 17, 2 },
+ { TOKEN_INT, slice_whole("5"), 17, 4 },
+ { TOKEN_LT, slice_whole("<"), 17, 6 },
+ { TOKEN_INT, slice_whole("10"), 17, 8 },
+ { TOKEN_RBRACE, slice_whole("}"), 17, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 17, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 17, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 18, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 18, 2 },
+ { TOKEN_INT, slice_whole("10"), 18, 4 },
+ { TOKEN_GT, slice_whole(">"), 18, 7 },
+ { TOKEN_INT, slice_whole("5"), 18, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 18, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 18, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 18, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 19, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 19, 2 },
+ { TOKEN_INT, slice_whole("5"), 19, 4 },
+ { TOKEN_LTE, slice_whole("<="), 19, 6 },
+ { TOKEN_INT, slice_whole("10"), 19, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 19, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 19, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 19, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 20, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 20, 2 },
+ { TOKEN_INT, slice_whole("10"), 20, 4 },
+ { TOKEN_GTE, slice_whole(">="), 20, 7 },
+ { TOKEN_INT, slice_whole("5"), 20, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 20, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 20, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 20, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 21, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 21, 2 },
+ { TOKEN_INT, slice_whole("10"), 21, 4 },
+ { TOKEN_NOTEQ, slice_whole("!="), 21, 7 },
+ { TOKEN_INT, slice_whole("5"), 21, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 21, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 21, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 21, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 22, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 22, 2 },
+ { TOKEN_INT, slice_whole("5"), 22, 4 },
+ { TOKEN_EQ, slice_whole("=="), 22, 6 },
+ { TOKEN_INT, slice_whole("5"), 22, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 22, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 22, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 22, 13 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 23, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 23, 2 },
+ { TOKEN_INT, slice_whole("5"), 23, 4 },
+ { TOKEN_AND, slice_whole("and"), 23, 6 },
+ { TOKEN_INT, slice_whole("5"), 23, 10 },
+ { TOKEN_RBRACE, slice_whole("}"), 23, 12 },
+ { TOKEN_RBRACE, slice_whole("}"), 23, 13 },
+ { TOKEN_CONTENT, slice_whole("\n"), 23, 14 },
+
+ { TOKEN_LBRACE, slice_whole("{"), 24, 1 },
+ { TOKEN_LBRACE, slice_whole("{"), 24, 2 },
+ { TOKEN_INT, slice_whole("5"), 24, 4 },
+ { TOKEN_OR, slice_whole("or"), 24, 6 },
+ { TOKEN_INT, slice_whole("5"), 24, 9 },
+ { TOKEN_RBRACE, slice_whole("}"), 24, 11 },
+ { TOKEN_RBRACE, slice_whole("}"), 24, 12 },
+ { TOKEN_CONTENT, slice_whole("\n"), 24, 13 },
+
+ { TOKEN_EOF, },
+ };
+ size_t i = 0;
+
+ do {
+ struct token token = lexer_next_token(lexer);
+ asserteq(token.type, expected[i].type);
+ asserteq(slice_cmp(&token.literal, &expected[i].literal), 0);
+ i++;
+ } while (expected[i].type != TOKEN_EOF);
+
+ lexer_destroy(lexer);
+ token_free_keywords();
+}
+
+int
+main(void)
+{
+ INIT_TESTS();
+ RUN_TEST(test_next_token);
+}
diff --git a/src/tests/parser.c b/src/tests/parser.c
new file mode 100644
index 0000000..eb5c2a1
--- /dev/null
+++ b/src/tests/parser.c
@@ -0,0 +1,542 @@
+#define _POSIX_C_SOURCE 200809L
+#include "parser.h"
+#include "tests/tests.h"
+
+#include "ast.h"
+#include "slice.h"
+
+#include <string.h>
+
+enum value_type {
+ VALUE_IDENT,
+ VALUE_INT,
+ VALUE_BOOL,
+ VALUE_STRING,
+};
+
+struct value {
+ union {
+ struct slice ident;
+ struct slice string;
+ int64_t integer;
+ bool boolean;
+ };
+ enum value_type type;
+};
+
+static void
+check_parser_errors(struct parser *parser, const char *file, int line,
+ const char *func)
+{
+ if (parser->errors->len > 0) {
+ printf("\n");
+ size_t i;
+ sds val;
+ vector_foreach (parser->errors, i, val) {
+ printf("parser error %lu: %s\n", i, val);
+ }
+ printf(TBLD TRED "FAIL!\n" TRST);
+ printf("%s:%d: %s: ", file, line, func);
+ printf("parser encountered errors\n");
+ abort();
+ }
+}
+
+#define check_parser_errors(p) \
+ check_parser_errors(p, __FILE__, __LINE__, __func__)
+
+static void
+test_integer_literal(struct expression *expr, int64_t val)
+{
+ char buf[128];
+ struct slice sval;
+ asserteq(expr->type, EXPRESSION_INT);
+ asserteq(expr->integer.value, val);
+ sprintf(buf, "%ld", val);
+ sval = slice_whole(buf);
+ asserteq(slice_cmp(&expr->token.literal, &sval), 0);
+}
+
+static void
+test_identifier(struct expression *expr, struct slice *ident)
+{
+ asserteq(expr->type, EXPRESSION_IDENT);
+ asserteq(slice_cmp(&expr->token.literal, ident), 0);
+}
+
+static void
+test_boolean_literal(struct expression *expr, bool val)
+{
+ char *str = val ? "true" : "false";
+ struct slice sval = slice_whole(str);
+ asserteq(expr->type, EXPRESSION_BOOL);
+ asserteq(expr->boolean.value, val);
+ asserteq(slice_cmp(&expr->token.literal, &sval), 0);
+}
+
+static void
+test_string_literal(struct expression *expr, const struct slice *val)
+{
+ asserteq(expr->type, EXPRESSION_STRING);
+ asserteq(slice_cmp(&expr->string.value, val), 0);
+}
+
+
+static inline void
+test_expected(struct expression *expr, struct value v)
+{
+ switch(v.type) {
+ case VALUE_IDENT:
+ test_identifier(expr, &v.ident);
+ break;
+ case VALUE_INT:
+ test_integer_literal(expr, v.integer);
+ break;
+ case VALUE_BOOL:
+ test_boolean_literal(expr, v.boolean);
+ break;
+ case VALUE_STRING:
+ test_string_literal(expr, &v.string);
+ break;
+ }
+}
+
+#define VIDENT(v) \
+ (struct value){ .type = VALUE_IDENT, .ident = slice_whole(v) }
+
+#define VINT(v) \
+ (struct value){ .type = VALUE_INT, .integer = v }
+
+#define VBOOL(v) \
+ (struct value){ .type = VALUE_BOOL, .boolean = v }
+
+#define VSTR(v) \
+ (struct value){ .type = VALUE_STRING, .string = slice_whole(v) }
+
+static inline void
+test_infix(struct infix *expr, struct value lval, struct slice *op,
+ struct value rval)
+{
+ test_expected(expr->left, lval);
+ asserteq(slice_cmp(&expr->operator, op), 0);
+ test_expected(expr->right, rval);
+}
+
+static inline void
+test_literal_variables(void)
+{
+ struct {
+ char *input;
+ struct value val;
+ } tests[] = {
+ { "{{ foo }}", VIDENT("foo"), },
+ { "{{ 20 }}", VINT(20), },
+ { "{{ true }}", VBOOL(true), },
+ { "{{ false }}", VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ test_expected(blk->variable.expression, tests[i].val);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_prefix_variables(void)
+{
+ struct {
+ char *input;
+ struct slice operator;
+ struct value val;
+ } tests[] = {
+ { "{{ !foo }}", slice_whole("!"), VIDENT("foo"), },
+ { "{{ -bar }}", slice_whole("-"), VIDENT("bar"), },
+ { "{{ -20 }}", slice_whole("-"), VINT(20), },
+ { "{{ !true }}", slice_whole("!"), VBOOL(true), },
+ { "{{ !false }}", slice_whole("!"), VBOOL(false), },
+ { "{{ not false }}", slice_whole("not"), VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_PREFIX);
+ struct expression *pref = blk->variable.expression;
+ asserteq(slice_cmp(&pref->prefix.operator, &tests[i].operator), 0);
+ test_expected(pref->prefix.right, tests[i].val);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_infix_variables(void)
+{
+ struct {
+ char *input;
+ struct value left;
+ struct slice operator;
+ struct value right;
+ } tests[] = {
+ { "{{ foo + bar }}", VIDENT("foo"), slice_whole("+"), VIDENT("bar"), },
+ { "{{ 6 - 9 }}", VINT(6),slice_whole("-"), VINT(9), },
+ { "{{ 4 * 20 }}", VINT(4), slice_whole("*"), VINT(20), },
+ { "{{ foo / 20 }}", VIDENT("foo"), slice_whole("/"), VINT(20), },
+ { "{{ \"str\" == \"str\" }}", VSTR("str"), slice_whole("=="), VSTR("str"), },
+ { "{{ true != false }}", VBOOL(true), slice_whole("!="), VBOOL(false), },
+ { "{{ 4 < 20 }}", VINT(4), slice_whole("<"), VINT(20), },
+ { "{{ 4 <= 20 }}", VINT(4), slice_whole("<="), VINT(20), },
+ { "{{ 100 > 20 }}", VINT(100), slice_whole(">"), VINT(20), },
+ { "{{ 100 >= 20 }}", VINT(100), slice_whole(">="), VINT(20), },
+ { "{{ true and true }}", VBOOL(true), slice_whole("and"), VBOOL(true), },
+ { "{{ true or false }}", VBOOL(true), slice_whole("or"), VBOOL(false), },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_INFIX);
+ test_infix(&blk->variable.expression->infix,
+ tests[i].left, &tests[i].operator, tests[i].right);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_map_variables(void)
+{
+ char *input = "{{ map.key }}";
+ struct value left = VIDENT("map");
+ struct value key = VIDENT("key");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_MAPKEY);
+ struct expression *map = blk->variable.expression;
+ test_expected(map->indexkey.left, left);
+ test_expected(map->indexkey.key, key);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_index_variables(void)
+{
+ char *input = "{{ arr[1 + 2] }}";
+ struct value left = VIDENT("arr");
+ struct value ileft = VINT(1);
+ struct slice iop = slice_whole("+");
+ struct value iright = VINT(2);
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ asserteq(blk->variable.expression->type, EXPRESSION_INDEX);
+ struct expression *map = blk->variable.expression;
+ test_expected(map->indexkey.left, left);
+ test_infix(&map->indexkey.key->infix, ileft, &iop, iright);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_operator_precedence(void)
+{
+ struct {
+ char *input;
+ char *expected;
+ } tests[] = {
+ {
+ "{{ -a * b }}",
+ "{{ ((-a) * b) }}",
+ },
+ {
+ "{{ 1 + 2 * 3 }}",
+ "{{ (1 + (2 * 3)) }}",
+ },
+ {
+ "{{ !-a }}",
+ "{{ (!(-a)) }}",
+ },
+ {
+ "{{ a + b + c }}",
+ "{{ ((a + b) + c) }}",
+ },
+ {
+ "{{ a + b - c }}",
+ "{{ ((a + b) - c) }}",
+ },
+ {
+ "{{ a * b * c }}",
+ "{{ ((a * b) * c) }}",
+ },
+ {
+ "{{ a * b / c }}",
+ "{{ ((a * b) / c) }}",
+ },
+ {
+ "{{ a + b / c }}",
+ "{{ (a + (b / c)) }}",
+ },
+ {
+ "{{ a + b * c + d / e - f }}",
+ "{{ (((a + (b * c)) + (d / e)) - f) }}",
+ },
+ {
+ "{{ 5 > 4 == 3 < 4 }}",
+ "{{ ((5 > 4) == (3 < 4)) }}",
+ },
+ {
+ "{{ 5 < 4 != 3 > 4 }}",
+ "{{ ((5 < 4) != (3 > 4)) }}",
+ },
+ {
+ "{{ 3 + 4 * 5 == 3 * 1 + 4 * 5 }}",
+ "{{ ((3 + (4 * 5)) == ((3 * 1) + (4 * 5))) }}",
+ },
+ {
+ "{{ (5 + 5) * 2 }}",
+ "{{ ((5 + 5) * 2) }}",
+ },
+ {
+ "{{ 2 / (5 + 5) }}",
+ "{{ (2 / (5 + 5)) }}",
+ },
+ {
+ "{{ (5 + 5) * 2 * (5 + 5) }}",
+ "{{ (((5 + 5) * 2) * (5 + 5)) }}",
+ },
+ {
+ "{{ -(5 + 5) }}",
+ "{{ (-(5 + 5)) }}",
+ },
+ {
+ "{{ foo[0] + bar[1 + 2] }}",
+ "{{ (foo[0] + bar[(1 + 2)]) }}",
+ },
+ {
+ "{{ foo.bar + foo.baz }}",
+ "{{ (foo.bar + foo.baz) }}",
+ },
+ {
+ "{{ foo.bar + bar[0].baz * foo.bar.baz }}",
+ "{{ (foo.bar + (bar[0].baz * foo.bar.baz)) }}",
+ },
+ { 0 },
+ };
+ for (size_t i = 0; tests[i].input != NULL; i++) {
+ struct parser *parser = parser_new(strdup("test"), tests[i].input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_VARIABLE);
+ sds output = block_string(blk, sdsempty());
+ asserteq(strcmp(output, tests[i].expected), 0);
+ sdsfree(output);
+ parser_destroy(parser);
+ template_destroy(tmpl);
+ }
+}
+
+static inline void
+test_loop_tag(void)
+{
+ char *input = "{% for v in seq %}"
+ "{% break %}"
+ "{% endfor %}";
+ struct slice item = slice_whole("v");
+ struct slice seq = slice_whole("seq");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_FOR);
+ asserteq(slice_cmp(&blk->tag.loop.item.token.literal, &item), 0);
+ struct expression *seqexpr = blk->tag.loop.seq;
+ asserteq(seqexpr->type, EXPRESSION_IDENT);
+ asserteq(slice_cmp(&seqexpr->ident.token.literal, &seq), 0);
+ asserteq(blk->tag.loop.subblocks->len, 2);
+ struct block *sub1 = blk->tag.loop.subblocks->values[0];
+ struct block *sub2 = blk->tag.loop.subblocks->values[1];
+ asserteq(sub1->type, BLOCK_TAG);
+ asserteq(sub2->type, BLOCK_TAG);
+ asserteq(sub1->tag.type, TAG_BREAK);
+ asserteq(sub2->tag.type, TAG_CLOSE);
+ asserteq(sub2->tag.token.type, TOKEN_ENDFOR);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_cond_tag(void)
+{
+ char *input = "{% if false %}"
+ "{{ foo }}"
+ "{% elif 1 > 2 %}"
+ "{{ bar }}"
+ "{% else %}"
+ "baz"
+ "{% endif %}";
+ struct value ifexp = VIDENT("foo");
+ struct value elifl = VINT(1);
+ struct slice elifop = slice_whole(">");
+ struct value elifr = VINT(2);
+ struct value elifexp = VIDENT("bar");
+ struct slice elsecont = slice_whole("baz");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_IF);
+
+ struct branch *b1 = blk->tag.cond.root;
+ assertneq(b1->condition, NULL);
+ asserteq(b1->condition->type, EXPRESSION_BOOL);
+ asserteq(b1->condition->token.type, TOKEN_FALSE);
+ asserteq(b1->subblocks->len, 1);
+ struct block *b1sub1 = b1->subblocks->values[0];
+ asserteq(b1sub1->type, BLOCK_VARIABLE);
+ test_expected(b1sub1->variable.expression, ifexp);
+
+ struct branch *b2 = b1->next;
+ assertneq(b2->condition, NULL);
+ test_infix(&b2->condition->infix, elifl, &elifop, elifr);
+ asserteq(b2->subblocks->len, 1);
+ struct block *b2sub1 = b2->subblocks->values[0];
+ asserteq(b2sub1->type, BLOCK_VARIABLE);
+ test_expected(b2sub1->variable.expression, elifexp);
+
+ struct branch *b3 = b2->next;
+ asserteq(b3->condition, NULL);
+ asserteq(b3->subblocks->len, 2);
+ struct block *b3sub1 = b3->subblocks->values[0];
+ asserteq(b3sub1->type, BLOCK_CONTENT);
+ asserteq(slice_cmp(&b3sub1->content.token.literal, &elsecont), 0);
+ struct block *b3sub2 = b3->subblocks->values[1];
+ asserteq(b3sub2->tag.type, TAG_CLOSE);
+ asserteq(b3sub2->tag.token.type, TOKEN_ENDIF);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_parent_tag(void)
+{
+ char *input = "{% extends \"base.html\" %}";
+ struct slice name = slice_whole("base.html");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_EXTENDS);
+ asserteq(slice_cmp(&blk->tag.parent.name->value, &name), 0);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static inline void
+test_tblock_tag(void)
+{
+ char *input = "{% block cock %}"
+ "{% endblock %}";
+ struct slice name = slice_whole("cock");
+ struct parser *parser = parser_new(strdup("test"), input);
+ struct template *tmpl = parser_parse_template(parser);
+ check_parser_errors(parser);
+
+ assertneq(tmpl, NULL);
+ asserteq(tmpl->blocks->len, 1);
+ struct block *blk = tmpl->blocks->values[0];
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_BLOCK);
+ asserteq(slice_cmp(&blk->tag.tblock.name.token.literal, &name), 0);
+ asserteq(blk->tag.tblock.subblocks->len, 1);
+ struct block *sub1 = blk->tag.tblock.subblocks->values[0];
+ asserteq(sub1->type, BLOCK_TAG);
+ asserteq(sub1->tag.type, TAG_CLOSE);
+ asserteq(sub1->tag.token.type, TOKEN_ENDBLOCK);
+
+ blk = hmap_gets(tmpl->tblocks, &name);
+ assertneq(blk, NULL);
+ asserteq(blk->type, BLOCK_TAG);
+ asserteq(blk->tag.type, TAG_BLOCK);
+ asserteq(slice_cmp(&blk->tag.tblock.name.token.literal, &name), 0);
+
+ parser_destroy(parser);
+ template_destroy(tmpl);
+}
+
+static void
+init(void)
+{
+ parser_init();
+}
+
+static void
+cleanup(void)
+{
+ parser_deinit();
+}
+
+int
+main(void)
+{
+ init();
+ INIT_TESTS();
+ RUN_TEST(test_literal_variables);
+ RUN_TEST(test_prefix_variables);
+ RUN_TEST(test_infix_variables);
+ RUN_TEST(test_map_variables);
+ RUN_TEST(test_index_variables);
+ RUN_TEST(test_operator_precedence);
+ RUN_TEST(test_loop_tag);
+ RUN_TEST(test_cond_tag);
+ RUN_TEST(test_parent_tag);
+ RUN_TEST(test_tblock_tag);
+ cleanup();
+}
diff --git a/src/tests/roscha.c b/src/tests/roscha.c
new file mode 100644
index 0000000..92b8bc4
--- /dev/null
+++ b/src/tests/roscha.c
@@ -0,0 +1,189 @@
+#define _POSIX_C_SOURCE 200809L
+#include "tests/tests.h"
+#include "roscha.h"
+
+#include <string.h>
+
+static void
+check_env_errors(struct roscha_env *env, const char *file, int line,
+ const char *func)
+{
+ struct vector *errors = roscha_env_check_errors(env);
+ if (!errors) return;
+ printf("\n");
+ size_t i;
+ sds val;
+ vector_foreach (errors, i, val) {
+ printf("parser error %lu: %s\n", i, val);
+ }
+ printf(TBLD TRED "FAIL!\n" TRST);
+ printf("%s:%d: %s: ", file, line, func);
+ printf("parser encountered errors\n");
+ abort();
+}
+
+#define check_env_errors(p) \
+ check_env_errors(p, __FILE__, __LINE__, __func__)
+
+static void
+test_eval_variable(void)
+{
+ char *input = "{{ foo.bar }}"
+ "{{ foo.bar + foo.baz }}"
+ "{{ foo.bar - foo.baz }}"
+ "{{ foo.bar * foo.baz }}"
+ "{{ foo.bar / foo.baz }}"
+ "{{ l }}"
+ "{{ l[0] }}, {{ l[1] }}"
+ "";
+ char *expected = "8"
+ "12"
+ "4"
+ "32"
+ "2"
+ "[ hello, world, ]"
+ "hello, world"
+ "";
+ struct roscha_object *foo = roscha_object_new(hmap_new());
+ roscha_hmap_set_new(foo, "bar", 8);
+ roscha_hmap_set_new(foo, "baz", 4);
+ struct roscha_object *l = roscha_object_new(vector_new());
+ roscha_vector_push_new(l, (slice_whole("hello")));
+ roscha_vector_push_new(l, (slice_whole("world")));
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ roscha_hmap_set(env->vars, "l", l);
+
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+ roscha_env_destroy(env);
+ sdsfree(got);
+ roscha_object_unref(foo);
+ roscha_object_unref(l);
+}
+
+static void
+test_eval_cond(void)
+{
+ char *input = "{% if foo > bar %}"
+ "Yes"
+ "{% elif baz %}"
+ "Maybe"
+ "{% else %}"
+ "No"
+ "{% endif %}";
+ char *expected1 = "No";
+ char *expected2 = "Maybe";
+ char *expected3 = "Yes";
+ struct roscha_object *foo = roscha_object_new(10);
+ struct roscha_object *bar = roscha_object_new(20);
+ struct roscha_object *baz = roscha_object_new(69);
+
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ roscha_hmap_set(env->vars, "bar", bar);
+
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected1), 0);
+ sdsfree(got);
+
+ roscha_hmap_set(env->vars, "baz", baz);
+ got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected2), 0);
+ sdsfree(got);
+
+ foo->integer = 420;
+ got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected3), 0);
+ sdsfree(got);
+
+ roscha_env_destroy(env);
+ roscha_object_unref(foo);
+ roscha_object_unref(bar);
+ roscha_object_unref(baz);
+}
+
+static void
+test_eval_loop(void)
+{
+ char *input = "{% for v in foo %}"
+ "{{ loop.index }}"
+ "{{ v }}"
+ "{% endfor %}";
+ char *expected = "0hello1world";
+
+ struct roscha_object *foo = roscha_object_new(vector_new());
+ roscha_vector_push_new(foo, (slice_whole("hello")));
+ roscha_vector_push_new(foo, (slice_whole("world")));
+
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("test"), input);
+ check_env_errors(env);
+ roscha_hmap_set(env->vars, "foo", foo);
+ sds got = roscha_env_render(env, "test");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+
+ sdsfree(got);
+ roscha_env_destroy(env);
+ roscha_object_unref(foo);
+}
+
+static void
+test_eval_child(void)
+{
+ char *parent = "hello{% block title %}{% endblock %}"
+ "{% block content %}Content{% endblock %}"
+ "{% block foot %}Foot{% endblock %}";
+ char *child = "{% extends \"parent\" %}"
+ "{% block title %}, world{% endblock %}"
+ "{% block content %}"
+ "In a beautiful place out in the country."
+ "{% endblock %}";
+ char *expected = "hello, world"
+ "In a beautiful place out in the country."
+ "Foot";
+ struct roscha_env *env = roscha_env_new();
+ roscha_env_add_template(env, strdup("parent"), parent);
+ check_env_errors(env);
+ roscha_env_add_template(env, strdup("child"), child);
+ check_env_errors(env);
+ sds got = roscha_env_render(env, "child");
+ check_env_errors(env);
+ asserteq(strcmp(got, expected), 0);
+
+ sdsfree(got);
+ roscha_env_destroy(env);
+}
+
+static void
+init(void)
+{
+ roscha_init();
+}
+
+static void
+cleanup(void)
+{
+ roscha_deinit();
+}
+
+int
+main(void)
+{
+ init();
+ INIT_TESTS();
+ RUN_TEST(test_eval_variable);
+ RUN_TEST(test_eval_cond);
+ RUN_TEST(test_eval_loop);
+ RUN_TEST(test_eval_child);
+ cleanup();
+}
diff --git a/src/tests/slice.c b/src/tests/slice.c
new file mode 100644
index 0000000..1f3875a
--- /dev/null
+++ b/src/tests/slice.c
@@ -0,0 +1,41 @@
+#include "tests/tests.h"
+#include "slice.h"
+
+#include <string.h>
+
+static void
+test_slice_cmp(void)
+{
+ struct slice s1a = {
+ .str = "hello world",
+ .start = 6,
+ .end = 11,
+ };
+ struct slice s1b = {
+ .str = "world",
+ .start = 0,
+ .end = 5,
+ };
+ asserteq(slice_cmp(&s1a, &s1b), 0);
+}
+
+static void
+test_slice_string(void)
+{
+ struct slice slice = {
+ .str = "hello world",
+ .start = 6,
+ .end = 11,
+ };
+ sds str = sdsempty();
+ slice_string(&slice, str);
+ asserteq(strcmp("world", str), 0);
+}
+
+int
+main(void)
+{
+ INIT_TESTS();
+ RUN_TEST(test_slice_cmp);
+ RUN_TEST(test_slice_string);
+}
diff --git a/src/token.c b/src/token.c
new file mode 100644
index 0000000..0d0092d
--- /dev/null
+++ b/src/token.c
@@ -0,0 +1,144 @@
+#include "token.h"
+
+#include <stdio.h>
+#include <stddef.h>
+
+#include "hmap.h"
+
+static struct hmap *keywords = NULL;
+static const char *keys[] = {
+ "and",
+ "or",
+ "not",
+ "for",
+ "in",
+ "break",
+ "endfor",
+ "true",
+ "false",
+ "if",
+ "elif",
+ "else",
+ "endif",
+ "extends",
+ "block",
+ "endblock",
+ NULL,
+};
+enum token_type values[] = {
+ TOKEN_AND,
+ TOKEN_OR,
+ TOKEN_NOT,
+ TOKEN_FOR,
+ TOKEN_IN,
+ TOKEN_BREAK,
+ TOKEN_ENDFOR,
+ TOKEN_TRUE,
+ TOKEN_FALSE,
+ TOKEN_IF,
+ TOKEN_ELIF,
+ TOKEN_ELSE,
+ TOKEN_ENDIF,
+ TOKEN_EXTENDS,
+ TOKEN_BLOCK,
+ TOKEN_ENDBLOCK,
+ TOKEN_EOF,
+};
+
+const char *token_types[] = {
+ "ILLEGAL",
+ "EOF",
+ /* Identifiers/Literals */
+ "IDENTIFIER",
+ "INTEGER",
+ "STRING",
+ /* Operators */
+ "=",
+ "+",
+ "-",
+ "!",
+ "*",
+ "/",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "==",
+ "!=",
+ "and",
+ "or",
+ "not",
+ /* Delimiters */
+ ".",
+ ",",
+ "(",
+ ")",
+ "{",
+ "}",
+ "[",
+ "]",
+ "#",
+ "%",
+ /* Keywords */
+ "for",
+ "in",
+ "break",
+ "endfor",
+ "true",
+ "false",
+ "if",
+ "elif",
+ "else",
+ "endif",
+ "extends",
+ "block",
+ "endblock",
+ /* The document content */
+ "CONTENT",
+};
+
+void
+token_init_keywords(void)
+{
+ if (keywords == NULL) {
+ keywords = hmap_new();
+ for (size_t i = 0; keys[i] != NULL; i++) {
+ hmap_set(keywords, keys[i], values + i);
+ }
+ }
+
+}
+
+enum token_type
+token_lookup_ident(const struct slice *ident)
+{
+ enum token_type *t = hmap_gets(keywords, ident);
+ if (t) {
+ return *t;
+ }
+
+ return TOKEN_IDENT;
+}
+
+extern inline const char *
+token_type_print(enum token_type t)
+{
+ return token_types[t];
+}
+
+sds
+token_string(struct token *token, sds str)
+{
+ const char *type = token_type_print(token->type);
+ sds slicebuf = sdsempty();
+ sdscatfmt(str, "TOKEN: type: %s, literal: %s", type,
+ slice_string(&token->literal, slicebuf));
+ sdsfree(slicebuf);
+ return str;
+}
+
+void
+token_free_keywords(void)
+{
+ hmap_free(keywords);
+}
diff --git a/src/vector.c b/src/vector.c
new file mode 100644
index 0000000..2962780
--- /dev/null
+++ b/src/vector.c
@@ -0,0 +1,49 @@
+#include "vector.h"
+
+static inline bool
+vector_grow(struct vector *vec)
+{
+ vec->cap *= 2;
+ vec->values = realloc(vec->values, sizeof(vec->values) * vec->cap);
+ return vec->values != NULL;
+}
+
+struct vector *
+vector_new_with_cap(size_t cap)
+{
+ struct vector *vec = malloc(sizeof(*vec));
+ if (!vec) return NULL;
+ vec->values = malloc(sizeof(vec->values) * cap);
+ if (!vec->values) {
+ free(vec);
+ return NULL;
+ }
+ vec->cap = cap;
+ vec->len = 0;
+
+ return vec;
+}
+
+ssize_t
+vector_push(struct vector *vec, void *val)
+{
+ if (vec->len >= vec->cap && !vector_grow(vec)) return -1;
+ vec->values[vec->len] = val;
+ vec->len++;
+
+ return vec->len - 1;
+}
+
+void *
+vector_pop(struct vector *vec)
+{
+ if (vec->len == 0) return NULL;
+ return vec->values[--vec->len];
+}
+
+void
+vector_free(struct vector *vec)
+{
+ free(vec->values);
+ free(vec);
+}