From 664ba87732f9feffcbcbe10c2f41c5288f90a533 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ludwig=20Mostr=C3=B6m?= <ludmo578@student.liu.se>
Date: Thu, 27 Mar 2025 12:11:48 +0100
Subject: [PATCH] UnaryOp implemented ("!" and "-") with tests

---
 include/ast/NodeValue.h   |   4 ++
 include/ast/UnaryOpNode.h |  85 +++++++++++++++++++++++++++++++
 source/ast/NodeValue.cc   |  14 ++++++
 source/ast/UnaryOpNode.cc |  77 ++++++++++++++++++++++++++++
 tests/TestUnaryOpNode.cc  | 103 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 283 insertions(+)
 create mode 100644 include/ast/UnaryOpNode.h
 create mode 100644 source/ast/UnaryOpNode.cc
 create mode 100644 tests/TestUnaryOpNode.cc

diff --git a/include/ast/NodeValue.h b/include/ast/NodeValue.h
index 93c2f19..2904a20 100644
--- a/include/ast/NodeValue.h
+++ b/include/ast/NodeValue.h
@@ -130,4 +130,8 @@ NodeValue operator||(const NodeValue& lhs, const NodeValue& rhs);
 // Power operation
 NodeValue pow(const NodeValue& lhs, const NodeValue& rhs);
 
+// Unary operators
+NodeValue operator-(const NodeValue& val);
+NodeValue operator!(const NodeValue& val);
+
 } // namespace funk
diff --git a/include/ast/UnaryOpNode.h b/include/ast/UnaryOpNode.h
new file mode 100644
index 0000000..3255594
--- /dev/null
+++ b/include/ast/UnaryOpNode.h
@@ -0,0 +1,85 @@
+/**
+ * @file BinaryOpNode.h
+ * @brief Defines the UnaryOpNode class for representing unary operations in the Funk AST.
+ */
+#pragma once
+
+#include "ast/ExpressionNode.h"
+#include "ast/LiteralNode.h"
+
+namespace funk
+{
+
+/**
+ * @brief Enumeration of all supported unary operators in the Funk language.
+ */
+enum class UnaryOp
+{
+    NEGATE, ///< Negation operator (-)
+    NOT,    ///< Logical NOT operator (!)
+};
+
+/**
+ * @brief Converts a unary operator to a string representation.
+ * @param op The unary operator
+ * @return String representation of the operator
+ */
+String op_to_s(UnaryOp op);
+
+/**
+ * @brief Class representing a unary operation in the Funk AST.
+ *
+ * UnaryOpNode represents operations that require one operand and
+ * an operator. These include arithmetic negation and logical NOT.
+ */
+class UnaryOpNode : public ExpressionNode
+{
+public:
+    /**
+     * @brief Constructs a unary operation node with an expression and an operator.
+     * @param op The unary operator to apply
+     * @param expr The expression to apply the operator to
+     */
+    UnaryOpNode(UnaryOp op, ExpressionNode* expr);
+
+    /**
+     * @brief Virtual destructor for proper cleanup of resources.
+     */
+    ~UnaryOpNode() override;
+
+    /**
+     * @brief Evaluates the unary operation.
+     * @return A node representing the result of applying the operator to the operand
+     */
+    Node* evaluate() const override;
+
+    /**
+     * @brief Converts the unary operation to a string representation.
+     * @return String representation of the unary operation
+     */
+    String to_s() const override;
+
+    /**
+     * @brief Gets the value that this unary operation evaluates to.
+     * @return The evaluated value of this unary operation
+     */
+    NodeValue get_value() const override;
+
+    /**
+     * @brief Gets the unary operator used in this operation.
+     * @return The unary operator
+     */
+    UnaryOp get_op() const;
+
+    /**
+     * @brief Gets the expression that the operator is applied to.
+     * @return Pointer to the expression
+     */
+    ExpressionNode* get_expr() const;
+
+private:
+    UnaryOp op;           ///< The unary operator
+    ExpressionNode* expr; ///< The expression to apply the operator to
+};
+
+} // namespace funk
\ No newline at end of file
diff --git a/source/ast/NodeValue.cc b/source/ast/NodeValue.cc
index ecec453..8206672 100644
--- a/source/ast/NodeValue.cc
+++ b/source/ast/NodeValue.cc
@@ -216,6 +216,20 @@ NodeValue pow(const NodeValue& lhs, const NodeValue& rhs)
     else { return std::pow(lhs.cast<double>(), rhs.cast<double>()); }
 }
 
+NodeValue operator-(const NodeValue& val)
+{
+    if (!val.is_numeric()) { throw TypeError("Cannot negate non-numeric value"); }
+
+    if (val.is_a<int>()) { return -val.get<int>(); }
+    return -val.cast<double>();
+}
+
+NodeValue operator!(const NodeValue& val)
+{
+    if (!val.is_a<bool>()) { throw TypeError("Cannot apply logical NOT to non-boolean value"); }
+    return !val.get<bool>();
+}
+
 template bool NodeValue::is_a<int>() const;
 template bool NodeValue::is_a<double>() const;
 template bool NodeValue::is_a<bool>() const;
