diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | COPYING | 7 | ||||
-rw-r--r-- | LICENSE | 458 | ||||
-rw-r--r-- | Makefile | 47 | ||||
-rw-r--r-- | README.md | 37 | ||||
-rw-r--r-- | include/ast.h | 221 | ||||
-rw-r--r-- | include/hmap.h | 72 | ||||
-rw-r--r-- | include/lexer.h | 33 | ||||
-rw-r--r-- | include/object.h | 171 | ||||
-rw-r--r-- | include/parser.h | 54 | ||||
-rw-r--r-- | include/roscha.h | 54 | ||||
-rw-r--r-- | include/slice.h | 37 | ||||
-rw-r--r-- | include/tests/tests.h | 45 | ||||
-rw-r--r-- | include/token.h | 84 | ||||
-rw-r--r-- | include/vector.h | 29 | ||||
m--------- | sds | 0 | ||||
-rw-r--r-- | src/ast.c | 322 | ||||
-rw-r--r-- | src/hmap.c | 222 | ||||
-rw-r--r-- | src/lexer.c | 264 | ||||
-rw-r--r-- | src/object.c | 242 | ||||
-rw-r--r-- | src/parser.c | 700 | ||||
-rw-r--r-- | src/roscha.c | 632 | ||||
-rw-r--r-- | src/slice.c | 63 | ||||
-rw-r--r-- | src/tests/lexer.c | 246 | ||||
-rw-r--r-- | src/tests/parser.c | 542 | ||||
-rw-r--r-- | src/tests/roscha.c | 189 | ||||
-rw-r--r-- | src/tests/slice.c | 41 | ||||
-rw-r--r-- | src/token.c | 144 | ||||
-rw-r--r-- | src/vector.c | 49 |
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 @@ -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. @@ -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); +} |