From c7fdbb9cfa662a0f8330b20fa587127392339653 Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:55:49 +0100 Subject: [PATCH 1/6] Fix: evaluate call receiver expression in IR Bug: MethodCallNode and MethodCallWithoutArgumentsNode used only type info for the receiver and never generated receiver IR. This dropped side effects and broke chained calls in cfg.dot. Also the printed call arity included an implicit receiver that is never emitted as a param, so call metadata was inconsistent.\n\nFix: generate IR for the receiver before arguments and make the printed arity match emitted params. --- src/ast/MethodCallNode.cpp | 3 ++- src/ast/MethodCallWithoutArgumentsNode.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ast/MethodCallNode.cpp b/src/ast/MethodCallNode.cpp index abc4e26..9a7b357 100644 --- a/src/ast/MethodCallNode.cpp +++ b/src/ast/MethodCallNode.cpp @@ -23,6 +23,7 @@ Operand MethodCallNode::generateIR(CFG &graph, SymbolTable &st) { } const auto &methodType = method->getType(); + object->generateIR(graph, st); for (const auto &arg : exprList->children) { const auto &argName = arg->generateIR(graph, st); @@ -33,7 +34,7 @@ Operand MethodCallNode::generateIR(CFG &graph, SymbolTable &st) { st.addVariable(methodType, name); const auto &methodName = id->value; - const auto argCount = std::to_string(exprList->children.size() + 1); + const auto argCount = std::to_string(exprList->children.size()); graph.addInstruction( new MethodCallTac(name, methodName, *caller_type, argCount)); return name; diff --git a/src/ast/MethodCallWithoutArgumentsNode.cpp b/src/ast/MethodCallWithoutArgumentsNode.cpp index 218e47c..ad7eb0e 100644 --- a/src/ast/MethodCallWithoutArgumentsNode.cpp +++ b/src/ast/MethodCallWithoutArgumentsNode.cpp @@ -21,11 +21,12 @@ Operand MethodCallWithoutArgumentsNode::generateIR(CFG &graph, } const auto &methodType = method->getType(); + object->generateIR(graph, st); const auto &name = graph.getTemporaryName(); st.addVariable(methodType, name); const auto &methodName = id->value; graph.addInstruction( - new MethodCallTac(name, methodName, *caller_type, "1")); + new MethodCallTac(name, methodName, *caller_type, "0")); return name; } From 7a9cb08f93e77ced2013470eb095ed0d5d0614e7 Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:56:09 +0100 Subject: [PATCH 2/6] Fix: generate IR for array expression operands Bug: ArrayAccessNode and ArrayLengthNode pulled the array name from node.value instead of evaluating the array expression. This produced malformed IR for cases like new int[3][0] and new int[3].length, and skipped array-expression side effects.\n\nFix: call array->generateIR(...) and use its operand for array access and length TAC. --- src/ast/ArrayAccessNode.cpp | 2 +- src/ast/ArrayLengthNode.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/ArrayAccessNode.cpp b/src/ast/ArrayAccessNode.cpp index 1300de6..81f0f29 100644 --- a/src/ast/ArrayAccessNode.cpp +++ b/src/ast/ArrayAccessNode.cpp @@ -2,8 +2,8 @@ #include "ir/Tac.hpp" Operand ArrayAccessNode::generateIR(CFG &graph, SymbolTable &st) { + auto arrayName = array->generateIR(graph, st); auto indexName = index->generateIR(graph, st); - auto arrayName = array->value; auto name = graph.getTemporaryName(); st.addIntegerVariable(name); graph.addInstruction(new ArrayAccessTac(name, arrayName, indexName)); diff --git a/src/ast/ArrayLengthNode.cpp b/src/ast/ArrayLengthNode.cpp index 9659ff2..6996c10 100644 --- a/src/ast/ArrayLengthNode.cpp +++ b/src/ast/ArrayLengthNode.cpp @@ -4,7 +4,7 @@ Operand ArrayLengthNode::generateIR(CFG &graph, SymbolTable &st) { auto name = graph.getTemporaryName(); st.addIntegerVariable(name); - auto arrayName = array->value; + auto arrayName = array->generateIR(graph, st); graph.addInstruction(new ArrayLengthTac(name, arrayName)); return name; } From d156fd47c2b1a2e0b0016f42b1d903a4e1e6578d Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:56:37 +0100 Subject: [PATCH 3/6] Fix: short-circuit IR for && and || Bug: AndNode/OrNode eagerly evaluated both operands and emitted boolean TAC, violating short-circuit semantics. This could execute RHS effects or faults even when LHS decides the result.\n\nFix: generate explicit control-flow blocks for RHS evaluation and true/false assignment, then join with the computed temporary boolean. --- src/ast/BooleanExpressionNode.cpp | 70 +++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/ast/BooleanExpressionNode.cpp b/src/ast/BooleanExpressionNode.cpp index dd394a9..39400dd 100644 --- a/src/ast/BooleanExpressionNode.cpp +++ b/src/ast/BooleanExpressionNode.cpp @@ -1,20 +1,76 @@ #include "ast/BooleanExpressionNode.hpp" -#include "ir/BooleanTac.hpp" +#include "ir/Tac.hpp" Operand AndNode::generateIR(CFG &graph, SymbolTable &st) { - auto lhs_name = left->generateIR(graph, st); - auto rhs_name = right->generateIR(graph, st); + auto lhsName = left->generateIR(graph, st); auto name = graph.getTemporaryName(); st.addBooleanVariable(name); - graph.addInstruction(new AndTac(name, lhs_name, rhs_name)); + + auto *rhsEvalBlock = graph.newBlock(); + auto *trueBlock = graph.newBlock(); + auto *falseBlock = graph.newBlock(); + auto *joinBlock = graph.newBlock(); + + auto *entryBlock = graph.getCurrentBlock(); + graph.addInstruction(new CondJumpTac(falseBlock->getName(), lhsName)); + graph.addInstruction(new JumpTac(rhsEvalBlock->getName())); + entryBlock->setTrueBlock(rhsEvalBlock); + entryBlock->setFalseBlock(falseBlock); + + graph.setCurrentBlock(rhsEvalBlock); + auto rhsName = right->generateIR(graph, st); + graph.addInstruction(new CondJumpTac(falseBlock->getName(), rhsName)); + graph.addInstruction(new JumpTac(trueBlock->getName())); + rhsEvalBlock->setTrueBlock(trueBlock); + rhsEvalBlock->setFalseBlock(falseBlock); + + graph.setCurrentBlock(trueBlock); + graph.addInstruction(new CopyTac(1, name)); + graph.addInstruction(new JumpTac(joinBlock->getName())); + trueBlock->setTrueBlock(joinBlock); + + graph.setCurrentBlock(falseBlock); + graph.addInstruction(new CopyTac(0, name)); + graph.addInstruction(new JumpTac(joinBlock->getName())); + falseBlock->setTrueBlock(joinBlock); + + graph.setCurrentBlock(joinBlock); return name; } Operand OrNode::generateIR(CFG &graph, SymbolTable &st) { - auto lhs_name = left->generateIR(graph, st); - auto rhs_name = right->generateIR(graph, st); + auto lhsName = left->generateIR(graph, st); auto name = graph.getTemporaryName(); st.addBooleanVariable(name); - graph.addInstruction(new OrTac(name, lhs_name, rhs_name)); + + auto *rhsEvalBlock = graph.newBlock(); + auto *trueBlock = graph.newBlock(); + auto *falseBlock = graph.newBlock(); + auto *joinBlock = graph.newBlock(); + + auto *entryBlock = graph.getCurrentBlock(); + graph.addInstruction(new CondJumpTac(rhsEvalBlock->getName(), lhsName)); + graph.addInstruction(new JumpTac(trueBlock->getName())); + entryBlock->setTrueBlock(trueBlock); + entryBlock->setFalseBlock(rhsEvalBlock); + + graph.setCurrentBlock(rhsEvalBlock); + auto rhsName = right->generateIR(graph, st); + graph.addInstruction(new CondJumpTac(falseBlock->getName(), rhsName)); + graph.addInstruction(new JumpTac(trueBlock->getName())); + rhsEvalBlock->setTrueBlock(trueBlock); + rhsEvalBlock->setFalseBlock(falseBlock); + + graph.setCurrentBlock(trueBlock); + graph.addInstruction(new CopyTac(1, name)); + graph.addInstruction(new JumpTac(joinBlock->getName())); + trueBlock->setTrueBlock(joinBlock); + + graph.setCurrentBlock(falseBlock); + graph.addInstruction(new CopyTac(0, name)); + graph.addInstruction(new JumpTac(joinBlock->getName())); + falseBlock->setTrueBlock(joinBlock); + + graph.setCurrentBlock(joinBlock); return name; } From 8e56c50e8528fe65dca96ec3eac7eb70b86805bd Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:57:00 +0100 Subject: [PATCH 4/6] Fix: preserve if-else join edges after nested branches Bug: IfElseNode wired join edges from the initial true/false blocks instead of the actual blocks active after generating each branch body. With nested control flow this could orphan the outer join block from CFG traversal and bytecode generation.\n\nFix: set branch-to-join edges from graph.getCurrentBlock() after each branch body is generated. Also removed an incorrect while-edge assignment that rewired the preheader instead of the loop body exit. --- src/ast/ControlStatementNode.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ast/ControlStatementNode.cpp b/src/ast/ControlStatementNode.cpp index 101888e..98702e2 100644 --- a/src/ast/ControlStatementNode.cpp +++ b/src/ast/ControlStatementNode.cpp @@ -15,8 +15,6 @@ Operand IfNode::generateIR(CFG &graph, SymbolTable &st) { currentBlock->setTrueBlock(trueBlock); currentBlock->setFalseBlock(joinBlock); - trueBlock->setTrueBlock(joinBlock); - graph.setCurrentBlock(trueBlock); stmt->generateIR(graph, st); graph.addInstruction(new JumpTac(joinLabel)); @@ -43,16 +41,15 @@ Operand IfElseNode::generateIR(CFG &graph, SymbolTable &st) { currentBlock->setTrueBlock(trueBlock); currentBlock->setFalseBlock(falseBlock); - trueBlock->setTrueBlock(joinBlock); - falseBlock->setTrueBlock(joinBlock); - graph.setCurrentBlock(trueBlock); stmt->generateIR(graph, st); graph.addInstruction(new JumpTac(joinLabel)); + graph.getCurrentBlock()->setTrueBlock(joinBlock); graph.setCurrentBlock(falseBlock); elseStmt->generateIR(graph, st); graph.addInstruction(new JumpTac(joinLabel)); + graph.getCurrentBlock()->setTrueBlock(joinBlock); graph.setCurrentBlock(joinBlock); @@ -76,7 +73,6 @@ Operand WhileNode::generateIR(CFG &graph, SymbolTable &st) { graph.addInstruction(new JumpTac(bodyBlock->getName())); graph.setCurrentBlock(bodyBlock); - currentBlock->setTrueBlock(headerBlock); stmt->generateIR(graph, st); graph.getCurrentBlock()->setTrueBlock(headerBlock); graph.addInstruction(new JumpTac(headerLabel)); From 916a1c6f7eb82c092cbfa72099e105dee0b5d519 Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:57:16 +0100 Subject: [PATCH 5/6] Fix: preserve operand type in conditional jump bytecode Bug: CondJumpTac emitted bytecode by pushing the stringified lhs field. Literal conditions like true/false became LOAD operations on variable names ('1'/'0') instead of constants.\n\nFix: use lhsOp when emitting CJMP so constants and variables are encoded correctly. --- src/ir/Tac.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/Tac.cpp b/src/ir/Tac.cpp index 344fb27..fc55945 100644 --- a/src/ir/Tac.cpp +++ b/src/ir/Tac.cpp @@ -48,7 +48,7 @@ void CondJumpTac::print(std::ostream &os) const { os << "iffalse " << lhs << " goto " << rhs << "\n"; } void CondJumpTac::generateBytecode(BytecodeMethodBlock &block) { - block.push(lhs).cjump(rhs); + block.push(lhsOp).cjump(rhs); } void MethodCallTac::print(std::ostream &os) const { From 215649e66fe3c6b541038a07b8d2e116aa169faa Mon Sep 17 00:00:00 2001 From: writemorecode Date: Tue, 10 Feb 2026 15:57:46 +0100 Subject: [PATCH 6/6] Fix: place STOP on reachable main CFG leaves Bug: bytecode generation appended STOP only to the main entry block. When control jumped into other blocks, execution could reach a block end with no instruction and crash the VM.\n\nFix: after generating main bytecode, traverse reachable CFG blocks and append STOP to leaf blocks (no true/false exits). --- src/ir/CFG.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/ir/CFG.cpp b/src/ir/CFG.cpp index 5513d43..eb8a7bf 100644 --- a/src/ir/CFG.cpp +++ b/src/ir/CFG.cpp @@ -1,11 +1,42 @@ #include #include #include +#include #include #include "ir/CFG.hpp" #include "semantic/TypeCheckVisitor.hpp" +namespace { +void appendStopToLeafBlocks(BBlock *root, BytecodeMethod &method) { + std::vector stack{root}; + std::unordered_set visited; + + while (!stack.empty()) { + auto *block = stack.back(); + stack.pop_back(); + + if (!visited.insert(block).second) { + continue; + } + + const auto hasTrueBlock = block->hasTrueBlock(); + const auto hasFalseBlock = block->hasFalseBlock(); + if (!hasTrueBlock && !hasFalseBlock) { + method.getBytecodeMethodBlock(block->getName()).stop(); + continue; + } + + if (hasTrueBlock) { + stack.push_back(block->getTrueBlock()); + } + if (hasFalseBlock) { + stack.push_back(block->getFalseBlock()); + } + } +} +} // namespace + std::string CFG::getTemporaryName() { auto name = "_t" + std::to_string(temporaryIndex); temporaryIndex++; @@ -53,7 +84,13 @@ const std::string *CFG::typeOf(const Node &node) const { } void CFG::generateBytecode(BytecodeProgram &program, SymbolTable &st) { + BBlock *mainRoot = nullptr; + for (auto *basicBlock : methodBlocks) { + if (mainRoot == nullptr) { + mainRoot = basicBlock; + } + const auto &className = basicBlock->getClassName(); const auto &methodName = basicBlock->getMethodName(); @@ -76,8 +113,9 @@ void CFG::generateBytecode(BytecodeProgram &program, SymbolTable &st) { basicBlock->generateBytecode(bytecodeMethod); } - const auto mainName = methodBlocks.front()->getName(); - auto &mainMethod = program.getBytecodeMethod(mainName); - auto &mainBlock = mainMethod.getBytecodeMethodBlock(mainName); - mainBlock.stop(); + + if (mainRoot != nullptr) { + auto &mainMethod = program.getBytecodeMethod(mainRoot->getName()); + appendStopToLeafBlocks(mainRoot, mainMethod); + } }