diff --git a/.gitignore b/.gitignore
index 0d20b64..34f2b04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
 *.pyc
+*.o
+*.a
diff --git a/.hgignore b/.hgignore
index bb6d589..b8428bb 100644
--- a/.hgignore
+++ b/.hgignore
@@ -1,3 +1,5 @@
 syntax: glob
 
 *.pyc
+*.o
+*.a
diff --git a/README.markdown b/README.markdown
index f860ef5..53df835 100644
--- a/README.markdown
+++ b/README.markdown
@@ -180,23 +180,21 @@
 TODO
 ----
 
-*   the compiler generates *horrible* string-handling code.  fix!
+*   libtamsin contains *horrible* string-handling code.  fix!
 *   meta-circular implementation of scanner -- what we have is pretty close
 *   meta-circular implementation of parser
 *   meta-circular implementation of interpreter!
-*   system library
+*   system library in its own Python module
 *   `bin/tamsin runast astfile.txt` -- for testing the meta-circular parser
+*   `bin/tamsin runscan scanfile.txt` -- for testing the meta-circular scanner
+*   `bin/tamsin scan file.tamsin` -- to generate a scanfile
 
 ### compiler ###
 
 *   handle term concatenation
-*   literal string escape sequences
 *   render NUL as EOF.  actually, we want 8-bit clean strings eventually
-*   implement any
-*   implement fail
 *   implement !
 *   implement alnum
-*   write `-ltamsin` library and link to it
 
 ### document ###
 
diff --git a/c_src/build.sh b/c_src/build.sh
new file mode 100755
index 0000000..197a8d1
--- /dev/null
+++ b/c_src/build.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+FILES="scanner term tamsin"
+
+for FILE in $FILES; do
+    gcc -c $FILE.c -o $FILE.o
+done
+
+ar -r libtamsin.a scanner.o term.o tamsin.o
+
diff --git a/c_src/scanner.c b/c_src/scanner.c
new file mode 100644
index 0000000..060c340
--- /dev/null
+++ b/c_src/scanner.c
@@ -0,0 +1,19 @@
+#include "tamsin.h"
+
+char scan(struct scanner *s) {
+    char c = s->buffer[s->position];
+    if (c == '\0') {
+        return '\0';
+    } else {
+        s->position++;
+        return c;
+    }
+};
+
+void unscan(struct scanner *s) {
+    s->position = s->reset_position;
+}
+
+void commit(struct scanner *s) {
+    s->reset_position = s->position;
+}
diff --git a/c_src/tamsin.c b/c_src/tamsin.c
new file mode 100644
index 0000000..c354e80
--- /dev/null
+++ b/c_src/tamsin.c
@@ -0,0 +1,48 @@
+#include "tamsin.h"
+
+void tamsin_eof(struct scanner *s) {
+    char c = scan(s);
+    unscan(s);
+    if (c == '\0') {
+        result = new_term("EOF");
+        ok = 1;
+    } else {
+        char t[100];
+        sprintf(t, "expected EOF found '%c'", c);
+        result = new_term(t);
+        ok = 0;
+    }
+}
+
+void tamsin_any(struct scanner *s) {
+    char c = scan(s);
+    if (c == '\0') {
+        unscan(s);
+        result = new_term("expected any token, found EOF");
+        ok = 0;
+    } else {
+        char t[2];
+        commit(s);
+        sprintf(t, "%c", c);
+        result = new_term(t);
+        ok = 1;
+    }
+}
+
+void tamsin_expect(struct scanner *s, char *token) {
+    char c = scan(s);
+    if (c == token[0]) {
+        commit(s);
+        char s[10];
+        strcpy(s, "a");
+        s[0] = c;
+        result = new_term(s);
+        ok = 1;
+    } else {
+        unscan(s);
+        char s[100];
+        sprintf(s, "expected '%c' found '%c'", token[0], c);
+        result = new_term(s);
+        ok = 0;
+    }
+};
diff --git a/c_src/tamsin.h b/c_src/tamsin.h
new file mode 100644
index 0000000..8036dc0
--- /dev/null
+++ b/c_src/tamsin.h
@@ -0,0 +1,43 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* terms */
+
+struct term {
+    const char *atom;
+    struct term_list *subterms;
+};
+
+struct term_list {
+    struct term *term;
+    struct term_list *next;
+};
+
+struct term *new_term(const char *);
+void add_subterm(struct term *, struct term *);
+void term_format_r(struct term *t);
+char *term_format(struct term *t);
+
+/* scanner */
+
+struct scanner {
+    char *buffer;
+    int position;
+    int reset_position;
+};
+
+char scan(struct scanner *);
+void unscan(struct scanner *);
+void commit(struct scanner *);
+
+/* tamsin */
+
+void tamsin_eof(struct scanner *);
+void tamsin_any(struct scanner *);
+void tamsin_expect(struct scanner *, char *);
+
+/* global state: result of last action */
+
+extern int ok;
+extern struct term *result;
diff --git a/c_src/term.c b/c_src/term.c
new file mode 100644
index 0000000..f344ea9
--- /dev/null
+++ b/c_src/term.c
@@ -0,0 +1,42 @@
+#include "tamsin.h"
+
+struct term *new_term(const char *atom) {
+    struct term *t;
+    t = malloc(sizeof(struct term));
+    t->atom = strdup(atom);
+    t->subterms = NULL;
+}
+
+void add_subterm(struct term *term, struct term *subterm) {
+    struct term_list *tl;
+    tl = malloc(sizeof(struct term_list));
+    tl->term = subterm;
+    tl->next = term->subterms;
+    term->subterms = tl;        
+}
+
+char fmtbuf[1000];  /* yeesh */
+
+void term_format_r(struct term *t) {
+    struct term_list *tl;
+
+    strcat(fmtbuf, t->atom);
+    if (t->subterms != NULL) {
+        strcat(fmtbuf, "(");
+        
+        for (tl = t->subterms; tl != NULL; tl = tl->next) {
+            term_format_r(tl->term);
+            if (tl->next != NULL) {
+                strcat(fmtbuf, ", ");
+            }
+        }
+        
+        strcat(fmtbuf, ")");
+    }
+}
+
+char *term_format(struct term *t) {
+    strcpy(fmtbuf, "");
+    term_format_r(t);
+    return fmtbuf;
+}
diff --git a/loadngo.sh b/loadngo.sh
index 1745b05..9c51761 100755
--- a/loadngo.sh
+++ b/loadngo.sh
@@ -1,6 +1,8 @@
 #!/bin/sh
 
