diff --git a/examples/io.funk b/examples/io.funk
new file mode 100644
index 0000000000000000000000000000000000000000..5dcde6bf9ea8c79171af0f89017fb10f10595f54
--- /dev/null
+++ b/examples/io.funk
@@ -0,0 +1 @@
+print("Hej " + read("Vad heter du?") + "!");
diff --git a/include/ast/expression/CallNode.h b/include/ast/expression/CallNode.h
new file mode 100644
index 0000000000000000000000000000000000000000..1491c28c7ccce6c878adca71b4b7c6fb24577cd4
--- /dev/null
+++ b/include/ast/expression/CallNode.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "ast/expression/ExpressionNode.h"
+#include "ast/expression/LiteralNode.h"
+#include "logging/LogMacros.h"
+#include "parser/BuiltIn.h"
+#include "token/Token.h"
+
+namespace funk
+{
+class CallNode : public ExpressionNode
+{
+public:
+    CallNode(const Token& identifier, const Vector<ExpressionNode*>& args);
+    ~CallNode() override;
+
+    Node* evaluate() const override;
+
+    String to_s() const override;
+
+    NodeValue get_value() const override;
+
+private:
+    Token identifier;
+    Vector<ExpressionNode*> args;
+};
+} // namespace funk
diff --git a/include/parser/BuiltIn.h b/include/parser/BuiltIn.h
new file mode 100644
index 0000000000000000000000000000000000000000..130f73ebeb19f727762743a0c639987905fa5975
--- /dev/null
+++ b/include/parser/BuiltIn.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "ast/expression/CallNode.h"
+#include "ast/expression/LiteralNode.h"
+#include "logging/LogMacros.h"
+
+namespace funk
+{
+/**
+ * @brief Forward declaration of CallNode.
+ */
+class CallNode;
+
+class BuiltIn
+{
+public:
+    static Node* print(const CallNode& call, const Vector<ExpressionNode*>& args);
+    static Node* read(const CallNode& call, const Vector<ExpressionNode*>& args);
+
+    static HashMap<String, Node* (*)(const CallNode&, const Vector<ExpressionNode*>&)> functions;
+};
+} // namespace funk
diff --git a/include/parser/Parser.h b/include/parser/Parser.h
index 0ff11c0d60b82e519659d71f65c149c13eebd66b..1f60ce4e5337c646d3885ac5b0f70323c473cc0d 100644
--- a/include/parser/Parser.h
+++ b/include/parser/Parser.h
@@ -15,6 +15,7 @@
 #include "ast/Node.h"
 #include "ast/declaration/DeclarationNode.h"
 #include "ast/expression/BinaryOpNode.h"
+#include "ast/expression/CallNode.h"
 #include "ast/expression/LiteralNode.h"
 #include "ast/expression/UnaryOpNode.h"
 
diff --git a/include/utils/Common.h b/include/utils/Common.h
index 38da432c9cc113c1b5b0d21be93d00597708ab30..87fd2f0f64052bc0166c3b1a5f72cb9126394d11 100644
--- a/include/utils/Common.h
+++ b/include/utils/Common.h
@@ -18,7 +18,9 @@
 #include <algorithm>
 
 using std::cerr;
+using std::cin;
 using std::cout;
+using std::getline;
 
 namespace funk
 {
diff --git a/source/ast/expression/CallNode.cc b/source/ast/expression/CallNode.cc
new file mode 100644
index 0000000000000000000000000000000000000000..eaba781f56fbb78c565db40c5fe8d4fbab000710
--- /dev/null
+++ b/source/ast/expression/CallNode.cc
@@ -0,0 +1,44 @@
+#include "ast/expression/CallNode.h"
+
+namespace funk
+{
+CallNode::CallNode(const Token& identifier, const Vector<ExpressionNode*>& args) :
+    ExpressionNode(identifier.get_location()), identifier(identifier), args(args)
+{
+}
+
+CallNode::~CallNode()
+{
+    for (Node* arg : args) { delete arg; }
+}
+
+Node* CallNode::evaluate() const
+{
+    LOG_DEBUG("Evaluating call to " + identifier.get_lexeme());
+    auto it = BuiltIn::functions.find(identifier.get_lexeme());
+    if (it != BuiltIn::functions.end()) { return it->second(*this, args); }
+    throw RuntimeError(location, "Unknown function: " + identifier.get_lexeme());
+}
+
+String CallNode::to_s() const
+{
+    String result{identifier.get_lexeme()};
+    result += "( ";
+    for (size_t i{0}; i < args.size(); i++)
+    {
+        result += args[i]->to_s();
+        if (i < args.size() - 1) { result += ", "; }
+    }
+    result += " )";
+    return result;
+}
+
+NodeValue CallNode::get_value() const
+{
+    ExpressionNode* result{dynamic_cast<ExpressionNode*>(evaluate())};
+    if (!result) { throw RuntimeError(location, "Call did not evaluate to an expression"); }
+
+    return result->get_value();
+}
+
+} // namespace funk
diff --git a/source/main.cc b/source/main.cc
index 5fba43d16accbbe298b04216de9f4a0c7ade220a..d8c959c59b28cefc38aeac312497fe33ea37f425 100644
--- a/source/main.cc
+++ b/source/main.cc
@@ -116,7 +116,7 @@ void process_file(const String& file, const Config& config)
             LOG_INFO("Abstract Syntax Tree:");
             std::istringstream stream(ast->to_s());
             String line;
-            while (std::getline(stream, line)) { LOG_INFO(line); }
+            while (getline(stream, line)) { LOG_INFO(line); }
         }
 
         LOG_DEBUG("Evaluating AST...");
