A simple scripting language in C++
Ferenc Szontágh
2025-04-18 6cc2945c1d1e6ca7bad0542c79de423df5e2db8b
implements object type
9 files modified
1 files added
164 ■■■■■ changed files
src/Interpreter/ExpressionBuilder.hpp 10 ●●●●● patch | view | raw | blame | history
src/Interpreter/ObjectExpressionNode.hpp 35 ●●●●● patch | view | raw | blame | history
src/Lexer/Operators.cpp 2 ●●● patch | view | raw | blame | history
src/Lexer/TokenType.hpp 3 ●●●●● patch | view | raw | blame | history
src/Parser/ParsedExpression.hpp 12 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 48 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.hpp 2 ●●●●● patch | view | raw | blame | history
src/Symbols/Value.hpp 10 ●●●●● patch | view | raw | blame | history
src/Symbols/VariableTypes.hpp 6 ●●●●● patch | view | raw | blame | history
test_scripts/object.vs 36 ●●●●● patch | view | raw | blame | history
src/Interpreter/ExpressionBuilder.hpp
@@ -10,6 +10,7 @@
#include "Interpreter/LiteralExpressionNode.hpp"
#include "Interpreter/UnaryExpressionNode.hpp"  // <-- új include
#include "Interpreter/CallExpressionNode.hpp"
#include "Interpreter/ObjectExpressionNode.hpp"
#include "Parser/ParsedExpression.hpp"
namespace Parser {
@@ -46,6 +47,15 @@
                }
                return std::make_unique<Interpreter::CallExpressionNode>(expr->name, std::move(callArgs));
            }
        case Kind::Object:
            {
                std::vector<std::pair<std::string, std::unique_ptr<Interpreter::ExpressionNode>>> members;
                members.reserve(expr->objectMembers.size());
                for (const auto &p : expr->objectMembers) {
                    members.emplace_back(p.first, buildExpressionFromParsed(p.second));
                }
                return std::make_unique<Interpreter::ObjectExpressionNode>(std::move(members));
            }
    }
    throw std::runtime_error("Unknown ParsedExpression kind");
src/Interpreter/ObjectExpressionNode.hpp
New file
@@ -0,0 +1,35 @@
#ifndef INTERPRETER_OBJECT_EXPRESSION_NODE_HPP
#define INTERPRETER_OBJECT_EXPRESSION_NODE_HPP
#include "ExpressionNode.hpp"
#include "Symbols/Value.hpp"
#include <vector>
#include <string>
#include <memory>
namespace Interpreter {
class ObjectExpressionNode : public ExpressionNode {
  public:
    using ObjectMap = Symbols::Value::ObjectMap;
    explicit ObjectExpressionNode(std::vector<std::pair<std::string, std::unique_ptr<ExpressionNode>>> members)
        : members_(std::move(members)) {}
    Symbols::Value evaluate(Interpreter & interpreter) const override {
        ObjectMap obj;
        for (const auto &kv : members_) {
            obj[kv.first] = kv.second->evaluate(interpreter);
        }
        return Symbols::Value(obj);
    }
    std::string toString() const override { return "[object]"; }
  private:
    std::vector<std::pair<std::string, std::unique_ptr<ExpressionNode>>> members_;
};
} // namespace Interpreter
#endif // INTERPRETER_OBJECT_EXPRESSION_NODE_HPP
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/TokenType.hpp
@@ -25,6 +25,7 @@
    KEYWORD_DOUBLE,
    KEYWORD_FLOAT,
    KEYWORD_BOOLEAN,
    KEYWORD_OBJECT,
    KEYWORD_FUNCTION_DECLARATION,
    KEYWORD_RETURN,
    KEYWORD_NULL,
@@ -69,6 +70,8 @@
            return "KEYWORD_FLOAT";
        case Lexer::Tokens::Type::KEYWORD_BOOLEAN:
            return "KEYWORD_BOOLEAN";
        case Lexer::Tokens::Type::KEYWORD_OBJECT:
            return "KEYWORD_OBJECT";
        case Lexer::Tokens::Type::KEYWORD_FUNCTION_DECLARATION:
            return "KEYWORD_FUNCTION";
        case Lexer::Tokens::Type::KEYWORD_RETURN:
