Try Catch
This commit is contained in:
parent
3138f6fb92
commit
227586c583
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -7,4 +7,6 @@ struct ExecutionContext {
|
||||
Value returnValue = NONE_VALUE;
|
||||
bool shouldBreak = false;
|
||||
bool shouldContinue = false;
|
||||
bool hasThrow = false;
|
||||
Value thrownValue = NONE_VALUE;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 + "'", "");
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.");
|
||||
22
tests.bob
22
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());
|
||||
print("done");
|
||||
14
tests/test_inc_property.bob
Normal file
14
tests/test_inc_property.bob
Normal 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
29
tests/test_try_catch.bob
Normal 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");
|
||||
|
||||
|
||||
95
tests/test_try_catch_extensive.bob
Normal file
95
tests/test_try_catch_extensive.bob
Normal 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");
|
||||
|
||||
|
||||
34
tests/test_try_catch_runtime.bob
Normal file
34
tests/test_try_catch_runtime.bob
Normal 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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user