diff --git a/source/ast/UnaryOpNode.cc b/source/ast/UnaryOpNode.cc
new file mode 100644
index 0000000..830c959
--- /dev/null
+++ b/source/ast/UnaryOpNode.cc
@@ -0,0 +1,77 @@
+#include "ast/UnaryOpNode.h"
+
+namespace funk
+{
+
+UnaryOpNode::UnaryOpNode(UnaryOp op, ExpressionNode* expr) :
+    ExpressionNode(expr->get_location()), op(op), expr(expr) {
+
+    };
+
+UnaryOpNode::~UnaryOpNode()
+{
+    delete expr;
+}
+
+Node* UnaryOpNode::evaluate() const
+{
+    if (cached_eval) { return cached_eval; }
+
+    NodeValue expr_value{expr->get_value()};
+    NodeValue result{};
+
+    try
+    {
+        switch (op)
+        {
+        case UnaryOp::NEGATE: result = -expr_value; break;
+        case UnaryOp::NOT: result = !expr_value; break;
+        default: throw RuntimeError(location, "Invalid unary operator");
+        }
+    }
+    catch (const TypeError& e)
+    {
+        throw TypeError(location, e.what());
+    }
+    catch (const RuntimeError& e)
+    {
+        throw RuntimeError(location, e.what());
+    }
+
+    return cached_eval = new LiteralNode(location, result);
+}
+
+String UnaryOpNode::to_s() const
+{
+    return "(" + op_to_s(op) + expr->to_s() + ")";
+}
+
+NodeValue UnaryOpNode::get_value() const
+{
+    ExpressionNode* result{dynamic_cast<ExpressionNode*>(evaluate())};
+    if (!result) { throw RuntimeError(location, "Unary operation did not evaluate to an expression"); }
+
+    return result->get_value();
+}
+
+UnaryOp UnaryOpNode::get_op() const
+{
+    return op;
+}
+
+ExpressionNode* UnaryOpNode::get_expr() const
+{
+    return expr;
+}
+
+String op_to_s(UnaryOp op)
+{
+    switch (op)
+    {
+    case UnaryOp::NEGATE: return "-";
+    case UnaryOp::NOT: return "!";
+    default: return "UNKNOWN";
+    }
+}
+
+}; // namespace funk
\ No newline at end of file
diff --git a/tests/TestUnaryOpNode.cc b/tests/TestUnaryOpNode.cc
new file mode 100644
index 0000000..2be026b
--- /dev/null
+++ b/tests/TestUnaryOpNode.cc
@@ -0,0 +1,103 @@
+#include "ast/LiteralNode.h"
+#include "ast/UnaryOpNode.h"
+#include "utils/Common.h"
+#include <gtest/gtest.h>
+
+using namespace funk;
+
+class TestUnaryOpNode : public ::testing::Test
+{
+protected:
+    void SetUp() override
+    {
+        // Setup common test objects
+        loc = SourceLocation{"test.funk", 0, 0};
+    }
+
+    void TearDown() override
+    {
+        // Cleanup code if needed
+    }
+
+    SourceLocation loc{"test.funk", 0, 0};
+};
+
+TEST_F(TestUnaryOpNode, Construction)
+{
+    LiteralNode* intVal = new LiteralNode(loc, 5);
+    LiteralNode* boolVal = new LiteralNode(loc, false);
+
+    UnaryOpNode negateNode(UnaryOp::NEGATE, intVal);
+    UnaryOpNode notNode(UnaryOp::NOT, boolVal);
+
+    ASSERT_EQ(negateNode.get_op(), UnaryOp::NEGATE);
+    ASSERT_EQ(negateNode.get_expr(), intVal);
+
+    ASSERT_EQ(notNode.get_op(), UnaryOp::NOT);
+    ASSERT_EQ(notNode.get_expr(), boolVal);
+}
+
+TEST_F(TestUnaryOpNode, NegateEvaluation)
+{
+    LiteralNode* val1 = new LiteralNode(loc, 5);
+    LiteralNode* val2 = new LiteralNode(loc, 3.14);
+
+    UnaryOpNode negateInt(UnaryOp::NEGATE, val1);
+    UnaryOpNode negateDouble(UnaryOp::NEGATE, val2);
+
+    ASSERT_EQ(negateInt.get_value().get<int>(), -5);
+    ASSERT_DOUBLE_EQ(negateDouble.get_value().get<double>(), -3.14);
+}
+
+TEST_F(TestUnaryOpNode, NotEvaluation)
+{
+    LiteralNode* val1 = new LiteralNode(loc, true);
+    LiteralNode* val2 = new LiteralNode(loc, false);
+
+    UnaryOpNode notTrue(UnaryOp::NOT, val1);
+    UnaryOpNode notFalse(UnaryOp::NOT, val2);
+
+    ASSERT_FALSE(notTrue.get_value().get<bool>());
+    ASSERT_TRUE(notFalse.get_value().get<bool>());
+}
+
+TEST_F(TestUnaryOpNode, ErrorCases)
+{
+    // Negate string value
+    {
+        LiteralNode* val = new LiteralNode(loc, String("test"));
+        UnaryOpNode node(UnaryOp::NEGATE, val);
+
+        ASSERT_THROW(node.get_value(), TypeError);
+    }
+
+    // Negate boolean value, apparently this works in c++ but it's ugly
+    {
+        LiteralNode* val = new LiteralNode(loc, true);
+        UnaryOpNode node(UnaryOp::NEGATE, val);
+
+        ASSERT_THROW(node.get_value(), TypeError);
+    }
+
+    // Logical NOT on non-boolean value
+    {
+        LiteralNode* val = new LiteralNode(loc, 5);
+        UnaryOpNode node(UnaryOp::NOT, val);
+
+        ASSERT_THROW(node.get_value(), TypeError);
+    }
+}
+
+TEST_F(TestUnaryOpNode, StringRepresentation)
+{
+    LiteralNode* val = new LiteralNode(loc, 5);
+    UnaryOpNode node(UnaryOp::NEGATE, val);
+
+    ASSERT_EQ(node.to_s(), "(-5)");
+}
+
+int main(int argc, char** argv)
+{
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
-- 
GitLab