diff --git a/src/headers/parsing/Lexer.h b/src/headers/parsing/Lexer.h index 79911ce..52beb55 100644 --- a/src/headers/parsing/Lexer.h +++ b/src/headers/parsing/Lexer.h @@ -26,6 +26,7 @@ enum TokenType{ AND, OR, TRUE, FALSE, IF, ELSE, FUNCTION, FOR, WHILE, DO, VAR, CLASS, EXTENDS, EXTENSION, SUPER, THIS, NONE, RETURN, BREAK, CONTINUE, + TRY, CATCH, FINALLY, THROW, // Compound assignment operators PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL, @@ -55,6 +56,7 @@ inline std::string enum_mapping[] = {"OPEN_PAREN", "CLOSE_PAREN", "OPEN_BRACE", "AND", "OR", "TRUE", "FALSE", "IF", "ELSE", "FUNCTION", "FOR", "WHILE", "DO", "VAR", "CLASS", "EXTENDS", "EXTENSION", "SUPER", "THIS", "NONE", "RETURN", "BREAK", "CONTINUE", + "TRY", "CATCH", "FINALLY", "THROW", // Compound assignment operators "PLUS_EQUAL", "MINUS_EQUAL", "STAR_EQUAL", "SLASH_EQUAL", "PERCENT_EQUAL", @@ -85,6 +87,10 @@ const std::map KEYWORDS { {"return", RETURN}, {"break", BREAK}, {"continue", CONTINUE}, + {"try", TRY}, + {"catch", CATCH}, + {"finally", FINALLY}, + {"throw", THROW}, }; struct Token diff --git a/src/headers/parsing/Parser.h b/src/headers/parsing/Parser.h index 06d1c40..69d4fa1 100644 --- a/src/headers/parsing/Parser.h +++ b/src/headers/parsing/Parser.h @@ -70,6 +70,8 @@ private: std::shared_ptr declaration(); std::shared_ptr classDeclaration(); std::shared_ptr extensionDeclaration(); + std::shared_ptr tryStatement(); + std::shared_ptr throwStatement(); std::shared_ptr varDeclaration(); diff --git a/src/headers/parsing/Statement.h b/src/headers/parsing/Statement.h index c886daa..80ae12c 100644 --- a/src/headers/parsing/Statement.h +++ b/src/headers/parsing/Statement.h @@ -38,6 +38,8 @@ struct StmtVisitor virtual void visitAssignStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitClassStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitExtensionStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; + virtual void visitTryStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; + virtual void visitThrowStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; }; struct Stmt : public std::enable_shared_from_this @@ -246,4 +248,27 @@ struct AssignStmt : Stmt { visitor->visitAssignStmt(std::static_pointer_cast(shared_from_this()), context); } +}; + +struct TryStmt : Stmt { + std::shared_ptr tryBlock; + Token catchVar; // IDENTIFIER or empty token if no catch + std::shared_ptr catchBlock; // may be null + std::shared_ptr finallyBlock; // may be null + + TryStmt(std::shared_ptr t, Token cvar, std::shared_ptr cblk, std::shared_ptr fblk) + : tryBlock(t), catchVar(cvar), catchBlock(cblk), finallyBlock(fblk) {} + + void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { + visitor->visitTryStmt(std::static_pointer_cast(shared_from_this()), context); + } +}; + +struct ThrowStmt : Stmt { + const Token keyword; + std::shared_ptr value; + ThrowStmt(Token kw, std::shared_ptr v) : keyword(kw), value(v) {} + void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { + visitor->visitThrowStmt(std::static_pointer_cast(shared_from_this()), context); + } }; \ No newline at end of file diff --git a/src/headers/runtime/ExecutionContext.h b/src/headers/runtime/ExecutionContext.h index ee43d76..7c8e911 100644 --- a/src/headers/runtime/ExecutionContext.h +++ b/src/headers/runtime/ExecutionContext.h @@ -7,4 +7,6 @@ struct ExecutionContext { Value returnValue = NONE_VALUE; bool shouldBreak = false; bool shouldContinue = false; + bool hasThrow = false; + Value thrownValue = NONE_VALUE; }; diff --git a/src/headers/runtime/Executor.h b/src/headers/runtime/Executor.h index f2f4af3..2f08213 100644 --- a/src/headers/runtime/Executor.h +++ b/src/headers/runtime/Executor.h @@ -40,6 +40,8 @@ public: void visitAssignStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; void visitClassStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; void visitExtensionStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; + void visitTryStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; + void visitThrowStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; private: void execute(const std::shared_ptr& statement, ExecutionContext* context); diff --git a/src/headers/runtime/Interpreter.h b/src/headers/runtime/Interpreter.h index 9431195..97e95ab 100644 --- a/src/headers/runtime/Interpreter.h +++ b/src/headers/runtime/Interpreter.h @@ -81,6 +81,11 @@ private: RuntimeDiagnostics diagnostics; // Utility functions for runtime operations std::unique_ptr evaluator; std::unique_ptr executor; + // Pending throw propagation from expression evaluation + bool hasPendingThrow = false; + Value pendingThrow = NONE_VALUE; + int tryDepth = 0; + bool inlineErrorReported = false; public: explicit Interpreter(bool isInteractive); @@ -117,6 +122,17 @@ public: bool getClassTemplate(const std::string& className, std::unordered_map& out) const; std::unordered_map buildMergedTemplate(const std::string& className) const; void addStdLibFunctions(); + // Throw propagation helpers + void setPendingThrow(const Value& v) { hasPendingThrow = true; pendingThrow = v; } + bool consumePendingThrow(Value& out) { if (!hasPendingThrow) return false; out = pendingThrow; hasPendingThrow = false; pendingThrow = NONE_VALUE; return true; } + // Try tracking + void enterTry() { tryDepth++; } + void exitTry() { if (tryDepth > 0) tryDepth--; } + bool isInTry() const { return tryDepth > 0; } + void markInlineErrorReported() { inlineErrorReported = true; } + bool hasInlineErrorReported() const { return inlineErrorReported; } + void clearInlineErrorReported() { inlineErrorReported = false; } + bool hasReportedError() const; diff --git a/src/sources/cli/bob.cpp b/src/sources/cli/bob.cpp index d74fcf8..4b9a4cf 100644 --- a/src/sources/cli/bob.cpp +++ b/src/sources/cli/bob.cpp @@ -75,8 +75,9 @@ void Bob::run(std::string source) } catch(std::exception &e) { - // Only suppress errors that have already been reported by the error reporter - if (errorReporter.hasReportedError()) { + // Only suppress errors that have already been reported inline/top-level + if (errorReporter.hasReportedError() || (interpreter && (interpreter->hasReportedError() || interpreter->hasInlineErrorReported()))) { + if (interpreter) interpreter->clearInlineErrorReported(); return; } diff --git a/src/sources/parsing/Parser.cpp b/src/sources/parsing/Parser.cpp index a365a49..fe4a08a 100644 --- a/src/sources/parsing/Parser.cpp +++ b/src/sources/parsing/Parser.cpp @@ -269,7 +269,8 @@ sptr(Expr) Parser::postfix() if (match({PLUS_PLUS, MINUS_MINUS})) { Token oper = previous(); if (!std::dynamic_pointer_cast(expr) && - !std::dynamic_pointer_cast(expr)) { + !std::dynamic_pointer_cast(expr) && + !std::dynamic_pointer_cast(expr)) { if (errorReporter) { errorReporter->reportError(oper.line, oper.column, "Parse Error", "Postfix increment/decrement can only be applied to variables or array elements", ""); @@ -585,6 +586,8 @@ std::shared_ptr Parser::functionExpression() { sptr(Stmt) Parser::statement() { if(match({RETURN})) return returnStatement(); + if(match({TRY})) return tryStatement(); + if(match({THROW})) return throwStatement(); if(match({IF})) return ifStatement(); if(match({DO})) return doWhileStatement(); if(match({WHILE})) return whileStatement(); @@ -784,6 +787,32 @@ std::vector Parser::block() return statements; } +std::shared_ptr Parser::tryStatement() { + // try { ... } (catch (e) { ... })? (finally { ... })? + auto tryBlock = statement(); + Token catchVar{IDENTIFIER, "", previous().line, previous().column}; + std::shared_ptr catchBlock = nullptr; + std::shared_ptr finallyBlock = nullptr; + if (match({CATCH})) { + consume(OPEN_PAREN, "Expected '(' after 'catch'."); + Token var = consume(IDENTIFIER, "Expected identifier for catch variable."); + catchVar = var; + consume(CLOSE_PAREN, "Expected ')' after catch variable."); + catchBlock = statement(); + } + if (match({FINALLY})) { + finallyBlock = statement(); + } + return msptr(TryStmt)(tryBlock, catchVar, catchBlock, finallyBlock); +} + +std::shared_ptr Parser::throwStatement() { + Token kw = previous(); + auto val = expression(); + consume(SEMICOLON, "Expected ';' after throw expression."); + return msptr(ThrowStmt)(kw, val); +} + sptr(Expr) Parser::finishCall(sptr(Expr) callee) { std::vector arguments; diff --git a/src/sources/runtime/Environment.cpp b/src/sources/runtime/Environment.cpp index 86ee0f4..9063d81 100644 --- a/src/sources/runtime/Environment.cpp +++ b/src/sources/runtime/Environment.cpp @@ -13,7 +13,9 @@ void Environment::assign(const Token& name, const Value& value) { return; } + // Report only if not within a try; otherwise let try/catch handle if (errorReporter) { + // We cannot check tryDepth here directly; rely on Executor to suppress double-reporting errorReporter->reportError(name.line, name.column, "Runtime Error", "Undefined variable '" + name.lexeme + "'", ""); } diff --git a/src/sources/runtime/Evaluator.cpp b/src/sources/runtime/Evaluator.cpp index f291766..a9077dc 100644 --- a/src/sources/runtime/Evaluator.cpp +++ b/src/sources/runtime/Evaluator.cpp @@ -37,8 +37,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr& expression) switch (expression->oper.type) { case MINUS: if (!right.isNumber()) { - interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error", - "Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme); throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme); } return Value(-right.asNumber()); @@ -48,8 +46,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr& expression) case BIN_NOT: if (!right.isNumber()) { - interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error", - "Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme); throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme); } return Value(static_cast(~(static_cast(right.asNumber())))); @@ -106,8 +102,6 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr& expression) default: break; // Unreachable } - interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error", - ErrorUtils::makeOperatorError(opName, left.getType(), right.getType()), opName); throw std::runtime_error(ErrorUtils::makeOperatorError(opName, left.getType(), right.getType())); } @@ -130,10 +124,7 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr& expression) throw std::runtime_error("Unknown operator: " + expression->oper.lexeme); } } catch (const std::runtime_error& e) { - // The Value operators provide good error messages, just add context - interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error", - e.what(), expression->oper.lexeme); - throw; + throw; // Propagate to statement driver (try/catch) without reporting here } } @@ -166,7 +157,7 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr& expres throw std::runtime_error("Invalid increment/decrement operator."); } - // Update the variable or array element + // Update the variable, array element, or object property if (auto varExpr = std::dynamic_pointer_cast(expression->operand)) { interpreter->getEnvironment()->assign(varExpr->name, Value(newValue)); } else if (auto arrayExpr = std::dynamic_pointer_cast(expression->operand)) { @@ -197,6 +188,14 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr& expres // Update the array element arr[idx] = Value(newValue); + } else if (auto propExpr = std::dynamic_pointer_cast(expression->operand)) { + // obj.prop++ / obj.prop-- + Value object = interpreter->evaluate(propExpr->object); + if (!object.isDict()) { + throw std::runtime_error("Can only increment/decrement properties on objects"); + } + std::unordered_map& dict = object.asDict(); + dict[propExpr->name.lexeme] = Value(newValue); } else { interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error", "Increment/decrement can only be applied to variables or array elements.", ""); @@ -268,8 +267,11 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr& expr if (array.isArray()) { // Handle array indexing if (!index.isNumber()) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Array index must be a number", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Array index must be a number", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Array index must be a number"); } @@ -277,8 +279,11 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr& expr const std::vector& arr = array.asArray(); if (idx < 0 || idx >= static_cast(arr.size())) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Array index out of bounds", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Array index out of bounds", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Array index out of bounds"); } @@ -287,8 +292,11 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr& expr } else if (array.isDict()) { // Handle dictionary indexing if (!index.isString()) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Dictionary key must be a string", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Dictionary key must be a string", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Dictionary key must be a string"); } @@ -303,8 +311,11 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr& expr } } else { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Can only index arrays and dictionaries", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Can only index arrays and dictionaries", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Can only index arrays and dictionaries"); } } @@ -435,8 +446,11 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr& expr) { return Value(anyFn); } - interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error", - "Cannot access property '" + propertyName + "' on this type", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error", + "Cannot access property '" + propertyName + "' on this type", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Cannot access property '" + propertyName + "' on this type"); } } @@ -449,8 +463,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr& ex if (array.isArray()) { // Handle array assignment if (!index.isNumber()) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Array index must be a number", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Array index must be a number", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Array index must be a number"); } @@ -458,8 +475,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr& ex std::vector& arr = array.asArray(); if (idx < 0 || idx >= static_cast(arr.size())) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Array index out of bounds", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Array index out of bounds", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Array index out of bounds"); } @@ -469,8 +489,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr& ex } else if (array.isDict()) { // Handle dictionary assignment if (!index.isString()) { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Dictionary key must be a string", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Dictionary key must be a string", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Dictionary key must be a string"); } @@ -481,8 +504,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr& ex return value; } else { - interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", - "Can only assign to array or dictionary elements", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", + "Can only assign to array or dictionary elements", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Can only assign to array or dictionary elements"); } } @@ -510,8 +536,11 @@ Value Evaluator::visitPropertyAssignExpr(const std::shared_ptrreportError(expr->name.line, expr->name.column, "Runtime Error", - "Cannot assign property '" + propertyName + "' on non-object", ""); + if (!interpreter->isInTry()) { + interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error", + "Cannot assign property '" + propertyName + "' on non-object", ""); + interpreter->markInlineErrorReported(); + } throw std::runtime_error("Cannot assign property '" + propertyName + "' on non-object"); } } diff --git a/src/sources/runtime/Executor.cpp b/src/sources/runtime/Executor.cpp index 05066ff..b0ebb96 100644 --- a/src/sources/runtime/Executor.cpp +++ b/src/sources/runtime/Executor.cpp @@ -12,13 +12,40 @@ Executor::Executor(Interpreter* interpreter, Evaluator* evaluator) Executor::~Executor() {} void Executor::interpret(const std::vector>& statements) { + ExecutionContext top; for (const auto& statement : statements) { - execute(statement, nullptr); + execute(statement, &top); + if (top.hasThrow) break; + } + if (top.hasThrow) { + // If already reported inline, don't double-report here + if (!interpreter->hasInlineErrorReported()) { + std::string msg = "Uncaught exception"; + if (top.thrownValue.isString()) msg = top.thrownValue.asString(); + if (top.thrownValue.isDict()) { + auto& d = top.thrownValue.asDict(); + auto it = d.find("message"); + if (it != d.end() && it->second.isString()) msg = it->second.asString(); + } + interpreter->reportError(0, 0, "Runtime Error", msg, ""); + } + // Clear inline marker after handling + interpreter->clearInlineErrorReported(); } } void Executor::execute(const std::shared_ptr& statement, ExecutionContext* context) { - statement->accept(this, context); + try { + statement->accept(this, context); + } catch (const std::runtime_error& e) { + if (context) { + std::unordered_map err; + err["type"] = Value(std::string("RuntimeError")); + err["message"] = Value(std::string(e.what())); + context->hasThrow = true; + context->thrownValue = Value(err); + } + } } void Executor::executeBlock(const std::vector>& statements, std::shared_ptr env, ExecutionContext* context) { @@ -27,7 +54,14 @@ void Executor::executeBlock(const std::vector>& statements for (const auto& statement : statements) { execute(statement, context); - if (context && (context->hasReturn || context->shouldBreak || context->shouldContinue)) { + // Bridge any pending throws from expression evaluation into the context + Value pending; + if (interpreter->consumePendingThrow(pending)) { + if (context) { context->hasThrow = true; context->thrownValue = pending; } + } + // If an inline reporter already handled this error and we are at top level (no try), + // avoid reporting it again here. + if (context && (context->hasReturn || context->shouldBreak || context->shouldContinue || context->hasThrow)) { interpreter->setEnvironment(previous); return; } @@ -42,6 +76,11 @@ void Executor::visitBlockStmt(const std::shared_ptr& statement, Execu void Executor::visitExpressionStmt(const std::shared_ptr& statement, ExecutionContext* context) { Value value = statement->expression->accept(evaluator); + Value thrown; + if (interpreter->consumePendingThrow(thrown)) { + if (context) { context->hasThrow = true; context->thrownValue = thrown; } + return; + } if (interpreter->isInteractiveMode()) std::cout << "\u001b[38;5;8m[" << interpreter->stringify(value) << "]\u001b[38;5;15m\n"; @@ -51,6 +90,8 @@ void Executor::visitVarStmt(const std::shared_ptr& statement, Execution Value value = NONE_VALUE; if (statement->initializer != nullptr) { value = statement->initializer->accept(evaluator); + Value thrownInit; + if (interpreter->consumePendingThrow(thrownInit)) { if (context) { context->hasThrow = true; context->thrownValue = thrownInit; } return; } } interpreter->getEnvironment()->define(statement->name.lexeme, value); } @@ -82,7 +123,10 @@ void Executor::visitReturnStmt(const std::shared_ptr& statement, Exe } void Executor::visitIfStmt(const std::shared_ptr& statement, ExecutionContext* context) { - if (interpreter->isTruthy(statement->condition->accept(evaluator))) { + Value condValIf = statement->condition->accept(evaluator); + Value thrownIf; + if (interpreter->consumePendingThrow(thrownIf)) { if (context) { context->hasThrow = true; context->thrownValue = thrownIf; } return; } + if (interpreter->isTruthy(condValIf)) { execute(statement->thenBranch, context); } else if (statement->elseBranch != nullptr) { execute(statement->elseBranch, context); @@ -97,7 +141,7 @@ void Executor::visitWhileStmt(const std::shared_ptr& statement, Execu while (interpreter->isTruthy(statement->condition->accept(evaluator))) { execute(statement->body, &loopContext); - + if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; } break; } if (loopContext.hasReturn) { if (context) { context->hasReturn = true; @@ -123,26 +167,17 @@ void Executor::visitDoWhileStmt(const std::shared_ptr& statement, E loopContext.isFunctionBody = context->isFunctionBody; } - do { + while (true) { execute(statement->body, &loopContext); - - if (loopContext.hasReturn) { - if (context) { - context->hasReturn = true; - context->returnValue = loopContext.returnValue; - } - break; - } - - if (loopContext.shouldBreak) { - break; - } - - if (loopContext.shouldContinue) { - loopContext.shouldContinue = false; - continue; - } - } while (interpreter->isTruthy(statement->condition->accept(evaluator))); + if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; } break; } + if (loopContext.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = loopContext.returnValue; } break; } + if (loopContext.shouldBreak) { break; } + if (loopContext.shouldContinue) { loopContext.shouldContinue = false; } + Value c = statement->condition->accept(evaluator); + Value thrown; + if (interpreter->consumePendingThrow(thrown)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; } break; } + if (!interpreter->isTruthy(c)) break; + } } void Executor::visitForStmt(const std::shared_ptr& statement, ExecutionContext* context) { @@ -155,9 +190,15 @@ void Executor::visitForStmt(const std::shared_ptr& statement, Execution loopContext.isFunctionBody = context->isFunctionBody; } - while (statement->condition == nullptr || interpreter->isTruthy(statement->condition->accept(evaluator))) { + while (true) { + if (statement->condition != nullptr) { + Value c = statement->condition->accept(evaluator); + Value thrown; + if (interpreter->consumePendingThrow(thrown)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; } break; } + if (!interpreter->isTruthy(c)) break; + } execute(statement->body, &loopContext); - + if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; } break; } if (loopContext.hasReturn) { if (context) { context->hasReturn = true; @@ -174,12 +215,16 @@ void Executor::visitForStmt(const std::shared_ptr& statement, Execution loopContext.shouldContinue = false; if (statement->increment != nullptr) { statement->increment->accept(evaluator); + Value thrown; + if (interpreter->consumePendingThrow(thrown)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; } break; } } continue; } if (statement->increment != nullptr) { statement->increment->accept(evaluator); + Value thrown; + if (interpreter->consumePendingThrow(thrown)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; } break; } } } } @@ -196,6 +241,60 @@ void Executor::visitContinueStmt(const std::shared_ptr& statement, } } +void Executor::visitTryStmt(const std::shared_ptr& statement, ExecutionContext* context) { + interpreter->enterTry(); + ExecutionContext inner; + if (context) inner.isFunctionBody = context->isFunctionBody; + execute(statement->tryBlock, &inner); + // Also capture any pending throw signaled by expressions + Value pending; + if (interpreter->consumePendingThrow(pending)) { + inner.hasThrow = true; + inner.thrownValue = pending; + } + // If thrown, handle catch + if (inner.hasThrow && statement->catchBlock) { + auto saved = interpreter->getEnvironment(); + auto env = std::make_shared(saved); + env->setErrorReporter(nullptr); + // Bind catch var if provided + if (!statement->catchVar.lexeme.empty()) { + env->define(statement->catchVar.lexeme, inner.thrownValue); + } + interpreter->setEnvironment(env); + ExecutionContext catchCtx; + catchCtx.isFunctionBody = inner.isFunctionBody; + execute(statement->catchBlock, &catchCtx); + inner.hasThrow = catchCtx.hasThrow; + inner.thrownValue = catchCtx.thrownValue; + interpreter->setEnvironment(saved); + } + // finally always + if (statement->finallyBlock) { + ExecutionContext fctx; + fctx.isFunctionBody = inner.isFunctionBody; + execute(statement->finallyBlock, &fctx); + if (fctx.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = fctx.returnValue; } return; } + if (fctx.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = fctx.thrownValue; } return; } + if (fctx.shouldBreak) { if (context) { context->shouldBreak = true; } return; } + if (fctx.shouldContinue) { if (context) { context->shouldContinue = true; } return; } + } + // propagate remaining control flow + if (inner.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = inner.returnValue; } interpreter->exitTry(); return; } + if (inner.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = inner.thrownValue; } interpreter->exitTry(); return; } + if (inner.shouldBreak) { if (context) { context->shouldBreak = true; } interpreter->exitTry(); return; } + if (inner.shouldContinue) { if (context) { context->shouldContinue = true; } interpreter->exitTry(); return; } + interpreter->exitTry(); +} + +void Executor::visitThrowStmt(const std::shared_ptr& statement, ExecutionContext* context) { + Value v = statement->value ? statement->value->accept(evaluator) : NONE_VALUE; + if (context) { + context->hasThrow = true; + context->thrownValue = v; + } +} + void Executor::visitAssignStmt(const std::shared_ptr& statement, ExecutionContext* context) { Value value = statement->value->accept(evaluator); diff --git a/src/sources/runtime/Interpreter.cpp b/src/sources/runtime/Interpreter.cpp index 88f6921..d894dd3 100644 --- a/src/sources/runtime/Interpreter.cpp +++ b/src/sources/runtime/Interpreter.cpp @@ -36,6 +36,10 @@ Value Interpreter::evaluate(const std::shared_ptr& expr) { return runTrampoline(result); } +bool Interpreter::hasReportedError() const { + return inlineErrorReported; +} + Value Interpreter::runTrampoline(Value initialResult) { Value current = initialResult; while (current.isThunk()) { @@ -429,6 +433,7 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr& expre for (const auto& stmt : function->body) { stmt->accept(executor.get(), &context); + if (context.hasThrow) { setPendingThrow(context.thrownValue); return NONE_VALUE; } if (context.hasReturn) { return context.returnValue; } @@ -467,9 +472,8 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr& expre for (const auto& stmt : function->body) { stmt->accept(executor.get(), &context); - if (context.hasReturn) { - return context.returnValue; - } + if (context.hasThrow) { setPendingThrow(context.thrownValue); return NONE_VALUE; } + if (context.hasReturn) { return context.returnValue; } } return context.returnValue; diff --git a/test_bob_language.bob b/test_bob_language.bob index a2b563f..1ffb21d 100644 --- a/test_bob_language.bob +++ b/test_bob_language.bob @@ -3303,5 +3303,8 @@ eval(readFile(path13)); var path14 = fileExists("tests/test_builtin_methods_style.bob") ? "tests/test_builtin_methods_style.bob" : "../tests/test_builtin_methods_style.bob"; eval(readFile(path14)); +var path15 = fileExists("tests/test_try_catch.bob") ? "tests/test_try_catch.bob" : "../tests/test_try_catch.bob"; +eval(readFile(path15)); + print("\nAll tests passed."); print("Test suite complete."); \ No newline at end of file diff --git a/tests.bob b/tests.bob index 9b69f01..4cf6374 100644 --- a/tests.bob +++ b/tests.bob @@ -68,13 +68,21 @@ class Test { } } -var t = Test(); -t.test(); +var arr = []; +for(var i = 0; i < 100; i++){ + arr.push(i); +} -extension number{ - func negate(){ - return -this; +var counter = 0; + +//try{ + while(true){ + print(arr[counter]); + counter++; + sleep(0.01); } -} +//}catch(e){ + +//} -print(69.negate()); \ No newline at end of file +print("done"); \ No newline at end of file diff --git a/tests/test_inc_property.bob b/tests/test_inc_property.bob new file mode 100644 index 0000000..f30c18a --- /dev/null +++ b/tests/test_inc_property.bob @@ -0,0 +1,14 @@ +print("\n--- Test: property increment/decrement ---"); + +var o = {"v": 10}; +o.v++; +assert(o.v == 11, "postfix inc on prop"); ++o.v; +assert(o.v == 12, "prefix inc on prop"); +o.v--; +assert(o.v == 11, "postfix dec on prop"); +--o.v; +assert(o.v == 10, "prefix dec on prop"); + +print("property inc/dec: PASS"); + diff --git a/tests/test_try_catch.bob b/tests/test_try_catch.bob new file mode 100644 index 0000000..7350e8a --- /dev/null +++ b/tests/test_try_catch.bob @@ -0,0 +1,29 @@ +print("\n--- Test: try/catch/finally (basic) ---"); + +// Basic: catch handles, finally always runs +var steps = []; +try { steps.push("try"); throw {"message":"boom"}; } +catch (e) { steps.push("catch:" + e.message); } +finally { steps.push("finally"); } +assert(steps[0] == "try" && steps[1] == "catch:boom" && steps[2] == "finally", "basic order"); + +// Nested: no inner catch, outer catches, finally still runs +var seen = []; +try { + try { throw {"message":"x"}; } finally { seen.push("innerFinally"); } +} +catch (e) { seen.push("outer:" + e.message); } +assert(seen[0] == "innerFinally" && seen[1] == "outer:x", "nested propagation"); + +// Finally overrides return +func r() { try { return 1; } finally { return 2; } } +assert(r() == 2, "finally overrides return"); + +// Throw non-dict +var info = ""; +try { throw "oops"; } catch (e) { info = type(e) + ":" + e; } +assert(info == "string:oops", "non-dict throw"); + +print("try/catch/finally basic: PASS"); + + diff --git a/tests/test_try_catch_extensive.bob b/tests/test_try_catch_extensive.bob new file mode 100644 index 0000000..51330a8 --- /dev/null +++ b/tests/test_try_catch_extensive.bob @@ -0,0 +1,95 @@ +print("\n--- Test: try/catch/finally (extensive) ---"); + +// 1) Basic catch and finally order +var steps = []; +try { steps.push("A"); throw {"message":"err"}; } catch (e) { steps.push("B:" + e.message); } finally { steps.push("C"); } +assert(steps.len() == 3, "basic order length"); +assert(steps[0] == "A", "order A"); +assert(steps[1] == "B:err", "order B"); +assert(steps[2] == "C", "order C"); + +// 2) No catch; finally runs; thrown propagates +var caughtOuter = false; var fin = []; +try { + try { fin.push("try"); throw {"message":"up"}; } finally { fin.push("finally"); } +} catch (e) { caughtOuter = (e.message == "up"); } +assert(caughtOuter == true, "outer caught propagated throw"); +assert(fin.len() == 2 && fin[1] == "finally", "finally executed without catch"); + +// 3) Rethrow from catch +caughtOuter = false; +try { + try { throw {"message":"boom"}; } + catch (e) { throw e; } +} catch (e) { caughtOuter = (e.message == "boom"); } +assert(caughtOuter == true, "rethrow works"); + +// 4) finally overriding return +func f() { + try { return 1; } finally { return 2; } +} +assert(f() == 2, "finally overrides return"); + +// 5) finally overriding throw +func g() { + try { throw {"message":"x"}; } finally { return 3; } +} +assert(g() == 3, "finally overrides throw with return"); + +// 6) Loop with continue/break inside finally +var i = 0; var seq = []; +while (i < 3) { + try { i = i + 1; seq.push(i); } + finally { + if (i < 3) continue; + } +} +assert(i == 3 && seq.len() == 3, "finally continue in loop"); + +i = 0; seq = []; +while (true) { + try { i = i + 1; seq.push(i); if (i == 2) throw {"message":"stop"}; } + catch (e) { seq.push("caught:" + e.message); } + finally { + if (i >= 2) break; + } +} +assert(seq.len() == 3 && seq[2] == "caught:stop", "finally break in loop"); + +// 7) Throw from method and catch in caller +class T { func f(x) { if (x < 0) throw {"message":"neg"}; return x; } } +var t = T(); +var ok = 0; +try { ok = t.f(5); } catch (e) { ok = -1; } +var got = ""; +try { t.f(-1); } catch (e) { got = e.message; } +assert(ok == 5 && got == "neg", "throw from method"); + +// 8) Throw non-dict value +var t = ""; +try { throw "oops"; } catch (e) { t = type(e) + ":" + e; } +assert(t == "string:oops", "throw non-dict"); + +// 9) Nested try with inner catch consuming error +var mark = []; +try { + try { throw {"message":"inner"}; } + catch (e) { mark.push(e.message); } +} catch (e) { mark.push("outer:" + e.message); } +assert(mark.len() == 1 && mark[0] == "inner", "inner catch consumes"); + +// 10) Method/extension throws and is caught +class P { func bad() { throw {"message":"m"}; } } +var p = P(); +var mmsg = ""; +try { p.bad(); } catch (e) { mmsg = e.message; } +assert(mmsg == "m", "method throw caught"); + +extension array { func thrower() { throw {"message":"arr"}; } } +var a = [1]; var amsg = ""; +try { a.thrower(); } catch (e) { amsg = e.message; } +assert(amsg == "arr", "extension throw caught"); + +print("try/catch/finally extensive: PASS"); + + diff --git a/tests/test_try_catch_runtime.bob b/tests/test_try_catch_runtime.bob new file mode 100644 index 0000000..933425c --- /dev/null +++ b/tests/test_try_catch_runtime.bob @@ -0,0 +1,34 @@ +print("\n--- Test: try/catch runtime errors ---"); + +// Division by zero +var caught = ""; +try { var x = 10 / 0; } +catch (e) { caught = e.type + ":" + e.message; } +assert(type(caught) == "string", "caught is string"); + +// Undefined variable +var c2 = ""; +try { var y = missingVar + 1; } +catch (e) { c2 = e.type; } +assert(c2 == "RuntimeError", "undefined variable caught"); + +// Array out-of-bounds +var c3 = ""; +try { var a = [1,2]; var z = a[5]; } +catch (e) { c3 = e.type; } +assert(c3 == "RuntimeError", "array oob caught"); + +// Dict key type error +var c4 = ""; +try { var d = {"x":1}; var z = d[1]; } +catch (e) { c4 = e.type; } +assert(c4 == "RuntimeError", "dict key type caught"); + +// Non-array/dict index +var c5 = ""; +try { var z = 42[0]; } +catch (e) { c5 = e.type; } +assert(c5 == "RuntimeError", "non-array index caught"); + +print("try/catch runtime errors: PASS"); +