A simple scripting language in C++
Ferenc Szontágh
2025-04-18 9a186053a690f2216b43355549c8aa7e2959583c
better error reporting
10 files modified
138 ■■■■■ changed files
src/Interpreter/AssignmentStatementNode.hpp 49 ●●●●● patch | view | raw | blame | history
src/Interpreter/CallStatementNode.hpp 10 ●●●● patch | view | raw | blame | history
src/Interpreter/ConditionalStatementNode.hpp 13 ●●●● patch | view | raw | blame | history
src/Interpreter/DeclareFunctionStatementNode.hpp 5 ●●●●● patch | view | raw | blame | history
src/Interpreter/DeclareVariableStatementNode.hpp 13 ●●●● patch | view | raw | blame | history
src/Interpreter/ForStatementNode.hpp 9 ●●●●● patch | view | raw | blame | history
src/Interpreter/Interpreter.hpp 17 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 3 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.hpp 9 ●●●●● patch | view | raw | blame | history
test_scripts/object.vs 10 ●●●● patch | view | raw | blame | history
src/Interpreter/AssignmentStatementNode.hpp
@@ -2,6 +2,8 @@
#define INTERPRETER_ASSIGNMENT_STATEMENT_NODE_HPP
#include "StatementNode.hpp"
// Include for unified runtime Exception
#include "Interpreter/Interpreter.hpp"
#include "ExpressionNode.hpp"
#include "Symbols/SymbolContainer.hpp"
#include "Symbols/Value.hpp"
@@ -36,9 +38,9 @@
        const std::string base_ns = symContainer->currentScopeName();
        const std::string var_ns = base_ns + ".variables";
        if (!symContainer->exists(targetName_, var_ns)) {
            throw std::runtime_error("Variable '" + targetName_ + "' does not exist in namespace: " + var_ns +
                                     " File: " + filename_ + ", Line: " + std::to_string(line_) +
                                     ", Column: " + std::to_string(column_));
            throw Exception(
                "Variable '" + targetName_ + "' does not exist in namespace: " + var_ns,
                filename_, line_, column_);
        }
        auto symbol = symContainer->get(var_ns, targetName_);
        // Copy current value for potential nested updates
@@ -48,20 +50,22 @@
        // Simple variable assignment
        if (propertyPath_.empty()) {
            // Type check
            if (newValue.getType() != varValue.getType()) {
                using namespace Variables;
                throw std::runtime_error("Type mismatch assigning to variable '" + targetName_ +
                                         "': expected '" + TypeToString(varValue.getType()) +
                                         "' but got '" + TypeToString(newValue.getType()) +
                                         "' File: " + filename_ + ", Line: " + std::to_string(line_) +
                                         ", Column: " + std::to_string(column_));
            }
        if (newValue.getType() != varValue.getType()) {
            using namespace Variables;
            throw Exception(
                "Type mismatch assigning to variable '" + targetName_ +
                "': expected '" + TypeToString(varValue.getType()) +
                "' but got '" + TypeToString(newValue.getType()) + "'",
                filename_, line_, column_);
        }
            symbol->setValue(newValue);
            return;
        }
        // Nested object property assignment
        if (varValue.getType() != Variables::Type::OBJECT) {
            throw std::runtime_error("Attempting to assign property on non-object variable '" + targetName_ + "'");
            throw Exception(
                "Attempting to assign property on non-object variable '" + targetName_ + "'",
                filename_, line_, column_);
        }
        // Traverse into nested maps
        using ObjectMap = Value::ObjectMap;
@@ -71,11 +75,15 @@
            const auto & key = propertyPath_[i];
            auto it = currMap->find(key);
            if (it == currMap->end()) {
                throw std::runtime_error("Property '" + key + "' not found on object '" + targetName_ + "'");
                throw Exception(
                    "Property '" + key + "' not found on object '" + targetName_ + "'",
                    filename_, line_, column_);
            }
            Value & child = it->second;
            if (child.getType() != Variables::Type::OBJECT) {
                throw std::runtime_error("Property '" + key + "' is not an object, cannot assign nested property");
                throw Exception(
                    "Property '" + key + "' is not an object, cannot assign nested property",
                    filename_, line_, column_);
            }
            currMap = &std::get<ObjectMap>(child.get());
        }
@@ -83,15 +91,18 @@
        const std::string & lastKey = propertyPath_.back();
        auto it = currMap->find(lastKey);
        if (it == currMap->end()) {
            throw std::runtime_error("Property '" + lastKey + "' not found on object '" + targetName_ + "'");
            throw Exception(
                "Property '" + lastKey + "' not found on object '" + targetName_ + "'",
                filename_, line_, column_);
        }
        // Type check against existing property
        if (newValue.getType() != it->second.getType()) {
            using namespace Variables;
            throw std::runtime_error("Type mismatch for property '" + lastKey + "': expected '" +
                                     TypeToString(it->second.getType()) + "' but got '" +
                                     TypeToString(newValue.getType()) + "' File: " + filename_ +
                                     ", Line: " + std::to_string(line_) + ", Column: " + std::to_string(column_));
            throw Exception(
                "Type mismatch for property '" + lastKey + "': expected '" +
                TypeToString(it->second.getType()) + "' but got '" +
                TypeToString(newValue.getType()) + "'",
                filename_, line_, column_);
        }
        // Assign and write back to symbol
        (*currMap)[lastKey] = newValue;
