A simple scripting language in C++
Ferenc Szontágh
2025-04-18 68693bb7b71bb5721bdda9dc16948b750ea71065
fix object accessing
7 files modified
9 files added
211 ■■■■ changed files
src/Interpreter/DeclareVariableStatementNode.hpp 14 ●●●●● patch | view | raw | blame | history
src/Interpreter/ExpressionBuilder.hpp 16 ●●●●● patch | view | raw | blame | history
src/Interpreter/MemberExpressionNode.hpp 40 ●●●●● patch | view | raw | blame | history
src/Lexer/Lexer.cpp 3 ●●●● patch | view | raw | blame | history
src/Lexer/Operators.cpp 2 ●●● patch | view | raw | blame | history
src/Lexer/Operators.hpp 38 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 44 ●●●● patch | view | raw | blame | history
temp_test.vs 1 ●●●● patch | view | raw | blame | history
test_scripts/object.vs 32 ●●●●● patch | view | raw | blame | history
tmp2.vs 1 ●●●● patch | view | raw | blame | history
tmp_invalid.vs 1 ●●●● patch | view | raw | blame | history
tmp_test.vs 6 ●●●●● patch | view | raw | blame | history
tmp_test2.vs 1 ●●●● patch | view | raw | blame | history
tmp_test3.vs 4 ●●●● patch | view | raw | blame | history
tmp_test4.vs 4 ●●●● patch | view | raw | blame | history
tmp_valid.vs 4 ●●●● patch | view | raw | blame | history
src/Interpreter/DeclareVariableStatementNode.hpp
@@ -31,13 +31,25 @@
        ns(ns) {}
    void interpret(Interpreter & interpreter) const override {
        // Evaluate the expression and enforce declared type matches actual value type
        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_));
        }
        // 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_));
        }
        // Create and add the variable symbol
        const auto variable = Symbols::SymbolFactory::createVariable(variableName_, value, ns, variableType_);
        Symbols::SymbolContainer::instance()->add(variable);
    }
src/Interpreter/ExpressionBuilder.hpp
@@ -10,6 +10,8 @@
#include "Interpreter/LiteralExpressionNode.hpp"
#include "Interpreter/UnaryExpressionNode.hpp"  // <-- új include
#include "Interpreter/CallExpressionNode.hpp"
#include "Interpreter/MemberExpressionNode.hpp"
#include "Interpreter/ObjectExpressionNode.hpp"
#include "Interpreter/ObjectExpressionNode.hpp"
#include "Parser/ParsedExpression.hpp"
@@ -27,6 +29,20 @@
        case Kind::Binary:
            {
                if (expr->op == "->") {
                    auto objExpr = buildExpressionFromParsed(expr->lhs);
                    std::string propName;
                    // RHS parsed expression should be a literal string or variable parser node
                    if (expr->rhs->kind == ParsedExpression::Kind::Literal &&
                        expr->rhs->value.getType() == Symbols::Variables::Type::STRING) {
                        propName = expr->rhs->value.get<std::string>();
                    } else if (expr->rhs->kind == ParsedExpression::Kind::Variable) {
                        propName = expr->rhs->name;
                    } else {
                        throw std::runtime_error("Invalid property name in member access");
                    }
                    return std::make_unique<Interpreter::MemberExpressionNode>(std::move(objExpr), propName);
                }
                auto lhs = buildExpressionFromParsed(expr->lhs);
                auto rhs = buildExpressionFromParsed(expr->rhs);
                return std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), expr->op, std::move(rhs));
