about summary refs log tree commit diff stats
path: root/cpp/tangle
diff options
context:
space:
mode:
Diffstat (limited to 'cpp/tangle')
-rw-r--r--cpp/tangle/000test.cc26
-rw-r--r--cpp/tangle/001trace.cc338
-rw-r--r--cpp/tangle/001trace.test.cc164
-rw-r--r--cpp/tangle/002main.cc61
-rw-r--r--cpp/tangle/030tangle.cc355
-rw-r--r--cpp/tangle/030tangle.test.cc251
-rw-r--r--cpp/tangle/boot.cc63
-rw-r--r--cpp/tangle/makefile23
8 files changed, 1281 insertions, 0 deletions
diff --git a/cpp/tangle/000test.cc b/cpp/tangle/000test.cc
new file mode 100644
index 00000000..2754b254
--- /dev/null
+++ b/cpp/tangle/000test.cc
@@ -0,0 +1,26 @@
+typedef void (*test_fn)(void);
+
+const test_fn Tests[] = {
+  #include "test_list"  // auto-generated; see makefile
+};
+
+bool Passed = true;
+
+long Num_failures = 0;
+
+#define CHECK(X) \
+  if (!(X)) { \
+    ++Num_failures; \
+    cerr << "\nF " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << '\n'; \
+    Passed = false; \
+    return; \
+  }
+
+#define CHECK_EQ(X, Y) \
+  if ((X) != (Y)) { \
+    ++Num_failures; \
+    cerr << "\nF " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): " << #X << " == " << #Y << '\n'; \
+    cerr << "  got " << (X) << '\n';  /* BEWARE: multiple eval */ \
+    Passed = false; \
+    return; \
+  }
diff --git a/cpp/tangle/001trace.cc b/cpp/tangle/001trace.cc
new file mode 100644
index 00000000..a99951e0
--- /dev/null
+++ b/cpp/tangle/001trace.cc
@@ -0,0 +1,338 @@
+bool Hide_warnings = false;
+
+struct trace_stream {
+  vector<pair<string, pair<int, string> > > past_lines;  // [(layer label, frame, line)]
+  unordered_map<string, int> frame;
+  // accumulator for current line
+  ostringstream* curr_stream;
+  string curr_layer;
+  string dump_layer;
+  trace_stream() :curr_stream(NULL) {}
+  ~trace_stream() { if (curr_stream) delete curr_stream; }
+
+  ostringstream& stream(string layer) {
+    newline();
+    curr_stream = new ostringstream;
+    curr_layer = layer;
+    return *curr_stream;
+  }
+
+  // be sure to call this before messing with curr_stream or curr_layer or frame
+  void newline() {
+    if (!curr_stream) return;
+    past_lines.push_back(pair<string, pair<int, string> >(curr_layer, pair<int, string>(frame[curr_layer], curr_stream->str())));
+    if (curr_layer == "dump")
+      cerr << with_newline(curr_stream->str());
+    else if ((!dump_layer.empty() && prefix_match(dump_layer, curr_layer))
+        || (!Hide_warnings && curr_layer == "warn"))
+      cerr << curr_layer << "/" << frame[curr_layer] << ": " << with_newline(curr_stream->str());
+    delete curr_stream;
+    curr_stream = NULL;
+  }
+
+  string readable_contents(string layer) {  // missing layer = everything, frame, hierarchical layers
+    newline();
+    ostringstream output;
+    string real_layer, frame;
+    parse_layer_and_frame(layer, &real_layer, &frame);
+    for (vector<pair<string, pair<int, string> > >::iterator p = past_lines.begin(); p != past_lines.end(); ++p)
+      if (layer.empty() || prefix_match(real_layer, p->first))
+        output << p->first << "/" << p->second.first << ": " << with_newline(p->second.second);
+    return output.str();
+  }
+
+  void dump_browseable_contents(string layer) {
+    ofstream dump("dump");
+    dump << "<div class='frame' frame_index='1'>start</div>\n";
+    for (vector<pair<string, pair<int, string> > >::iterator p = past_lines.begin(); p != past_lines.end(); ++p) {
+      if (p->first != layer) continue;
+      dump << "<div class='frame";
+      if (p->second.first > 1) dump << " hidden";
+      dump << "' frame_index='" << p->second.first << "'>";
+      dump << p->second.second;
+      dump << "</div>\n";
+    }
+    dump.close();
+  }
+
+  string with_newline(string s) {
+    if (s[s.size()-1] != '\n') return s+'\n';
+    return s;
+  }
+};
+
+
+
+trace_stream* Trace_stream = NULL;
+
+// Top-level helper. IMPORTANT: can't nest.
+#define trace(layer)  !Trace_stream ? cerr /*print nothing*/ : Trace_stream->stream(layer)
+// Warnings should go straight to cerr by default since calls to trace() have
+// some unfriendly constraints (they delay printing, they can't nest)
+#define RAISE  ((!Trace_stream || !Hide_warnings) ? cerr /*do print*/ : Trace_stream->stream("warn")) << __FILE__ << ":" << __LINE__ << " "
+// Just debug logging without any test support.
+#define dbg cerr << __FUNCTION__ << '(' << __FILE__ << ':' << __LINE__ << ") "
+
+// RAISE << die exits after printing -- unless Hide_warnings is set.
+struct die {};
+ostream& operator<<(ostream& os, unused die) {
+  if (Hide_warnings) return os;
+  os << "dying";
+  exit(1);
+}
+
+#define CLEAR_TRACE  delete Trace_stream, Trace_stream = new trace_stream;
+
+#define DUMP(layer)  cerr << Trace_stream->readable_contents(layer)
+
+// Trace_stream is a resource, lease_tracer uses RAII to manage it.
+struct lease_tracer {
+  lease_tracer() { Trace_stream = new trace_stream; }
+  ~lease_tracer() { delete Trace_stream, Trace_stream = NULL; }
+};
+
+#define START_TRACING_UNTIL_END_OF_SCOPE  lease_tracer leased_tracer;
+
+void trace_all(const string& label, const list<string>& in) {
+  for (list<string>::const_iterator p = in.begin(); p != in.end(); ++p)
+    trace(label) << *p;
+}
+
+bool check_trace_contents(string FUNCTION, string FILE, int LINE, string expected) {  // missing layer == anywhere, frame, hierarchical layers
+  vector<string> expected_lines = split(expected, "");
+  size_t curr_expected_line = 0;
+  while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+    ++curr_expected_line;
+  if (curr_expected_line == expected_lines.size()) return true;
+  Trace_stream->newline();
+  ostringstream output;
+  string layer, frame, contents;
+  parse_layer_frame_contents(expected_lines[curr_expected_line], &layer, &frame, &contents);
+  for (vector<pair<string, pair<int, string> > >::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (!layer.empty() && !prefix_match(layer, p->first))
+      continue;
+
+    if (!frame.empty() && strtol(frame.c_str(), NULL, 0) != p->second.first)
+      continue;
+
+    if (contents != p->second.second)
+      continue;
+
+    ++curr_expected_line;
+    while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+      ++curr_expected_line;
+    if (curr_expected_line == expected_lines.size()) return true;
+    parse_layer_frame_contents(expected_lines[curr_expected_line], &layer, &frame, &contents);
+  }
+
+  ++Num_failures;
+  cerr << "\nF " << FUNCTION << "(" << FILE << ":" << LINE << "): missing [" << contents << "] in trace:\n";
+  DUMP(layer);
+  Passed = false;
+  return false;
+}
+
+void parse_layer_frame_contents(const string& orig, string* layer, string* frame, string* contents) {
+  string layer_and_frame;
+  parse_contents(orig, ": ", &layer_and_frame, contents);
+  parse_layer_and_frame(layer_and_frame, layer, frame);
+}
+
+void parse_contents(const string& s, const string& delim, string* prefix, string* contents) {
+  string::size_type pos = s.find(delim);
+  if (pos == NOT_FOUND) {
+    *prefix = "";
+    *contents = s;
+  }
+  else {
+    *prefix = s.substr(0, pos);
+    *contents = s.substr(pos+delim.size());
+  }
+}
+
+void parse_layer_and_frame(const string& orig, string* layer, string* frame) {
+  size_t last_slash = orig.rfind('/');
+  if (last_slash == NOT_FOUND
+      || last_slash == orig.size()-1  // trailing slash indicates hierarchical layer
+      || orig.find_last_not_of("0123456789") != last_slash) {
+    *layer = orig;
+    *frame = "";
+  }
+  else {
+    *layer = orig.substr(0, last_slash);
+    *frame = orig.substr(last_slash+1);
+  }
+}
+
+
+
+bool check_trace_contents(string FUNCTION, string FILE, int LINE, string layer, string expected) {  // empty layer == everything, multiple layers, hierarchical layers
+  vector<string> expected_lines = split(expected, "");
+  size_t curr_expected_line = 0;
+  while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+    ++curr_expected_line;
+  if (curr_expected_line == expected_lines.size()) return true;
+  Trace_stream->newline();
+  ostringstream output;
+  vector<string> layers = split(layer, ",");
+  for (vector<pair<string, pair<int, string> > >::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (!layer.empty() && !any_prefix_match(layers, p->first))
+      continue;
+    if (p->second.second != expected_lines[curr_expected_line])
+      continue;
+    ++curr_expected_line;
+    while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+      ++curr_expected_line;
+    if (curr_expected_line == expected_lines.size()) return true;
+  }
+
+  ++Num_failures;
+  cerr << "\nF " << FUNCTION << "(" << FILE << ":" << LINE << "): missing [" << expected_lines[curr_expected_line] << "] in trace:\n";
+  DUMP(layer);
+  Passed = false;
+  return false;
+}
+
+#define CHECK_TRACE_CONTENTS(...)  check_trace_contents(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
+
+int trace_count(string layer) {
+  return trace_count(layer, "");
+}
+
+int trace_count(string layer, string line) {
+  Trace_stream->newline();
+  long result = 0;
+  vector<string> layers = split(layer, ",");
+  for (vector<pair<string, pair<int, string> > >::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (any_prefix_match(layers, p->first))
+      if (line == "" || p->second.second == line)
+        ++result;
+  }
+  return result;
+}
+
+int trace_count(string layer, int frame, string line) {
+  Trace_stream->newline();
+  long result = 0;
+  vector<string> layers = split(layer, ",");
+  for (vector<pair<string, pair<int, string> > >::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (any_prefix_match(layers, p->first) && p->second.first == frame)
+      if (line == "" || p->second.second == line)
+        ++result;
+  }
+  return result;
+}
+
+#define CHECK_TRACE_WARNS()  CHECK(trace_count("warn") > 0)
+#define CHECK_TRACE_DOESNT_WARN() \
+  if (trace_count("warn") > 0) { \
+    ++Num_failures; \
+    cerr << "\nF " << __FUNCTION__ << "(" << __FILE__ << ":" << __LINE__ << "): unexpected warnings\n"; \
+    DUMP("warn"); \
+    Passed = false; \
+    return; \
+  }
+
+bool trace_doesnt_contain(string layer, string line) {
+  return trace_count(layer, line) == 0;
+}
+
+bool trace_doesnt_contain(string expected) {
+  vector<string> tmp = split(expected, ": ");
+  return trace_doesnt_contain(tmp[0], tmp[1]);
+}
+
+bool trace_doesnt_contain(string layer, int frame, string line) {
+  return trace_count(layer, frame, line) == 0;
+}
+
+#define CHECK_TRACE_DOESNT_CONTAIN(...)  CHECK(trace_doesnt_contain(__VA_ARGS__))
+
+
+
+// manage layer counts in Trace_stream using RAII
+struct lease_trace_frame {
+  string layer;
+  lease_trace_frame(string l) :layer(l) {
+    if (!Trace_stream) return;
+    Trace_stream->newline();
+    ++Trace_stream->frame[layer];
+  }
+  ~lease_trace_frame() {
+    if (!Trace_stream) return;
+    Trace_stream->newline();
+    --Trace_stream->frame[layer];
+  }
+};
+#define new_trace_frame(layer)  lease_trace_frame leased_frame(layer);
+
+bool check_trace_contents(string FUNCTION, string FILE, int LINE, string layer, int frame, string expected) {  // multiple layers, hierarchical layers
+  vector<string> expected_lines = split(expected, "");  // hack: doesn't handle newlines in embedded in lines
+  size_t curr_expected_line = 0;
+  while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+    ++curr_expected_line;
+  if (curr_expected_line == expected_lines.size()) return true;
+  Trace_stream->newline();
+  ostringstream output;
+  vector<string> layers = split(layer, ",");
+  for (vector<pair<string, pair<int, string> > >::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
+    if (!layer.empty() && !any_prefix_match(layers, p->first))
+      continue;
+    if (p->second.first != frame)
+      continue;
+    if (p->second.second != expected_lines[curr_expected_line])
+      continue;
+    ++curr_expected_line;
+    while (curr_expected_line < expected_lines.size() && expected_lines[curr_expected_line].empty())
+      ++curr_expected_line;
+    if (curr_expected_line == expected_lines.size()) return true;
+  }
+
+  ++Num_failures;
+  cerr << "\nF " << FUNCTION << "(" << FILE << ":" << LINE << "): missing [" << expected_lines[curr_expected_line] << "] in trace/" << frame << ":\n";
+  DUMP(layer);
+  Passed = false;
+  return false;
+}
+
+#define CHECK_TRACE_TOP(layer, expected)  CHECK_TRACE_CONTENTS(layer, 1, expected)
+
+
+
+vector<string> split(string s, string delim) {
+  vector<string> result;
+  string::size_type begin=0, end=s.find(delim);
+  while (true) {
+    if (end == NOT_FOUND) {
+      result.push_back(string(s, begin, NOT_FOUND));
+      break;
+    }
+    result.push_back(string(s, begin, end-begin));
+    begin = end+delim.size();
+    end = s.find(delim, begin);
+  }
+  return result;
+}
+
+bool any_prefix_match(const vector<string>& pats, const string& needle) {
+  if (pats.empty()) return false;
+  if (*pats[0].rbegin() != '/')
+    // prefix match not requested
+    return find(pats.begin(), pats.end(), needle) != pats.end();
+  // first pat ends in a '/'; assume all pats do.
+  for (vector<string>::const_iterator p = pats.begin(); p != pats.end(); ++p)
+    if (headmatch(needle, *p)) return true;
+  return false;
+}
+
+bool prefix_match(const string& pat, const string& needle) {
+  if (*pat.rbegin() != '/')
+    // prefix match not requested
+    return pat == needle;
+  return headmatch(needle, pat);
+}
+
+bool headmatch(const string& s, const string& pat) {
+  if (pat.size() > s.size()) return false;
+  return std::mismatch(pat.begin(), pat.end(), s.begin()).first == pat.end();
+}
diff --git a/cpp/tangle/001trace.test.cc b/cpp/tangle/001trace.test.cc
new file mode 100644
index 00000000..e0db457c
--- /dev/null
+++ b/cpp/tangle/001trace.test.cc
@@ -0,0 +1,164 @@
+void test_trace_check_compares() {
+  CHECK_TRACE_CONTENTS("test layer", "");
+  trace("test layer") << "foo";
+  CHECK_TRACE_CONTENTS("test layer", "foo");
+}
+
+void test_trace_check_filters_layers() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  CHECK_TRACE_CONTENTS("test layer 1", "foo");
+}
+
+void test_trace_check_ignores_other_lines() {
+  trace("test layer 1") << "foo";
+  trace("test layer 1") << "bar";
+  CHECK_TRACE_CONTENTS("test layer 1", "foo");
+}
+
+void test_trace_check_always_finds_empty_lines() {
+  CHECK_TRACE_CONTENTS("test layer 1", "");
+}
+
+void test_trace_check_treats_empty_layers_as_wildcards() {
+  trace("test layer 1") << "foo";
+  CHECK_TRACE_CONTENTS("", "foo");
+}
+
+void test_trace_check_multiple_lines_at_once() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  CHECK_TRACE_CONTENTS("", "foobar");
+}
+
+void test_trace_check_always_finds_empty_lines2() {
+  CHECK_TRACE_CONTENTS("test layer 1", "");
+}
+
+void test_trace_orders_across_layers() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  trace("test layer 1") << "qux";
+  CHECK_TRACE_CONTENTS("", "foobarqux");
+}
+
+void test_trace_orders_across_layers2() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  trace("test layer 1") << "qux";
+  CHECK_TRACE_CONTENTS("foobarqux");
+}
+
+void test_trace_checks_ordering_spanning_multiple_layers() {
+  trace("layer1") << "foo";
+  trace("layer2") << "bar";
+  trace("layer1") << "qux";
+  CHECK_TRACE_CONTENTS("layer1: foolayer2: barlayer1: qux");
+}
+
+void test_trace_segments_within_layers() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  new_trace_frame("test layer 1");
+  trace("test layer 1") << "qux";
+  CHECK_TRACE_CONTENTS("test layer 1", "fooqux");
+  CHECK_TRACE_CONTENTS("test layer 1", 0, "foo");
+  CHECK_TRACE_DOESNT_CONTAIN("test layer 1", 1, "foo");
+}
+
+void test_trace_checks_ordering_across_layers_and_frames() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  new_trace_frame("test layer 1");
+  trace("test layer 1") << "qux";
+  CHECK_TRACE_CONTENTS("test layer 1/0: footest layer 2: bartest layer 1: qux");
+  CHECK_TRACE_CONTENTS("test layer 1: footest layer 2: bartest layer 1/1: qux");
+}
+
+void trace_test_fn(int n) {
+  if (n == 0) return;
+  new_trace_frame("foo");
+  trace("foo") << "before: " << n;
+  trace_test_fn(n-1);
+  trace("foo") << "after: " << n;
+}
+
+void test_trace_keeps_level_together() {
+  CHECK_TRACE_CONTENTS("foo", "");
+  trace_test_fn(4);
+  CHECK_TRACE_CONTENTS("foo", 2, "before: 3after: 3");
+}
+
+void test_trace_supports_multiple_layers() {
+  trace("test layer 1") << "foo";
+  trace("test layer 2") << "bar";
+  trace("test layer 1") << "qux";
+  CHECK_TRACE_CONTENTS("test layer 1,test layer 2", "foobarqux");
+}
+
+void test_trace_supports_hierarchical_layers() {
+  trace("test layer/a") << "foo";
+  trace("different layer/c") << "foo 2";
+  trace("test layer/b") << "bar";
+  CHECK_TRACE_CONTENTS("test layer/", "foobar");
+}
+
+void test_trace_supports_count() {
+  trace("test layer 1") << "foo";
+  trace("test layer 1") << "foo";
+  CHECK_EQ(trace_count("test layer 1", "foo"), 2);
+}
+
+void test_trace_supports_count2() {
+  trace("test layer 1") << "foo";
+  trace("test layer 1") << "bar";
+  CHECK_EQ(trace_count("test layer 1"), 2);
+}
+
+// pending: DUMP tests
+// pending: readable_contents() adds newline if necessary.
+// pending: RAISE also prints to stderr.
+// pending: RAISE doesn't print to stderr if Hide_warnings is set.
+// pending: RAISE doesn't have to be saved if Hide_warnings is set, just printed.
+// pending: RAISE prints to stderr if Trace_stream is NULL.
+// pending: RAISE prints to stderr if Trace_stream is NULL even if Hide_warnings is set.
+// pending: RAISE << ... die() doesn't die if Hide_warnings is set.
+
+
+
+// can't check trace because trace methods call 'split'
+
+void test_split_returns_at_least_one_elem() {
+  vector<string> result = split("", ",");
+  CHECK_EQ(result.size(), 1);
+  CHECK_EQ(result[0], "");
+}
+
+void test_split_returns_entire_input_when_no_delim() {
+  vector<string> result = split("abc", ",");
+  CHECK_EQ(result.size(), 1);
+  CHECK_EQ(result[0], "abc");
+}
+
+void test_split_works() {
+  vector<string> result = split("abc,def", ",");
+  CHECK_EQ(result.size(), 2);
+  CHECK_EQ(result[0], "abc");
+  CHECK_EQ(result[1], "def");
+}
+
+void test_split_works2() {
+  vector<string> result = split("abc,def,ghi", ",");
+  CHECK_EQ(result.size(), 3);
+  CHECK_EQ(result[0], "abc");
+  CHECK_EQ(result[1], "def");
+  CHECK_EQ(result[2], "ghi");
+}
+
+void test_split_handles_multichar_delim() {
+  vector<string> result = split("abc,,def,,ghi", ",,");
+  CHECK_EQ(result.size(), 3);
+  CHECK_EQ(result[0], "abc");
+  CHECK_EQ(result[1], "def");
+  CHECK_EQ(result[2], "ghi");
+}
diff --git a/cpp/tangle/002main.cc b/cpp/tangle/002main.cc
new file mode 100644
index 00000000..851811c8
--- /dev/null
+++ b/cpp/tangle/002main.cc
@@ -0,0 +1,61 @@
+string Last_file = "";
+int main(int argc, const char* argv[]) {
+  Last_file = flag_value("--until", argc, argv);
+  if (flag("test", argc, argv))
+    return run_tests();
+  return tangle_files_in_cwd();
+}
+
+bool eof(istream& in) {
+  in.peek();
+  return in.eof();
+}
+
+bool flag(const string& flag, int argc, const char* argv[]) {
+  for (int i = 1; i < argc; ++i)
+    if (string(argv[i]) == flag)
+      return true;
+  return false;
+}
+
+string flag_value(const string& flag, int argc, const char* argv[]) {
+  for (int i = 1; i < argc-1; ++i)
+    if (string(argv[i]) == flag)
+      return argv[i+1];
+  return "";
+}
+
+
+
+//// test harness
+
+int run_tests() {
+  time_t t; time(&t);
+  cerr << "C tests: " << ctime(&t);
+  for (unsigned long i=0; i < sizeof(Tests)/sizeof(Tests[0]); ++i) {
+    START_TRACING_UNTIL_END_OF_SCOPE;
+    setup();
+    (*Tests[i])();
+    verify();
+  }
+
+  cerr << '\n';
+  if (Num_failures > 0)
+    cerr << Num_failures << " failure"
+         << (Num_failures > 1 ? "s" : "")
+         << '\n';
+  return Num_failures;
+}
+
+void verify() {
+  Hide_warnings = false;
+  if (!Passed)
+    ;
+  else
+    cerr << ".";
+}
+
+void setup() {
+  Hide_warnings = false;
+  Passed = true;
+}
diff --git a/cpp/tangle/030tangle.cc b/cpp/tangle/030tangle.cc
new file mode 100644
index 00000000..2dda8667
--- /dev/null
+++ b/cpp/tangle/030tangle.cc
@@ -0,0 +1,355 @@
+#include<sys/param.h>
+
+int tangle_files_in_cwd() {
+  list<string> result;
+  vector<char*> files = sorted_files(".", /*no extension*/ "");
+  for (vector<char*>::iterator p = files.begin(); p != files.end(); ++p) {
+    if ((*p)[0] < '0' || (*p)[0] > '9') continue;
+    if (!Last_file.empty() && *p > Last_file) break;
+    ifstream in(*p);
+    tangle(in, result);
+  }
+  for (list<string>::iterator p = result.begin(); p != result.end(); ++p)
+    cout << *p << '\n';
+  return 0;
+}
+
+void tangle(istream& in, list<string>& out) {
+  string curr_line;
+  while (!in.eof()) {
+    getline(in, curr_line);
+    if (starts_with(curr_line, ":("))
+      process_next_hunk(in, trim(curr_line), out);
+    else
+      out.push_back(curr_line);
+  }
+  trace_all("tangle", out);
+}
+
+string Toplevel = "run";
+
+void process_next_hunk(istream& in, const string& directive, list<string>& out) {
+  list<string> hunk;
+  string curr_line;
+  while (!in.eof()) {
+    std::streampos old = in.tellg();
+    getline(in, curr_line);
+    if (starts_with(curr_line, ":(")) {
+      in.seekg(old);
+      break;
+    }
+    else {
+      hunk.push_back(curr_line);
+    }
+  }
+
+  istringstream directive_stream(directive.substr(2));  // length of ":("
+  string cmd = next_tangle_token(directive_stream);
+
+  if (cmd == "code") {
+    out.insert(out.end(), hunk.begin(), hunk.end());
+    return;
+  }
+
+  if (cmd == "scenarios") {
+    Toplevel = next_tangle_token(directive_stream);
+    return;
+  }
+
+  if (cmd == "scenario") {
+    list<string> result;
+    string name = next_tangle_token(directive_stream);
+    emit_test(name, hunk, result);
+    out.insert(out.end(), result.begin(), result.end());
+    return;
+  }
+
+  if (cmd == "before" || cmd == "after" || cmd == "replace" || cmd == "replace{}" || cmd == "delete" || cmd == "delete{}") {
+    string pat = next_tangle_token(directive_stream);
+    if (pat == "") {
+      RAISE << "No target for " << cmd << " directive.\n" << die();
+      return;
+    }
+    list<string>::iterator target = find_substr(out, pat);
+    if (target == out.end()) {
+      RAISE << "Couldn't find target " << pat << '\n' << die();
+      return;
+    }
+
+    indent_all(hunk, target);
+
+    if (cmd == "before") {
+      out.splice(target, hunk);
+    }
+    else if (cmd == "after") {
+      ++target;
+      out.splice(target, hunk);
+    }
+    else if (cmd == "replace" || cmd == "delete") {
+      out.splice(target, hunk);
+      out.erase(target);
+    }
+    else if (cmd == "replace{}" || cmd == "delete{}") {
+      if (find_trim(hunk, ":OLD_CONTENTS") == hunk.end()) {
+        out.splice(target, hunk);
+        out.erase(target, balancing_curly(target));
+      }
+      else {
+        list<string>::iterator next = balancing_curly(target);
+        list<string> old_version;
+        old_version.splice(old_version.begin(), out, target, next);
+        old_version.pop_back();  old_version.pop_front();  // contents only please, not surrounding curlies
+
+        list<string>::iterator new_pos = find_trim(hunk, ":OLD_CONTENTS");
+        indent_all(old_version, new_pos);
+        hunk.splice(new_pos, old_version);
+        hunk.erase(new_pos);
+        out.splice(next, hunk);
+      }
+    }
+    return;
+  }
+
+  RAISE << "unknown directive " << cmd << '\n';
+}
+
+// indent all lines in l like indentation at exemplar
+void indent_all(list<string>& l, list<string>::iterator exemplar) {
+  string curr_indent = indent(*exemplar);
+  for (list<string>::iterator p = l.begin(); p != l.end(); ++p)
+    if (!p->empty())
+      p->insert(p->begin(), curr_indent.begin(), curr_indent.end());
+}
+
+string next_tangle_token(istream& in) {
+  in >> std::noskipws;
+  ostringstream out;
+  skip_whitespace(in);
+  if (in.peek() == '"')
+    slurp_tangle_string(in, out);
+  else
+    slurp_word(in, out);
+  return out.str();
+}
+
+void slurp_tangle_string(istream& in, ostream& out) {
+  in.get();
+  char c;
+  while (in >> c) {
+    if (c == '\\')  // only works for double-quotes
+      continue;
+    if (c == '"')
+      break;
+    out << c;
+  }
+}
+
+void slurp_word(istream& in, ostream& out) {
+  char c;
+  while (in >> c) {
+    if (isspace(c) || c == ')') {
+      in.putback(c);
+      break;
+    }
+    out << c;
+  }
+}
+
+void skip_whitespace(istream& in) {
+  while (isspace(in.peek()))
+    in.get();
+}
+
+list<string>::iterator balancing_curly(list<string>::iterator orig) {
+  list<string>::iterator curr = orig;
+  long open_curlies = 0;
+  do {
+    for (string::iterator p = curr->begin(); p != curr->end(); ++p) {
+      if (*p == '{') ++open_curlies;
+      if (*p == '}') --open_curlies;
+    }
+    ++curr;
+    // no guard so far against unbalanced curly
+  } while (open_curlies != 0);
+  return curr;
+}
+
+// A scenario is one or more sessions separated by calls to CLEAR_TRACE ('===')
+//  A session is one or more lines of input
+//  followed by a return value ('=>')
+//  followed by one or more lines expected in trace in order ('+')
+//  followed by one or more lines trace shouldn't include ('-')
+// Remember to update is_input below if you add to this format.
+void emit_test(const string& name, list<string>& lines, list<string>& result) {
+  result.push_back("void test_"+name+"() {");
+  while (any_non_input_line(lines)) {
+    if (!any_line_starts_with(lines, "=>"))
+      emit_session(lines, result);  // simpler version; no need to check result
+    else
+      emit_result_checking_session(lines, result);
+    if (!lines.empty() && lines.front()[0] == '+')
+      result.push_back("  CHECK_TRACE_CONTENTS(\""+expected_in_trace(lines)+"\");");
+    while (!lines.empty() && lines.front()[0] == '-') {
+      result.push_back("  CHECK_TRACE_DOESNT_CONTAIN(\""+expected_not_in_trace(lines.front())+"\");");
+      lines.pop_front();
+    }
+    if (!lines.empty() && lines.front() == "===") {
+      result.push_back("  CLEAR_TRACE;");
+      lines.pop_front();
+    }
+  }
+  result.push_back("}");
+
+  while (!lines.empty() &&
+         (trim(lines.front()).empty() || starts_with(lines.front(), "//")))
+    lines.pop_front();
+  if (!lines.empty()) {
+    cerr << lines.size() << " unprocessed lines in scenario.\n";
+    exit(1);
+  }
+}
+
+void emit_session(list<string>& lines, list<string>& result) {
+  result.push_back("  "+Toplevel+"(\""+input_lines(lines)+"\");");
+}
+
+void emit_result_checking_session(list<string>& lines, list<string>& result) {
+  result.push_back("{");
+  result.push_back("  ostringstream os;");
+  result.push_back("  TEMP(tmp, "+Toplevel+"(\""+input_lines(lines)+"\"));");
+  result.push_back("  os << tmp;");
+  if (!lines.empty() && starts_with(lines.front(), "=>")) {
+    size_t pos = lines.front().find("=>")+2;  // length of '=>'
+    result.push_back("  CHECK_EQ(os.str(), \""+trim(string(lines.front(), pos))+"\");");
+    lines.pop_front();
+  }
+  result.push_back("}");
+}
+
+bool is_input(const string& line) {
+  return line != "===" && line[0] != '+' && line[0] != '-' && !starts_with(line, "=>");
+}
+
+string input_lines(list<string>& hunk) {
+  string result;
+  while (!hunk.empty() && is_input(hunk.front())) {
+    result += hunk.front()+"";  // temporary delimiter; replace with escaped newline after escaping other backslashes
+    hunk.pop_front();
+  }
+  return escape(result);
+}
+
+string expected_in_trace(list<string>& hunk) {
+  string result;
+  while (!hunk.empty() && hunk.front()[0] == '+') {
+    hunk.front().erase(0, 1);
+    result += hunk.front()+"";
+    hunk.pop_front();
+  }
+  return escape(result);
+}
+
+string expected_not_in_trace(const string& line) {
+  return escape(line.substr(1));
+}
+
+list<string>::iterator find_substr(list<string>& in, const string& pat) {
+  for (list<string>::iterator p = in.begin(); p != in.end(); ++p)
+    if (p->find(pat) != NOT_FOUND)
+      return p;
+  return in.end();
+}
+
+list<string>::iterator find_trim(list<string>& in, const string& pat) {
+  for (list<string>::iterator p = in.begin(); p != in.end(); ++p)
+    if (trim(*p) == pat)
+      return p;
+  return in.end();
+}
+
+string escape(string s) {
+  s = replace_all(s, "\\", "\\\\");
+  s = replace_all(s, "\"", "\\\"");
+  s = replace_all(s, "", "\\n");
+  return s;
+}
+
+string replace_all(string s, const string& a, const string& b) {
+  for (size_t pos = s.find(a); pos != NOT_FOUND; pos = s.find(a, pos+b.size()))
+    s = s.replace(pos, a.size(), b);
+  return s;
+}
+
+bool any_line_starts_with(const list<string>& lines, const string& pat) {
+  for (list<string>::const_iterator p = lines.begin(); p != lines.end(); ++p)
+    if (starts_with(*p, pat)) return true;
+  return false;
+}
+
+bool any_non_input_line(const list<string>& lines) {
+  for (list<string>::const_iterator p = lines.begin(); p != lines.end(); ++p)
+    if (!is_input(*p)) return true;
+  return false;
+}
+
+#include <locale>
+using std::isspace;  // unicode-aware
+
+// does s start with pat, after skipping whitespace?
+// pat can't start with whitespace
+bool starts_with(const string& s, const string& pat) {
+  for (size_t pos = 0; pos < s.size(); ++pos)
+    if (!isspace(s[pos]))
+      return s.compare(pos, pat.size(), pat) == 0;
+  return false;
+}
+
+string indent(const string& s) {
+  for (size_t pos = 0; pos < s.size(); ++pos)
+    if (!isspace(s[pos]))
+      return s.substr(0, pos);
+  return "";
+}
+
+string strip_indent(const string& s, size_t n) {
+  if (s.empty()) return "";
+  string::const_iterator curr = s.begin();
+  while (curr != s.end() && n > 0 && isspace(*curr)) {
+    ++curr;
+    --n;
+  }
+  return string(curr, s.end());
+}
+
+string trim(const string& s) {
+  string::const_iterator first = s.begin();
+  while (first != s.end() && isspace(*first))
+    ++first;
+  if (first == s.end()) return "";
+
+  string::const_iterator last = --s.end();
+  while (last != s.begin() && isspace(*last))
+    --last;
+  ++last;
+  return string(first, last);
+}
+
+#include<dirent.h>
+
+vector<char*> sorted_files(const char* dirname, const char* ext) {
+  vector<char*> result;
+  dirent** files;
+  int num_files = scandir(dirname, &files, NULL, alphasort);
+  for (int i = 0; i < num_files; ++i) {
+    unsigned long n = strlen(files[i]->d_name), extn = strlen(ext);
+    if (n < extn) continue;
+    if (strncmp(&files[i]->d_name[n-extn], ext, extn)) continue;
+    if (!isdigit(files[i]->d_name[0])) continue;
+    char* s = new char[n+1];
+    strncpy(s, files[i]->d_name, n+1);
+    result.push_back(s);
+    free(files[i]);
+  }
+  free(files);
+  return result;
+}
diff --git a/cpp/tangle/030tangle.test.cc b/cpp/tangle/030tangle.test.cc
new file mode 100644
index 00000000..36ce2d1f
--- /dev/null
+++ b/cpp/tangle/030tangle.test.cc
@@ -0,0 +1,251 @@
+void test_tangle() {
+  istringstream in("a\nb\nc\n:(before b)\nd\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "adbc");
+}
+
+void test_tangle2() {
+  istringstream in("a\nb\nc\n:(after b)\nd\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "abdc");
+}
+
+void test_tangle_at_end() {
+  istringstream in("a\nb\nc\n:(after c)\nd\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "abcd");
+}
+
+void test_tangle_indents_hunks_correctly() {
+  istringstream in("a\n  b\nc\n:(after b)\nd\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "a  b  dc");
+}
+
+void test_tangle_warns_on_missing_target() {
+  Hide_warnings = true;
+  istringstream in(":(before)\nabc def\n");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_TRACE_WARNS();
+}
+
+void test_tangle_warns_on_unknown_target() {
+  Hide_warnings = true;
+  istringstream in(":(before \"foo\")\nabc def\n");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_TRACE_WARNS();
+}
+
+void test_tangle_delete_range_of_lines() {
+  istringstream in("a\nb {\nc\n}\n:(delete{} \"b\")\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "a");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "b");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "c");
+}
+
+void test_tangle_replace() {
+  istringstream in("a\nb\nc\n:(replace b)\nd\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "adc");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "b");
+}
+
+void test_tangle_replace_range_of_lines() {
+  istringstream in("a\nb {\nc\n}\n:(replace{} \"b\")\nd\ne\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "ade");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "b {");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "c");
+}
+
+void test_tangle_replace_tracks_old_lines() {
+  istringstream in("a\nb {\nc\n}\n:(replace{} \"b\")\nd\n:OLD_CONTENTS\ne\n");
+  list<string> dummy;
+  tangle(in, dummy);
+  CHECK_TRACE_CONTENTS("tangle", "adce");
+  CHECK_TRACE_DOESNT_CONTAIN("tangle", "b {");
+}
+
+// todo: include line numbers in tangle errors
+
+
+
+void test_tangle_supports_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc def\n+layer1: pqr\n+layer2: xyz");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc def\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqrlayer2: xyz\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_supports_configurable_toplevel() {
+  istringstream in(":(scenarios foo)\n:(scenario does_bar)\nabc def\n+layer1: pqr");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  foo(\"abc def\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+
+  istringstream cleanup(":(scenarios run)\n");
+  tangle(cleanup, lines);
+}
+
+void test_tangle_supports_strings_in_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc \"def\"\n+layer1: pqr\n+layer2: \"xyz\"");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc \\\"def\\\"\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqrlayer2: \\\"xyz\\\"\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_supports_strings_in_scenarios2() {
+  istringstream in(":(scenario does_bar)\nabc \"\"\n+layer1: pqr\n+layer2: \"\"");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc \\\"\\\"\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqrlayer2: \\\"\\\"\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_supports_multiline_input_in_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc def\n  efg\n+layer1: pqr\n+layer2: \"\"");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc def\\n  efg\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqrlayer2: \\\"\\\"\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_supports_reset_in_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc def\n===\nefg\n+layer1: pqr\n+layer2: \"\"");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc def\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CLEAR_TRACE;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"efg\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqrlayer2: \\\"\\\"\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_can_check_for_absence_at_end_of_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc def\n  efg\n+layer1: pqr\n-layer1: xyz");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc def\\n  efg\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: xyz\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_can_check_for_absence_at_end_of_scenarios2() {
+  istringstream in(":(scenario does_bar)\nabc def\n  efg\n-layer1: pqr\n-layer1: xyz");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  run(\"abc def\\n  efg\\n\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_DOESNT_CONTAIN(\"layer1: xyz\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_can_check_return_values_of_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc def\n=> pqr");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "{");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  ostringstream os;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  TEMP(tmp, run(\"abc def\\n\"));");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  os << tmp;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_EQ(os.str(), \"pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+void test_tangle_can_check_return_values_of_multiple_scenarios() {
+  istringstream in(":(scenario does_bar)\nabc\n=> pqr\n+layer1: pqr\ndef\n=> xyz\n");
+  list<string> lines;
+  tangle(in, lines);
+  CHECK_EQ(lines.front(), "void test_does_bar() {");  lines.pop_front();
+  CHECK_EQ(lines.front(), "{");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  ostringstream os;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  TEMP(tmp, run(\"abc\\n\"));");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  os << tmp;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_EQ(os.str(), \"pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_TRACE_CONTENTS(\"layer1: pqr\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "{");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  ostringstream os;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  TEMP(tmp, run(\"def\\n\"));");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  os << tmp;");  lines.pop_front();
+  CHECK_EQ(lines.front(), "  CHECK_EQ(os.str(), \"xyz\");");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK_EQ(lines.front(), "}");  lines.pop_front();
+  CHECK(lines.empty());
+}
+
+
+
+void test_trim() {
+  CHECK_EQ(trim(""), "");
+  CHECK_EQ(trim(" "), "");
+  CHECK_EQ(trim("  "), "");
+  CHECK_EQ(trim("a"), "a");
+  CHECK_EQ(trim(" a"), "a");
+  CHECK_EQ(trim("  a"), "a");
+  CHECK_EQ(trim("  ab"), "ab");
+  CHECK_EQ(trim("a "), "a");
+  CHECK_EQ(trim("a  "), "a");
+  CHECK_EQ(trim("ab  "), "ab");
+  CHECK_EQ(trim(" a "), "a");
+  CHECK_EQ(trim("  a  "), "a");
+  CHECK_EQ(trim("  ab  "), "ab");
+}
+
+void test_strip_indent() {
+  CHECK_EQ(strip_indent("", 0), "");
+  CHECK_EQ(strip_indent("", 1), "");
+  CHECK_EQ(strip_indent("", 3), "");
+  CHECK_EQ(strip_indent(" ", 0), " ");
+  CHECK_EQ(strip_indent(" a", 0), " a");
+  CHECK_EQ(strip_indent(" ", 1), "");
+  CHECK_EQ(strip_indent(" a", 1), "a");
+  CHECK_EQ(strip_indent(" ", 2), "");
+  CHECK_EQ(strip_indent(" a", 2), "a");
+  CHECK_EQ(strip_indent("  ", 0), "  ");
+  CHECK_EQ(strip_indent("  a", 0), "  a");
+  CHECK_EQ(strip_indent("  ", 1), " ");
+  CHECK_EQ(strip_indent("  a", 1), " a");
+  CHECK_EQ(strip_indent("  ", 2), "");
+  CHECK_EQ(strip_indent("  a", 2), "a");
+  CHECK_EQ(strip_indent("  ", 3), "");
+  CHECK_EQ(strip_indent("  a", 3), "a");
+}
diff --git a/cpp/tangle/boot.cc b/cpp/tangle/boot.cc
new file mode 100644
index 00000000..89f943a8
--- /dev/null
+++ b/cpp/tangle/boot.cc
@@ -0,0 +1,63 @@
+// C++ style:
+//  no pointers except cell*
+//  use long as the default integer type; it's always large enough to hold pointers
+
+#define unused __attribute__((unused))
+
+#include<cstdio>
+#include<cstring>
+#include<cstdlib>
+#include<errno.h>
+#include<time.h>
+#include<math.h>
+#include<vector>
+using std::vector;
+#include<list>
+using std::list;
+#include<stack>
+using std::stack;
+#include<utility>
+using std::pair;
+
+#include<tr1/unordered_map>
+using std::tr1::unordered_map;
+#include<tr1/unordered_set>
+using std::tr1::unordered_set;
+#include<algorithm>
+
+#include<string>
+using std::string;
+const size_t NOT_FOUND = string::npos;
+
+#include<iostream>
+using std::istream;
+using std::ostream;
+using std::iostream;
+using std::cin;
+using std::cout;
+using std::cerr;
+
+#include<sstream>
+using std::stringstream;
+using std::istringstream;
+using std::ostringstream;
+
+#include<fstream>
+using std::ifstream;
+using std::ofstream;
+
+
+
+// interpreter decls
+
+#include "type_list"
+
+#include "function_list"
+
+// interpreter impl
+
+#include "file_list"
+
+// interpreter tests
+
+#include "test_file_list"
diff --git a/cpp/tangle/makefile b/cpp/tangle/makefile
new file mode 100644
index 00000000..3d938c09
--- /dev/null
+++ b/cpp/tangle/makefile
@@ -0,0 +1,23 @@
+tangle: makefile type_list function_list file_list test_file_list test_list
+	g++ -O3 -Wall -Wextra -fno-strict-aliasing boot.cc -o tangle
+
+type_list: boot.cc [0-9]*.cc
+	@# assumes struct decl has space before '{'
+	@grep -h "^struct .* {" [0-9]*.cc |perl -pwe 's/(struct *[^ ]*).*/$$1;/' > type_list
+	@grep -h typedef [0-9]*.cc >> type_list
+
+function_list: boot.cc [0-9]*.cc
+	@# assumes function decl has space before '{'
+	@grep -h "^[^ #].*) {" [0-9]*.cc |perl -pwe 's/ {.*/;/' > function_list
+
+file_list: boot.cc [0-9]*.cc
+	@ls [0-9]*.cc |grep -v "\.test\.cc$$" |perl -pwe 's/.*/#include "$$&"/' > file_list
+
+test_file_list: [0-9]*.test.cc
+	@ls [0-9]*.test.cc |perl -pwe 's/.*/#include "$$&"/' > test_file_list
+
+test_list: [0-9]*.cc
+	@grep -h "^[[:space:]]*void test_" [0-9]*.cc |perl -pwe 's/^\s*void (.*)\(\) {$$/$$1,/' > test_list
+
+clean:
+	rm -rf tangle *_list