diff --git a/include/ast/BlockNode.h b/include/ast/BlockNode.h
index d1ec053029b16dd18ca585d020f14667c606ecb9..7879f9038b9a7464818f1149128014c0c29f0eda 100644
--- a/include/ast/BlockNode.h
+++ b/include/ast/BlockNode.h
@@ -17,6 +17,8 @@ public:
     Vector<Node*> get_statements() const;
 
     Node* evaluate() const override;
+    Node* evaluate_same_scope() const;
+
     String to_s() const override;
 
 private:
diff --git a/include/parser/BuiltIn.h b/include/parser/BuiltIn.h
index 130f73ebeb19f727762743a0c639987905fa5975..cd5dbd676f6f42cd84eb16f53c302c0d8d0fe3f6 100644
--- a/include/parser/BuiltIn.h
+++ b/include/parser/BuiltIn.h
@@ -16,6 +16,7 @@ class BuiltIn
 public:
     static Node* print(const CallNode& call, const Vector<ExpressionNode*>& args);
     static Node* read(const CallNode& call, const Vector<ExpressionNode*>& args);
+    static Node* fast_exit(const CallNode& call, const Vector<ExpressionNode*>& args);
 
     static HashMap<String, Node* (*)(const CallNode&, const Vector<ExpressionNode*>&)> functions;
 };
diff --git a/include/parser/Parser.h b/include/parser/Parser.h
index d7e1462e62c0f09031a63d300f6c884fac1247aa..bb44e6ea3a8c878ba0837a852773f74881240c57 100644
--- a/include/parser/Parser.h
+++ b/include/parser/Parser.h
@@ -47,6 +47,12 @@ public:
      */
     ~Parser();
 