-INPUT=`cat`
 bin/tamsin compile $1 > foo.c && \
-   gcc foo.c -o foo && \
-   ./foo "$INPUT"
+   gcc -Ic_src -Lc_src foo.c -o foo -ltamsin && \
+   ./foo `cat`
+R=$?
+rm -f foo.c foo
+exit $R
diff --git a/src/tamsin/compiler.py b/src/tamsin/compiler.py
index 5ac3799..83faed5 100644
--- a/src/tamsin/compiler.py
+++ b/src/tamsin/compiler.py
@@ -10,140 +10,12 @@
 /*
  * Generated code!  Edit at your own risk!
  */
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+#include <tamsin.h>
 
 /* global state: result of last action */
 
 int ok;
 struct term *result;
-
-/* terms */
-
-struct term {
-    const char *atom;
-    struct term_list *subterms;
-};
-
-struct term_list {
-    struct term *term;
-    struct term_list *next;
-};
-
-struct term *new_term(const char *atom) {
-    struct term *t;
-    t = malloc(sizeof(struct term));
-    t->atom = strdup(atom);
-    t->subterms = NULL;
-}
-
-void add_subterm(struct term *term, struct term *subterm) {
-    struct term_list *tl;
-    tl = malloc(sizeof(struct term_list));
-    tl->term = subterm;
-    tl->next = term->subterms;
-    term->subterms = tl;        
-}
-
-char fmtbuf[1000];  /* yeesh */
-
-void term_format_r(struct term *t) {
-    struct term_list *tl;
-
-    strcat(fmtbuf, t->atom);
-    if (t->subterms != NULL) {
-        strcat(fmtbuf, "(");
-        
-        for (tl = t->subterms; tl != NULL; tl = tl->next) {
-            term_format_r(tl->term);
-            if (tl->next != NULL) {
-                strcat(fmtbuf, ", ");
-            }
-        }
-        
-        strcat(fmtbuf, ")");
-    }
-}
-
-char *term_format(struct term *t) {
-    strcpy(fmtbuf, "");
-    term_format_r(t);
-    return fmtbuf;
-}
-
-/* scanner */
-
-struct scanner {
-    char *buffer;
-    int position;
-    int reset_position;
-};
-
-char scan(struct scanner *s) {
-    char c = s->buffer[s->position];
-    if (c == '\0') {
-        return '\0';
-    } else {
-        s->position++;
-        return c;
-    }
-};
-
-void unscan(struct scanner *s) {
-    s->position = s->reset_position;
-}
-
-void commit(struct scanner *s) {
-    s->reset_position = s->position;
-}
-
-void tamsin_eof(struct scanner *s) {
-    char c = scan(s);
-    unscan(s);
-    if (c == '\0') {
-        result = new_term("EOF");
-        ok = 1;
-    } else {
-        char t[100];
-        sprintf(t, "expected EOF found '%c'", c);
-        result = new_term(t);
-        ok = 0;
-    }
-}
-
-void tamsin_any(struct scanner *s) {
-    char c = scan(s);
-    if (c == '\0') {
-        unscan(s);
-        result = new_term("expected any token, found EOF");
-        ok = 0;
-    } else {
-        char t[2];
-        commit(s);
-        sprintf(t, "%c", c);
-        result = new_term(t);
-        ok = 1;
-    }
-}
-
-void tamsin_expect(struct scanner *s, char *token) {
-    char c = scan(s);
-    if (c == token[0]) {
-        commit(s);
-        char s[10];
-        strcpy(s, "a");
-        s[0] = c;
-        result = new_term(s);
-        ok = 1;
-    } else {
-        unscan(s);
-        char s[100];
-        sprintf(s, "expected '%c' found '%c'", token[0], c);
-        result = new_term(s);
-        ok = 0;
-    }
-};
 
 struct scanner * scanner;
 '''