From 5d66c96a190a396a1535c89bed4e33c2a005fe8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yaroslav=20de=20la=20Pe=C3=B1a=20Smirnov?=
 <yps@yaroslavps.com>
Date: Thu, 24 Mar 2022 01:04:02 +0300
Subject: 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.
---
 src/ast.c          | 322 ++++++++++++++++++++++++
 src/hmap.c         | 222 +++++++++++++++++
 src/lexer.c        | 264 ++++++++++++++++++++
 src/object.c       | 242 ++++++++++++++++++
 src/parser.c       | 700 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/roscha.c       | 632 +++++++++++++++++++++++++++++++++++++++++++++++
 src/slice.c        |  63 +++++
 src/tests/lexer.c  | 246 +++++++++++++++++++
 src/tests/parser.c | 542 +++++++++++++++++++++++++++++++++++++++++
 src/tests/roscha.c | 189 +++++++++++++++
 src/tests/slice.c  |  41 ++++
 src/token.c        | 144 +++++++++++
 src/vector.c       |  49 ++++
 13 files changed, 3656 insertions(+)
 create mode 100644 src/ast.c
 create mode 100644 src/hmap.c
 create mode 100644 src/lexer.c
 create mode 100644 src/object.c
 create mode 100644 src/parser.c
 create mode 100644 src/roscha.c
 create mode 100644 src/slice.c
 create mode 100644 src/tests/lexer.c
 create mode 100644 src/tests/parser.c
 create mode 100644 src/tests/roscha.c
 create mode 100644 src/tests/slice.c
 create mode 100644 src/token.c
 create mode 100644 src/vector.c

(limited to 'src')

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);
+}
-- 
cgit v1.2.3