+    /**
+     * @brief Sets the tokens to parse
+     * @param tokens The tokens to parse
+     */
+    void set_tokens(const Vector<Token>& tokens);
+
     /**
      * @brief Parses the token stream and returns the root AST node
      * @param args The arguments passed to the program
diff --git a/include/utils/Common.h b/include/utils/Common.h
index 87fd2f0f64052bc0166c3b1a5f72cb9126394d11..2cfde8a494e978f3908fc09e932b40cf2233727a 100644
--- a/include/utils/Common.h
+++ b/include/utils/Common.h
@@ -20,6 +20,7 @@
 using std::cerr;
 using std::cin;
 using std::cout;
+using std::endl;
 using std::getline;
 
 namespace funk
diff --git a/source/ast/BlockNode.cc b/source/ast/BlockNode.cc
index 826cdfbace5962e014b8c7b7a57c12442b82e03f..c9892ec9e692b8279596d0747a8ba47d0ff9332b 100644
--- a/source/ast/BlockNode.cc
+++ b/source/ast/BlockNode.cc
@@ -30,6 +30,13 @@ Node* BlockNode::evaluate() const
     return result;
 }
 
+Node* BlockNode::evaluate_same_scope() const
+{
+    Node* result{};
+    for (Node* statement : statements) { result = statement->evaluate(); }
+    return result;
+}
+
 String BlockNode::to_s() const
 {
     String repr{};
diff --git a/source/main.cc b/source/main.cc
index 87dc2f0b814a1837270269a169e1c468bb7786d7..54573944adcd1243b905969e9f8a7851f177e898 100644
--- a/source/main.cc
+++ b/source/main.cc
@@ -47,7 +47,7 @@ bool setup(ArgParser& parser, Config& config)
     // Print help message
     if (parser.has_option("--help"))
     {
-        cout << ArgParser::help("funk [options] <file>", options) << "\n";
+        cout << ArgParser::help("funk [options] <file>", options) << endl;
         return false;
     }
 
@@ -121,10 +121,62 @@ void process_file(const String& file, const Config& config, const Vector<String>
     catch (const FunkError& e)
     {
         LOG_ERROR("Error processing file " + file + ": " + e.what());
-        cerr << "Error: " << e.trace() << '\n';
+        cerr << "Error: " << e.trace() << endl;
     }
 }
 
+/**
+ * @brief Funk REPL
+ * Reads and executes Funk code from the standard input
+ */
+void repl()
+{
+    cout << "Funk REPL. Press Ctrl+D to exit or type 'exit()'." << endl << endl;
+
+    String input;
+    Vector<Token> tokens{};
+    Parser parser{tokens, ""};
+    Scope::instance().push();
+
+    while (true)
+    {
+        cout << ">>> ";
+        cout.flush();
+        if (!getline(cin, input)) { break; }
+        if (input.empty()) { continue; }
+
+        try
+        {
+            // Add a semicolon to the input if it doesn't end with one
+            if (input.back() != ';') { input += ";"; }
+
+            Lexer lexer{input + "\n", ""};
+            Vector<Token> tokens{lexer.tokenize()};
+
+            parser.set_tokens(tokens);
+            BlockNode* ast{static_cast<BlockNode*>(parser.parse())};
+
+            Node* result{ast->evaluate_same_scope()};
+            if (result) { cout << result->to_s() << endl; }
+        }
+        catch (const FunkError& e)
+        {
+            cerr << "Error: " << e.trace() << endl;
+        }
+        catch (const std::exception& e)
+        {
+            cerr << "Error: " << e.what() << endl;
+        }
+        catch (...)
+        {
+            cerr << "Unknown error occurred" << endl;
+        }
+    }
+
+    Scope::instance().pop();
+    cout << "Bye!" << endl;
+}
+
 /**
  * @brief Main entry point for the Funk interpreter
  * Parses command line arguments and processes input files
@@ -142,7 +194,7 @@ int main(int argc, char* argv[])
 
     // Process each file
     if (parser.has_file()) { process_file(parser.get_file(), config, parser.get_args()); }
-    else { cout << "Funk REPL, not implemented yet\n"; }
+    else { repl(); }
 
     return 0;
 }
diff --git a/source/parser/BuiltIn.cc b/source/parser/BuiltIn.cc
index 005e8d7ccea807bb6b0f1078268a06db3da33b2d..66067137eb5f99c71c0d67920ee5d3b94000bb71 100644
--- a/source/parser/BuiltIn.cc
+++ b/source/parser/BuiltIn.cc
@@ -11,7 +11,7 @@ Node* BuiltIn::print(const CallNode& call, const Vector<ExpressionNode*>& args)
         if (!result) { throw RuntimeError(arg->get_location(), "Print argument did not evaluate to an expression"); }
         cout << result->get_value().cast<String>() << " ";
     }
-    cout << "\n";
+    cout << endl;
     return new LiteralNode(call.get_location(), None{});
 }
 
@@ -23,7 +23,15 @@ Node* BuiltIn::read(const CallNode& call, const Vector<ExpressionNode*>& args)
     return new LiteralNode(call.get_location(), input);
 }
 
+Node* BuiltIn::fast_exit(const CallNode& call [[maybe_unused]], const Vector<ExpressionNode*>& args)
+{
+    int status{0};
+    if (!args.empty()) { status = dynamic_cast<LiteralNode*>(args[0]->evaluate())->get_value().cast<int>(); }
+
+    exit(status);
+}
+
 HashMap<String, Node* (*)(const CallNode&, const Vector<ExpressionNode*>&)> BuiltIn::functions{
-    {"print", print}, {"read", read}};
+    {"print", print}, {"read", read}, {"exit", fast_exit}};
 
 } // namespace funk
diff --git a/source/parser/Parser.cc b/source/parser/Parser.cc
index d35ab7090a8b6ede22da105723616b7e7c80c9b6..bcaca9fa61ea7c1c4fb27790aeceed4d927bc40a 100644
--- a/source/parser/Parser.cc
+++ b/source/parser/Parser.cc
@@ -24,6 +24,12 @@ Node* Parser::parse(const Vector<String>& args)
     return block;
 }
 
+void Parser::set_tokens(const Vector<Token>& tokens)
+{
+    this->tokens = tokens;
+    this->index = 0;
+}
+
 Parser Parser::load(String filename)
 {
     Lexer lexer{read_file(filename), filename};
@@ -72,6 +78,9 @@ Node* Parser::parse_statement()
 {
     LOG_DEBUG("Parse statement");
 
+    // Empty statement, just a semicolon
+    if (match(TokenType::SEMICOLON)) { return new LiteralNode(peek_prev().get_location(), NodeValue(None())); }
+
     Node* control{parse_control()};
     if (control) { return control; }