Add evaluator
diff --git a/ide/NbRuntime.cpp b/ide/NbRuntime.cpp
new file mode 100644
index 0000000..8454597
--- /dev/null
+++ b/ide/NbRuntime.cpp
@@ -0,0 +1,122 @@
+#include <QCoreApplication>
+
+#include "NbRuntime.h"
+#include "../Parser.h"
+
+NbRuntime::NbRuntime(QObject *parent)
+    : QThread(parent)
+{
+}
+
+void NbRuntime::queueCell(Cell *cell)
+{
+    if (!_cells.contains(cell))
+    {
+        qInfo() << "Queueing cell";
+
+        _cells.append(cell);
+
+        emit cellWaiting(cell);
+
+        if (!_running)
+            evalRemaining();
+    }
+}
+
+void NbRuntime::unqueueCell(Cell *cell)
+{
+    if (cell == _running)
+    {
+        // Exception should propagate back up to evalRemaining()
+        _eval.quit();
+    }
+    else
+    {
+        _cells.removeOne(cell);
+    }
+}
+
+void NbRuntime::evalRemaining()
+{
+    qInfo() << "evalRemaining";
+
+    while (!_cells.empty())
+    {
+        QCoreApplication::processEvents();
+
+        Cell *cell = _cells.first();
+        _cells.removeFirst();
+
+        _running = cell;
+
+        Parser parser(cell->code());
+        RuntimeResult result;
+
+        emit cellRunning(cell);
+
+        try
+        {
+            // Allow this cell to be quit
+            QCoreApplication::processEvents();
+
+            while (true)
+            {
+                ParseResult ret;
+                Function func;
+                AstNode ast;
+
+                if ((ret = parser.parseFunctionDefinition(&func)))
+                {
+                    _eval.addFunction(func);
+                }
+                else if (ret.status() == ParseResult::INCOMPLETE)
+                {
+                    emit cellFailedToParse(cell, ret);
+                    goto endOfCell; // JANK!
+                }
+                else if ((ret = parser.parseOne(&ast)))
+                {
+                    RuntimeResult nodeRes = _eval.evaluate(ast, _ctx);
+                    result += nodeRes;
+
+                    if (!nodeRes.success())
+                    {
+                        break;
+                    }
+                }
+                else if (ret.status() == ParseResult::INCOMPLETE)
+                {
+                    emit cellFailedToParse(cell, ret);
+                    break;
+                }
+                else
+                {
+                    parser.skip();
+
+                    if (!parser.atEnd())
+                    {
+                        emit cellFailedToParse(cell, ParseResult(ParseResult::NO_MATCH, "Garbage at end of input", parser.save()));
+                        goto endOfCell;
+                    }
+
+                    break;
+                }
+            }
+
+            emit cellFinishedRunning(cell, result);
+
+        endOfCell:
+            _running = nullptr;
+        }
+        catch (EvalQuitException &ex)
+        {
+            _running = nullptr;
+            emit cellQuit(cell);
+        }
+        catch (StackOverflowException &ex)
+        {
+            _running = nullptr;
+            emit cellFinishedRunning(cell, RuntimeResult(ex));
+        }
+    }
+}