src/Interpreter/MemberExpressionNode.hpp
New file
@@ -0,0 +1,40 @@
#ifndef INTERPRETER_MEMBER_EXPRESSION_NODE_HPP
#define INTERPRETER_MEMBER_EXPRESSION_NODE_HPP
#include "ExpressionNode.hpp"
#include "Symbols/Value.hpp"
#include <stdexcept>
namespace Interpreter {
// Expression node for member access: object->property
class MemberExpressionNode : public ExpressionNode {
  public:
    MemberExpressionNode(std::unique_ptr<ExpressionNode> objectExpr, std::string propertyName)
        : objectExpr_(std::move(objectExpr)), propertyName_(std::move(propertyName)) {}
    Symbols::Value evaluate(Interpreter & interpreter) const override {
        Symbols::Value objVal = objectExpr_->evaluate(interpreter);
        if (objVal.getType() != Symbols::Variables::Type::OBJECT) {
            throw std::runtime_error("Attempted to access member '" + propertyName_ + "' of non-object");
        }
        const auto &map = std::get<Symbols::Value::ObjectMap>(objVal.get());
        auto it = map.find(propertyName_);
        if (it == map.end()) {
            throw std::runtime_error("Property '" + propertyName_ + "' not found on object");
        }
        return it->second;
    }
    std::string toString() const override {
        return objectExpr_->toString() + "->" + propertyName_;
    }
  private:
    std::unique_ptr<ExpressionNode> objectExpr_;
    std::string                     propertyName_;
};
} // namespace Interpreter
#endif // INTERPRETER_MEMBER_EXPRESSION_NODE_HPP
src/Lexer/Lexer.cpp
@@ -272,7 +272,8 @@
            { &OPERATOR_RELATIONAL, Tokens::Type::OPERATOR_RELATIONAL },
            { &OPERATOR_INCREMENT,  Tokens::Type::OPERATOR_INCREMENT  },
            { &OPERATOR_ASSIGNMENT, Tokens::Type::OPERATOR_ASSIGNMENT },
            { &OPERATOR_LOGICAL,    Tokens::Type::OPERATOR_LOGICAL    }
            { &OPERATOR_LOGICAL,    Tokens::Type::OPERATOR_LOGICAL    },
            { &PUNCTUATION,         Tokens::Type::PUNCTUATION         }
        };
        for (const auto & [vec_ptr, type] : two_char_op_types) {
src/Lexer/Operators.cpp
@@ -8,7 +8,7 @@
const std::vector<std::string> OPERATOR_LOGICAL    = { "&&", "||" };
const std::vector<std::string> OPERATOR_ARITHMETIC = { "+", "-", "*", "/", "%", "!" };
const std::vector<std::string> PUNCTUATION         = { "(", ")", "{", "}", "[", "]", ",", ";", ":" };
const std::vector<std::string> PUNCTUATION         = { "(", ")", "{", "}", "[", "]", ",", ";", ":", "->" };
bool contains(const std::vector<std::string> & vec, const std::string & value) {
    return std::find(vec.begin(), vec.end(), value) != vec.end();
src/Lexer/Operators.hpp
@@ -24,6 +24,9 @@
bool isBinaryOperator(const std::string & op);
inline int getPrecedence(const std::string & op) {
    if (op == "->") {
        return 5;  // Member access has highest precedence
    }
    if (op == "u-" || op == "u+" || op == "u!") {
        return 4;
    }
@@ -59,25 +62,36 @@
                                      std::vector<Parser::ParsedExpressionPtr> & output_queue) {
    // Literal operands: number, string, or keyword literals (e.g., true/false/null)
    if (token.type == Tokens::Type::NUMBER) {
        // Numeric literal: auto-detect integer/double/float
        output_queue.push_back(
            Parser::ParsedExpression::makeLiteral(
                Symbols::Value::fromString(token.value, /*autoDetectType*/ true)));
        // Numeric literal: only allowed if expected is numeric or unspecified
        if (expected_var_type != Symbols::Variables::Type::NULL_TYPE &&
            expected_var_type != Symbols::Variables::Type::INTEGER &&
            expected_var_type != Symbols::Variables::Type::DOUBLE &&
            expected_var_type != Symbols::Variables::Type::FLOAT) {
            return false;
        }
        // Auto-detect or cast to expected numeric type
        auto val = Symbols::Value::fromString(token.value, /*autoDetectType*/ true);
        output_queue.push_back(Parser::ParsedExpression::makeLiteral(val));
        return true;
    }
    if (token.type == Tokens::Type::STRING_LITERAL) {
        // String literal: use literal value
        output_queue.push_back(
            Parser::ParsedExpression::makeLiteral(
                Symbols::Value(token.value)));
        // String literal: only allowed if expected is string or unspecified
        if (expected_var_type != Symbols::Variables::Type::NULL_TYPE &&
            expected_var_type != Symbols::Variables::Type::STRING) {
            return false;
        }
        output_queue.push_back(Parser::ParsedExpression::makeLiteral(Symbols::Value(token.value)));
        return true;
    }
    if (token.type == Tokens::Type::KEYWORD) {
        // Keyword literal: e.g., true, false, null
        // Auto-detect boolean or null as needed
        output_queue.push_back(
            Parser::ParsedExpression::makeLiteral(
                Symbols::Value::fromString(token.value, /*autoDetectType*/ true)));
        auto val = Symbols::Value::fromString(token.value, /*autoDetectType*/ true);
        auto vtype = val.getType();
        // only allowed if expected matches or unspecified
        if (expected_var_type != Symbols::Variables::Type::NULL_TYPE && expected_var_type != vtype) {
            return false;
        }
        output_queue.push_back(Parser::ParsedExpression::makeLiteral(val));
        return true;
    }
    if (token.type == Tokens::Type::VARIABLE_IDENTIFIER) {
src/Parser/Parser.cpp
@@ -406,7 +406,32 @@
            continue;
        }
        if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.lexeme == "(") {
        // Member access: '->'
        else if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.lexeme == "->") {
            std::string op(token.lexeme);
            // Shunting-yard: handle operator precedence
            while (!operator_stack.empty()) {
                const std::string & top = operator_stack.top();
                if ((Lexer::isLeftAssociative(op) && Lexer::getPrecedence(op) <= Lexer::getPrecedence(top)) ||
                    (!Lexer::isLeftAssociative(op) && Lexer::getPrecedence(op) < Lexer::getPrecedence(top))) {
                    operator_stack.pop();
                    // Binary operator: pop two operands
                    if (output_queue.size() < 2) {
                        Parser::reportError("Malformed expression", token);
                    }
                    auto rhs = std::move(output_queue.back()); output_queue.pop_back();
                    auto lhs = std::move(output_queue.back()); output_queue.pop_back();
                    output_queue.push_back(Lexer::applyOperator(op, std::move(rhs), std::move(lhs)));
                } else {
                    break;
                }
            }
            operator_stack.push(op);
            consumeToken();
            expect_unary = true;
        }
        // Grouping parentheses
        else if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.lexeme == "(") {
            operator_stack.push("(");
            consumeToken();
            expect_unary = true;
@@ -520,11 +545,18 @@
            operator_stack.push(op);
            consumeToken();
            expect_unary = true;
        } else if (token.type == Lexer::Tokens::Type::NUMBER || token.type == Lexer::Tokens::Type::STRING_LITERAL ||
                   token.type == Lexer::Tokens::Type::KEYWORD ||
                   token.type == Lexer::Tokens::Type::VARIABLE_IDENTIFIER) {
            if (Lexer::pushOperand(token, expected_var_type, output_queue) == false) {
                Parser::reportError("Invalid type", token, "literal or variable");
        } else if (token.type == Lexer::Tokens::Type::NUMBER
                   || token.type == Lexer::Tokens::Type::STRING_LITERAL
                   || token.type == Lexer::Tokens::Type::KEYWORD
                   || token.type == Lexer::Tokens::Type::VARIABLE_IDENTIFIER
                   || token.type == Lexer::Tokens::Type::IDENTIFIER) {
            if (token.type == Lexer::Tokens::Type::IDENTIFIER) {
                // Treat bare identifiers as variable references for member access
                output_queue.push_back(ParsedExpression::makeVariable(token.value));
            } else {
                if (Lexer::pushOperand(token, expected_var_type, output_queue) == false) {
                    Parser::reportError("Invalid type", token, "literal or variable");
                }
            }
            consumeToken();
            expect_unary = false;
temp_test.vs
New file
@@ -0,0 +1 @@
object $person = { name: "Test" }; printnl($person->name);
test_scripts/object.vs
@@ -1,36 +1,28 @@
object $person = {
    name: "Szoni",
    double age: 37.6
    string name: "Szoni",
    int age: 37
};
printnl("Hello, ", $person->name, " your age is: ", $person->age);
object $person2 = {
    string name: "Not Szoni",
    int age: 37,
    object $children: {
    object children: {
        string name: "Child1",
        int age: 10
    }
};
printnl("Person2: ", $person2->name, " age: ", $person2->age, " child: ", $person2->children->name, " age: ", $person2->children->age);
object $children2 = {
     string name: "Child2",
        int age: 15
};
object $test = $person2;
printnl("Person2: ", $test->name, " age: ", $test->age, " child: ", $test->children->name, " age: ", $test->children->age);
object $family = {
           $children: $children, // this is valid too
    object $children: $children2
};
object $family3 = {
    string age: 10  // this is invalid, drops error
};
string $person_name = $person->name;
printnl("Person name: ", $person_name);
printnl("Hello, ", $person->name);              // prints out: Hello, Szoni
println("Type: ", typeof($person);              // prints out: Type: object
println("Type: ", typeof($person2->name));      // prints out: Type: string
println("Type: ", typeof($person2->age));       // prints out: Type: double
printnl("Child1 old age: ",$person2->children->age);
$person2->children->age = $person2->children->age + 2;
tmp2.vs
New file
@@ -0,0 +1 @@
object $o = 123;
tmp_invalid.vs
New file
@@ -0,0 +1 @@
object $o = {int age: "abc"};
tmp_test.vs
New file
@@ -0,0 +1,6 @@
object $person = {
    name: "Szoni",
    age: 37
};
printnl("Hello, ", $person->name);
tmp_test2.vs
New file
@@ -0,0 +1 @@
object $child = { name: "Test", age: 5 }; object $parent = { child: $child }; printnl("Name: ",$parent->child->name, " Age: ", $parent->child->age);
tmp_test3.vs
New file
@@ -0,0 +1,4 @@
object $child1 = { name: "Child1", age: 10 };
object $child2 = { name: "Child2", age: 15 };
object $family = { $children: $child1, object $children: $child2 };
printnl($family->children->name);
tmp_test4.vs
New file
@@ -0,0 +1,4 @@
object $person = { name: "Test", age: 20 };
printnl(typeof($person));
printnl(typeof($person->name));
printnl(typeof($person->age));
tmp_valid.vs
New file
@@ -0,0 +1,4 @@
object $o = { int age: 11 };
printnl("Age: ",$o->age);
printnl("Type: ", typeof($o));
printnl("Type: ", typeof($o->age));