diff --git a/src/bindings/perl/hammer.i b/src/bindings/perl/hammer.i
index 5103a98c38d4eae35e7bc1ba4a9aa69887954058..49036574003f299aa85d72b6b762f66e67b6df2c 100644
--- a/src/bindings/perl/hammer.i
+++ b/src/bindings/perl/hammer.i
@@ -28,6 +28,26 @@
   }
  }
 
+%typemap(in) void*[] {
+  if (!SvROK($input))
+    SWIG_exception_fail(SWIG_TypeError, "Expected array ref");
+
+  if (SvTYPE(SvRV($input)) != SVt_PVAV)
+    SWIG_exception_fail(SWIG_TypeError, "Expected array ref");
+
+  AV* av = (AV*) SvRV($input);
+  size_t amax = av_top_index(av) + 1; // I want the length, not the top index...
+  // TODO: is this array copied?
+  $1 = malloc(amax * sizeof(*$1));
+  $1[amax] = NULL;
+  for (int i = 0; i < amax; i++) {
+    int res = SWIG_ConvertPtr(*av_fetch(av, i, 0), &($1[i]), SWIGTYPE_p_HParser_, 0|0);
+    if (!SWIG_IsOK(res)) {
+      SWIG_exception_fail(SWIG_ArgError(res), "Expected a list of parsers and only parsers");
+    }
+  }
+ }
+
 %typemap(in) uint8_t {
   if (SvIOKp($input)) {
     $1 = SvIV($input);
@@ -52,7 +72,12 @@
 
 %rename("token") h_token;
 %rename("%(regex:/^h_(.*)/\\1/)s", regextarget=1) "^h_u?int(64|32|16|8)";
+%rename("end_p") h_end_p;
+%rename("left") h_left;
+%rename("middle") h_middle;
+%rename("right") h_right;
 %rename("int_range") h_int_range;
+%rename("whitespace") h_whitespace;
 %include "../swig/hammer.i";
   
 
@@ -76,7 +101,7 @@
       // TODO: return PyINT if appropriate
       return newSVuv(token->token_data.uint);
     case TT_SEQUENCE: {
-      AV* aret;
+      AV* aret = newAV();
       av_extend(aret, token->token_data.seq->used);
       for (int i = 0; i < token->token_data.seq->used; i++) {
 	av_store(aret, i, hpt_to_perl(token->token_data.seq->elements[i]));
@@ -85,9 +110,7 @@
     }
     default:
       if (token->token_type == h_tt_perl) {
-	return  (SV*)token->token_data.user;
-	
-	return SvREFCNT_inc(ret);
+	return  SvREFCNT_inc((SV*)token->token_data.user);
       } else {
 	return SWIG_NewPointerObj((void*)token, SWIGTYPE_p_HParsedToken_, 0 | 0);
 	// TODO: support registry
@@ -124,6 +147,41 @@
     return res;
   }
 
+ static HParsedToken* call_action(const HParseResult *p, void* user_data  ) {
+   SV *func = (SV*)user_data;
+
+   dSP;
+   ENTER;
+   SAVETMPS;
+   PUSHMARK(SP);
+   if (p->ast != NULL) {
+     mXPUSHs(hpt_to_perl(p->ast));
+   } else {
+     mXPUSHs(newSV(0));
+   }
+   PUTBACK;
+   
+   int nret = call_sv(func, G_SCALAR);
+
+   SPAGAIN;
+   if (nret != 1)
+     croak("Expected 1 return value, got %d", nret);
+   
+   HParsedToken *ret = h_arena_malloc(p->arena, sizeof(*ret));
+   memset(ret, 0, sizeof(*ret));
+   ret->token_type = h_tt_perl;
+   ret->token_data.user = SvREFCNT_inc(POPs);
+   if (p->ast != NULL) {
+     ret->index = p->ast->index;
+     ret->bit_offset = p->ast->bit_offset;
+   }
+   PUTBACK;
+   FREETMPS;
+   LEAVE;
+
+   return ret;
+ }
+ 
 %}
 %inline {
   HParser* ch(uint8_t chr) {
@@ -132,12 +190,15 @@
   HParser* ch_range(uint8_t c0, uint8_t c1) {
     return h_action(h_ch_range(c0,c1), h__to_char, NULL);
   }
-  HParser* in(const uint8_t *charset, size_t length) {
+  HParser* h__in(const uint8_t *charset, size_t length) {
     return h_action(h_in(charset, length), h__to_char, NULL);
   }
-  HParser* not_in(const uint8_t *charset, size_t length) {
+  HParser* h__not_in(const uint8_t *charset, size_t length) {
     return h_action(h_not_in(charset, length), h__to_char, NULL);
   }
+  HParser* action(HParser *parser, SV* sub) {
+    return h_action(parser, call_action, SvREFCNT_inc(sub));
+  }
  }
 
 %extend HParser_ {
@@ -154,3 +215,19 @@
         return h_compile($self, backend, NULL) == 0;
     }
 }
+
+%perlcode %{
+  sub sequence {
+    return hammerc::h_sequence__a([@_]);
+  }
+  sub choice {
+    return hammerc::h_choice__a([@_]);
+  }
+  sub in {
+    return h__in(join('',@_));
+  }
+  sub not_in {
+    return h__not_in(join('',@_));
+  }
+
+  %}
diff --git a/src/bindings/perl/t/hammer.t b/src/bindings/perl/t/hammer.t
index 81e441076560fc482e73eab2aa9e2caa851ba23e..424aa9c03e9b4b033311a76209b484440da2afa9 100644
--- a/src/bindings/perl/t/hammer.t
+++ b/src/bindings/perl/t/hammer.t
@@ -1,8 +1,10 @@
 # -*- cperl -*-
 use warnings;
-use Test::More tests => 12;
+use strict;
+use Test::More tests => 21;
 use hammer;
 
+
 sub check_parse_eq {
   my ($parser, $input, $expected) = @_;
   my $actual;
@@ -10,6 +12,7 @@ sub check_parse_eq {
     $actual = $parser->parse($input);
   };
   if ($@) {
+    diag($@);
     ok($@ eq "");
   } else {
     is_deeply($actual, $expected);
@@ -51,6 +54,7 @@ subtest "ch_range" => sub {
 
 SKIP: {
   use integer;
+  no warnings 'portable'; # I know the hex constants are not portable. that's why this test is skipped on <64 bit systems.
   skip "Needs 64-bit support", 2 if 0x4000000 * 2 eq -1; # TODO: Not sure if this works; may need $Config{ivsize} >= 8
   subtest "int64" => sub {
     my $parser = hammer::int64();
@@ -110,6 +114,78 @@ subtest "int_range" => sub { # test 12
   check_parse_failed($parser, "\x0b");
 };
 
-1;
+subtest "whitespace" => sub {
+  my $parser = hammer::whitespace(hammer::ch('a'));
+  check_parse_eq($parser, "a", "a");
+  check_parse_eq($parser, " a", "a");
+  check_parse_eq($parser, "  a", "a");
+  check_parse_eq($parser, "\t\n\ra", "a");
+};
+
+subtest "whitespace-end" => sub {
+  my $parser = hammer::whitespace(hammer::end_p());
+  check_parse_eq($parser, "", undef);
+  check_parse_eq($parser, "  ", undef);
+  check_parse_failed($parser, "  x", undef)
+};
+
+subtest "left" => sub { # test 15
+  my $parser = hammer::left(hammer::ch('a'),
+			    hammer::ch(' '));
+  check_parse_eq($parser, "a ", "a");
+  check_parse_failed($parser, "a");
+  check_parse_failed($parser, " ");
+};
+
+subtest "right" => sub {
+  my $parser = hammer::right(hammer::ch(' '),
+			     hammer::ch('a'));
+  check_parse_eq($parser, " a", "a");
+  check_parse_failed($parser, "a");
+  check_parse_failed($parser, " ");
+};
+
+subtest "middle" => sub {
+  my $parser = hammer::middle(hammer::ch(' '),
+			      hammer::ch('a'),
+			      hammer::ch(' '));
+  check_parse_eq($parser, " a ", "a");
+  for my $test_string (split('/', "a/ / a/a / b /ba / ab")) {
+    check_parse_failed($parser, $test_string);
+  }
+};
+
+subtest "action" => sub {
+  my $parser = hammer::action(hammer::sequence(hammer::choice(hammer::ch('a'),
+							      hammer::ch('A')),
+					       hammer::choice(hammer::ch('b'),
+							      hammer::ch('B'))),
+			      sub { [map(uc, @{+shift})]; });
+  check_parse_eq($parser, "ab", ['A', 'B']);
+  check_parse_eq($parser, "AB", ['A', 'B']);
+  check_parse_eq($parser, 'Ab', ['A', 'B']);
+  check_parse_failed($parser, "XX");
+};
+
+
+subtest "in" => sub {
+  my $parser = hammer::in('a'..'c');
+  check_parse_eq($parser, 'a', 'a');
+  check_parse_eq($parser, 'b', 'b');
+  check_parse_eq($parser, 'c', 'c');
+  check_parse_failed($parser, 'd');
+};
 
+subtest "not_in" => sub { # test 20
+  my $parser = hammer::not_in('a'..'c');
+  check_parse_failed($parser, 'a');
+  check_parse_failed($parser, 'b');
+  check_parse_failed($parser, 'c');
+  check_parse_eq($parser, 'd', 'd');
+};
 
+subtest "end_p" => sub {
+  my $parser = hammer::sequence(hammer::ch('a'), hammer::end_p());
+  check_parse_eq($parser, 'a', ['a']);
+  check_parse_failed($parser, 'aa');
+};