src/Interpreter/CallStatementNode.hpp
@@ -7,6 +7,8 @@
#include "ExpressionNode.hpp"
#include "Interpreter/Interpreter.hpp"
// Include for unified runtime Exception (inherits BaseException)
#include "BaseException.hpp"
#include "Interpreter/OperationContainer.hpp"
#include "StatementNode.hpp"
#include "Symbols/FunctionSymbol.hpp"
@@ -54,15 +56,17 @@
        const std::string fnSymNs   = currentNs + ".functions";
        auto              sym       = sc->get(fnSymNs, functionName_);
        if (!sym || sym->getKind() != Kind::Function) {
            throw std::runtime_error("Function not found: " + functionName_);
            throw Exception("Function not found: " + functionName_, filename_, line_, column_);
        }
        auto funcSym = std::static_pointer_cast<FunctionSymbol>(sym);
        // Check parameter count
        const auto & params = funcSym->parameters();
        if (params.size() != argValues.size()) {
            throw std::runtime_error("Function '" + functionName_ + "' expects " + std::to_string(params.size()) +
                                     " args, got " + std::to_string(argValues.size()));
            throw Exception(
                "Function '" + functionName_ + "' expects " + std::to_string(params.size()) +
                " args, got " + std::to_string(argValues.size()),
                filename_, line_, column_);
        }
        // Enter function scope to bind parameters and execute body
src/Interpreter/ConditionalStatementNode.hpp
@@ -4,8 +4,10 @@
 #include <vector>
 #include <memory>
 #include <string>
 #include "Interpreter/StatementNode.hpp"
 #include "Interpreter/ExpressionNode.hpp"
