aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYaroslav de la Peña Smirnov <yps@yaroslavps.com>2022-03-24 01:04:02 +0300
committerYaroslav de la Peña Smirnov <yps@yaroslavps.com>2022-03-24 01:04:02 +0300
commit5d66c96a190a396a1535c89bed4e33c2a005fe8d (patch)
tree36a681d8cf226cf30f06b2764c008077d9655dc7 /src
downloadroscha-5d66c96a190a396a1535c89bed4e33c2a005fe8d.tar.gz
roscha-5d66c96a190a396a1535c89bed4e33c2a005fe8d.zip
Initial commit
Basically it works, just needs some polishing and maybe a couple of features that I could actually use. Also probably better docs. Not sure if it will be of use to anybody besides me.
Diffstat (limited to 'src')
-rw-r--r--src/ast.c322
-rw-r--r--src/hmap.c222
-rw-r--r--src/lexer.c264
-rw-r--r--src/object.c242
-rw-r--r--src/parser.c700
-rw-r--r--src/roscha.c632
-rw-r--r--src/slice.c63
-rw-r--r--src/tests/lexer.c246
-rw-r--r--src/tests/parser.c542
-rw-r--r--src/tests/roscha.c189
-rw-r--r--src/tests/slice.c41
-rw-r--r--src/token.c144
-rw-r--r--src/vector.c49
13 files changed, 3656 insertions, 0 deletions
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);
+}