diff --git a/source/parser/BuiltIn.cc b/source/parser/BuiltIn.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9c1c4221426216c1cfd21afd011e7dad660d1719
--- /dev/null
+++ b/source/parser/BuiltIn.cc
@@ -0,0 +1,31 @@
+#include "parser/BuiltIn.h"
+
+namespace funk
+{
+
+Node* BuiltIn::print(const CallNode& call, const Vector<ExpressionNode*>& args)
+{
+    LOG_DEBUG("Executing print");
+    if (args.empty()) { cout << "\n"; }
+    for (ExpressionNode* arg : args)
+    {
+        ExpressionNode* result{dynamic_cast<ExpressionNode*>(arg->evaluate())};
+        if (!result) { throw RuntimeError(arg->get_location(), "Print argument did not evaluate to an expression"); }
+        cout << result->get_value().cast<String>() << "\n";
+    }
+    return new LiteralNode(call.get_location(), None{});
+}
+
+Node* BuiltIn::read(const CallNode& call, const Vector<ExpressionNode*>& args)
+{
+    LOG_DEBUG("Executing read");
+    if (!args.empty()) { print(call, args); }
+    String input;
+    getline(cin, input);
+    return new LiteralNode(call.get_location(), input);
+}
+
+HashMap<String, Node* (*)(const CallNode&, const Vector<ExpressionNode*>&)> BuiltIn::functions{
+    {"print", print}, {"read", read}};
+
+} // namespace funk
diff --git a/source/parser/Parser.cc b/source/parser/Parser.cc
index ac96590834acdd778014dcea107f43d82820bc35..877c7dfb55e146e4c5949fe88e9ba44bc63d3038 100644
--- a/source/parser/Parser.cc
+++ b/source/parser/Parser.cc
@@ -268,6 +268,22 @@ Node* Parser::parse_literal()
 
 Node* Parser::parse_identifier()
 {
+    LOG_DEBUG("Parse identifier");
+    Token identifier{next()};
+
+    if (match(TokenType::L_PAR))
+    {
+        Vector<ExpressionNode*> arguments{};
+        if (!check(TokenType::R_PAR))
+        {
+            do {
+                arguments.push_back(dynamic_cast<ExpressionNode*>(parse_expression()));
+            } while (match(TokenType::COMMA));
+        }
+        if (!match(TokenType::R_PAR)) { throw SyntaxError(peek().get_location(), "Expected ')'"); }
+        return new CallNode(identifier, arguments);
+    }
+
     return nullptr;
 }
 
diff --git a/source/utils/Exception.cc b/source/utils/Exception.cc
index dff1c445cc1e073c524a672ac971aa4bf1e44a8c..954cbe57cbe8e59626c97dac065eae1a3dd5ade8 100644
--- a/source/utils/Exception.cc
+++ b/source/utils/Exception.cc
@@ -25,12 +25,9 @@ String FunkError::trace() const
     std::istringstream stream(source);
     String line;
 
-    for (int i = 1; i < location.line && std::getline(stream, line); i++) {}
+    for (int i = 1; i < location.line && getline(stream, line); i++) {}
 
-    if (std::getline(stream, line))
-    {
-        os << "    " << line << '\n' << "    " << String(location.column - 1, ' ') << "^\n";
-    }
+    if (getline(stream, line)) { os << "    " << line << '\n' << "    " << String(location.column - 1, ' ') << "^\n"; }
 
     os << what() << '\n';