Tail call testing

This commit is contained in:
Bobby Lucero 2025-08-05 19:06:52 -04:00
parent 313c996edd
commit b87b342dff
3 changed files with 129 additions and 18 deletions

View File

@ -12,6 +12,38 @@
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
#include <stack> #include <stack>
#include <optional>
#include <functional>
// 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<Value()>;
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 { class Interpreter : public ExprVisitor, public StmtVisitor {
@ -46,14 +78,21 @@ private:
std::vector<std::shared_ptr<BuiltinFunction> > builtinFunctions; std::vector<std::shared_ptr<BuiltinFunction> > builtinFunctions;
std::vector<std::shared_ptr<Function> > functions; std::vector<std::shared_ptr<Function> > functions;
ErrorReporter* errorReporter; ErrorReporter* errorReporter;
bool inThunkExecution = false;
Value evaluate(const std::shared_ptr<Expr>& expr); Value evaluate(const std::shared_ptr<Expr>& expr);
Value evaluateWithoutTrampoline(const std::shared_ptr<Expr>& expr);
bool isEqual(Value a, Value b); bool isEqual(Value a, Value b);
bool isWholeNumer(double num); bool isWholeNumer(double num);
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context = nullptr); void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context = nullptr);
void executeBlock(std::vector<std::shared_ptr<Stmt> > statements, std::shared_ptr<Environment> env, ExecutionContext* context = nullptr); void executeBlock(std::vector<std::shared_ptr<Stmt> > statements, std::shared_ptr<Environment> env, ExecutionContext* context = nullptr);
void addStdLibFunctions(); void addStdLibFunctions();
// Trampoline execution
Value runTrampoline(Value initialResult);
public: public:
bool isTruthy(Value object); bool isTruthy(Value object);
std::string stringify(Value object); std::string stringify(Value object);

View File

@ -11,6 +11,7 @@
class Environment; class Environment;
class Function; class Function;
class BuiltinFunction; class BuiltinFunction;
class Thunk;
// Type tags for the Value union // Type tags for the Value union
enum ValueType { enum ValueType {
@ -19,7 +20,8 @@ enum ValueType {
VAL_BOOLEAN, VAL_BOOLEAN,
VAL_STRING, VAL_STRING,
VAL_FUNCTION, VAL_FUNCTION,
VAL_BUILTIN_FUNCTION VAL_BUILTIN_FUNCTION,
VAL_THUNK
}; };
// Tagged value system (like Lua) - no heap allocation for simple values // Tagged value system (like Lua) - no heap allocation for simple values
@ -29,6 +31,7 @@ struct Value {
bool boolean; bool boolean;
Function* function; Function* function;
BuiltinFunction* builtin_function; BuiltinFunction* builtin_function;
Thunk* thunk;
}; };
ValueType type; ValueType type;
std::string string_value; // Store strings outside the union for safety 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(std::string&& s) : type(ValueType::VAL_STRING), string_value(std::move(s)) {}
Value(Function* f) : function(f), type(ValueType::VAL_FUNCTION) {} Value(Function* f) : function(f), type(ValueType::VAL_FUNCTION) {}
Value(BuiltinFunction* bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {} Value(BuiltinFunction* bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {}
Value(Thunk* t) : thunk(t), type(ValueType::VAL_THUNK) {}
// Move constructor // Move constructor
Value(Value&& other) noexcept Value(Value&& other) noexcept
@ -94,6 +98,7 @@ struct Value {
inline bool isString() const { return type == ValueType::VAL_STRING; } inline bool isString() const { return type == ValueType::VAL_STRING; }
inline bool isFunction() const { return type == ValueType::VAL_FUNCTION; } inline bool isFunction() const { return type == ValueType::VAL_FUNCTION; }
inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_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; } inline bool isNone() const { return type == ValueType::VAL_NONE; }
// Value extraction (safe, with type checking) - inline for performance // 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 const std::string& asString() const { return string_value; }
inline Function* asFunction() const { return isFunction() ? function : nullptr; } inline Function* asFunction() const { return isFunction() ? function : nullptr; }
inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function : nullptr; } inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function : nullptr; }
inline Thunk* asThunk() const { return isThunk() ? thunk : nullptr; }
// Truthiness check - inline for performance // Truthiness check - inline for performance
inline bool isTruthy() const { inline bool isTruthy() const {
@ -112,6 +118,7 @@ struct Value {
case ValueType::VAL_STRING: return !string_value.empty(); case ValueType::VAL_STRING: return !string_value.empty();
case ValueType::VAL_FUNCTION: return function != nullptr; case ValueType::VAL_FUNCTION: return function != nullptr;
case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function != nullptr; case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function != nullptr;
case ValueType::VAL_THUNK: return thunk != nullptr;
default: return false; default: return false;
} }
} }
@ -127,6 +134,7 @@ struct Value {
case ValueType::VAL_STRING: return string_value == other.string_value; case ValueType::VAL_STRING: return string_value == other.string_value;
case ValueType::VAL_FUNCTION: return function == other.function; case ValueType::VAL_FUNCTION: return function == other.function;
case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function == other.builtin_function; case ValueType::VAL_BUILTIN_FUNCTION: return builtin_function == other.builtin_function;
case ValueType::VAL_THUNK: return thunk == other.thunk;
default: return false; default: return false;
} }
} }
@ -151,6 +159,7 @@ struct Value {
case ValueType::VAL_STRING: return string_value; case ValueType::VAL_STRING: return string_value;
case ValueType::VAL_FUNCTION: return "<function>"; case ValueType::VAL_FUNCTION: return "<function>";
case ValueType::VAL_BUILTIN_FUNCTION: return "<builtin_function>"; case ValueType::VAL_BUILTIN_FUNCTION: return "<builtin_function>";
case ValueType::VAL_THUNK: return "<thunk>";
default: return "unknown"; default: return "unknown";
} }
} }

