diff --git a/src/bindings/lua/hammer.lua b/src/bindings/lua/hammer.lua
index b3d975b9f9fde08f60a9438ac90f6c0eafdc40c4..2ee1656a098633801610a4ee181366d13dd69d10 100644
--- a/src/bindings/lua/hammer.lua
+++ b/src/bindings/lua/hammer.lua
@@ -176,20 +176,31 @@ counted_array = ffi.metatype("HCountedArray", arr_mt)
 local bytes_mt = {
   __call = function(self)
     local ret = ""
-    print(self.len)
-    for i = 0, tonumber(self.len)-1
+    for i = 0, tonumber(ffi.cast("uintptr_t", ffi.cast("void *", self.len)))-1
       do ret = ret .. string.char(self.token[i])
     end
     return ret
   end
 }
 local byte_string = ffi.metatype("HBytes", bytes_mt)
--- local parsed_token
--- local tok_mt = {
---   __call = function(self)
---   end
--- }
--- parsed_token = ffi.metatype("HParsedToken", tok_mt)
+
+local token_types = ffi.new("HTokenType")
+
+local parsed_token
+local tok_mt = {
+  __call = function(self)
+     if self.token_type == ffi.C.TT_BYTES then
+       return self.bytes()
+     elseif self.token_type == ffi.C.TT_SINT then
+       return tonumber(ffi.cast("intptr_t", ffi.cast("void *", self.sint)))
+     elseif self.token_type == ffi.C.TT_UINT then
+       return tonumber(ffi.cast("uintptr_t", ffi.cast("void *", self.uint)))
+     elseif self.token_type == ffi.C.TT_SEQUENCE then
+       return self.seq()
+     end
+  end
+}
+parsed_token = ffi.metatype("HParsedToken", tok_mt)
 
 function hammer.token(str)
   return h.h_token(str, #str)
diff --git a/src/bindings/lua/test.lua b/src/bindings/lua/test.lua
index cc0707b167672463fc6d7b90bcf429be8a09ff48..6b566ecc447bf9eae876c25bdf934d81559bea8d 100644
--- a/src/bindings/lua/test.lua
+++ b/src/bindings/lua/test.lua
@@ -3,6 +3,7 @@ describe("Combinator tests", function()
 
   setup(function()
     hammer = require("hammer")
+    ffi = require("ffi")
   end)
 
   teardown(function()
@@ -25,7 +26,7 @@ describe("Combinator tests", function()
     local parser = hammer.ch(0xa2)
     it("parses a matching char", function()
       local ret = parser:parse(string.char(0xa2))
-      assert.are.same(string.char(0xa2), ret.ast.uint)
+      assert.are.same(string.char(0xa2), string.char(ret.ast()))
     end)
     it("rejects a non-matching char", function()
       local ret = parser:parse(string.char(0xa3))
@@ -37,7 +38,7 @@ describe("Combinator tests", function()
     local parser = hammer.ch_range("a", "c")
     it("parses a char in the range", function()
       local ret = parser:parse("b")
-      assert.are.same("b", ret.ast.uint)
+      assert.are.same("b", string.char(ret.ast()))
     end)
     it("rejects a char outside the range", function()
       local ret = parser:parse("d")
@@ -97,7 +98,7 @@ describe("Combinator tests", function()
     local parser = hammer.uint64()
     it("parses a valid 64-bit unsigned int", function()
       local ret = parser:parse(string.char(0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00))
-      assert.are.same(0x200000000, ret.ast.uint)
+      assert.are.same(0x200000000, ret.ast())
     end)
     it("does not parse an invalid 64-bit unsigned int", function()
       local ret = parser:parse(string.char(0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00))
@@ -109,7 +110,7 @@ describe("Combinator tests", function()
     local parser = hammer.uint32()
     it("parses a valid 32-bit unsigned int", function()
       local ret = parser:parse(string.char(0x00, 0x02, 0x00, 0x00))
-      assert.are.same(0x20000, ret.ast.uint)
+      assert.are.same(0x20000, ret.ast())
     end)
     it("does not parse an invalid 32-bit unsigned int", function()
       local ret = parser:parse(string.char(0x00, 0x02, 0x00))
@@ -121,7 +122,7 @@ describe("Combinator tests", function()
     local parser = hammer.uint16()
     it("parses a valid 16-bit unsigned int", function()
       local ret = parser:parse(string.char(0x02, 0x00))
-      assert.are.same(0x200, ret.ast.uint)
+      assert.are.same(0x200, ret.ast())
     end)
     it("does not parse an invalid 16-bit unsigned int", function()
       local ret = parser:parse(string.char(0x02))
@@ -133,7 +134,7 @@ describe("Combinator tests", function()
     local parser = hammer.uint8()
     it("parses a valid 8-bit unsigned int", function()
       local ret = parser:parse(string.char(0x78))
-      assert.are.same(0x78, ret.ast.uint)
+      assert.are.same(0x78, ret.ast())
     end)
     it("does not parse an invalid 8=bit unsigned int", function()
       local ret = parser:parse("")
@@ -145,7 +146,7 @@ describe("Combinator tests", function()
     local parser = hammer.int_range(hammer.uint8(), 3, 10)
     it("parses a value in the range", function()
       local ret = parser:parse(string.char(0x05))
-      assert.are.same(5, ret.ast.uint)
+      assert.are.same(5, ret.ast())
     end)
     it("does not parse a value outside the range", function()
       local ret = parser:parse(string.char(0xb))
@@ -158,19 +159,19 @@ describe("Combinator tests", function()
     local parser2 = hammer.whitespace(hammer.end_p())
     it("parses a string with no whitespace", function()
       local ret = parser:parse("a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("parses a string with a leading space", function()
       local ret = parser:parse(" a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("parses a string with leading spaces", function()
       local ret = parser:parse("  a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("parses a string with a leading tab", function()
       local ret = parser:parse("\ta")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("does not parse a string with a leading underscore", function()
       local ret = parser:parse("_a")
@@ -194,7 +195,7 @@ describe("Combinator tests", function()
     local parser = hammer.left(hammer.ch("a"), hammer.ch(" "))
     it("parses the leftmost character", function()
       local ret = parser:parse("a ")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("does not parse a string that is too short", function()
       local ret = parser:parse("a")
@@ -214,7 +215,7 @@ describe("Combinator tests", function()
     local parser = hammer.right(hammer.ch(" "), hammer.ch("a"))
     it("parses the rightmost character", function()
       local ret = parser:parse(" a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("does not parse a string that starts with the wrong character", function()
       local ret = parser:parse("a")
@@ -234,7 +235,7 @@ describe("Combinator tests", function()
     local parser = hammer.middle(hammer.ch(" "), hammer.ch("a"), hammer.ch(" "))
     it("parses the middle character", function()
       local ret = parser:parse(" a ")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("does not parse a string that is too short", function()
       local ret = parser:parse("a")
@@ -262,21 +263,21 @@ describe("Combinator tests", function()
 
   describe("Semantic action tests", function()
     local function upcase(result, user_data)
-      local chars = result.ast.seq()
+      local chars = result.ast()
       local ret = ""
       for i, v in ipairs(chars)
-        do ret = ret .. string.char(tonumber(v.uint)):upper()
+        do ret = ret .. string.char(v()):upper()
       end
-      return ret
+      return ffi.new("HParsedToken", {hammer.TT_BYTES, ret})
     end
     local parser = hammer.action(hammer.sequence(hammer.choice(hammer.ch("a"), hammer.ch("A")), hammer.choice(hammer.ch("b"), hammer.ch("B"))), upcase, nil)
     it("converts a lowercase 'ab' to uppercase", function()
       local ret = parser:parse("ab")
-      assert.are.same({"A", "B"}, ret.ast.seq())
+      assert.are.same("AB", ret.ast())
     end)
     it("accepts an uppercase 'AB' unchanged", function()
       local ret = parser:parse("AB")
-      assert.are.same({"A", "B"}, ret.ast.seq())
+      assert.are.same("AB", ret.ast())
     end)
     it("rejects strings that don't match the underlying parser", function()
       local ret = parser:parse("XX")
@@ -288,7 +289,7 @@ describe("Combinator tests", function()
     local parser = hammer.in_({"a", "b", "c"})
     it("parses a character that is in the included set", function()
       local ret = parser:parse("b")
-      assert.are.same("b", ret.ast.uint)
+      assert.are.same("b", string.char(ret.ast()))
     end)
     it("does not parse a character that is not in the included set", function()
       local ret = parser:parse("d")
@@ -300,7 +301,7 @@ describe("Combinator tests", function()
     local parser = hammer.not_in({"a", "b", "c"})
     it("parses a character that is not in the excluded set", function()
       local ret = parser:parse("d")
-      assert.are.same("d", ret.ast.uint)
+      assert.are.same("d", string.char(ret.ast()))
     end)
     it("does not parse a character that is in the excluded set", function()
       local ret = parser:parse("a")
@@ -309,10 +310,10 @@ describe("Combinator tests", function()
   end)
 
   describe("End-of-input tests", function()
-    local parser = hammer.seq()uence(hammer.ch("a"), hammer.end_p())
+    local parser = hammer.sequence(hammer.ch("a"), hammer.end_p())
     it("parses a string that ends where it is expected to", function()
       local ret = parser:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
     end)
     it("does not parse a string that is too long", function()
       local ret = parser:parse("aa")
@@ -333,7 +334,7 @@ describe("Combinator tests", function()
     local parser2 = hammer.sequence(hammer.ch("a"), hammer.whitespace(hammer.ch("b")))
     it("parses a string matching the sequence", function()
       local ret = parser:parse("ab")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
     end)
     it("does not parse a string that is too short", function()
       local ret = parser:parse("a")
@@ -345,14 +346,14 @@ describe("Combinator tests", function()
     end)
     it("parses a whitespace-optional string with no whitespace", function()
       local ret = parser2:parse("ab")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
     end)
     -- it("parses a whitespace-optional string containing whitespace", function()
     --   local ret = parser:parse("a b")
-    --   assert.are.same({"a", "b"}, ret.ast.seq()) -- this is the line that segfaults
+    --   assert.are.same({"a", "b"}, ret.ast()) -- this is the line that segfaults
     --   print("in sequence")
     --   ret = parser:parse("a  b")
-    --   assert.are.same({"a", "b"}, ret.ast.seq())
+    --   assert.are.same({"a", "b"}, ret.ast())
     -- end)
   end)
 
@@ -360,9 +361,9 @@ describe("Combinator tests", function()
     local parser = hammer.choice(hammer.ch("a"), hammer.ch("b"))
     it("parses a character in the choice set", function()
       local ret = parser:parse("a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
       ret = parser:parse("b")
-      assert.are.same("b", ret.ast.uint)
+      assert.are.same("b", string.char(ret.ast()))
     end)
     it("does not parse a character not in the choice set", function()
       local ret = parser:parse("c")
@@ -375,9 +376,9 @@ describe("Combinator tests", function()
     local parser2 = hammer.butnot(hammer.ch_range("0", "9"), hammer.ch("6"))
     it("succeeds when 'a' matches but 'ab' doesn't", function()
       local ret = parser:parse("a")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
       ret = parser:parse("aa")
-      assert.are.same("a", ret.ast.uint)
+      assert.are.same("a", string.char(ret.ast()))
     end)
     it("fails when p2's result is longer than p1's", function()
       local ret = parser:parse("ab")
@@ -393,7 +394,7 @@ describe("Combinator tests", function()
     local parser = hammer.difference(hammer.token("ab"), hammer.ch("a"))
     it("succeeds when 'ab' matches and its result is longer than the result for 'a'", function()
       local ret = parser:parse("ab")
-      assert.are.same("ab", ret.ast.bytes())
+      assert.are.same("ab", ret.ast())
     end)
     it("fails if 'ab' doesn't match", function()
       local ret = parser:parse("a")
@@ -405,11 +406,11 @@ describe("Combinator tests", function()
     local parser = hammer.xor(hammer.ch_range("0", "6"), hammer.ch_range("5", "9"))
     it("parses a value only in the first range", function()
       local ret = parser:parse("0")
-      assert.are.same("0", ret.ast.uint)
+      assert.are.same("0", string.char(ret.ast()))
     end)
     it("parses a value only in the second range", function()
       local ret = parser:parse("9")
-      assert.are.same("9", ret.ast.uint)
+      assert.are.same("9", string.char(ret.ast()))
     end)
     it("does not parse a value inside both ranges", function()
       local ret = parser:parse("5")
@@ -425,17 +426,17 @@ describe("Combinator tests", function()
     local parser = hammer.many(hammer.choice(hammer.ch("a"), hammer.ch("b")))
     it("parses an empty string", function()
       local ret = parser:parse("")
-      assert.are.same({}, ret.ast.seq())
+      assert.are.same({}, ret.ast())
     end)
     it("parses a single repetition of the pattern", function()
       local ret = parser:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
       ret = parser:parse("b")
-      assert.are.same({"b"}, ret.ast.seq())
+      assert.are.same({"b"}, ret.ast())
     end)
     it("parses multiple repetitions of the pattern", function()
       local ret = parser:parse("aabbaba")
-      assert.are.same({"a", "a", "b", "b", "a", "b", "a"}, ret.ast.seq())
+      assert.are.same({"a", "a", "b", "b", "a", "b", "a"}, ret.ast())
     end)
   end)
 
@@ -447,13 +448,13 @@ describe("Combinator tests", function()
     end)
     it("parses a single repetition of the pattern", function()
       local ret = parser:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
       ret = parser:parse("b")
-      assert.are.same({"b"}, ret.ast.seq())
+      assert.are.same({"b"}, ret.ast())
     end)
     it("parses multiple repetitions of the pattern", function()
       local ret = parser:parse("aabbaba")
-      assert.are.same({"a", "a", "b", "b", "a", "b", "a"}, ret.ast.seq())
+      assert.are.same({"a", "a", "b", "b", "a", "b", "a"}, ret.ast())
     end)
     it("does not parse a string that does not start with one of the patterns to repeat", function()
       local ret = parser:parse("daabbabadef")
@@ -469,7 +470,7 @@ describe("Combinator tests", function()
     end)
     it("parses a string containing the correct number of repetitions", function()
       local ret = parser:parse("abdef")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
     end)
     it("does not parse a string that does not start with a character in the repetition set", function()
       local ret = parser:parse("dabdef")
@@ -481,13 +482,13 @@ describe("Combinator tests", function()
     local parser = hammer.sequence(hammer.ch("a"), hammer.optional(hammer.choice(hammer.ch("b"), hammer.ch("c"))), hammer.ch("d"))
     it("parses a string containing either optional character", function()
       local ret = parser:parse("abd")
-      assert.are.same({"a", "b", "d"}, ret.ast.seq())
+      assert.are.same({"a", "b", "d"}, ret.ast())
       ret = parser:parse("acd")
-      assert.are.same({"a", "c", "d"}, ret.ast.seq())
+      assert.are.same({"a", "c", "d"}, ret.ast())
     end)
     it("parses a string missing one of the optional characters", function()
       local ret = parser:parse("ad")
-      assert.are.same({"a", {}, "d"}, ret.ast.seq())
+      assert.are.same({"a", {}, "d"}, ret.ast())
     end)
     it("does not parse a string containing a character not among the optional ones", function()
       local ret = parser:parse("aed")
@@ -499,7 +500,7 @@ describe("Combinator tests", function()
     local parser = hammer.sequence(hammer.ch("a"), hammer.ignore(hammer.ch("b")), hammer.ch("c"))
     it("parses a string containing the pattern to ignore, and leaves that pattern out of the result", function()
       local ret = parser:parse("abc")
-      assert.are.same({"a", "c"}, ret.ast.seq())
+      assert.are.same({"a", "c"}, ret.ast())
     end)
     it("does not parse a string not containing the pattern to ignore", function()
       local ret = parser:parse("ac")
@@ -511,23 +512,23 @@ describe("Combinator tests", function()
     local parser = hammer.sepBy(hammer.choice(hammer.ch("1"), hammer.ch("2"), hammer.ch("3")), hammer.ch(","))
     it("parses an ordered list", function()
       local ret = parser:parse("1,2,3")
-      assert.are.same({"1", "2", "3"}, ret.ast.seq())
+      assert.are.same({"1", "2", "3"}, ret.ast())
     end)
     it("parses an unordered list", function()
       local ret = parser:parse("1,3,2")
-      assert.are.same({"1", "3", "2"}, ret.ast.seq())
+      assert.are.same({"1", "3", "2"}, ret.ast())
     end)
     it("parses a list not containing all options", function()
       local ret = parser:parse("1,3")
-      assert.are.same({"1", "3"}, ret.ast.seq())
+      assert.are.same({"1", "3"}, ret.ast())
     end)
     it("parses a unary list", function()
       local ret = parser:parse("3")
-      assert.are.same({"3"}, ret.ast.seq())
+      assert.are.same({"3"}, ret.ast())
     end)
     it("parses an empty list", function()
       local ret = parser:parse("")
-      assert.are.same({}, ret.ast.seq())
+      assert.are.same({}, ret.ast())
     end)
   end)
 
@@ -535,20 +536,20 @@ describe("Combinator tests", function()
     local parser = hammer.sepBy1(hammer.choice(hammer.ch("1"), hammer.ch("2"), hammer.ch("3")), hammer.ch(","))
     it("parses an ordered list", function()
       local ret = parser:parse("1,2,3")
-      assert.are.same({"1", "2", "3"}, ret.ast.seq())
+      assert.are.same({"1", "2", "3"}, ret.ast())
     end)
     it("parses an unordered list", function()
       local ret = parser:parse("1,3,2")
-      assert.are.same({"1", "3", "2"}, ret.ast.seq())
+      assert.are.same({"1", "3", "2"}, ret.ast())
     end)
     it("parses a list not containing all options", function()
       local ret = parser:parse("1,3")
-      assert.are.same({"1", "3"}, ret.ast.seq())
+      assert.are.same({"1", "3"}, ret.ast())
     end)
     -- it("parses a unary list", function()
     --   local ret = parser:parse("3")
     --   print("in sepBy1")
-    --   assert.are.same({"3"}, ret.ast.seq()) -- this line also segfaults
+    --   assert.are.same({"3"}, ret.ast()) -- this line also segfaults
     -- end)
     it("does not parse an empty list", function()
       local ret = parser:parse("")
@@ -562,15 +563,15 @@ describe("Combinator tests", function()
     local parser3 = hammer.sequence(hammer.ch("a"), hammer.epsilon_p())
     it("parses an empty string between two characters", function()
       local ret = parser:parse("ab")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
     end)
     it("parses an empty string before a character", function()
       local ret = parser2:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
     end)
-    it("parses a ", function()
+    it("parses an empty string after a character", function()
       local ret = parser3:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
     end)
   end)
 
@@ -581,9 +582,9 @@ describe("Combinator tests", function()
     local parser = hammer.attr_bool(hammer.many1(hammer.choice(hammer.ch("a"), hammer.ch("b"))), equals)
     it("parses successfully when both characters are the same (i.e., the validation function succeeds)", function()
       local ret = parser:parse("aa")
-      assert.are.same({"a", "a"}, ret.ast.seq())
+      assert.are.same({"a", "a"}, ret.ast())
       ret = parser:parse("bb")
-      assert.are.same({"b", "b"}, ret.ast.seq())
+      assert.are.same({"b", "b"}, ret.ast())
     end)
     it("does not parse successfully when the characters are different (i.e., the validation function fails)", function()
       local ret = parser:parse("ab")
@@ -597,7 +598,7 @@ describe("Combinator tests", function()
     local parser3 = hammer.sequence(hammer.ch("1"), hammer.and_(hammer.ch("2")))
     it("parses successfully when the lookahead matches the next character to parse", function()
       local ret = parser:parse("0")
-      assert.are.same({"0"}, ret.ast.seq())
+      assert.are.same({"0"}, ret.ast())
     end)
     it("does not parse successfully when the lookahead does not match the next character to parse", function()
       local ret = parser2:parse("0")
@@ -605,7 +606,7 @@ describe("Combinator tests", function()
     end)
     it("parses successfully when the lookahead is there", function()
       local ret = parser3:parse("12")
-      assert.are.same({"1"}, ret.ast.seq())
+      assert.are.same({"1"}, ret.ast())
     end)
   end)
 
@@ -614,7 +615,7 @@ describe("Combinator tests", function()
     local parser2 = hammer.sequence(hammer.ch("a"), hammer.choice(hammer.sequence(hammer.ch("+"), hammer.not_(hammer.ch("+"))), hammer.token("++")), hammer.ch("b"))
     it("parses a single plus correctly in the 'choice' example", function()
       local ret = parser:parse("a+b")
-      assert.are.same({"a", "+", "b"}, ret.ast.seq())
+      assert.are.same({"a", "+", "b"}, ret.ast())
     end)
     it("does not parse a double plus correctly in the 'choice' example", function()
       local ret = parser:parse("a++b")
@@ -622,11 +623,11 @@ describe("Combinator tests", function()
     end)
     it("parses a single plus correctly in the 'not' example", function()
       local ret = parser2:parse("a+b")
-      assert.are.same({"a", {"+"}, "b"}, ret.ast.seq())
+      assert.are.same({"a", {"+"}, "b"}, ret.ast())
     end)
     it("parses a double plus correctly in the 'not' example", function()
       local ret = parser2:parse("a++b")
-      assert.are.same({"a", "++", "b"}, ret.ast.seq())
+      assert.are.same({"a", "++", "b"}, ret.ast())
     end)
   end)
 
@@ -636,16 +637,15 @@ describe("Combinator tests", function()
     -- it("parses the base case", function()
     --   print("in leftrec")
     --   local ret = parser:parse("a") -- this line segfaults
-    --   assert.are.same({"a"}, ret.ast.seq())
+    --   assert.are.same({"a"}, ret.ast())
     -- end)
     it("parses one level of recursion", function()
-      print("in leftrec")
       local ret = parser:parse("aa")
-      assert.are.same({"a", "a"}, ret.ast.seq())
+      assert.are.same({"a", "a"}, ret.ast())
     end)
     it("parses two levels of recursion", function()
       local ret = parser:parse("aaa")
-      assert.are.same({{"a", "a"}, "a"}, ret.ast.seq())
+      assert.are.same({{"a", "a"}, "a"}, ret.ast())
     end)
   end)
 
@@ -654,15 +654,15 @@ describe("Combinator tests", function()
     hammer.bind_indirect(parser, hammer.choice(hammer.sequence(hammer.ch("a"), parser), hammer.epsilon_p()))
     it("parses the base case", function()
       local ret = parser:parse("a")
-      assert.are.same({"a"}, ret.ast.seq())
+      assert.are.same({"a"}, ret.ast())
     end)
     it("parses one level of recursion", function()
       local ret = parser:parse("aa")
-      assert.are.same({"a", {"a"}}, ret.ast.seq())
+      assert.are.same({"a", {"a"}}, ret.ast())
     end)
     it("parses two levels of recursion", function()
       local ret = parser:parse("aaa")
-      assert.are.same({"a", {"a", {"a"}}}, ret.ast.seq())
+      assert.are.same({"a", {"a", {"a"}}}, ret.ast())
     end)
   end)
 
@@ -684,25 +684,25 @@ describe("Combinator tests", function()
     local parser8 = hammer.with_endianness(lb, u5)
     it("parses big-endian cases", function()
       local ret = parser1:parse("abcd")
-      assert.are.same(0x61626364, ret.ast.uint)
+      assert.are.same(0x61626364, ret.ast())
       ret = parser2:parse("abcd")
-      assert.are.same(0xc, ret.ast.uint)
+      assert.are.same(0xc, ret.ast())
     end)
     it("parses little-endian cases", function()
       local ret = parser3:parse("abcd")
-      assert.are.same(0x61626364, ret.ast.uint)
+      assert.are.same(0x61626364, ret.ast())
       ret = parser4:parse("abcd")
-      assert.are.same(0xc, ret.ast.uint)
+      assert.are.same(0xc, ret.ast())
     end)
     it("parses mixed-endian cases", function()
       local ret = parser5:parse("abcd")
-      assert.are.same(0x61626364, ret.ast.uint)
+      assert.are.same(0x61626364, ret.ast())
       ret = parser6:parse("abcd")
-      assert.are.same(0x1, ret.ast.uint)
+      assert.are.same(0x1, ret.ast())
       ret = parser7:parse("abcd")
-      assert.are.same(0x64636261, ret.ast.uint)
+      assert.are.same(0x64636261, ret.ast())
       ret = parser8:parse("abcd")
-      assert.are.same(0xc, ret.ast.uint)
+      assert.are.same(0xc, ret.ast())
     end)
   end)
 
@@ -710,8 +710,8 @@ describe("Combinator tests", function()
     local parser = hammer.sequence(hammer.put_value(hammer.uint8(), "size"), hammer.token("foo"), hammer.length_value(hammer.get_value("size"), hammer.uint8()))
     it("parses a string that has enough bytes for the specified length", function()
       local ret = parser:parse(string.char(0x06) .. "fooabcdef")
-      assert.are.same("foo", ret.ast.seq[2])
-      assert.are.same({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}, ret.ast.seq[3])
+      assert.are.same("foo", ret.ast()[2])
+      assert.are.same({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}, ret.ast()[3])
     end)
     it("does not parse a string that does not have enough bytes for the specified length", function()
       local ret = parser:parse(string.char(0x06) .. "fooabcde")
@@ -723,17 +723,17 @@ describe("Combinator tests", function()
     local parser = hammer.permutation(hammer.ch("a"), hammer.ch("b"), hammer.ch("c"))
     it("parses a permutation of 'abc'", function()
       local ret = parser:parse("abc")
-      assert.are.same({"a", "b", "c"}, ret.ast.seq())
+      assert.are.same({"a", "b", "c"}, ret.ast())
       ret = parser:parse("acb")
-      assert.are.same({"a", "c", "b"}, ret.ast.seq())
+      assert.are.same({"a", "c", "b"}, ret.ast())
       ret = parser:parse("bac")
-      assert.are.same({"b", "a", "c"}, ret.ast.seq())
+      assert.are.same({"b", "a", "c"}, ret.ast())
       ret = parser:parse("bca")
-      assert.are.same({"b", "c", "a"}, ret.ast.seq())
+      assert.are.same({"b", "c", "a"}, ret.ast())
       ret = parser:parse("cab")
-      assert.are.same({"c", "a", "b"}, ret.ast.seq())
+      assert.are.same({"c", "a", "b"}, ret.ast())
       ret = parser:parse("cba")
-      assert.are.same({"c", "b", "a"}, ret.ast.seq())
+      assert.are.same({"c", "b", "a"}, ret.ast())
     end)
     it("does not parse a string that is not a permutation of 'abc'", function()
       local ret = parser:parse("a")
@@ -746,21 +746,21 @@ describe("Combinator tests", function()
     parser = hammer.permutation(hammer.ch("a"), hammer.ch("b"), hammer.optional(hammer.ch("c")))
     it("parses a string that is a permutation of 'ab[c]'", function()
       local ret = parser:parse("abc")
-      assert.are.same({"a", "b", "c"}, ret.ast.seq())
+      assert.are.same({"a", "b", "c"}, ret.ast())
       ret = parser:parse("acb")
-      assert.are.same({"a", "c", "b"}, ret.ast.seq())
+      assert.are.same({"a", "c", "b"}, ret.ast())
       ret = parser:parse("bac")
-      assert.are.same({"b", "a", "c"}, ret.ast.seq())
+      assert.are.same({"b", "a", "c"}, ret.ast())
       ret = parser:parse("bca")
-      assert.are.same({"b", "c", "a"}, ret.ast.seq())
+      assert.are.same({"b", "c", "a"}, ret.ast())
       ret = parser:parse("cab")
-      assert.are.same({"c", "a", "b"}, ret.ast.seq())
+      assert.are.same({"c", "a", "b"}, ret.ast())
       ret = parser:parse("cba")
-      assert.are.same({"c", "b", "a"}, ret.ast.seq())
+      assert.are.same({"c", "b", "a"}, ret.ast())
       ret = parser:parse("ab")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
       ret = parser:parse("ba")
-      assert.are.same({"b", "a"}, ret.ast.seq())
+      assert.are.same({"b", "a"}, ret.ast())
     end)
     it("does not parse a string that is not a permutation of 'ab[c]'", function()
       local ret = parser:parse("a")
@@ -783,21 +783,21 @@ describe("Combinator tests", function()
     parser = hammer.permutation(hammer.optional(hammer.ch("c")), hammer.ch("a"), hammer.ch("b"))
     it("parses a string that is a permutation of '[c]ab'", function()
       local ret = parser:parse("abc")
-      assert.are.same({"a", "b", "c"}, ret.ast.seq())
+      assert.are.same({"a", "b", "c"}, ret.ast())
       ret = parser:parse("acb")
-      assert.are.same({"a", "c", "b"}, ret.ast.seq())
+      assert.are.same({"a", "c", "b"}, ret.ast())
       ret = parser:parse("bac")
-      assert.are.same({"b", "a", "c"}, ret.ast.seq())
+      assert.are.same({"b", "a", "c"}, ret.ast())
       ret = parser:parse("bca")
-      assert.are.same({"b", "c", "a"}, ret.ast.seq())
+      assert.are.same({"b", "c", "a"}, ret.ast())
       ret = parser:parse("cab")
-      assert.are.same({"c", "a", "b"}, ret.ast.seq())
+      assert.are.same({"c", "a", "b"}, ret.ast())
       ret = parser:parse("cba")
-      assert.are.same({"c", "b", "a"}, ret.ast.seq())
+      assert.are.same({"c", "b", "a"}, ret.ast())
       ret = parser:parse("ab")
-      assert.are.same({"a", "b"}, ret.ast.seq())
+      assert.are.same({"a", "b"}, ret.ast())
       ret = parser:parse("ba")
-      assert.are.same({"b", "a"}, ret.ast.seq())
+      assert.are.same({"b", "a"}, ret.ast())
     end)
     it("does not parse a string that is not a permutation of '[c]ab'", function()
       local ret = parser:parse("a")