From 07479d6dcc3d99c93fa9c40264cf1ef5a866e04d Mon Sep 17 00:00:00 2001 From: Tomasz Okon Date: Mon, 25 Aug 2025 19:09:24 +0000 Subject: [PATCH] Complex semantic analyzer, its tests and main function --- include/minic/SemanticAnalyzer.hpp | 98 +++++++++----- src/Parser.cpp | 2 + src/SemanticAnalyzer.cpp | 201 ++++++++++++++++++++++++---- src/main.cpp | 51 ++++++- tests/TestSemanticAnalyzer.cpp | 206 ++++++++++++++++++++++++++--- 5 files changed, 477 insertions(+), 81 deletions(-) diff --git a/include/minic/SemanticAnalyzer.hpp b/include/minic/SemanticAnalyzer.hpp index 23cfb76..ecd7e3c 100644 --- a/include/minic/SemanticAnalyzer.hpp +++ b/include/minic/SemanticAnalyzer.hpp @@ -1,11 +1,12 @@ #ifndef MINIC_SEMANTIC_ANALYZER_HPP #define MINIC_SEMANTIC_ANALYZER_HPP -#include "AST.hpp" #include "ASTVisitor.hpp" -#include +#include "minic/AST.hpp" +#include #include #include +#include /** * @namespace minic @@ -15,6 +16,19 @@ namespace minic { +/** + * @class SemanticError + * @brief Custom exception for semantic errors, allowing for better error reporting. + */ +class SemanticError : public std::runtime_error +{ +public: + explicit SemanticError(const std::string& message) + : std::runtime_error(message) + { + } +}; + /** * @class SemanticAnalyzer * @brief Performs semantic analysis by traversing the AST and validating program correctness. @@ -46,7 +60,7 @@ class SemanticAnalyzer : public ASTVisitor * * Cleans up any analyzer resources. Override is provided to satisfy polymorphic base class behavior. */ - ~SemanticAnalyzer(); + ~SemanticAnalyzer() override; /** * @brief Visits the Program AST node and performs top-level semantic checks. @@ -88,38 +102,56 @@ class SemanticAnalyzer : public ASTVisitor void visit(const Expr& expr) override; private: + using SymbolTable = std::unordered_map; ///< Maps variable names to their TokenType. + + std::stack scopes_; ///< Stack of symbol tables for nested scopes. + std::unordered_map functions_; ///< Global function table (name to return type). + + TokenType current_function_type_ = TokenType::KEYWORD_VOID; ///< Track current function's return type. + /** - * @struct SymbolTable - * @brief Simple mapping of variable names to their token/type information for the current scope. - * - * This table supports basic operations required by the analyzer: - * - Checking whether an identifier is declared. - * - Retrieving the type associated with a declared identifier. - * - * Note: get_type throws std::runtime_error if a lookup fails; callers should handle or translate - * such errors into user-facing diagnostics as appropriate. + * @brief Pushes a new scope onto the stack. */ - struct SymbolTable - { - std::map variables; ///< Maps variable names to their TokenType. - - /** - * @brief Determines whether a variable name is present in the table. - * @param name The variable identifier to check. - * @return True if declared in this table, false otherwise. - */ - bool is_declared(const std::string& name) const; - - /** - * @brief Retrieves the type associated with a variable name. - * @param name The variable identifier to look up. - * @return The TokenType associated with name. - * @throws std::runtime_error if the variable is not found. - */ - TokenType get_type(const std::string& name) const; - }; - - SymbolTable current_scope_; ///< The symbol table representing the current (active) scope. + void push_scope(); + + /** + * @brief Pops the current scope from the stack. + */ + void pop_scope(); + + /** + * @brief Symbol table operations + */ + bool is_declared_in_current_scope(const std::string& name) const; + + /** + * @brief Checks if a variable is declared in any scope. + * @param name The variable name to check. + * @return True if declared, false otherwise. + */ + bool is_declared(const std::string& name) const; + + /** + * @brief Gets the type of a variable from the symbol table. + * @param name The variable name to look up. + * @return The variable's TokenType, or TokenType::UNKNOWN if not found. + */ + TokenType get_type(const std::string& name) const; + + /** + * @brief Infers the type of an expression. + * @param expr The expression to analyze. + * @return The inferred TokenType. + */ + TokenType infer_type(const Expr& expr); + + /** + * @brief Validates binary operator compatibility. + * @param op The binary operator token. + * @param left_type The inferred type of the left operand. + * @param right_type The inferred type of the right operand. + */ + void validate_binary_op(TokenType op, TokenType left_type, TokenType right_type); }; } // namespace minic diff --git a/src/Parser.cpp b/src/Parser.cpp index 699fe20..5a7e759 100644 --- a/src/Parser.cpp +++ b/src/Parser.cpp @@ -207,6 +207,8 @@ std::unique_ptr Parser::parse_var_decl_statement() std::vector> Parser::parse_block() { std::vector> statements; + while (check(TokenType::NEWLINE)) + advance(); // Skip newlines consume(TokenType::LBRACE, "Expected '{'"); while (check(TokenType::NEWLINE)) advance(); // Skip newlines diff --git a/src/SemanticAnalyzer.cpp b/src/SemanticAnalyzer.cpp index 369f88a..df04113 100644 --- a/src/SemanticAnalyzer.cpp +++ b/src/SemanticAnalyzer.cpp @@ -2,31 +2,32 @@ namespace minic { -SemanticAnalyzer::SemanticAnalyzer() = default; -SemanticAnalyzer::~SemanticAnalyzer() = default; - -bool SemanticAnalyzer::SymbolTable::is_declared(const std::string& name) const +SemanticAnalyzer::SemanticAnalyzer() { - return variables.find(name) != variables.end(); + push_scope(); // Global scope } -TokenType SemanticAnalyzer::SymbolTable::get_type(const std::string& name) const +SemanticAnalyzer::~SemanticAnalyzer() = default; + +void SemanticAnalyzer::visit(const Program& program) { - auto it = variables.find(name); - if (it == variables.end()) + // Check for function redefinitions + for (const auto& func : program.functions) { - throw std::runtime_error("SymbolTable: '" + name + "' not found"); + if (functions_.find(func->name) != functions_.end()) + { + throw SemanticError("Function '" + func->name + "' redefined"); + } + functions_[func->name] = func->return_type; } - return it->second; -} -void SemanticAnalyzer::visit(const Program& program) -{ for (const auto& func : program.functions) { - current_scope_ = SymbolTable(); + current_function_type_ = func->return_type; + push_scope(); // New scope for each function visit(*func); + pop_scope(); } } @@ -35,11 +36,11 @@ void SemanticAnalyzer::visit(const Function& function) // Parameter declarations for (const auto& param : function.parameters) { - if (current_scope_.is_declared(param.name)) + if (is_declared_in_current_scope(param.name)) { - throw std::runtime_error("Parameter '" + param.name + "' redeclared"); + throw SemanticError("Parameter '" + param.name + "' redeclared"); } - current_scope_.variables[param.name] = param.type; + scopes_.top()[param.name] = param.type; } // Function body @@ -53,54 +54,97 @@ void SemanticAnalyzer::visit(const Stmt& stmt) { if (auto* decl = dynamic_cast(&stmt)) { - if (current_scope_.is_declared(decl->name)) + if (is_declared_in_current_scope(decl->name)) + { + throw SemanticError("Variable '" + decl->name + "' redeclared in current scope"); + } + if (decl->type == TokenType::KEYWORD_VOID) { - throw std::runtime_error("Variable '" + decl->name + "' redeclared"); + throw SemanticError("Cannot declare variable '" + decl->name + "' as void"); } - current_scope_.variables[decl->name] = decl->type; + scopes_.top()[decl->name] = decl->type; if (decl->initializer) { visit(*decl->initializer); + TokenType init_type = infer_type(*decl->initializer); + if (init_type != decl->type) + { + throw SemanticError("Type mismatch in declaration of '" + decl->name + "': expected " + std::to_string(static_cast(decl->type)) + ", got " + std::to_string(static_cast(init_type))); + } } } else if (auto* assign = dynamic_cast(&stmt)) { - if (!current_scope_.is_declared(assign->name)) + TokenType var_type = get_type(assign->name); + if (var_type == TokenType::KEYWORD_VOID) { - throw std::runtime_error("Variable '" + assign->name + "' not declared"); + throw SemanticError("Cannot assign to void variable '" + assign->name + "'"); } visit(*assign->value); + TokenType value_type = infer_type(*assign->value); + if (var_type != value_type) + { + throw SemanticError("Type mismatch in assignment to '" + assign->name + "': expected " + std::to_string(static_cast(var_type)) + ", got " + std::to_string(static_cast(value_type))); + } } else if (auto* ret = dynamic_cast(&stmt)) { if (ret->value) { visit(*ret->value); + TokenType ret_type = infer_type(*ret->value); + if (ret_type != current_function_type_) + { + throw SemanticError("Return type mismatch: expected " + std::to_string(static_cast(current_function_type_)) + ", got " + std::to_string(static_cast(ret_type))); + } + } + else + { + if (current_function_type_ != TokenType::KEYWORD_VOID) + { + throw SemanticError("Non-void function must return a value"); + } } } else if (auto* if_stmt = dynamic_cast(&stmt)) { visit(*if_stmt->condition); + TokenType cond_type = infer_type(*if_stmt->condition); + if (cond_type != TokenType::KEYWORD_INT) + { + throw SemanticError("If condition must be int type, got " + std::to_string(static_cast(cond_type))); + } + push_scope(); for (const auto& s : if_stmt->then_branch) { visit(*s); } + pop_scope(); + push_scope(); for (const auto& s : if_stmt->else_branch) { visit(*s); } + pop_scope(); } else if (auto* while_stmt = dynamic_cast(&stmt)) { visit(*while_stmt->condition); + TokenType cond_type = infer_type(*while_stmt->condition); + if (cond_type != TokenType::KEYWORD_INT) + { + throw SemanticError("While condition must be int type, got " + std::to_string(static_cast(cond_type))); + } + push_scope(); for (const auto& s : while_stmt->body) { visit(*s); } + pop_scope(); } else { - throw std::runtime_error("Unknown statement type"); + throw SemanticError("Unknown statement type"); } } @@ -108,15 +152,15 @@ void SemanticAnalyzer::visit(const Expr& expr) { if (auto* id = dynamic_cast(&expr)) { - if (!current_scope_.is_declared(id->name)) - { - throw std::runtime_error("Variable '" + id->name + "' not declared"); - } + get_type(id->name); // Throws if undeclared } else if (auto* bin = dynamic_cast(&expr)) { visit(*bin->left); visit(*bin->right); + TokenType left_type = infer_type(*bin->left); + TokenType right_type = infer_type(*bin->right); + validate_binary_op(bin->op, left_type, right_type); } else if (dynamic_cast(&expr) || dynamic_cast(&expr)) { @@ -124,8 +168,107 @@ void SemanticAnalyzer::visit(const Expr& expr) } else { - throw std::runtime_error("Unknown expression type"); + throw SemanticError("Unknown expression type"); + } +} + +void SemanticAnalyzer::push_scope() +{ + scopes_.push(SymbolTable()); +} + +void SemanticAnalyzer::pop_scope() +{ + if (scopes_.empty()) + { + throw SemanticError("Scope stack underflow"); + } + scopes_.pop(); +} + +bool SemanticAnalyzer::is_declared_in_current_scope(const std::string& name) const +{ + if (scopes_.empty()) + return false; + return scopes_.top().find(name) != scopes_.top().end(); +} + +bool SemanticAnalyzer::is_declared(const std::string& name) const +{ + std::stack copy = scopes_; + while (!copy.empty()) + { + const auto& table = copy.top(); + if (table.find(name) != table.end()) + return true; + copy.pop(); + } + return false; +} + +TokenType SemanticAnalyzer::get_type(const std::string& name) const +{ + std::stack copy = scopes_; + while (!copy.empty()) + { + const auto& table = copy.top(); + auto var_it = table.find(name); + if (var_it != table.end()) + return var_it->second; + copy.pop(); + } + throw SemanticError("Variable '" + name + "' not declared"); +} + +TokenType SemanticAnalyzer::infer_type(const Expr& expr) +{ + if (dynamic_cast(&expr)) + { + return TokenType::KEYWORD_INT; + } + else if (dynamic_cast(&expr)) + { + return TokenType::KEYWORD_STR; + } + else if (auto* id = dynamic_cast(&expr)) + { + return get_type(id->name); + } + else if (auto* bin = dynamic_cast(&expr)) + { + TokenType left_type = infer_type(*bin->left); + TokenType right_type = infer_type(*bin->right); + if (left_type == TokenType::KEYWORD_INT && right_type == TokenType::KEYWORD_INT) + { + return TokenType::KEYWORD_INT; + } + else + { + throw SemanticError("Type inference failed for binary expression"); + } + } + else + { + throw SemanticError("Cannot infer type for unknown expression"); + } +} + +void SemanticAnalyzer::validate_binary_op(TokenType op, TokenType left_type, TokenType right_type) +{ + bool is_arithmetic = (op == TokenType::OP_PLUS || op == TokenType::OP_MINUS || op == TokenType::OP_MULTIPLY || op == TokenType::OP_DIVIDE); + bool is_comparison = (op == TokenType::OP_EQUAL || op == TokenType::OP_NOT_EQUAL || op == TokenType::OP_LESS || op == TokenType::OP_LESS_EQ || op == TokenType::OP_GREATER || op == TokenType::OP_GREATER_EQ); + + if (is_arithmetic || is_comparison) + { + if (left_type != TokenType::KEYWORD_INT || right_type != TokenType::KEYWORD_INT) + { + throw SemanticError("Operands for operator must be int"); + } + } + else + { + throw SemanticError("Unsupported binary operator"); } } -} // namespace minic +} // namespace minic \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c617002..baeba8c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,6 @@ #include "minic/Lexer.hpp" +#include "minic/Parser.hpp" +#include "minic/SemanticAnalyzer.hpp" #include #include @@ -6,9 +8,56 @@ int main(int argc, char** argv) { if (argc < 2) { - std::cerr << "Usage: cminusminus \n"; + std::cerr << "Usage: minic \n"; return 1; } + + std::ifstream input_file(argv[1]); + if (!input_file) + { + std::cerr << "Error: Could not open input file.\n"; + return 1; + } + std::cout << "Compiling: " << argv[1] << "\n"; + + std::string source((std::istreambuf_iterator(input_file)), + std::istreambuf_iterator()); + + std::vector tokens; + try + { + minic::Lexer lexer(source); + tokens = lexer.Lex(); + } + catch (const std::exception& e) + { + std::cerr << "Error while lexing: " << e.what() << "\n"; + return 1; + } + + std::unique_ptr program; + try + { + minic::Parser parser(tokens); + program = parser.parse(); + } + catch (const std::exception& e) + { + std::cerr << "Error while parsing: " << e.what() << "\n"; + return 1; + } + + try + { + minic::SemanticAnalyzer analyzer; + analyzer.visit(*program); + } + catch (const std::exception& e) + { + std::cerr << "Error while analyzing semantics: " << e.what() << "\n"; + return 1; + } + return 0; } \ No newline at end of file diff --git a/tests/TestSemanticAnalyzer.cpp b/tests/TestSemanticAnalyzer.cpp index 127bd0f..b729b34 100644 --- a/tests/TestSemanticAnalyzer.cpp +++ b/tests/TestSemanticAnalyzer.cpp @@ -67,7 +67,7 @@ TEST_F(SemanticAnalyzerTest, UndeclaredAssign) funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, RedeclaredVariable) @@ -82,7 +82,7 @@ TEST_F(SemanticAnalyzerTest, RedeclaredVariable) funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, RedeclaredParameter) @@ -95,7 +95,7 @@ TEST_F(SemanticAnalyzerTest, RedeclaredParameter) funcs.push_back(minic::BuildFunction("func", minic::TokenType::KEYWORD_VOID, std::move(params), {})); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, ValidIfStatement) @@ -125,8 +125,7 @@ TEST_F(SemanticAnalyzerTest, ValidIfStatement) TEST_F(SemanticAnalyzerTest, UndeclaredInIfCondition) { auto cond = std::make_unique("undeclared"); - auto if_stmt = std::make_unique( - std::move(cond), + auto if_stmt = std::make_unique(std::move(cond), std::vector> {}, std::vector> {}); @@ -137,7 +136,7 @@ TEST_F(SemanticAnalyzerTest, UndeclaredInIfCondition) funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, ValidWhileLoop) @@ -168,7 +167,7 @@ TEST_F(SemanticAnalyzerTest, ValidWhileLoop) TEST_F(SemanticAnalyzerTest, UndeclaredInWhileBody) { - auto cond = std::make_unique(1); + auto cond = std::make_unique(1); // True std::vector> loop_body; loop_body.push_back(std::make_unique("undeclared", std::make_unique(0))); auto while_stmt = std::make_unique(std::move(cond), std::move(loop_body)); @@ -180,7 +179,7 @@ TEST_F(SemanticAnalyzerTest, UndeclaredInWhileBody) funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, ValidReturnLiteral) @@ -190,7 +189,7 @@ TEST_F(SemanticAnalyzerTest, ValidReturnLiteral) body.push_back(std::move(ret)); std::vector> funcs; - funcs.push_back(minic::BuildFunction("func", minic::TokenType::KEYWORD_VOID, {}, std::move(body))); + funcs.push_back(minic::BuildFunction("func", minic::TokenType::KEYWORD_STR, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); EXPECT_NO_THROW(analyzer_.visit(*program)); @@ -206,7 +205,7 @@ TEST_F(SemanticAnalyzerTest, UndeclaredInReturn) funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, ValidParameters) @@ -228,7 +227,7 @@ TEST_F(SemanticAnalyzerTest, UnknownStmtType) { }; // Mock unknown auto unknown = std::make_unique(); - EXPECT_THROW(analyzer_.visit(*unknown), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*unknown), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, UnknownExprType) @@ -237,7 +236,7 @@ TEST_F(SemanticAnalyzerTest, UnknownExprType) { }; // Mock unknown auto unknown = std::make_unique(); - EXPECT_THROW(analyzer_.visit(*unknown), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*unknown), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, FullValidProgramViaParser) @@ -261,23 +260,20 @@ TEST_F(SemanticAnalyzerTest, FullInvalidProgramViaParser) " x = 5; // Undeclared\n" "}\n"; auto program = ParseSource(source); - EXPECT_THROW(analyzer_.visit(*program), std::runtime_error); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); } TEST_F(SemanticAnalyzerTest, VoidStringDecl) { auto decl_void = std::make_unique(minic::TokenType::KEYWORD_VOID, "v"); - auto decl_str = std::make_unique(minic::TokenType::KEYWORD_STR, "s", - std::make_unique("test")); std::vector> body; body.push_back(std::move(decl_void)); - body.push_back(std::move(decl_str)); std::vector> funcs; funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); auto program = minic::BuildSimpleProgram(std::move(funcs)); - EXPECT_NO_THROW(analyzer_.visit(*program)); // Passes, as no type rules enforced yet + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); // Now throws for void var } TEST_F(SemanticAnalyzerTest, EmptyProgram) @@ -294,4 +290,178 @@ TEST_F(SemanticAnalyzerTest, MultipleFunctions) auto program = minic::BuildSimpleProgram(std::move(funcs)); EXPECT_NO_THROW(analyzer_.visit(*program)); // No cross-function checks yet -} \ No newline at end of file +} + +// New tests for added functionalities + +TEST_F(SemanticAnalyzerTest, TypeMismatchDeclInit) +{ + auto decl = std::make_unique(minic::TokenType::KEYWORD_INT, "x", + std::make_unique("invalid")); + std::vector> body; + body.push_back(std::move(decl)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, TypeMismatchAssign) +{ + auto decl = std::make_unique(minic::TokenType::KEYWORD_STR, "s", + std::make_unique("ok")); + auto assign = std::make_unique("s", std::make_unique(42)); + std::vector> body; + body.push_back(std::move(decl)); + body.push_back(std::move(assign)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, ReturnTypeMismatch) +{ + auto ret = std::make_unique(std::make_unique("mismatch")); + std::vector> body; + body.push_back(std::move(ret)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("func", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, MissingReturnInNonVoid) +{ + auto ret = std::make_unique(nullptr); // Void return + std::vector> body; + body.push_back(std::move(ret)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("func", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, BinaryOpTypeMismatch) +{ + auto decl_int = std::make_unique(minic::TokenType::KEYWORD_INT, "i", std::make_unique(1)); + auto decl_str = std::make_unique(minic::TokenType::KEYWORD_STR, "s", std::make_unique("str")); + auto bin = std::make_unique(std::make_unique("i"), + minic::TokenType::OP_PLUS, + std::make_unique("s")); + auto assign = std::make_unique("i", std::move(bin)); + std::vector> body; + body.push_back(std::move(decl_int)); + body.push_back(std::move(decl_str)); + body.push_back(std::move(assign)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, NestedScopeRedeclOK) +{ + auto outer_decl = std::make_unique(minic::TokenType::KEYWORD_INT, "x", std::make_unique(1)); + auto cond = std::make_unique(1); + std::vector> if_body; + if_body.push_back(std::make_unique(minic::TokenType::KEYWORD_INT, "x", std::make_unique(2))); // Shadow OK + if_body.push_back(std::make_unique("x", std::make_unique(3))); + auto if_stmt = std::make_unique(std::move(cond), std::move(if_body), std::vector> {}); + auto outer_assign = std::make_unique("x", std::make_unique(4)); + + std::vector> body; + body.push_back(std::move(outer_decl)); + body.push_back(std::move(if_stmt)); + body.push_back(std::move(outer_assign)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_NO_THROW(analyzer_.visit(*program)); +} + +TEST_F(SemanticAnalyzerTest, NestedScopeUndeclaredInner) +{ + auto cond = std::make_unique(1); + std::vector> if_body; + if_body.push_back(std::make_unique("inner_undeclared", std::make_unique(1))); + auto if_stmt = std::make_unique(std::move(cond), std::move(if_body), std::vector> {}); + + std::vector> body; + body.push_back(std::move(if_stmt)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, FunctionRedefinition) +{ + std::vector> funcs; + funcs.push_back(minic::BuildFunction("dup", minic::TokenType::KEYWORD_INT, {}, {})); + funcs.push_back(minic::BuildFunction("dup", minic::TokenType::KEYWORD_VOID, {}, {})); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, IfConditionTypeMismatch) +{ + auto cond = std::make_unique("not_int"); + auto if_stmt = std::make_unique(std::move(cond), std::vector> {}, std::vector> {}); + + std::vector> body; + body.push_back(std::move(if_stmt)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, WhileConditionTypeMismatch) +{ + auto cond = std::make_unique("not_int"); + auto while_stmt = std::make_unique(std::move(cond), std::vector> {}); + + std::vector> body; + body.push_back(std::move(while_stmt)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +} + +TEST_F(SemanticAnalyzerTest, UnsupportedBinaryOp) +{ + auto bin = std::make_unique(std::make_unique(1), + minic::TokenType::OP_ASSIGN, // Invalid op for binary expr + std::make_unique(2)); + auto ret = std::make_unique(std::move(bin)); + + std::vector> body; + body.push_back(std::move(ret)); + + std::vector> funcs; + funcs.push_back(minic::BuildFunction("main", minic::TokenType::KEYWORD_INT, {}, std::move(body))); + + auto program = minic::BuildSimpleProgram(std::move(funcs)); + EXPECT_THROW(analyzer_.visit(*program), minic::SemanticError); +}