src/Parser/ParsedExpression.hpp
@@ -16,7 +16,7 @@
using ParsedExpressionPtr = std::unique_ptr<ParsedExpression>;
struct ParsedExpression {
    enum class Kind : std::uint8_t { Literal, Variable, Binary, Unary, Call };
    enum class Kind : std::uint8_t { Literal, Variable, Binary, Unary, Call, Object };
    Kind kind;
@@ -29,6 +29,7 @@
    ParsedExpressionPtr rhs;
    // For function call arguments
    std::vector<ParsedExpressionPtr> args;
    std::vector<std::pair<std::string, ParsedExpressionPtr>> objectMembers;
    // Constructor for literal
    static ParsedExpressionPtr makeLiteral(const Symbols::Value & val) {
@@ -70,6 +71,13 @@
        expr->kind       = Kind::Call;
        expr->name       = name;
        expr->args       = std::move(arguments);
        return expr;
    }
    // Constructor for object literal
    static ParsedExpressionPtr makeObject(std::vector<std::pair<std::string, ParsedExpressionPtr>> members) {
        auto expr = std::make_unique<ParsedExpression>();
        expr->kind = Kind::Object;
        expr->objectMembers = std::move(members);
        return expr;
    }
@@ -116,6 +124,8 @@
                    auto funcSym = std::static_pointer_cast<Symbols::FunctionSymbol>(symbol);
                    return funcSym->returnType();
                }
            case Kind::Object:
                return Symbols::Variables::Type::OBJECT;
            default:
                throw std::runtime_error("Unknown expression kind");
