Try Catch

This commit is contained in:
Bobby Lucero 2025-08-11 18:26:41 -04:00
parent 3138f6fb92
commit 227586c583
18 changed files with 470 additions and 70 deletions

View File

@ -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<std::string, TokenType> KEYWORDS {
{"return", RETURN},
{"break", BREAK},
{"continue", CONTINUE},
{"try", TRY},
{"catch", CATCH},
{"finally", FINALLY},
{"throw", THROW},
};
struct Token

View File

@ -70,6 +70,8 @@ private:
std::shared_ptr<Stmt> declaration();
std::shared_ptr<Stmt> classDeclaration();
std::shared_ptr<Stmt> extensionDeclaration();
std::shared_ptr<Stmt> tryStatement();
std::shared_ptr<Stmt> throwStatement();
std::shared_ptr<Stmt> varDeclaration();

View File

@ -38,6 +38,8 @@ struct StmtVisitor
virtual void visitAssignStmt(const std::shared_ptr<AssignStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitClassStmt(const std::shared_ptr<ClassStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitTryStmt(const std::shared_ptr<struct TryStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitThrowStmt(const std::shared_ptr<struct ThrowStmt>& stmt, ExecutionContext* context = nullptr) = 0;
};
struct Stmt : public std::enable_shared_from_this<Stmt>
@ -246,4 +248,27 @@ struct AssignStmt : Stmt
{
visitor->visitAssignStmt(std::static_pointer_cast<AssignStmt>(shared_from_this()), context);
}
};
struct TryStmt : Stmt {
std::shared_ptr<Stmt> tryBlock;
Token catchVar; // IDENTIFIER or empty token if no catch
std::shared_ptr<Stmt> catchBlock; // may be null
std::shared_ptr<Stmt> finallyBlock; // may be null
TryStmt(std::shared_ptr<Stmt> t, Token cvar, std::shared_ptr<Stmt> cblk, std::shared_ptr<Stmt> fblk)
: tryBlock(t), catchVar(cvar), catchBlock(cblk), finallyBlock(fblk) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitTryStmt(std::static_pointer_cast<TryStmt>(shared_from_this()), context);
}
};
struct ThrowStmt : Stmt {
const Token keyword;
std::shared_ptr<Expr> value;
ThrowStmt(Token kw, std::shared_ptr<Expr> v) : keyword(kw), value(v) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitThrowStmt(std::static_pointer_cast<ThrowStmt>(shared_from_this()), context);
}
};

View File

@ -7,4 +7,6 @@ struct ExecutionContext {
Value returnValue = NONE_VALUE;
bool shouldBreak = false;
bool shouldContinue = false;
bool hasThrow = false;
Value thrownValue = NONE_VALUE;
};

View File

@ -40,6 +40,8 @@ public:
void visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context = nullptr) override;
void visitClassStmt(const std::shared_ptr<ClassStmt>& statement, ExecutionContext* context = nullptr) override;
void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& statement, ExecutionContext* context = nullptr) override;
void visitTryStmt(const std::shared_ptr<TryStmt>& statement, ExecutionContext* context = nullptr) override;
void visitThrowStmt(const std::shared_ptr<ThrowStmt>& statement, ExecutionContext* context = nullptr) override;
private:
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context);

View File

@ -81,6 +81,11 @@ private:
RuntimeDiagnostics diagnostics; // Utility functions for runtime operations
std::unique_ptr<Evaluator> evaluator;
std::unique_ptr<Executor> 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<std::string, Value>& out) const;
std::unordered_map<std::string, Value> 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;

View File

@ -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;
}

View File

@ -269,7 +269,8 @@ sptr(Expr) Parser::postfix()
if (match({PLUS_PLUS, MINUS_MINUS})) {
Token oper = previous();
if (!std::dynamic_pointer_cast<VarExpr>(expr) &&
!std::dynamic_pointer_cast<ArrayIndexExpr>(expr)) {
!std::dynamic_pointer_cast<ArrayIndexExpr>(expr) &&
!std::dynamic_pointer_cast<PropertyExpr>(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<Expr> 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<sptr(Stmt)> Parser::block()
return statements;
}
std::shared_ptr<Stmt> Parser::tryStatement() {
// try { ... } (catch (e) { ... })? (finally { ... })?
auto tryBlock = statement();
Token catchVar{IDENTIFIER, "", previous().line, previous().column};
std::shared_ptr<Stmt> catchBlock = nullptr;
std::shared_ptr<Stmt> 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<Stmt> 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<sptr(Expr)> arguments;

View File

@ -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 + "'", "");
}

View File

@ -37,8 +37,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& 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<UnaryExpr>& 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<double>(~(static_cast<long>(right.asNumber()))));
@ -106,8 +102,6 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& 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<BinaryExpr>& 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<IncrementExpr>& 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<VarExpr>(expression->operand)) {
interpreter->getEnvironment()->assign(varExpr->name, Value(newValue));
} else if (auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expression->operand)) {
@ -197,6 +188,14 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expres
// Update the array element
arr[idx] = Value(newValue);
} else if (auto propExpr = std::dynamic_pointer_cast<PropertyExpr>(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<std::string, Value>& 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<ArrayIndexExpr>& 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<ArrayIndexExpr>& expr
const std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(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<ArrayIndexExpr>& 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<ArrayIndexExpr>& 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<PropertyExpr>& 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<ArrayAssignExpr>& 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<ArrayAssignExpr>& ex
std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(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<ArrayAssignExpr>& 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<ArrayAssignExpr>& 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_ptr<PropertyAssignExp
dict[propertyName] = value;
return value; // Return the assigned value
} else {
interpreter->reportError(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");
}
}

View File

@ -12,13 +12,40 @@ Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
Executor::~Executor() {}
void Executor::interpret(const std::vector<std::shared_ptr<Stmt>>& 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<Stmt>& statement, ExecutionContext* context) {
statement->accept(this, context);
try {
statement->accept(this, context);
} catch (const std::runtime_error& e) {
if (context) {
std::unordered_map<std::string, Value> 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<std::shared_ptr<Stmt>>& statements, std::shared_ptr<Environment> env, ExecutionContext* context) {
@ -27,7 +54,14 @@ void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& 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<BlockStmt>& statement, Execu
void Executor::visitExpressionStmt(const std::shared_ptr<ExpressionStmt>& 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<VarStmt>& 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<ReturnStmt>& statement, Exe
}
void Executor::visitIfStmt(const std::shared_ptr<IfStmt>& 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<WhileStmt>& 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<DoWhileStmt>& 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<ForStmt>& statement, ExecutionContext* context) {
@ -155,9 +190,15 @@ void Executor::visitForStmt(const std::shared_ptr<ForStmt>& 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<ForStmt>& 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<ContinueStmt>& statement,
}
}
void Executor::visitTryStmt(const std::shared_ptr<TryStmt>& 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<Environment>(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<ThrowStmt>& 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<AssignStmt>& statement, ExecutionContext* context) {
Value value = statement->value->accept(evaluator);

View File

@ -36,6 +36,10 @@ Value Interpreter::evaluate(const std::shared_ptr<Expr>& 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<CallExpr>& 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<CallExpr>& 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;

View File

@ -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.");

View File

@ -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());
print("done");

View File

@ -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");

29
tests/test_try_catch.bob Normal file
View File

@ -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");

View File

@ -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");

View File

@ -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");