From b87b342dffe86483c94a0dc423f069c8d77f5ae8 Mon Sep 17 00:00:00 2001 From: Bobby Lucero Date: Tue, 5 Aug 2025 19:06:52 -0400 Subject: [PATCH] Tail call testing --- headers/Interpreter.h | 39 +++++++++++++++++ headers/Value.h | 11 ++++- source/Interpreter.cpp | 97 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/headers/Interpreter.h b/headers/Interpreter.h index 371ccc1..dbd207e 100644 --- a/headers/Interpreter.h +++ b/headers/Interpreter.h @@ -12,6 +12,38 @@ #include #include #include +#include +#include + +// Forward declaration +class Interpreter; + +// RAII helper for thunk execution flag +struct ScopedThunkFlag { + bool& flag; + bool prev; + ScopedThunkFlag(bool& f) : flag(f), prev(f) { flag = true; } + ~ScopedThunkFlag() { flag = prev; } +}; + +// Thunk class for trampoline-based tail call optimization +class Thunk { +public: + using ThunkFunction = std::function; + + explicit Thunk(ThunkFunction func) : func(std::move(func)) {} + + Value execute() const { + return func(); + } + + bool isThunk() const { return true; } + +private: + ThunkFunction func; +}; + + class Interpreter : public ExprVisitor, public StmtVisitor { @@ -46,14 +78,21 @@ private: std::vector > builtinFunctions; std::vector > functions; ErrorReporter* errorReporter; + bool inThunkExecution = false; + + Value evaluate(const std::shared_ptr& expr); + Value evaluateWithoutTrampoline(const std::shared_ptr& expr); bool isEqual(Value a, Value b); bool isWholeNumer(double num); void execute(const std::shared_ptr& statement, ExecutionContext* context = nullptr); void executeBlock(std::vector > statements, std::shared_ptr env, ExecutionContext* context = nullptr); void addStdLibFunctions(); + // Trampoline execution + Value runTrampoline(Value initialResult); + public: bool isTruthy(Value object); std::string stringify(Value object); diff --git a/headers/Value.h b/headers/Value.h index b606d33..5797e9e 100644 --- a/headers/Value.h +++ b/headers/Value.h @@ -11,6 +11,7 @@ class Environment; class Function; class BuiltinFunction; +class Thunk; // Type tags for the Value union enum ValueType { @@ -19,7 +20,8 @@ enum ValueType { VAL_BOOLEAN, VAL_STRING, VAL_FUNCTION, - VAL_BUILTIN_FUNCTION + VAL_BUILTIN_FUNCTION, + VAL_THUNK }; // Tagged value system (like Lua) - no heap allocation for simple values @@ -29,6 +31,7 @@ struct Value { bool boolean; Function* function; BuiltinFunction* builtin_function; + Thunk* thunk; }; ValueType type; std::string string_value; // Store strings outside the union for safety @@ -42,6 +45,7 @@ struct Value { Value(std::string&& s) : type(ValueType::VAL_STRING), string_value(std::move(s)) {} Value(Function* f) : function(f), type(ValueType::VAL_FUNCTION) {} Value(BuiltinFunction* bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {} + Value(Thunk* t) : thunk(t), type(ValueType::VAL_THUNK) {} // Move constructor Value(Value&& other) noexcept @@ -94,6 +98,7 @@ struct Value { inline bool isString() const { return type == ValueType::VAL_STRING; } inline bool isFunction() const { return type == ValueType::VAL_FUNCTION; } inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_FUNCTION; } + inline bool isThunk() const { return type == ValueType::VAL_THUNK; } inline bool isNone() const { return type == ValueType::VAL_NONE; } // Value extraction (safe, with type checking) - inline for performance @@ -102,6 +107,7 @@ struct Value { inline const std::string& asString() const { return string_value; } inline Function* asFunction() const { return isFunction() ? function : nullptr; } inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function : nullptr; } + inline Thunk* asThunk() const { return isThunk() ? thunk : nullptr; } // Truthiness check - inline for performance inline bool isTruthy() const { @@ -112,6 +118,7 @@ struct Value { case ValueType::VAL_STRING: return !string_value.empty(); case ValueType::VAL_FUNCTION: return function != nullptr; case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function != nullptr; + case ValueType::VAL_THUNK: return thunk != nullptr; default: return false; } } @@ -127,6 +134,7 @@ struct Value { case ValueType::VAL_STRING: return string_value == other.string_value; case ValueType::VAL_FUNCTION: return function == other.function; case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function == other.builtin_function; + case ValueType::VAL_THUNK: return thunk == other.thunk; default: return false; } } @@ -151,6 +159,7 @@ struct Value { case ValueType::VAL_STRING: return string_value; case ValueType::VAL_FUNCTION: return ""; case ValueType::VAL_BUILTIN_FUNCTION: return ""; + case ValueType::VAL_THUNK: return ""; default: return "unknown"; } } diff --git a/source/Interpreter.cpp b/source/Interpreter.cpp index ee1e401..423ffc9 100644 --- a/source/Interpreter.cpp +++ b/source/Interpreter.cpp @@ -24,6 +24,10 @@ struct ReturnContext { ReturnContext() : returnValue(NONE_VALUE), hasReturn(false) {} }; +// Trampoline-based tail call optimization - no exceptions needed + + + Value Interpreter::visitLiteralExpr(const std::shared_ptr& expr) { if(expr->isNull) return NONE_VALUE; @@ -596,27 +600,65 @@ Value Interpreter::visitCallExpr(const std::shared_ptr& expression) { " arguments but got " + std::to_string(arguments.size()) + "."); } - auto previousEnv = environment; - environment = std::make_shared(function->closure); - environment->setErrorReporter(errorReporter); - - for (size_t i = 0; i < function->params.size(); i++) { - environment->define(function->params[i], arguments[i]); - } - - ExecutionContext context; - context.isFunctionBody = true; - - for (const auto& stmt : function->body) { - execute(stmt, &context); - if (context.hasReturn) { + // Check if this is a tail call + if (expression->isTailCall) { + // Create a thunk for tail call optimization + auto thunk = new Thunk([this, function, arguments]() -> Value { + // Set up the environment for the tail call + auto previousEnv = environment; + environment = std::make_shared(function->closure); + environment->setErrorReporter(errorReporter); + + for (size_t i = 0; i < function->params.size(); i++) { + environment->define(function->params[i], arguments[i]); + } + + ExecutionContext context; + context.isFunctionBody = true; + + // Use RAII to manage thunk execution flag + ScopedThunkFlag _inThunk(inThunkExecution); + + // Execute function body + for (const auto& stmt : function->body) { + execute(stmt, &context); + if (context.hasReturn) { + environment = previousEnv; + return context.returnValue; + } + } + environment = previousEnv; return context.returnValue; + }); + + // Return the thunk as a Value + return Value(thunk); + } else { + // Normal function call - create new environment + auto previousEnv = environment; + environment = std::make_shared(function->closure); + environment->setErrorReporter(errorReporter); + + for (size_t i = 0; i < function->params.size(); i++) { + environment->define(function->params[i], arguments[i]); } + + ExecutionContext context; + context.isFunctionBody = true; + + // Execute function body + for (const auto& stmt : function->body) { + execute(stmt, &context); + if (context.hasReturn) { + environment = previousEnv; + return context.returnValue; + } + } + + environment = previousEnv; + return context.returnValue; } - - environment = previousEnv; - return context.returnValue; } throw std::runtime_error("Can only call functions and classes."); @@ -682,6 +724,8 @@ void Interpreter::visitReturnStmt(const std::shared_ptr& statement, { Value value = NONE_VALUE; if (statement->value != nullptr) { + // For tail calls, the trampoline handling is done in visitCallExpr + // We just need to evaluate normally value = evaluate(statement->value); } @@ -731,9 +775,28 @@ void Interpreter::executeBlock(std::vector > statements, s } Value Interpreter::evaluate(const std::shared_ptr& expr) { + Value result = expr->accept(this); + if (inThunkExecution) { + return result; // Don't use trampoline when inside a thunk + } + return runTrampoline(result); +} + +Value Interpreter::evaluateWithoutTrampoline(const std::shared_ptr& expr) { return expr->accept(this); } +Value Interpreter::runTrampoline(Value initialResult) { + Value current = initialResult; + + while (current.isThunk()) { + // Execute the thunk to get the next result + current = current.asThunk()->execute(); + } + + return current; +} + bool Interpreter::isTruthy(Value object) { if(object.isBoolean())