diff --git a/.travis.yml b/.travis.yml
index e55bf3ffd04abe0e7354501ea463e0ada25f2d84..2328d03275b8109b1756831e06a756df2b409ea4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -54,11 +54,18 @@ matrix:
       language: php
       php: "5.4"
       env: BINDINGS=php CC=clang
+    - compiler: gcc
+      language: dotnet
+      env: BINDINGS=dotnet
+    - compiler: clang
+      language: dotnet
+      env: BINDINGS=dotnet CC=clang
 before_install:
   - sudo apt-get update -qq
   - if [ "$BINDINGS" != "none" ]; then sudo apt-get install -qq swig; fi
   - if [ "$BINDINGS" == "perl" ]; then sudo add-apt-repository ppa:dns/irc -y; sudo apt-get update -qq; sudo apt-get install -qq swig=2.0.8-1irc1~12.04; fi
   - if [ "$BINDINGS" == "python" ]; then sudo apt-get install -qq python-dev; fi
+  - if [ "$BINDINGS" == "dotnet" ]; then sudo add-apt-repository ppa:directhex/monoxide -y; sudo apt-get update -qq; sudo apt-get install -qq mono-devel mono-mcs nunit nunit-console; mozroots --import --sync; fi
 
 install: true
 before_script:
diff --git a/HACKING b/HACKING
index 970a2491e9bcc03b39cc7e3fd18b74c2da136d0a..44f59912c50edd2bf76f01dbe19652926abc41e6 100644
--- a/HACKING
+++ b/HACKING
@@ -59,4 +59,8 @@ There is a language-independent representation of the Hammer test
 suite in `lib/test-suite`.  This is intended to be used with the
 tsparser.pl prolog library, along with a language-specific frontend.
 
