summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--LICENSE13
-rw-r--r--Makefile20
-rw-r--r--README18
-rw-r--r--compile.c480
-rw-r--r--debug.c177
-rw-r--r--doc/thac.153
-rw-r--r--doc/thalatta.5299
-rw-r--r--lex.c279
-rw-r--r--main.c61
-rw-r--r--parse.c637
-rw-r--r--say.c89
-rw-r--r--str.c33
-rw-r--r--thac.h323
-rw-r--r--val.c144
-rw-r--r--var.c112
-rw-r--r--vertex.c100
17 files changed, 2842 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..78cb2b8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.o
+*.core
+tags
+thac
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1e2943d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..808887a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+OBJS += compile.o
+OBJS += debug.o
+OBJS += lex.o
+OBJS += main.o
+OBJS += parse.o
+OBJS += say.o
+OBJS += str.o
+OBJS += val.o
+OBJS += var.o
+OBJS += vertex.o
+
+all: thac
+
+thac: ${OBJS}
+	${CC} ${LDFLAGS} -o $@ ${OBJS} ${LDLIBS}
+
+${OBJS}: thac.h
+
+clean:
+	rm -f thac ${OBJS}
diff --git a/README b/README
new file mode 100644
index 0000000..e27fcf9
--- /dev/null
+++ b/README
@@ -0,0 +1,18 @@
+Thalatta
+========
+Thalatta is a small language for making graphs. This is a source code
+of its implementation thac, that is a single-pass compiler of a graph,
+interpreter of a language.
+
+Building
+========
+You need to have
+- An ANSI environment
+- A C compiler with support of
+  - anonymous, unnamed strctures
+  - passing structures by value
+
+Then run either:
+$ make
+or, if you don't have make installed:
+$ $CC *.c
diff --git a/compile.c b/compile.c
new file mode 100644
index 0000000..d488414
--- /dev/null
+++ b/compile.c
@@ -0,0 +1,480 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+struct val nil = {VVAR, {.name = "nil"}};
+static struct var valnil = {"nil", {VNIL, {0}}};
+struct scope basescope = {NULL, &valnil, 1};
+
+static struct val zero = {VNUM, {0}};
+
+static struct val
+rnum(struct node *node, struct scope *scope)
+{
+	struct val val;
+
+	val.type = VNUM;
+	val.num = node->nnum;
+
+	return val;
+}
+
+static struct val
+rvar(struct node *node, struct scope *scope) {
+	struct val val;
+
+	val.type = VVAR;
+	val.name = node->nvar;
+
+	return val;
+}
+
+static struct val
+roper(struct node *node, struct scope *scope)
+{
+	struct val l, m, r, ref, val;
+	struct scope *lscope;
+	vlong i, j;
+
+	val.type = VNUM;
+	switch (node->noper.type)
+	{
+#define BIN(NAME, OPER) break; case NAME: \
+{ \
+	l = getval(VNUM, eval(node->noper.l, scope), scope, 1); \
+	r = getval(VNUM, eval(node->noper.r, scope), scope, 1); \
+	val.num = l.num OPER r.num; \
+}
+	BIN(OSUM, +)
+	BIN(OSUB, -)
+	BIN(OMUL, *)
+	BIN(ODIV, /)
+	BIN(OMOD, %)
+	BIN(OLSHIFT, <<)
+	BIN(ORSHIFT, >>)
+	BIN(OLESS, <)
+	BIN(OLESSEQ, <=)
+	BIN(OGREAT, >)
+	BIN(OGREATEQ, >=)
+	BIN(OEQ, ==)
+	BIN(ONEQ, !=)
+	BIN(OAND, &&)
+	BIN(OOR, ||)
+	BIN(OBOR, |)
+	BIN(OBAND, &)
+	BIN(OBXOR, ^)
+#undef BIN
+	break; case OPOW:
+		l = getval(VNUM, eval(node->noper.l, scope), scope, 1);
+		r = getval(VNUM, eval(node->noper.r, scope), scope, 1);
+
+		val.num = 1;
+		for (i = 0; i < r.num; ++i)
+			val.num *= l.num;
+#define UN(NAME, OPER) break; case NAME: \
+{ \
+	m = getval(VNUM, eval(node->noper.m, scope), scope, 1); \
+	val.num = OPER m.num; \
+}
+	UN(ONOT, !)
+	UN(OINV, ~)
+#undef UN
+	break; case OASSIGN:
+	{
+		struct val shouldfree = nil;
+		struct var *var;
+
+		val = getval(VVAR, eval(node->noper.l, scope), scope, 0);
+		lscope = scope;
+		i = findvar(val.name, &lscope);
+		if (i > 0)
+			shouldfree = deref(val, scope);
+
+		r = deref(eval(node->noper.r, scope), scope);
+
+		/* it's fine to use scope here as it won't be used after */
+		assignvar(val.name, copyval(r), &scope);
+		freeval(shouldfree);
+		freeval(r);
+	}
+#define ASS(NAME, OPER) break; case NAME: \
+{ \
+	ref = getval(VVAR, eval(node->noper.l, scope), scope, 0); \
+	l = getval(VNUM, ref, scope, 1); \
+	r = getval(VNUM, eval(node->noper.r, scope), scope, 1); \
+	val.num = l.num OPER r.num; \
+	assignvar(ref.name, val, &scope); \
+	val = ref; \
+}
+	ASS(OASSSUM, +)
+	ASS(OASSSUB, -)
+	ASS(OASSMUL, *)
+	ASS(OASSDIV, /)
+	ASS(OASSMOD, %)
+	ASS(OASSBOR, |)
+	ASS(OASSBAND, &)
+	ASS(OASSBXOR, ^)
+	ASS(OASSLSHIFT, <<)
+	ASS(OASSRSHIFT, >>)
+#undef ASS
+#define POST(NAME, OPER) break; case NAME: \
+{ \
+	ref = getval(VVAR, eval(node->noper.m, scope), scope, 0); \
+	val = getval(VNUM, ref, scope, 1); \
+	m = val; \
+	OPER m.num; \
+	assignvar(ref.name, m, &scope); \
+}
+	POST(OPOSTDECR, --)
+	POST(OPOSTINCR, ++)
+#undef POST
+#define PRE(NAME, OPER) break; case NAME: \
+{ \
+	ref = getval(VVAR, eval(node->noper.m, scope), scope, 0); \
+	val = getval(VNUM, ref, scope, 1); \
+	OPER val.num; \
+	assignvar(ref.name, val, &scope); \
+}
+	PRE(OPREDECR, --)
+	PRE(OPREINCR, ++)
+#undef PRE
+	break; case OCOND:
+		l = getval(VNUM, eval(node->noper.l, scope), scope, 1);
+		val = eval(l.num ? node->noper.m : node->noper.r, scope);
+	break; case OARRAY:
+	{
+		char varname[GENVARLEN];
+		int c;
+
+		if ((c = varnextchar(scope)) == -1)
+			complain(1, "too nested array is wanted (27+)");
+		snprintf(varname, sizeof(varname), "@%c", c);
+
+		i = assignvar(varname, zero, &scope);
+		l = getval(VNUM, eval(node->noper.l, scope), scope, 1);
+
+		val.type = VARR;
+		val.arr = NULL;
+		val.len = 0;
+
+		for (j = 0; j < l.num; ++j, scope->vars[i].val.num = j)
+		{
+			r = deref(eval(node->noper.r, scope), scope);
+
+			val.arr = realloc(val.arr, sizeof(*val.arr) * ++val.len);
+			if (!val.arr)
+				dieno(1, "realloc()");
+
+			val.arr[j] = copyval(r);
+		}
+		delvarscope(scope, i);
+	}
+	break; case OINDEX:
+		ref = eval(node->noper.l, scope);
+		l = getval(VARR, ref, scope, 1);
+		r = getval(VNUM, eval(node->noper.r, scope), scope, 1);
+		if (r.num < 0 || r.num >= l.len)
+			complain(1, "duh that's oob (0<=%lld<%lld)", r.num, l.len);
+		val = copyval(l.arr[r.num]);
+		if (ref.type == VARR)
+			freeval(ref);
+	break; case OSLICE:
+		ref = eval(node->noper.l, scope);
+		l = getval(VARR, ref, scope, 1);
+		m = getval(VNUM, eval(node->noper.m, scope), scope, 1);
+		r = getval(VNUM, eval(node->noper.r, scope), scope, 1);
+		m.num = min(l.len, max(0, m.num));
+		r.num = min(l.len, max(0, r.num));
+
+		val.type = VARR;
+		val.len = r.num - m.num;
+		val.arr = malloc(sizeof(*val.arr) * val.len);
+		if (!val.arr)
+			dieno(1, "malloc()");
+
+		for (i = m.num; i < r.num; ++i)
+			val.arr[i - m.num] = copyval(l.arr[i]);
+
+		if (ref.type == VARR)
+			freeval(ref);
+	break; case OCONCAT:
+	{
+		struct val refl, refr;
+		refl = eval(node->noper.l, scope);
+		l = getval(VARR, refl, scope, 1);
+		refr = eval(node->noper.r, scope);
+		r = getval(VARR, refr, scope, 1);
+
+		val.type = VARR;
+		val.len = l.len + r.len;
+		val.arr = malloc(sizeof(*val.arr) * val.len);
+		if (!val.arr)
+			dieno(1, "malloc()");
+
+		for (i = 0; i < l.len; ++i)
+			val.arr[i] = copyval(l.arr[i]);
+		for (i = 0; i < r.len; ++i)
+			val.arr[l.len + i] = copyval(r.arr[i]);
+
+		if (refl.type == VARR)
+			freeval(refl);
+		if (refr.type == VARR)
+			freeval(refr);
+	}
+	break; case OLEN:
+		m = getval(VARR, eval(node->noper.m, scope), scope, 1);
+		val.num = m.len;
+	break; case OASSERT:
+		m = getval(VNUM, eval(node->noper.m, scope), scope, 1);
+		if (!(val.num = m.num))
+			complain(1, "assertion has failed!");
+	break; case OCONNECT:
+		val = connectval(
+			eval(node->noper.l, scope),
+			eval(node->noper.r, scope),
+			scope
+		);
+	break; default:
+		die(1, "undone %s: %s", nodename(node->type), nopername(node->noper.type));
+	}
+
+	return val;
+}
+
+static struct val
+rnode(struct node *node, struct scope *scope)
+{
+	struct val prop, val;
+	ulong i;
+
+	val.type = VNUM;
+	val.num = mkvert();
+
+	for (i = 0; i < node->nnode.len; ++i)
+	{
+		prop = getval(VNUM, eval(node->nnode.params[i], scope), scope, 1);
+		addprop(val.num, prop.num);
+	}
+
+	return val;
+}
+
+static struct val
+rcomp(struct node *node, struct scope *scope)
+{
+	struct scope lscope = {scope, NULL, 0};
+	struct val val = nil;
+	ulong i;
+
+	for (i = 0; i < node->ncomp.len; ++i)
+	{
+		val = eval(node->ncomp.stmts[i], &lscope);
+		if (val.type == VBREAK || val.type == VCONT)
+			break;
+	}
+
+	for (i = 0; i < lscope.len; ++i)
+		freeval(lscope.vars[i].val);
+	free(lscope.vars);
+	return val;
+}
+
+static struct val
+rcond(struct node *node, struct scope *scope)
+{
+	struct val val;
+	val = getval(VNUM, eval(node->ncond.cond, scope), scope, 1);
+	return eval(val.num ? node->ncond.t : node->ncond.f, scope);
+}
+
+static struct val
+rfor(struct node *node, struct scope *scope)
+{
+	struct val val;
+
+	eval(node->nfor.before, scope);
+loop:
+	val = getval(VNUM, eval(node->nfor.cond, scope), scope, 1);
+	if (!val.num)
+		return nil;
+
+	val = eval(node->nfor.code, scope);
+	if (val.type == VBREAK)
+		return nil;
+
+	eval(node->nfor.between, scope);
+	goto loop;
+}
+
+static struct val
+rforeachval(struct val arr, struct scope *scope, struct node *code)
+{
+	char varname[GENVARLEN];
+	struct scope *tscope;
+	ulong i, j, target;
+	struct val val;
+
+	snprintf(varname, sizeof(varname), "@%lu", varnextnum(scope));
+	i = assignvar(varname, zero, &scope);
+
+	for (j = 0; j < arr.len; ++j, scope->vars[i].val.num = j)
+	{
+		if (arr.arr[j].type == VARR)
+		{
+			val = rforeachval(arr.arr[j], scope, code);
+		}
+		else
+		{
+			tscope = scope;
+			target = assignvar("@", copyval(arr.arr[j]), &tscope);
+			val = eval(code, scope);
+			delvarscope(tscope, target);
+		}
+		if (val.type == VBREAK)
+			return val;
+	}
+
+	delvarscope(scope, i);
+	return nil;
+}
+
+static struct val
+rforeach(struct node *node, struct scope *scope)
+{
+	struct val arr;
+
+	arr = getval(VARR, eval(node->nforeach.obj, scope), scope, 1);
+	rforeachval(arr, scope, node->nforeach.code);
+
+	return nil;
+}
+
+static struct val
+rmod(struct node *node, struct scope *scope)
+{
+	struct val val;
+
+	val.type = VNUM;
+	val.num = (vlong)node;
+
+	return val;
+}
+
+/*
+ * TODO: module doesn't know caller's environment, so it should not
+ * depend on its scope, but rather make an own one at rmod()
+ */
+static struct val
+rwith(struct node *node, struct scope *scope)
+{
+	struct scope argscope = {scope, NULL, 0};
+	struct scope modscope = {&argscope, NULL, 0};
+	struct node *mod;
+	struct val val;
+	ulong i;
+
+	val = getval(VNUM, eval(node->nwith.mod, scope), scope, 1);
+	mod = (struct node *)val.num;
+
+	if (mod->nmod.len != node->nwith.len)
+		complain(
+			1,
+			"have %lu parameter%s, got %lu argument%s",
+			mod->nmod.len,
+			mod->nmod.len != 1 ? "s" : "",
+			node->nwith.len,
+			mod->nwith.len != 1 ? "s" : ""
+		);
+
+	argscope.len = mod->nmod.len;
+	argscope.vars = malloc(sizeof(*argscope.vars) * argscope.len);
+	if (!argscope.vars)
+		dieno(1, "malloc()");
+
+	for (i = 0; i < argscope.len; ++i)
+	{
+		argscope.vars[i].name = mod->nmod.params[i];
+		argscope.vars[i].val = deref(eval(node->nwith.args[i], scope), scope);
+	}
+
+	for (i = 0; i < mod->nmod.code->ncomp.len; ++i)
+	{
+		val = eval(mod->nmod.code->ncomp.stmts[i], &modscope);
+		switch (val.type)
+		{
+		case VBREAK: case VCONT:
+			complain(1, "cannot %s out of module", valname(val.type));
+		break; default: break;
+		}
+	}
+
+	eval(node->nwith.code, &modscope);
+
+	for (i = 0; i < modscope.len; ++i)
+		freeval(modscope.vars[i].val);
+	free(modscope.vars);
+	return nil;
+}
+
+static struct val
+rbreak(struct node *node, struct scope *scope)
+{
+	struct val val;
+
+	val.type = node->type == NBREAK ? VBREAK : VCONT;
+
+	return val;
+}
+
+static struct val
+rerr(struct node *node, struct scope *scope)
+{
+	struct val val;
+
+	complain(1, "unknown node %s found", nodename(node->type));
+
+	/* not reached */
+	return nil;
+}
+
+struct val
+eval(struct node *node, struct scope *scope)
+{
+	if (!node)
+		return nil;
+
+#define is(t) (node->type == t)
+	return (is(NCOMP)    ? rcomp :
+	        is(NCOND)    ? rcond :
+	        is(NFOR)     ? rfor :
+	        is(NFOREACH) ? rforeach :
+	        is(NMOD)     ? rmod :
+	        is(NNUM)     ? rnum :
+	        is(NNODE)    ? rnode :
+	        is(NOPER)    ? roper :
+	        is(NWITH)    ? rwith :
+	        is(NVAR)     ? rvar :
+	        is(NBREAK)   ? rbreak :
+	        is(NCONT)    ? rbreak :
+	                       rerr)(node, scope);
+#undef is
+}
diff --git a/debug.c b/debug.c
new file mode 100644
index 0000000..ecb4f78
--- /dev/null
+++ b/debug.c
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+/*
+ * These functions are called for debugging if needed
+ */
+
+#include "thac.h"
+
+
+void walknode(struct node *x, ulong depth)
+{
+	int i;
+
+	for (i = 0; i < depth; ++i)
+		fputs("   ", stderr);
+	fprintf(stderr, "%s", nodename(x->type));
+
+	switch (x->type)
+	{
+	break; case NOPER:
+	{
+		fprintf(stderr, ": %s\n", nopername(x->noper.type));
+		switch (x->noper.type)
+		{
+		break; case OINDEX:
+		{
+			walknode(x->noper.l, depth + 1);
+			walknode(x->noper.m, depth + 1);
+			if (x->noper.r)
+				walknode(x->noper.r, depth + 1);
+		}
+		break; case OASSERT: case OLEN: case OINV:
+		case ONOT: case OPOSTDECR: case OPOSTINCR:
+		case OPREDECR: case OPREINCR:
+		{
+			walknode(x->noper.m, depth + 1);
+		}
+		break; case OCOND:
+		{
+			walknode(x->noper.l, depth + 1);
+			walknode(x->noper.m, depth + 1);
+			walknode(x->noper.r, depth + 1);
+		}
+		break; default:
+		{
+			walknode(x->noper.l, depth + 1);
+			walknode(x->noper.r, depth + 1);
+		}
+		}
+	}
+	break; case NNODE:
+	{
+		ulong i;
+		fputc('\n', stderr);
+		for (i = 0; i < x->nnode.len; ++i)
+			walknode(x->nnode.params[i], depth + 1);
+	}
+	break; case NCOMP:
+	{
+		ulong i;
+		fputc('\n', stderr);
+		for (i = 0; i < x->ncomp.len; ++i)
+			walknode(x->ncomp.stmts[i], depth + 1);
+	}
+	break; case NMOD:
+	{
+		ulong i;
+		fputs(": ", stderr);
+		for (i = 0; i < x->nmod.len; ++i)
+			fprintf(stderr, "%s ", x->nmod.params[i]);
+		fputc('\n', stderr);
+		walknode(x->nmod.code, depth + 1);
+	}
+	break; case NVAR:
+	{
+		fprintf(stderr, ": %s\n", x->nvar);
+	}
+	break; case NNUM:
+	{
+		fprintf(stderr, ": %lld\n", x->nnum);
+	}
+	break; case NFOR:
+	{
+		fputc('\n', stderr);
+		if (x->nfor.before)
+			walknode(x->nfor.before, depth + 1);
+		if (x->nfor.cond)
+			walknode(x->nfor.cond, depth + 1);
+		if (x->nfor.between)
+			walknode(x->nfor.between, depth + 1);
+		walknode(x->nfor.code, depth + 1);
+	}
+	break; case NFOREACH:
+	{
+		fputc('\n', stderr);
+		walknode(x->nforeach.obj, depth + 1);
+		walknode(x->nforeach.code, depth + 1);
+	}
+	break; case NCOND:
+	{
+		fputc('\n', stderr);
+		walknode(x->ncond.cond, depth + 1);
+		walknode(x->ncond.t, depth + 1);
+		if (x->ncond.f)
+			walknode(x->ncond.f, depth + 1);
+	}
+	break; case NWITH:
+	{
+		ulong i;
+		fputc('\n', stderr);
+		walknode(x->nwith.mod, depth + 1);
+		for (i = 0; i < x->nwith.len; ++i)
+			walknode(x->nwith.args[i], depth + 1);
+		walknode(x->nwith.code, depth + 1);
+	}
+	break; default: putc('\n', stderr);
+	}
+}
+
+void
+walkscope(struct scope *scope, ulong depth)
+{
+#define printdepth() for (j = 0; j < depth; ++j) fputs("   ", stderr)
+	ulong i, j;
+
+	printdepth();
+	fprintf(stderr, "scope(%lu)=%p:\n", scope->len, scope);
+	++depth;
+	if (scope->parent)
+		walkscope(scope->parent, depth);
+
+	for (i = 0; i < scope->len; ++i)
+	{
+		printdepth();
+		fprintf(
+			stderr,
+			"%s:%s=%lld\n",
+			scope->vars[i].name,
+			valname(scope->vars[i].val.type),
+			scope->vars[i].val.num
+		);
+	}
+}
+
+void
+walkval(struct val *v, ulong depth)
+{
+	ulong i;
+
+	for (i = 0; i < depth; ++i)
+		fputs("   ", stderr);
+	fprintf(stderr, "%s", valname(v->type));
+
+	switch (v->type)
+	{
+	break; case VARR:
+		fprintf(stderr, ": %lu\n", v->len);
+		for (i = 0; i < v->len; ++i)
+			walkval(&v->arr[i], depth + 1);
+	break; case VNUM: fprintf(stderr, ": %lld\n", v->num);
+	break; case VVAR: fprintf(stderr, ": %s\n", v->name);
+	break; default: putc('\n', stderr);
+	}
+}
diff --git a/doc/thac.1 b/doc/thac.1
new file mode 100644
index 0000000..1da5f4c
--- /dev/null
+++ b/doc/thac.1
@@ -0,0 +1,53 @@
+.Dd April  5, 2026
+.Dt THAC 1
+.Os
+.
+.Sh NAME
+.Nm thac
+.Nd thalatta compiler
+.
+.Sh SYNOPSIS
+.Nm
+.Ar file
+.
+.Sh DESCRIPTION
+.Nm
+implements a procedural graph description language.
+It evaluates
+.Ar file 
+and then emits resulting graph in the following format:
+.Do
+.Va prop0
+.Va ...
+.Va propN
+|
+.Va out0
+.Va ...
+.Va outM
+.Dc ,
+where
+.Bl -dash -width Ds -compact
+.It
+.Va N ,
+.Va M
+are nonnegative integers
+.It
+.Va X
+in
+.Va propX
+is an integer
+.It
+.Va X
+in
+.Va outX
+is a nonnegative integer
+.El
+.
+.Sh RETURN VALUES
+.Ex -std
+.
+.Sh SEE ALSO
+.Xr thalatta 5
+.
+.Sh AUTHORS
+.An Nakidai Perumenei Aq Mt nakidai@disroot.org
diff --git a/doc/thalatta.5 b/doc/thalatta.5
new file mode 100644
index 0000000..2cc35b7
--- /dev/null
+++ b/doc/thalatta.5
@@ -0,0 +1,299 @@
+.Dd April  5, 2026
+.Dt THALATTA 5
+.Os
+.
+.Sh NAME
+.Nm thalatta
+.Nd procedural graph description language
+.
+.Sh DESCRIPTION
+.Nm
+is a language used to generate attributed digraphs.
+It is intended to be minimalist,
+yet provides enough syntax for convenient graph construction.
+.Nm
+has C-like syntax,
+but introduces some new features as well,
+while removing unnecessary ones for graph context.
+This manual shows only where
+.Nm
+differs from C.
+.
+.Ss Values
+There are only 3 types in
+.Nm :
+arrays, integers, and nil.
+.
+.Pp
+Arrays are constant:
+a new one is allocated on operations such as assignment,
+concatenation etc.
+.
+.Pp
+Integers store not only user-defined numbers,
+but also node and module identifiers used by an implementation.
+These identifiers are used by the implementation
+and should not be edited by the programmer.
+.
+.Pp
+Nil type is the one that does not hold anything.
+It can be used as a placeholder.
+.Nm
+has a predefined variable
+.Dv nil
+of the type of the same name.
+.
+.Ss Operations
+.Nm
+has operations very like in C,
+excluding function calls,
+structures,
+and some others.
+Instead introduces some new ones:
+.Bl -tag -width Ds
+.It Connecting
+.Nm
+is a graph description language.
+The
+.Ic <-
+operator is used to connect a graph.
+Its precedencence is like of an addition.
+It creates a connection from right to left,
+returning the left operand, so
+.Bd -literal -offset indent -compact
+a <- b <- c;
+.Ed
+connects both b and c to a,
+not b to a and c to b.
+.
+.Pp
+Connecting a node to an array
+means connecting it all the cells from the array.
+Connecting array to array is possible only if they are isomorphic,
+i.e.,
+they are of the same length,
+and all subarrays are of the same length recursively,
+if applicable.
+.
+.It Generation
+.Nm
+lacks array initializers like in C.
+Instead,
+there is a prefix operator
+.Ic Bq Va expr ,
+where
+.Va expr
+must be an integer.
+This will allocate an array
+of size
+.Va expr .
+For each array cell of depth n
+there is a variable starting with @
+followed by an nth letter of the alphabet
+storing current depth's index.
+.
+.Pp
+For example,
+this code:
+.Bd -literal -offset indent -compact
+[2][3]node(@a, @b);
+.Ed
+.
+.Pp
+generates 4 nodes with properties as in binary count:
+.Bd -literal -offset indent -compact
+0 0 |
+0 1 |
+0 2 |
+1 0 |
+1 1 |
+1 2 |
+.Ed
+.
+.It Slicing
+Besides simple indexing,
+.Nm
+has a postfix operator
+.Ic Bq Va l Ic : Va r
+that returns a slice of given array
+from
+.Va l
+inclusive to
+.Va r
+non inclusive.
+Both bounds are clamped
+to the array limits.
+.
+.It Concatenation
+The
+.Ic ><
+operator is used to concatenate 2 arrays.
+Its precedence is like of a multiplication.
+.
+.It Exponentiation
+Uses
+.Ic **
+operator,
+is done before multiplication and right-associative.
+.El
+.
+.Pp
+.Nm
+has also the prefix operator
+.Ic len
+to get an array length,
+and the prefix operator
+.Ic assert
+aborting the execution
+if its argument evaluates to zero,
+and returning it otherwise.
+.
+.Ss Nodes
+Nodes are created using the following syntax:
+.Bd -literal -offset indent -compact
+node(prop1, prop2);
+.Ed
+where properties are numbers.
+This will return a number \(em
+a unique node identifier.
+.
+.Ss Foreach
+The
+.Ic foreach
+construct is used to iterate over elements of an array.
+It recursively walks through its argument,
+for each cell executing its body with variables
+.Va @0
+.Va @1
+\&...
+set just like the generation operator does.
+In addition,
+it sets the
+.Va @
+variable to the current element.
+.
+.Pp
+This code ensures that every number in array
+.Va arr
+is greater than zero:
+.Bd -literal -offset indent
+foreach (arr) {
+	assert(@ > 0);
+}
+.Ed
+.
+.Ss Modules
+Instead of functions,
+.Nm
+has modules.
+A module with parameters
+.Ar a ,
+.Ar b
+can be defined like that:
+.Bd -literal -offset indent
+mod (a, b) { }
+.Ed
+It is possible to assign this
+to a variable.
+.
+.Pp
+Then,
+to use this module later,
+there is
+.Ic with
+statement,
+that first evaluates the module
+and then executes its body,
+with parent scope set to the scope
+of the module.
+.
+.Pp
+This code tests whether the variable x is positive,
+though in somewhat weird way to show the module usage:
+.Bd -literal -offset indent
+foo = mod(a) {
+	b = a;
+}
+
+with foo(x) {
+	assert(b > 0);
+}
+.Ed
+.
+.Sh SYNTAX
+This is the syntax of
+.Nm
+expressed in ABNF:
+.Bd -literal -indent offset
+code = *(stmt)
+stmt = comp / for / foreach / if / with / expr ";" / break / continue
+continue = "continue" ";"
+break = "break" ";"
+with = "with" expr "(" [*(expr ",") expr] ")" stmt
+if = "if" "(" expr ")" stmt ["else" stmt]
+foreach = "foreach" "(" expr ")" stmt
+for = "for" "(" [expr] ";" [expr] ";" [expr] ")" stmt
+expr-assign-oper =/ "|=" / "^=" / "=" / ">>=" / "<<=" / "%="
+expr-assign-oper =  "+=" / "-=" / "*=" / "/=" / "%=" / "&="
+expr-assign      =  expr-cond [expr-assign-oper expr-assign]
+expr-cond = expr-or ? expr : expr-cond
+expr-pref = expr-post / ("assert" / "len" / "~" / "!" / "--" / "++") expr-pref
+expr-post = expr-primary *("--" / "++" / "[" expr [":" expr] "]")
+expr-mod = "mod" "(" [*(id ",") id] ")" comp
+expr-node = "node" "(" [*(expr ",") expr] ")"
+expr-primary = NUMBER / id / "nil" / expr-node / expr-mod / "(" expr ")"
+comp = "{" *(stmt) "}"
+expr = expr-assign
+.Ed
+.
+.Sh EXAMPLES
+This snippet creates a graph of a CLA adder:
+.Bd -literal -offset indent
+and = 0;
+xor = 1;
+
+add = mod(a, b, c)
+{
+	assert(len(a) == len(b));
+	l = len(a);
+	out = [l]node(xor);
+	{
+		p = [l]node(xor) <- a <- b;
+		g = [l]node(and) <- a <- b;
+		out <- p;
+
+		foreach (out)
+		{
+			@ <- (node(and) <- p[0:@0] <- c);
+			for (i = 0; i < @0; ++i)
+				@ <- (node(and) <- p[i+1:@0] <- g[i]);
+		}
+	}
+};
+
+first = [8]node(xor, 0);
+second = [8]node(xor, 1);
+carry = node(xor, 2);
+with add(first, second, carry) { }
+.Ed
+.
+.Sh SEE ALSO
+.Xr thac 1
+.
+.Rs
+.%A B. W. Kernighan
+.%A D. M. Ritchie
+.%D 1978
+.%B The C Programming Language
+.Re
+.
+.Rs
+.%A D. Crocker
+.%A P. Overell
+.%D January 2008
+.%R RFC 5234
+.%T Augmented BNF for Syntax Specifications: ABNF
+.Re
+.
+.Sh AUTHORS
+.An Nakidai Perumenei Aq Mt nakidai@disroot.org
diff --git a/lex.c b/lex.c
new file mode 100644
index 0000000..29b56f0
--- /dev/null
+++ b/lex.c
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <ctype.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+static struct tok storage;
+struct tok curtok;
+static int isunget;
+ulong curline;
+
+static int
+isidish(int ch)
+{
+	static char lut[256] = {
+		['a'] = 1, ['b'] = 1, ['c'] = 1, ['d'] = 1, ['e'] = 1, ['f'] = 1,
+		['g'] = 1, ['h'] = 1, ['i'] = 1, ['j'] = 1, ['k'] = 1, ['l'] = 1,
+		['m'] = 1, ['n'] = 1, ['o'] = 1, ['p'] = 1, ['q'] = 1, ['r'] = 1,
+		['s'] = 1, ['t'] = 1, ['u'] = 1, ['v'] = 1, ['w'] = 1, ['x'] = 1,
+		['y'] = 1, ['z'] = 1, ['A'] = 1, ['B'] = 1, ['C'] = 1, ['D'] = 1,
+		['E'] = 1, ['F'] = 1, ['G'] = 1, ['H'] = 1, ['I'] = 1, ['J'] = 1,
+		['K'] = 1, ['L'] = 1, ['M'] = 1, ['N'] = 1, ['O'] = 1, ['P'] = 1,
+		['Q'] = 1, ['R'] = 1, ['S'] = 1, ['T'] = 1, ['U'] = 1, ['V'] = 1,
+		['W'] = 1, ['X'] = 1, ['Y'] = 1, ['Z'] = 1, ['@'] = 1, ['_'] = 1,
+		['0'] = 1, ['1'] = 1, ['2'] = 1, ['3'] = 1, ['4'] = 1, ['5'] = 1,
+		['6'] = 1, ['7'] = 1, ['8'] = 1, ['9'] = 1, ['\''] = 1,
+	};
+
+	if (ch < 0 || ch > 255)
+		return 0;
+
+	return lut[ch];
+}
+
+static void
+append(int ch)
+{
+	if (curtok.info.cap < curtok.info.len + 1)
+		curtok.info.p = realloc(
+			curtok.info.p,
+			curtok.info.cap = (curtok.info.cap + sizeof(*curtok.info.p)) * 2
+		);
+	if (!curtok.info.p)
+		dieno(1, "realloc()");
+	curtok.info.p[curtok.info.len++] = ch;
+}
+
+static int
+get(void)
+{
+	int ch;
+
+	if ((ch = getc(curfile)) == EOF)
+		return -1;
+	append(ch);
+
+	curline += (ch == '\n');
+
+	return ch;
+}
+
+static int
+unget(int ch)
+{
+	--curtok.info.len;
+	curline -= (ch == '\n');
+	return ungetc(ch, curfile);
+}
+
+void
+word(void)
+{
+	char *s;
+	int ch;
+
+	while ((ch = get()) != EOF)
+		if (!isidish(ch))
+			break;
+	if (ch != EOF)
+		unget(ch);
+	append('\0');
+
+	s = curtok.info.p;
+
+	curtok.type = TID;
+#define is(wanted) !strcmp(s, (wanted))
+	curtok.type = isdigit(*s)    ? TNUM :
+	              is("assert")   ? TKEYASSERT :
+	              is("if")       ? TKEYIF :
+	              is("else")     ? TKEYELSE :
+	              is("for")      ? TKEYFOR :
+	              is("foreach")  ? TKEYFOREACH :
+	              is("len")      ? TKEYLEN :
+	              is("mod")      ? TKEYMOD :
+	              is("node")     ? TKEYNODE :
+	              is("with")     ? TKEYWITH :
+	              is("break")    ? TKEYBREAK :
+	              is("continue") ? TKEYCONT :
+		               TID;
+#undef is
+
+	return;
+}
+
+void
+oper(void)
+{
+	int ch;
+
+	switch (ch = get())
+	{
+	/* these are not start of any other operator */
+#define SINGLE(ch, t) case (ch): curtok.type = (t); append('\0'); return
+	SINGLE('{', TOPBRACE);
+	SINGLE('}', TCLBRACE);
+	SINGLE('[', TOPBRACK);
+	SINGLE(']', TCLBRACK);
+	SINGLE('(', TOPPAREN);
+	SINGLE(')', TCLPAREN);
+	SINGLE(':', TCOL);
+	SINGLE(',', TCOMMA);
+	SINGLE('?', TQUEST);
+	SINGLE(';', TSEMICOL);
+	SINGLE('~', TTILDE);
+#undef SINGLE
+	/*
+	 * these are cases when operator is one of:
+	 * t1  = {ch1}
+	 * t21 = {ch1}{ch21}
+	 * t22 = {ch1}{ch22}
+	 */
+#define DOUBLE(ch1, ch21, ch22, t1, t21, t22) case (ch1): \
+{ \
+	int next; \
+	curtok.type = (t1); \
+	if ((next = get()) == EOF) \
+		(void)0; \
+	else if (next == (ch21)) \
+		curtok.type = (t21); \
+	else if (next == (ch22)) \
+		curtok.type = (t22); \
+	else \
+		unget(next); \
+	append('\0'); \
+	return; \
+}
+	DOUBLE('&', '=', '&', TAMPER, TASSAMPER, TAND);
+	DOUBLE('^', '=', EOF, TCARET, TASSCARET, 0);
+	DOUBLE('=', '=', EOF, TASSIGN, TEQ, 0);
+	DOUBLE('|', '=', '|', TPIPE, TASSPIPE, TOR);
+	DOUBLE('/', '=', EOF, TSLASH, TASSSLASH, 0);
+	DOUBLE('%', '=', EOF, TPERC, TASSPERC, 0);
+	DOUBLE('!', '=', EOF, TEXCLAM, TNEQ, 0);
+	DOUBLE('-', '=', '-', TMINUS, TASSMINUS, TDECR);
+	DOUBLE('+', '=', '+', TPLUS, TASSPLUS, TINCR);
+	DOUBLE('*', '=', '*', TASTER, TASSASTER, TPOW);
+#undef DOUBLE
+	/*
+	 * these are cases when operator is one of:
+	 * t1  = {ch1}
+	 * t21 = {ch1}{ch21}
+	 * t22 = {ch1}{ch22}
+	 * t23 = {ch1}{ch1}
+	 * t3  = {ch1}{ch1}{ch3}
+	 */
+#define TRIPLE(ch1, ch21, ch22, ch3, t1, t21, t22, t23, t3) case (ch1): \
+{ \
+	int next; \
+	curtok.type = (t1); \
+	if ((next = get()) == EOF) \
+		(void)0; \
+	else if (next == (ch21)) \
+		curtok.type = (t21); \
+	else if (next == (ch22)) \
+		curtok.type = (t22); \
+	else if (next == ch) \
+	{ \
+		curtok.type = (t23); \
+		if ((next = get()) == EOF) \
+			(void)0; \
+		else if (next == (ch3)) \
+			curtok.type = (t3); \
+		else \
+			unget(next); \
+	} \
+	else \
+		unget(next); \
+	append('\0'); \
+	return; \
+}
+	TRIPLE('>', '=', '<', '=', TGREAT, TGREATEQ, TCONCAT, TRSHIFT, TASSRSHIFT);
+	TRIPLE('<', '=', '-', '=', TLESS, TLESSEQ, TARRLEFT, TLSHIFT, TASSLSHIFT);
+#undef TRIPLE
+	}
+
+	complain(1, "unknown operator starting with %c", ch);
+}
+
+enum tok_t
+gettok(void)
+{
+	int ch;
+
+	curtok.info = (struct tinfo){NULL, 0, 0};
+
+	if (isunget)
+	{
+		isunget = 0;
+		curtok = storage;
+		/*say("reread %s(`%s')", tokname(curtok.type), curtok.info.p)*/;
+		return curtok.type;
+	}
+
+	for (; (ch = get()) != EOF; curtok.info.len = 0)
+		if (!isspace(ch))
+			goto found;
+	return curtok.type = TEOF;
+found:
+	unget(ch);
+
+	if (isidish(ch))
+		word();
+	else
+		oper();
+
+	/*say("read %s(`%s')", tokname(curtok.type), curtok.info.p)*/;
+	return curtok.type;
+}
+
+void
+ungettok()
+{
+	storage = curtok;
+	isunget = 1;
+}
+
+int
+_exptok(enum tok_t first, ...)
+{
+	enum tok_t next;
+	va_list ap;
+	int res;
+
+	if (gettok() == TEOF)
+		return 0;
+	if (curtok.type == first)
+		return 1;
+
+	res = 0;
+	va_start(ap, first);
+	while ((next = va_arg(ap, enum tok_t)) != TEOF)
+		if (curtok.type == next)
+		{
+			res = 1;
+			break;
+		}
+	va_end(ap);
+
+	return res;
+}
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..56a094c
--- /dev/null
+++ b/main.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+
+FILE *curfile;
+char *curname, *progname;
+
+void
+usage(char *name)
+{
+	fprintf(stderr, "usage: %s <file>\n", name);
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct scope scope = {&basescope, NULL, 0};
+	struct node *stmt;
+	struct val val;
+
+	progname = *argv;
+
+	if (!argv[1])
+		usage(*argv);
+
+	curname = argv[1];
+	curfile = fopen(curname, "r");
+	curline = 1;
+
+	if (!curfile)
+		dieno(1, "fopen(`%s')", argv[1]);
+
+	while ((stmt = getstmt()))
+	{
+		val = eval(stmt, &scope);
+		if (val.type == VBREAK || val.type == VCONT)
+			complain(1, "cannot %s out of nothing", valname(val.type));
+	}
+	showverts();
+
+	return 0;
+}
diff --git a/parse.c b/parse.c
new file mode 100644
index 0000000..b334db1
--- /dev/null
+++ b/parse.c
@@ -0,0 +1,637 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+
+
+/*
+ * expr = expr-assign
+ */
+#define pexpr pexpr_assign
+static struct node *pexpr(void);
+static struct node *pstmt(void);
+
+static enum noper_t
+associate(enum tok_t t)
+{
+	switch (t)
+	{
+	case TKEYLEN:    return OLEN;
+	case TKEYASSERT: return OASSERT;
+	case TARRLEFT:   return OCONNECT;
+	case TCONCAT:    return OCONCAT;
+	case TAMPER:     return OBAND;
+	case TPIPE:      return OBOR;
+	case TCARET:     return OBXOR;
+	case TLSHIFT:    return OLSHIFT;
+	case TRSHIFT:    return ORSHIFT;
+	case TTILDE:     return OINV;
+	case TAND:       return OAND;
+	case TEXCLAM:    return ONOT;
+	case TOR:        return OOR;
+	case TEQ:        return OEQ;
+	case TNEQ:       return ONEQ;
+	case TLESS:      return OLESS;
+	case TLESSEQ:    return OLESSEQ;
+	case TGREAT:     return OGREAT;
+	case TGREATEQ:   return OGREATEQ;
+	case TPLUS:      return OSUM;
+	case TMINUS:     return OSUB;
+	case TASTER:     return OMUL;
+	case TSLASH:     return ODIV;
+	case TPERC:      return OMOD;
+	case TPOW:       return OPOW;
+	case TASSAMPER:  return OASSBAND;
+	case TASSASTER:  return OASSMUL;
+	case TASSCARET:  return OASSBXOR;
+	case TASSIGN:    return OASSIGN;
+	case TASSLSHIFT: return OASSLSHIFT;
+	case TASSMINUS:  return OASSSUB;
+	case TASSPERC:   return OASSMOD;
+	case TASSPIPE:   return OASSBOR;
+	case TASSPLUS:   return OASSSUM;
+	case TASSRSHIFT: return OASSRSHIFT;
+	case TASSSLASH:  return OASSDIV;
+	default: complain(1, "%s cannot be associated", tokname(t));
+	}
+
+	/* not reached */
+	return 0;
+}
+
+static struct node *
+mknode(enum node_t t)
+{
+	struct node *res = malloc(sizeof(*res));
+	if (!res)
+		dieno(1, "malloc()");
+	res->type = t;
+	return res;
+}
+
+#define BINLEFT(NAME, NEXT, ...) static struct node *NAME(void) \
+{ \
+	struct node *child, *node; \
+	if (!(child = NEXT())) \
+		return NULL; \
+loop: \
+	if (!exptok(__VA_ARGS__)) \
+	{ \
+		ungettok(); \
+		return child; \
+	} \
+	node = mknode(NOPER); \
+	node->noper.type = associate(curtok.type); \
+	node->noper.l = child; \
+	if (!(node->noper.r = NEXT())) \
+		complain(1, "no right side for %s", nopername(node->noper.type)); \
+	child = node; \
+	goto loop; \
+}
+
+#define BINRIGHT(NAME, NEXT, ...) static struct node *NAME(void) \
+{ \
+	struct node *child, *node; \
+	if (!(child = NEXT())) \
+		return NULL; \
+	if (!exptok(__VA_ARGS__)) \
+	{ \
+		ungettok(); \
+		return child; \
+	} \
+	node = mknode(NOPER); \
+	node->noper.type = associate(curtok.type); \
+	node->noper.l = child; \
+	if (!(node->noper.r = NAME())) \
+		complain(1, "no right side for %s", nopername(node->noper.type)); \
+	return node; \
+}
+
+/*
+ * comp = "{" *(stmt) "}"
+ */
+static struct node *
+pcomp(void)
+{
+	struct node *node;
+	ulong i = 0;
+
+	if (!exptok(TOPBRACE))
+		return NULL;
+	node = mknode(NCOMP);
+	node->ncomp.stmts = NULL;
+
+loop:
+	/* TODO: add cap and increase like in lex.c */
+	node->ncomp.stmts = realloc(
+		node->ncomp.stmts,
+		sizeof(struct node *) * (i + 1)
+	);
+
+	switch (gettok())
+	{
+	break; case TEOF: complain(1, "no closing brace");
+	break; case TCLBRACE: goto end;
+	break; default:
+		ungettok();
+		if (!(node->ncomp.stmts[i] = pstmt()))
+			complain(1, "no statement");
+	}
+	++i;
+	goto loop;
+end:
+	node->ncomp.len = i;
+	return node;
+}
+
+/*
+ * expr-primary = NUMBER / id / "nil" / expr-node / expr-mod / "(" expr ")"
+ * expr-node = "node" "(" [*(expr ",") expr] ")"
+ * expr-mod = "mod" "(" [*(id ",") id] ")" comp
+ */
+static struct node *
+pexpr_primary(void)
+{
+	struct node *node = NULL;
+	ulong i = 0;
+
+	switch (gettok())
+	{
+	break; case TEOF: return NULL;
+	break; case TNUM:
+		node = mknode(NNUM);
+		node->nnum = atol(curtok.info.p);
+	break; case TID:
+		node = mknode(NVAR);
+		node->nvar = curtok.info.p;
+	break; case TOPPAREN:
+		if (!(node = pexpr()))
+			complain(1, "empty paretheses");
+		if (!exptok(TCLPAREN))
+			complain(1, "no closing parenthesis");
+	break; case TKEYNODE:
+		node = mknode(NNODE);
+		node->nnode.params = NULL;
+
+		if (!exptok(TOPPAREN))
+			complain(1, "no parentheses after node");
+nodeloop:
+		/* TODO: add cap and increase like in lex.c */
+		node->nnode.params = realloc(
+			node->nnode.params,
+			sizeof(struct node *) * (i + 1)
+		);
+		if (!node->nnode.params)
+			dieno(1, "realloc()");
+
+		switch(gettok())
+		{
+		break; case TEOF: complain(1, "no closing parenthesis");
+		break; case TCLPAREN: goto nodeend;
+		break; case TCOMMA:
+			if (!i || !(node->nnode.params[i] = pexpr()))
+				complain(1, "empty param in node");
+		break; default:
+			ungettok();
+			if (i)
+				complain(1, "no comma");
+			else if (!(node->nnode.params[i] = pexpr()))
+				complain(1, "empty param in node");
+		}
+		++i;
+		goto nodeloop;
+nodeend:
+		node->nnode.len = i;
+	break; case TKEYMOD:
+		node = mknode(NMOD);
+		node->nmod.params = NULL;
+
+		if (!exptok(TOPPAREN))
+			complain(1, "no parentheses after mod");
+modloop:
+		/* TODO: add cap and increase like in lex.c */
+		node->nmod.params = realloc(
+			node->nmod.params,
+			sizeof(char *) * (i + 1)
+		);
+		if (!node->nmod.params)
+			dieno(1, "realloc()");
+
+		switch(gettok())
+		{
+		break; case TEOF: complain(1, "no closing parenthesis");
+		break; case TCLPAREN: goto modend;
+		break; case TCOMMA:
+			if (!i || gettok() != TID)
+				complain(1, "expected id as a param name");
+			else
+				node->nmod.params[i] = curtok.info.p;
+		break; default:
+			ungettok();
+			if (i)
+				complain(1, "no comma");
+			else if (gettok() != TID)
+				complain(1, "expected id as a param name");
+			else
+				node->nmod.params[i] = curtok.info.p;
+		}
+		++i;
+		goto modloop;
+modend:
+		node->nmod.len = i;
+
+		if (!(node->nmod.code = pcomp()))
+			complain(1, "no comp for mod");
+	break; default:
+		ungettok();
+	}
+
+	return node;
+}
+
+/*
+ * expr-post = expr-primary *("--" / "++" / "[" expr [":" expr] "]")
+ */
+static struct node *
+pexpr_post(void)
+{
+	struct node *child, *node;
+
+	child = pexpr_primary();
+	if (!child)
+		return NULL;
+
+loop:
+	switch (gettok())
+	{
+	break; case TEOF: return child;
+	break; case TDECR: case TINCR:
+		node = mknode(NOPER);
+		node->noper.type = curtok.type == TDECR ? OPOSTDECR : OPOSTINCR;
+		node->noper.m = child;
+	break; case TOPBRACK:
+		node = mknode(NOPER);
+		node->noper.type = OINDEX;
+		node->noper.l = child;
+
+		if (!(node->noper.r = pexpr()))
+			complain(1, "no inside for []");
+		switch (gettok())
+		{
+		break; case TEOF: complain(1, "no closing bracket");
+		break; case TCOL:
+			node->noper.type = OSLICE;
+			node->noper.m = node->noper.r;
+			if (!(node->noper.r = pexpr()))
+				complain(1, "no right side for []");
+			if (!exptok(TCLBRACK))
+				complain(1, "invalid syntax for []");
+		/*FT*/ case TCLBRACK:
+		break; default: complain(1, "invalid syntax for []");
+		}
+	break; default:
+		ungettok();
+		return child;
+	}
+
+	child = node;
+	goto loop;
+}
+
+/*
+ * expr-pref = expr-post / ("assert" / "len" / "~" / "!" / "--" / "++") expr-pref
+ */
+static struct node *
+pexpr_pref(void)
+{
+	struct node *node;
+
+	switch (gettok())
+	{
+	break; case TEOF: return NULL;
+	break; case TKEYASSERT: case TKEYLEN: case TTILDE:
+	case TEXCLAM: case TDECR: case TINCR:
+		node = mknode(NOPER);
+		node->noper.type = curtok.type == TDECR ? OPREDECR :
+			   curtok.type == TINCR ? OPREINCR :
+			   associate(curtok.type);
+		if (!(node->noper.m = pexpr_pref()))
+			complain(1, "no operand for %s", nopername(node->noper.type));
+	break; case TOPBRACK:
+		node = mknode(NOPER);
+		node->noper.type = OARRAY;
+
+		if (!(node->noper.l = pexpr()))
+			complain(1, "no inside for []");
+		if (!exptok(TCLBRACK))
+			complain(1, "invalid syntax for []");
+		if (!(node->noper.r = pexpr_pref()))
+			complain(1, "no operand for []");
+	break; default:
+		ungettok();
+		return pexpr_post();
+	}
+
+	return node;
+}
+
+/* expr-pow   = expr-pref ["**" expr-pow] */
+BINRIGHT(pexpr_pow, pexpr_pref, TPOW)
+/* expr-mult  = expr-pow *(("*" / "><" / "%" / "/") expr-pow) */
+BINLEFT(pexpr_mult, pexpr_pow, TASTER, TCONCAT, TPERC, TSLASH)
+/* expr-add   = expr-mult *(("<-" / "-" / "+") expr-mult) */
+BINLEFT(pexpr_add, pexpr_mult, TARRLEFT, TMINUS, TPLUS)
+/* expr-shift = expr-add *(("<<" / ">>") expr-add) */
+BINLEFT(pexpr_shift, pexpr_add, TLSHIFT, TRSHIFT)
+/* expr-rel   = expr-shift *(("<" / "<=" / ">" / ">=") expr-shift) */
+BINLEFT(pexpr_rel, pexpr_shift, TLESS, TLESSEQ, TGREAT, TGREATEQ)
+/* expr-eq    = expr-rel *(("==" / "!=") expr-rel) */
+BINLEFT(pexpr_eq, pexpr_rel, TEQ, TNEQ)
+/* expr-band  = expr-eq *("&" expr-eq) */
+BINLEFT(pexpr_band, pexpr_eq, TAMPER)
+/* expr-bxor  = expr-band *("^" expr-band) */
+BINLEFT(pexpr_bxor, pexpr_band, TCARET)
+/* expr-bor   = expr-bxor *("|" expr-bxor) */
+BINLEFT(pexpr_bor, pexpr_bxor, TPIPE)
+/* expr-and   = expr-bor *("&&" expr-bor) */
+BINLEFT(pexpr_and, pexpr_bor, TAND)
+/* expr-or    = expr-and *("||" expr-and) */
+BINLEFT(pexpr_or, pexpr_and, TOR)
+
+/*
+ * expr-cond = expr-or ? expr : expr-cond
+ */
+static struct node *
+pexpr_cond(void)
+{
+	struct node *child, *node;
+
+	child = pexpr_or();
+	if (!child)
+		return NULL;
+
+	if (!exptok(TQUEST))
+	{
+		ungettok();
+		return child;
+	}
+
+	node = mknode(NOPER);
+	node->noper.type = OCOND;
+	node->noper.l = child;
+
+	if (!(node->noper.m = pexpr()))
+		complain(1, "no then branch in ternary if");
+
+	if (!exptok(TCOL))
+		complain(1, "no colon for ternary if");
+
+	if (!(node->noper.r = pexpr_cond()))
+		complain(1, "no else branch for ternary if");
+
+	return node;
+}
+
+/*
+ * expr-assign      =  expr-cond [expr-assign-oper expr-assign]
+ * expr-assign-oper =  "+=" / "-=" / "*=" / "/=" / "%=" / "&="
+ * expr-assign-oper =/ "|=" / "^=" / "=" / ">>=" / "<<=" / "%="
+ */
+BINRIGHT(
+	pexpr_assign, pexpr_cond,
+	TASSAMPER, TASSASTER, TASSCARET, TASSIGN, TASSLSHIFT,
+	TASSMINUS, TASSPERC, TASSPIPE, TASSPLUS, TASSRSHIFT,
+	TASSSLASH
+)
+
+/*
+ * for = "for" "(" [expr] ";" [expr] ";" [expr] ")" stmt
+ */
+struct node *
+pfor(void)
+{
+	struct node *node;
+
+	if (!exptok(TKEYFOR))
+	{
+		say("%s:%d: pfor selected but no TKEYFOR?", __FILE__, __LINE__);
+		ungettok();
+		return NULL;
+	}
+
+	node = mknode(NFOR);
+
+	if (!exptok(TOPPAREN))
+		complain(1, "no parentheses after for");
+	node->nfor.before = pexpr();
+	if (!exptok(TSEMICOL))
+		complain(1, "invalid for parameters");
+	node->nfor.cond = pexpr();
+	if (!exptok(TSEMICOL))
+		complain(1, "invalid for parameters");
+	node->nfor.between = pexpr();
+	if (!exptok(TCLPAREN))
+		complain(1, "invalid for parameters");
+	if (!(node->nfor.code = pstmt()))
+		complain(1, "no statement for for");
+
+	return node;
+}
+
+/*
+ * foreach = "foreach" "(" expr ")" stmt
+ */
+struct node *
+pforeach(void)
+{
+	struct node *node;
+
+	if (!exptok(TKEYFOREACH))
+	{
+		say("%s:%d: pforeach selected but no TKEYFOREACH?", __FILE__, __LINE__);
+		ungettok();
+		return NULL;
+	}
+
+	node = mknode(NFOREACH);
+
+	if (!exptok(TOPPAREN)
+	 || !(node->nforeach.obj = pexpr())
+	 || !exptok(TCLPAREN))
+		complain(1, "invalid foreach parameter");
+	if (!(node->nforeach.code = pstmt()))
+		complain(1, "no statement for foreach");
+
+	return node;
+}
+
+/*
+ * if = "if" "(" expr ")" stmt ["else" stmt]
+ */
+struct node *
+pif(void)
+{
+	struct node *node;
+
+	if (!exptok(TKEYIF))
+	{
+		say("%s:%d: pif selected but no TKEYIF?", __FILE__, __LINE__);
+		ungettok();
+		return NULL;
+	}
+
+	node = mknode(NCOND);
+
+	if (!exptok(TOPPAREN)
+	 || !(node->ncond.cond = pexpr())
+	 || !exptok(TCLPAREN))
+		complain(1, "invalid if parameter");
+	if (!(node->ncond.t = pstmt()))
+		complain(1, "no statement for then if branch");
+
+	node->ncond.f = NULL;
+	if (exptok(TKEYELSE))
+		if (!(node->ncond.f = pstmt()))
+			complain(1, "no statement for else if branch");
+		else;
+	else
+		ungettok();
+
+	return node;
+}
+
+/*
+ * with = "with" expr "(" [*(expr ",") expr] ")" stmt
+ */
+struct node *
+pwith(void)
+{
+	struct node *node;
+	ulong i = 0;
+
+	if (!exptok(TKEYWITH))
+	{
+		say("%s:%d: pwith selected but no TKEYWITH?", __FILE__, __LINE__);
+		ungettok();
+		return NULL;
+	}
+
+	node = mknode(NWITH);
+	node->nwith.args = NULL;
+
+	if (!(node->nwith.mod = pexpr()))
+		complain(1, "no module specified");
+	if (!exptok(TOPPAREN))
+		complain(1, "no parameters in with");
+loop:
+	/* TODO: add cap and increase like in lex.c */
+	node->nwith.args = realloc(
+		node->nwith.args,
+		sizeof(struct nnode *) * (i + 1)
+	);
+	if (!node->nwith.args)
+		dieno(1, "realloc()");
+
+	switch(gettok())
+	{
+	break; case TEOF: complain(1, "no closing parenthesis");
+	break; case TCLPAREN: goto end;
+	break; case TCOMMA:
+		if (!i || !(node->nwith.args[i] = pexpr()))
+			complain(1, "empty param in with");
+	break; default:
+		ungettok();
+		if (i)
+			complain(1, "no comma");
+		else if (!(node->nwith.args[i] = pexpr()))
+			complain(1, "empty param in with");
+	}
+	++i;
+	goto loop;
+end:
+	if (!(node->nwith.code = pstmt()))
+		complain(1, "no statement for with");
+	node->nwith.len = i;
+
+	return node;
+}
+
+/*
+ * break = "break" ";"
+ * continue = "continue" ";"
+ */
+struct node *
+pbreak(void)
+{
+	struct node *node;
+	enum tok_t t;
+
+	if (!exptok(TKEYBREAK, TKEYCONT))
+	{
+		say("%s:%d: break selected but no break/continue?", __FILE__, __LINE__);
+		ungettok();
+		return NULL;
+	}
+
+	node = mknode(NWITH);
+	node->type = (t = curtok.type) == TKEYBREAK ? NBREAK : NCONT;
+
+	if (!exptok(TSEMICOL))
+		complain(1, "expected `;' after %s", tokname(t));
+
+	return node;
+}
+
+/*
+ * stmt = comp / for / foreach / if / with / expr ";" / break / continue
+ */
+struct node *
+pstmt(void)
+{
+	struct node *node;
+
+	if (gettok() == TEOF)
+		return NULL;
+	ungettok();
+
+	switch (curtok.type)
+	{
+	break; case TOPBRACE:    return pcomp();
+	break; case TKEYFOR:     return pfor();
+	break; case TKEYFOREACH: return pforeach();
+	break; case TKEYIF:      return pif();
+	break; case TKEYWITH:    return pwith();
+	break; case TKEYBREAK:
+	/*FT*/ case TKEYCONT:    return pbreak();
+	break; default:
+		node = pexpr();
+		if (!exptok(TSEMICOL))
+			complain(1, "expected `;' at the end of an expression");
+		return node ? node : pstmt();
+	}
+
+	/* not reached */
+	return NULL;
+}
+
+struct node *
+getstmt(void)
+{
+	return pstmt();
+}
diff --git a/say.c b/say.c
new file mode 100644
index 0000000..5a0c587
--- /dev/null
+++ b/say.c
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+void vsay(char *fmt, va_list ap)
+{
+	fprintf(stderr, "%s:%ld: ", curname, curline);
+	vfprintf(stderr, fmt, ap);
+	fputc('\n', stderr);
+}
+
+void
+say(char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsay(fmt, ap);
+	va_end(ap);
+}
+
+void
+complain(int code, char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsay(fmt, ap);
+	va_end(ap);
+
+	exit(code);
+}
+
+void
+die(int code, char *fmt, ...)
+{
+	va_list ap;
+
+	fputs(progname, stderr);
+	if (fmt)
+	{
+		fputs(": ", stderr);
+		va_start(ap, fmt);
+		vfprintf(stderr, fmt, ap);
+		va_end(ap);
+	}
+	fputc('\n', stderr);
+
+	exit(code);
+}
+
+void
+dieno(int code, char *fmt, ...)
+{
+	va_list ap;
+
+	fputs(progname, stderr);
+	if (fmt)
+	{
+		fputs(": ", stderr);
+		va_start(ap, fmt);
+		vfprintf(stderr, fmt, ap);
+		va_end(ap);
+	}
+	fprintf(stderr, ": %s\n", strerror(errno));
+
+	exit(code);
+}
diff --git a/str.c b/str.c
new file mode 100644
index 0000000..c8ed336
--- /dev/null
+++ b/str.c
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <stddef.h>
+
+
+#define STRGEN(X) case X: return #X;
+#define STRNAME(NAME, TYPES) char *NAME##name(enum NAME##_t t) \
+{ \
+	switch (t) {TYPES(STRGEN)} \
+	/* not reached */ \
+	return NULL; \
+}
+
+STRNAME(tok, TOKTYPES);
+STRNAME(node, NODETYPES);
+STRNAME(val, VALTYPES);
+STRNAME(noper, NOPERTYPES);
diff --git a/thac.h b/thac.h
new file mode 100644
index 0000000..37dd9f5
--- /dev/null
+++ b/thac.h
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __THAC_H__
+#define __THAC_H__
+
+#define _POSIX_SOURCE
+
+#include <stdio.h>
+
+
+enum
+{
+	GENVARLEN = 16,
+};
+
+typedef unsigned long ulong;
+typedef long long vlong;
+
+#define lengthof(X) (sizeof(X)/sizeof(*(X)))
+#define max(X, Y) ((X) > (Y) ? (X) : (Y))
+#define min(X, Y) ((X) < (Y) ? (X) : (Y))
+
+#define ENUMGEN(X) X,
+
+#define TOKTYPES(X) \
+	X(TEOF) \
+	X(TAMPER) /* & */ \
+	X(TAND) /* && */ \
+	X(TARRLEFT) /* <- */ \
+	X(TASSAMPER) /* &= */ \
+	X(TASSASTER) /* *= */ \
+	X(TASSCARET) /* ^= */ \
+	X(TASSIGN) /* = */ \
+	X(TASSLSHIFT) /* <<= */ \
+	X(TASSMINUS) /* -= */ \
+	X(TASSPERC) /* %= */ \
+	X(TASSPIPE) /* |= */ \
+	X(TASSPLUS) /* += */ \
+	X(TASSRSHIFT) /* >>= */ \
+	X(TASSSLASH) /* /= */ \
+	X(TASTER) /* * */ \
+	X(TCARET) /* ^ */ \
+	X(TCLBRACE) /* } */ \
+	X(TCLBRACK) /* ] */ \
+	X(TCLPAREN) /* ) */ \
+	X(TCOL) /* : */ \
+	X(TCOMMA) /* , */ \
+	X(TCONCAT) /* >< */ \
+	X(TDECR) /* -- */ \
+	X(TEQ) /* == */ \
+	X(TEXCLAM) /* ! */ \
+	X(TID) /* <identifier> */ \
+	X(TINCR) /* ++ */ \
+	X(TKEYASSERT) /* assert */ \
+	X(TKEYBREAK) /* break */ \
+	X(TKEYCONT) /* continue */ \
+	X(TKEYELSE) /* else */ \
+	X(TKEYFOR) /* for */ \
+	X(TKEYFOREACH) /* foreach */ \
+	X(TKEYIF) /* if */ \
+	X(TKEYLEN) /* len */ \
+	X(TKEYMOD) /* mod */ \
+	X(TKEYNODE) /* node */ \
+	X(TKEYWITH) /* with */ \
+	X(TLESS) /* < */ \
+	X(TLESSEQ) /* <= */ \
+	X(TLSHIFT) /* << */ \
+	X(TMINUS) /* - */ \
+	X(TGREAT) /* > */ \
+	X(TGREATEQ) /* >= */ \
+	X(TNEQ) /* != */ \
+	X(TNUM) /* <number> */ \
+	X(TOPBRACE) /* { */ \
+	X(TOPBRACK) /* [ */ \
+	X(TOPPAREN) /* ( */ \
+	X(TOR) /* || */ \
+	X(TPERC) /* % */ \
+	X(TPIPE) /* | */ \
+	X(TPLUS) /* + */ \
+	X(TPOW) /* ** */ \
+	X(TQUEST) /* ? */ \
+	X(TRSHIFT) /* >> */ \
+	X(TSEMICOL) /* ; */ \
+	X(TSLASH) /* / */ \
+	X(TTILDE) /* ~ */ \
+	/* TOKTYPES */
+
+#define NODETYPES(X) \
+	X(NCOMP) /* compound statement */ \
+	X(NCOND) /* if statement */ \
+	X(NFOR) /* for statement */ \
+	X(NFOREACH) /* for statement */ \
+	X(NMOD) /* module expression */ \
+	X(NNUM) /* number */ \
+	X(NNODE) /* node expression */ \
+	X(NOPER) /* (unary (m) / binary (l r) / ternary (l m r)) operator */ \
+	X(NWITH) /* module invocation */ \
+	X(NVAR) /* variable reference */ \
+	X(NBREAK) /* break statement to exit the loop */ \
+	X(NCONT) /* continue statement to go to next loop iteration */ \
+	/* NODETYPES */
+
+#define NOPERTYPES(X) \
+	X(OLEN) \
+	X(OASSERT) \
+	X(OCONNECT) \
+	X(OINDEX) \
+	X(OSLICE) \
+	X(OARRAY) \
+	X(OCONCAT) \
+	/* binary logic */ \
+	X(OBAND) \
+	X(OBOR) \
+	X(OBXOR) \
+	X(OLSHIFT) \
+	X(ORSHIFT) \
+	X(OINV) \
+	/* logic */ \
+	X(OAND) \
+	X(ONOT) \
+	X(OOR) \
+	X(OCOND) \
+	/* relations */ \
+	X(OEQ) \
+	X(ONEQ) \
+	X(OLESS) \
+	X(OLESSEQ) \
+	X(OGREAT) \
+	X(OGREATEQ) \
+	/* algebraic */ \
+	X(OPOSTDECR) \
+	X(OPOSTINCR) \
+	X(OPREDECR) \
+	X(OPREINCR) \
+	X(OSUM) \
+	X(OSUB) \
+	X(OMUL) \
+	X(ODIV) \
+	X(OMOD) \
+	X(OPOW) \
+	/* assignment */ \
+	X(OASSBAND) \
+	X(OASSMUL) \
+	X(OASSBXOR) \
+	X(OASSIGN) \
+	X(OASSLSHIFT) \
+	X(OASSSUB) \
+	X(OASSMOD) \
+	X(OASSBOR) \
+	X(OASSSUM) \
+	X(OASSRSHIFT) \
+	X(OASSDIV) \
+	/* NOPERTYPES */
+
+#define VALTYPES(X) \
+	X(VNIL) \
+	X(VBREAK) \
+	X(VCONT) \
+	X(VNUM) \
+	X(VVAR) \
+	X(VARR) \
+	/* VALTYPES */
+
+struct tok
+{
+	enum tok_t {TOKTYPES(ENUMGEN)} type;
+	struct tinfo
+	{
+		char *p;
+		ulong len, cap;
+	} info;
+};
+
+struct node
+{
+	enum node_t {NODETYPES(ENUMGEN)} type;
+	union
+	{
+		struct ncomp
+		{
+			struct node **stmts;
+			ulong len;
+		} ncomp;
+		struct ncond
+		{
+			struct node *cond, *t, *f;
+		} ncond;
+		struct nfor
+		{
+			struct node *before, *cond, *between, *code;
+		} nfor;
+		struct nforeach
+		{
+			struct node *obj, *code;
+		} nforeach;
+		struct nmod
+		{
+			struct node *code;
+			char **params;
+			ulong len;
+		} nmod;
+		vlong nnum;
+		struct nnode
+		{
+			struct node **params;
+			ulong len;
+		} nnode;
+		struct noper
+		{
+			enum noper_t {NOPERTYPES(ENUMGEN)} type;
+			struct node *l, *m, *r;
+		} noper;
+		struct nwith
+		{
+			struct node *mod, **args, *code;
+			ulong len;
+		} nwith;
+		char *nvar;
+	};
+};
+
+struct val
+{
+	enum val_t {VALTYPES(ENUMGEN)} type;
+	union
+	{
+		char *name; /* VVAR */
+		vlong num; /* VNUM */
+		struct /* VARR; */
+		{
+			struct val *arr;
+			ulong len;
+		};
+	};
+};
+
+struct scope
+{
+	struct scope *parent;
+	struct var
+	{
+		char *name;
+		struct val val;
+	} *vars;
+	ulong len;
+};
+
+struct vertex
+{
+	struct
+	{
+		vlong *arr;
+		ulong len;
+	} props;
+	struct
+	{
+		ulong *arr;
+		ulong len;
+	} nbrs;
+};
+
+extern FILE *curfile;
+extern char *curname, *progname;
+extern ulong curline;
+
+extern struct scope basescope;
+extern struct tok curtok;
+extern struct val nil;
+
+void complain(int code, char *, ...);
+void say(char *, ...);
+void die(int code, char *, ...);
+void dieno(int code, char *, ...);
+
+enum tok_t gettok(void);
+void ungettok(void);
+#define exptok(...) _exptok(__VA_ARGS__, TEOF)
+int _exptok(enum tok_t, ...);
+
+struct node *getstmt(void);
+struct val eval(struct node *, struct scope *);
+
+long findvar(char *, struct scope **);
+ulong assignvar(char *, struct val, struct scope **);
+int varnextchar(struct scope *);
+ulong varnextnum(struct scope *);
+void delvarscope(struct scope *, ulong);
+
+struct val deref(struct val, struct scope *);
+struct val getval(enum val_t, struct val, struct scope *, int);
+struct val copyval(struct val old);
+void freeval(struct val);
+struct val connectval(struct val, struct val, struct scope *);
+
+ulong mkvert(void);
+void addprop(ulong, vlong);
+void addedge(ulong, ulong);
+void showverts(void);
+
+void walkval(struct val *, ulong);
+void walknode(struct node *, ulong);
+void walkscope(struct scope *, ulong);
+
+char *tokname(enum tok_t);
+char *nodename(enum node_t);
+char *valname(enum val_t);
+char *nopername(enum noper_t);
+
+#endif /* __THAC_H__ */
diff --git a/val.c b/val.c
new file mode 100644
index 0000000..b8dc30d
--- /dev/null
+++ b/val.c
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+
+struct val
+deref(struct val val, struct scope *scope)
+{
+	long i;
+
+	if (val.type == VVAR)
+	{
+		i = findvar(val.name, &scope);
+		if (i < 0)
+			complain(1, "no such variable: %s", val.name);
+		val = scope->vars[i].val;
+	}
+
+	return val;
+}
+
+struct val
+getval(enum val_t t, struct val val, struct scope *scope, int doderef)
+{
+	if (doderef)
+		val = deref(val, scope);
+	if (val.type != t)
+		complain(1, "expected %s, got %s", valname(t), valname(val.type));
+	return val;
+}
+
+struct val
+copyval(struct val old)
+{
+	struct val new;
+	ulong i;
+
+	new = old;
+	if (new.type == VARR)
+	{
+		new.arr = malloc(sizeof(*new.arr) * new.len);
+		if (!new.arr)
+			dieno(1, "malloc()");
+
+		for (i = 0; i < new.len; ++i)
+			new.arr[i] = copyval(old.arr[i]);
+	}
+
+	return new;
+}
+
+void
+freeval(struct val val)
+{
+	ulong i;
+
+	if (val.type == VARR)
+	{
+		for (i = 0; i < val.len; ++i)
+			freeval(val.arr[i]);
+		free(val.arr);
+	}
+}
+
+static void
+connectarr(struct val arr, ulong single, int arr_to_single)
+{
+	ulong i;
+
+	for (i = 0; i < arr.len; ++i) switch (arr.arr[i].type)
+	{
+	break; case VARR:
+		connectarr(arr.arr[i], single, arr_to_single);
+	break; case VNUM:
+		if (arr_to_single)
+			addedge(arr.arr[i].num, single);
+		else
+			addedge(single, arr.arr[i].num);
+	break; default: complain(1, "%s can not be connected", valname(arr.arr[i].type));
+	}
+}
+
+static void
+connectarrarr(struct val from, struct val to)
+{
+	ulong i;
+
+	if (from.len != to.len)
+		complain(1, "cannot connect nonisomorphic arrays");
+
+	for (i = 0; i < from.len; ++i)
+	{
+		if (from.arr[i].type != to.arr[i].type)
+			complain(1, "cannot connect nonisomorphic arrays");
+		switch (from.arr[i].type)
+		{
+		break; case VARR: connectarrarr(from.arr[i], to.arr[i]);
+		break; case VNUM: addedge(from.arr[i].num, to.arr[i].num);
+		break; default: complain(1, "%s can not be connected", valname(from.arr[i].type));
+		}
+	}
+}
+
+struct val
+connectval(struct val refl, struct val refr, struct scope *scope)
+{
+	struct val l, r;
+
+	l = deref(refl, scope);
+	r = deref(refr, scope);
+
+	if (l.type == VNUM && r.type == VNUM)
+		addedge(r.num, l.num);
+	else if (l.type == VARR && r.type == VNUM)
+		connectarr(l, r.num, 0);
+	else if (l.type == VNUM && r.type == VARR)
+		connectarr(r, l.num, 1);
+	else if (l.type == VARR && r.type == VARR)
+		connectarrarr(r, l);
+	else
+		complain(1, "cannot connect %s and %s", valname(l.type), valname(r.type));
+
+	if (refr.type != VVAR)
+		freeval(r);
+
+	return refl;
+}
diff --git a/var.c b/var.c
new file mode 100644
index 0000000..658f00b
--- /dev/null
+++ b/var.c
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "thac.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+long
+findvar(char *name, struct scope **scope)
+{
+	struct var *var;
+	ulong i;
+
+	for (; *scope; *scope = (*scope)->parent) for (i = 0; i < (*scope)->len; ++i)
+	{
+		var = &(*scope)->vars[i];
+		if (!strcmp(var->name, name))
+			return i;
+	}
+
+	return -1;
+}
+
+ulong
+assignvar(char *name, struct val val, struct scope **scope)
+{
+	struct scope *lscope;
+	long i;
+
+	lscope = *scope;
+	i = findvar(name, &lscope);
+	if (i < 0)
+	{
+		/* TODO: add cap and increment like in lex.c */
+		(*scope)->vars = realloc(
+			(*scope)->vars,
+			sizeof(*(*scope)->vars) * ++(*scope)->len
+		);
+		if (!(*scope)->vars)
+			dieno(1, "realloc()");
+		i = (*scope)->len - 1;
+	} else
+	{
+		scope = &lscope;
+	}
+	(*scope)->vars[i].name = name;
+	(*scope)->vars[i].val = val;
+	return i;
+}
+
+int
+varnextchar(struct scope *scope)
+{
+	struct var *var;
+	ulong i;
+	int c;
+
+	for (c = -1; scope; scope = scope->parent) for (i = 0; i < scope->len; ++i)
+	{
+		var = &scope->vars[i];
+		if (var->name[0] == '@' && islower(var->name[1]))
+			c = max(c, var->name[1]);
+
+		if (c == -1)
+			continue;
+
+		if (c == 'z')
+			return -1;
+		return var->name[1] + 1;
+	}
+
+	return 'a';
+}
+
+ulong
+varnextnum(struct scope *scope)
+{
+	struct var *var;
+	ulong i;
+
+	for (; scope; scope = scope->parent) for (i = 0; i < scope->len; ++i)
+	{
+		var = &scope->vars[i];
+		if (var->name[0] == '@' && isdigit(var->name[1]))
+			return atoll(&var->name[1]) + 1;
+	}
+
+	return 0;
+}
+
+void
+delvarscope(struct scope *scope, ulong i)
+{
+	freeval(scope->vars[i].val);
+	scope->vars[i] = scope->vars[--scope->len];
+}
diff --git a/vertex.c b/vertex.c
new file mode 100644
index 0000000..b8290b1
--- /dev/null
+++ b/vertex.c
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+/*
+ * Unfortunately, the name "node" is already taken by the parser, so in
+ * this compiler language nodes are called vertices
+ */
+
+#include "thac.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+
+/* TODO: add cap and increase like in lex.c */
+static struct vertex *verts;
+static ulong len;
+
+ulong
+mkvert(void)
+{
+	static struct vertex vert = {{NULL, 0}, {NULL, 0}};
+
+	verts = realloc(verts, sizeof(*verts) * ++len);
+	if (!verts)
+		dieno(1, "realloc()");
+
+	verts[len - 1] = vert;
+
+	return len - 1;
+}
+
+void
+addprop(ulong i, vlong prop)
+{
+	if (i < 0 || i >= len)
+		complain(1, "addprop: how would you want this? (0<=%lld<%lld)", i, len);
+
+	verts[i].props.arr = realloc(
+		verts[i].props.arr,
+		sizeof(*verts[i].props.arr) * ++verts[i].props.len
+	);
+	if (!verts[i].props.arr)
+		dieno(1, "realloc()");
+
+	verts[i].props.arr[verts[i].props.len - 1] = prop;
+}
+
+void
+addedge(ulong from, ulong to)
+{
+	ulong i;
+
+	if (from < 0 || from >= len)
+		complain(1, "addedge: how would you want this? (0<=%lld<%lld)", from, len);
+	if (to < 0 || to >= len)
+		complain(1, "addedge: how would you want this? (0<=%lld<%lld)", from, len);
+
+	for (i = 0; i < verts[from].nbrs.len; ++i)
+		if (verts[from].nbrs.arr[i] == to)
+			return;
+
+	verts[from].nbrs.arr = realloc(
+		verts[from].nbrs.arr,
+		sizeof(*verts[from].nbrs.arr) * ++verts[from].nbrs.len
+	);
+	if (!verts[from].nbrs.arr)
+		dieno(1, "realloc()");
+
+	verts[from].nbrs.arr[verts[from].nbrs.len - 1] = to;
+}
+
+void
+showverts(void)
+{
+	ulong i, j;
+
+	for (i = 0; i < len; ++i)
+	{
+		for (j = 0; j < verts[i].props.len; ++j)
+			printf("%lld ", verts[i].props.arr[j]);
+		putchar('|');
+		for (j = 0; j < verts[i].nbrs.len; ++j)
+			printf(" %lu", verts[i].nbrs.arr[j]);
+		putchar('\n');
+	}
+}