#include "Interpreter/StatementNode.hpp"
// Include for unified runtime Exception
#include "Interpreter/Interpreter.hpp"
#include "Interpreter/ExpressionNode.hpp"
 namespace Interpreter {
@@ -36,10 +38,9 @@
         bool cond = false;
         if (val.getType() == Symbols::Variables::Type::BOOLEAN) {
             cond = val.get<bool>();
         } else {
             throw std::runtime_error("Condition did not evaluate to boolean at " + filename_ +
                                      ":" + std::to_string(line_) + "," + std::to_string(column_));
         }
        } else {
            throw Exception("Condition did not evaluate to boolean", filename_, line_, column_);
        }
         // Execute appropriate branch
         const auto & branch = cond ? thenBranch_ : elseBranch_;
         for (const auto & stmt : branch) {
src/Interpreter/DeclareFunctionStatementNode.hpp
@@ -8,6 +8,8 @@
#include "ExpressionNode.hpp"
#include "Interpreter.hpp"
#include "Interpreter/StatementNode.hpp"
// Include for unified runtime Exception
#include "Interpreter/Interpreter.hpp"
#include "Symbols/ParameterContainer.hpp"
#include "Symbols/SymbolContainer.hpp"
#include "Symbols/SymbolFactory.hpp"
@@ -37,8 +39,7 @@
    void interpret(Interpreter & /*interpreter*/) const override {
        //Symbols::Value value = expression_->evaluate(interpreter);
        if (Symbols::SymbolContainer::instance()->exists(functionName_)) {
            throw std::runtime_error("Function already declared: " + functionName_ + " file: " + filename_ +
                                     ", line: " + std::to_string(line_) + ", column: " + std::to_string(column_));
            throw Exception("Function already declared: " + functionName_, filename_, line_, column_);
        }
        const auto func = Symbols::SymbolFactory::createFunction(functionName_, ns, params_, "", returnType_);
        Symbols::SymbolContainer::instance()->add(func);
src/Interpreter/DeclareVariableStatementNode.hpp
@@ -8,6 +8,8 @@
#include "ExpressionNode.hpp"
#include "Interpreter.hpp"
#include "Interpreter/StatementNode.hpp"
// Include for unified runtime Exception
#include "Interpreter/Interpreter.hpp"
#include "Symbols/SymbolContainer.hpp"
#include "Symbols/SymbolFactory.hpp"
@@ -35,18 +37,17 @@
        Symbols::Value value = expression_->evaluate(interpreter);
        // Check for duplicate declaration
        if (Symbols::SymbolContainer::instance()->exists(variableName_)) {
            throw std::runtime_error("Variable already declared: " + variableName_ + " File: " + filename_ +
                                     ", Line: " + std::to_string(line_) + ", Column: " + std::to_string(column_));
            throw Exception("Variable already declared: " + variableName_, filename_, line_, column_);
        }
        // Enforce type correctness: the evaluated value must match the declared type
        if (value.getType() != variableType_) {
            using namespace Symbols::Variables;
            std::string expected = TypeToString(variableType_);
            std::string actual   = TypeToString(value.getType());
            throw std::runtime_error("Type mismatch for variable '" + variableName_ +
                                     "': expected '" + expected + "' but got '" + actual +
                                     "' File: " + filename_ + ", Line: " + std::to_string(line_) +
                                     ", Column: " + std::to_string(column_));
            throw Exception(
                "Type mismatch for variable '" + variableName_ +
                "': expected '" + expected + "' but got '" + actual + "'",
                filename_, line_, column_);
        }
        // Create and add the variable symbol
        const auto variable = Symbols::SymbolFactory::createVariable(variableName_, value, ns, variableType_);
src/Interpreter/ForStatementNode.hpp
@@ -5,8 +5,10 @@
 #include <memory>
 #include <string>
 #include <stdexcept>
 #include "Interpreter/StatementNode.hpp"
 #include "Interpreter/ExpressionNode.hpp"
#include "Interpreter/StatementNode.hpp"
// Include for unified runtime Exception
#include "Interpreter/Interpreter.hpp"
#include "Interpreter/ExpressionNode.hpp"
 #include "Symbols/Value.hpp"
 #include "Symbols/SymbolContainer.hpp"
 #include "Symbols/SymbolFactory.hpp"
@@ -45,8 +47,7 @@
        // Evaluate iterable expression
        auto iterableVal = iterableExpr_->evaluate(interpreter);
        if (iterableVal.getType() != Variables::Type::OBJECT) {
            throw std::runtime_error("For-in loop applied to non-object at " + filename_ + ":" + std::to_string(line_) +
                                     "," + std::to_string(column_));
            throw Exception("For-in loop applied to non-object", filename_, line_, column_);
        }
        // Access underlying object map
        const auto & objMap = std::get<Value::ObjectMap>(iterableVal.get());
src/Interpreter/Interpreter.hpp
@@ -7,6 +7,23 @@
#include "Interpreter/Operation.hpp"
#include "Interpreter/OperationContainer.hpp"
#include "Symbols/SymbolContainer.hpp"
#include "BaseException.hpp"
// Exception type for runtime errors, includes file, line, and column context
namespace Interpreter {
class Exception : public BaseException {
public:
    Exception(const std::string &msg, const std::string &filename, int line, size_t column) {
        rawMessage_ = msg;
        context_ = std::string(" in file \"") + filename + "\" at line: " + std::to_string(line)
                   + ", column: " + std::to_string(column);
        formattedMessage_ = formatMessage();
    }
    std::string formatMessage() const override {
        return std::string("[Runtime ERROR] >>") + context_ + " << : " + rawMessage_;
    }
};
} // namespace Interpreter
namespace Interpreter {
src/Parser/Parser.cpp
@@ -1,4 +1,6 @@
#include "Parser/Parser.hpp"
// Static filename for unified error reporting in Parser::Exception
std::string Parser::Parser::Exception::current_filename_;
#include <stack>
@@ -781,6 +783,7 @@
void Parser::parseScript(const std::vector<Lexer::Tokens::Token> & tokens, std::string_view input_string,
                         const std::string & filename) {
    ::Parser::Parser::Exception::current_filename_ = filename;
    tokens_              = tokens;
    input_str_view_      = input_string;
    current_token_index_ = 0;
src/Parser/Parser.hpp
@@ -22,11 +22,13 @@
    class Exception : public BaseException {
      public:
        using BaseException::BaseException;
        // Filename for error reporting
        static std::string current_filename_;
        Exception(const std::string & msg, const std::string & expected, const Lexer::Tokens::Token & token) {
            rawMessage_ = msg + ": " + token.dump();
            context_ =
                " at line: " + std::to_string(token.line_number) + ", column: " + std::to_string(token.column_number);
            context_ = " in file \"" + current_filename_ + "\" at line: " + std::to_string(token.line_number)
                       + ", column: " + std::to_string(token.column_number);
            if (expected.empty() == false) {
                rawMessage_ += " (expected: " + expected + ")";
            }
@@ -38,7 +40,8 @@
            if (expected.empty() == false) {
                rawMessage_ += " (expected: " + expected + ")";
            }
            context_          = " at line: " + std::to_string(line) + ", column: " + std::to_string(col);
            context_ = " in file \"" + current_filename_ + "\" at line: " + std::to_string(line)
                       + ", column: " + std::to_string(col);
            formattedMessage_ = formatMessage();
        }
test_scripts/object.vs
@@ -1,6 +1,6 @@
object $person = {
    string name: "Szoni",
    string name: "Batman",
    int age: 37
};
@@ -8,7 +8,7 @@
object $person2 = {
    string name: "Not Szoni",
    string name: "Not Batman",
    int age: 37,
    object children: {
        string name: "Child1",
@@ -24,9 +24,9 @@
printnl("Person name: ", $person_name);
printnl("Child1 old age: ",$person2->children->age);
$person2->children->age = $person2->children->age + 2;
printnl("Child1 new age: ",$person2->children->age);
printnl("Child1 old age: ", $person2->children->age);
$person2->children->age = 22;
printnl("Child1 new age: ", $person2->children->age);
int $age = 10;
if ($person2->children->age > 18) {