diff --git a/py/lexer.c b/py/lexer.c
index 88bc0a1aae2212e4c76c05971bc873ce4f0acdab..cd2e05ece01d4432e51bda6e264be4596185b664 100644
--- a/py/lexer.c
+++ b/py/lexer.c
@@ -10,6 +10,9 @@
 
 #define TAB_SIZE (8)
 
+// TODO seems that CPython allows NULL byte in the input stream
+// don't know if that's intentional or not, but we don't allow it
+
 struct _py_lexer_t {
     const char *name;           // name of source
     void *stream_data;          // data for stream
diff --git a/py/repl.c b/py/repl.c
new file mode 100644
index 0000000000000000000000000000000000000000..f295aff23db50a43cfbee66fd83273d2e38b6ed4
--- /dev/null
+++ b/py/repl.c
@@ -0,0 +1,44 @@
+#include "misc.h"
+#include "repl.h"
+
+bool str_startswith_word(const char *str, const char *head) {
+    int i;
+    for (i = 0; str[i] && head[i]; i++) {
+        if (str[i] != head[i]) {
+            return false;
+        }
+    }
+    return head[i] == '\0' && (str[i] == '\0' || !g_unichar_isalpha(str[i]));
+}
+
+bool py_repl_is_compound_stmt(const char *line) {
+    // compound if line starts with a certain keyword
+    if (
+           str_startswith_word(line, "if")
+        || str_startswith_word(line, "while")
+        || str_startswith_word(line, "for")
+        || str_startswith_word(line, "true")
+        || str_startswith_word(line, "with")
+        || str_startswith_word(line, "def")
+        || str_startswith_word(line, "class")
+        || str_startswith_word(line, "@")
+       ) {
+        return true;
+    }
+
+    // also "compound" if unmatched open bracket
+    int n_paren = 0;
+    int n_brack = 0;
+    int n_brace = 0;
+    for (const char *l = line; *l; l++) {
+        switch (*l) {
+            case '(': n_paren += 1; break;
+            case ')': n_paren -= 1; break;
+            case '[': n_brack += 1; break;
+            case ']': n_brack -= 1; break;
+            case '{': n_brace += 1; break;
+            case '}': n_brace -= 1; break;
+        }
+    }
+    return n_paren > 0 || n_brack > 0 || n_brace > 0;
+}
diff --git a/py/repl.h b/py/repl.h
new file mode 100644
index 0000000000000000000000000000000000000000..014e8609b4edadff0bdfbe70dce42c5b79f5a8c8
--- /dev/null
+++ b/py/repl.h
@@ -0,0 +1 @@
+bool py_repl_is_compound_stmt(const char *line);
diff --git a/unix/Makefile b/unix/Makefile
index 7c8b5a2b9a38a0b26e57d75d763cdf5bf017093c..9dd17f8350d99c5298fd8f934c9fb65889957433 100644
--- a/unix/Makefile
+++ b/unix/Makefile
@@ -30,6 +30,7 @@ PY_O = \
 	emitinlinethumb.o \
 	runtime.o \
 	vm.o \
+	repl.o \
 
 OBJ = $(addprefix $(BUILD)/, $(SRC_C:.c=.o) $(PY_O))
 LIB = -lreadline
diff --git a/unix/main.c b/unix/main.c
index f7b06d4f8343c8c0a74ee0ada07d154aa2ce55cc..12aca6ddf29fa10b79c2f42dd6bc44d7809d9cfa 100644
--- a/unix/main.c
+++ b/unix/main.c
@@ -10,32 +10,10 @@
 #include "parse.h"
 #include "compile.h"
 #include "runtime.h"
+#include "repl.h"
 
 #include <readline/readline.h>
 
-bool str_startswith_word(const char *str, const char *head) {
-    int i;
-    for (i = 0; str[i] && head[i]; i++) {
-        if (str[i] != head[i]) {
-            return false;
-        }
-    }
-    return head[i] == '\0' && (str[i] == '\0' || !g_unichar_isalpha(str[i]));
-}
-
-bool is_compound_stmt(const char *line) {
-    // TODO also "compound" if unmatched open bracket
-    return
-           str_startswith_word(line, "if")
-        || str_startswith_word(line, "while")
-        || str_startswith_word(line, "for")
-        || str_startswith_word(line, "true")
-        || str_startswith_word(line, "with")
-        || str_startswith_word(line, "def")
-        || str_startswith_word(line, "class")
-        || str_startswith_word(line, "@");
-}
-
 char *str_join(const char *s1, int sep_char, const char *s2) {
     int l1 = strlen(s1);
     int l2 = strlen(s2);
@@ -46,6 +24,7 @@ char *str_join(const char *s1, int sep_char, const char *s2) {
         l1 += 1;
     }
     memcpy(s + l1, s2, l2);
+    s[l1 + l2] = 0;
     return s;
 }
 
@@ -56,7 +35,7 @@ void do_repl() {
             // EOF
             return;
         }
-        if (is_compound_stmt(line)) {
+        if (py_repl_is_compound_stmt(line)) {
             for (;;) {
                 char *line2 = readline("... ");
                 if (line2 == NULL || strlen(line2) == 0) {