A simple scripting language in C++
Ferenc Szontágh
2025-04-19 55abb4f6f81fc370e349385b38dffb05fa9d5dcb
add vscodium / vscode and vim syntax highlight
8 files modified
16 files added
661 ■■■■ changed files
assets/vim/ftdetect/voidscript.vim 1 ●●●● patch | view | raw | blame | history
assets/vim/indent/voidscript.vim 54 ●●●●● patch | view | raw | blame | history
assets/vim/syntax/voidscript.vim 77 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/.gitignore 11 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/LICENSE 21 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/README.md 57 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/create-vscode-package.sh 28 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/package.json 44 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/src/grammar/language-configuration.json 82 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/src/grammar/voidscript.tmLanguage.json 43 ●●●●● patch | view | raw | blame | history
assets/vscode/voidscript-syntax/tsconfig.json 12 ●●●●● patch | view | raw | blame | history
cli/main.cpp 28 ●●●● patch | view | raw | blame | history
src/Interpreter/CallExpressionNode.hpp 143 ●●●● patch | view | raw | blame | history
src/Interpreter/ExpressionBuilder.hpp 4 ●●● patch | view | raw | blame | history
src/Modules/BaseModule.hpp 16 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/PrintModule.hpp 9 ●●●●● patch | view | raw | blame | history
src/Parser/ParsedExpression.hpp 4 ●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 10 ●●●● patch | view | raw | blame | history
src/VoidScript.hpp 11 ●●●● patch | view | raw | blame | history
test_error.vs 1 ●●●● patch | view | raw | blame | history
test_scripts/test_error.vs 1 ●●●● patch | view | raw | blame | history
test_scripts/test_error2.vs 1 ●●●● patch | view | raw | blame | history
test_scripts/test_throw_error.vs 2 ●●●●● patch | view | raw | blame | history
test_scripts/throw_error_test.vs 1 ●●●● patch | view | raw | blame | history
assets/vim/ftdetect/voidscript.vim
New file
@@ -0,0 +1 @@
au BufRead,BufNewFile *.vs,*.vscript set filetype=voidscript
assets/vim/indent/voidscript.vim
New file
@@ -0,0 +1,54 @@
" Advanced VoidScript indent
if exists("b:did_indent")
  finish
endif
let b:did_indent = 1
setlocal indentexpr=GetVoidScriptIndent()
setlocal indentkeys=o,O,0},0),=else,=elif
function! GetVoidScriptIndent()
  let lnum = prevnonblank(v:lnum - 1)
  if lnum == 0
    return 0
  endif
  let prevline = getline(lnum)
  let currline = getline(v:lnum)
  " Strip comments
  let prev = substitute(prevline, '#.*$', '', '')
  let prev = substitute(prev, '//.*$', '', '')
  let curr = substitute(currline, '#.*$', '', '')
  let curr = substitute(curr, '//.*$', '', '')
  let indent = indent(lnum)
  " De-indent for lines that start with } or )
  if curr =~ '^\s*[\]\)}]'
    return indent - &shiftwidth
  endif
  " De-indent for else or elif
  if curr =~ '^\s*\(else\|elif\)\>'
    return indent - &shiftwidth
  endif
  " Object inline literals: ignore increase if likely inline
  if prev =~ '^\s*object\s\+\$\?\w\+\s*=\s*{'
    return indent
  endif
  " Increase indent after these block starters
  if prev =~ '\v<((if|else|elif|for|while|function|class|object)\b.*)?{[ \t]*$'
    return indent + &shiftwidth
  endif
  " Increase indent for lines ending with just a {
  if prev =~ '{\s*$'
    return indent + &shiftwidth
  endif
  " Keep same indent
  return indent
endfunction
assets/vim/syntax/voidscript.vim
New file
@@ -0,0 +1,77 @@
" Vim syntax file for VoidScript
" Save as ~/.vim/syntax/voidscript.vim
if exists("b:current_syntax")
  finish
