Add base.token_decoder interface
diff --git a/lang/builtin/builtin.go b/lang/builtin/builtin.go
index 1bf829f..22e2dd6 100644
--- a/lang/builtin/builtin.go
+++ b/lang/builtin/builtin.go
@@ -373,12 +373,14 @@
 	"hasher_u32",
 	"image_decoder",
 	"io_transformer",
+	"token_decoder",
 }
 
 var InterfacesMap = map[string]bool{
 	"hasher_u32":     true,
 	"image_decoder":  true,
 	"io_transformer": true,
+	"token_decoder":  true,
 }
 
 var InterfaceFuncs = []string{
@@ -408,6 +410,10 @@
 
 	"io_transformer.transform_io?(dst: io_writer, src: io_reader, workbuf: slice u8)",
 	"io_transformer.workbuf_len() range_ii_u64",
+
+	// ---- token_decoder
+
+	"token_decoder.decode_tokens?(dst: token_writer, src: io_reader)",
 }
 
 // The "T1" and "T2" types here are placeholders for generic "slice T" or
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index 955d55c..46455c0 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -3382,6 +3382,45 @@
 
 #endif  // defined(__cplusplus) || defined(WUFFS_IMPLEMENTATION)
 
+// --------
+
+extern const char* wuffs_base__token_decoder__vtable_name;
+
+typedef struct {
+  wuffs_base__status (*decode_tokens)(void* self,
+                                      wuffs_base__token_buffer* a_dst,
+                                      wuffs_base__io_buffer* a_src);
+} wuffs_base__token_decoder__func_ptrs;
+
+typedef struct wuffs_base__token_decoder__struct wuffs_base__token_decoder;
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__token_decoder__decode_tokens(wuffs_base__token_decoder* self,
+                                         wuffs_base__token_buffer* a_dst,
+                                         wuffs_base__io_buffer* a_src);
+
+#if defined(__cplusplus) || defined(WUFFS_IMPLEMENTATION)
+
+struct wuffs_base__token_decoder__struct {
+  struct {
+    uint32_t magic;
+    uint32_t active_coroutine;
+    wuffs_base__vtable first_vtable;
+  } private_impl;
+
+#ifdef __cplusplus
+
+  inline wuffs_base__status  //
+  decode_tokens(wuffs_base__token_buffer* a_dst, wuffs_base__io_buffer* a_src) {
+    return wuffs_base__token_decoder__decode_tokens(this, a_dst, a_src);
+  }
+
+#endif  // __cplusplus
+
+};  // struct wuffs_base__token_decoder__struct
+
+#endif  // defined(__cplusplus) || defined(WUFFS_IMPLEMENTATION)
+
 // ----------------
 
 #ifdef __cplusplus
@@ -5239,6 +5278,12 @@
 
 // ---------------- Upcasts
 
+static inline wuffs_base__token_decoder*  //
+wuffs_json__decoder__upcast_as__wuffs_base__token_decoder(
+    wuffs_json__decoder* p) {
+  return (wuffs_base__token_decoder*)p;
+}
+
 // ---------------- Public Function Prototypes
 
 WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
@@ -5266,6 +5311,7 @@
   struct {
     uint32_t magic;
     uint32_t active_coroutine;
+    wuffs_base__vtable vtable_for__wuffs_base__token_decoder;
     wuffs_base__vtable null_vtable;
 
     uint32_t p_decode_tokens[1];
@@ -5318,6 +5364,11 @@
                                            wuffs_version, initialize_flags);
   }
 
+  inline wuffs_base__token_decoder*  //
+  upcast_as__wuffs_base__token_decoder() {
+    return (wuffs_base__token_decoder*)this;
+  }
+
   inline wuffs_base__status  //
   decode_tokens(wuffs_base__token_buffer* a_dst, wuffs_base__io_buffer* a_src) {
     return wuffs_json__decoder__decode_tokens(this, a_dst, a_src);
@@ -7077,6 +7128,41 @@
   return wuffs_base__utility__empty_range_ii_u64();
 }
 
+// --------
+
+const char* wuffs_base__token_decoder__vtable_name =
+    "{vtable}wuffs_base__token_decoder";
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__token_decoder__decode_tokens(wuffs_base__token_decoder* self,
+                                         wuffs_base__token_buffer* a_dst,
+                                         wuffs_base__io_buffer* a_src) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__token_decoder__vtable_name) {
+      const wuffs_base__token_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__token_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->decode_tokens)(self, a_dst, a_src);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
 // ---------------- Images
 
 const uint32_t wuffs_base__pixel_format__bits_per_channel[16] = {
@@ -18673,6 +18759,14 @@
 
 // ---------------- VTables
 
+const wuffs_base__token_decoder__func_ptrs
+    wuffs_json__decoder__func_ptrs_for__wuffs_base__token_decoder = {
+        (wuffs_base__status(*)(void*,
+                               wuffs_base__token_buffer*,
+                               wuffs_base__io_buffer*))(
+            &wuffs_json__decoder__decode_tokens),
+};
+
 // ---------------- Initializer Implementations
 
 wuffs_base__status WUFFS_BASE__WARN_UNUSED_RESULT  //
@@ -18716,6 +18810,10 @@
   }
 
   self->private_impl.magic = WUFFS_BASE__MAGIC;
+  self->private_impl.vtable_for__wuffs_base__token_decoder.vtable_name =
+      wuffs_base__token_decoder__vtable_name;
+  self->private_impl.vtable_for__wuffs_base__token_decoder.function_pointers =
+      (const void*)(&wuffs_json__decoder__func_ptrs_for__wuffs_base__token_decoder);
   return wuffs_base__make_status(NULL);
 }
 