-No language-specific frontends have been written yet.
+Only the C# frontend exists so far; to regenerate the test suites using it, run
+
+    $ swipl -q  -t halt -g tsgencsharp:prolog tsgencsharp.pl \
+          >../src/bindings/dotnet/test/hammer_tests.cs
+
diff --git a/README.md b/README.md
index 39834b32d1e2d6805f4a483779f3063f5ed33f4a..1e1dee9a10e599ca18f4b03f1adfb7ec386ba99c 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Features
   * Perl
   * [Go](https://github.com/prevoty/hammer)
   * PHP
-  * .NET (not yet implemented)
+  * .NET 
 
 Installing
 ==========
@@ -39,8 +39,10 @@ Installing
 * python2.7-dev (for Python bindings)
 * a JDK (for Java bindings)
 * a working [phpenv](https://github.com/CHH/phpenv) configuration (for PHP bindings)
+* mono-devel and mono-mcs (>= 3.0.6) (for .NET bindings)
+* nunit (for testing .NET bindings)
 
-To build, type `scons`. To run the built-in test suite, type `scons test`. For a debug build, add `--variant=debug`
+To build, type `scons`. To run the built-in test suite, type `scons test`. For a debug build, add `--variant=debug`.
 
 To build bindings, pass a "bindings" argument to scons, e.g. `scons bindings=python`. `scons bindings=python test` will build Python bindings and run tests for both C and Python. `--variant=debug` is valid here too. You can build more than one set of bindings at a time; just separate them with commas, e.g. `scons bindings=python,perl`.
 
@@ -69,6 +71,8 @@ The Python bindings only work with Python 2.7. SCons doesn't work with Python 3,
 
 The requirement for SWIG >= 2.0.8 for Perl bindings is due to a [known bug](http://sourceforge.net/p/swig/patches/324/) in SWIG. [ppa:dns/irc](https://launchpad.net/~dns/+archive/irc) has backports of SWIG 2.0.8 for Ubuntu versions 10.04-12.10; you can also [build SWIG from source](http://www.swig.org/download.html).
 
+The .NET bindings are for Mono 3.0.6 and greater. If you're on a Debian-based distro that only provides Mono 2 (e.g., Ubuntu 12.04), there are backports for [3.0.x](http://www.meebey.net/posts/mono_3.0_preview_debian_ubuntu_packages/), and a [3.2.x PPA](https://launchpad.net/~directhex/+archive/monoxide) maintained by the Mono team.
+
 Community
 =========
 Please join us at `#hammer` on `irc.upstandinghackers.com` if you have any questions or just want to talk about parsing.
diff --git a/SConstruct b/SConstruct
index a9f9c670cab5505c3d3043e0e7937c94c88f065c..17b1009e0f2121a07de9bc94da8d889e17d91e6f 100644
--- a/SConstruct
+++ b/SConstruct
@@ -7,9 +7,12 @@ import sys
 vars = Variables(None, ARGUMENTS)
 vars.Add(PathVariable('DESTDIR', "Root directory to install in (useful for packaging scripts)", None, PathVariable.PathIsDirCreate))
 vars.Add(PathVariable('prefix', "Where to install in the FHS", "/usr/local", PathVariable.PathAccept))
-vars.Add(ListVariable('bindings', 'Language bindings to build', 'none', ['python', 'perl', 'php']))
+vars.Add(ListVariable('bindings', 'Language bindings to build', 'none', ['dotnet', 'perl', 'php', 'python']))
 
-env = Environment(ENV = {'PATH' : os.environ['PATH']}, variables = vars, tools=['default', 'scanreplace'], toolpath=['tools'])
+env = Environment(ENV = {'PATH' : os.environ['PATH']},
+                  variables = vars,
+                  tools=['default', 'scanreplace', 'csharp/mono'],
+                  toolpath=['tools'])
 
 if not 'bindings' in env:
     env['bindings'] = []
diff --git a/lib/test-suite b/lib/test-suite
index 207ece4054fabb0c31041e8c394cbe28b60c33ef..6c15b3d60fe77e81ce0594e3f993726bb0f5f53f 100644
--- a/lib/test-suite
+++ b/lib/test-suite
@@ -215,7 +215,7 @@ many1 {
   test "daabbabadef" --> fail;
 }
 
-repeat-n {
+repeat_n {
   parser repeat_n(choice(ch('a'),ch('b')),0x2);
   test "adef" --> fail;
   test "abdef" --> ['a','b'];
@@ -270,24 +270,27 @@ and {
 }
 
 not {
-  parser sequence(ch('a'), choice(token('+'), token("++")), ch('b'));
+  parser sequence(ch('a'), choice(token("+"), token("++")), ch('b'));
   test "a+b" --> ['a',"+",'b'];
   test "a++b" --> fail;
 
-  parser sequence(ch('a'), choice(sequence(token('+'), not(ch('+'))),
+  parser sequence(ch('a'), choice(sequence(token("+"), not(ch('+'))),
                                   token("++")),
                   ch('b'));
   test "a+b" --> ['a', ["+"], 'b'];
   test "a++b" --> ['a', "++", 'b'];
 }
 
-leftrec {
-  subparser $lr = choice(sequence($lr, ch('a')), epsilon_p());
-  parser $lr;
-  test "a" --> ['a'];
-  test "aa" --> [['a'],'a'];
-  test "aaa" --> [[['a'],'a'],'a'];
-}
+## This doesn't work for some reason; it segfaults. We'll leave it for
+## later.
+#
+#leftrec {
+#  subparser $lr = choice(sequence($lr, ch('a')), epsilon_p());
+#  parser $lr;
+#  test "a" --> ['a'];
+#  #test "aa" --> [['a'],'a'];
+#  #test "aaa" --> [[['a'],'a'],'a'];
+#}
 
 rightrec {
   subparser $rr = choice(sequence(ch('a'), $rr), epsilon_p());
diff --git a/lib/tsgencsharp.pl b/lib/tsgencsharp.pl
new file mode 100644
index 0000000000000000000000000000000000000000..a31ffd02cf07bf730b70bf952783648c7334ed7d
--- /dev/null
+++ b/lib/tsgencsharp.pl
@@ -0,0 +1,212 @@
+% -*- prolog -*-
+% Run with:
+% $ swipl -q  -t halt -g tsgencsharp:prolog tsgencsharp.pl >output-file
+% Note: this needs to be run from the lib/ directory.
+
+% So,
+% swipl -q  -t halt -g tsgencsharp:prolog tsgencsharp.pl >../src/bindings/dotnet/test/hammer_tests.cs 
+
+
+:- module(tsgencsharp,
+          [gen_ts/2]).
+
+:- expects_dialect(swi).
+:- use_module(tsparser).
+
+% TODO: build a Box-like pretty-printer
+
+format_parser_name(Name, Result) :-
+    atom_codes(Name, [CInit|CName]),
+    code_type(RInit, to_upper(CInit)),
+    append("Hammer.", [RInit|CName], Result), !.
+
+format_test_name(Name, Result) :-
+    atom_codes(Name, [CInit|CName]),
+    code_type(RInit, to_upper(CInit)),
+    append("Test", [RInit|CName], Result), !.
+
+indent(0) --> "", !.
+indent(N) -->
+    {N > 0},
+    "    ",
+    {Np is N - 1},
+    indent(Np).
+
+pp_char_guts(0x22) -->
+    "\\\"", !.
+pp_char_guts(0x27) -->
+    "\\'", !.
+pp_char_guts(A) -->
+    { A >= 0x20, A < 0x7F } ->
+    [A];
+    "\\x",
+    { H is A >> 4, L is A /\ 0xF,
+      code_type(Hc, xdigit(H)),
+      code_type(Lc, xdigit(L)) },
+    [Hc,Lc].
+
+pp_hexnum_guts(0) --> !.
+pp_hexnum_guts(A) -->
+    { L is A /\ 0xF,
+      H is A >> 4,
+      code_type(Lc, xdigit(L)) },
+    pp_hexnum_guts(H),
+    [Lc], !.
+pp_string_guts([]) --> !.
+pp_string_guts([X|Xs]) -->
+    pp_char_guts(X),
+    pp_string_guts(Xs), !.
+
+pp_parser_args([]) --> !.
+pp_parser_args([X|Rest]) -->
+    pp_parser(X),
+    pp_parser_args_rest(Rest).
+pp_parser_args_rest([]) --> !.
+pp_parser_args_rest([X|Xs]) -->
+    ", ",
+    pp_parser(X),
+    pp_parser_args_rest(Xs).
+
+pp_parser(parser(Name, Args)) -->
+    !,
+    {format_parser_name(Name,Fname)},
+    Fname,
+    "(",
+    pp_parser_args(Args),
+    ")".
+pp_parser(string(Str)) --> !,
+    "\"",
+    pp_string_guts(Str),
+    "\"", !.
+pp_parser(num(0)) --> "0", !.
+pp_parser(num(Num)) --> !,
+    ( {Num < 0} ->
+      "-0x", {RNum is -Num}; "0x", {RNum = Num} ),
+    pp_hexnum_guts(RNum).
+pp_parser(char(C)) --> !,
+    "'", pp_char_guts(C), "'", !.
+
+pp_parser(ref(Name)) -->
+    {atom_codes(Name,CName)},
+    "sp_", CName, !.
+
+
+pp_parser(A) -->
+    { writef("WTF is a %w?\n", [A]),
+      !, fail
+    }.
+
+pp_test_elem(decl, parser(_)) --> !.
+pp_test_elem(init, parser(_)) --> !.
+pp_test_elem(exec, parser(P)) -->
+    !, indent(3),
+    "parser = ",
+    pp_parser(P),
+    ";\n".
+pp_test_elem(decl, subparser(Name,_)) -->
+    !, indent(3),
+    "IndirectParser ", pp_parser(ref(Name)),
+    " = Hammer.Indirect();\n".
+pp_test_elem(init, subparser(Name, Parser)) -->
+    !, indent(3),
+    pp_parser(ref(Name)), ".Bind(",
+    pp_parser(Parser),
+    ");\n".
+pp_test_elem(exec, subparser(_,_)) --> !.
+pp_test_elem(decl, test(_,_)) --> !.
+pp_test_elem(init, test(_,_)) --> !.
+pp_test_elem(decl, testFail(_)) --> !.
+pp_test_elem(init, testFail(_)) --> !.
+pp_test_elem(exec, test(Str, Result)) -->
+    !, indent(3),
+    "  CheckParseOK(parser, ", pp_parser(string(Str)),
+    ", ",
+    pp_parse_result(Result),
+    ");\n".
+pp_test_elem(exec, testFail(Str)) -->
+    !, indent(3),
+    "  CheckParseFail(parser, ", pp_parser(string(Str)),
+    ");\n".
+
+% pp_test_elem(_, _) --> !.
+
+pp_result_seq([]) --> !.
+pp_result_seq([X|Xs]) --> !,
+    pp_parse_result(X),
+    pp_result_seq_r(Xs).
+pp_result_seq_r([]) --> !.
+pp_result_seq_r([X|Xs]) --> !,
+    ", ",
+    pp_parse_result(X),
+    pp_result_seq_r(Xs).
+
+pp_byte_seq([]) --> !.
+pp_byte_seq([X|Xs]) --> !,
+    pp_parser(num(X)),
+    pp_byte_seq_r(Xs).
+pp_byte_seq_r([]) --> !.
+pp_byte_seq_r([X|Xs]) --> !,
+    ", ",
+    pp_parser(num(X)),
+    pp_byte_seq_r(Xs).
+
+pp_parse_result(char(C)) --> !,
+    %"(System.UInt64)",
+    pp_parser(char(C)).
+pp_parse_result(seq(Args)) --> !,
+    "new object[]{ ", pp_result_seq(Args), "}".
+pp_parse_result(none) --> !,
+    "null".
+pp_parse_result(uint(V)) --> !,
+    "(System.UInt64)", pp_parser(num(V)).
+pp_parse_result(sint(V)) --> !,
+    "(System.Int64)(", pp_parser(num(V)), ")".
+pp_parse_result(string(A)) --> !,
+    "new byte[]{ ", pp_byte_seq(A), "}".
+%pp_parse_result(A) -->
+%    "\x1b[1;31m",
+%    {with_output_to(codes(C), write(A))},
+%    C,
+%    "\x1b[0m".
+
+
+pp_test_elems(_, []) --> !.
+pp_test_elems(Phase, [X|Xs]) -->
+    !,
+    pp_test_elem(Phase,X),
+    pp_test_elems(Phase,Xs).
+
+pp_test_case(testcase(Name, Elems)) -->
+    !,
+    indent(2), "[Test]\n",
+    { format_test_name(Name, TName) },
+    indent(2), "public void ", TName, "() {\n",
+    indent(3), "Parser parser;\n",
+    pp_test_elems(decl, Elems),
+    pp_test_elems(init, Elems),
+    pp_test_elems(exec, Elems),
+    indent(2), "}\n".
+
+
+pp_test_cases([]) --> !.
+pp_test_cases([A|As]) -->
+    pp_test_case(A),
+    pp_test_cases(As).
+
+pp_test_suite(Suite) -->
+    "namespace Hammer.Test {\n",
+    indent(1), "using NUnit.Framework;\n",
+    %indent(1), "using Hammer;\n",
+    indent(1), "[TestFixture]\n",
+    indent(1), "public partial class HammerTest {\n",
+    pp_test_cases(Suite),
+    indent(1), "}\n",
+    "}\n".
+
+gen_ts(Foo,Str) :-
+    phrase(pp_test_suite(Foo),Str).
+
+prolog :-
+    read_tc(A),
+    gen_ts(A, Res),
+    writef("%s", [Res]).
diff --git a/src/bindings/dotnet/README.md b/src/bindings/dotnet/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c206056f3f470bd0a9507edc2e73b17bd300219f
--- /dev/null
+++ b/src/bindings/dotnet/README.md
@@ -0,0 +1 @@
+The minimum version of Mono required to use these bindings is 3.0.x. The 2.x series is not currently supported, though we'll happily accept a PR for one.
\ No newline at end of file
diff --git a/src/bindings/dotnet/SConscript b/src/bindings/dotnet/SConscript
new file mode 100644
index 0000000000000000000000000000000000000000..94f874ee41cc4741cff950ef4a88478dcfc06b31
--- /dev/null
+++ b/src/bindings/dotnet/SConscript
@@ -0,0 +1,59 @@
+# -*- python -*-
+import os.path
+Import("env libhammer_shared testruns targets")
+
+dotnetenv = env.Clone()
+
+dotnetenv.Append(CCFLAGS=["-fpic", '-DSWIG', '-Wno-all',
+                       '-Wno-extra', '-Wno-error',
+                       '-DHAMMER_INTERNAL__NO_STDARG_H'],
+              CPPPATH=["../.."],
+              LIBS=['hammer'],
+              LIBPATH=["../.."],
+              SWIGFLAGS=["-DHAMMER_INTERNAL__NO_STDARG_H",
+                         "-Isrc/", "-csharp",
+                         "-dllimport","hammer_dotnet",
+                         "-namespace", "Hammer.Internal"])
+import os
+
+swig = ['hammer.i']
+thisdir = os.path.join(os.path.dirname(str(libhammer_shared[0])), "bindings","dotnet")
+csfiles = os.path.join(thisdir, "*.cs")
+
+# These AlwaysBuilds are annoying, but alas there doesn't seem to be a
+# better way. I'd love to be corrected. Send PRs!
+
+# This also generates a bunch of .cs files, which we'll just use this
+# target to stand in for.
+hammer_wrap = AlwaysBuild(dotnetenv.Command(['hammer_wrap.c'], swig,
+                                            ["rm %s/*.cs || true" % (thisdir,),
+                                             "swig $SWIGFLAGS $SOURCE"]))
+libhammer_dotnet = dotnetenv.SharedLibrary(['hammer_dotnet'], hammer_wrap)
+hammer_dll = AlwaysBuild(dotnetenv.Command(['hammer.dll'], Glob('ext/*.cs'),
+                                           '$CSC -t:library -unsafe -out:$TARGET %s/*.cs $SOURCE' %(thisdir,)))
+Depends(hammer_dll, hammer_wrap)
+Default(libhammer_dotnet, hammer_dll)
+
+dotnettestenv = dotnetenv.Clone()
+
+def makeCIL(env, cmd):
+    libs = cmd.split(' ')
+    for lib in libs:
+        env.Append(CILLIBS=[lib[3:]])
+
+dotnettestenv.ParseConfig('pkg-config --libs nunit', makeCIL)
+dotnettestenv.Append(CILLIBS=[thisdir + "/hammer.dll"])
+dotnettestlib = dotnettestenv.CLILibrary('hammer_test.dll', Glob('test/*.cs'))
+Depends(dotnettestlib, hammer_dll)
+
+dotnettestenv['ENV']["LD_LIBRARY_PATH"] = ":".join([thisdir, os.path.dirname(str(libhammer_shared[0]))])
+dotnettestexec = dotnettestenv.Command(None, dotnettestlib, "nunit-console $SOURCE")
+Depends(dotnettestlib, hammer_dll)
+Depends(dotnettestlib, libhammer_dotnet)
+dotnettest = Alias("testdotnet", [dotnettestexec], dotnettestexec)
+AlwaysBuild(dotnettestexec)
+testruns.append(dotnettestexec)
+
+#dotnetinstallexec = dotnetenv.Command(None, libhammer_dotnet, "make install -C " + targetdir)
+#dotnetinstall = Alias("installdotnet", [dotnetinstallexec], dotnetinstallexec)
+#targets.append(dotnetinstall)
diff --git a/src/bindings/dotnet/ext/hammer.cs b/src/bindings/dotnet/ext/hammer.cs
new file mode 100644
index 0000000000000000000000000000000000000000..fa7de568f5b219c817ee9ef871cf1c05de8cbab3
--- /dev/null
+++ b/src/bindings/dotnet/ext/hammer.cs
@@ -0,0 +1,388 @@
+using Hammer.Internal;
+using System;
+using System.Runtime.InteropServices;
+using System.Collections;
+namespace Hammer
+{
+
+  public delegate Object HAction(Object obj);
+  public delegate bool HPredicate(Object obj);
+
+  public class ParseError : Exception
+  {
+    public readonly string Reason;
+    public ParseError() : this(null) {}
+    public ParseError(string reason) : base() {
+      Reason = reason;
+    }
+  }
+    
+
+  public class Parser
+  {
+    internal HParser wrapped;
+    internal System.Collections.IList pins; // objects that need to stay in scope for this one
+    internal Parser(HParser parser)
+    {
+      wrapped = parser;
+      pins = new System.Collections.ArrayList();
+    }
+    
+    internal Parser Pin(Object o)
+    {
+      pins.Add(o);
+      return this;
+    }
+
+    public Object Parse(byte[] str)
+    {
+      byte[] strp;
+      if (str.Length == 0)
+        strp = new byte[1];
+      else
+        strp = str;
+      try {
+        unsafe {
+          fixed(byte* b = &strp[0]) {
+            HParseResult res = hammer.h_parse(wrapped, (IntPtr)b, (uint)str.Length);
+            if (res != null) {
+              // TODO: free the PR
+              return Unmarshal(res.ast);
+            } else {
+              throw new ParseError();
+            }
+          }
+        }
+      } catch (ParseError e) {
+        return null;
+      }
+    }
+    
+    internal Object Unmarshal(HParsedToken tok)
+    {
+      // TODO
+      switch(tok.token_type) {
+      case HTokenType.TT_NONE:
+        return null;
+      case HTokenType.TT_BYTES:
+        {
+          byte[] ret = new byte[tok.token_data.bytes.len];
+          Marshal.Copy(tok.token_data.bytes.token,
+                       ret,
+                       0, ret.Length);
+          return ret;
+        }
+      case HTokenType.TT_SINT:
+        return (System.Int64)tok.token_data.sint;
+      case HTokenType.TT_UINT:
+        return (System.UInt64)tok.token_data._uint;
+      case HTokenType.TT_SEQUENCE:
+        {
+          Object[] ret = new Object[tok.token_data.seq.used];
+          for (uint i = 0; i < ret.Length; i++)
+            ret[i] = Unmarshal(tok.token_data.seq.at(i));
+          return ret;
+        }
+      default:
+        if (tok.token_type == Hammer.tt_dotnet)
+          {
+            HTaggedToken tagged = hammer.h_parsed_token_get_tagged_token(tok);
+            Object cb = Hammer.tag_to_action[tagged.label];
+            Object unmarshalled = Unmarshal(tagged.token);
+            if (cb is HAction) {
+              HAction act = (HAction)cb;
+              return act(unmarshalled);
+            } else if (cb is HPredicate) {
+              HPredicate pred = (HPredicate)cb;
+              if (!pred(unmarshalled))
+                throw new ParseError("Predicate failed");
+              else
+                return unmarshalled;
+            }
+          }
+        throw new Exception("Should not reach here");
+      }
+    }
+    
+  }
+    
+  public class IndirectParser : Parser
+  {
+    internal IndirectParser(HParser parser)
+    : base(parser)
+    {
+    }
+    
+    public void Bind(Parser p)
+    {
+      hammer.h_bind_indirect(this.wrapped, p.wrapped);
+    }
+  }
+
+  public class Hammer
+  {
+    internal static IDictionary tag_to_action;
+    internal static ulong charify_action;
+    internal static HTokenType tt_dotnet;
+    static Hammer()
+    {
+      tt_dotnet = hammer.h_allocate_token_type("com.upstandinghackers.hammer.dotnet.tagged");
+      hammer.h_set_dotnet_tagged_token_type(tt_dotnet);
+      tag_to_action = new System.Collections.Hashtable();
+      charify_action = RegisterAction(x => {
+          //System.Console.WriteLine(x.GetType());
+          return char.ConvertFromUtf32((int)(ulong)x)[0];
+        });
+    }
+    
+    internal static ulong RegisterAction(HAction action)
+    {
+      ulong newAction = (ulong)tag_to_action.Count;
+      tag_to_action[newAction] = action;
+      return newAction;
+    }
+    
+    internal static ulong RegisterPredicate(HPredicate predicate)
+    {
+      ulong newPredicate = (ulong)tag_to_action.Count;
+      tag_to_action[newPredicate] = predicate;
+      return newPredicate;
+    }
+    
+    internal static Parser CharParser(Parser p)
+    {
+      return new Parser(hammer.h_tag(p.wrapped, charify_action)).Pin(p);
+    }
+
+    internal static byte[] ToBytes(string s)
+    {
+      // Probably not what you want unless you're parsing binary data.
+      // This is just a one-to-one encoding of the string's codepoints
+      byte[] ret = new byte[s.Length];
+      for (int i = 0; i < s.Length; i++)
+        {
+          ret[i] = (byte)s[i];
+        }
+      return ret;
+    }
+    
+    internal static IntPtr[] BuildParserArray(Parser[] parsers)
+    {
+      IntPtr[] rlist = new IntPtr[parsers.Length+1];
+      for (int i = 0; i < parsers.Length; i++)
+        {
+          rlist[i] = HParser.getCPtr(parsers[i].wrapped).Handle;
+        }
+      rlist[parsers.Length] = IntPtr.Zero;
+      return rlist;
+    }
+    public static Parser Sequence(params Parser[] parsers)
+    {
+      // TODO
+      IntPtr[] plist = BuildParserArray(parsers);
+      unsafe
+        {
+          fixed (IntPtr *pp = &plist[0])
+            {
+              return new Parser(hammer.h_sequence__a((IntPtr)pp)).Pin(parsers);
+            }
+        }
+    }
+
+    public static Parser Choice(params Parser[] parsers)
+    {
+      // TODO
+      IntPtr[] plist = BuildParserArray(parsers);
+      unsafe
+        {
+          fixed (IntPtr *pp = &plist[0])
+            {
+              return new Parser(hammer.h_choice__a((IntPtr)pp)).Pin(parsers);
+            }
+        }
+    }
+
+    public static IndirectParser Indirect()
+    {
+      return new IndirectParser(hammer.h_indirect());
+    }
+
+    public static Parser Ch(byte ch)
+    {
+      return CharParser(new Parser(hammer.h_ch(ch)));
+      
+    }
+    public static Parser Ch(char ch)
+    {
+      return Ch((byte)ch);
+    }
+
+    public static Parser Ch_range(byte c1, byte c2)
+    {
+      return CharParser(new Parser(hammer.h_ch_range(c1, c2)));
+    }
+
+    public static Parser Ch_range(char c1, char c2)
+    {
+      return CharParser(new Parser(hammer.h_ch_range((byte)c1, (byte)c2)));
+    }
+
+    public static Parser Int_range(Parser p, System.Int64 i1, System.Int64 i2)
+    {
+      return new Parser(hammer.h_int_range(p.wrapped, i1, i2));
+    }
+    
+    public static Parser Token(byte[] token)
+    {
+      unsafe {
+        fixed(byte* b = &token[0])
+          {
+            return new Parser(hammer.h_token((IntPtr)b, (uint)token.Length));
+          }
+      }
+    }
+
+    public static Parser In(byte[] charset)
+    {
+      unsafe {
+        fixed(byte* b = &charset[0])
+          {
+            return CharParser(new Parser(hammer.h_in((IntPtr)b, (uint)charset.Length)));
+          }
+      }
+    }
+
+    public static Parser Not_in(byte[] charset)
+    {
+      unsafe {
+        fixed(byte* b = &charset[0])
+          {
+            return CharParser(new Parser(hammer.h_not_in((IntPtr)b, (uint)charset.Length)));
+          }
+      }
+    }
+
+    public static Parser Token(string token)
+    {
+      // Encodes in UTF-8
+      return Token(ToBytes(token));
+    }
+
+    public static Parser In(string charset)
+    {
+      // Encodes in UTF-8
+      return In(ToBytes(charset));
+    }
+
+    public static Parser Not_in(string charset)
+    {
+      // Encodes in UTF-8
+      return Not_in(ToBytes(charset));
+    }
+
+    // No-arg parsers
+    public static Parser Int8() {return new Parser(hammer.h_int8());}
+    public static Parser Int16() {return new Parser(hammer.h_int16());}
+    public static Parser Int32() {return new Parser(hammer.h_int32());}
+    public static Parser Int64() {return new Parser(hammer.h_int64());}
+    public static Parser Uint8() {return new Parser(hammer.h_uint8());}
+    public static Parser Uint16() {return new Parser(hammer.h_uint16());}
+    public static Parser Uint32() {return new Parser(hammer.h_uint32());}
+    public static Parser Uint64() {return new Parser(hammer.h_uint64());}
+    
+    public static Parser End_p() {return new Parser(hammer.h_end_p());}
+    public static Parser Nothing_p() {return new Parser(hammer.h_nothing_p());}
+    public static Parser Epsilon_p() {return new Parser(hammer.h_epsilon_p());}
+
+    // 1-arg parsers
+    public static Parser Ignore(Parser p)
+    {
+      return new Parser(hammer.h_ignore(p.wrapped)).Pin(p);
+    }
+
+    public static Parser Not(Parser p)
+    {
+      return new Parser(hammer.h_not(p.wrapped)).Pin(p);
+    }
+
+    public static Parser Whitespace(Parser p)
+    {
+      return new Parser(hammer.h_whitespace(p.wrapped)).Pin(p);
+    }
+
+    public static Parser Optional(Parser p)
+    {
+      return new Parser(hammer.h_optional(p.wrapped)).Pin(p);
+    }
+
+    public static Parser And(Parser p)
+    {
+      return new Parser(hammer.h_and(p.wrapped)).Pin(p);
+    }
+    
+    public static Parser Many(Parser p)
+    {
+      return new Parser(hammer.h_many(p.wrapped)).Pin(p);
+    }
+
+    public static Parser Many1(Parser p)
+    {
+      return new Parser(hammer.h_many1(p.wrapped)).Pin(p);
+    }
+
+    public static Parser SepBy(Parser p, Parser sep)
+    {
+      return new Parser(hammer.h_sepBy(p.wrapped, sep.wrapped)).Pin(p);
+    }
+
+    public static Parser SepBy1(Parser p, Parser sep)
+    {
+      return new Parser(hammer.h_sepBy1(p.wrapped, sep.wrapped)).Pin(p);
+    }
+
+    // 2-arg parsers
+    
+    public static Parser Left(Parser p1, Parser p2)
+    {
+      return new Parser(hammer.h_left(p1.wrapped, p2.wrapped)).Pin(p1).Pin(p2);
+    }
+    public static Parser Right(Parser p1, Parser p2)
+    {
+      return new Parser(hammer.h_right(p1.wrapped, p2.wrapped));
+    }
+    public static Parser Xor(Parser p1, Parser p2)
+    {
+      return new Parser(hammer.h_xor(p1.wrapped, p2.wrapped));
+    }
+    public static Parser Difference(Parser p1, Parser p2)
+    {
+      return new Parser(hammer.h_difference(p1.wrapped, p2.wrapped));
+    }
+    public static Parser Butnot(Parser p1, Parser p2)
+    {
+      return new Parser(hammer.h_butnot(p1.wrapped, p2.wrapped));
+    }
+
+
+    // Multi-arg parsers
+    public static Parser Middle(Parser p1, Parser p2, Parser p3)
+    {
+      return new Parser(hammer.h_middle(p1.wrapped, p2.wrapped, p3.wrapped));
+    }
+    public static Parser Repeat_n(Parser p, uint count)
+    {
+      return new Parser(hammer.h_repeat_n(p.wrapped, count));
+    }
+    public static Parser Action(Parser p, HAction action)
+    {
+      ulong actionNo = Hammer.RegisterAction(action);
+      return new Parser(hammer.h_tag(p.wrapped, actionNo)).Pin(p).Pin(action);
+    }
+    public static Parser AttrBool(Parser p, HPredicate predicate)
+    {
+      ulong predNo = Hammer.RegisterPredicate(predicate);
+      return new Parser(hammer.h_tag(p.wrapped, predNo)).Pin(p).Pin(predicate);
+    }
+  }
+  
+}
\ No newline at end of file
diff --git a/src/bindings/dotnet/hammer.i b/src/bindings/dotnet/hammer.i
new file mode 100644
index 0000000000000000000000000000000000000000..98ef59b0dc7ea30e52b83343880bf1be3e7b1f43
--- /dev/null
+++ b/src/bindings/dotnet/hammer.i
@@ -0,0 +1,80 @@
+%module hammer;
+
+%include "stdint.i"
+
+ // Special attention needs to be paid to:
+ // h_parse
+ // h_token
+ // h_in
+ // h_not_in
+
+ //%typemap(cstype)  uint8_t* "byte[]"
+ //%typemap(csin, pre="unsafe { fixed(byte* temp$csinput = &$csinput[0]) {", terminator="}}") uint8_t* "(IntPtr)temp$csinput"
+ //%typemap(csvarin) uint8_t
+%typemap(imtype) uint8_t* "IntPtr"
+%typemap(cstype) uint8_t* "IntPtr"
+%typemap(csin) uint8_t* "$csinput"
+%typemap(csvarout) uint8_t* %{
+    get {
+      return $imcall;
+    }
+  %}
+
+%typemap(imtype) void*[] "IntPtr"
+%typemap(cstype) void*[] "IntPtr"
+%typemap(csin) void*[] "$csinput"
+
+%ignore h_bit_writer_get_buffer;
+//%apply (char *STRING, size_t LENGTH) {(uint8_t* str, size_t len)};
+//%apply (uint8_t* str, size_t len) {(const uint8_t* input, size_t length)}
+//%apply (uint8_t* str, size_t len) {(const uint8_t* str, const size_t len)}
+//%apply (uint8_t* str, size_t len) {(const uint8_t* charset, size_t length)}
+
+%typemap(csclassmodifiers) SWIGTYPE "internal class";
+%csmethodmodifiers "internal";
+
+%extend HCountedArray_ {
+  HParsedToken* at(unsigned int posn) {
+    if (posn >= $self->used)
+      return NULL;
+    return $self->elements[posn];
+  }
+ }
+
+%include "../swig/hammer.i";
+
+%{
+HTokenType dotnet_tagged_token_type;
+ %}
+%inline {
+  void h_set_dotnet_tagged_token_type(HTokenType new_tt) {
+    dotnet_tagged_token_type = new_tt;
+  }
+  // Need this inline as well
+  struct HTaggedToken {
+    HParsedToken *token;
+    uint64_t label;
+  };
+
+// this is to make it easier to access via SWIG
+struct HTaggedToken *h_parsed_token_get_tagged_token(HParsedToken* hpt) {
+  return (struct HTaggedToken*)hpt->token_data.user;
+}
+
+HParsedToken *act_tag(const HParseResult* p, void* user_data) {
+  struct HTaggedToken *tagged = H_ALLOC(struct HTaggedToken);
+  tagged->label = *(uint64_t*)user_data;
+  tagged->token = p->ast;
+  return h_make(p->arena, dotnet_tagged_token_type, tagged);
+}
+
+HParser *h_tag__m(HAllocator *mm__, HParser *p, uint64_t tag) {
+  uint64_t *tagptr = h_new(uint64_t, 1);
+  *tagptr = tag;
+  return h_action__m(mm__, p, act_tag, tagptr);
+}
+
+HParser *h_tag(HParser *p, uint64_t tag) {
+  return h_tag__m(&system_allocator, p, tag);
+}
+ }
diff --git a/src/bindings/dotnet/test/hammer_hand_tests.cs b/src/bindings/dotnet/test/hammer_hand_tests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..c6aaac7b7612366852913fc434a405dad7f1a91f
--- /dev/null
+++ b/src/bindings/dotnet/test/hammer_hand_tests.cs
@@ -0,0 +1,35 @@
+namespace Hammer.Test
+{
+  using NUnit.Framework;
+  [TestFixture]
+  public partial class HammerTest
+  {
+    [Test]
+    public void TestAction()
+    {
+      Parser parser = Hammer.Action(Hammer.Sequence(Hammer.Choice(Hammer.Ch('a'),
+                                                                  Hammer.Ch('A')),
+                                                    Hammer.Choice(Hammer.Ch('b'),
+                                                                  Hammer.Ch('B'))),
+                                    (HAction)(x => string.Join(",",(object[])x)));
+      CheckParseOK(parser, "ab", "a,b");
+      CheckParseOK(parser, "AB", "A,B");
+      CheckParseFail(parser, "XX");
+    }
+    [Test]
+    public void TestAttrBool()
+    {
+      Parser parser = Hammer.AttrBool(Hammer.Many1(Hammer.Choice(Hammer.Ch('a'),
+                                                                 Hammer.Ch('b'))),
+                                      (HPredicate)(x => { 
+                                          object[] elems = (object[])x;
+                                          return elems.Length > 1 && (char)elems[0] == (char)elems[1];
+                                        }));
+      
+      CheckParseOK(parser, "aa", new object[]{ 'a','a' });
+      CheckParseOK(parser, "bb", new object[]{ 'b','b' });
+      CheckParseFail(parser, "ab");
+                                                                  
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/bindings/dotnet/test/hammer_tests.cs b/src/bindings/dotnet/test/hammer_tests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ac0ac9e2c53c44fa839ea1879d3bbdb74cea3ce8
--- /dev/null
+++ b/src/bindings/dotnet/test/hammer_tests.cs
@@ -0,0 +1,316 @@
+namespace Hammer.Test {
+    using NUnit.Framework;
+    [TestFixture]
+    public partial class HammerTest {
+        [Test]
+        public void TestToken() {
+            Parser parser;
+            parser = Hammer.Token("95\xa2");
+              CheckParseOK(parser, "95\xa2", new byte[]{ 0x39, 0x35, 0xa2});
+              CheckParseFail(parser, "95\xa2");
+        }
+        [Test]
+        public void TestCh() {
+            Parser parser;
+            parser = Hammer.Ch(0xa2);
+              CheckParseOK(parser, "\xa2", '\xa2');
+              CheckParseFail(parser, "\xa3");
+        }
+        [Test]
+        public void TestCh_range() {
+            Parser parser;
+            parser = Hammer.Ch_range(0x61, 0x63);
+              CheckParseOK(parser, "b", 'b');
+              CheckParseFail(parser, "d");
+        }
+        [Test]
+        public void TestInt64() {
+            Parser parser;
+            parser = Hammer.Int64();
+              CheckParseOK(parser, "\xff\xff\xff\xfe\x00\x00\x00\x00", (System.Int64)(-0x200000000));
+              CheckParseFail(parser, "\xff\xff\xff\xfe\x00\x00\x00");
+        }
+        [Test]
+        public void TestInt32() {
+            Parser parser;
+            parser = Hammer.Int32();
+              CheckParseOK(parser, "\xff\xfe\x00\x00", (System.Int64)(-0x20000));
+              CheckParseFail(parser, "\xff\xfe\x00");
+              CheckParseOK(parser, "\x00\x02\x00\x00", (System.Int64)(0x20000));
+              CheckParseFail(parser, "\x00\x02\x00");
+        }
+        [Test]
+        public void TestInt16() {
+            Parser parser;
+            parser = Hammer.Int16();
+              CheckParseOK(parser, "\xfe\x00", (System.Int64)(-0x200));
+              CheckParseFail(parser, "\xfe");
+              CheckParseOK(parser, "\x02\x00", (System.Int64)(0x200));
+              CheckParseFail(parser, "\x02");
+        }
+        [Test]
+        public void TestInt8() {
+            Parser parser;
+            parser = Hammer.Int8();
+              CheckParseOK(parser, "\x88", (System.Int64)(-0x78));
+              CheckParseFail(parser, "");
+        }
+        [Test]
+        public void TestUint64() {
+            Parser parser;
+            parser = Hammer.Uint64();
+              CheckParseOK(parser, "\x00\x00\x00\x02\x00\x00\x00\x00", (System.UInt64)0x200000000);
+              CheckParseFail(parser, "\x00\x00\x00\x02\x00\x00\x00");
+        }
+        [Test]
+        public void TestUint32() {
+            Parser parser;
+            parser = Hammer.Uint32();
+              CheckParseOK(parser, "\x00\x02\x00\x00", (System.UInt64)0x20000);
+              CheckParseFail(parser, "\x00\x02\x00");
+        }
+        [Test]
+        public void TestUint16() {
+            Parser parser;
+            parser = Hammer.Uint16();
+              CheckParseOK(parser, "\x02\x00", (System.UInt64)0x200);
+              CheckParseFail(parser, "\x02");
+        }
+        [Test]
+        public void TestUint8() {
+            Parser parser;
+            parser = Hammer.Uint8();
+              CheckParseOK(parser, "x", (System.UInt64)0x78);
+              CheckParseFail(parser, "");
+        }
+        [Test]
+        public void TestInt_range() {
+            Parser parser;
+            parser = Hammer.Int_range(Hammer.Uint8(), 0x3, 0x10);
+              CheckParseOK(parser, "\x05", (System.UInt64)0x5);
+              CheckParseFail(parser, "\x0b");
+        }
+        [Test]
+        public void TestWhitespace() {
+            Parser parser;
+            parser = Hammer.Whitespace(Hammer.Ch(0x61));
+              CheckParseOK(parser, "a", 'a');
+              CheckParseOK(parser, " a", 'a');
+              CheckParseOK(parser, "  a", 'a');
+              CheckParseOK(parser, "\x09a", 'a');
+              CheckParseFail(parser, "_a");
+            parser = Hammer.Whitespace(Hammer.End_p());
+              CheckParseOK(parser, "", null);
+              CheckParseOK(parser, "  ", null);
+              CheckParseFail(parser, "  x");
+        }
+        [Test]
+        public void TestLeft() {
+            Parser parser;
+            parser = Hammer.Left(Hammer.Ch(0x61), Hammer.Ch(0x20));
+              CheckParseOK(parser, "a ", 'a');
+              CheckParseFail(parser, "a");
+              CheckParseFail(parser, " ");
+              CheckParseFail(parser, "ba");
+        }
+        [Test]
+        public void TestMiddle() {
+            Parser parser;
+            parser = Hammer.Middle(Hammer.Ch(' '), Hammer.Ch('a'), Hammer.Ch(' '));
+              CheckParseOK(parser, " a ", 'a');
+              CheckParseFail(parser, "a");
+              CheckParseFail(parser, " a");
+              CheckParseFail(parser, "a ");
+              CheckParseFail(parser, " b ");
+              CheckParseFail(parser, "ba ");
+              CheckParseFail(parser, " ab");
+        }
+        [Test]
+        public void TestIn() {
+            Parser parser;
+            parser = Hammer.In("abc");
+              CheckParseOK(parser, "b", 'b');
+              CheckParseFail(parser, "d");
+        }
+        [Test]
+        public void TestNot_in() {
+            Parser parser;
+            parser = Hammer.Not_in("abc");
+              CheckParseOK(parser, "d", 'd');
+              CheckParseFail(parser, "a");
+        }
+        [Test]
+        public void TestEnd_p() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.End_p());
+              CheckParseOK(parser, "a", new object[]{ 'a'});
+              CheckParseFail(parser, "aa");
+        }
+        [Test]
+        public void TestNothing_p() {
+            Parser parser;
+            parser = Hammer.Nothing_p();
+              CheckParseFail(parser, "a");
+        }
+        [Test]
+        public void TestSequence() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Ch('b'));
+              CheckParseOK(parser, "ab", new object[]{ 'a', 'b'});
+              CheckParseFail(parser, "a");
+              CheckParseFail(parser, "b");
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Whitespace(Hammer.Ch('b')));
+              CheckParseOK(parser, "ab", new object[]{ 'a', 'b'});
+              CheckParseOK(parser, "a b", new object[]{ 'a', 'b'});
+              CheckParseOK(parser, "a  b", new object[]{ 'a', 'b'});
+        }
+        [Test]
+        public void TestChoice() {
+            Parser parser;
+            parser = Hammer.Choice(Hammer.Ch('a'), Hammer.Ch('b'));
+              CheckParseOK(parser, "a", 'a');
+              CheckParseOK(parser, "b", 'b');
+              CheckParseOK(parser, "ab", 'a');
+              CheckParseFail(parser, "c");
+        }
+        [Test]
+        public void TestButnot() {
+            Parser parser;
+            parser = Hammer.Butnot(Hammer.Ch('a'), Hammer.Token("ab"));
+              CheckParseOK(parser, "a", 'a');
+              CheckParseFail(parser, "ab");
+              CheckParseOK(parser, "aa", 'a');
+            parser = Hammer.Butnot(Hammer.Ch_range('0', '9'), Hammer.Ch('6'));
+              CheckParseOK(parser, "5", '5');
+              CheckParseFail(parser, "6");
+        }
+        [Test]
+        public void TestDifference() {
+            Parser parser;
+            parser = Hammer.Difference(Hammer.Token("ab"), Hammer.Ch('a'));
+              CheckParseOK(parser, "ab", new byte[]{ 0x61, 0x62});
+              CheckParseFail(parser, "a");
+        }
+        [Test]
+        public void TestXor() {
+            Parser parser;
+            parser = Hammer.Xor(Hammer.Ch_range('0', '6'), Hammer.Ch_range('5', '9'));
+              CheckParseOK(parser, "0", '0');
+              CheckParseOK(parser, "9", '9');
+              CheckParseFail(parser, "5");
+              CheckParseFail(parser, "a");
+        }
+        [Test]
+        public void TestMany() {
+            Parser parser;
+            parser = Hammer.Many(Hammer.Choice(Hammer.Ch('a'), Hammer.Ch('b')));
+              CheckParseOK(parser, "", new object[]{ });
+              CheckParseOK(parser, "a", new object[]{ 'a'});
+              CheckParseOK(parser, "b", new object[]{ 'b'});
+              CheckParseOK(parser, "aabbaba", new object[]{ 'a', 'a', 'b', 'b', 'a', 'b', 'a'});
+        }
+        [Test]
+        public void TestMany1() {
+            Parser parser;
+            parser = Hammer.Many1(Hammer.Choice(Hammer.Ch('a'), Hammer.Ch('b')));
+              CheckParseFail(parser, "");
+              CheckParseOK(parser, "a", new object[]{ 'a'});
+              CheckParseOK(parser, "b", new object[]{ 'b'});
+              CheckParseOK(parser, "aabbaba", new object[]{ 'a', 'a', 'b', 'b', 'a', 'b', 'a'});
+              CheckParseFail(parser, "daabbabadef");
+        }
+        [Test]
+        public void TestRepeat_n() {
+            Parser parser;
+            parser = Hammer.Repeat_n(Hammer.Choice(Hammer.Ch('a'), Hammer.Ch('b')), 0x2);
+              CheckParseFail(parser, "adef");
+              CheckParseOK(parser, "abdef", new object[]{ 'a', 'b'});
+              CheckParseFail(parser, "dabdef");
+        }
+        [Test]
+        public void TestOptional() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Optional(Hammer.Choice(Hammer.Ch('b'), Hammer.Ch('c'))), Hammer.Ch('d'));
+              CheckParseOK(parser, "abd", new object[]{ 'a', 'b', 'd'});
+              CheckParseOK(parser, "acd", new object[]{ 'a', 'c', 'd'});
+              CheckParseOK(parser, "ad", new object[]{ 'a', null, 'd'});
+              CheckParseFail(parser, "aed");
+              CheckParseFail(parser, "ab");
+              CheckParseFail(parser, "ac");
+        }
+        [Test]
+        public void TestIgnore() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Ignore(Hammer.Ch('b')), Hammer.Ch('c'));
+              CheckParseOK(parser, "abc", new object[]{ 'a', 'c'});
+              CheckParseFail(parser, "ac");
+        }
+        [Test]
+        public void TestSepBy() {
+            Parser parser;
+            parser = Hammer.SepBy(Hammer.Choice(Hammer.Ch('1'), Hammer.Ch('2'), Hammer.Ch('3')), Hammer.Ch(','));
+              CheckParseOK(parser, "1,2,3", new object[]{ '1', '2', '3'});
+              CheckParseOK(parser, "1,3,2", new object[]{ '1', '3', '2'});
+              CheckParseOK(parser, "1,3", new object[]{ '1', '3'});
+              CheckParseOK(parser, "3", new object[]{ '3'});
+              CheckParseOK(parser, "", new object[]{ });
+        }
+        [Test]
+        public void TestSepBy1() {
+            Parser parser;
+            parser = Hammer.SepBy1(Hammer.Choice(Hammer.Ch('1'), Hammer.Ch('2'), Hammer.Ch('3')), Hammer.Ch(','));
+              CheckParseOK(parser, "1,2,3", new object[]{ '1', '2', '3'});
+              CheckParseOK(parser, "1,3,2", new object[]{ '1', '3', '2'});
+              CheckParseOK(parser, "1,3", new object[]{ '1', '3'});
+              CheckParseOK(parser, "3", new object[]{ '3'});
+              CheckParseFail(parser, "");
+        }
+        [Test]
+        public void TestAnd() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.And(Hammer.Ch('0')), Hammer.Ch('0'));
+              CheckParseOK(parser, "0", new object[]{ '0'});
+              CheckParseFail(parser, "1");
+            parser = Hammer.Sequence(Hammer.And(Hammer.Ch('0')), Hammer.Ch('1'));
+              CheckParseFail(parser, "0");
+              CheckParseFail(parser, "1");
+            parser = Hammer.Sequence(Hammer.Ch('1'), Hammer.And(Hammer.Ch('2')));
+              CheckParseOK(parser, "12", new object[]{ '1'});
+              CheckParseFail(parser, "13");
+        }
+        [Test]
+        public void TestNot() {
+            Parser parser;
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Choice(Hammer.Token("+"), Hammer.Token("++")), Hammer.Ch('b'));
+              CheckParseOK(parser, "a+b", new object[]{ 'a', new byte[]{ 0x2b}, 'b'});
+              CheckParseFail(parser, "a++b");
+            parser = Hammer.Sequence(Hammer.Ch('a'), Hammer.Choice(Hammer.Sequence(Hammer.Token("+"), Hammer.Not(Hammer.Ch('+'))), Hammer.Token("++")), Hammer.Ch('b'));
+              CheckParseOK(parser, "a+b", new object[]{ 'a', new object[]{ new byte[]{ 0x2b}}, 'b'});
+              CheckParseOK(parser, "a++b", new object[]{ 'a', new byte[]{ 0x2b, 0x2b}, 'b'});
+        }
+        [Test]
+        public void TestRightrec() {
+            Parser parser;
+            IndirectParser sp_rr = Hammer.Indirect();
+            sp_rr.Bind(Hammer.Choice(Hammer.Sequence(Hammer.Ch('a'), sp_rr), Hammer.Epsilon_p()));
+            parser = sp_rr;
+              CheckParseOK(parser, "a", new object[]{ 'a'});
+              CheckParseOK(parser, "aa", new object[]{ 'a', new object[]{ 'a'}});
+              CheckParseOK(parser, "aaa", new object[]{ 'a', new object[]{ 'a', new object[]{ 'a'}}});
+        }
+        [Test]
+        public void TestAmbiguous() {
+            Parser parser;
+            IndirectParser sp_d = Hammer.Indirect();
+            IndirectParser sp_p = Hammer.Indirect();
+            IndirectParser sp_e = Hammer.Indirect();
+            sp_d.Bind(Hammer.Ch('d'));
+            sp_p.Bind(Hammer.Ch('+'));
+            sp_e.Bind(Hammer.Choice(Hammer.Sequence(sp_e, sp_p, sp_e), sp_d));
+            parser = sp_e;
+              CheckParseOK(parser, "d", 'd');
+              CheckParseOK(parser, "d+d", new object[]{ 'd', '+', 'd'});
+              CheckParseOK(parser, "d+d+d", new object[]{ new object[]{ 'd', '+', 'd'}, '+', 'd'});
+        }
+    }
+}
diff --git a/src/bindings/dotnet/test/test_support.cs b/src/bindings/dotnet/test/test_support.cs
new file mode 100644
index 0000000000000000000000000000000000000000..98e2cdd74c2ed08e75fc6e9361b24cb531beb899
--- /dev/null
+++ b/src/bindings/dotnet/test/test_support.cs
@@ -0,0 +1,125 @@
+using System;
+using Hammer;
+namespace Hammer.Test
+{
+  using NUnit.Framework;
+
+  public partial class HammerTest
+  {
+
+    protected bool DeepEquals(Object o1, Object o2)
+    {
+      if (o1.Equals(o2))
+        return true;
+      if (o1.GetType() != o2.GetType())
+        return false;
+      if (o1 is byte[])
+        {
+          byte[] a1 = (byte[])o1, a2 = (byte[])o2;
+          if (a1.Length != a2.Length)
+            return false;
+          for (uint i = 0; i < a1.Length; i++)
+            if (a1[i] != a2[i])
+              return false;
+          return true;
+        }
+      else if (o1 is Object[])
+        {
+          Object[] a1 = (Object[])o1, a2 = (Object[])o2;
+          if (a1.Length != a2.Length)
+            return false;
+          for (uint i = 0; i < a1.Length; i++)
+            if (!DeepEquals(a1[i],a2[i]))
+              return false;
+          return true;
+        }
+      else
+        return false;
+    }
+
+    protected static string ToString(Object o)
+    {
+      if (o == null)
+        {
+          return "null";
+        }
+      if (o is byte[])
+        {
+          string ret = "<";
+          byte[] a = (byte[])o;
+          for (uint i = 0; i < a.Length; i++)
+            {
+              if (i != 0)
+                ret += ".";
+              ret += a[i].ToString("X2");
+            }
+          ret += ">";
+          return ret;
+        }
+      else if (o is Object[])
+        {
+          Object[] a = (Object[])o;
+          string ret = "[";
+          
+          for (uint i = 0; i < a.Length; i++)
+            {
+              if (i != 0)
+                ret += " ";
+              ret += ToString(a[i]);
+            }
+          ret += "]";
+          return ret;
+        }
+      else if (o is System.Int64)
+        {
+          System.Int64 i = (System.Int64)o;
+          return (i < 0 ? "s-0x" : "s0x") + i.ToString("X");
+        }
+      else if (o is System.UInt64)
+        {
+          System.UInt64 i = (System.UInt64)o;
+          return "u0x" + i.ToString("X");
+        }
+      else if (o is System.String)
+        {
+          return "\"" + o.ToString() + "\"";
+        }
+      else if (o is System.Char)
+        {
+          return "\'" + o.ToString() + "\'";
+        }
+      else
+        return "WAT(" + o.GetType() + ")";
+    }
+    
+
+    internal static byte[] ToBytes(string s)
+    {
+      // Probably not what you want unless you're parsing binary data.
+      // This is just a one-to-one encoding of the string's codepoints
+      byte []ret = new byte[s.Length];
+      for (int i = 0; i < s.Length; i++)
+        {
+          ret[i] = (byte)s[i];
+        }
+      return ret;
+    }
+
+    protected void CheckParseOK(Parser p, string probe, Object expected)
+    {
+      Object ret = p.Parse(ToBytes(probe));
+      Assert.That(ret, Is.Not.Null);
+      //System.Console.WriteLine(ToString(ret));
+      //System.Console.WriteLine(ToString(expected));
+      if (!DeepEquals(ret, expected))
+        Assert.Fail();
+      else
+        Assert.Pass();
+    }
+    protected void CheckParseFail(Parser p, string probe)
+    {
+      Object ret = p.Parse(ToBytes(probe));
+      Assert.That(ret, Is.Null);
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/bindings/python/SConscript b/src/bindings/python/SConscript
index e7b956fac0b8f7ead58797d456834f35f8914f5c..dac2d9596a58fdd2e8dd4edbcde46aa31b4d6024 100644
--- a/src/bindings/python/SConscript
+++ b/src/bindings/python/SConscript
@@ -16,7 +16,7 @@ pytests = ['hammer_tests.py']
 pytestexec = pytestenv.Command(['hammer.pyc', 'hammer_tests.pyc'], pytests + libhammer_python, "LD_LIBRARY_PATH=" + os.path.dirname(str(libhammer_shared[0])) + " nosetests -vv $SOURCE")
 pytest = Alias("testpython", [pytestexec], pytestexec)
 AlwaysBuild(pytestexec)
-testruns.extend(pytest)
+testruns.append(pytest)
 
 pyinstallexec = pythonenv.Command(None, libhammer_python, 'python ' + os.path.join(pydir, 'setup.py ') + ' install')
 pyinstall = Alias("installpython", [pyinstallexec], pyinstallexec)
diff --git a/src/hammer.h b/src/hammer.h
index 2914b8ffee80f8f0409546a85f737e8920480d51..dc403c0c407fc6f786a3ce96cacea858ec6190ea 100644
--- a/src/hammer.h
+++ b/src/hammer.h
@@ -666,13 +666,13 @@ void h_benchmark_report(FILE* stream, HBenchmarkResults* results);
 
 // {{{ Token type registry
 /// Allocate a new, unused (as far as this function knows) token type.
-int h_allocate_token_type(const char* name);
+HTokenType h_allocate_token_type(const char* name);
 
 /// Get the token type associated with name. Returns -1 if name is unkown
-int h_get_token_type_number(const char* name);
+HTokenType h_get_token_type_number(const char* name);
 
 /// Get the name associated with token_type. Returns NULL if the token type is unkown
-const char* h_get_token_type_name(int token_type);
+const char* h_get_token_type_name(HTokenType token_type);
 // }}}
 
 #ifdef __cplusplus
diff --git a/src/registry.c b/src/registry.c
index c59b6ea9f6e0214c279d51b3997b5770278928bc..60aa8863e53b5c4c32175adb430fc87df069e901 100644
--- a/src/registry.c
+++ b/src/registry.c
@@ -22,14 +22,14 @@
 
 typedef struct Entry_ {
   const char* name;
-  int value;
+  HTokenType value;
 } Entry;
 
 static void *tt_registry = NULL;
 static Entry** tt_by_id = NULL;
-static int tt_by_id_sz = 0;
+static unsigned int tt_by_id_sz = 0;
 #define TT_START TT_USER
-static int tt_next = TT_START;
+static HTokenType tt_next = TT_START;
 
 /*
   // TODO: These are for the extension registry, which does not yet have a good name.
@@ -45,12 +45,12 @@ static int compare_entries(const void* v1, const void* v2) {
   return strcmp(e1->name, e2->name);
 }
 
-int h_allocate_token_type(const char* name) {
+HTokenType h_allocate_token_type(const char* name) {
   Entry* new_entry = malloc(sizeof(*new_entry));
   new_entry->name = name;
-  new_entry->value = -1;
+  new_entry->value = 0;
   Entry* probe = *(Entry**)tsearch(new_entry, &tt_registry, compare_entries);
-  if (probe->value != -1) {
+  if (probe->value != 0) {
     // Token type already exists...
     // TODO: treat this as a bug?
     free(new_entry);
@@ -70,16 +70,16 @@ int h_allocate_token_type(const char* name) {
     return probe->value;
   }
 }
-int h_get_token_type_number(const char* name) {
+HTokenType h_get_token_type_number(const char* name) {
   Entry e;
   e.name = name;
   Entry **ret = (Entry**)tfind(&e, &tt_registry, compare_entries);
   if (ret == NULL)
-    return -1;
+    return 0;
   else
     return (*ret)->value;
 }
-const char* h_get_token_type_name(int token_type) {
+const char* h_get_token_type_name(HTokenType token_type) {
   if (token_type >= tt_next || token_type < TT_START)
     return NULL;
   else
diff --git a/src/t_misc.c b/src/t_misc.c
index 74a57ca9b438e75fcbf222b4bbbc79a951c39022..92c2b326471d48a82fcae8f110d1febad58c6fe3 100644
--- a/src/t_misc.c
+++ b/src/t_misc.c
@@ -26,7 +26,7 @@ static void test_tt_registry(void) {
     g_test_message("Unknown token type should not return a name");
     g_test_fail();
   }
-  g_check_cmp_int32(h_get_token_type_number("com.upstandinghackers.test.unkown_token_type"), ==, -1);
+  g_check_cmp_int32(h_get_token_type_number("com.upstandinghackers.test.unkown_token_type"), ==, 0);
 }
 
 void register_misc_tests(void) {
diff --git a/tools/csharp/.hgignore b/tools/csharp/.hgignore
new file mode 100644
index 0000000000000000000000000000000000000000..b6261c931f0996160a00ccdfabe32d8647524398
--- /dev/null
+++ b/tools/csharp/.hgignore
@@ -0,0 +1 @@
+.*~
diff --git a/tools/csharp/README b/tools/csharp/README
new file mode 100644
index 0000000000000000000000000000000000000000..adff8b2e6a4386939c90363d276847a53a548734
--- /dev/null
+++ b/tools/csharp/README
@@ -0,0 +1 @@
+Grabbed from https://bitbucket.org/russel/scons_csharp, revision 058956ce21722f806a560c24305775f74dd71d3f 
diff --git a/tools/csharp/__init__.py b/tools/csharp/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..af4f51983579fa5ccfd0d522971b0a48ed6a27bf
--- /dev/null
+++ b/tools/csharp/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding:utf-8; -*-
+
+# Copyright (c) 2012 The SCons Foundation
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from csharp import exists, generate
diff --git a/tools/csharp/csharp.py b/tools/csharp/csharp.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b38b45319dc406bb1ea81ea1a8919d4c7af0028
--- /dev/null
+++ b/tools/csharp/csharp.py
@@ -0,0 +1,503 @@
+# -*- coding:utf-8; -*-
+
+# Copyright (c) 2009-10 The SCons Foundation
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#  This C# Tool taken from http://www.scons.org/wiki/CsharpBuilder and amended
+#  by the patch from Issue 1912 at http://scons.tigris.org/issues/show_bug.cgi?id=1912
+
+#  Amended and extended by Russel Winder <russel.winder@concertant.com>
+
+#  On the SCons wiki page there are two distinct tools, one for the Microsoft C# system and one for Mono.
+#  This is an attempt to meld to two based initially on the Microsoft C# tool with amendmnets from the Mono
+#  tool.
+
+import os.path
+import SCons.Builder
+import SCons.Node.FS
+import SCons.Util
+from SCons.Node.Python import Value
+
+# needed for adding methods to environment
+from SCons.Script.SConscript import SConsEnvironment
+
+# parses env['VERSION'] for major, minor, build, and revision
+def parseVersion(env):
+    """parses env['VERSION'] for major, minor, build, and revision"""
+    if type(env['VERSION']) is tuple or type(env['VERSION']) is list:
+        major, minor, build, revision = env['VERSION']
+    elif type(env['VERSION']) is str:
+        major, minor, build, revision = env['VERSION'].split('.')
+        major = int(major)
+        minor = int(minor)
+        build = int(build)
+        revision = int(revision)
+    return (major, minor, build, revision)
+
+def getVersionAsmDirective(major, minor, build, revision):
+    return '[assembly: AssemblyVersion("%d.%d.%d.%d")]' % (major, minor, build, revision)
+
+def generateVersionId(env, target, source):
+    out = open(target[0].path, 'w')
+    out.write('using System;using System.Reflection;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;\n')
+    out.write(source[0].get_contents())
+    out.close()
+
+# used so that we can capture the return value of an executed command
+def subprocess(cmdline):
+    """used so that we can capture the return value of an executed command"""
+    import subprocess
+    startupinfo = subprocess.STARTUPINFO()
+    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+    proc = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE, startupinfo=startupinfo, shell=False)
+    data, err = proc.communicate()
+    return proc.wait(), data, err
+
+def generatePublisherPolicyConfig(env, target, source):
+    """this method assumes that source list corresponds to [0]=version, [1]=assembly base name, [2]=assembly file node"""
+    # call strong name tool against compiled assembly and parse output for public token
+    outputFolder = os.path.split(target[0].tpath)[0]
+    pubpolicy = os.path.join(outputFolder, source[2].name)
+    rv, data, err = subprocess('sn -T ' + pubpolicy)
+    import re
+    tok_re = re.compile(r"([a-z0-9]{16})[\r\n ]{0,3}$")
+    match = tok_re.search(data)
+    tok = match.group(1)
+
+    # calculate version range to redirect from
+    version = source[0].value
+    oldVersionStartRange = '%s.%s.0.0' % (version[0], version[1])
+    newVersion = '%s.%s.%s.%s' % (version[0], version[1], version[2], version[3])
+    build = int(version[2])
+    rev = int(version[3])
+
+    # on build 0 and rev 0 or 1, no range is needed. otherwise calculate range
+    if (build == 0 and (rev == 0 or rev == 1)):
+        oldVersionRange = oldVersionStartRange
+    else:
+        if rev - 1 < 0:
+            endRevisionRange = '99'
+            endBuildRange = str(build-1)
+        else:
+            endRevisionRange = str(rev - 1)
+            endBuildRange = str(build)
+            oldVersionEndRange = '%s.%s.%s.%s' % (version[0], version[1], endBuildRange, endRevisionRange)
+            oldVersionRange = '%s-%s' % (oldVersionStartRange, oldVersionEndRange)
+
+    # write .net config xml out to file
+    out = open(target[0].path, 'w')
+    out.write('''\
+ <configuration><runtime><assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+   <dependentAssembly>
+     <assemblyIdentity name="%s" publicKeyToken="%s"/>
+     <bindingRedirect oldVersion="%s" newVersion="%s"/>
+   </dependentAssembly>
+ </assemblyBinding></runtime></configuration>
+ ''' % (source[1].value, tok, oldVersionRange, newVersion))
+    out.close()
+
+def getKeyFile(node, sources):
+    """search for key file"""
+    for file in node.children():
+        if file.name.endswith('.snk'):
+            sources.append(file)
+            return
+
+    # if not found look in included netmodules (first found is used)
+    for file in node.children():
+        if file.name.endswith('.netmodule'):
+            for file2 in file.children():
+                if file2.name.endswith('.snk'):
+                    sources.append(file2)
+                    return
+
+def PublisherPolicy(env, target, **kw):
+    """creates the publisher policy dll, mapping the major.minor.0.0 calls to the
+    major, minor, build, and revision passed in through the dictionary VERSION key"""
+    sources = []
+    # get version and generate .config file
+    version = parseVersion(kw)
+    asm = os.path.splitext(target[0].name)[0]
+    configName = 'policy.%d.%d.%s.%s' % (version[0], version[1], asm, 'config')
+    targ = 'policy.%d.%d.%s' % (version[0], version[1], target[0].name)
+    config = env.Command(configName, [Value(version), Value(asm), target[0]], generatePublisherPolicyConfig)
+    sources.append(config[0])
+
+    # find .snk key
+    getKeyFile(target[0], sources)
+
+    return env.CLIAsmLink(targ, sources, **kw)
+
+def CLIRefs(env, refs, paths = [], **kw):
+    listRefs = []
+    normpaths = [env.Dir(p).abspath for p in paths]
+    normpaths += env['CLIREFPATHS']
+
+    for ref in refs:
+        if not ref.endswith(env['SHLIBSUFFIX']):
+            ref += env['SHLIBSUFFIX']
+        if not ref.startswith(env['SHLIBPREFIX']):
+            ref = env['SHLIBPREFIX'] + ref
+        pathref = detectRef(ref, normpaths, env)
+        if pathref:
+            listRefs.append(pathref)
+
+    return listRefs
+
+def CLIMods(env, refs, paths = [], **kw):
+    listMods = []
+    normpaths = [env.Dir(p).abspath for p in paths]
+    normpaths += env['CLIMODPATHS']
+
+    for ref in refs:
+        if not ref.endswith(env['CLIMODSUFFIX']):
+            ref += env['CLIMODSUFFIX']
+        pathref = detectRef(ref, normpaths, env)
+        if pathref:
+            listMods.append(pathref)
+
+    return listMods
+
+def detectRef(ref, paths, env):
+    """look for existance of file (ref) at one of the paths"""
+    for path in paths:
+        if path.endswith(ref):
+            return path
+        pathref = os.path.join(path, ref)
+        if os.path.isfile(pathref):
+            return pathref
+
+    return ''
+
+def AddToRefPaths(env, files, **kw):
+    # the file name is included in path reference because otherwise checks for that output file
+    # by CLIRefs/CLIMods would fail until after it has been built.  Since SCons makes a pass
+    # before building anything, that file won't be there.  Only after the second pass will it be built
+    ref = env.FindIxes(files, 'SHLIBPREFIX', 'SHLIBSUFFIX').abspath
+    env['CLIREFPATHS'] = [ref] + env['CLIREFPATHS']
+    return 0
+
+def AddToModPaths(env, files, **kw):
+    mod = env.FindIxes(files, 'CLIMODPREFIX', 'CLIMODSUFFIX').abspath
+    env['CLIMODPATHS'] = [mod] + env['CLIMODPATHS']
+    return 0
+
+def cscFlags(target, source, env, for_signature):
+    listCmd = []
+    if (env.has_key('WINEXE')):
+        if (env['WINEXE'] == 1):
+            listCmd.append('-t:winexe')
+    return listCmd
+
+def cscSources(target, source, env, for_signature):
+    listCmd = []
+
+    for s in source:
+        if (str(s).endswith('.cs')):  # do this first since most will be source files
+            listCmd.append(s)
+        elif (str(s).endswith('.resources')):
+            listCmd.append('-resource:%s' % s.get_string(for_signature))
+        elif (str(s).endswith('.snk')):
+            listCmd.append('-keyfile:%s' % s.get_string(for_signature))
+        else:
+            # just treat this as a generic unidentified source file
+            listCmd.append(s)
+
+    return listCmd
+
+def cscSourcesNoResources(target, source, env, for_signature):
+    listCmd = []
+
+    for s in source:
+        if (str(s).endswith('.cs')):  # do this first since most will be source files
+            listCmd.append(s)
+        elif (str(s).endswith('.resources')): # resources cannot be embedded in netmodules
+            pass
+        elif (str(s).endswith('.snk')):
+            listCmd.append('-keyfile:%s' % s.get_string(for_signature))
+        else:
+            # just treat this as a generic unidentified source file
+            listCmd.append(s)
+
+    return listCmd
+
+def cscRefs(target, source, env, for_signature):
+    listCmd = []
+
+    if (env.has_key('ASSEMBLYREFS')):
+        refs = SCons.Util.flatten(env['ASSEMBLYREFS'])
+        for ref in refs:
+            if SCons.Util.is_String(ref):
+                listCmd.append('-reference:%s' % ref)
+            else:
+                listCmd.append('-reference:%s' % ref.abspath)
+
+    return listCmd
+
+def cscMods(target, source, env, for_signature):
+    listCmd = []
+
+    if (env.has_key('NETMODULES')):
+        mods = SCons.Util.flatten(env['NETMODULES'])
+        for mod in mods:
+            listCmd.append('-addmodule:%s' % mod)
+
+    return listCmd
+
+# TODO: this currently does not allow sources to be embedded (-embed flag)
+def alLinkSources(target, source, env, for_signature):
+    listCmd = []
+
+    for s in source:
+        if (str(s).endswith('.snk')):
+            listCmd.append('-keyfile:%s' % s.get_string(for_signature))
+        else:
+            # just treat this as a generic unidentified source file
+            listCmd.append('-link:%s' % s.get_string(for_signature))
+
+    if env.has_key('VERSION'):
+        version = parseVersion(env)
+        listCmd.append('-version:%d.%d.%d.%d' % version)
+
+    return listCmd
+
+def cliLinkSources(target, source, env, for_signature):
+    listCmd = []
+
+    # append source item. if it is a netmodule and has child resources, also append those
+    for s in source:
+        # all source items should go into listCmd
+        listCmd.append('%s' % s.get_string(for_signature))
+
+        if (str(s).endswith('.netmodule')):
+            for child in s.children():
+                if child.name.endswith('.resources'):
+                    listCmd.append('/assemblyresource:%s' % child.get_string(for_signature))
+
+    return listCmd
+
+def add_version(target, source, env):
+    if env.has_key('VERSION'):
+        if SCons.Util.is_String(target[0]):
+            versionfile = target[0] + '_VersionInfo.cs'
+        else:
+            versionfile = target[0].name + '_VersionInfo.cs'
+        source.append(env.Command(versionfile, [Value(getVersionAsmDirective(*parseVersion(env)))], generateVersionId))
+    return (target, source)
+
+# this check is needed because .NET assemblies like to have '.' in the name.
+# scons interprets that as an extension and doesn't append the suffix as a result
+def lib_emitter(target, source, env):
+    newtargets = []
+    for tnode in target:
+        t = tnode.name
+        if not t.endswith(env['SHLIBSUFFIX']):
+            t += env['SHLIBSUFFIX']
+        newtargets.append(t)
+
+    return (newtargets, source)
+
+def add_depends(target, source, env):
+    """Add dependency information before the build order is established"""
+
+    if (env.has_key('NETMODULES')):
+        mods = SCons.Util.flatten(env['NETMODULES'])
+        for mod in mods:
+            # add as dependency
+            for t in target:
+                env.Depends(t, mod)
+
+    if (env.has_key('ASSEMBLYREFS')):
+        refs = SCons.Util.flatten(env['ASSEMBLYREFS'])
+        for ref in refs:
+            # add as dependency
+            for t in target:
+                env.Depends(t, ref)
+
+    return (target, source)
+
+csc_action = SCons.Action.Action('$CSCCOM', '$CSCCOMSTR')
+
+MsCliBuilder = SCons.Builder.Builder(action = '$CSCCOM',
+                                     source_factory = SCons.Node.FS.default_fs.Entry,
+                                     emitter = add_version,
+                                     suffix = '.exe')
+
+csclib_action = SCons.Action.Action('$CSCLIBCOM', '$CSCLIBCOMSTR')
+
+MsCliLibBuilder = SCons.Builder.Builder(action = '$CSCLIBCOM',
+                                        source_factory = SCons.Node.FS.default_fs.Entry,
+                                        emitter = [lib_emitter, add_version, add_depends],
+                                        suffix = '$SHLIBSUFFIX')
+
+cscmod_action = SCons.Action.Action('$CSCMODCOM', '$CSCMODCOMSTR')
+
+MsCliModBuilder = SCons.Builder.Builder(action = '$CSCMODCOM',
+                                        source_factory = SCons.Node.FS.default_fs.Entry,
+                                        emitter = [add_version, add_depends],
+                                        suffix = '$CLIMODSUFFIX')
+
+def module_deps(target, source, env):
+    for s in source:
+        dir = s.dir.srcdir
+        if (dir is not None and dir is not type(None)):
+            for t in target:
+                env.Depends(t,s)
+    return (target, source)
+
+clilink_action = SCons.Action.Action('$CLILINKCOM', '$CLILINKCOMSTR')
+
+MsCliLinkBuilder = SCons.Builder.Builder(action = '$CLILINKCOM',
+                                         source_factory = SCons.Node.FS.default_fs.Entry,
+                                         emitter = [lib_emitter, add_version, module_deps], # don't know the best way yet to get module dependencies added
+                                         suffix = '.dll') #'$SHLIBSUFFIX')
+
+# TODO : This probably needs some more work... it hasn't been used since
+# finding the abilities of the VS 2005 C++ linker for .NET.
+MsCliAsmLinkBuilder = SCons.Builder.Builder(action = '$CLIASMLINKCOM',
+                                            source_factory = SCons.Node.FS.default_fs.Entry,
+                                            suffix = '.dll')
+
+typelib_prefix = 'Interop.'
+
+def typelib_emitter(target, source, env):
+    newtargets = []
+    for tnode in target:
+        t = tnode.name
+        if not t.startswith(typelib_prefix):
+            t = typelib_prefix + t
+        newtargets.append(t)
+
+    return (newtargets, source)
+
+def tlbimpFlags(target, source, env, for_signature):
+    listCmd = []
+
+    basename = os.path.splitext(target[0].name)[0]
+    # strip off typelib_prefix (such as 'Interop.') so it isn't in the namespace
+    if basename.startswith(typelib_prefix):
+        basename = basename[len(typelib_prefix):]
+    listCmd.append('-namespace:%s' % basename)
+
+    listCmd.append('-out:%s' % target[0].tpath)
+
+    for s in source:
+        if (str(s).endswith('.snk')):
+            listCmd.append('-keyfile:%s' % s.get_string(for_signature))
+
+    return listCmd
+
+typelibimp_action = SCons.Action.Action('$TYPELIBIMPCOM', '$TYPELIBIMPCOMSTR')
+
+MsCliTypeLibBuilder = SCons.Builder.Builder(action = '$TYPELIBIMPCOM',
+                                            source_factory = SCons.Node.FS.default_fs.Entry,
+                                            emitter = [typelib_emitter, add_depends],
+                                            suffix = '.dll')
+
+res_action = SCons.Action.Action('$CLIRCCOM', '$CLIRCCOMSTR')
+
+def res_emitter(target, source, env):
+    # prepend NAMESPACE if provided
+    if (env.has_key('NAMESPACE')):
+        newtargets = []
+        for t in target:
+            tname = t.name
+
+            # this is a cheesy way to get rid of '.aspx' in .resx file names
+            idx = tname.find('.aspx.')
+            if idx >= 0:
+                tname = tname[:idx] + tname[idx+5:]
+
+            newtargets.append('%s.%s' % (env['NAMESPACE'], tname))
+        return (newtargets, source)
+    else:
+        return (targets, source)
+
+MsCliResBuilder = SCons.Builder.Builder(action=res_action,
+                                        emitter=res_emitter,
+                                        src_suffix='.resx',
+                                        suffix='.resources',
+                                        src_builder=[],
+                                        source_scanner=SCons.Tool.SourceFileScanner)
+
+SCons.Tool.SourceFileScanner.add_scanner('.resx', SCons.Defaults.CScan)
+
+def generate(env):
+    envpaths = env['ENV']['PATH']
+    env['CLIREFPATHS']  = envpaths.split(os.pathsep)
+    env['CLIMODPATHS']  = []
+    env['ASSEMBLYREFS'] = []
+    env['NETMODULES']   = []
+
+    env['BUILDERS']['CLIProgram'] = MsCliBuilder
+    env['BUILDERS']['CLIAssembly'] = MsCliLibBuilder
+    env['BUILDERS']['CLILibrary'] = MsCliLibBuilder
+    env['BUILDERS']['CLIModule']  = MsCliModBuilder
+    env['BUILDERS']['CLILink']    = MsCliLinkBuilder
+    env['BUILDERS']['CLIAsmLink'] = MsCliAsmLinkBuilder
+    env['BUILDERS']['CLIRes'] = MsCliResBuilder
+    env['BUILDERS']['CLITypeLib'] = MsCliTypeLibBuilder
+
+    env['CSC']          = env.Detect('gmcs') or 'csc'
+    env['_CSCLIBS']     = "${_stripixes('-r:', CILLIBS, '', '-r', '', __env__)}"
+    env['_CSCLIBPATH']  = "${_stripixes('-lib:', CILLIBPATH, '', '-r', '', __env__)}"
+    env['CSCFLAGS']     = SCons.Util.CLVar('-nologo -noconfig')
+    env['_CSCFLAGS']    = cscFlags
+    env['_CSC_SOURCES'] = cscSources
+    env['_CSC_SOURCES_NO_RESOURCES'] = cscSourcesNoResources
+    env['_CSC_REFS']    = cscRefs
+    env['_CSC_MODS']    = cscMods
+    env['CSCCOM']       = '$CSC $CSCFLAGS $_CSCFLAGS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES'
+    env['CSCLIBCOM']    = '$CSC -t:library $CSCFLAGS $_CSCFLAGS $_CSCLIBPATH $_CSCLIBS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES'
+    env['CSCMODCOM']    = '$CSC -t:module $CSCFLAGS $_CSCFLAGS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES_NO_RESOURCES'
+    env['CLIMODPREFIX'] = ''
+    env['CLIMODSUFFIX'] = '.netmodule'
+    env['CSSUFFIX']     = '.cs'
+
+    # this lets us link .netmodules together into a single assembly
+    env['CLILINK']      = 'link'
+    env['CLILINKFLAGS'] = SCons.Util.CLVar('-nologo -ltcg -dll -noentry')
+    env['_CLILINK_SOURCES'] = cliLinkSources
+    env['CLILINKCOM']   = '$CLILINK $CLILINKFLAGS -out:${TARGET.abspath} $_CLILINK_SOURCES' # $SOURCES'
+
+    env['CLIASMLINK']   = 'al'
+    env['CLIASMLINKFLAGS'] = SCons.Util.CLVar('')
+    env['_ASMLINK_SOURCES'] = alLinkSources
+    env['CLIASMLINKCOM'] = '$CLIASMLINK $CLIASMLINKFLAGS -out:${TARGET.abspath} $_ASMLINK_SOURCES'
+
+    env['CLIRC']        = 'resgen'
+    env['CLIRCFLAGS']   = ''
+    env['CLIRCCOM']     = '$CLIRC $CLIRCFLAGS $SOURCES $TARGETS'
+
+    env['TYPELIBIMP']       = 'tlbimp'
+    env['TYPELIBIMPFLAGS'] = SCons.Util.CLVar('-sysarray')
+    env['_TYPELIBIMPFLAGS'] = tlbimpFlags
+    env['TYPELIBIMPCOM']    = '$TYPELIBIMP $SOURCES $TYPELIBIMPFLAGS $_TYPELIBIMPFLAGS'
+
+    SConsEnvironment.CLIRefs = CLIRefs
+    SConsEnvironment.CLIMods = CLIMods
+    SConsEnvironment.AddToRefPaths = AddToRefPaths
+    SConsEnvironment.AddToModPaths = AddToModPaths
+    SConsEnvironment.PublisherPolicy = PublisherPolicy
+
+def exists(env):
+    return env.Detect('csc') or env.Detect('gmcs')
diff --git a/tools/csharp/csharp.xml b/tools/csharp/csharp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9ecd3802ad60e59efd6aeb697fd695dcef59d081
--- /dev/null
+++ b/tools/csharp/csharp.xml
@@ -0,0 +1,156 @@
+<!--
+__COPYRIGHT__
+
+This file is processed by the bin/SConsDoc.py module.
+See its __doc__ string for a discussion of the format.
+-->
+<tool name="mscs">
+<summary>
+Sets construction variables for the Microsoft CSharp Compiler
+</summary>
+</tool>
+
+<builder name="CLILibrary">
+<summary>
+Builds a .NET assembly (dynamically linkable binary) from a list of sources.
+<example>
+env.CLILibrary('MyAsm', 'MyAsm.cs')
+</example>
+</summary>
+</builder>
+
+<builder name="CLILink">
+<summary>
+Uses Microsoft C++ linker to link netmodules into a single .NET assembly:
+
+<example>
+env.CLILink('Common', ['mod1.netmodule', 'mod2.netmodule'])
+</example>
+</summary>
+</builder>
+
+<builder name="CLIModule">
+<summary>
+Builds a .NET netmodule (statically linkable binary) from a list of sources:
+
+<example>
+env.CLIModule('MyMod', 'MyMod.cs')
+</example>
+</summary>
+</builder>
+
+<builder name="CLIProgram">
+<summary>
+Builds a .NET executable from a list of sources.
+If the $WINEXE value is set, the sources will be compiled as a windows app, rather than a console app:
+
+<example>
+env.Program('MyApp', 'MyApp.cs', WINEXE=1)
+</example>
+</summary>
+</builder>
+
+<builder name="CLIRes">
+<summary>
+Builds a Microsoft binary resource file (extension of .resources) from XML source files.
+If the $NAMESPACE value is set, its value is prepended to the name of the target file:
+
+<example>
+env.CLIRes('app.resx', NAMESPACE='MyCompany.ProductX')
+</example>
+</summary>
+</builder>
+
+<builder name="CLITypeLib">
+<summary>
+Builds a .NET interop assembly that contains converted type definitions found within a COM type library DLL.  Prepends the .NET assembly with 'Interop.':
+
+<example>
+env.CLITypeLib('MyLib', ['MyLib.dll', 'keyfile.snk'])
+</example>
+</summary>
+</builder>
+
+<cvar name="CLILINK">
+<summary>
+The Microsoft C++ linker.
+</summary>
+</cvar>
+
+<cvar name="CLILINKFLAGS">
+<summary>
+General options passed to the Microsoft C++ linker.
+</summary>
+</cvar>
+
+<cvar name="CLILINKCOM">
+<summary>
+The command line used to link .NET netmodules into an assembly.  Any options specified in the $CLILINKFLAGS construction variable is included on this command line.
+</summary>
+</cvar>
+
+<cvar name="CSC">
+<summary>
+The CSharp Compiler.
+</summary>
+</cvar>
+
+<cvar name="CSCFLAGS">
+<summary>
+General options that are passed to the CSharp Compiler.
+</summary>
+</cvar>
+
+<cvar name="CSCCOM">
+<summary>
+The command line used to compile a CSharp source file to a console or windows executable. Any options specified in the $CSCFLAGS is included on this command line.
+</summary>
+</cvar>
+
+<cvar name="CSCLIBCOM">
+<summary>
+The command line used to compile a CSharp source file to a .NET assembly. Any options specified in the $CSCFLAGS is included on this command line.
+</summary>
+</cvar>
+
+<cvar name="CSCMODCOM">
+<summary>
+The command line used to compile a CSharp source file to a .NET netmodule. Any options specified in the $CSCFLAGS is included on this command line.
+</summary>
+</cvar>
+
+<cvar name="CLIRC">
+<summary>
+The Microsoft .NET resource compiler.
+</summary>
+</cvar>
+
+<cvar name="CLIRCFLAGS">
+<summary>
+General options passed to the Microsoft .NET resource compiler.
+</summary>
+</cvar>
+
+<cvar name="CLIRCCOM">
+<summary>
+The command line used to compile XML resource files to a .NET resource binary.  Any options specified in the $CLIRCFLAGS construction variable is included on this command line.
+</summary>
+</cvar>
+
+<cvar name="TYPELIBIMP">
+<summary>
+The Microsoft Type Library Importer.
+</summary>
+</cvar>
+
+<cvar name="TYPELIBIMPFLAGS">
+<summary>
+General options passed to the Microsoft Type Library Importer.
+</summary>
+</cvar>
+
+<cvar name="TYPELIBIMPCOM">
+<summary>
+The command line used to convert type definitions found within a COM type library into equivalent definitions in a .NET assembly.  Any options specified in the $TYPELIBIMPFLAGS construction variable is included on this command line.
+</summary>
+</cvar>
diff --git a/tools/csharp/mono.py b/tools/csharp/mono.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2cc380e7ab8fe4b7aa5face89b51898927f56a5
--- /dev/null
+++ b/tools/csharp/mono.py
@@ -0,0 +1,56 @@
+# -*- mode:python; coding:utf-8; -*-
+
+# Copyright (c) 2009 The SCons Foundation
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#  A tool for processing C# code.
+
+#  This C# Tool for Mono taken from http://www.scons.org/wiki/CsharpBuilder.
+
+import os.path
+import SCons.Builder
+import SCons.Node.FS
+import SCons.Util
+
+csccom = "$CSC $CSCFLAGS -out:${TARGET.abspath} $SOURCES"
+csclibcom = "$CSC -t:library $CSCLIBFLAGS $_CSCLIBPATH $_CSCLIBS -out:${TARGET.abspath} $SOURCES"
+
+McsBuilder = SCons.Builder.Builder(action = '$CSCCOM',
+                                   source_factory = SCons.Node.FS.default_fs.Entry,
+                                   suffix = '.exe')
+
+McsLibBuilder = SCons.Builder.Builder(action = '$CSCLIBCOM',
+                                      source_factory = SCons.Node.FS.default_fs.Entry,
+                                      suffix = '.dll')
+
+def generate(env):
+    env['BUILDERS']['CLIProgram'] = McsBuilder
+    env['BUILDERS']['CLILibrary'] = McsLibBuilder
+    
+    env['CSC']        = 'mcs'
+    env['_CSCLIBS']    = "${_stripixes('-r:', CILLIBS, '', '-r', '', __env__)}"
+    env['_CSCLIBPATH'] = "${_stripixes('-lib:', CILLIBPATH, '', '-r', '', __env__)}"
+    env['CSCFLAGS']   = SCons.Util.CLVar('')
+    env['CSCCOM']     = SCons.Action.Action(csccom)
+    env['CSCLIBCOM']  = SCons.Action.Action(csclibcom)
+    
+def exists(env):
+    return internal_zip or env.Detect('mcs')