endif
" --- Keywords ---
syntax keyword voidscriptKeyword const function return for if else new sizeof this
" --- Access Modifiers ---
syntax keyword voidscriptAccess public private protected
" --- Types ---
syntax keyword voidscriptType int string float double boolean object class void
" --- Control values ---
syntax keyword voidscriptBoolean true false null
" --- Variable Names ---
syntax match voidscriptVariable /\$\k\+/
" --- Class definitions ---
syntax match voidscriptClass /\<class\s\+\k\+\>/
" --- Object property access (this->something or object->key) ---
syntax match voidscriptObjectAccess /\<this\>\|->/
" --- Function calls (e.g. func(...)) ---
syntax match voidscriptFunction /\<\k\+\s*(/ containedin=ALL
" --- Numbers ---
syntax match voidscriptNumber /\<\d\+\(\.\d\+\)\?\>/
" --- Strings with variable highlighting ---
syntax region voidscriptString start=/"/ skip=/\\"/ end=/"/ contains=voidscriptVariable
" --- Object literal keys (key: value) ---
syntax match voidscriptObjectKey /\<\k\+\>\s*:/
" Comments
syntax match voidscriptComment "#.*"
syntax match voidscriptComment "\/\/.*" contains=voidscriptTodo
highlight link voidscriptComment Comment
" Optional: highlight TODO, FIXME, NOTE in comments
syntax match voidscriptTodo "\(TODO\|FIXME\|NOTE\):" contained
highlight link voidscriptTodo Todo
" --- Operators ---
syntax match voidscriptOperator /==\|!=\|<=\|>=\|[-+*/%<>=!&|]/
" --- Braces & Delimiters ---
syntax match voidscriptBraces /[{}[\]()]/
syntax match voidscriptArrow /->/
" --- Highlight groups ---
highlight link voidscriptKeyword Keyword
highlight link voidscriptAccess StorageClass
highlight link voidscriptType Type
highlight link voidscriptBoolean Boolean
highlight link voidscriptVariable Identifier
highlight link voidscriptFunction Function
highlight link voidscriptNumber Number
highlight link voidscriptString String
highlight link voidscriptComment Comment
highlight link voidscriptTodo Todo
highlight link voidscriptOperator Operator
highlight link voidscriptBraces Delimiter
highlight link voidscriptArrow Operator
highlight link voidscriptClass Structure
highlight link voidscriptObjectAccess Operator
highlight link voidscriptObjectKey Identifier
let b:current_syntax = "voidscript"
assets/vscode/voidscript-syntax/.gitignore
New file
@@ -0,0 +1,11 @@
# Node.js
node_modules/
.vscode/
*.vsix
.idea/
*.sublime-workspace
*.sublime-project
.DS_Store
Thumbs.db
package-lock.json
.vscode/settings.json
assets/vscode/voidscript-syntax/LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 fszontagh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
assets/vscode/voidscript-syntax/README.md
New file
@@ -0,0 +1,57 @@
# VoidScript Syntax Highlighting
This Visual Studio Code extension provides syntax highlighting for the [VoidScript](https://github.com/fszontagh/voidscript) scripting language.
## Features
- Syntax highlighting for VoidScript source files with extensions `.vs` and `.voidscript`.
## Requirements
- Visual Studio Code version 1.58.0 or higher or compatible VSCodium
## Installation
### From Visual Studio Marketplace
1. Open the Extensions view (`Ctrl+Shift+X` / `Cmd+Shift+X` on macOS).
2. Search for **VoidScript Syntax**.
3. Click **Install**.
### From VSIX Package
1. Download the `voidscript-syntax-<version>.vsix` file from the [Releases](https://github.com/fszontagh/voidscript/releases) page.
2. In VS Code, open the Command Palette (`Ctrl+Shift+P`), then select **Extensions: Install from VSIX...**.
3. Choose the downloaded `.vsix` file.
## Development & Building
To build and package the extension from source:
```bash
git clone https://github.com/fszontagh/voidscript.git
cd voidscript/assets/vscode/voidscript-syntax
npm install
npm run build
```
After building, a `.vsix` package (e.g., `voidscript-syntax-1.0.0.vsix`) will be generated in the current directory.
To install the extension from file: `code --install-extension voidscript-syntax-1.0.0.vsix`
## Usage
Open a VoidScript file (extension `.vs` or `.voidscript`) in VS Code. The syntax highlighting will be applied automatically.
## Contributing
Contributions to the grammar and syntax definitions are welcome!
1. Fork the repository and create a new branch.
2. Make changes under `src/grammar/`.
3. Commit and push your changes.
4. Open a Pull Request.
## License
MIT License. See the [LICENSE](LICENSE) file for details.
assets/vscode/voidscript-syntax/create-vscode-package.sh
New file
@@ -0,0 +1,28 @@
#!/bin/bash
# Check if 'vsce' (Visual Studio Code Extension Manager) is installed
if ! command -v vsce &> /dev/null
then
    echo "'vsce' not found! Please install it to proceed."
    echo "Install with: npm install -g vsce"
    exit 1
fi
# Check if the package.json file exists
if [ ! -f "package.json" ]; then
    echo "package.json not found. Are you running this script in the extension's root directory?"
    exit 1
fi
# Start packaging the extension
echo "Packaging extension..."
vsce package
# If packaging succeeded, inform the user
if [ $? -eq 0 ]; then
    echo "Extension package created successfully!"
else
    echo "Error: Packaging failed."
    exit 1
fi
assets/vscode/voidscript-syntax/package.json
New file
@@ -0,0 +1,44 @@
{
    "name": "voidscript-syntax",
    "displayName": "VoidScript Syntax",
    "description": "Syntax highlighting for VoidScript language.",
    "license": "MIT",
    "version": "1.0.0",
    "publisher": "fszontagh",
    "repository": {
      "type": "git",
      "url": "https://github.com/fszontagh/voidscript.git"
    },
    "engines": {
      "vscode": "^1.58.0"
    },
    "contributes": {
      "languages": [
        {
          "id": "voidscript",
          "extensions": [".voidscript", ".vs"],
          "aliases": ["VoidScript"],
          "configuration": "./src/grammar/language-configuration.json",
          "mimetypes": ["application/x-voidscript", "application/vpodscript"]
        }
      ],
      "grammars": [
        {
          "language": "voidscript",
          "scopeName": "source.voidscript",
          "path": "./src/grammar/voidscript.tmLanguage.json"
        }
      ]
    },
    "scripts": {
      "vscode:prepublish": "npm install",
      "compile": "echo \"No compile needed\"",
      "create:package": "vsce package",
      "build": "npm run vscode:prepublish; npm run create:package"
    },
    "devDependencies": {
      "typescript": "^4.0.0",
      "vscode": "^1.1.37",
      "eslint": "^7.23.0"
    }
  }
assets/vscode/voidscript-syntax/src/grammar/language-configuration.json
New file
@@ -0,0 +1,82 @@
{
  "comments": {
    "lineComment": "#",
    "blockComment": [
      "/*",
      "*/"
    ]
  },
  "brackets": [
    [
      "{",
      "}"
    ],
    [
      "[",
      "]"
    ],
    [
      "(",
      ")"
    ]
  ],
  "surroundingPairs": [
    [
      "{",
      "}"
    ],
    [
      "[",
      "]"
    ],
    [
      "(",
      ")"
    ],
    [
      "'",
      "'"
    ],
    [
      "\"",
      "\""
    ]
  ],
  "autoClosingPairs": [
    {
      "open": "(",
      "close": ")",
      "notIn": [
        "string"
      ]
    },
    {
      "open": "\"",
      "close": "\"",
      "notIn": [
        "string"
      ]
    },
    {
      "open": "'",
      "close": "'",
      "notIn": [
        "string"
      ]
    },
    {
      "open": "{",
      "close": "}",
      "notIn": [
        "string"
      ]
    },
    {
      "open": "[",
      "close": "]",
      "notIn": [
        "string"
      ]
    },
  ]
}
assets/vscode/voidscript-syntax/src/grammar/voidscript.tmLanguage.json
New file
@@ -0,0 +1,43 @@
{
  "fileTypes": ["voidscript", "vs"],
    "scopeName": "source.voidscript",
    "patterns": [
      {
        "name": "comment.line.number-sign.voidscript",
        "match": "#.*"
      },
      {
        "name": "comment.line.double-slash.voidscript",
        "match": "//.*"
      },
      {
        "name": "keyword.control.voidscript",
        "match": "\\b(function|class|const|object|string|int|boolean|double|float|if|else|for|while|switch|case|break|continue|new|return|throw|typeof)\\b"
      },
      {
        "name": "variable.other.local.voidscript",
        "match": "\\$[a-zA-Z_][a-zA-Z0-9_]*"
      },
      {
        "name": "variable.language.this.voidscript",
        "match": "\\bthis\\b"
      },
      {
        "name": "string.quoted.double.voidscript",
        "match": "\"([^\"]|\\\\\")*\""
      },
      {
        "name": "constant.numeric.voidscript",
        "match": "\\b\\d+(?:\\.\\d+)?\\b"
      },
      {
        "name": "constant.language.boolean.voidscript",
        "match": "\\b(true|false)\\b"
      },
      {
        "name": "keyword.operator.voidscript",
        "match": "(->|\\+=|-=|\\*=|\\/=|%=|&&|\\|\\||==|!=|>=|<=|>|<|\\+|-|\\*|\\/|%|=|!)"
      }
    ],
    "repository": {}
  }
assets/vscode/voidscript-syntax/tsconfig.json
New file
@@ -0,0 +1,12 @@
{
    "compilerOptions": {
      "target": "ES6",
      "module": "commonjs",
      "outDir": "./out",
      "rootDir": "./src",
      "strict": true
    },
    "include": [
      "src/**/*"
    ]
  }
cli/main.cpp
@@ -1,6 +1,7 @@
#include <filesystem>
#include <iostream>
#include <unordered_map>
#include <unistd.h>  // for isatty, STDIN_FILENO
#include "options.h"
#include "VoidScript.hpp"
@@ -17,6 +18,8 @@
    for (const auto & [key, value] : params) {
        usage.append(" [" + key + "]");
    }
    // [file] is optional; if omitted, script is read from stdin
    usage.append(" [file]");
    // Parse arguments: allow --help, --version, --debug[=component], and a single file
    bool debugLexer       = false;
    bool debugParser      = false;
@@ -61,6 +64,9 @@
                std::cerr << usage << "\n";
                return 1;
            }
        } else if (a == "-") {
            // Read script from stdin
            file = a;
        } else if (a.starts_with("-")) {
            std::cerr << "Error: Unknown option '" << a << "'\n";
            std::cerr << usage << "\n";
@@ -74,17 +80,27 @@
        }
    }
    if (file.empty()) {
        std::cerr << "Error: No input file specified\n";
        // No input file specified: read script from stdin
        file = "-";
    }
    // If reading from stdin but stdin is a tty (no piped input), show usage
    if (file == "-" && isatty(STDIN_FILENO)) {
        std::cerr << usage << "\n";
        return 1;
    }
    if (!std::filesystem::exists(file)) {
        std::cerr << "Error: File " << file << " does not exist.\n";
        return 1;
    // Determine if reading from a file or stdin
    std::string filename;
    if (file == "-") {
        // Read script from standard input
        filename = file;
    } else {
        if (!std::filesystem::exists(file)) {
            std::cerr << "Error: File " << file << " does not exist.\n";
            return 1;
        }
        filename = std::filesystem::canonical(file).string();
    }
    const std::string filename = std::filesystem::canonical(file).string();
    // Initialize and run with debug options
    VoidScript voidscript(filename, debugLexer, debugParser, debugInterp, debugSymbolTable);
src/Interpreter/CallExpressionNode.hpp
@@ -21,79 +21,92 @@
 /**
  * @brief Expression node representing a function call returning a value.
  */
 class CallExpressionNode : public ExpressionNode {
     std::string functionName_;
     std::vector<std::unique_ptr<ExpressionNode>> args_;
class CallExpressionNode : public ExpressionNode {
    std::string functionName_;
    std::vector<std::unique_ptr<ExpressionNode>> args_;
    // Source location for error reporting
    std::string filename_;
    int line_;
    size_t column_;
   public:
     CallExpressionNode(std::string functionName,
                        std::vector<std::unique_ptr<ExpressionNode>> args) :
         functionName_(std::move(functionName)), args_(std::move(args)) {}
                        std::vector<std::unique_ptr<ExpressionNode>> args,
                        const std::string & filename,
                        int line, size_t column) :
         functionName_(std::move(functionName)), args_(std::move(args)),
         filename_(filename), line_(line), column_(column) {}
     Symbols::Value evaluate(Interpreter &interpreter) const override {
         using namespace Symbols;
         // Evaluate argument expressions
         std::vector<Value> argValues;
         argValues.reserve(args_.size());
         for (const auto &expr : args_) {
             argValues.push_back(expr->evaluate(interpreter));
         }
         try {
             // Evaluate argument expressions
             std::vector<Value> argValues;
             argValues.reserve(args_.size());
             for (const auto &expr : args_) {
                 argValues.push_back(expr->evaluate(interpreter));
             }
         // Built-in function
         auto &mgr = Modules::ModuleManager::instance();
         if (mgr.hasFunction(functionName_)) {
             return mgr.callFunction(functionName_, argValues);
         }
             // Built-in function
             auto &mgr = Modules::ModuleManager::instance();
             if (mgr.hasFunction(functionName_)) {
                 return mgr.callFunction(functionName_, argValues);
             }
        // User-defined function: lookup through scope hierarchy
        SymbolContainer *sc = SymbolContainer::instance();
        std::string lookupNs = sc->currentScopeName();
        std::shared_ptr<FunctionSymbol> funcSym;
        // Search for function symbol in current and parent scopes
        while (true) {
            std::string fnSymNs = lookupNs + ".functions";
            auto sym = sc->get(fnSymNs, functionName_);
            if (sym && sym->getKind() == Kind::Function) {
                funcSym = std::static_pointer_cast<FunctionSymbol>(sym);
                break;
            }
            auto pos = lookupNs.find_last_of('.');
            if (pos == std::string::npos) {
                break;
            }
            lookupNs = lookupNs.substr(0, pos);
        }
        if (!funcSym) {
            throw std::runtime_error("Function not found: " + functionName_);
        }
        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()));
        }
        // Enter function scope and bind parameters
        const std::string fnOpNs = funcSym->context() + "." + functionName_;
        sc->enter(fnOpNs);
        for (size_t i = 0; i < params.size(); ++i) {
            const auto &p = params[i];
            const Value &v = argValues[i];
            auto varSym = SymbolFactory::createVariable(p.name, v, fnOpNs);
            sc->add(varSym);
        }
        // Execute function body operations and capture return
        Symbols::Value returnValue;
        auto ops = Operations::Container::instance()->getAll(fnOpNs);
        for (const auto &op : ops) {
            try {
                interpreter.runOperation(*op);
            } catch (const ReturnException &ret) {
                returnValue = ret.value();
                break;
            }
        }
        sc->enterPreviousScope();
        return returnValue;
             // User-defined function: lookup through scope hierarchy
             SymbolContainer *sc = SymbolContainer::instance();
             std::string lookupNs = sc->currentScopeName();
             std::shared_ptr<FunctionSymbol> funcSym;
             // Search for function symbol in current and parent scopes
             while (true) {
                 std::string fnSymNs = lookupNs + ".functions";
                 auto sym = sc->get(fnSymNs, functionName_);
                 if (sym && sym->getKind() == Kind::Function) {
                     funcSym = std::static_pointer_cast<FunctionSymbol>(sym);
                     break;
                 }
                 auto pos = lookupNs.find_last_of('.');
                 if (pos == std::string::npos) {
                     break;
                 }
                 lookupNs = lookupNs.substr(0, pos);
             }
             if (!funcSym) {
                 throw std::runtime_error("Function not found: " + functionName_);
             }
             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()));
             }
             // Enter function scope and bind parameters
             const std::string fnOpNs = funcSym->context() + "." + functionName_;
             sc->enter(fnOpNs);
             for (size_t i = 0; i < params.size(); ++i) {
                 const auto &p = params[i];
                 const Value &v = argValues[i];
                 auto varSym = SymbolFactory::createVariable(p.name, v, fnOpNs);
                 sc->add(varSym);
             }
             // Execute function body operations and capture return
             Symbols::Value returnValue;
             auto ops = Operations::Container::instance()->getAll(fnOpNs);
             for (const auto &op : ops) {
                 try {
                     interpreter.runOperation(*op);
                 } catch (const ReturnException &ret) {
                     returnValue = ret.value();
                     break;
                 }
             }
             sc->enterPreviousScope();
             return returnValue;
         } catch (const std::exception &e) {
             throw ::Interpreter::Exception(e.what(), filename_, line_, column_);
         }
         // Unreachable: all paths either return or throw
         return Symbols::Value();
     }
     std::string toString() const override {
src/Interpreter/ExpressionBuilder.hpp
@@ -69,7 +69,9 @@
                for (const auto &arg : expr->args) {
                    callArgs.push_back(buildExpressionFromParsed(arg));
                }
                return std::make_unique<Interpreter::CallExpressionNode>(expr->name, std::move(callArgs));
                // Create call node with source location
                return std::make_unique<Interpreter::CallExpressionNode>(expr->name,
                    std::move(callArgs), expr->filename, expr->line, expr->column);
            }
        case Kind::Object:
            {
src/Modules/BaseModule.hpp
@@ -3,6 +3,8 @@
#define MODULES_BASEMODULE_HPP
// Base exception type for module errors
#include "../BaseException.hpp"
namespace Modules {
/**
@@ -20,5 +22,19 @@
    virtual void registerModule() = 0;
};
/**
 * @brief Exception type for errors thrown within module functions.
 * Inherit from BaseException to allow rich error messages.
 */
class Exception : public ::BaseException {
  public:
    /**
     * Construct a module exception with a message.
     * @param msg Error message
     */
    explicit Exception(const std::string & msg)
        : BaseException(msg) {}
};
} // namespace Modules
#endif // MODULES_BASEMODULE_HPP
src/Modules/BuiltIn/PrintModule.hpp
@@ -37,6 +37,15 @@
            std::cerr << "\n";
            return Symbols::Value();
        });
        // Built-in error thrower: throws a module exception with provided message
        mgr.registerFunction("throw_error", [](const std::vector<Symbols::Value> & args) {
            if (args.size() != 1 || args[0].getType() != Symbols::Variables::Type::STRING) {
                throw Exception("throw_error requires exactly one string argument");
            }
            std::string msg = args[0].get<std::string>();
            throw Exception(msg);
            return Symbols::Value();  // never reached
        });
    }
};
src/Parser/ParsedExpression.hpp
@@ -30,6 +30,10 @@
    // For function call arguments
    std::vector<ParsedExpressionPtr> args;
    std::vector<std::pair<std::string, ParsedExpressionPtr>> objectMembers;
    // Source location for error reporting
    std::string filename;
    int line = 0;
    size_t column = 0;
    // Constructor for literal
    static ParsedExpressionPtr makeLiteral(const Symbols::Value & val) {
src/Parser/Parser.cpp
@@ -927,8 +927,14 @@
                }
            }
            expect(Lexer::Tokens::Type::PUNCTUATION, ")");
            // Create call expression node
            output_queue.push_back(ParsedExpression::makeCall(func_name, std::move(call_args)));
            // Create call expression node with source location
            {
                auto pe = ParsedExpression::makeCall(func_name, std::move(call_args));
                pe->filename = this->current_filename_;
                pe->line = token.line_number;
                pe->column = token.column_number;
                output_queue.push_back(std::move(pe));
            }
            expect_unary = false;
        } else if (token.type == Lexer::Tokens::Type::OPERATOR_ARITHMETIC ||
                   token.type == Lexer::Tokens::Type::OPERATOR_RELATIONAL ||
src/VoidScript.hpp
@@ -3,6 +3,8 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <iostream>
#include <iterator>
#include "Interpreter/Interpreter.hpp"
#include "Lexer/Lexer.hpp"
@@ -32,15 +34,18 @@
    std::shared_ptr<Parser::Parser> parser = nullptr;
    static std::string readFile(const std::string & file) {
        // Read from stdin if '-' is specified
        if (file == "-") {
            return std::string(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
        }
        if (!std::filesystem::exists(file)) {
            throw std::runtime_error("File " + file + " does not exits");
            throw std::runtime_error("File " + file + " does not exist");
        }
        std::ifstream input(file, std::ios::in);
        if (!input.is_open()) {
            throw std::runtime_error("Could not open file " + file);
            return "";
        }
        std::string content = std::string((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
        std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
        input.close();
        return content;
    }
test_error.vs
New file
@@ -0,0 +1 @@
test_scripts/test_error.vs
New file
@@ -0,0 +1 @@
test_scripts/test_error2.vs
New file
@@ -0,0 +1 @@
test_scripts/test_throw_error.vs
New file
@@ -0,0 +1,2 @@
// Test script for throw_error built-in function
throw_error("Oops here");
test_scripts/throw_error_test.vs
New file
@@ -0,0 +1 @@