diff --git a/std/json/decode_json.wuffs b/std/json/decode_json.wuffs
index 2281e30..20ee6a8 100644
--- a/std/json/decode_json.wuffs
+++ b/std/json/decode_json.wuffs
@@ -12,9 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// TODO: "decoder implements base.token_decoder".
-
-pub struct decoder?(
+pub struct decoder? implements base.token_decoder(
 )(
 	// stack is conceptually an array of bits, implemented as an array of u32.
 	// The N'th bit being 0 or 1 means that we're in an array or object, where
diff --git a/test/c/std/json.c b/test/c/std/json.c
index ebc7153..efbed9f 100644
--- a/test/c/std/json.c
+++ b/test/c/std/json.c
@@ -227,33 +227,18 @@
                    &dec, sizeof dec, WUFFS_VERSION,
                    WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED));
 
-  wuffs_base__token_buffer have = ((wuffs_base__token_buffer){
-      .data = global_have_token_slice,
-  });
-  wuffs_base__io_buffer src = ((wuffs_base__io_buffer){
-      .data = global_src_slice,
-  });
-  CHECK_STRING(read_file(&src, "test/data/github-tags.json"));
+  const uint64_t vbc = WUFFS_BASE__TOKEN__VBC__STRUCTURE;
+  const uint64_t vbd = WUFFS_BASE__TOKEN__VBD__STRUCTURE__POP |
+                       WUFFS_BASE__TOKEN__VBD__STRUCTURE__FROM_LIST |
+                       WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_NONE;
+  const uint64_t len = 1;
 
-  wuffs_base__status status =
-      wuffs_json__decoder__decode_tokens(&dec, &have, &src);
-  if (0) {
-    uint64_t pos = 0;
-    size_t i;
-    for (i = have.meta.ri; i < have.meta.wi; i++) {
-      wuffs_base__token* t = &have.data.ptr[i];
-      uint64_t len = wuffs_base__token__length(t);
-      uint64_t bc = wuffs_base__token__value_base_category(t);
-      uint64_t bd = wuffs_base__token__value_base_detail(t);
-      printf("i=%3zu\tp=%4" PRIu64 " (0x%03" PRIX64 ")\tl=%3" PRIu64
-             "\tbc=%3" PRIu64 "\tbd=0x%06" PRIX64 "\n",
-             i, pos, pos, len, bc, bd);
-      pos = wuffs_base__u64__sat_add(pos, len);
-    }
-    CHECK_STATUS("decode_tokens", status);
-  }
-
-  return NULL;
+  return do_test__wuffs_base__token_decoder(
+      wuffs_json__decoder__upcast_as__wuffs_base__token_decoder(&dec),
+      "test/data/github-tags.json", 0, SIZE_MAX,
+      (vbc << WUFFS_BASE__TOKEN__VALUE_BASE_CATEGORY__SHIFT) |
+          (vbd << WUFFS_BASE__TOKEN__VALUE_BASE_DETAIL__SHIFT) |
+          (len << WUFFS_BASE__TOKEN__LENGTH__SHIFT));
 }
 
 const char*  //
diff --git a/test/c/testlib/testlib.c b/test/c/testlib/testlib.c
index 713b95e..2d06613 100644
--- a/test/c/testlib/testlib.c
+++ b/test/c/testlib/testlib.c
@@ -1011,10 +1011,10 @@
     }
   }
 
-  if ((want_width > 0) && (want_height > 0)) {
+  if ((have_width > 0) && (have_height > 0)) {
     wuffs_base__color_u32_argb_premul have_final_pixel =
-        wuffs_base__pixel_buffer__color_u32_at(&pb, want_width - 1,
-                                               want_height - 1);
+        wuffs_base__pixel_buffer__color_u32_at(&pb, have_width - 1,
+                                               have_height - 1);
     if (have_final_pixel != want_final_pixel) {
       RETURN_FAIL("final pixel: have 0x%08" PRIX32 ", want 0x%08" PRIX32,
                   have_final_pixel, want_final_pixel);
@@ -1054,9 +1054,33 @@
   if (have.meta.wi != want_wi) {
     RETURN_FAIL("dst wi: have %zu, want %zu", have.meta.wi, want_wi);
   }
-  if ((want_wi > 0) && (have.data.ptr[want_wi - 1] != want_final_byte)) {
+  if ((have.meta.wi > 0) &&
+      (have.data.ptr[have.meta.wi - 1] != want_final_byte)) {
     RETURN_FAIL("final byte: have 0x%02X, want 0x%02X",
-                have.data.ptr[want_wi - 1], want_final_byte);
+                have.data.ptr[have.meta.wi - 1], want_final_byte);
+  }
+  return NULL;
+}
+
+const char*  //
+do_test__wuffs_base__token_decoder(wuffs_base__token_decoder* b,
+                                   const char* src_filename,
+                                   size_t src_ri,
+                                   size_t src_wi,
+                                   uint64_t want_final_token_repr) {
+  wuffs_base__token_buffer have = ((wuffs_base__token_buffer){
+      .data = global_have_token_slice,
+  });
+  wuffs_base__io_buffer src = ((wuffs_base__io_buffer){
+      .data = global_src_slice,
+  });
+  CHECK_STRING(read_file_fragment(&src, src_filename, src_ri, src_wi));
+  CHECK_STATUS("decode_tokens",
+               wuffs_base__token_decoder__decode_tokens(b, &have, &src));
+  if ((have.meta.wi > 0) &&
+      (have.data.ptr[have.meta.wi - 1].repr != want_final_token_repr)) {
+    RETURN_FAIL("final token: have 0x%" PRIX64 ", want 0x%" PRIX64,
+                have.data.ptr[have.meta.wi - 1].repr, want_final_token_repr);
   }
   return NULL;
 }