diff --git a/src/Makefile b/src/Makefile
index 834a834c27d92b8362228c130fcb363af97af399..06da6c142512fb7edc2b2fa7637e328ccaa584f7 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -48,6 +48,7 @@ TESTS := t_benchmark.o \
 	 t_bitreader.o \
 	 t_bitwriter.o \
 	 t_parser.o \
+	 t_grammar.o \
 	 test_suite.o
 
 OUTPUTS := libhammer.a \
diff --git a/src/t_grammar.c b/src/t_grammar.c
new file mode 100644
index 0000000000000000000000000000000000000000..10a0853c7b75dfecb25c0cfa50ed8623b9b64b82
--- /dev/null
+++ b/src/t_grammar.c
@@ -0,0 +1,43 @@
+#include <glib.h>
+#include "hammer.h"
+#include "internal.h"
+#include "cfgrammar.h"
+#include "test_suite.h"
+
+static void test_end(void) {
+  const HParser *p = h_end_p();
+  HCFGrammar *g = h_cfgrammar(&system_allocator, p);
+
+  g_check_hashtable_size(g->nts, 1);
+  g_check_hashtable_size(g->geneps, 0);
+
+  g_check_derives_epsilon_not(g, p);
+}
+
+static void test_example_1(void) {
+  const HParser *c = h_many(h_ch('x'));
+  const HParser *q = h_sequence(c, h_ch('y'), NULL);
+  const HParser *p = h_choice(q, h_end_p(), NULL);
+  HCFGrammar *g = h_cfgrammar(&system_allocator, p);
+
+  g_check_nonterminal(g, c);
+  g_check_nonterminal(g, q);
+  g_check_nonterminal(g, p);
+
+  g_check_derives_epsilon(g, c);
+  g_check_derives_epsilon_not(g, q);
+  g_check_derives_epsilon_not(g, p);
+
+  g_check_firstset_present(g, p, end_token);
+  g_check_firstset_present(g, p, char_token('x'));
+  g_check_firstset_present(g, p, char_token('y'));
+
+  g_check_followset_absent(g, c, end_token);
+  g_check_followset_absent(g, c, char_token('x'));
+  g_check_followset_present(g, c, char_token('y'));
+}
+
+void register_grammar_tests(void) {
+  g_test_add_func("/core/grammar/end", test_end);
+  g_test_add_func("/core/grammar/example_1", test_example_1);
+}
diff --git a/src/test_suite.c b/src/test_suite.c
index 8d2913a580eb59a2eaf7ed03ed0e26200605640f..109c2e2f4ee16a498a926f377b15628c97fbff4b 100644
--- a/src/test_suite.c
+++ b/src/test_suite.c
@@ -22,6 +22,7 @@
 extern void register_bitreader_tests();
 extern void register_bitwriter_tests();
 extern void register_parser_tests();
+extern void register_grammar_tests();
 extern void register_benchmark_tests();
 
 int main(int argc, char** argv) {
@@ -31,6 +32,7 @@ int main(int argc, char** argv) {
   register_bitreader_tests();
   register_bitwriter_tests();
   register_parser_tests();
+  register_grammar_tests();
   register_benchmark_tests();
 
   g_test_run();
diff --git a/src/test_suite.h b/src/test_suite.h
index 24932bb4e370672f104e27ea8439889d3eba67b5..c4212b255b04228b899d70d95b11af82e895779a 100644
--- a/src/test_suite.h
+++ b/src/test_suite.h
@@ -88,6 +88,56 @@
     }									\
   } while(0)
 
+#define g_check_hashtable_present(table, key) do {			\
+    if(!h_hashtable_present(table, key)) {				\
+      g_test_message("Check failed: key should have been in table, but wasn't"); \
+      g_test_fail();							\
+    }									\
+  } while(0)
+
+#define g_check_hashtable_absent(table, key) do {			\
+    if(h_hashtable_present(table, key)) {				\
+      g_test_message("Check failed: key shouldn't have been in table, but was"); \
+      g_test_fail();							\
+    }									\
+  } while(0)
+
+#define g_check_hashtable_size(table, n) do {				\
+    size_t expected = n;						\
+    size_t actual = (table)->used;					\
+    if(actual != expected) {						\
+      g_test_message("Check failed: table size should have been %lu, but was %lu", \
+		     expected, actual);					\
+      g_test_fail();							\
+    }									\
+  } while(0)
+
+#define g_check_terminal(grammar, parser) \
+  g_check_hashtable_absent(grammar->nts, h_desugar(&system_allocator, parser))
+
+#define g_check_nonterminal(grammar, parser) \
+  g_check_hashtable_present(grammar->nts, h_desugar(&system_allocator, parser))
+
+#define g_check_derives_epsilon(grammar, parser) \
+  g_check_hashtable_present(grammar->geneps, h_desugar(&system_allocator, parser))
+
+#define g_check_derives_epsilon_not(grammar, parser) \
+  g_check_hashtable_absent(grammar->geneps, h_desugar(&system_allocator, parser))
+
+#define g_check_firstset_present(grammar, parser, token) \
+  g_check_hashtable_present(h_first_symbol(grammar, h_desugar(&system_allocator, parser)), (void *)token)
+
+#define g_check_firstset_absent(grammar, parser, token) \
+  g_check_hashtable_absent(h_first_symbol(grammar, h_desugar(&system_allocator, parser)), (void *)token)
+
+#define g_check_followset_present(grammar, parser, token) \
+  g_check_hashtable_present(h_follow(grammar, h_desugar(&system_allocator, parser)), (void *)token)
+
+#define g_check_followset_absent(grammar, parser, token) \
+  g_check_hashtable_absent(h_follow(grammar, h_desugar(&system_allocator, parser)), (void *)token)
+
+
+
 
 #define g_check_cmpint(n1, op, n2) g_check_inttype("%d", int, n1, op, n2)
 #define g_check_cmplong(n1, op, n2) g_check_inttype("%ld", long, n1, op, n2)