versescript
Version:
VerseScript is specifically built for microtron AI's AR/VR Verse platform. VerseScript was made to develop apps on verse, however it can be used for more generic usage. It works well in any javascript project, imports javascript functions or classes to be
1,052 lines (942 loc) • 34.5 kB
JavaScript
import parser from "./parser.js";
//const parser = require("./parser.js");
class VerseScript {
constructor(baseUrl = "") {
this.globalScope = {};
this.classes = {};
this.jsClasses = {};
this.decorators = {
log: this.createLogDecorator(),
deprecated: this.createDeprecatedDecorator(),
memoize: this.createMemoizeDecorator()
};
this.jsFunctions = {
print: (msg) => {
// Better handling of different types for print
let output = msg;
if (typeof msg === 'object' && msg !== null) {
try {
output = JSON.stringify(msg, null, 2);
} catch (e) {
output = String(msg);
}
}
console.log(output);
},
add: (a, b) => a + b,
toJson: (obj) => {
const replacer = (key, value) => {
if (key === 'constructor') {
return undefined;
}
return value;
};
return JSON.stringify(obj, replacer);
},
parseJson: (str) => {
const parsed = JSON.parse(str);
if (typeof parsed === 'object' && parsed !== null) {
delete parsed.constructor;
}
return parsed;
},
getStringLength: (str) => str.length,
arrayPush: (arr, item) => {
arr.push(item);
return arr;
},
arrayPop: (arr) => arr.pop(),
};
this.currentScope = this.globalScope;
this.currentThis = null;
this.macros = {};
this.modules = new Map();
this.currentModule = null;
this.moduleQueue = [];
this.baseUrl = baseUrl;
}
formatError(error, location = null) {
let errorMessage = "";
// Handle different types of errors
if (typeof error === 'string') {
errorMessage = error;
} else if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'object' && error !== null) {
try {
errorMessage = JSON.stringify(error);
} catch (e) {
errorMessage = error.toString();
}
} else {
errorMessage = String(error);
}
// Add location information if available
if (location) {
const filename = this.currentModule ? this.currentModule.filename : 'unknown';
errorMessage += `\nAt ${filename}:${location.start.line}:${location.start.column}`;
}
return errorMessage;
}
logError(error, context = "", location = null) {
const formattedError = this.formatError(error, location);
console.error(`${context}${context ? ": " : ""}${formattedError}`);
// Also log the raw error object for debugging
if (typeof error === 'object' && error !== null) {
console.error("Raw error object:", error);
}
return formattedError;
}
addJsFunction(name, func) {
if (typeof func !== "function") {
throw new Error("Second argument must be a function");
}
this.jsFunctions[name] = func;
}
addDecorator(name, decorator) {
this.decorators[name] = decorator;
}
createLogDecorator() {
return {
method: (target, name, descriptor) => {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name} with args:`, args);
const result = original.apply(this, args);
console.log(`${name} returned:`, result);
return result;
};
return descriptor;
}
};
}
createDeprecatedDecorator() {
return {
method: (target, name, descriptor) => {
const original = descriptor.value;
descriptor.value = function(...args) {
console.warn(`Warning: ${name} is deprecated`);
return original.apply(this, args);
};
return descriptor;
}
};
}
createMemoizeDecorator() {
return {
method: (target, name, descriptor) => {
const original = descriptor.value;
const cache = new Map();
descriptor.value = function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = original.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
};
}
addJsClass(name, jsClass) {
if (
typeof jsClass !== "function" ||
typeof jsClass.prototype !== "object"
) {
throw new Error("Second argument must be a class");
}
this.jsClasses[name] = class extends jsClass {
constructor(...args) {
super(...args);
Object.getOwnPropertyNames(jsClass.prototype).forEach((key) => {
if (key !== "constructor" && typeof this[key] === "function") {
this[key] = this[key].bind(this);
}
});
}
};
}
async interpretMultiple(files) {
this.moduleQueue = files.map((file) => ({
filename: file,
content: null,
}));
while (this.moduleQueue.length > 0) {
const { filename } = this.moduleQueue.shift();
await this.interpret(filename);
}
return this.modules.get(files[0]).exports;
}
async interpret(input, isFilename = false) {
let content;
let filename;
if (isFilename) {
filename = input;
if (this.modules.has(filename)) {
return this.modules.get(filename).exports;
}
content = await this.fetchModuleContent(filename);
} else {
content = input;
filename = "inline-script";
}
const module = {
exports: {},
filename,
content, // Store content for error reporting
loaded: false,
};
this.modules.set(filename, module);
this.currentModule = module;
try {
const ast = parser.parse(content);
for (const node of ast.body) {
await this.evaluateNode(node);
}
} catch (error) {
if (error.location) {
const lines = content.split("\n");
const line = lines[error.location.start.line - 1];
const pointer = " ".repeat(error.location.start.column - 1) + "^";
const enhancedMessage = `${error.message}\nAt ${filename}:${error.location.start.line}:${error.location.start.column}\n${line}\n${pointer}`;
console.error("Parse Error:", enhancedMessage);
throw new Error(enhancedMessage);
} else {
console.error("Interpreter Error:", this.formatError(error));
throw error;
}
}
module.loaded = true;
this.currentModule = null;
return module.exports;
}
async fetchModuleContent(filename) {
const response = await fetch(this.baseUrl + filename);
if (!response.ok) {
throw new Error(`Failed to fetch module: ${filename}`);
}
return await response.text();
}
async evaluateImport(node) {
const sourceModule = await this.loadModuleFile(node.source.value);
for (const specifier of node.specifiers) {
if (!(specifier.name.name in sourceModule.exports)) {
throw new Error(
`Module '${node.source.value}' does not export '${specifier.name.name}'`,
);
}
this.currentScope[specifier.alias.name] =
sourceModule.exports[specifier.name.name];
}
}
async evaluateExport(node) {
if (node.declaration) {
await this.evaluateNode(node.declaration);
if (node.declaration.type === "variableDeclaration") {
this.currentModule.exports[node.declaration.id.name] =
this.currentScope[node.declaration.id.name];
} else if (
node.declaration.type === "functionDefinition" ||
node.declaration.type === "classDefinition"
) {
this.currentModule.exports[node.declaration.name.name] =
this.currentScope[node.declaration.name.name];
}
} else if (node.specifiers) {
for (const specifier of node.specifiers) {
this.currentModule.exports[specifier.alias.name] =
this.currentScope[specifier.name.name];
}
}
}
async loadModuleFile(moduleName) {
const extension = ".vs";
const moduleFile = new URL(moduleName + extension, this.baseUrl).href;
if (this.modules.has(moduleFile)) {
return this.modules.get(moduleFile);
}
return await this.interpret(moduleFile, true);
}
async evaluateNode(node) {
try {
switch (node.type) {
case "classDefinition":
await this.defineClass(node);
break;
case "variableDeclaration":
const value = node.init ? await this.evaluateNode(node.init) : undefined;
this.currentScope[node.id.name] = value;
break;
case "nullLiteral":
return null;
case "macroExpansion":
return await this.expandMacro(node);
case "functionDefinition":
const func = async (...args) => {
const previousScope = this.currentScope;
this.currentScope = Object.create(this.currentScope);
node.params.forEach((param, index) => {
this.currentScope[param.name] = args[index];
});
try {
const result = await this.evaluateBlock(node.body);
return result;
} catch (e) {
if (e.type === "return") {
return e.value;
}
throw e;
} finally {
this.currentScope = previousScope;
}
};
this.currentScope[node.name.name] = func;
break;
case "functionExpression":
return async (...args) => {
const previousScope = this.currentScope;
this.currentScope = Object.create(this.currentScope);
node.params.forEach((param, index) => {
this.currentScope[param.name] = args[index];
});
try {
const result = await this.evaluateBlock(node.body);
return result;
} catch (e) {
if (e.type === "return") {
return e.value;
}
throw e;
} finally {
this.currentScope = previousScope;
}
};
case "ifStatement":
if (await this.evaluateNode(node.test)) {
await this.evaluateBlock(node.consequent);
} else if (node.alternate) {
if (Array.isArray(node.alternate)) {
await this.evaluateBlock(node.alternate);
} else {
await this.evaluateNode(node.alternate);
}
}
break;
case "tryCatchStatement":
try {
await this.evaluateBlock(node.tryBlock);
} catch (error) {
const previousScope = this.currentScope;
this.currentScope = Object.create(this.currentScope);
// Format the caught error for VerseScript
let formattedError = error;
if (typeof error === 'object' && error !== null && !error.type) {
formattedError = this.formatError(error);
}
this.currentScope[node.errorParam.name] = formattedError;
try {
await this.evaluateBlock(node.catchBlock);
} finally {
this.currentScope = previousScope;
}
}
break;
case "forLoop":
await this.evaluateForLoop(node);
break;
case "whileLoop":
while (await this.evaluateNode(node.test)) {
await this.evaluateBlock(node.body);
}
break;
case "returnStatement":
this.returnValue = node.value ? await this.evaluateNode(node.value) : undefined;
throw { type: "return", value: this.returnValue };
case "throwStatement":
const throwValue = await this.evaluateNode(node.value);
const formattedThrowError = this.formatError(throwValue, node.loc);
this.logError(throwValue, "Thrown error", node.loc);
throw throwValue;
case "assignment":
return await this.evaluateAssignment(node);
case "objectCreation":
return await this.createObject(node);
case "functionCall":
return await this.callFunction(node);
case "self":
if (!this.currentThis) {
throw new Error("'self' used outside of a class method");
}
return this.currentThis;
case "memberExpression":
return await this.evaluateMemberExpression(node);
case "identifier":
return this.resolveIdentifier(node);
case "awaitExpression":
const awaitedValue = await this.evaluateNode(node.argument);
return awaitedValue;
case "integer":
case "float":
case "string":
case "boolean":
case "char":
return node.value;
case "logicalOr":
return await this.evaluateNode(node.left) || await this.evaluateNode(node.right);
case "logicalAnd":
return await this.evaluateNode(node.left) && await this.evaluateNode(node.right);
case "equality":
return await this.evaluateEquality(node);
case "comparison":
return await this.evaluateComparison(node);
case "addition":
case "multiplication":
return await this.evaluateBinaryOperation(node);
case "unary":
return await this.evaluateUnaryOperation(node);
case "updateExpression":
return await this.evaluateUpdateExpression(node);
case "macroDefinition":
this.defineMacro(node);
break;
case "importDeclaration":
await this.evaluateImport(node);
break;
case "exportDeclaration":
await this.evaluateExport(node);
break;
case "arrayLiteral":
return await Promise.all(node.elements.map(element => this.evaluateNode(element)));
case "objectLiteral":
const obj = {};
for (const property of node.properties) {
const value = await this.evaluateNode(property.value);
obj[property.key] = value;
}
return obj;
case "expressionStatement":
return await this.evaluateNode(node.expression);
default:
throw new Error(`Unknown node type: ${node.type}`);
}
} catch (error) {
if (error.type === "return") {
throw error; // Re-throw return statements without modification
}
// Enhanced error handling with location information
const filename = this.currentModule ? this.currentModule.filename : 'unknown';
let errorMessage = this.formatError(error);
if (node.loc && !errorMessage.includes("At ")) {
errorMessage += `\nAt ${filename}:${node.loc.start.line}:${node.loc.start.column}`;
// Try to get the actual line of code for better context
if (this.currentModule && this.currentModule.content) {
const lines = this.currentModule.content.split('\n');
const errorLine = lines[node.loc.start.line - 1];
if (errorLine) {
const pointer = " ".repeat(node.loc.start.column - 1) + "^";
errorMessage += `\n${errorLine}\n${pointer}`;
}
}
}
// Log the error with context
console.error(`Runtime Error: ${errorMessage}`);
if (typeof error === 'object' && error !== null) {
console.error("Error details:", error);
}
// Create a new error with enhanced message
const enhancedError = new Error(errorMessage);
enhancedError.originalError = error;
enhancedError.location = node.loc;
throw enhancedError;
}
}
async defineClass(node) {
// Initialize classInfo with default values
let classInfo = {
methods: {},
properties: {},
constructor: (instance, args) => {},
parentClass: null
};
// Handle class inheritance
if (node.superClass) {
classInfo.parentClass = this.classes[node.superClass.name];
if (!classInfo.parentClass) {
throw new Error(`Superclass ${node.superClass.name} is not defined`);
}
}
// Apply class decorators if present
if (node.decorators && node.decorators.length > 0) {
for (const decorator of node.decorators) {
const decoratorFn = this.decorators[decorator.name.name];
if (!decoratorFn) {
throw new Error(`Unknown decorator: @${decorator.name.name}`);
}
if (decoratorFn.class) {
classInfo = decoratorFn.class(classInfo);
}
}
}
// Process class members (methods and properties)
for (const member of node.members) {
const isPrivate = member.name.name.startsWith('_');
if (member.type === "methodDefinition") {
// Create basic method descriptor
let descriptor = {
value: this.createMethod(member, node.name.name).bind(this),
writable: true,
enumerable: true,
configurable: true
};
// Apply method decorators if present
if (member.decorators && member.decorators.length > 0) {
for (const decorator of member.decorators) {
const decoratorFn = this.decorators[decorator.name.name];
if (!decoratorFn) {
throw new Error(`Unknown decorator: @${decorator.name.name}`);
}
if (decoratorFn.method) {
descriptor = decoratorFn.method(classInfo, member.name.name, descriptor);
}
}
}
// Store the method with its privacy flag
classInfo.methods[member.name.name] = {
method: descriptor.value,
isPrivate
};
} else if (member.type === "propertyDefinition") {
// Handle property definitions
const value = member.value !== null ? await this.evaluateNode(member.value) : undefined;
classInfo.properties[member.name.name] = {
value,
isPrivate
};
}
}
// Handle constructor if defined
if (node.constructor) {
classInfo.constructor = this.createConstructor(node.constructor, node.name.name).bind(this);
}
// Store the class definition
this.classes[node.name.name] = classInfo;
// Create the constructor function for creating instances
const classConstructor = async (...args) => {
const instance = Object.create(null);
// Add constructor reference
instance.constructor = { name: node.name.name };
// Build prototype chain and add members
let currentClass = this.classes[node.name.name];
while (currentClass) {
// Add methods from current class
Object.entries(currentClass.methods).forEach(([name, methodInfo]) => {
if (!methodInfo.isPrivate || this.currentThis === instance) {
instance[name] = (...args) => methodInfo.method(instance, ...args);
}
});
// Add properties from current class
Object.entries(currentClass.properties).forEach(([name, propertyInfo]) => {
if (!propertyInfo.isPrivate || this.currentThis === instance) {
Object.defineProperty(instance, name, {
value: propertyInfo.value,
writable: true,
enumerable: true,
configurable: true
});
}
});
// Move up the inheritance chain
currentClass = currentClass.parentClass;
}
// Call the constructor
const result = await classInfo.constructor(instance, args);
// Ensure instance properties are properly set
if (result !== undefined) {
Object.assign(instance, result);
}
return instance;
};
// Store constructor in global scope
this.globalScope[node.name.name] = classConstructor;
}
createMethod(node, className) {
return async (instance, ...args) => {
const previousScope = this.currentScope;
const previousThis = this.currentThis;
this.currentScope = Object.create(this.globalScope);
this.currentThis = instance;
this.currentScope.self = instance;
const classInfo = this.classes[className];
if (classInfo && classInfo.parentClass) {
this.currentScope.super = {};
Object.entries(classInfo.parentClass.methods).forEach(([methodName, methodInfo]) => {
if (!methodInfo.isPrivate) {
this.currentScope.super[methodName] = async (...args) => {
return await methodInfo.method(instance, ...args);
};
}
});
}
node.params.forEach((param, index) => {
this.currentScope[param.name] = args[index];
});
try {
const result = await this.evaluateBlock(node.body);
return result;
} catch (e) {
if (e.type === "return") {
return e.value;
}
throw e;
} finally {
this.currentScope = previousScope;
this.currentThis = previousThis;
}
};
}
createConstructor(node, className) {
return async (instance, args) => {
const previousScope = this.currentScope;
const previousThis = this.currentThis;
this.currentScope = Object.create(this.globalScope);
this.currentThis = instance;
// Add 'self' to the scope
this.currentScope.self = instance;
// Add 'super' to the scope
const classInfo = this.classes[className];
if (classInfo && classInfo.parentClass) {
this.currentScope.super = (...superArgs) => {
return classInfo.parentClass.constructor(instance, superArgs);
};
}
node.params.forEach((param, index) => {
this.currentScope[param.name] = args[index];
});
try {
const result = await this.evaluateBlock(node.body);
// Ensure that the instance properties are set
Object.assign(instance, this.currentScope.self);
return result;
} finally {
this.currentScope = previousScope;
this.currentThis = previousThis;
}
};
}
async createObject(node) {
if (this.classes[node.className.name]) {
const classInfo = this.classes[node.className.name];
const instance = Object.create(null);
// Add constructor property
instance.constructor = { name: node.className.name };
// Set up the prototype chain
let currentClass = classInfo;
const prototypeChain = [];
while (currentClass) {
prototypeChain.unshift(currentClass);
currentClass = currentClass.parentClass;
}
// Attach methods to the instance, including private methods
prototypeChain.forEach(cls => {
Object.entries(cls.methods).forEach(([name, methodInfo]) => {
instance[name] = (...args) => methodInfo.method(instance, ...args);
});
});
// Attach properties to the instance, including private properties
prototypeChain.forEach(cls => {
Object.entries(cls.properties).forEach(([name, propertyInfo]) => {
instance[name] = propertyInfo.value;
});
});
// Call only the constructor of the instantiated class
const args = await Promise.all(node.arguments.map((arg) => this.evaluateNode(arg)));
await classInfo.constructor(instance, args);
return instance;
} else if (this.jsClasses[node.className.name]) {
const JsClass = this.jsClasses[node.className.name];
const args = await Promise.all(node.arguments.map((arg) => this.evaluateNode(arg)));
const instance = new JsClass(...args);
return instance;
} else {
// Use the stored constructor function instead
if (this.globalScope[node.className.name]) {
const Constructor = this.globalScope[node.className.name];
const args = await Promise.all(node.arguments.map((arg) => this.evaluateNode(arg)));
return await Constructor(...args);
}
console.error("Class not found:", node.className.name);
throw new Error(`Unknown class: ${node.className.name}`);
}
}
async callFunction(node) {
let func;
let thisArg = null;
try {
if (node.callee.type === "memberExpression") {
const obj = await this.evaluateNode(node.callee.object);
const prop = node.callee.property.name;
try {
func = await this.evaluateMemberExpression(node.callee);
} catch (error) {
const errorMsg = `Error accessing method '${prop}': ${this.formatError(error)}`;
this.logError(error, "Method access error", node.loc);
throw new Error(errorMsg);
}
thisArg = obj;
} else if (node.callee.type === "identifier") {
func = this.resolveIdentifier(node.callee);
} else {
func = await this.evaluateNode(node.callee);
}
if (typeof func !== "function") {
const calleeName = node.callee.name || node.callee.type || "Expression";
const error = new Error(`'${calleeName}' is not a function (got ${typeof func})`);
this.logError(error, "Function call error", node.loc);
throw error;
}
const args = await Promise.all(node.arguments.map(arg => this.evaluateNode(arg)));
const previousThis = this.currentThis;
this.currentThis = thisArg;
try {
const result = await func.apply(thisArg, args);
return result;
} catch (error) {
const calleeName = node.callee.name || "anonymous function";
const formattedError = `Error in function '${calleeName}': ${this.formatError(error)}`;
this.logError(error, `Function execution error (${calleeName})`, node.loc);
throw new Error(formattedError);
} finally {
this.currentThis = previousThis;
}
} catch (error) {
// Re-throw with additional context if not already enhanced
if (!error.message.includes("Function call error") && !error.message.includes("Function execution error")) {
const enhancedError = new Error(`Function call failed: ${this.formatError(error)}`);
this.logError(error, "Function call failure", node.loc);
throw enhancedError;
}
throw error;
}
}
resolveIdentifier(node) {
if (node.name in this.currentScope) {
return this.currentScope[node.name];
}
if (this.currentThis && node.name in this.currentThis) {
return this.currentThis[node.name];
}
if (node.name in this.globalScope) {
return this.globalScope[node.name];
}
if (node.name in this.jsFunctions) {
return this.jsFunctions[node.name];
}
if (node.name in this.jsClasses) {
return this.jsClasses[node.name];
}
throw new Error(`Undefined variable: ${node.name}`);
}
async evaluateMemberExpression(node) {
let obj;
if (node.object.type === "self") {
obj = this.currentThis;
} else {
obj = await this.evaluateNode(node.object);
}
const prop = node.computed ? await this.evaluateNode(node.property) : node.property.name;
if (obj && typeof obj === 'object') {
const className = obj.constructor ? obj.constructor.name : null;
const classInfo = className ? this.classes[className] : null;
if (classInfo) {
const method = classInfo.methods[prop];
const property = classInfo.properties[prop];
if ((method && method.isPrivate) || (property && property.isPrivate)) {
if (this.currentThis !== obj) {
throw new Error(`Cannot access private member '${prop}'`);
}
}
}
}
if (prop.startsWith && prop.startsWith('_') && this.currentThis !== obj) {
throw new Error(`Cannot access private member '${prop}'`);
}
const result = obj[prop];
return result;
}
async evaluateAssignment(node) {
const value = await this.evaluateNode(node.right);
if (node.left.type === "identifier") {
this.currentScope[node.left.name] = value;
} else if (node.left.type === "memberExpression") {
const obj = await this.evaluateNode(node.left.object);
if (node.left.computed) {
const prop = await this.evaluateNode(node.left.property);
obj[prop] = value;
} else {
obj[node.left.property.name] = value;
}
}
return value;
}
async evaluateBlock(block) {
for (const statement of block) {
await this.evaluateNode(statement);
}
}
async evaluateForLoop(node) {
try {
if (node.init) {
await this.evaluateNode(node.init);
}
while (true) {
if (node.test && !(await this.evaluateNode(node.test))) {
break;
}
await this.evaluateBlock(node.body);
if (node.update) {
await this.evaluateNode(node.update);
}
}
} finally {
// Cleanup if needed
}
}
createFunction(node) {
return async (...args) => {
const previousScope = this.currentScope;
this.currentScope = Object.create(this.currentScope);
node.params.forEach((param, index) => {
this.currentScope[param.name] = args[index];
});
try {
await this.evaluateBlock(node.body);
} catch (e) {
if (e.type === "return") {
return e.value;
}
throw e;
} finally {
this.currentScope = previousScope;
}
return undefined;
};
}
async evaluateEquality(node) {
const left = await this.evaluateNode(node.left);
const right = await this.evaluateNode(node.right);
switch (node.operator) {
case "==":
return left == right;
case "!=":
return left != right;
}
}
async evaluateComparison(node) {
const left = await this.evaluateNode(node.left);
const right = await this.evaluateNode(node.right);
switch (node.operator) {
case "<":
return left < right;
case "<=":
return left <= right;
case ">":
return left > right;
case ">=":
return left >= right;
}
}
async evaluateBinaryOperation(node) {
const left = await this.evaluateNode(node.left);
const right = await this.evaluateNode(node.right);
switch (node.operator) {
case "+":
return left + right;
case "-":
return left - right;
case "*":
return left * right;
case "/":
return left / right;
case "%":
return left % right;
}
}
async evaluateUnaryOperation(node) {
const operand = await this.evaluateNode(node.right);
switch (node.operator) {
case "-":
return -operand;
case "!":
return !operand;
case "typeof":
return typeof operand;
}
}
async evaluateUpdateExpression(node) {
if (node.argument.type === "identifier") {
const currentValue = this.resolveIdentifier(node.argument);
let newValue;
if (node.operator === "++") {
newValue = currentValue + 1;
} else if (node.operator === "--") {
newValue = currentValue - 1;
}
// Update the variable
this.currentScope[node.argument.name] = newValue;
// Return the appropriate value based on prefix/postfix
return node.prefix ? newValue : currentValue;
} else if (node.argument.type === "memberExpression") {
const obj = await this.evaluateNode(node.argument.object);
const prop = node.argument.computed ? await this.evaluateNode(node.argument.property) : node.argument.property.name;
const currentValue = obj[prop];
let newValue;
if (node.operator === "++") {
newValue = currentValue + 1;
} else if (node.operator === "--") {
newValue = currentValue - 1;
}
// Update the property
obj[prop] = newValue;
// Return the appropriate value based on prefix/postfix
return node.prefix ? newValue : currentValue;
} else {
throw new Error("Invalid left-hand side expression in assignment");
}
}
defineMacro(node) {
this.macros[node.name.name] = {
params: node.params,
body: node.body.trim(),
};
}
async expandMacro(node) {
const macro = this.macros[node.name.name];
if (!macro) {
const error = new Error(`Unknown macro: ${node.name.name}`);
this.logError(error, "Macro expansion error", node.loc);
throw error;
}
let expandedBody = macro.body;
for (let i = 0; i < macro.params.length; i++) {
const param = macro.params[i];
const argValue = await this.evaluateNode(node.arguments[i]);
const regex = new RegExp(`\\b${param.name}\\b`, "g");
expandedBody = expandedBody.replace(regex, JSON.stringify(argValue));
}
try {
const expandedAst = parser.parse(expandedBody, {
startRule: "expression",
});
return await this.evaluateNode(expandedAst);
} catch (error) {
const formattedError = `Error parsing expanded macro '${node.name.name}': ${this.formatError(error)}`;
console.error(formattedError);
console.error("Expanded macro body:", expandedBody);
throw new Error(formattedError);
}
}
}
export default VerseScript;
//module.exports = VerseScript;