diff --git a/src/Makefile b/src/Makefile
index cc08eee88add9b8b613b40db3940c87220ab12e6..264608b5ecc2891c61b58f51d3f187d954c05fdb 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -4,6 +4,7 @@ PARSERS := \
 	bits \
 	token \
 	whitespace \
+	ignoreseq \
 	ch \
 	action \
 	charset \
diff --git a/src/hammer.c b/src/hammer.c
index 11492c3fdfb28705f7eaad360f5d99af74068fb9..3bbd53791987ebc80a4a7d32408feac9adcf4cdf 100644
--- a/src/hammer.c
+++ b/src/hammer.c
@@ -380,6 +380,37 @@ static void test_whitespace(void) {
   g_check_parse_failed(whitespace_, "_a", 2);
 }
 
+static void test_left(void) {
+  const HParser *left_ = h_left(h_ch('a'), h_ch(' '));
+
+  g_check_parse_ok(left_, "a ", 2, "u0x61");
+  g_check_parse_failed(left_, "a", 1);
+  g_check_parse_failed(left_, " ", 1);
+  g_check_parse_failed(left_, "ab", 2);
+}
+
+static void test_right(void) {
+  const HParser *right_ = h_right(h_ch(' '), h_ch('a'));
+
+  g_check_parse_ok(right_, " a", 2, "u0x61");
+  g_check_parse_failed(right_, "a", 1);
+  g_check_parse_failed(right_, " ", 1);
+  g_check_parse_failed(right_, "ba", 2);
+}
+
+static void test_middle(void) {
+  const HParser *middle_ = h_middle(h_ch(' '), h_ch('a'), h_ch(' '));
+
+  g_check_parse_ok(middle_, " a ", 3, "u0x61");
+  g_check_parse_failed(middle_, "a", 1);
+  g_check_parse_failed(middle_, " ", 1);
+  g_check_parse_failed(middle_, " a", 2);
+  g_check_parse_failed(middle_, "a ", 2);
+  g_check_parse_failed(middle_, " b ", 3);
+  g_check_parse_failed(middle_, "ba ", 3);
+  g_check_parse_failed(middle_, " ab", 3);
+}
+
 #include <ctype.h>
 
 const HParsedToken* upcase(const HParseResult *p) {
@@ -608,6 +639,9 @@ void register_parser_tests(void) {
   g_test_add_func("/core/parser/float32", test_float32);
 #endif
   g_test_add_func("/core/parser/whitespace", test_whitespace);
+  g_test_add_func("/core/parser/left", test_left);
+  g_test_add_func("/core/parser/right", test_right);
+  g_test_add_func("/core/parser/middle", test_middle);
   g_test_add_func("/core/parser/action", test_action);
   g_test_add_func("/core/parser/in", test_in);
   g_test_add_func("/core/parser/not_in", test_not_in);
diff --git a/src/hammer.h b/src/hammer.h
index d0eac9d1d2be3bd860bf05c79d60018223b039fb..f7e9a9beb92452147d5e2dddc98195b4bd0a59ce 100644
--- a/src/hammer.h
+++ b/src/hammer.h
@@ -216,6 +216,30 @@ const HParser* h_uint8();
  */
 const HParser* h_whitespace(const HParser* p);
 
+/**
+ * Given two parsers, p and q, returns a parser that parses them in
+ * sequence but only returns p's result.
+ *
+ * Result token type: p's result type
+ */
+const HParser* h_left(const HParser* p, const HParser* q);
+
+/**
+ * Given two parsers, p and q, returns a parser that parses them in
+ * sequence but only returns q's result.
+ *
+ * Result token type: q's result type
+ */
+const HParser* h_right(const HParser* p, const HParser* q);
+
+/**
+ * Given three parsers, p, x, and q, returns a parser that parses them in
+ * sequence but only returns x's result.
+ *
+ * Result token type: x's result type
+ */
+const HParser* h_middle(const HParser* p, const HParser* x, const HParser* q);
+
 /**
  * Given another parser, p, and a function f, returns a parser that 
  * applies p, then applies f to everything in the AST of p's result. 
diff --git a/src/parsers/ignoreseq.c b/src/parsers/ignoreseq.c
new file mode 100644
index 0000000000000000000000000000000000000000..8aac2c82c5f09658c9962860f7f625bff70523f2
--- /dev/null
+++ b/src/parsers/ignoreseq.c
@@ -0,0 +1,73 @@
+#include "parser_internal.h"
+
+
+//
+// general case: parse sequence, pick one result
+//
+
+typedef struct {
+  const HParser **parsers;
+  size_t count;         // how many parsers in 'ps'
+  size_t which;         // whose result to return
+} HIgnoreSeq;
+
+static HParseResult* parse_ignoreseq(void* env, HParseState *state) {
+  const HIgnoreSeq *seq = (HIgnoreSeq*)env;
+  HParseResult *res = NULL;
+
+  for (size_t i=0; i < seq->count; ++i) {
+    HParseResult *tmp = h_do_parse(seq->parsers[i], state);
+    if (!tmp)
+      return NULL;
+    else if (i == seq->which)
+      res = tmp;
+  }
+
+  return res;
+}
+
+static const HParserVtable ignoreseq_vt = {
+  .parse = parse_ignoreseq,
+};
+
+
+//
+// API frontends
+//
+
+static const HParser* h_leftright(const HParser* p, const HParser* q, size_t which) {
+  HIgnoreSeq *seq = g_new(HIgnoreSeq, 1);
+  seq->parsers = g_new(const HParser*, 2);
+  seq->parsers[0] = p;
+  seq->parsers[1] = q;
+  seq->count = 2;
+  seq->which = which;
+
+  HParser *ret = g_new(HParser, 1);
+  ret->vtable = &ignoreseq_vt;
+  ret->env = (void*)seq;
+  return ret;
+}
+
+const HParser* h_left(const HParser* p, const HParser* q) {
+  return h_leftright(p, q, 0);
+}
+
+const HParser* h_right(const HParser* p, const HParser* q) {
+  return h_leftright(p, q, 1);
+}
+
+const HParser* h_middle(const HParser* p, const HParser* x, const HParser* q) {
+  HIgnoreSeq *seq = g_new(HIgnoreSeq, 1);
+  seq->parsers = g_new(const HParser*, 3);
+  seq->parsers[0] = p;
+  seq->parsers[1] = x;
+  seq->parsers[2] = q;
+  seq->count = 3;
+  seq->which = 1;
+
+  HParser *ret = g_new(HParser, 1);
+  ret->vtable = &ignoreseq_vt;
+  ret->env = (void*)seq;
+  return ret;
+}