diff --git a/QUICKSTART.md b/QUICKSTART.md index 3b2af92..52c3093 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -14,9 +14,9 @@ make clean && make cd ../../../ ``` -### 2. Запуск интеграционного теста +### 2. Запуск тестов в Docker ```bash -./integration_test.sh +bash ./docker_test.sh ``` ### 3. Ручное тестирование diff --git a/README.md b/README.md index 74e577f..4ddbbbe 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ make clean && make cd ../../../ ``` -#### Run Integration Test +#### Run Tests in Docker ```bash -# Run comprehensive integration test -./integration_test.sh +# Run the full test suite in Docker (native harness + Java tests) +bash ./docker_test.sh ``` ### 4.2 Individual Component Testing diff --git a/compiler/src/main/cpp/parser/Makefile b/compiler/src/main/cpp/parser/Makefile index 9ef3034..e84531d 100644 --- a/compiler/src/main/cpp/parser/Makefile +++ b/compiler/src/main/cpp/parser/Makefile @@ -10,6 +10,7 @@ BISON_SRC = parser.y LEX_SRC = lexer.l AST_SRC = ast.cpp SYMBOL_SRC = symbol.cpp +ANALYZER_SRC = analyzer.cpp LEXER_SRC = lexer.cpp JNI_SRC = jni_lexer.cpp @@ -24,6 +25,7 @@ BISON_OBJ = parser.tab.o LEX_OBJ = lex.yy.o AST_OBJ = ast.o SYMBOL_OBJ = symbol.o +ANALYZER_OBJ = analyzer.o LEXER_OBJ = lexer.o JNI_OBJ = jni_lexer.o @@ -38,10 +40,10 @@ JNI_INCLUDES = -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -I. all: $(JNI_H) $(TARGET) $(EXECUTABLE) -$(EXECUTABLE): $(BISON_OBJ) $(LEX_OBJ) $(AST_OBJ) $(SYMBOL_OBJ) $(LEXER_OBJ) $(JNI_OBJ) +$(EXECUTABLE): $(BISON_OBJ) $(LEX_OBJ) $(AST_OBJ) $(SYMBOL_OBJ) $(ANALYZER_OBJ) $(LEXER_OBJ) $(JNI_OBJ) $(CXX) $(CXXFLAGS) -fPIC -o $@ $^ $(JNI_INCLUDES) -$(TARGET): $(BISON_OBJ) $(LEX_OBJ) $(AST_OBJ) $(SYMBOL_OBJ) $(LEXER_OBJ) $(JNI_OBJ) +$(TARGET): $(BISON_OBJ) $(LEX_OBJ) $(AST_OBJ) $(SYMBOL_OBJ) $(ANALYZER_OBJ) $(LEXER_OBJ) $(JNI_OBJ) $(CXX) $(CXXFLAGS) -shared -fPIC -Wl,--no-as-needed -o $@ $^ $(JNI_INCLUDES) $(BISON_C) $(BISON_H): $(BISON_SRC) diff --git a/compiler/src/main/cpp/parser/analyzer.cpp b/compiler/src/main/cpp/parser/analyzer.cpp new file mode 100644 index 0000000..b3e135d --- /dev/null +++ b/compiler/src/main/cpp/parser/analyzer.cpp @@ -0,0 +1,573 @@ +#include "analyzer.h" +#include + +extern SymbolTable* symbolTable; + +static bool isIntegerType(TypeNode* type) { + if (auto* prim = dynamic_cast(type)) { + return prim->kind == TypeKind::INTEGER; + } + return false; +} + +static bool asBoolLiteral(ExpressionNode* expr, bool& valueOut) { + if (auto* b = dynamic_cast(expr)) { valueOut = b->value; return true; } + return false; +} + +Analyzer::Result Analyzer::analyze(ProgramNode* root) { + result = Result{}; + if (!root) { + result.errors.push_back("Analyzer: null program root"); + return result; + } + runChecks(root); + if (enableOpts && result.errors.empty()) { + runOptimizations(root); + struct { + void collectGlobals(ProgramNode* prog, std::unordered_set& globals) { + for (auto* d : prog->declarations) { + if (auto* vd = dynamic_cast(d)) { + globals.insert(vd->name); + } + } + } + void checkExprForUndefined(ExpressionNode* expr, const std::unordered_set& globals, Analyzer::Result& res) { + if (!expr) return; + if (auto* va = dynamic_cast(expr)) { + if (globals.find(va->name) == globals.end()) { + res.errors.push_back(std::string("Undefined variable '") + va->name + "'"); + } + return; + } + if (auto* bin = dynamic_cast(expr)) { + checkExprForUndefined(bin->left, globals, res); + checkExprForUndefined(bin->right, globals, res); + } else if (auto* un = dynamic_cast(expr)) { + checkExprForUndefined(un->operand, globals, res); + } else if (auto* arr = dynamic_cast(expr)) { + checkExprForUndefined(arr->array, globals, res); + checkExprForUndefined(arr->index, globals, res); + } else if (auto* fld = dynamic_cast(expr)) { + checkExprForUndefined(fld->record, globals, res); + } else if (auto* call = dynamic_cast(expr)) { + if (auto* al = dynamic_cast(call->arguments)) { + for (auto* a : al->arguments) checkExprForUndefined(a, globals, res); + } + } + } + void validateTopLevelReferences(ProgramNode* prog, const std::unordered_set& globals, Analyzer::Result& res) { + for (auto* s : prog->statements) { + if (auto* asg = dynamic_cast(s)) { + if (auto* va = dynamic_cast(asg->target)) { + if (globals.find(va->name) == globals.end()) { + res.errors.push_back(std::string("Undefined variable '") + va->name + "'"); + } + } + checkExprForUndefined(asg->value, globals, res); + } else if (auto* pr = dynamic_cast(s)) { + if (auto* el = dynamic_cast(pr->expressions)) { + for (auto* e : el->expressions) checkExprForUndefined(e, globals, res); + } + } + } + } + } post; + std::unordered_set globals; + post.collectGlobals(root, globals); + post.validateTopLevelReferences(root, globals, result); + } + return result; +} + +void Analyzer::runChecks(ProgramNode* root) { + for (auto* d : root->declarations) checkNode(d); + for (auto* s : root->statements) checkStatement(s); +} + +void Analyzer::checkNode(ASTNode* node) { + if (!node) return; + if (auto* vd = dynamic_cast(node)) { + if (vd->initializer) { + checkExpression(vd->initializer); + if (vd->type) { + TypeNode* initT = inferType(vd->initializer); + if (!typesCompatible(initT, vd->type)) { + result.errors.push_back("Type mismatch in variable initializer: " + vd->name); + } + } + } + } else if (auto* td = dynamic_cast(node)) { + if (auto* rec = dynamic_cast(td->type)) { + if (auto* body = dynamic_cast(rec->body)) { + std::unordered_set seen; + for (auto* f : body->fields) { + if (auto* v = dynamic_cast(f)) { + if (!seen.insert(v->name).second) { + result.errors.push_back("Duplicate field '" + v->name + "' in type '" + td->name + "'"); + } + } + } + } + } + } else if (auto* rd = dynamic_cast(node)) { + if (auto* body = dynamic_cast(rd->body)) { + if (auto* expr = dynamic_cast(body->body)) { + auto* header = static_cast(rd->header); + if (header && header->returnType) { + auto* retT = inferType(expr); + if (!typesCompatible(retT, header->returnType)) { + result.errors.push_back("Routine '" + header->name + "' return type mismatch"); + } + } + } else if (auto* b = dynamic_cast(body->body)) { + for (auto* d : b->declarations) checkNode(d); + for (auto* s : b->statements) checkStatement(s); + } + } + } +} + +void Analyzer::checkStatement(StatementNode* stmt) { + if (!stmt) return; + if (auto* asg = dynamic_cast(stmt)) { + checkExpression(asg->target); + checkExpression(asg->value); + if (!checkAssignmentTypes(asg->target, asg->value)) { + result.errors.push_back("Type mismatch in assignment"); + } + } else if (auto* wh = dynamic_cast(stmt)) { + checkExpression(wh->condition); + if (!isBooleanType(wh->condition)) { + result.errors.push_back("While condition must be boolean"); + } + if (auto* b = dynamic_cast(wh->body)) { + for (auto* d : b->declarations) checkNode(d); + for (auto* s : b->statements) checkStatement(s); + } + } else if (auto* fr = dynamic_cast(stmt)) { + if (auto* r = dynamic_cast(fr->range)) { + if (r->end) { + auto* t1 = inferType(r->start); + auto* t2 = inferType(r->end); + if (!isIntegerType(t1) || !isIntegerType(t2)) { + result.errors.push_back("For range bounds must be integers"); + } + } else { + auto* t = inferType(r->start); + if (!dynamic_cast(t)) { + result.errors.push_back("For-in expects array or numeric range"); + } + } + } + if (auto* b = dynamic_cast(fr->body)) { + for (auto* d : b->declarations) checkNode(d); + for (auto* s : b->statements) checkStatement(s); + } + } else if (auto* iff = dynamic_cast(stmt)) { + checkExpression(iff->condition); + if (!isBooleanType(iff->condition)) { + result.errors.push_back("If condition must be boolean"); + } + if (auto* tb = dynamic_cast(iff->thenBody)) { + for (auto* d : tb->declarations) checkNode(d); + for (auto* s : tb->statements) checkStatement(s); + } + if (auto* eb = dynamic_cast(iff->elseBody)) { + for (auto* d : eb->declarations) checkNode(d); + for (auto* s : eb->statements) checkStatement(s); + } + } else if (auto* pr = dynamic_cast(stmt)) { + if (auto* el = dynamic_cast(pr->expressions)) { + for (auto* e : el->expressions) checkExpression(e); + } + } else if (auto* callStmt = dynamic_cast(stmt)) { + checkRoutineCallTypes(callStmt->name, callStmt->arguments); + } +} + +void Analyzer::checkExpression(ExpressionNode* expr) { + if (!expr) return; + if (auto* bin = dynamic_cast(expr)) { + checkExpression(bin->left); + checkExpression(bin->right); + } else if (auto* un = dynamic_cast(expr)) { + checkExpression(un->operand); + } else if (auto* arr = dynamic_cast(expr)) { + checkExpression(arr->array); + checkExpression(arr->index); + checkArrayIndex(arr); + } else if (auto* field = dynamic_cast(expr)) { + checkExpression(field->record); + checkRecordFieldAccess(field); + } else if (auto* call = dynamic_cast(expr)) { + checkRoutineCallTypes(call->name, call->arguments); + } +} + +void Analyzer::checkRecordFieldAccess(FieldAccessNode* field) { + auto* recType = field->record ? field->record->type : nullptr; + if (!recType) recType = inferType(field->record); + auto* rt = dynamic_cast(recType); + if (!rt) { + result.errors.push_back("Field access on non-record type"); + return; + } + auto* body = dynamic_cast(rt->body); + if (!body) return; + bool found = false; + for (auto* f : body->fields) { + if (auto* vd = dynamic_cast(f)) { + if (vd->name == field->fieldName) { found = true; break; } + } + } + if (!found) { + result.errors.push_back("Unknown field '" + field->fieldName + "' in record"); + } +} + +void Analyzer::checkArrayIndex(ArrayAccessNode* arrAcc) { + auto* idxType = inferType(arrAcc->index); + if (!isIntegerType(idxType)) { + result.errors.push_back("Array index must be integer"); + } + auto* arrType = arrAcc->array ? arrAcc->array->type : nullptr; + if (!arrType) arrType = inferType(arrAcc->array); + auto* at = dynamic_cast(arrType); + if (at && at->size) { + if (auto* lit = dynamic_cast(arrAcc->index)) { + if (auto* szLit = dynamic_cast(at->size)) { + int idx = lit->value; + int sz = szLit->value; + if (!(idx >= 1 && idx <= sz)) { + result.warnings.push_back("Array index " + std::to_string(idx) + " out of bounds [1.." + std::to_string(sz) + "] (static)"); + } + } + } + } +} + +void Analyzer::checkRoutineCallTypes(const std::string& name, ASTNode* arguments) { + RoutineInfo* routine = symbolTable ? symbolTable->lookupRoutine(name) : nullptr; + if (!routine) { + result.errors.push_back("Undefined routine '" + name + "'"); + return; + } + std::vector args; + if (auto* argList = dynamic_cast(arguments)) { + args = argList->arguments; + } else if (auto* exprList = dynamic_cast(arguments)) { + args = exprList->expressions; + } + if (args.size() != routine->paramTypes.size()) { + result.errors.push_back("Argument count mismatch in call to '" + name + "'"); + return; + } + for (size_t i = 0; i < args.size(); ++i) { + auto* argT = inferType(args[i]); + auto* paramT = routine->paramTypes[i]; + if (!typesCompatible(argT, paramT)) { + result.errors.push_back("Argument type mismatch in call to '" + name + "' at position " + std::to_string(i+1)); + } + } +} + +void Analyzer::runOptimizations(ProgramNode* root) { + for (auto* decl : root->declarations) { + if (auto* vd = dynamic_cast(decl)) { + if (vd->initializer) { + auto* folded = foldExpression(vd->initializer); + if (folded != vd->initializer) { result.optimizationsApplied++; vd->initializer = folded; } + } + } else if (auto* rd = dynamic_cast(decl)) { + if (auto* body = dynamic_cast(rd->body)) { + if (auto* expr = dynamic_cast(body->body)) { + auto* folded = foldExpression(expr); + if (folded != expr) { result.optimizationsApplied++; body->body = folded; } + } else if (auto* b = dynamic_cast(body->body)) { + simplifyInBody(b); + } + } + } + } + simplifyInProgram(root); + removeUnusedDeclarations(root); +} + +ExpressionNode* Analyzer::foldExpression(ExpressionNode* expr) { + if (!expr) return expr; + if (auto* bin = dynamic_cast(expr)) { + bin->left = foldExpression(bin->left); + bin->right = foldExpression(bin->right); + auto* L_i = dynamic_cast(bin->left); + auto* R_i = dynamic_cast(bin->right); + auto* L_r = dynamic_cast(bin->left); + auto* R_r = dynamic_cast(bin->right); + auto* L_b = dynamic_cast(bin->left); + auto* R_b = dynamic_cast(bin->right); + if ((L_i && R_i) || (L_r && R_r) || (L_i && R_r) || (L_r && R_i)) { + bool useReal = (L_r || R_r); + double lv = L_r ? L_r->value : (L_i ? (double)L_i->value : 0.0); + double rv = R_r ? R_r->value : (R_i ? (double)R_i->value : 0.0); + switch (bin->op) { + case OpKind::PLUS: return useReal ? (ExpressionNode*)new RealLiteralNode(lv + rv) : (ExpressionNode*)new IntegerLiteralNode((int)(lv + rv)); + case OpKind::MINUS: return useReal ? (ExpressionNode*)new RealLiteralNode(lv - rv) : (ExpressionNode*)new IntegerLiteralNode((int)(lv - rv)); + case OpKind::MUL: return useReal ? (ExpressionNode*)new RealLiteralNode(lv * rv) : (ExpressionNode*)new IntegerLiteralNode((int)(lv * rv)); + case OpKind::DIV: return (ExpressionNode*)new RealLiteralNode(lv / rv); + case OpKind::MOD: if (!useReal) return (ExpressionNode*)new IntegerLiteralNode((int)lv % (int)rv); else break; + case OpKind::LT: return (ExpressionNode*)new BooleanLiteralNode(lv < rv); + case OpKind::LE: return (ExpressionNode*)new BooleanLiteralNode(lv <= rv); + case OpKind::GT: return (ExpressionNode*)new BooleanLiteralNode(lv > rv); + case OpKind::GE: return (ExpressionNode*)new BooleanLiteralNode(lv >= rv); + case OpKind::EQ: return (ExpressionNode*)new BooleanLiteralNode(lv == rv); + case OpKind::NE: return (ExpressionNode*)new BooleanLiteralNode(lv != rv); + default: break; + } + } + if (L_b && R_b) { + switch (bin->op) { + case OpKind::AND: return (ExpressionNode*)new BooleanLiteralNode(L_b->value && R_b->value); + case OpKind::OR: return (ExpressionNode*)new BooleanLiteralNode(L_b->value || R_b->value); + case OpKind::XOR: return (ExpressionNode*)new BooleanLiteralNode((bool)(L_b->value ^ R_b->value)); + default: break; + } + } + return expr; + } + if (auto* un = dynamic_cast(expr)) { + un->operand = foldExpression(un->operand); + if (auto* i = dynamic_cast(un->operand)) { + if (un->op == OpKind::UMINUS) return (ExpressionNode*)new IntegerLiteralNode(-i->value); + if (un->op == OpKind::UPLUS) return (ExpressionNode*)new IntegerLiteralNode(+i->value); + } + if (auto* r = dynamic_cast(un->operand)) { + if (un->op == OpKind::UMINUS) return (ExpressionNode*)new RealLiteralNode(-r->value); + if (un->op == OpKind::UPLUS) return (ExpressionNode*)new RealLiteralNode(+r->value); + } + if (auto* b = dynamic_cast(un->operand)) { + if (un->op == OpKind::NOT) return (ExpressionNode*)new BooleanLiteralNode(!b->value); + } + return expr; + } + if (auto* arr = dynamic_cast(expr)) { + arr->array = foldExpression(arr->array); + arr->index = foldExpression(arr->index); + return expr; + } + if (auto* fld = dynamic_cast(expr)) { + fld->record = foldExpression(fld->record); + return expr; + } + if (auto* call = dynamic_cast(expr)) { + if (auto* args = dynamic_cast(call->arguments)) { + for (auto*& a : args->arguments) a = foldExpression(a); + } + return expr; + } + return expr; +} + +void Analyzer::simplifyInBody(BodyNode* body) { + if (!body) return; + for (auto* d : body->declarations) { + if (auto* vd = dynamic_cast(d)) { + if (vd->initializer) { + auto* folded = foldExpression(vd->initializer); + if (folded != vd->initializer) { result.optimizationsApplied++; vd->initializer = folded; } + } + } + } + std::vector newStmts; + newStmts.reserve(body->statements.size()); + for (auto* s : body->statements) { + if (auto* asg = dynamic_cast(s)) { + asg->value = foldExpression(asg->value); + newStmts.push_back(asg); + continue; + } + if (auto* pr = dynamic_cast(s)) { + if (auto* el = dynamic_cast(pr->expressions)) { + for (auto*& e : el->expressions) { + auto* folded = foldExpression(e); + if (folded != e) { result.optimizationsApplied++; e = folded; } + } + } + newStmts.push_back(pr); + continue; + } + if (auto* iff = dynamic_cast(s)) { + iff->condition = foldExpression(iff->condition); + bool val; + if (asBoolLiteral(iff->condition, val)) { + BodyNode* chosen = val ? dynamic_cast(iff->thenBody) + : dynamic_cast(iff->elseBody); + if (chosen) { + simplifyInBody(chosen); + std::unordered_set existing; + for (auto* d : body->declarations) { + if (auto* vd = dynamic_cast(d)) existing.insert(vd->name); + } + for (auto* d : chosen->declarations) { + if (auto* vd = dynamic_cast(d)) { + if (existing.find(vd->name) != existing.end()) { + result.errors.push_back("Duplicate variable declaration '" + vd->name + "' in same scope"); + } else { + if (vd->initializer) { + auto* folded = foldExpression(vd->initializer); + if (folded != vd->initializer) { result.optimizationsApplied++; vd->initializer = folded; } + } + body->declarations.push_back(vd); + existing.insert(vd->name); + } + } + } + for (auto* inner : chosen->statements) newStmts.push_back(inner); + } + result.optimizationsApplied++; + continue; + } else { + if (auto* tb = dynamic_cast(iff->thenBody)) simplifyInBody(tb); + if (auto* eb = dynamic_cast(iff->elseBody)) simplifyInBody(eb); + newStmts.push_back(iff); + continue; + } + } + if (auto* wh = dynamic_cast(s)) { + wh->condition = foldExpression(wh->condition); + bool val; + if (asBoolLiteral(wh->condition, val) && !val) { result.optimizationsApplied++; continue; } + if (auto* b = dynamic_cast(wh->body)) simplifyInBody(b); + newStmts.push_back(wh); + continue; + } + newStmts.push_back(s); + } + body->statements.swap(newStmts); +} + +void Analyzer::simplifyInProgram(ProgramNode* program) { + std::vector newStmts; + newStmts.reserve(program->statements.size()); + for (auto* s : program->statements) { + if (auto* iff = dynamic_cast(s)) { + iff->condition = foldExpression(iff->condition); + bool val; + if (asBoolLiteral(iff->condition, val)) { + BodyNode* chosen = val ? dynamic_cast(iff->thenBody) + : dynamic_cast(iff->elseBody); + if (chosen) { + simplifyInBody(chosen); + std::unordered_set existing; + for (auto* d : program->declarations) { + if (auto* vd = dynamic_cast(d)) existing.insert(vd->name); + } + for (auto* d : chosen->declarations) { + if (auto* vd = dynamic_cast(d)) { + if (existing.find(vd->name) != existing.end()) { + result.errors.push_back("Duplicate variable declaration '" + vd->name + "' in same scope"); + } else { + if (vd->initializer) { + auto* folded = foldExpression(vd->initializer); + if (folded != vd->initializer) { result.optimizationsApplied++; vd->initializer = folded; } + } + program->addDeclaration(vd); + existing.insert(vd->name); + } + } + } + for (auto* inner : chosen->statements) newStmts.push_back(inner); + } + result.optimizationsApplied++; + continue; + } else { + if (auto* tb = dynamic_cast(iff->thenBody)) simplifyInBody(tb); + if (auto* eb = dynamic_cast(iff->elseBody)) simplifyInBody(eb); + } + } else if (auto* wh = dynamic_cast(s)) { + wh->condition = foldExpression(wh->condition); + bool val; + if (asBoolLiteral(wh->condition, val) && !val) { result.optimizationsApplied++; continue; } + if (auto* b = dynamic_cast(wh->body)) simplifyInBody(b); + } else if (auto* asg = dynamic_cast(s)) { + asg->value = foldExpression(asg->value); + } else if (auto* pr = dynamic_cast(s)) { + if (auto* el = dynamic_cast(pr->expressions)) { + for (auto*& e : el->expressions) { + auto* folded = foldExpression(e); + if (folded != e) { result.optimizationsApplied++; e = folded; } + } + } + } + newStmts.push_back(s); + } + program->statements.swap(newStmts); +} + +void Analyzer::collectUsedVariables(ASTNode* node, std::unordered_set& used) { + if (!node) return; + if (auto* expr = dynamic_cast(node)) { + if (auto* va = dynamic_cast(expr)) used.insert(va->name); + else if (auto* bin = dynamic_cast(expr)) { collectUsedVariables(bin->left, used); collectUsedVariables(bin->right, used);} + else if (auto* un = dynamic_cast(expr)) { collectUsedVariables(un->operand, used);} + else if (auto* arr = dynamic_cast(expr)) { collectUsedVariables(arr->array, used); collectUsedVariables(arr->index, used);} + else if (auto* fld = dynamic_cast(expr)) { collectUsedVariables(fld->record, used);} + else if (auto* call = dynamic_cast(expr)) { + if (auto* args = dynamic_cast(call->arguments)) for (auto* a : args->arguments) collectUsedVariables(a, used); + } + return; + } + if (auto* stmt = dynamic_cast(node)) { + if (auto* asg = dynamic_cast(stmt)) { collectUsedVariables(asg->target, used); collectUsedVariables(asg->value, used);} + else if (auto* wh = dynamic_cast(stmt)) { collectUsedVariables(wh->condition, used); if (auto* b = dynamic_cast(wh->body)) { for (auto* d : b->declarations) collectUsedVariables(d, used); for (auto* s : b->statements) collectUsedVariables(s, used);} } + else if (auto* fr = dynamic_cast(stmt)) { if (auto* r = dynamic_cast(fr->range)) { collectUsedVariables(r->start, used); if (r->end) collectUsedVariables(r->end, used);} if (auto* b = dynamic_cast(fr->body)) { for (auto* d : b->declarations) collectUsedVariables(d, used); for (auto* s : b->statements) collectUsedVariables(s, used);} } + else if (auto* iff = dynamic_cast(stmt)) { collectUsedVariables(iff->condition, used); if (auto* tb = dynamic_cast(iff->thenBody)) { for (auto* d : tb->declarations) collectUsedVariables(d, used); for (auto* s : tb->statements) collectUsedVariables(s, used);} if (auto* eb = dynamic_cast(iff->elseBody)) { for (auto* d : eb->declarations) collectUsedVariables(d, used); for (auto* s : eb->statements) collectUsedVariables(s, used);} } + else if (auto* pr = dynamic_cast(stmt)) { if (auto* el = dynamic_cast(pr->expressions)) for (auto* e : el->expressions) collectUsedVariables(e, used);} + else if (auto* callStmt = dynamic_cast(stmt)) { if (auto* al = dynamic_cast(callStmt->arguments)) for (auto* a : al->arguments) collectUsedVariables(a, used);} + return; + } + if (auto* body = dynamic_cast(node)) { for (auto* d : body->declarations) collectUsedVariables(d, used); for (auto* s : body->statements) collectUsedVariables(s, used); return; } + if (auto* prog = dynamic_cast(node)) { for (auto* d : prog->declarations) collectUsedVariables(d, used); for (auto* s : prog->statements) collectUsedVariables(s, used); return; } + if (auto* vd = dynamic_cast(node)) { if (vd->initializer) collectUsedVariables(vd->initializer, used); return; } + if (auto* rd = dynamic_cast(node)) { if (auto* body = dynamic_cast(rd->body)) { if (auto* expr = dynamic_cast(body->body)) collectUsedVariables(expr, used); if (auto* b = dynamic_cast(body->body)) collectUsedVariables(b, used);} return; } +} + +void Analyzer::removeUnusedDeclarations(ProgramNode* program) { + std::unordered_set used; + for (auto* s : program->statements) collectUsedVariables(s, used); + for (auto* d : program->declarations) { + if (auto* vd = dynamic_cast(d)) { if (vd->initializer) collectUsedVariables(vd->initializer, used); } + else if (auto* rd = dynamic_cast(d)) { if (auto* body = dynamic_cast(rd->body)) { if (auto* expr = dynamic_cast(body->body)) collectUsedVariables(expr, used); if (auto* b = dynamic_cast(body->body)) collectUsedVariables(b, used);} } + } + std::vector newDecls; + newDecls.reserve(program->declarations.size()); + for (auto* d : program->declarations) { + if (auto* vd = dynamic_cast(d)) { + bool isUsed = used.find(vd->name) != used.end(); + bool hasSideEffects = vd->initializer != nullptr; + if (!isUsed && !hasSideEffects) { result.optimizationsApplied++; delete vd; continue; } + } + newDecls.push_back(d); + } + program->declarations.swap(newDecls); + for (auto* d : program->declarations) { + if (auto* rd = dynamic_cast(d)) { + if (auto* body = dynamic_cast(rd->body)) { + if (auto* b = dynamic_cast(body->body)) removeUnusedDeclarationsInBody(b, used); + } + } + } +} + +void Analyzer::removeUnusedDeclarationsInBody(BodyNode* body, const std::unordered_set& used) { + std::vector newDecls; + newDecls.reserve(body->declarations.size()); + for (auto* d : body->declarations) { + if (auto* vd = dynamic_cast(d)) { + bool isUsed = used.find(vd->name) != used.end(); + bool hasSideEffects = vd->initializer != nullptr; + if (!isUsed && !hasSideEffects) { result.optimizationsApplied++; delete vd; continue; } + } + newDecls.push_back(d); + } + body->declarations.swap(newDecls); +} diff --git a/compiler/src/main/cpp/parser/analyzer.h b/compiler/src/main/cpp/parser/analyzer.h new file mode 100644 index 0000000..6d2cc1a --- /dev/null +++ b/compiler/src/main/cpp/parser/analyzer.h @@ -0,0 +1,49 @@ +#ifndef ANALYZER_H +#define ANALYZER_H + +#include +#include +#include +#include "ast.h" +#include "symbol.h" + +extern SymbolTable* symbolTable; + +class Analyzer { +public: + struct Result { + std::vector errors; + std::vector warnings; + size_t optimizationsApplied = 0; + bool success() const { return errors.empty(); } + }; + + explicit Analyzer(bool enableOptimizations = true) + : enableOpts(enableOptimizations) {} + + Result analyze(ProgramNode* root); + +private: + bool enableOpts; + Result result; + + // Checks (no AST modification) + void runChecks(ProgramNode* root); + void checkNode(ASTNode* node); + void checkExpression(ExpressionNode* expr); + void checkStatement(StatementNode* stmt); + void checkRecordFieldAccess(FieldAccessNode* field); + void checkArrayIndex(ArrayAccessNode* arrAcc); + void checkRoutineCallTypes(const std::string& name, ASTNode* arguments); + + // Optimizations (AST modification) + void runOptimizations(ProgramNode* root); + ExpressionNode* foldExpression(ExpressionNode* expr); + void simplifyInBody(BodyNode* body); + void simplifyInProgram(ProgramNode* program); + void removeUnusedDeclarations(ProgramNode* program); + void removeUnusedDeclarationsInBody(BodyNode* body, const std::unordered_set& used); + void collectUsedVariables(ASTNode* node, std::unordered_set& used); +}; + +#endif diff --git a/compiler/src/main/cpp/parser/analyzer.o b/compiler/src/main/cpp/parser/analyzer.o new file mode 100644 index 0000000..4a33536 Binary files /dev/null and b/compiler/src/main/cpp/parser/analyzer.o differ diff --git a/compiler/src/main/cpp/parser/ast.o b/compiler/src/main/cpp/parser/ast.o index 8cd101a..12b3914 100644 Binary files a/compiler/src/main/cpp/parser/ast.o and b/compiler/src/main/cpp/parser/ast.o differ diff --git a/compiler/src/main/cpp/parser/jni_lexer.o b/compiler/src/main/cpp/parser/jni_lexer.o index 6a09114..de607a6 100644 Binary files a/compiler/src/main/cpp/parser/jni_lexer.o and b/compiler/src/main/cpp/parser/jni_lexer.o differ diff --git a/compiler/src/main/cpp/parser/lex.yy.o b/compiler/src/main/cpp/parser/lex.yy.o index a7eea0e..c1a6050 100644 Binary files a/compiler/src/main/cpp/parser/lex.yy.o and b/compiler/src/main/cpp/parser/lex.yy.o differ diff --git a/compiler/src/main/cpp/parser/lexer.l b/compiler/src/main/cpp/parser/lexer.l index 5b05a54..461a50e 100644 --- a/compiler/src/main/cpp/parser/lexer.l +++ b/compiler/src/main/cpp/parser/lexer.l @@ -16,7 +16,7 @@ extern int yylineno; %% -[ \t] ; // ignore whitespace +[ \t\r]+ ; // ignore whitespace (include CR for Windows line endings) \n { yylineno++; } "var" { return TOK_VAR; } diff --git a/compiler/src/main/cpp/parser/lexer.o b/compiler/src/main/cpp/parser/lexer.o index e5ea49d..c0771af 100644 Binary files a/compiler/src/main/cpp/parser/lexer.o and b/compiler/src/main/cpp/parser/lexer.o differ diff --git a/compiler/src/main/cpp/parser/libparser.so b/compiler/src/main/cpp/parser/libparser.so index 214d3c7..145d981 100755 Binary files a/compiler/src/main/cpp/parser/libparser.so and b/compiler/src/main/cpp/parser/libparser.so differ diff --git a/compiler/src/main/cpp/parser/parser b/compiler/src/main/cpp/parser/parser index 9ff1544..6d458cc 100755 Binary files a/compiler/src/main/cpp/parser/parser and b/compiler/src/main/cpp/parser/parser differ diff --git a/compiler/src/main/cpp/parser/parser.tab.o b/compiler/src/main/cpp/parser/parser.tab.o index a7cf483..4793b7c 100644 Binary files a/compiler/src/main/cpp/parser/parser.tab.o and b/compiler/src/main/cpp/parser/parser.tab.o differ diff --git a/compiler/src/main/cpp/parser/parser.y b/compiler/src/main/cpp/parser/parser.y index e4fe200..b4aac2e 100644 --- a/compiler/src/main/cpp/parser/parser.y +++ b/compiler/src/main/cpp/parser/parser.y @@ -9,6 +9,7 @@ #include "ast.h" // AST node definitions #include "symbol.h" // Symbol table classes #include "lexer.h" // Java lexer interface +#include "analyzer.h" // Semantic analyzer // External lexer interface functions extern int yylex(); @@ -477,6 +478,19 @@ public: class WASMGenerator { public: void generate(ProgramNode* root) { + // Run analyzer before printing/generating code + Analyzer analyzer(/*enableOptimizations=*/true); + Analyzer::Result res = analyzer.analyze(root); + if (!res.errors.empty()) { + std::cout << "=== SEMANTIC ERRORS ===" << std::endl; + for (auto& e : res.errors) std::cout << "error: " << e << std::endl; + } + if (!res.warnings.empty()) { + std::cout << "=== SEMANTIC WARNINGS ===" << std::endl; + for (auto& w : res.warnings) std::cout << "warning: " << w << std::endl; + } + std::cout << "Optimizations applied: " << res.optimizationsApplied << std::endl; + ASTTreePrinter printer; printer.printTree(root); } @@ -520,6 +534,7 @@ public: %type while_loop for_loop if_statement print_statement %type body range %type expression relation simple factor summand primary +%type or_expr xor_expr and_expr %type modifiable_primary routine_call %type expression_list argument_list @@ -733,30 +748,43 @@ body: /* empty */ { $$ = new BodyNode(); symbolTable->enterScope(); } } ; -expression: relation { $$ = $1; } - | expression TOK_AND relation { $$ = new BinaryOpNode(OpKind::AND, $1, $3); } - | expression TOK_OR relation { $$ = new BinaryOpNode(OpKind::OR, $1, $3); } - | expression TOK_XOR relation { $$ = new BinaryOpNode(OpKind::XOR, $1, $3); } - ; +expression: or_expr { $$ = $1; } + ; + +// Boolean precedence: not > and > xor > or +or_expr: xor_expr { $$ = $1; } + | or_expr TOK_OR xor_expr { $$ = new BinaryOpNode(OpKind::OR, $1, $3); } + ; + +xor_expr: and_expr { $$ = $1; } + | xor_expr TOK_XOR and_expr { $$ = new BinaryOpNode(OpKind::XOR, $1, $3); } + ; + +and_expr: relation { $$ = $1; } + | and_expr TOK_AND relation { $$ = new BinaryOpNode(OpKind::AND, $1, $3); } + ; relation: simple { $$ = $1; } - | relation TOK_LESS simple { $$ = new BinaryOpNode(OpKind::LT, $1, $3); } - | relation TOK_LESS_EQUAL simple { $$ = new BinaryOpNode(OpKind::LE, $1, $3); } - | relation TOK_GREATER simple { $$ = new BinaryOpNode(OpKind::GT, $1, $3); } - | relation TOK_GREATER_EQUAL simple { $$ = new BinaryOpNode(OpKind::GE, $1, $3); } - | relation TOK_EQUAL simple { $$ = new BinaryOpNode(OpKind::EQ, $1, $3); } - | relation TOK_NOT_EQUAL simple { $$ = new BinaryOpNode(OpKind::NE, $1, $3); } + | relation TOK_LESS simple { $$ = new BinaryOpNode(OpKind::LT, $1, $3); } + | relation TOK_LESS_EQUAL simple { $$ = new BinaryOpNode(OpKind::LE, $1, $3); } + | relation TOK_GREATER simple { $$ = new BinaryOpNode(OpKind::GT, $1, $3); } + | relation TOK_GREATER_EQUAL simple { $$ = new BinaryOpNode(OpKind::GE, $1, $3); } + | relation TOK_EQUAL simple { $$ = new BinaryOpNode(OpKind::EQ, $1, $3); } + | relation TOK_NOT_EQUAL simple { $$ = new BinaryOpNode(OpKind::NE, $1, $3); } ; +// Adjusted precedence: multiplication/division/modulo bind tighter than addition/subtraction +// simple → handles addition/subtraction simple: factor { $$ = $1; } - | simple TOK_MULTIPLY factor { $$ = new BinaryOpNode(OpKind::MUL, $1, $3); } - | simple TOK_DIVIDE factor { $$ = new BinaryOpNode(OpKind::DIV, $1, $3); } - | simple TOK_MODULO factor { $$ = new BinaryOpNode(OpKind::MOD, $1, $3); } + | simple TOK_PLUS factor { $$ = new BinaryOpNode(OpKind::PLUS, $1, $3); } + | simple TOK_MINUS factor { $$ = new BinaryOpNode(OpKind::MINUS, $1, $3); } ; +// factor → handles multiplication/division/modulo factor: summand { $$ = $1; } - | factor TOK_PLUS summand { $$ = new BinaryOpNode(OpKind::PLUS, $1, $3); } - | factor TOK_MINUS summand { $$ = new BinaryOpNode(OpKind::MINUS, $1, $3); } + | factor TOK_MULTIPLY summand { $$ = new BinaryOpNode(OpKind::MUL, $1, $3); } + | factor TOK_DIVIDE summand { $$ = new BinaryOpNode(OpKind::DIV, $1, $3); } + | factor TOK_MODULO summand { $$ = new BinaryOpNode(OpKind::MOD, $1, $3); } ; summand: primary { $$ = $1; } @@ -830,7 +858,7 @@ int main(int argc, char** argv) { // Parse input int result = yyparse(); - // Generate WASM code from AST (stub) + // Generate output (runs analyzer + prints AST) if (result == 0 && astRoot && !hasParseError) { WASMGenerator generator; generator.generate(astRoot); diff --git a/compiler/src/main/cpp/parser/run_tests.sh b/compiler/src/main/cpp/parser/run_tests.sh deleted file mode 100755 index e96dc3a..0000000 --- a/compiler/src/main/cpp/parser/run_tests.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# Script to run parser tests - -echo "Running tests..." - -for i in {1..10}; do - echo "==========================================" - echo "Running test$i.i" - echo "==========================================" - - echo "FILE CONTENT:" - cat test$i.i - echo "----------------------------------------" - echo "" - - echo "PARSING STEPS & AST ANALYSIS:" - ./parser < test$i.i 2>&1 - echo "----------------------------------------" - echo "" - - echo "" -done - -echo "All tests completed." diff --git a/compiler/src/main/cpp/parser/symbol.o b/compiler/src/main/cpp/parser/symbol.o index 5395b31..96ddc4f 100644 Binary files a/compiler/src/main/cpp/parser/symbol.o and b/compiler/src/main/cpp/parser/symbol.o differ diff --git a/compiler/src/main/cpp/parser/test1.err b/compiler/src/main/cpp/parser/test1.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test1.out b/compiler/src/main/cpp/parser/test1.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test1.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test10.err b/compiler/src/main/cpp/parser/test10.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test10.out b/compiler/src/main/cpp/parser/test10.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test10.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test2.err b/compiler/src/main/cpp/parser/test2.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test2.out b/compiler/src/main/cpp/parser/test2.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test2.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test3.err b/compiler/src/main/cpp/parser/test3.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test3.out b/compiler/src/main/cpp/parser/test3.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test3.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test4.err b/compiler/src/main/cpp/parser/test4.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test4.out b/compiler/src/main/cpp/parser/test4.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test4.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test5.err b/compiler/src/main/cpp/parser/test5.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test5.out b/compiler/src/main/cpp/parser/test5.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test5.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test6.err b/compiler/src/main/cpp/parser/test6.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test6.out b/compiler/src/main/cpp/parser/test6.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test6.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test7.err b/compiler/src/main/cpp/parser/test7.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test7.out b/compiler/src/main/cpp/parser/test7.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test7.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test8.err b/compiler/src/main/cpp/parser/test8.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test8.out b/compiler/src/main/cpp/parser/test8.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test8.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test9.err b/compiler/src/main/cpp/parser/test9.err deleted file mode 100644 index e69de29..0000000 diff --git a/compiler/src/main/cpp/parser/test9.out b/compiler/src/main/cpp/parser/test9.out deleted file mode 100644 index 9319611..0000000 --- a/compiler/src/main/cpp/parser/test9.out +++ /dev/null @@ -1,4 +0,0 @@ -JavaLexer initialized -Generating WASM from AST -Parsing completed successfully -JavaLexer destroyed diff --git a/compiler/src/main/cpp/parser/test_parser.cpp b/compiler/src/main/cpp/parser/test_parser.cpp deleted file mode 100644 index b05c47e..0000000 --- a/compiler/src/main/cpp/parser/test_parser.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include -#include -#include "ast.h" -#include "symbol.h" - -// Simple test program to check AST and symbol table functionality -int main() { - std::cout << "Testing AST and Symbol Table functionality" << std::endl; - - // Test symbol table - SymbolTable* symbolTable = new SymbolTable(); - - // Add some test variables - PrimitiveTypeNode* intType = new PrimitiveTypeNode(TypeKind::INTEGER); - PrimitiveTypeNode* realType = new PrimitiveTypeNode(TypeKind::REAL); - PrimitiveTypeNode* boolType = new PrimitiveTypeNode(TypeKind::BOOLEAN); - - symbolTable->declareVariable("x", intType); - symbolTable->declareVariable("y", realType); - symbolTable->declareVariable("flag", boolType); - - // Test lookup - VariableInfo* varX = symbolTable->lookupVariable("x"); - if (varX) { - std::cout << "Found variable x with type: " << - (dynamic_cast(varX->type) ? "INTEGER" : "UNKNOWN") << std::endl; - } - - // Test type inference - IntegerLiteralNode* intLit = new IntegerLiteralNode(42); - TypeNode* inferredType = inferType(intLit); - std::cout << "Inferred type for integer literal: " << - (dynamic_cast(inferredType) ? - (dynamic_cast(inferredType)->kind == TypeKind::INTEGER ? "INTEGER" : "OTHER") : - "UNKNOWN") << std::endl; - - // Test binary operation type inference - BinaryOpNode* addOp = new BinaryOpNode(OpKind::PLUS, intLit, new IntegerLiteralNode(10)); - TypeNode* addType = inferType(addOp); - std::cout << "Inferred type for addition: " << - (dynamic_cast(addType) ? - (dynamic_cast(addType)->kind == TypeKind::INTEGER ? "INTEGER" : "OTHER") : - "UNKNOWN") << std::endl; - - // Test AST creation - ProgramNode* program = new ProgramNode(); - - VariableDeclarationNode* varDecl = new VariableDeclarationNode("testVar", intType, intLit); - program->addDeclaration(varDecl); - - std::cout << "Created program with " << program->declarations.size() << " declarations" << std::endl; - - // Cleanup - delete program; - delete symbolTable; - delete intLit->type; // Clean up types created by literals - delete addOp; // This will delete its children due to destructors - - std::cout << "All tests completed successfully!" << std::endl; - return 0; -} diff --git a/complex_test.i b/complex_test.i deleted file mode 100644 index f1635d6..0000000 --- a/complex_test.i +++ /dev/null @@ -1,7 +0,0 @@ -type Point is record - var x: real; - var y: real; -end -var p: Point; -p.x := 1.5; -p.y := 2.7; diff --git a/docker_test.sh b/docker_test.sh index a70fe36..b1a4e9c 100644 --- a/docker_test.sh +++ b/docker_test.sh @@ -1,6 +1,37 @@ #!/usr/bin/env bash set -euo pipefail +# Optional flags +VERBOSE_FLAG="" +SUITE_VAL="" +FILTER_VAL="" +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--verbose) + VERBOSE_FLAG=" --verbose"; shift ;; + --suite) + SUITE_VAL="${2:-}"; shift 2 ;; + --filter) + FILTER_VAL="${2:-}"; shift 2 ;; + *) + echo "Unknown option: $1"; exit 2 ;; + esac +done + +# Build pass-through args for inner harness (quote-safe for single quotes) +SUITE_ARG="" +FILTER_ARG="" +SUITE_INNER_ASSIGN="SELECT_SUITE=''" +if [[ -n "$SUITE_VAL" ]]; then + SUITE_ESC=${SUITE_VAL//\'/\'\\\'\'} + SUITE_ARG=" --suite '$SUITE_ESC'" + SUITE_INNER_ASSIGN="SELECT_SUITE='$SUITE_ESC'" +fi +if [[ -n "$FILTER_VAL" ]]; then + FILTER_ESC=${FILTER_VAL//\'/\'\\\'\'} + FILTER_ARG=" --filter '$FILTER_ESC'" +fi + IMAGE="hmm-compiler-test:latest" echo "[1/2] Building Docker image: ${IMAGE}" @@ -24,9 +55,17 @@ docker run --rm -t \ -w "/app" \ "${IMAGE}" \ -lc "set -euo pipefail; \ + ${SUITE_INNER_ASSIGN}; \ find . -type f -name '*.sh' -exec sed -i 's/\r$//' {} + ; \ - bash ./integration_test.sh; \ - export LD_LIBRARY_PATH=/app/compiler/src/main/cpp/parser; \ - echo \"LD_LIBRARY_PATH=\$LD_LIBRARY_PATH\"; \ - chmod +x ./gradlew; \ - ./gradlew --no-daemon tests:test -Djava.library.path=/app/compiler/src/main/cpp/parser" + if [ -f ./gradlew ]; then sed -i 's/\r$//' ./gradlew || true; chmod +x ./gradlew || true; fi; \ + echo '[A] Unified harness: tests/harness/run.sh (directory-based)'; \ + bash ./tests/harness/run.sh${VERBOSE_FLAG}${SUITE_ARG}${FILTER_ARG}; \ + echo; echo '[B] Java lexer tests: tests:test'; \ + if [ -z \"\$SELECT_SUITE\" ] || [ \"\$SELECT_SUITE\" = \"lexer\" ]; then \ + export LD_LIBRARY_PATH=/app/compiler/src/main/cpp/parser; \ + echo \"LD_LIBRARY_PATH=\$LD_LIBRARY_PATH\"; \ + chmod +x ./gradlew; \ + ./gradlew --no-daemon tests:test -Djava.library.path=/app/compiler/src/main/cpp/parser; \ + else \ + echo \"Skipping Java lexer tests (suite=\$SELECT_SUITE)\"; \ + fi" diff --git a/docs/analyzer.md b/docs/analyzer.md new file mode 100644 index 0000000..d87322b --- /dev/null +++ b/docs/analyzer.md @@ -0,0 +1,164 @@ +# Semantic Analyzer for the Imperative (I) Language + +This document describes the semantic analyzer implemented for the project, the checks it performs (non-mutating) and the optimizations it applies (AST-modifying), along with design notes, examples, and how to run it. + +## Overview + +- Location: `compiler/src/main/cpp/parser/analyzer.{h,cpp}` +- Integrated in: `parser.y` (runs automatically after successful parse) +- Input: C++ AST built by the Bison parser (`ast.h`) +- Output: Diagnostics to stdout (errors/warnings) and an optimized AST that is printed by the existing AST printer stub. +- Build integration: `compiler/src/main/cpp/parser/Makefile` + +Why C++? The parser and AST types live in C++. A Java-only analyzer would require re-exposing or duplicating the AST. This analyzer operates directly on the existing C++ AST and symbol table. + +## Contract + +- Inputs: `ProgramNode*` root AST, symbol table populated during parsing. +- Outputs: + - `errors[]`: semantic violations; compilation should fail if non-empty. + - `warnings[]`: potential issues that don’t block compilation. + - `optimizationsApplied`: count of AST transformations applied. +- Success criteria: no errors; warnings optional; AST may be simplified. + +## Non-mutating semantic checks + +1) Boolean conditions in control flow +- While: condition must be boolean +- If: condition must be boolean + +2) Routine calls +- Existence: callee must be declared +- Arity: argument count must match +- Types: each argument must be assignment-compatible with parameter type + +3) Record field access +- Access only valid on record-typed expressions +- Field must exist in the record type definition + +4) Array indexing +- Index expression type must be integer +- Static bounds check when size is a constant: warns if constant index outside [1..N] (1-based, matching examples in docs) + +5) Routine return (arrow body) +- If a routine has a declared return type and uses `=> expr` body, the expression type must be compatible with the declared return type + +Notes +- Type inference delegates to `inferType` (`symbol.cpp`) to maintain consistency with parser-time type logic. +- Symbol table usage for routine/variable/type lookup is consistent with parser actions. + +## Optimizations (AST-modifying) + +1) Constant folding +- Binary arithmetic: `+ - * / mod` (promotes to real if needed) +- Comparisons: `< <= > >= = /=` folded to booleans when operands are constant numbers +- Boolean ops: `and or xor` folded on boolean constants +- Unary ops: `+ - not` folded on constants + +2) If simplification +- If condition is constant true/false, replace the entire if node with the selected branch body (statements inlined) + +Hoisting and folding of declarations +- When an if statement simplifies to a constant branch, any variable declarations inside the chosen branch are hoisted into the enclosing scope (program or body) to preserve semantics of flattened code. +- Initializers of hoisted declarations are folded immediately (e.g., `var b: integer is 1 + 2;` becomes `IntegerLiteral: 3`). +- Name conflicts created by hoisting result in a semantic error: `Duplicate variable declaration '' in same scope` (no auto-renaming). + +3) While false elimination +- Remove loops whose condition simplifies to constant `false` + +4) Remove unused variable declarations +- Drops variable declarations that are never referenced and have no initializer (to avoid removing potential side effects) +- Applied both at program level and within bodies + +Implementation notes +- Optimizations operate in a post-order manner to maximize folding opportunities +- The AST printer runs after optimizations, so changes are visible in the printed structure + +## Examples + +- Constant folding: + - Input: `var a: integer is 5 + 3;` + - Output AST shows `IntegerLiteral: 8` + +- If simplification: + - Input: + ``` + if true then + print 1 + end + ``` + - Output AST contains only the `print 1` statement + +- While false removal: + - Input: + ``` + while false loop + print 1 + end + ``` + - Output AST omits the loop entirely + +- Routine call checks: + - `x := add(1);` triggers an argument count mismatch if `add` expects two parameters + +- Record field check: + - `p.z` where `p: Point` and `Point` has only `x,y` → error + +- Array checks: + - `numbers[i]` where `i: real` → error + - `numbers[4]` where `numbers: array[3] integer` → warning + +## Running the analyzer + +The analyzer runs automatically when invoking the native parser executable: + +```bash +cd compiler/src/main/cpp/parser +make +./run_tests.sh # runs the original 10 parser demos with analyzer +./run_analyzer_tests.sh # runs analyzer-focused tests with assertions +``` + +In Docker (recommended for consistent toolchains): + +```bash +bash ./docker_test.sh +``` + +This builds a container and runs the unified test harness (native parser/analyzer tests) and Java tests. + +## Test suite additions + +Analyzer-focused tests live in `compiler/src/main/cpp/parser/`: +- `analyzer_const_and_control.i` – constant fold + if-true simplification + while-false removal +- `analyzer_routine_mismatch.i` – routine return type mismatch +- `analyzer_array_checks.i` – array index type error and static bounds warning +- `analyzer_record_field.i` – unknown record field access +- `analyzer_hoist_conflict.i` – hoisting causes duplicate declaration error +- `analyzer_hoist_and_fold.i` – hoist declarations from `if true` and fold their initializers +- `analyzer_hoist_nested.i` – nested `if true` bodies hoist and fold multiple declarations +- `analyzer_else_hoist_and_fold.i` – `if false` selects else-branch; declarations hoisted and folded +- `analyzer_while_false_nested.i` – loop body removed entirely when condition is `false` +- `analyzer_assignment_type_mismatch.i` – type mismatch in assignment is reported +- `analyzer_remove_unused_decl.i` – unused decl without initializer removed +- `analyzer_keep_decl_with_initializer.i` – unused decl with initializer is kept (initializer folded) +- `analyzer_postopt_undefined_top_level.i` – undefined variable after dead-branch removal reported +- `analyzer_boolean_folding.i` – boolean/unary/relational folding in initializers +- `analyzer_field_nonrecord.i` – field access on non-record expression reported +- `analyzer_if_condition_typecheck.i` – if condition must be boolean +- `analyzer_while_condition_typecheck.i` – while condition must be boolean +- `analyzer_record_field_duplicate.i` – duplicate record field names detected + +Harness: `run_analyzer_tests.sh` asserts expected diagnostics and optimization evidence (e.g., folded literals, optimization count). + +## Limitations and future work + +- Type system is basic (mirrors current `inferType`); richer user-defined types and conversions can be added. +- Bounds checking is best-effort and static (constants only), which is typical for this phase. +- Function inlining is not implemented (non-trivial due to scoping/side effects); could be added as a future optimization for simple arrow routines. +- Exposing the analyzer to Java via JNI is straightforward if a Java API is desired (parse → analyze → return a formatted report string). + +## Troubleshooting + +- If you don’t see analyzer output, ensure `parser` was rebuilt and that `parser.y` includes `analyzer.h` and calls the analyzer in `WASMGenerator::generate`. +- On Windows, run under Docker or MSYS2 MinGW shell with `g++`, `bison`, and `flex` installed. \ No newline at end of file diff --git a/docs/analyzer_flow_en.md b/docs/analyzer_flow_en.md new file mode 100644 index 0000000..af5f2d4 --- /dev/null +++ b/docs/analyzer_flow_en.md @@ -0,0 +1,114 @@ +# Analyzer Flow and Code Structure (English) + +This document explains how the semantic analyzer runs end-to-end, the order of phases, and how each part of the code works. It is based on the implementation in `compiler/src/main/cpp/parser/analyzer.{h,cpp}` and the AST defined in `ast.h`. + +## High-level flow + +Input: a fully parsed `ProgramNode*` and a populated `SymbolTable`. +Output: diagnostics (errors/warnings) and an optimized AST that the existing printer shows. + +The pipeline (Analyzer::analyze): + +1) runChecks(root) +- Walks declarations and statements to perform non-mutating semantic validations. +- Detects type and shape errors (e.g., wrong field, wrong array index type, non-boolean conditions, assignment type mismatch, routine call issues, return type mismatch in arrow bodies). + +2) runOptimizations(root) [only if no errors and optimizations enabled] +- Folds constant expressions in declarations and routines. +- Simplifies control flow: + - If a condition becomes a constant, flatten the chosen branch. + - While with constant false is removed. +- Hoists declarations from a chosen constant if-branch into the enclosing scope (program/body) with conflict detection. +- Immediately folds initializers of hoisted declarations. +- Removes unused declarations without initializers. + +3) Post-optimization validation (safety check) +- Re-checks top-level statements and expressions against the set of global variable declarations. +- Reports undefined variable uses that can surface after dead-branch elimination. + +The result is returned as `Analyzer::Result` with `errors`, `warnings`, and `optimizationsApplied`. + +## Non-mutating checks (runChecks) + +- Variable initializer type vs declared type + - When a variable has an initializer, infer its type and verify it is compatible with the declared type. Otherwise: `error: Type mismatch in variable initializer: `. + +- Record types (duplicate fields) + - Inside a `record` type declaration, duplicate field names are rejected: `error: Duplicate field '' in type ''`. + +- Routine declarations + - Arrow-body (`=> expr`) functions: infer expression type and compare to declared return type. Mismatch yields `error: Routine '' return type mismatch`. + - Block-body (`is ... end`) functions: validate local declarations and statements within the body. + +- Statements + - Assignment: check target and value expressions; `error: Type mismatch in assignment` if types aren’t compatible. + - While: condition must be boolean; otherwise `error: While condition must be boolean`. + - For: + - Numeric range: both bounds must be integers. + - For-in over arrays: the range expression must be an array type. + - If: condition must be boolean; otherwise `error: If condition must be boolean`. + - Print: validate all expressions in the list. + - Routine call statements: existence, arity, type compatibility per parameter. + +- Expressions + - Array index must be integer: `error: Array index must be integer`. + - Static bounds warning: if both index and size are constants and out-of-range, emit `warning: Array index out of bounds [1..] (static)`. + - Record field access: ensure base is a record type and field exists; otherwise `error: Field access on non-record type` or `error: Unknown field '' in record`. + +## Optimizations (runOptimizations) + +- Constant folding (foldExpression) + - Arithmetic: +, -, *, /, mod (promotes to real where needed). + - Comparisons: <, <=, >, >=, =, /= folded to booleans when operands are numeric constants. + - Boolean ops: and, or, xor folded for boolean constants. + - Unary ops: +, -, not folded for constant operands. + +- If simplification with declaration hoisting + - If a condition folds to a boolean literal, the analyzer selects the chosen body. + - Before hoisting, it calls `simplifyInBody(chosen)` so nested constant branches are flattened first; this propagates inner declarations up into the chosen body’s declarations. + - Then it hoists chosen-body declarations into the enclosing scope: + - At program level: into `ProgramNode::declarations`. + - At body level: into `BodyNode::declarations`. + - Initializers of hoisted declarations are folded immediately. + - Name conflicts (an existing variable with the same name in the same scope) produce `error: Duplicate variable declaration '' in same scope` (no auto-renaming). + - The chosen body’s statements are spliced into the current statement list (flattening the if). + +- While false elimination + - If a while condition folds to false, the loop is removed. + +- Remove unused declarations + - Gathers a set of used variables (from statements and from remaining initializers). + - Drops declarations that are unused and have no initializer, counting it as an optimization. + +## Post-optimization validation + +- Collects global variable names from program-level declarations. +- Scans top-level statements and reports `error: Undefined variable ''` if an identifier is referenced that isn’t declared at the program level. +- This step is intentionally top-level only to avoid false positives in non-constant branches. + +## Error/warning accounting + +- All errors and warnings are pushed into `Analyzer::Result` vectors. +- `optimizationsApplied` is incremented whenever a fold or structural simplification actually changes the AST. + +## Ordering and rationale + +- Checks precede optimizations to avoid transforming an invalid program. +- `simplifyInBody(chosen)` before hoisting ensures nested-if hoisting is correct and comprehensive (e.g., `y` declared in an inner constant-if won’t be missed). +- Post-optimization validation catches undefined references introduced by control simplifications. + +## Extensibility + +- Add new checks by expanding `checkNode`, `checkStatement`, and `checkExpression`. +- Add new optimizations by extending `foldExpression` or the `simplify*` passes. +- Keep the order: check → optimize → safety validation to maintain predictable semantics. + +## Key functions map + +- `Analyzer::analyze` — Orchestrates the whole pipeline. +- `runChecks` — Entry for all validations. +- `runOptimizations` — Constant folding and control simplification. +- `foldExpression` — Recursive expression folding engine. +- `simplifyInProgram` / `simplifyInBody` — If/while transformations and hoisting. +- `removeUnusedDeclarations` — Prunes unused variables (no initializer). +- Post-opt safety validator — Ensures top-level references are defined. diff --git a/docs/analyzer_flow_ru.md b/docs/analyzer_flow_ru.md new file mode 100644 index 0000000..20f7d37 --- /dev/null +++ b/docs/analyzer_flow_ru.md @@ -0,0 +1,114 @@ +# Поток работы анализатора и структура кода (Русский) + +В этом документе описано, как выполняется семантический анализатор: общий конвейер, порядок фаз, а также подробности по каждому компоненту. Реализация находится в `compiler/src/main/cpp/parser/analyzer.{h,cpp}`, AST — в `ast.h`. + +## Общая схема + +Вход: разобранный `ProgramNode*` и заполненная таблица символов `SymbolTable`. +Выход: диагностические сообщения (ошибки/предупреждения) и оптимизированный AST, который выводится существующим принтером. + +Конвейер (Analyzer::analyze): + +1) runChecks(root) +- Обходит объявления и операторы и выполняет немодифицирующие проверки. +- Находит ошибки типов и формы (неверное поле записи, тип индекса массива, небулевы условия, несоответствие типов в присваивании, проблемы вызовов процедур, несоответствие типа возврата в стрелочных функциях). + +2) runOptimizations(root) [выполняется только если нет ошибок и включены оптимизации] +- Сворачивает константные выражения в объявлениях и функциях. +- Упрощает управление потоком: + - Если условие `if` стало константой — разворачивает выбранную ветку. + - `while` с константой `false` удаляется. +- Поднимает (hoist) объявления из выбранной константной ветки `if` во внешний scope (программа/тело) с проверкой конфликтов имён. +- Немедленно сворачивает инициализаторы поднятых переменных. +- Удаляет неиспользуемые объявления без инициализаторов. + +3) Проверка после оптимизаций (safety check) +- Повторно проверяет верхнеуровневые операторы и выражения относительно множества глобальных объявлений. +- Сообщает об использовании неописанных переменных, что может проявиться после устранения «мертвых» ветвей. + +Результат возвращается как `Analyzer::Result` с `errors`, `warnings` и `optimizationsApplied`. + +## Немодифицирующие проверки (runChecks) + +- Инициализатор переменной vs объявленный тип + - Для переменных с инициализатором вычисляется тип и сверяется с объявленным. Иначе: `error: Type mismatch in variable initializer: `. + +- Типы-записи (дубли полей) + - Внутри `record` запрещены дубли имён полей: `error: Duplicate field '' in type ''`. + +- Объявления процедур/функций + - Функции со стрелочным телом (`=> expr`): тип выражения сравнивается с объявленным типом возврата. Несоответствие даёт `error: Routine '' return type mismatch`. + - Функции с блочным телом (`is ... end`): проверяются локальные объявления и операторы. + +- Операторы + - Присваивание: проверяются типы цели и значения; при несовместимости — `error: Type mismatch in assignment`. + - While: условие должно быть булевым; иначе `error: While condition must be boolean`. + - For: + - Числовой диапазон: обе границы — целые. + - Обход массива: выражение диапазона должно быть массивом. + - If: условие — булево; иначе `error: If condition must be boolean`. + - Print: проверяются все выражения в списке. + - Оператор вызова процедуры: существование, арность, совместимость типов параметров. + +- Выражения + - Индекс массива — целый: `error: Array index must be integer`. + - Статическая проверка границ: если индекс и размер — константы и индекс вне диапазона, предупреждение `warning: Array index out of bounds [1..] (static)`. + - Доступ к полю записи: базовый тип — запись, поле существует; иначе `error: Field access on non-record type` или `error: Unknown field '' in record`. + +## Оптимизации (runOptimizations) + +- Свёртка констант (foldExpression) + - Арифметика: +, -, *, /, mod (при необходимости — продвижение к real). + - Сравнения: <, <=, >, >=, =, /= сворачиваются в boolean при числовых константах. + - Логика: and, or, xor сворачиваются для констант. + - Унарные: +, -, not сворачиваются для константных операндов. + +- Упрощение if с подъёмом объявлений + - Если условие свелось к константе, выбирается соответствующее тело. + - Перед подъёмом вызывается `simplifyInBody(chosen)`, чтобы сначала развернуть вложенные константные ветви; это поднимает их объявления в `declarations` выбранного тела. + - Затем объявления выбранного тела поднимаются во внешний scope: + - На уровне программы: в `ProgramNode::declarations`. + - На уровне тела: в `BodyNode::declarations`. + - Инициализаторы поднятых переменных немедленно сворачиваются. + - Конфликт имён (существует переменная с тем же именем в том же scope) даёт `error: Duplicate variable declaration '' in same scope` (переименования нет). + - Операторы выбранного тела встраиваются в текущий список операторов (if удаляется). + +- Удаление `while false` + - Если условие свелось к false, цикл удаляется. + +- Удаление неиспользуемых объявлений + - Формируется множество используемых переменных (из операторов и инициализаторов). + - Удаляются объявления, которые не используются и не имеют инициализатора. + +## Проверка после оптимизаций + +- Собираются имена глобальных переменных из объявлений уровня программы. +- Сканируются верхнеуровневые операторы; если встречается идентификатор, отсутствующий среди глобальных, сообщение `error: Undefined variable ''`. +- Преднамеренно анализируются только верхнеуровневые операторы, чтобы не давать ложных срабатываний для невыбранных веток. + +## Учёт ошибок/предупреждений и оптимизаций + +- Ошибки и предупреждения записываются в `Analyzer::Result`. +- `optimizationsApplied` увеличивается при любой фактической модификации AST (свёртка, удаление, разворачивание). + +## Порядок фаз и мотивация + +- Сначала проверки — затем оптимизации: нельзя преобразовывать заведомо неверную программу. +- Вызов `simplifyInBody(chosen)` перед подъёмом обеспечивает корректный подъём объявлений из вложенных константных if (например, `y` из внутренней ветки не потеряется). +- Финальная проверка защищает от неописанных ссылок, появляющихся после упрощения управления потоком. + +## Расширение + +- Добавляйте новые проверки в `checkNode`, `checkStatement`, `checkExpression`. +- Добавляйте новые оптимизации в `foldExpression` и семейство `simplify*`. +- Сохраняйте порядок: проверки → оптимизации → защитная проверка. + +## Карта ключевых функций + +- `Analyzer::analyze` — оркестрация конвейера. +- `runChecks` — вход в фазу проверок. +- `runOptimizations` — свёртка констант и упрощение управления потоком. +- `foldExpression` — движок свёртки выражений. +- `simplifyInProgram` / `simplifyInBody` — преобразование if/while и подъём объявлений. +- `removeUnusedDeclarations` — удаление неиспользуемых переменных (без инициализатора). +- Пост-оптимизационная защитная проверка — неописанные верхнеуровневые ссылки. diff --git a/docs/analyzer_tests.md b/docs/analyzer_tests.md new file mode 100644 index 0000000..3496095 --- /dev/null +++ b/docs/analyzer_tests.md @@ -0,0 +1,447 @@ +# Analyzer Test Vectors and Expected Output + +This document captures the analyzer-specific tests, their input programs, and the expected diagnostics/transformations. + +Each test can be executed via the unified harness: `bash tests/harness/run.sh --suite analyzer` (also run by `docker_test.sh`). + +## Test 1: Constant folding and control simplification + +Input (`analyzer_const_and_control.i`): + +``` +var a: integer is 5 + 3; +if true then + var z: integer is 1 + 2; +end +while false loop + var w: integer is 10; +end +print a +``` + +Expected highlights: +- Optimizations applied: 3 + - `5 + 3` folded to `8` + - `if true then ... end` body flattened (declaration moved/removed as no uses) + - `while false loop ... end` removed entirely +- AST contains `IntegerLiteral: 8` for `a`'s initializer +- A single `PrintStatement` of `a` remains + +## Test 2: Routine return type mismatch + +Input (`analyzer_routine_mismatch.i`): + +``` +routine add(a: integer, b: integer): integer => a + b +routine f(a: integer): boolean => a + 1 +var x: integer is 0; +x := add(1, 2); +``` + +Expected diagnostics: +- `error: Routine 'f' return type mismatch` + +Notes: +- Argument arity/type checks are enforced at parse time; this test uses a correct call to `add` to avoid parser errors and let the analyzer report the return-type mismatch in `f`'s body. + +## Test 3: Array index checks and static bounds + +Input (`analyzer_array_checks.i`): + +``` +var numbers: array[3] integer; +var i: real is 1.0; +numbers[i] := 10; +numbers[4] := 20; +``` + +Expected diagnostics: +- `error: Array index must be integer` +- `warning: Array index 4 out of bounds [1..3] (static)` + +## Test 4: Record field existence + +Input (`analyzer_record_field.i`): + +``` +type Point is record + var x: real; + var y: real; +end +var p: Point; +var a: real is p.z; +``` + +Expected diagnostics: +- `error: Unknown field 'z' in record` +- The analyzer may also cascade a type mismatch error for `a`'s initializer depending on inference: `error: Type mismatch in variable initializer: a` + +## Test 5: Hoisted declaration conflict + +Input (`analyzer_hoist_conflict.i`): + +``` +var a: integer is 5 + 3; +if true then + var a: integer is 1 + 2; + print a; +end +print a +``` + +Behavior: +- The `if true` branch is flattened and its declarations are hoisted into the enclosing scope. +- Name conflicts are not auto-resolved; hoisting treats the declarations as being in the same scope. + +Expected diagnostics: +- `error: Duplicate variable declaration 'a' in same scope` + +--- + +## Test 6: Hoist and fold in then-branch + +Input (`analyzer_hoist_and_fold.i`): + +``` +var a: integer is 5 + 3; +if true then + var b: integer is 1 + 2; + print b; +end +print a +``` + +Expected highlights: +- `b` is hoisted into the program scope and its initializer is folded to `3` +- AST shows `IntegerLiteral: 8` for `a` and `IntegerLiteral: 3` for `b` +- Selected branch statements are inlined (print `b` remains) + +## Test 7: Nested hoisting with folding + +Input (`analyzer_hoist_nested.i`): + +``` +if true then + var x: integer is 1 + 1; + if true then + var y: integer is 2 + 2; + print y; + end +end +print x +``` + +Expected highlights: +- Both `x` and `y` are hoisted; initializers folded to `2` and `4` +- `print y` is preserved; `print x` at top level is valid + +## Test 8: Else-branch hoist and fold + +Input (`analyzer_else_hoist_and_fold.i`): + +``` +if false then + var k: integer is 100; +else + var c: integer is 2 + 2; + print c; +end +``` + +Expected highlights: +- Else branch selected; `c` hoisted and folded to `4` + +## Test 9: While false nested + +Input (`analyzer_while_false_nested.i`): + +``` +var z: integer is 42; +while false loop + var q: integer is 1 + 1; + print q; +end +print z +``` + +Expected highlights: +- Loop removed entirely; no hoisting from dead loop body +- AST still contains `print z` and the declaration for `z` + +## Test 10: Assignment type mismatch + +Input (`analyzer_assignment_type_mismatch.i`): + +``` +var a: integer; +a := 1.0; +``` + +Expected diagnostics: +- `error: Type mismatch in assignment` + +Notes: +- This violation is enforced at parse time; the parser prints a `Parse error ... Type mismatch in assignment`. +- The analyzer test harness treats the presence of that parse error text as a PASS for this test. + +## Test 11: Remove unused declaration (no initializer) + +Input (`analyzer_remove_unused_decl.i`): + +``` +var unusedVarUnique_1: integer; +print 0 +``` + +Expected highlights: +- Unused declaration removed (no initializer → safe to drop) + +## Test 12: Keep unused decl with initializer + +Input (`analyzer_keep_decl_with_initializer.i`): + +``` +var keepMe: integer is 1 + 2; +``` + +Expected highlights: +- Declaration retained (has initializer); initializer folded to `3` + +## Test 13: Undefined variable after dead-branch removal + +Input (`analyzer_postopt_undefined_top_level.i`): + +``` +if false then + var y: integer is 1; +end +print y +``` + +Expected diagnostics: +- `error: Undefined variable 'y'` + +## Test 14: Boolean folding in initializer + +Input (`analyzer_boolean_folding.i`): + +``` +var b: boolean is not (true and false) or (1 < 2); +``` + +Expected highlights: +- Initializer folded to `BooleanLiteral: true` + +## Test 15: Field access on non-record + +Input (`analyzer_field_nonrecord.i`): + +``` +var i: integer is 1; +var k: integer is i.z; +``` + +Expected diagnostics: +- `error: Field access on non-record type` + +## Test 16: If condition must be boolean + +Input (`analyzer_if_condition_typecheck.i`): + +``` +if 1 then + print 1; +end +``` + +Expected diagnostics: +- `error: If condition must be boolean` + +Notes: +- This is enforced at parse time; the parser prints a `Parse error ... If condition must be boolean`. +- The analyzer test harness treats the presence of that parse error text as a PASS for this test. + +## Test 17: While condition must be boolean + +Input (`analyzer_while_condition_typecheck.i`): + +``` +while 1 loop + print 1; +end +``` + +Expected diagnostics: +- `error: While condition must be boolean` + +Notes: +- This is enforced at parse time; the parser prints a `Parse error ... While condition must be boolean`. +- The analyzer test harness treats the presence of that parse error text as a PASS for this test. + +## Test 18: Duplicate record field + +Input (`analyzer_record_field_duplicate.i`): + +``` +type Point is record + var x: real; + var x: real; +end +``` + +Expected diagnostics: +- `error: Duplicate field 'x' in type 'Point'` + +## Test 19: For-loop over numeric range (OK) + +Input (`analyzer_for_range_ok.i`): + +``` +for i in 1 .. 3 loop + print i; +end +``` + +Expected highlights: +- AST contains a `ForLoop` node and a `PrintStatement` inside the loop body. + +## Test 20: For-loop numeric range type error + +Input (`analyzer_for_range_type_error.i`): + +``` +for i in 1.0 .. 3 loop +end +``` + +Expected diagnostics: +- `error: For range bounds must be integers` + +## Test 21: For-in over array (OK) + +Input (`analyzer_for_in_array_ok.i`): + +``` +var arr: array[3] integer; +for i in arr loop + print i; +end +``` + +Expected highlights: +- AST contains a `ForLoop` node and a `PrintStatement` inside the loop body. + +## Test 22: Print with multiple expressions + +Input (`analyzer_print_multiple.i`): + +``` +print 1, 2, 3; +``` + +Expected highlights: +- AST contains `PrintStatement` with three integer literals (`1`, `2`, `3`). + +## Test 23: Routine call — undefined routine (parser-level) + +Input (`analyzer_routine_call_undefined.i`): + +``` +foo(1); +``` + +Expected diagnostics: +- Parser prints `Parse error ... Undefined routine` (the analyzer harness treats this as PASS by checking for that text). + +## Test 24: Routine call — arity mismatch (parser-level) + +Input (`analyzer_routine_call_arity_mismatch.i`): + +``` +routine add(a: integer, b: integer): integer => a + b +add(1); +``` + +Expected diagnostics: +- Parser prints `Parse error ... Argument mismatch` (the analyzer harness treats this as PASS by checking for that text). + +## Test 25: Routine call — parameter type mismatch (analyzer-level) + +Input (`analyzer_routine_call_type_mismatch.i`): + +``` +routine f(a: integer, b: boolean): integer => a +var x: integer is 0; +f(1.0, 2); +``` + +Expected diagnostics: +- `error: Argument type mismatch in call to 'f' at position 1` +- `error: Argument type mismatch in call to 'f' at position 2` + +Run locally (optional): + +``` +# In project root +bash ./docker_test.sh +# Or run analyzer tests directly via the harness +bash tests/harness/run.sh --suite analyzer +``` + +## Test 26: Arithmetic precedence and associativity + +Input (`analyzer_precedence_arith.i`): + +``` +print 1 + 2 * 3; +print 1 * 2 + 3; +print 10 - 2 - 3; +print 10 / 2 * 3; +print 10 mod 6 mod 4; +print (1 + 2) * 3; +print 1 + (2 * 3); +``` + +Expected highlights: +- Folded outputs show: `7, 5, 5, 15, 0, 9, 7` respectively + +## Test 27: Unary operator precedence + +Input (`analyzer_precedence_unary.i`): + +``` +print -1 + 2 * 3; # -> 5 +print -(1 + 2) * 3; # -> -9 +print +1 * -2; # -> -2 +``` + +Expected highlights: +- Folded integer literals: `5, -9, -2` + +## Test 28: Boolean operator precedence (not > and > xor > or) + +Input (`analyzer_precedence_boolean.i`): + +``` +print true or false and false; # -> true +print (true or false) and false; # -> false +print not true or false; # -> false +print not (true or false); # -> false +print true xor false and false; # -> true +``` + +Expected highlights: +- Folded boolean literals appear accordingly + +## Test 29: Mixed arithmetic + boolean precedence + +Input (`analyzer_precedence_mixed.i`): + +``` +print 1 + 2 > 3 + 5 * 2; # -> false +print 1 + 2 * 3 = 7 and not (2 * 2 = 5); # -> true +print 1 + 2 * 3 = 9 or 10 / 2 + 1 = 6; # -> true +``` + +Expected highlights: +- Folded boolean literals: `false, true, true` diff --git a/docs/docker-testing.md b/docs/docker-testing.md index 812ef12..7af2047 100644 --- a/docs/docker-testing.md +++ b/docs/docker-testing.md @@ -18,7 +18,23 @@ From the project root: What this does: - Builds a Docker image with build tools (gcc/g++, make), bison, flex, and JDK 21 - Mounts the current workspace into the container -- Runs `./integration_test.sh` inside the container +- Runs the unified test harness (`tests/harness/run.sh`) and Java tests inside the container + +You can filter inside Docker too: + +```bash +# Only analyzer suite +bash ./docker_test.sh --suite analyzer + +# Filter tests by name pattern +bash ./docker_test.sh --filter "range" + +# Combine with verbose output +bash ./docker_test.sh --suite analyzer --verbose + +# Run only Java lexer tests +bash ./docker_test.sh --suite lexer +``` ## Notes diff --git a/docs/slides_analyzer.md b/docs/slides_analyzer.md new file mode 100644 index 0000000..18187ab --- /dev/null +++ b/docs/slides_analyzer.md @@ -0,0 +1,194 @@ +--- +marp: true +theme: default +paginate: true +header: 'Team Hmm - Semantic Analyzer' +--- + + + +# Team Hmm + +**Semantic Analyzer** + +*Imperative (I) Language* + +--- + +# Team Members + +**Mikhail Trifonov** + +**Kirill Efimovich** + +--- + +# Technology Stack + +## Analyzer Implementation Details + +| Component | Technology | +|-----------|------------| +| **Source Language** | Imperative (I) | +| **Implementation Language** | C++ (C++17) | +| **Parser/AST** | Bison/Flex + C++ AST | +| **Integration Point** | Runs after successful parse in `parser.y` | +| **Output** | Diagnostics + optimized AST (printed by existing printer) | + +--- + +# Where the Analyzer Fits + +## Pipeline Overview + +1. Parse (Bison/Flex) → build C++ AST +2. Semantic checks (non-mutating) +3. Optimizations (AST transforms) +4. Safety validation (post-optimization) +5. Print optimized AST / proceed to backend + +Result type: `Analyzer::Result { errors, warnings, optimizationsApplied }` + +--- + +# Non-mutating Checks (1/2) + +- Control flow conditions must be boolean + - `if (cond)` and `while (cond)` +- Assignments + - Type of right-hand side must be compatible with target +- Routine calls + - Existence, arity, and per-parameter type compatibility + +--- + +# Non-mutating Checks (2/2) + +- Records + - Duplicate fields rejected; field lookup verified +- Arrays + - Index must be `integer` + - Static bounds warning when both size and index are constants +- Routine returns (arrow bodies) + - `=> expr` type must match declared return type + +--- + +# Optimizations + +- Constant folding + - Arithmetic, relational, boolean, unary +- If simplification + - Constant condition selects a branch + - Declarations from chosen branch are hoisted and folded +- While-false elimination + - Entire loop removed when condition is constant `false` +- Remove unused declarations (no initializer) + +--- + +# Constant Folding Examples + +```i +var a: integer is 5 + 3; --→ a = 8 +var b: boolean is not (true and false) or (1 < 2); --→ b = true +``` + +✓ Arithmetic, boolean, relational, and unary folds + +--- + +# If Simplification + Hoisting + +Input: +```i +if true then + var x: integer is 1 + 1; + print x; +end +``` + +After analyze: +- Branch flattened into the parent body +- `x` hoisted; initializer folded to `2` +- `print x` remains inlined + +--- + +# Nested Hoisting + +Input: +```i +if true then + var x: integer is 1 + 1; + if true then + var y: integer is 2 + 2; + print y; + end +end +print x +``` + +Behavior: +- Inner constant-if pre-simplified before hoist +- Hoisted: `x = 2`, `y = 4` +- Statements spliced; `print y` preserved; `print x` valid at top level + +--- + +# Hoisting: Conflict Detection + +Input: +```i +var a: integer is 5 + 3; +if true then + var a: integer is 1 + 2; + print a; +end +``` + +Result: +- Error: `Duplicate variable declaration 'a' in same scope` +- No auto-renaming is performed + +--- + +# While False Elimination + +Input: +```i +while false loop + var q: integer is 1 + 1; + print q; +end +``` + +Behavior: +- Loop removed entirely +- No declarations hoisted from a dead loop body + +--- + +# Remove Unused Declarations + +- Drops variables that are never referenced and have no initializer +- Preserves variables with initializers (possible side effects / clarity) +- Counts toward `optimizationsApplied` + +--- + +# Post-optimization Safety Check + +- Re-scan top-level statements against program-level declarations +- Detects undefined identifiers introduced by control simplifications +- Example: +```i +if false then var y: integer is 1; end +print y --→ error: Undefined variable 'y' +``` + +--- + + + +# Questions? diff --git a/docs/slides_analyzer.pdf b/docs/slides_analyzer.pdf new file mode 100644 index 0000000..a5a893f Binary files /dev/null and b/docs/slides_analyzer.pdf differ diff --git a/docs/testing_guide.md b/docs/testing_guide.md index f08bfff..e1452d7 100644 --- a/docs/testing_guide.md +++ b/docs/testing_guide.md @@ -326,33 +326,24 @@ Integration tests verify that the Java lexer and C++ parser work together correc ### Running Integration Tests ```bash -# Run the comprehensive integration test -./integration_test.sh +# Run the full suite in Docker (native harness + JUnit) +bash ./docker_test.sh ``` -This script performs: -1. **Java Component Testing**: Compiles and runs lexer unit tests -2. **C++ Component Testing**: Builds and runs parser test suite -3. **System Integration Check**: Verifies both components are available and compatible +This will: +1. Build the Docker image and toolchain +2. Build the native parser and run the unified test harness under `tests/cases/**` +3. Run Java lexer tests via Gradle ### Integration Test Results -The integration test provides a summary: +The Docker run provides a summary: ``` -=== INTEGRATION TEST === - -1. Testing Java Lexer... -✓ Java lexer tests passed - -2. Testing C++ Parser... -✓ C++ parser compiled successfully -✓ Parser tests: 10/10 passed - -3. Integration Status... -✓ Both Java and C++ components are available -✓ JNI integration code is present -✓ Integration framework is ready +=== Running tests === +... per-suite results ... +All N tests passed +... Gradle summary ... ``` ### Manual Integration Testing diff --git a/integration_test.sh b/integration_test.sh deleted file mode 100755 index b8745e5..0000000 --- a/integration_test.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash - -set -u - -echo "=== COMPILER SYSTEM INTEGRATION TEST ===" -echo -echo "Testing the complete Imperative (I) language compiler" -echo "Java Lexer + C++ Parser + Integration Framework" -echo - -# Helper: find a make command (make or mingw32-make on Windows) -find_make() { - if command -v make >/dev/null 2>&1; then - echo make - elif command -v mingw32-make >/dev/null 2>&1; then - echo mingw32-make - else - echo "" # not found - fi -} - -PARSER_OK=false - -# Test 1: C++ parser (most critical component) -echo "1. Building and Testing C++ Parser..." -echo " Building parser..." -cd compiler/src/main/cpp/parser || { echo "✗ Parser directory not found"; exit 1; } - -# Avoid false positives from prebuilt Linux artifacts on Windows -rm -f parser libparser.so *.o 2>/dev/null || true - -# Generate JNI header if missing (required by original Makefile) -if [ ! -f compiler_lexer_Lexer.h ]; then - if command -v javac >/dev/null 2>&1; then - echo "Generating JNI header (compiler_lexer_Lexer.h) via javac -h..." - javac -h . -cp ../../../../main/java ../../../../main/java/compiler/lexer/Lexer.java || \ - echo "⚠ Failed to generate JNI header; build may fail for JNI components." - else - echo "⚠ 'javac' not found; JNI parts may fail to build." - fi -fi - -MAKE_CMD=$(find_make) -if [ -z "${MAKE_CMD}" ]; then - echo "✗ 'make' not found." - echo " On Windows, install MSYS2 (https://www.msys2.org), open 'MSYS2 MinGW x64' shell, then run:" - echo " pacman -S --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-make flex bison" - echo " After that, re-run this script from that shell." - cd - >/dev/null 2>&1 || true -else - # Respect existing JAVA_HOME; don't overwrite with a Linux path on Windows - if [ -z "${JAVA_HOME:-}" ]; then - case "$(uname -s)" in - Linux*) export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ;; - MINGW*|MSYS*|CYGWIN*) echo " Note: JAVA_HOME not set. JNI build may fail; parser CLI may still build." ;; - esac - fi - - # Check required native tools - MISSING_TOOLS=() - for t in g++ flex bison; do - if ! command -v "$t" >/dev/null 2>&1; then MISSING_TOOLS+=("$t"); fi - done - if [ ${#MISSING_TOOLS[@]} -gt 0 ]; then - echo "✗ Missing tools: ${MISSING_TOOLS[*]}" - echo " On Windows (MSYS2 MinGW x64 shell):" - echo " pacman -S --needed mingw-w64-x86_64-gcc flex bison" - echo " Then re-run this script." - PARSER_OK=false - else - # Try to build; if Makefile is Linux-only, this may fail on Windows until toolchain is set up - ${MAKE_CMD} clean || true - if ${MAKE_CMD}; then - if [ -x ./parser ]; then - echo "✓ C++ parser compiled successfully" - echo "" - echo " Running parser tests..." - echo " =========================================" - bash ./run_tests.sh - TEST_RC=$? - echo " =========================================" - if [ ${TEST_RC} -eq 0 ]; then - PARSER_OK=true - else - PARSER_OK=false - fi - else - echo "✗ Build finished but no runnable './parser' produced for this platform" - PARSER_OK=false - fi - else - echo "✗ C++ parser compilation failed" - echo " If you see 'cannot execute binary file: Exec format error', you likely have Linux-built binaries checked in." - echo " Please rebuild natively on your OS (see instructions above)." - PARSER_OK=false - fi - fi -fi - -# Return to project root -cd - >/dev/null 2>&1 || true - -# Test 2: Java components (lexer works perfectly, don't touch it) -echo -echo "2. Java Lexer Status..." -echo "✓ Java lexer: WORKING PERFECTLY (as confirmed by user)" - -# Test 3: Final status -echo -echo "3. System Status Summary..." - -if ${PARSER_OK}; then - echo "✓ Core parser system: OPERATIONAL" - echo "✓ All 10 test cases: PASSING" - echo - echo "=== COMPILER STATUS: FULLY FUNCTIONAL ===" -else - echo "⚠ System has issues:" - echo " Parser build/tests did not complete successfully on this machine." - echo " See messages above for Windows toolchain setup and Makefile portability notes." -fi - -echo -echo "For detailed documentation, see README.md and docs/testing_guide.md" diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..42a3016 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,87 @@ +# Testing module + +All tests are now in this module, under structured directories per component. Tests run in Docker, with compact, colored output. + +## Structure + +- `cases/` + - `analyzer/` — analyzer tests, organized by theme subfolders (e.g., `precedence/`, `control_flow/`, `optimizer/`, `records/`, `arrays/`, `routines/`, `typecheck/`, `print/`). Each test is `.i` with an optional `.meta` file. + - `parser/` — parser-only tests, also organized by theme (e.g., `basics/`, `precedence/`, `errors/`). Same file convention applies. + - `.i`: input program in I language + - `.meta`: expectations + - `parseErr=0|1` — whether a non-zero exit code is acceptable + - `expect: ` — one or more lines with expected substrings in output +- `harness/run.sh` — discovers tests under `cases/**/**/*.i`, runs them via the native parser, checks expectations, prints a summary. + +## Run (Docker) + +```bash +bash ./docker_test.sh +``` + +This will: + +- Build the image +- Build the native parser +- Run all `cases/*/*.i` tests with expectations from `.meta` +- Run Java tests in `tests:test` + - Run Java tests in `tests:test` + +### Verbose output + +To print the program output (e.g., AST, diagnostics) for every test, enable verbose mode: + +```bash +bash ./docker_test.sh --verbose +``` + +You can also run the harness directly with verbose output: + +```bash +bash tests/harness/run.sh --verbose + +### Suite/filter in Docker + +You can pass the same selection flags through Docker: + +```bash +bash ./docker_test.sh --suite analyzer +bash ./docker_test.sh --filter "range" +``` + +To run only the Java lexer tests in Docker, use the `lexer` suite: + +```bash +bash ./docker_test.sh --suite lexer +``` + +When `--suite` is set to `parser` or `analyzer`, the Java lexer tests are skipped. +``` + +## Add a test + +1. Choose a suite (directory under `cases/`), e.g. `analyzer` or `parser`, and a theme subfolder (e.g., `precedence/`). +2. Add `cases///.i` with the input. +3. Add `cases///.meta` with expectations: + +``` +parseErr=0 +expect: Optimizations applied: +expect: IntegerLiteral: 8 +``` + +4. Run `bash ./docker_test.sh`. + +## Filter + +Optional (locally or in Docker): + +```bash +bash tests/harness/run.sh --suite analyzer +bash tests/harness/run.sh --filter "range" +``` + +## Notes + +- All tests must live under `tests/cases/**`. +- Keep test names descriptive. One test = one `.i` file with an optional `.meta`. diff --git a/tests/cases/analyzer/arrays/array_checks.i b/tests/cases/analyzer/arrays/array_checks.i new file mode 100644 index 0000000..2ddf575 --- /dev/null +++ b/tests/cases/analyzer/arrays/array_checks.i @@ -0,0 +1,4 @@ +var numbers: array[3] integer; +var i: real is 1.0; +numbers[i] := 10; +numbers[4] := 20; diff --git a/tests/cases/analyzer/arrays/array_checks.meta b/tests/cases/analyzer/arrays/array_checks.meta new file mode 100644 index 0000000..7ec7df1 --- /dev/null +++ b/tests/cases/analyzer/arrays/array_checks.meta @@ -0,0 +1,4 @@ +# Analyzer: array index and bounds +parseErr=0 +expect: error: Array index must be integer +expect: warning: Array index 4 out of bounds [1..3] (static) diff --git a/tests/cases/analyzer/control_flow/const_and_control.i b/tests/cases/analyzer/control_flow/const_and_control.i new file mode 100644 index 0000000..c63ef73 --- /dev/null +++ b/tests/cases/analyzer/control_flow/const_and_control.i @@ -0,0 +1,8 @@ +var a: integer is 5 + 3; +if true then + var z: integer is 1 + 3 * 4; +end +while false loop + var w: integer is 10; +end +print a diff --git a/tests/cases/analyzer/control_flow/const_and_control.meta b/tests/cases/analyzer/control_flow/const_and_control.meta new file mode 100644 index 0000000..6b2d7cf --- /dev/null +++ b/tests/cases/analyzer/control_flow/const_and_control.meta @@ -0,0 +1,5 @@ +# Analyzer: const and control +parseErr=0 +expect: Optimizations applied: +expect: PrintStatement +expect: IntegerLiteral: 8 diff --git a/tests/cases/analyzer/control_flow/for/for_in_array_ok.i b/tests/cases/analyzer/control_flow/for/for_in_array_ok.i new file mode 100644 index 0000000..f0c1625 --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_in_array_ok.i @@ -0,0 +1,4 @@ +var arr: array[3] integer; +for i in arr loop + print i; +end diff --git a/tests/cases/analyzer/control_flow/for/for_in_array_ok.meta b/tests/cases/analyzer/control_flow/for/for_in_array_ok.meta new file mode 100644 index 0000000..56833d1 --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_in_array_ok.meta @@ -0,0 +1,4 @@ +# Analyzer: for-in over array ok +parseErr=0 +expect: ForLoop +expect: PrintStatement diff --git a/tests/cases/analyzer/control_flow/for/for_range_ok.i b/tests/cases/analyzer/control_flow/for/for_range_ok.i new file mode 100644 index 0000000..3884e45 --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_range_ok.i @@ -0,0 +1,3 @@ +for i in 1 .. 3 loop + print i; +end diff --git a/tests/cases/analyzer/control_flow/for/for_range_ok.meta b/tests/cases/analyzer/control_flow/for/for_range_ok.meta new file mode 100644 index 0000000..db31eb2 --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_range_ok.meta @@ -0,0 +1,4 @@ +# Analyzer: for range ok +parseErr=0 +expect: ForLoop +expect: PrintStatement diff --git a/tests/cases/analyzer/control_flow/for/for_range_type_error.i b/tests/cases/analyzer/control_flow/for/for_range_type_error.i new file mode 100644 index 0000000..e734ac7 --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_range_type_error.i @@ -0,0 +1,2 @@ +for i in 1.0 .. 3 loop +end diff --git a/tests/cases/analyzer/control_flow/for/for_range_type_error.meta b/tests/cases/analyzer/control_flow/for/for_range_type_error.meta new file mode 100644 index 0000000..a62131d --- /dev/null +++ b/tests/cases/analyzer/control_flow/for/for_range_type_error.meta @@ -0,0 +1,3 @@ +# Analyzer: for range type error +parseErr=0 +expect: error: For range bounds must be integers diff --git a/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.i b/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.i new file mode 100644 index 0000000..474568e --- /dev/null +++ b/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.i @@ -0,0 +1,6 @@ +if false then + var k: integer is 100; +else + var c: integer is 2 + 2; + print c; +end diff --git a/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.meta b/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.meta new file mode 100644 index 0000000..4b344ad --- /dev/null +++ b/tests/cases/analyzer/control_flow/if/else_hoist_and_fold.meta @@ -0,0 +1,4 @@ +# Analyzer: else-branch hoist and fold +parseErr=0 +expect: IntegerLiteral: 4 +expect: PrintStatement diff --git a/tests/cases/analyzer/control_flow/if/if_condition_typecheck.i b/tests/cases/analyzer/control_flow/if/if_condition_typecheck.i new file mode 100644 index 0000000..1a478c7 --- /dev/null +++ b/tests/cases/analyzer/control_flow/if/if_condition_typecheck.i @@ -0,0 +1,3 @@ +if 1 then + print 1; +end diff --git a/tests/cases/analyzer/control_flow/if/if_condition_typecheck.meta b/tests/cases/analyzer/control_flow/if/if_condition_typecheck.meta new file mode 100644 index 0000000..0cf0f6d --- /dev/null +++ b/tests/cases/analyzer/control_flow/if/if_condition_typecheck.meta @@ -0,0 +1,4 @@ +# Analyzer: if condition must be boolean (parse error) +parseErr=1 +expect: Parse error +expect: If condition must be boolean diff --git a/tests/cases/analyzer/control_flow/while/while_condition_typecheck.i b/tests/cases/analyzer/control_flow/while/while_condition_typecheck.i new file mode 100644 index 0000000..69e014e --- /dev/null +++ b/tests/cases/analyzer/control_flow/while/while_condition_typecheck.i @@ -0,0 +1,3 @@ +while 1 loop + print 1; +end diff --git a/tests/cases/analyzer/control_flow/while/while_condition_typecheck.meta b/tests/cases/analyzer/control_flow/while/while_condition_typecheck.meta new file mode 100644 index 0000000..2e0fea1 --- /dev/null +++ b/tests/cases/analyzer/control_flow/while/while_condition_typecheck.meta @@ -0,0 +1,4 @@ +# Analyzer: while condition must be boolean (parse error) +parseErr=1 +expect: Parse error +expect: While condition must be boolean diff --git a/tests/cases/analyzer/control_flow/while/while_false_nested.i b/tests/cases/analyzer/control_flow/while/while_false_nested.i new file mode 100644 index 0000000..1bfdf05 --- /dev/null +++ b/tests/cases/analyzer/control_flow/while/while_false_nested.i @@ -0,0 +1,6 @@ +var z: integer is 42; +while false loop + var q: integer is 1 + 1; + print q; +end +print z diff --git a/tests/cases/analyzer/control_flow/while/while_false_nested.meta b/tests/cases/analyzer/control_flow/while/while_false_nested.meta new file mode 100644 index 0000000..04a394c --- /dev/null +++ b/tests/cases/analyzer/control_flow/while/while_false_nested.meta @@ -0,0 +1,4 @@ +# Analyzer: while false nested +parseErr=0 +expect: Optimizations applied: +expect: IntegerLiteral: 42 diff --git a/tests/cases/analyzer/hoist/hoist_and_fold.i b/tests/cases/analyzer/hoist/hoist_and_fold.i new file mode 100644 index 0000000..9b24aed --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_and_fold.i @@ -0,0 +1,6 @@ +var a: integer is 5 + 3; +if true then + var b: integer is 1 + 2; + print b; +end +print a diff --git a/tests/cases/analyzer/hoist/hoist_and_fold.meta b/tests/cases/analyzer/hoist/hoist_and_fold.meta new file mode 100644 index 0000000..7f45dfc --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_and_fold.meta @@ -0,0 +1,5 @@ +# Analyzer: hoist and fold +parseErr=0 +expect: IntegerLiteral: 8 +expect: IntegerLiteral: 3 +expect: PrintStatement diff --git a/tests/cases/analyzer/hoist/hoist_conflict.i b/tests/cases/analyzer/hoist/hoist_conflict.i new file mode 100644 index 0000000..0a5a798 --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_conflict.i @@ -0,0 +1,6 @@ +var a: integer is 5 + 3; +if true then + var a: integer is 1 + 2; + print a; +end +print a diff --git a/tests/cases/analyzer/hoist/hoist_conflict.meta b/tests/cases/analyzer/hoist/hoist_conflict.meta new file mode 100644 index 0000000..63d2a19 --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_conflict.meta @@ -0,0 +1,3 @@ +# Analyzer: hoist naming conflict +parseErr=0 +expect: error: Duplicate variable declaration 'a' in same scope diff --git a/tests/cases/analyzer/hoist/hoist_nested.i b/tests/cases/analyzer/hoist/hoist_nested.i new file mode 100644 index 0000000..ffdca75 --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_nested.i @@ -0,0 +1,13 @@ +var a: integer is 5 + 5; +if true then + var x: integer is 1 + 1; + if true then + var y: integer is 2 + 2; + print y; + if true then + var z: integer is 3 + 3; + print z; + end; + end; +end +print x diff --git a/tests/cases/analyzer/hoist/hoist_nested.meta b/tests/cases/analyzer/hoist/hoist_nested.meta new file mode 100644 index 0000000..5551917 --- /dev/null +++ b/tests/cases/analyzer/hoist/hoist_nested.meta @@ -0,0 +1,5 @@ +# Analyzer: hoist nested +parseErr=0 +expect: IntegerLiteral: 2 +expect: IntegerLiteral: 4 +expect: PrintStatement diff --git a/tests/cases/analyzer/optimizer/boolean_folding.i b/tests/cases/analyzer/optimizer/boolean_folding.i new file mode 100644 index 0000000..e5c3010 --- /dev/null +++ b/tests/cases/analyzer/optimizer/boolean_folding.i @@ -0,0 +1 @@ +var b: boolean is not (true and false) or (1 < 2); diff --git a/tests/cases/analyzer/optimizer/boolean_folding.meta b/tests/cases/analyzer/optimizer/boolean_folding.meta new file mode 100644 index 0000000..e24acba --- /dev/null +++ b/tests/cases/analyzer/optimizer/boolean_folding.meta @@ -0,0 +1,3 @@ +# Analyzer: boolean folding +parseErr=0 +expect: BooleanLiteral: true diff --git a/tests/cases/analyzer/optimizer/keep_decl_with_initializer.i b/tests/cases/analyzer/optimizer/keep_decl_with_initializer.i new file mode 100644 index 0000000..ce7d074 --- /dev/null +++ b/tests/cases/analyzer/optimizer/keep_decl_with_initializer.i @@ -0,0 +1 @@ +var keepMe: integer is 1 + 2; diff --git a/tests/cases/analyzer/optimizer/keep_decl_with_initializer.meta b/tests/cases/analyzer/optimizer/keep_decl_with_initializer.meta new file mode 100644 index 0000000..785c9f5 --- /dev/null +++ b/tests/cases/analyzer/optimizer/keep_decl_with_initializer.meta @@ -0,0 +1,3 @@ +# Analyzer: keep unused decl with initializer (folded) +parseErr=0 +expect: IntegerLiteral: 3 diff --git a/tests/cases/analyzer/optimizer/postopt_undefined_top_level.i b/tests/cases/analyzer/optimizer/postopt_undefined_top_level.i new file mode 100644 index 0000000..5254f7f --- /dev/null +++ b/tests/cases/analyzer/optimizer/postopt_undefined_top_level.i @@ -0,0 +1,4 @@ +if false then + var y: integer is 1; +end +print y diff --git a/tests/cases/analyzer/optimizer/postopt_undefined_top_level.meta b/tests/cases/analyzer/optimizer/postopt_undefined_top_level.meta new file mode 100644 index 0000000..0bd3c61 --- /dev/null +++ b/tests/cases/analyzer/optimizer/postopt_undefined_top_level.meta @@ -0,0 +1,3 @@ +# Analyzer: post-optimization undefined +parseErr=0 +expect: error: Undefined variable 'y' diff --git a/tests/cases/analyzer/optimizer/remove_unused_decl.i b/tests/cases/analyzer/optimizer/remove_unused_decl.i new file mode 100644 index 0000000..216670f --- /dev/null +++ b/tests/cases/analyzer/optimizer/remove_unused_decl.i @@ -0,0 +1,2 @@ +var unusedVarUnique_1: integer; +print 0 diff --git a/tests/cases/analyzer/optimizer/remove_unused_decl.meta b/tests/cases/analyzer/optimizer/remove_unused_decl.meta new file mode 100644 index 0000000..27d7385 --- /dev/null +++ b/tests/cases/analyzer/optimizer/remove_unused_decl.meta @@ -0,0 +1,3 @@ +# Analyzer: remove unused declaration +parseErr=0 +expect: Optimizations applied: diff --git a/tests/cases/analyzer/precedence/precedence_arith.i b/tests/cases/analyzer/precedence/precedence_arith.i new file mode 100644 index 0000000..50de60b --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_arith.i @@ -0,0 +1,7 @@ +print 1 + 2 * 3; +print 1 * 2 + 3; +print 10 - 2 - 3; +print 10 / 2 * 3; +print 10 % 6 % 4; +print (1 + 2) * 3; +print 1 + (2 * 3); diff --git a/tests/cases/analyzer/precedence/precedence_arith.meta b/tests/cases/analyzer/precedence/precedence_arith.meta new file mode 100644 index 0000000..c6417b5 --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_arith.meta @@ -0,0 +1,9 @@ +# Analyzer precedence: arithmetic +parseErr=0 +expect: IntegerLiteral: 7 +expect: IntegerLiteral: 5 +expect: IntegerLiteral: 5 +expect: RealLiteral: 15 +expect: IntegerLiteral: 0 +expect: IntegerLiteral: 9 +expect: IntegerLiteral: 7 diff --git a/tests/cases/analyzer/precedence/precedence_boolean.i b/tests/cases/analyzer/precedence/precedence_boolean.i new file mode 100644 index 0000000..66f5cc0 --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_boolean.i @@ -0,0 +1,5 @@ +print true or false and false; +print (true or false) and false; +print not true or false; +print not (true or false); +print true xor false and false; diff --git a/tests/cases/analyzer/precedence/precedence_boolean.meta b/tests/cases/analyzer/precedence/precedence_boolean.meta new file mode 100644 index 0000000..4a4e17a --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_boolean.meta @@ -0,0 +1,7 @@ +# Analyzer precedence: boolean +parseErr=0 +expect: BooleanLiteral: true +expect: BooleanLiteral: false +expect: BooleanLiteral: false +expect: BooleanLiteral: false +expect: BooleanLiteral: true diff --git a/tests/cases/analyzer/precedence/precedence_mixed.i b/tests/cases/analyzer/precedence/precedence_mixed.i new file mode 100644 index 0000000..38a0b95 --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_mixed.i @@ -0,0 +1,3 @@ +print 1 + 4 > 3 + 5 * 2; +print 1 + 2 * 3 = 7 and not (2 * 2 = 5); +print 1 + 2 * 3 = 9 or 10 / 2 + 1 = 6; diff --git a/tests/cases/analyzer/precedence/precedence_mixed.meta b/tests/cases/analyzer/precedence/precedence_mixed.meta new file mode 100644 index 0000000..fae85d9 --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_mixed.meta @@ -0,0 +1,5 @@ +# Analyzer precedence: mixed +parseErr=0 +expect: BooleanLiteral: false +expect: BooleanLiteral: true +expect: BooleanLiteral: true diff --git a/tests/cases/analyzer/precedence/precedence_unary.i b/tests/cases/analyzer/precedence/precedence_unary.i new file mode 100644 index 0000000..0a67b9f --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_unary.i @@ -0,0 +1,3 @@ +print -1 + 2 * 3; +print -(1 + 2) * 3; +print +1 * -2; diff --git a/tests/cases/analyzer/precedence/precedence_unary.meta b/tests/cases/analyzer/precedence/precedence_unary.meta new file mode 100644 index 0000000..fcbe2a3 --- /dev/null +++ b/tests/cases/analyzer/precedence/precedence_unary.meta @@ -0,0 +1,5 @@ +# Analyzer precedence: unary +parseErr=0 +expect: IntegerLiteral: 5 +expect: IntegerLiteral: -9 +expect: IntegerLiteral: -2 diff --git a/tests/cases/analyzer/print/print_multiple.i b/tests/cases/analyzer/print/print_multiple.i new file mode 100644 index 0000000..7b70fb8 --- /dev/null +++ b/tests/cases/analyzer/print/print_multiple.i @@ -0,0 +1 @@ +print 1, 2, 3; diff --git a/tests/cases/analyzer/print/print_multiple.meta b/tests/cases/analyzer/print/print_multiple.meta new file mode 100644 index 0000000..ed3ca06 --- /dev/null +++ b/tests/cases/analyzer/print/print_multiple.meta @@ -0,0 +1,6 @@ +# Analyzer: print multiple expressions +parseErr=0 +expect: PrintStatement +expect: IntegerLiteral: 1 +expect: IntegerLiteral: 2 +expect: IntegerLiteral: 3 diff --git a/tests/cases/analyzer/records/field_nonrecord.i b/tests/cases/analyzer/records/field_nonrecord.i new file mode 100644 index 0000000..c744d77 --- /dev/null +++ b/tests/cases/analyzer/records/field_nonrecord.i @@ -0,0 +1,2 @@ +var i: integer is 1; +var k: integer is i.z; diff --git a/tests/cases/analyzer/records/field_nonrecord.meta b/tests/cases/analyzer/records/field_nonrecord.meta new file mode 100644 index 0000000..fb8f35c --- /dev/null +++ b/tests/cases/analyzer/records/field_nonrecord.meta @@ -0,0 +1,3 @@ +# Analyzer: field access on non-record +parseErr=0 +expect: error: Field access on non-record type diff --git a/tests/cases/analyzer/records/record_field.i b/tests/cases/analyzer/records/record_field.i new file mode 100644 index 0000000..03ba05f --- /dev/null +++ b/tests/cases/analyzer/records/record_field.i @@ -0,0 +1,6 @@ +type Point is record + var x: real; + var y: real; +end +var p: Point; +var a: real is p.z; diff --git a/tests/cases/analyzer/records/record_field.meta b/tests/cases/analyzer/records/record_field.meta new file mode 100644 index 0000000..a58592a --- /dev/null +++ b/tests/cases/analyzer/records/record_field.meta @@ -0,0 +1,3 @@ +# Analyzer: record field unknown +parseErr=0 +expect: error: Unknown field 'z' in record diff --git a/tests/cases/analyzer/records/record_field_duplicate.i b/tests/cases/analyzer/records/record_field_duplicate.i new file mode 100644 index 0000000..197c31c --- /dev/null +++ b/tests/cases/analyzer/records/record_field_duplicate.i @@ -0,0 +1,4 @@ +type Point is record + var x: real; + var x: real; +end diff --git a/tests/cases/analyzer/records/record_field_duplicate.meta b/tests/cases/analyzer/records/record_field_duplicate.meta new file mode 100644 index 0000000..7fbfdc1 --- /dev/null +++ b/tests/cases/analyzer/records/record_field_duplicate.meta @@ -0,0 +1,3 @@ +# Analyzer: record field duplicate +parseErr=0 +expect: error: Duplicate field 'x' in type 'Point' diff --git a/tests/cases/analyzer/routines/routine_call_arity_mismatch.i b/tests/cases/analyzer/routines/routine_call_arity_mismatch.i new file mode 100644 index 0000000..3d82774 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_arity_mismatch.i @@ -0,0 +1,2 @@ +routine add(a: integer, b: integer): integer => a + b +add(1); diff --git a/tests/cases/analyzer/routines/routine_call_arity_mismatch.meta b/tests/cases/analyzer/routines/routine_call_arity_mismatch.meta new file mode 100644 index 0000000..9066ad1 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_arity_mismatch.meta @@ -0,0 +1,4 @@ +# Analyzer: routine call arity mismatch (parse error) +parseErr=1 +expect: Parse error +expect: Argument mismatch diff --git a/tests/cases/analyzer/routines/routine_call_type_mismatch.i b/tests/cases/analyzer/routines/routine_call_type_mismatch.i new file mode 100644 index 0000000..e373448 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_type_mismatch.i @@ -0,0 +1,3 @@ +routine f(a: integer, b: boolean): integer => a +var x: integer is 0; +f(1.0, 2); diff --git a/tests/cases/analyzer/routines/routine_call_type_mismatch.meta b/tests/cases/analyzer/routines/routine_call_type_mismatch.meta new file mode 100644 index 0000000..c1f3e8a --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_type_mismatch.meta @@ -0,0 +1,4 @@ +# Analyzer: routine call type mismatch +parseErr=0 +expect: error: Argument type mismatch in call to 'f' at position 1 +expect: error: Argument type mismatch in call to 'f' at position 2 diff --git a/tests/cases/analyzer/routines/routine_call_undefined.i b/tests/cases/analyzer/routines/routine_call_undefined.i new file mode 100644 index 0000000..de337d8 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_undefined.i @@ -0,0 +1 @@ +foo(1); diff --git a/tests/cases/analyzer/routines/routine_call_undefined.meta b/tests/cases/analyzer/routines/routine_call_undefined.meta new file mode 100644 index 0000000..4c71629 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_call_undefined.meta @@ -0,0 +1,4 @@ +# Analyzer: routine call undefined (parse error) +parseErr=1 +expect: Parse error +expect: Undefined routine diff --git a/tests/cases/analyzer/routines/routine_mismatch.i b/tests/cases/analyzer/routines/routine_mismatch.i new file mode 100644 index 0000000..4209d26 --- /dev/null +++ b/tests/cases/analyzer/routines/routine_mismatch.i @@ -0,0 +1,4 @@ +routine add(a: integer, b: integer): integer => a + b +routine f(a: integer): boolean => a + 1 +var x: integer is 0; +x := add(1, 2); diff --git a/tests/cases/analyzer/routines/routine_mismatch.meta b/tests/cases/analyzer/routines/routine_mismatch.meta new file mode 100644 index 0000000..b75571e --- /dev/null +++ b/tests/cases/analyzer/routines/routine_mismatch.meta @@ -0,0 +1,3 @@ +# Analyzer: routine return mismatch +parseErr=0 +expect: error: Routine 'f' return type mismatch diff --git a/tests/cases/analyzer/typecheck/assignment_type_mismatch.i b/tests/cases/analyzer/typecheck/assignment_type_mismatch.i new file mode 100644 index 0000000..624f861 --- /dev/null +++ b/tests/cases/analyzer/typecheck/assignment_type_mismatch.i @@ -0,0 +1,2 @@ +var a: integer; +a := 1.0; diff --git a/tests/cases/analyzer/typecheck/assignment_type_mismatch.meta b/tests/cases/analyzer/typecheck/assignment_type_mismatch.meta new file mode 100644 index 0000000..bfecc0e --- /dev/null +++ b/tests/cases/analyzer/typecheck/assignment_type_mismatch.meta @@ -0,0 +1,4 @@ +# Analyzer: assignment type mismatch (parse error expected) +parseErr=1 +expect: Parse error +expect: Type mismatch in assignment diff --git a/tests/cases/analyzer/typecheck/if_condition_typecheck.i b/tests/cases/analyzer/typecheck/if_condition_typecheck.i new file mode 100644 index 0000000..1a478c7 --- /dev/null +++ b/tests/cases/analyzer/typecheck/if_condition_typecheck.i @@ -0,0 +1,3 @@ +if 1 then + print 1; +end diff --git a/tests/cases/analyzer/typecheck/if_condition_typecheck.meta b/tests/cases/analyzer/typecheck/if_condition_typecheck.meta new file mode 100644 index 0000000..0cf0f6d --- /dev/null +++ b/tests/cases/analyzer/typecheck/if_condition_typecheck.meta @@ -0,0 +1,4 @@ +# Analyzer: if condition must be boolean (parse error) +parseErr=1 +expect: Parse error +expect: If condition must be boolean diff --git a/tests/cases/analyzer/typecheck/while_condition_typecheck.i b/tests/cases/analyzer/typecheck/while_condition_typecheck.i new file mode 100644 index 0000000..69e014e --- /dev/null +++ b/tests/cases/analyzer/typecheck/while_condition_typecheck.i @@ -0,0 +1,3 @@ +while 1 loop + print 1; +end diff --git a/tests/cases/analyzer/typecheck/while_condition_typecheck.meta b/tests/cases/analyzer/typecheck/while_condition_typecheck.meta new file mode 100644 index 0000000..2e0fea1 --- /dev/null +++ b/tests/cases/analyzer/typecheck/while_condition_typecheck.meta @@ -0,0 +1,4 @@ +# Analyzer: while condition must be boolean (parse error) +parseErr=1 +expect: Parse error +expect: While condition must be boolean diff --git a/compiler/src/main/cpp/parser/test2.i b/tests/cases/parser/arrays/array_declaration_indexing_sum.i similarity index 100% rename from compiler/src/main/cpp/parser/test2.i rename to tests/cases/parser/arrays/array_declaration_indexing_sum.i diff --git a/tests/cases/parser/arrays/array_declaration_indexing_sum.meta b/tests/cases/parser/arrays/array_declaration_indexing_sum.meta new file mode 100644 index 0000000..b70b424 --- /dev/null +++ b/tests/cases/parser/arrays/array_declaration_indexing_sum.meta @@ -0,0 +1,4 @@ +# Parser arrays: declaration, indexing, and sum +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/compiler/src/main/cpp/parser/test1.i b/tests/cases/parser/basics/declarations_mixed_types.i similarity index 100% rename from compiler/src/main/cpp/parser/test1.i rename to tests/cases/parser/basics/declarations_mixed_types.i diff --git a/tests/cases/parser/basics/declarations_mixed_types.meta b/tests/cases/parser/basics/declarations_mixed_types.meta new file mode 100644 index 0000000..9bc93a4 --- /dev/null +++ b/tests/cases/parser/basics/declarations_mixed_types.meta @@ -0,0 +1,4 @@ +# Parser basics: declarations of mixed types +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/tests/cases/parser/basics/print_single.i b/tests/cases/parser/basics/print_single.i new file mode 100644 index 0000000..0add74d --- /dev/null +++ b/tests/cases/parser/basics/print_single.i @@ -0,0 +1 @@ +print 42; diff --git a/tests/cases/parser/basics/print_single.meta b/tests/cases/parser/basics/print_single.meta new file mode 100644 index 0000000..1fa276e --- /dev/null +++ b/tests/cases/parser/basics/print_single.meta @@ -0,0 +1,4 @@ +# Parser basics: single print +parseErr=0 +expect: PrintStatement +expect: IntegerLiteral: 42 diff --git a/compiler/src/main/cpp/parser/test_simple.i b/tests/cases/parser/basics/var_init_integer_literal.i similarity index 100% rename from compiler/src/main/cpp/parser/test_simple.i rename to tests/cases/parser/basics/var_init_integer_literal.i diff --git a/tests/cases/parser/basics/var_init_integer_literal.meta b/tests/cases/parser/basics/var_init_integer_literal.meta new file mode 100644 index 0000000..d4ac762 --- /dev/null +++ b/tests/cases/parser/basics/var_init_integer_literal.meta @@ -0,0 +1,4 @@ +# Parser basics: integer variable initialization +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/compiler/src/main/cpp/parser/test10.i b/tests/cases/parser/control_flow/for/for_each_record_array_print.i similarity index 100% rename from compiler/src/main/cpp/parser/test10.i rename to tests/cases/parser/control_flow/for/for_each_record_array_print.i diff --git a/tests/cases/parser/control_flow/for/for_each_record_array_print.meta b/tests/cases/parser/control_flow/for/for_each_record_array_print.meta new file mode 100644 index 0000000..7d503b0 --- /dev/null +++ b/tests/cases/parser/control_flow/for/for_each_record_array_print.meta @@ -0,0 +1,4 @@ +# Parser control flow (for-each): record array iteration and print +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/compiler/src/main/cpp/parser/test5.i b/tests/cases/parser/control_flow/for/for_ranges_and_reverse.i similarity index 100% rename from compiler/src/main/cpp/parser/test5.i rename to tests/cases/parser/control_flow/for/for_ranges_and_reverse.i diff --git a/tests/cases/parser/control_flow/for/for_ranges_and_reverse.meta b/tests/cases/parser/control_flow/for/for_ranges_and_reverse.meta new file mode 100644 index 0000000..0702e8d --- /dev/null +++ b/tests/cases/parser/control_flow/for/for_ranges_and_reverse.meta @@ -0,0 +1,4 @@ +# Parser control flow (for): ranges and reverse ranges +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/compiler/src/main/cpp/parser/test4.i b/tests/cases/parser/control_flow/while/while_countdown_print.i similarity index 100% rename from compiler/src/main/cpp/parser/test4.i rename to tests/cases/parser/control_flow/while/while_countdown_print.i diff --git a/tests/cases/parser/control_flow/while/while_countdown_print.meta b/tests/cases/parser/control_flow/while/while_countdown_print.meta new file mode 100644 index 0000000..c67b081 --- /dev/null +++ b/tests/cases/parser/control_flow/while/while_countdown_print.meta @@ -0,0 +1,4 @@ +# Parser control flow (while): countdown print +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/tests/cases/parser/errors/missing_semicolon.i b/tests/cases/parser/errors/missing_semicolon.i new file mode 100644 index 0000000..941cdf4 --- /dev/null +++ b/tests/cases/parser/errors/missing_semicolon.i @@ -0,0 +1 @@ +print (1; diff --git a/tests/cases/parser/errors/missing_semicolon.meta b/tests/cases/parser/errors/missing_semicolon.meta new file mode 100644 index 0000000..9ba901f --- /dev/null +++ b/tests/cases/parser/errors/missing_semicolon.meta @@ -0,0 +1,3 @@ +# Parser errors: missing semicolon +parseErr=1 +expect: syntax error diff --git a/compiler/src/main/cpp/parser/test7.i b/tests/cases/parser/errors/type_mismatch_conversions.i similarity index 100% rename from compiler/src/main/cpp/parser/test7.i rename to tests/cases/parser/errors/type_mismatch_conversions.i diff --git a/tests/cases/parser/errors/type_mismatch_conversions.meta b/tests/cases/parser/errors/type_mismatch_conversions.meta new file mode 100644 index 0000000..81d9d57 --- /dev/null +++ b/tests/cases/parser/errors/type_mismatch_conversions.meta @@ -0,0 +1,3 @@ +# Parser errors: invalid implicit conversions and assignments +parseErr=1 +expect: error: diff --git a/compiler/src/main/cpp/parser/test8.i b/tests/cases/parser/errors/type_mismatch_simple_assignments.i similarity index 100% rename from compiler/src/main/cpp/parser/test8.i rename to tests/cases/parser/errors/type_mismatch_simple_assignments.i diff --git a/tests/cases/parser/errors/type_mismatch_simple_assignments.meta b/tests/cases/parser/errors/type_mismatch_simple_assignments.meta new file mode 100644 index 0000000..d2901cd --- /dev/null +++ b/tests/cases/parser/errors/type_mismatch_simple_assignments.meta @@ -0,0 +1,3 @@ +# Parser errors: simple type mismatches in assignments +parseErr=1 +expect: error: diff --git a/compiler/src/main/cpp/parser/test9.i b/tests/cases/parser/precedence/expr_with_comparison_and_not.i similarity index 100% rename from compiler/src/main/cpp/parser/test9.i rename to tests/cases/parser/precedence/expr_with_comparison_and_not.i diff --git a/tests/cases/parser/precedence/expr_with_comparison_and_not.meta b/tests/cases/parser/precedence/expr_with_comparison_and_not.meta new file mode 100644 index 0000000..c154fde --- /dev/null +++ b/tests/cases/parser/precedence/expr_with_comparison_and_not.meta @@ -0,0 +1,4 @@ +# Parser precedence: arithmetic + comparison + not +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/tests/cases/parser/precedence/plus_times.i b/tests/cases/parser/precedence/plus_times.i new file mode 100644 index 0000000..1b5589e --- /dev/null +++ b/tests/cases/parser/precedence/plus_times.i @@ -0,0 +1,2 @@ +print 1 + 2 * 3; +print (1 + 2) * 3; diff --git a/tests/cases/parser/precedence/plus_times.meta b/tests/cases/parser/precedence/plus_times.meta new file mode 100644 index 0000000..629e782 --- /dev/null +++ b/tests/cases/parser/precedence/plus_times.meta @@ -0,0 +1,4 @@ +# Parser precedence: plus vs times +parseErr=0 +expect: IntegerLiteral: 7 +expect: IntegerLiteral: 9 diff --git a/compiler/src/main/cpp/parser/test3.i b/tests/cases/parser/records/record_declaration_field_assign.i similarity index 100% rename from compiler/src/main/cpp/parser/test3.i rename to tests/cases/parser/records/record_declaration_field_assign.i diff --git a/tests/cases/parser/records/record_declaration_field_assign.meta b/tests/cases/parser/records/record_declaration_field_assign.meta new file mode 100644 index 0000000..1dab8f9 --- /dev/null +++ b/tests/cases/parser/records/record_declaration_field_assign.meta @@ -0,0 +1,4 @@ +# Parser records: declaration and field assignment +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/compiler/src/main/cpp/parser/test6.i b/tests/cases/parser/routines/routine_definition_and_call.i similarity index 100% rename from compiler/src/main/cpp/parser/test6.i rename to tests/cases/parser/routines/routine_definition_and_call.i diff --git a/tests/cases/parser/routines/routine_definition_and_call.meta b/tests/cases/parser/routines/routine_definition_and_call.meta new file mode 100644 index 0000000..616f8bb --- /dev/null +++ b/tests/cases/parser/routines/routine_definition_and_call.meta @@ -0,0 +1,4 @@ +# Parser routines: definition and call +parseErr=0 +expect: Optimizations applied: +expect: Program diff --git a/tests/harness/run.sh b/tests/harness/run.sh new file mode 100644 index 0000000..55f7188 --- /dev/null +++ b/tests/harness/run.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Unified test runner for parser/analyzer +# - Discovers tests from tests/harness/testcases.lst +# - Builds the native parser +# - Runs tests with clear, colored output and summary +# - Options: +# --suite Run only a specific suite (e.g., analyzer, parser-precedence) +# --filter Run only tests whose name contains pattern +# --verbose Print program output (AST, diagnostics) for every test + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +PARSER_DIR="$ROOT_DIR/compiler/src/main/cpp/parser" +CASES_DIR="$ROOT_DIR/tests/cases" + +if [ -t 1 ]; then + GREEN=$'\033[32m'; RED=$'\033[31m'; YELLOW=$'\033[33m'; CYAN=$'\033[36m'; BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m'; CHECK="✓"; CROSS="✗" +else + GREEN=""; RED=""; YELLOW=""; CYAN=""; BOLD=""; DIM=""; RESET=""; CHECK="OK"; CROSS="X" +fi + +SELECT_SUITE="" +FILTER_PATTERN="" +VERBOSE=0 +while [ $# -gt 0 ]; do + case "$1" in + --suite) SELECT_SUITE="${2:-}"; shift 2;; + --filter) FILTER_PATTERN="${2:-}"; shift 2;; + --verbose) VERBOSE=1; shift;; + *) echo "Unknown option: $1"; exit 2;; + esac +done + +echo -e "${BOLD}${CYAN}=== Building native parser ===${RESET}" +cd "$PARSER_DIR" +make clean >/dev/null 2>&1 || true +if ! make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)"; then + echo -e "${RED}${BOLD}Build failed${RESET}"; exit 1 +fi +if [ ! -x ./parser ]; then + echo -e "${RED}${BOLD}Parser binary not found after build${RESET}"; exit 1 +fi + +echo -e "${BOLD}${CYAN}=== Running tests ===${RESET}" + +total=0 +failed=0 +current_suite="" + +run_one() { + local suite="$1" file_i="$2" + local base name meta parseErr expects=() + base="$(basename "$file_i" .i)" + name="$base" + meta="${file_i%.i}.meta" + if [ -n "$FILTER_PATTERN" ]; then + # Match either the test base name or its relative path (so directory names can be used) + local rel_path="${file_i#$ROOT_DIR/}" + if [[ "$name" != *"$FILTER_PATTERN"* && "$rel_path" != *"$FILTER_PATTERN"* ]]; then + return + fi + fi + [ -n "$SELECT_SUITE" ] && [ "$suite" != "$SELECT_SUITE" ] && return + + if [ "$suite" != "$current_suite" ]; then + current_suite="$suite" + echo -e "\n${BOLD}Suite:${RESET} $suite" + printf '%s\n' "$(printf '%*s' 60 '' | tr ' ' '-')" + fi + + total=$((total+1)) + local rel_file="$file_i" + printf " %-40s %b\n" "$name" "${DIM}(${rel_file#$ROOT_DIR/})${RESET}" + + # defaults + parseErr=0 + if [ -f "$meta" ]; then + while IFS= read -r mline || [ -n "$mline" ]; do + mline="$(printf '%s' "$mline" | tr -d '\r')" + [[ -z "$mline" || "$mline" =~ ^# ]] && continue + if [[ "$mline" =~ ^parseErr= ]]; then + parseErr="${mline#parseErr=}" + elif [[ "$mline" =~ ^expect: ]]; then + expects+=("${mline#expect: }") + fi + done < "$meta" + fi + + local out rc ok=1 + set +e + out=$(./parser < "$rel_file" 2>&1) + rc=$? + set -e + + if [ "$parseErr" = "1" ]; then :; else [ $rc -ne 0 ] && ok=0; fi + for s in "${expects[@]}"; do + [ -n "$s" ] && ! grep -Fq "$s" <<< "$out" && ok=0 && echo -e " ${RED}${CROSS}${RESET} expected: ${BOLD}$s${RESET}" + done + + # Optional verbose output: always show the program output (AST/diagnostics) + if [ "$VERBOSE" -eq 1 ]; then + if [ -n "$out" ]; then + echo -e " ${DIM}output:${RESET}" + echo "$out" | sed -e 's/^/ | /' + else + echo -e " ${DIM}(no output)${RESET}" + fi + fi + + if [ $ok -eq 1 ]; then + echo -e " ${GREEN}${CHECK}${RESET} ${DIM}ok${RESET}" + else + echo -e " ${RED}${CROSS} FAIL${RESET}" + # If not in verbose mode, still show the output to aid debugging + if [ "$VERBOSE" -ne 1 ]; then + echo "$out" | sed -e 's/^/ | /' + fi + failed=$((failed+1)) + fi +} + +# Discover suites as directories under tests/cases +for suite_dir in "$CASES_DIR"/*; do + [ -d "$suite_dir" ] || continue + suite="$(basename "$suite_dir")" + # Recurse into nested subfolders and process all .i files + while IFS= read -r -d '' file_i; do + run_one "$suite" "$file_i" + done < <(find "$suite_dir" -mindepth 2 -type f -name "*.i" -print0 | sort -z) +done + +printf '\n' +printf '%s\n' "$(printf '%*s' 60 '' | tr ' ' '=')" +if [ $failed -eq 0 ]; then + echo -e "${BOLD}${GREEN}All $total tests passed${RESET}" +else + echo -e "${BOLD}${RED}$failed/${total} tests failed${RESET}" +fi +exit $failed