src/Parser/Parser.cpp
@@ -6,6 +6,7 @@
#include "Lexer/Operators.hpp"
// Statements and expression building for conditional and block parsing
#include "Interpreter/ConditionalStatementNode.hpp"
// #include "Interpreter/ForStatementNode.hpp"  // removed until for-in loops are implemented
#include "Interpreter/CallStatementNode.hpp"
#include "Interpreter/DeclareVariableStatementNode.hpp"
#include "Interpreter/ReturnStatementNode.hpp"
@@ -34,6 +35,7 @@
    { "string",   Lexer::Tokens::Type::KEYWORD_STRING               },
    { "boolean",  Lexer::Tokens::Type::KEYWORD_BOOLEAN              },
    { "bool",     Lexer::Tokens::Type::KEYWORD_BOOLEAN              },
    { "object",   Lexer::Tokens::Type::KEYWORD_OBJECT               },
    // ... other keywords ...
};
@@ -44,6 +46,7 @@
    { Lexer::Tokens::Type::KEYWORD_STRING,  Symbols::Variables::Type::STRING    },
    { Lexer::Tokens::Type::KEYWORD_NULL,    Symbols::Variables::Type::NULL_TYPE },
    { Lexer::Tokens::Type::KEYWORD_BOOLEAN, Symbols::Variables::Type::BOOLEAN   },
    { Lexer::Tokens::Type::KEYWORD_OBJECT,  Symbols::Variables::Type::OBJECT    },
};
void Parser::parseVariableDefinition() {
@@ -357,6 +360,51 @@
    while (true) {
        auto token = currentToken();
        // Object literal: { key: value, ... }
        if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.value == "{") {
            // Consume '{'
            consumeToken();
            std::vector<std::pair<std::string, ParsedExpressionPtr>> members;
            // Parse members until '}'
            if (!(currentToken().type == Lexer::Tokens::Type::PUNCTUATION && currentToken().value == "}")) {
                while (true) {
                    // Optional type tag before key
                    Symbols::Variables::Type memberType = Symbols::Variables::Type::UNDEFINED_TYPE;
                    if (Parser::variable_types.find(currentToken().type) != Parser::variable_types.end()) {
                        memberType = parseType();
                    }
                    // Key must be an identifier or variable identifier
                    if (currentToken().type != Lexer::Tokens::Type::IDENTIFIER &&
                        currentToken().type != Lexer::Tokens::Type::VARIABLE_IDENTIFIER) {
                        reportError("Expected identifier for object key");
                    }
                    std::string key = currentToken().value;
                    // Strip '$' if present
                    if (!key.empty() && key[0] == '$') {
                        key = key.substr(1);
                    }
                    consumeToken();
                    // Expect ':' delimiter
                    expect(Lexer::Tokens::Type::PUNCTUATION, ":");
                    // Parse value expression (pass tag type if provided)
                    Symbols::Variables::Type expectType = (memberType == Symbols::Variables::Type::UNDEFINED_TYPE)
                                                            ? Symbols::Variables::Type::NULL_TYPE
                                                            : memberType;
                    auto valueExpr = parseParsedExpression(expectType);
                    members.emplace_back(key, std::move(valueExpr));
                    if (match(Lexer::Tokens::Type::PUNCTUATION, ",")) {
                        continue;
                    }
                    break;
                }
            }
            // Expect closing '}'
            expect(Lexer::Tokens::Type::PUNCTUATION, "}");
            // Create object literal parsed expression
            output_queue.push_back(ParsedExpression::makeObject(std::move(members)));
            expect_unary = false;
            continue;
        }
        if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.lexeme == "(") {
            operator_stack.push("(");
src/Parser/Parser.hpp
@@ -209,6 +209,8 @@
    void parseReturnStatement();
    // Parse an if-else conditional statement
    void parseIfStatement();
    // Parse a for-in loop over object members
    void parseForStatement();
    // Parse a statement node for use inside blocks (not added to operation container)
    std::unique_ptr<Interpreter::StatementNode> parseStatementNode();
src/Symbols/Value.hpp
@@ -4,6 +4,7 @@
#include <algorithm>
#include <stdexcept>
#include <string>
#include <map>
#include <variant>
#include "VariableTypes.hpp"
@@ -12,7 +13,8 @@
class Value {
  public:
    using Variant = std::variant<int, double, float, std::string, bool>;
    using ObjectMap = std::map<std::string, Value>;
    using Variant = std::variant<int, double, float, std::string, bool, ObjectMap>;
    Value() = default;
@@ -27,6 +29,10 @@
    Value(const char * v) : value_(std::string(v)) { type_ = Symbols::Variables::Type::STRING; }
    Value(bool v) : value_(v) { type_ = Symbols::Variables::Type::BOOLEAN; }
    /**
     * @brief Construct an object value from a map of member names to Values.
     */
    Value(const ObjectMap & v) : value_(v) { type_ = Symbols::Variables::Type::OBJECT; }
    Value(const std::string & str, bool autoDetectType) { *this = fromString(str, autoDetectType); }
@@ -70,6 +76,8 @@
                    return val ? "true" : "false";
                } else if constexpr (std::is_same_v<T, std::string>) {
                    return val;
                } else if constexpr (std::is_same_v<T, ObjectMap>) {
                    return "[object]";
                } else {
                    return std::to_string(val);
                }
src/Symbols/VariableTypes.hpp
@@ -7,7 +7,7 @@
namespace Symbols::Variables {
enum class Type : std::uint8_t { INTEGER, DOUBLE, FLOAT, STRING, BOOLEAN, NULL_TYPE, UNDEFINED_TYPE };
enum class Type : std::uint8_t { INTEGER, DOUBLE, FLOAT, STRING, BOOLEAN, OBJECT, NULL_TYPE, UNDEFINED_TYPE };
const std::unordered_map<std::string, Type> StringToTypeMap = {
    { "int",       Type::INTEGER        },
@@ -17,6 +17,7 @@
    { "bool",      Type::BOOLEAN        },
    { "boolean",   Type::BOOLEAN        },
    { "null",      Type::NULL_TYPE      },
    { "object",    Type::OBJECT         },
    { "undefined", Type::UNDEFINED_TYPE },
};
@@ -26,8 +27,9 @@
    { Type::FLOAT,          "float"      },
    { Type::STRING,         "string"     },
    { Type::BOOLEAN,        "bool"       },
    { Type::OBJECT,         "object"     },
    { Type::NULL_TYPE,      "null"       },
    { Type::UNDEFINED_TYPE, "undeffined" },
    { Type::UNDEFINED_TYPE, "undefined"  },
};
inline static std::string TypeToString(Symbols::Variables::Type type) {
test_scripts/object.vs
@@ -1,4 +1,36 @@
object $person = {
    name: "Szoni",
    age: 37
};
    double age: 37.6
};
object $person2 = {
    string name: "Not Szoni",
    int age: 37,
    object $children: {
        string name: "Child1",
        int age: 10
    }
};
object $children2 = {
     string name: "Child2",
        int age: 15
};
object $family = {
           $children: $children, // this is valid too
    object $children: $children2
};
object $family3 = {
    string age: 10  // this is invalid, drops error
};
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