A simple scripting language in C++
Ferenc Szontágh
2025-04-19 48d9278f0b75098e83e58c589ea86d006358604d
add for ( loop, and docs
2 files modified
8 files added
2 files deleted
531 ■■■■■ changed files
codex.md 111 ●●●●● patch | view | raw | blame | history
docs/ArrayModule.md 25 ●●●●● patch | view | raw | blame | history
docs/FileModule.md 54 ●●●●● patch | view | raw | blame | history
docs/JsonModule.md 41 ●●●●● patch | view | raw | blame | history
docs/PrintModule.md 34 ●●●●● patch | view | raw | blame | history
docs/StringModule.md 54 ●●●●● patch | view | raw | blame | history
docs/VariableHelpersModule.md 37 ●●●●● patch | view | raw | blame | history
src/Interpreter/CStyleForStatementNode.hpp 70 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 99 ●●●●● patch | view | raw | blame | history
temp_test_io.txt 1 ●●●● patch | view | raw | blame | history
temp_test_io2.txt 1 ●●●● patch | view | raw | blame | history
test_scripts/array.vs 4 ●●●● patch | view | raw | blame | history
codex.md
New file
@@ -0,0 +1,111 @@
<!--
  Codex Developer Guide
  Provides quick references for navigating and extending the VoidScript codebase.
-->
# VoidScript Codex
## Table of Contents
- [1. Project Structure](#1-project-structure)
- [2. Build System](#2-build-system)
- [3. Modules](#3-modules)
- [4. Lexer](#4-lexer)
- [5. Parser](#5-parser)
- [6. AST & Interpreter](#6-ast--interpreter)
- [7. OperationsFactory & Container](#7-operationsfactory--container)
- [8. Adding New Syntax](#8-adding-new-syntax)
- [9. Testing & Debugging](#9-testing--debugging)
---
## 1. Project Structure
```
/cli                  # CLI binary entrypoint (main.cpp)
/src
  /Lexer              # Tokenizer: Lexer.cpp, Operators.hpp/.cpp, Token.hpp
  /Parser             # Parser: Parser.hpp/.cpp, ParsedExpression.hpp
  /Interpreter        # AST nodes (ExpressionNode, StatementNode, OperationsFactory, etc.)
  /Modules            # Built‑in modules (Array, File, JSON, Print, String, VariableHelpers)
  /Symbols            # SymbolTable, SymbolContainer, Value, Types
/Modules              # External/plugin modules (e.g., CurlModule)
test_scripts          # Reference scripts (.vs) for tests/examples
build                 # Out‑of‑source build directory
```
## 2. Build System
- Uses CMake (top‑level `CMakeLists.txt`).
- Configure: `cmake -S . -B build`
- Build: `cmake --build build` (or `ninja -C build`).
- Binaries & libraries under `build/`:
  - `build/voidscript` (CLI)
  - `build/libvoidscript.so`
  - `build/Modules/<ModuleName>`
## 3. Modules
### Built‑in
Located in `src/Modules/BuiltIn/`. They export C++ functions to the interpreter via `BuiltinModule` interface.
### Plugins
Under `/Modules/<YourModule>/`:
1. Create `CMakeLists.txt` next to `PluginInit.cpp`.
2. Implement hook `VS_MODULE_INIT()` to register functions.
3. Build installs to `build/Modules/<YourModule>`.
## 4. Lexer
- Files: `src/Lexer/Lexer.hpp/.cpp`, `Operators.hpp/.cpp`, `Token.hpp`.
- `Operators.hpp` defines token categories (arithmetic, logical, relational, assignment, punctuation).
- In `Lexer::matchOperatorOrPunctuation`, longest‑match two‑char operators first.
- To add a token:
  1. Update the appropriate vector in `Operators.cpp`.
  2. If it’s a literal/identifier change, adjust `matchIdentifierOrKeyword` or `matchNumber`.
## 5. Parser
- Files: `src/Parser/Parser.hpp/.cpp`.
- Entry: `Parser::parseScript()`, which loops `parseStatement()` until EOF.
- Statement handlers:
  - Top‑level: `parseVariableDefinition()`, `parseAssignmentStatement()`, `parseIfStatement()`, `parseWhileStatement()`, `parseForStatement()`, `parseCallStatement()`, `parseReturnStatement()`.
  - In‑block: `parseStatementNode()`, plus `parseIfStatementNode()`, `parseForStatementNode()`.
- Expression parsing: `parseParsedExpression()` implements a shunting‑yard algorithm; outputs `ParsedExpression` AST fragments.
## 6. AST & Interpreter
- AST node interfaces under `src/Interpreter/`:
  - `ExpressionNode` (evaluate returns `Symbols::Value`)
  - `StatementNode` (interpret for side‑effects)
- Mapping from `ParsedExpression` to concrete `ExpressionNode` in `Parser/ExpressionBuilder.hpp`.
- Each node implements `evaluate()` or `interpret()`.
- To add a node:
  1. Create header & source in `Interpreter/`.
  2. Update `ExpressionBuilder` (for expr nodes) or parser (for stmt nodes).
## 7. OperationsFactory & Container
- `Interpreter/OperationsFactory.hpp/.cpp`: factories to record operations (Assignment, Call, Conditional, Loop, Return).
- `Operations::Container` holds a list of operations per scope; executed by `Interpreter`.
- Top‑level parser uses `OperationsFactory` to enqueue AST `StatementNode` into the container.
## 8. Adding New Syntax
1. **Lexer**: add or extend token definitions in `Operators.hpp` and adjust `Lexer.cpp` matching logic.
2. **Parser**:
   - Register new keyword in `Parser::keywords` (if needed).
   - Write a new `parseXxx()` or extend `parseStatementNode()`/`parseParsedExpression()`.
   - For expressions, integrate into the shunting‑yard switch.
3. **AST**: implement a new `ExpressionNode` or `StatementNode` subclass with semantics.
4. **Interpreter**: define `evaluate()` or `interpret()` in the node.
5. **OperationsFactory**: if top‑level, add a factory method to emit the operation.
6. **Tests**: write a `.vs` script under `test_scripts/` and run with debug flags.
## 9. Testing & Debugging
- Sample scripts in `test_scripts/`.
- Run interpreter with debug levels:
  - Lexer: `voidscript --debug=lexer script.vs`
  - Parser: `voidscript --debug=parser script.vs`
  - Interpreter: `voidscript --debug=interpreter script.vs`
- Use `printnl(...)` functions in scripts to observe runtime behavior.
---
## Maintenance
This document must be updated whenever changes are made to the codebase (lexer, parser, AST nodes, modules, build system, etc.) to keep the guide current.
*Generated by Codex CLI Agent*
*Always keep this file in sync with project changes.*
docs/ArrayModule.md
New file
@@ -0,0 +1,25 @@
 # ArrayModule
 Provides the `sizeof` function to get the number of elements in an array (object map).
 ## Functions
 ### sizeof
 - **Signature:** `sizeof(array) -> int`
 - **Description:** Returns the number of elements in `array`.
 - **Parameters:**
   - `array` (object): The array (object map) whose elements to count.
 - **Returns:** Integer count of elements.
 - **Errors:**
   - Incorrect number of arguments.
   - Argument is not an array (object).
 ## Example
 ```vs
 var arr = {};
 arr["first"] = 10;
 arr["second"] = 20;
 printnl("Length:", sizeof(arr));  // Length: 2
 ```
docs/FileModule.md
New file
@@ -0,0 +1,54 @@
 # FileModule
 Provides simple file I/O functions.
 ## Functions
 ### file_get_contents
 - **Signature:** `file_get_contents(filename) -> string`
 - **Description:** Reads entire content of a file.
 - **Parameters:**
   - `filename` (string): Path to the file.
 - **Returns:** File content as a string.
 - **Errors:**
   - Incorrect number of arguments.
   - `filename` not a string.
   - File does not exist or cannot be opened.
 ### file_put_contents
 - **Signature:** `file_put_contents(filename, content, overwrite) -> undefined`
 - **Description:** Writes `content` to `filename`.
 - **Parameters:**
   - `filename` (string): Path to the file.
   - `content` (string): Content to write.
   - `overwrite` (bool): Whether to overwrite existing file.
 - **Returns:** VoidScript `undefined`.
 - **Errors:**
   - Incorrect number of arguments.
   - Wrong types of arguments.
   - File exists when `overwrite` is false.
   - Unable to open or write to file.
 ### file_exists
 - **Signature:** `file_exists(filename) -> bool`
 - **Description:** Checks if a file exists.
 - **Parameters:**
   - `filename` (string): Path to the file.
 - **Returns:** Boolean (true if exists, false otherwise).
 - **Errors:**
   - Incorrect number of arguments.
   - `filename` not a string.
 ## Example
 ```vs
 var filename = "example.txt";
 if (!file_exists(filename)) {
     file_put_contents(filename, "Hello, VoidScript!", true);
 }
 var content = file_get_contents(filename);
 printnl(content);
 ```
docs/JsonModule.md
New file
@@ -0,0 +1,41 @@
 # JsonModule
 Provides JSON serialization and deserialization functions.
 ## Functions
 ### json_encode
 - **Signature:** `json_encode(value) -> string`
 - **Description:** Serializes a VoidScript value to a JSON string.
 - **Parameters:**
   - `value`: A VoidScript value (int, double, bool, string, object, null).
 - **Returns:** JSON string representation.
 - **Errors:**
   - Incorrect number of arguments.
 ### json_decode
 - **Signature:** `json_decode(json) -> object|value`
 - **Description:** Parses a JSON string into a VoidScript value.
 - **Parameters:**
   - `json` (string): A valid JSON string.
 - **Returns:** VoidScript value (object, number, bool, null).
 - **Errors:**
   - Incorrect number of arguments.
   - `json` not a string.
   - Invalid JSON format.
 - **Note:** Only JSON objects (`{}`), primitives, and null are supported; arrays (`[]`) are not supported.
 ## Example
 ```vs
 var obj = {};
 obj["name"] = "Alice";
 obj["age"] = 30;
 var jsonStr = json_encode(obj);
 printnl(jsonStr);  // {"name":"Alice","age":30}
 var decoded = json_decode(jsonStr);
 printnl(decoded["name"]);  // Alice
 ```
docs/PrintModule.md
New file
@@ -0,0 +1,34 @@
 # PrintModule
 Provides printing functions to standard output and error.
 ## Functions
 ### print
 - **Signature:** `print(...values) -> undefined`
 - **Description:** Prints the string representation of each value without a newline.
 - **Parameters:** Any number of values.
 - **Returns:** undefined.
 ### printnl
 - **Signature:** `printnl(...values) -> undefined`
 - **Description:** Prints the string representation of each value followed by a newline.
 - **Parameters:** Any number of values.
 - **Returns:** undefined.
 ### error
 - **Signature:** `error(...values) -> undefined`
 - **Description:** Prints the string representation of each value to standard error followed by a newline.
 - **Parameters:** Any number of values.
 - **Returns:** undefined.
 ## Example
 ```vs
 print("Hello, ");
 printnl("world!");  // prints "Hello, world!" with newline
 error("An error occurred");  // prints to stderr
 ```
docs/StringModule.md
New file
@@ -0,0 +1,54 @@
 # StringModule
 Provides helper functions for string manipulation.
 ## Functions
 ### string_length
 - **Signature:** `string_length(str) -> int`
 - **Description:** Returns the length of the string `str`.
 - **Parameters:**
   - `str` (string): The input string.
 - **Returns:** Integer length.
 - **Errors:**
   - Incorrect number of arguments.
   - `str` not a string.
 ### string_replace
 - **Signature:** `string_replace(str, from, to, replace_all) -> string`
 - **Description:** Replaces occurrences of substring `from` with `to` in `str`.
 - **Parameters:**
   - `str` (string): The input string.
   - `from` (string): Substring to replace.
   - `to` (string): Replacement substring.
   - `replace_all` (bool): `true` to replace all occurrences, `false` to replace only the first.
 - **Returns:** New string with replacements.
 - **Errors:**
   - Incorrect number of arguments.
   - Wrong argument types.
   - `from` is empty.
 ### string_substr
 - **Signature:** `string_substr(str, from, length) -> string`
 - **Description:** Extracts a substring from `str`.
 - **Parameters:**
   - `str` (string): The input string.
   - `from` (int): Starting index (0-based).
   - `length` (int): Number of characters to extract.
 - **Returns:** The substring.
 - **Errors:**
   - Incorrect number of arguments.
   - Wrong argument types.
   - `from` or `length` negative or out of range.
 ## Example
 ```vs
 var s = "Hello, world!";
 printnl(string_length(s));  // 13
 printnl(string_replace(s, "world", "VoidScript", false));  // Hello, VoidScript!
 printnl(string_substr(s, 7, 5));  // world
 ```
docs/VariableHelpersModule.md
New file
@@ -0,0 +1,37 @@
 # VariableHelpersModule
 Provides helper functions to inspect variable types.
 ## Functions
 ### typeof
 - **Signature:** `typeof(value) -> string`
 - **Description:** Returns the type name of `value`: `"int"`, `"double"`, `"float"`, `"string"`, `"bool"`, `"object"`, `"null"`, or `"undefined"`.
 - **Parameters:**
   - `value`: Any VoidScript value.
 - **Returns:** Type name as string.
 - **Errors:** Incorrect number of arguments.
 - **Signature:** `typeof(value, typeName) -> bool`
 - **Description:** Checks if the type of `value` matches `typeName`.
 - **Parameters:**
   - `value`: Any VoidScript value.
   - `typeName` (string): The type name to compare.
 - **Returns:** Boolean indicating if types match.
 - **Errors:**
   - Incorrect number of arguments.
   - `typeName` not a string.
 ## Example
 ```vs
 var x = 42;
 printnl(typeof(x));              // int
 printnl(typeof(x, "int"));     // true
 printnl(typeof(x, "string"));  // false
 var s = "hello";
 printnl(typeof(s));              // string
 printnl(typeof(s, "bool"));    // false
 ```
src/Interpreter/CStyleForStatementNode.hpp
New file
@@ -0,0 +1,70 @@
 #ifndef INTERPRETER_CSTYLEFORSTATEMENTNODE_HPP
 #define INTERPRETER_CSTYLEFORSTATEMENTNODE_HPP
 #include <vector>
 #include <memory>
 #include <string>
 #include "Interpreter/StatementNode.hpp"
 #include "Interpreter/Interpreter.hpp"
 #include "Interpreter/ExpressionNode.hpp"
 #include "Symbols/Value.hpp"
 namespace Interpreter {
 /**
  * @brief Statement node representing a C-style for loop: for(init; cond; incr) { body }
  */
 class CStyleForStatementNode : public StatementNode {
  private:
    std::unique_ptr<StatementNode> initStmt_;
    std::unique_ptr<ExpressionNode> condExpr_;
    std::unique_ptr<StatementNode> incrStmt_;
    std::vector<std::unique_ptr<StatementNode>> body_;
  public:
    CStyleForStatementNode(std::unique_ptr<StatementNode> initStmt,
                           std::unique_ptr<ExpressionNode> condExpr,
                           std::unique_ptr<StatementNode> incrStmt,
                           std::vector<std::unique_ptr<StatementNode>> body,
                           const std::string & file_name,
                           int line,
                           size_t column)
      : StatementNode(file_name, line, column),
        initStmt_(std::move(initStmt)),
        condExpr_(std::move(condExpr)),
        incrStmt_(std::move(incrStmt)),
        body_(std::move(body)) {}
    void interpret(Interpreter & interpreter) const override {
        try {
            using namespace Symbols;
            // Initialization
            initStmt_->interpret(interpreter);
            // Loop condition and body
            while (true) {
                Value condVal = condExpr_->evaluate(interpreter);
                if (condVal.getType() != Variables::Type::BOOLEAN) {
                    throw Exception("For loop condition not boolean", filename_, line_, column_);
                }
                if (!condVal.get<bool>()) break;
                for (const auto & stmt : body_) {
                    stmt->interpret(interpreter);
                }
                // Increment step
                incrStmt_->interpret(interpreter);
            }
        } catch (const Exception &) {
            throw;
        } catch (const std::exception & e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
    }
    std::string toString() const override {
        return "CStyleForStatementNode at " + filename_ + ":" + std::to_string(line_);
    }
};
} // namespace Interpreter
#endif // INTERPRETER_CSTYLEFORSTATEMENTNODE_HPP
src/Parser/Parser.cpp
@@ -13,6 +13,7 @@
#include "Interpreter/DeclareVariableStatementNode.hpp"
#include "Interpreter/ExpressionBuilder.hpp"
#include "Interpreter/ForStatementNode.hpp"
#include "Interpreter/CStyleForStatementNode.hpp"
#include "Interpreter/ReturnStatementNode.hpp"
#include "Symbols/SymbolContainer.hpp"
@@ -167,6 +168,54 @@
    std::string firstName = firstTok.value;
    if (!firstName.empty() && firstName[0] == '$') {
        firstName = firstName.substr(1);
    }
    // C-style for loop: for (type $i = init; cond; incr) { ... }
    if (match(Lexer::Tokens::Type::OPERATOR_ASSIGNMENT, "=")) {
        // Parse initialization expression
        auto initExpr = parseParsedExpression(elemType);
        expect(Lexer::Tokens::Type::PUNCTUATION, ";");
        // Parse condition expression
        auto condExpr = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
        expect(Lexer::Tokens::Type::PUNCTUATION, ";");
        // Parse increment statement
        std::unique_ptr<Interpreter::StatementNode> incrStmt;
        {
            auto incrTok = currentToken();
            if (incrTok.type == Lexer::Tokens::Type::VARIABLE_IDENTIFIER) {
                auto identTok = consumeToken();
                std::string incrName = identTok.value;
                if (!incrName.empty() && incrName[0] == '$') incrName = incrName.substr(1);
                if (match(Lexer::Tokens::Type::OPERATOR_INCREMENT, "++")) {
                    auto lhs = std::make_unique<Interpreter::IdentifierExpressionNode>(incrName);
                    auto rhs = std::make_unique<Interpreter::LiteralExpressionNode>(Symbols::Value(1));
                    auto bin = std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), "+", std::move(rhs));
                    incrStmt = std::make_unique<Interpreter::AssignmentStatementNode>(incrName, std::vector<std::string>(), std::move(bin), this->current_filename_, incrTok.line_number, incrTok.column_number);
                } else if (match(Lexer::Tokens::Type::OPERATOR_INCREMENT, "--")) {
                    auto lhs = std::make_unique<Interpreter::IdentifierExpressionNode>(incrName);
                    auto rhs = std::make_unique<Interpreter::LiteralExpressionNode>(Symbols::Value(1));
                    auto bin = std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), "-", std::move(rhs));
                    incrStmt = std::make_unique<Interpreter::AssignmentStatementNode>(incrName, std::vector<std::string>(), std::move(bin), this->current_filename_, incrTok.line_number, incrTok.column_number);
                } else {
                    reportError("Expected '++' or '--' in for-loop increment");
                }
            } else {
                reportError("Expected variable name in for-loop increment");
            }
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, ")");
        expect(Lexer::Tokens::Type::PUNCTUATION, "{");
        // Parse loop body
        std::vector<std::unique_ptr<Interpreter::StatementNode>> body;
        while (!(currentToken().type == Lexer::Tokens::Type::PUNCTUATION && currentToken().value == "}")) {
            body.push_back(parseStatementNode());
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, "}");
        // Build nodes for C-style for
        auto initExprNode = buildExpressionFromParsed(initExpr);
        auto initStmt = std::make_unique<Interpreter::DeclareVariableStatementNode>(firstName, Symbols::SymbolContainer::instance()->currentScopeName(), elemType, std::move(initExprNode), this->current_filename_, firstTok.line_number, firstTok.column_number);
        auto condExprNode = buildExpressionFromParsed(condExpr);
        auto * cnode = new Interpreter::CStyleForStatementNode(std::move(initStmt), std::move(condExprNode), std::move(incrStmt), std::move(body), this->current_filename_, forToken.line_number, forToken.column_number);
        return std::unique_ptr<Interpreter::StatementNode>(cnode);
    }
    // Determine loop form: key,value or simple element loop
    std::string keyName, valName;
@@ -431,6 +480,56 @@
    if (!firstName.empty() && firstName[0] == '$') {
        firstName = firstName.substr(1);
    }
    // C-style for loop: for (type $i = init; cond; incr) { ... }
    if (match(Lexer::Tokens::Type::OPERATOR_ASSIGNMENT, "=")) {
        // Parse initialization expression
        auto initExpr = parseParsedExpression(elemType);
        expect(Lexer::Tokens::Type::PUNCTUATION, ";");
        // Parse condition expression
        auto condExpr = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
        expect(Lexer::Tokens::Type::PUNCTUATION, ";");
        // Parse increment statement
        std::unique_ptr<Interpreter::StatementNode> incrStmt;
        {
            auto incrTok = currentToken();
            if (incrTok.type == Lexer::Tokens::Type::VARIABLE_IDENTIFIER) {
                auto identTok = consumeToken();
                std::string incrName = identTok.value;
                if (!incrName.empty() && incrName[0] == '$') incrName = incrName.substr(1);
                if (match(Lexer::Tokens::Type::OPERATOR_INCREMENT, "++")) {
                    auto lhs = std::make_unique<Interpreter::IdentifierExpressionNode>(incrName);
                    auto rhs = std::make_unique<Interpreter::LiteralExpressionNode>(Symbols::Value(1));
                    auto bin = std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), "+", std::move(rhs));
                    incrStmt = std::make_unique<Interpreter::AssignmentStatementNode>(incrName, std::vector<std::string>(), std::move(bin), this->current_filename_, incrTok.line_number, incrTok.column_number);
                } else if (match(Lexer::Tokens::Type::OPERATOR_INCREMENT, "--")) {
                    auto lhs = std::make_unique<Interpreter::IdentifierExpressionNode>(incrName);
                    auto rhs = std::make_unique<Interpreter::LiteralExpressionNode>(Symbols::Value(1));
                    auto bin = std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), "-", std::move(rhs));
                    incrStmt = std::make_unique<Interpreter::AssignmentStatementNode>(incrName, std::vector<std::string>(), std::move(bin), this->current_filename_, incrTok.line_number, incrTok.column_number);
                } else {
                    reportError("Expected '++' or '--' in for-loop increment");
                }
            } else {
                reportError("Expected variable name in for-loop increment");
            }
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, ")");
        expect(Lexer::Tokens::Type::PUNCTUATION, "{");
        // Parse loop body
        std::vector<std::unique_ptr<Interpreter::StatementNode>> body;
        while (!(currentToken().type == Lexer::Tokens::Type::PUNCTUATION && currentToken().value == "}")) {
            body.push_back(parseStatementNode());
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, "}");
        // Build nodes for C-style for
        auto initExprNode = buildExpressionFromParsed(initExpr);
        auto initStmt = std::make_unique<Interpreter::DeclareVariableStatementNode>(firstName, Symbols::SymbolContainer::instance()->currentScopeName(), elemType, std::move(initExprNode), this->current_filename_, firstTok.line_number, firstTok.column_number);
        auto condExprNode = buildExpressionFromParsed(condExpr);
        auto cnode = std::make_unique<Interpreter::CStyleForStatementNode>(std::move(initStmt), std::move(condExprNode), std::move(incrStmt), std::move(body), this->current_filename_, forToken.line_number, forToken.column_number);
        Operations::Container::instance()->add(Symbols::SymbolContainer::instance()->currentScopeName(),
                                               Operations::Operation{Operations::Type::Loop, "", std::move(cnode)});
        return;
    }
    // Determine loop form: key,value or simple element loop
    std::string keyName, valName;
    Symbols::Variables::Type keyType;
temp_test_io.txt
File was deleted
temp_test_io2.txt
File was deleted
test_scripts/array.vs
@@ -18,3 +18,7 @@
int $size = sizeof($array);
printnl("The size of the $array: ", $size);
for (int $i = 0; $i < sizeof($intArray); $i++) {
    printnl("Value: ", $intArray[$i]);
}