View File

@ -24,6 +24,10 @@ struct ReturnContext {
ReturnContext() : returnValue(NONE_VALUE), hasReturn(false) {} ReturnContext() : returnValue(NONE_VALUE), hasReturn(false) {}
}; };
// Trampoline-based tail call optimization - no exceptions needed
Value Interpreter::visitLiteralExpr(const std::shared_ptr<LiteralExpr>& expr) { Value Interpreter::visitLiteralExpr(const std::shared_ptr<LiteralExpr>& expr) {
if(expr->isNull) return NONE_VALUE; if(expr->isNull) return NONE_VALUE;
@ -596,27 +600,65 @@ Value Interpreter::visitCallExpr(const std::shared_ptr<CallExpr>& expression) {
" arguments but got " + std::to_string(arguments.size()) + "."); " arguments but got " + std::to_string(arguments.size()) + ".");
} }
auto previousEnv = environment; // Check if this is a tail call
environment = std::make_shared<Environment>(function->closure); if (expression->isTailCall) {
environment->setErrorReporter(errorReporter); // 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<Environment>(function->closure);
environment->setErrorReporter(errorReporter);
for (size_t i = 0; i < function->params.size(); i++) { for (size_t i = 0; i < function->params.size(); i++) {
environment->define(function->params[i], arguments[i]); environment->define(function->params[i], arguments[i]);
} }
ExecutionContext context; ExecutionContext context;
context.isFunctionBody = true; 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;
}
}
for (const auto& stmt : function->body) {
execute(stmt, &context);
if (context.hasReturn) {
environment = previousEnv; environment = previousEnv;
return context.returnValue; return context.returnValue;
} });
}
environment = previousEnv; // Return the thunk as a Value
return context.returnValue; return Value(thunk);
} else {
// Normal function call - create new environment
auto previousEnv = environment;
environment = std::make_shared<Environment>(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;
}
} }
throw std::runtime_error("Can only call functions and classes."); throw std::runtime_error("Can only call functions and classes.");
@ -682,6 +724,8 @@ void Interpreter::visitReturnStmt(const std::shared_ptr<ReturnStmt>& statement,
{ {
Value value = NONE_VALUE; Value value = NONE_VALUE;
if (statement->value != nullptr) { if (statement->value != nullptr) {
// For tail calls, the trampoline handling is done in visitCallExpr
// We just need to evaluate normally
value = evaluate(statement->value); value = evaluate(statement->value);
} }
@ -731,9 +775,28 @@ void Interpreter::executeBlock(std::vector<std::shared_ptr<Stmt> > statements, s
} }
Value Interpreter::evaluate(const std::shared_ptr<Expr>& expr) { Value Interpreter::evaluate(const std::shared_ptr<Expr>& 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>& expr) {
return expr->accept(this); 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) { bool Interpreter::isTruthy(Value object) {
if(object.isBoolean()) if(object.isBoolean())