import type * as AST from "./ast"; import { parseMethodName } from "./ast"; const INTEGER_OPERATORS = new Set(["%", "&", "|", "^", "<<", ">>"]); const ARITHMETIC_OPERATORS = new Set(["+", "-", "*", "/"]); const COMPARISON_OPERATORS = new Set(["<", "<=", ">", ">=", "==", "!="]); const OPERATOR_HELPERS: Record = { // Arithmetic "+": "$.add", "-": "$.sub", "*": "$.mul", "/": "$.div", // Comparison "<": "$.lt", "<=": "$.le", ">": "$.gt", ">=": "$.ge", "==": "$.eq", "!=": "$.ne", // Integer "%": "$.mod", "&": "$.bitand", "|": "$.bitor", "^": "$.bitxor", "<<": "$.shl", ">>": "$.shr", }; export interface GeneratorOptions { indent?: string; runtime?: string; functions?: string; globals?: string; locals?: string; } export class CodeGenerator { private indent: string; private runtime: string; private functions: string; private globals: string; private locals: string; private indentLevel = 0; private currentClass: string | null = null; private currentFunction: string | null = null; constructor(options: GeneratorOptions = {}) { this.indent = options.indent ?? " "; this.runtime = options.runtime ?? "$"; this.functions = options.functions ?? "$f"; this.globals = options.globals ?? "$g"; this.locals = options.locals ?? "$l"; } private getAccessInfo(target: AST.Expression): { getter: string; setter: (value: string) => string; postIncHelper?: string; postDecHelper?: string; } | null { // Variable: $x or %x if (target.type === "Variable") { const name = JSON.stringify(target.name); const store = target.scope === "global" ? this.globals : this.locals; return { getter: `${store}.get(${name})`, setter: (value) => `${store}.set(${name}, ${value})`, postIncHelper: `${store}.postInc(${name})`, postDecHelper: `${store}.postDec(${name})`, }; } // MemberExpression: obj.prop if (target.type === "MemberExpression") { const obj = this.expression(target.object); const prop = target.property.type === "Identifier" ? JSON.stringify(target.property.name) : this.expression(target.property); return { getter: `${this.runtime}.prop(${obj}, ${prop})`, setter: (value) => `${this.runtime}.setProp(${obj}, ${prop}, ${value})`, postIncHelper: `${this.runtime}.propPostInc(${obj}, ${prop})`, postDecHelper: `${this.runtime}.propPostDec(${obj}, ${prop})`, }; } // IndexExpression: $arr[0] or obj[key] if (target.type === "IndexExpression") { const indices = Array.isArray(target.index) ? target.index.map((i) => this.expression(i)) : [this.expression(target.index)]; // Variable with index: $foo[0] becomes $foo0 if (target.object.type === "Variable") { const baseName = JSON.stringify(target.object.name); const store = target.object.scope === "global" ? this.globals : this.locals; const indicesStr = indices.join(", "); return { getter: `${store}.get(${baseName}, ${indicesStr})`, setter: (value) => `${store}.set(${baseName}, ${indicesStr}, ${value})`, postIncHelper: `${store}.postInc(${baseName}, ${indicesStr})`, postDecHelper: `${store}.postDec(${baseName}, ${indicesStr})`, }; } // Object index access: obj[key] const obj = this.expression(target.object); const index = indices.length === 1 ? indices[0] : `${this.runtime}.key(${indices.join(", ")})`; return { getter: `${this.runtime}.getIndex(${obj}, ${index})`, setter: (value) => `${this.runtime}.setIndex(${obj}, ${index}, ${value})`, postIncHelper: `${this.runtime}.indexPostInc(${obj}, ${index})`, postDecHelper: `${this.runtime}.indexPostDec(${obj}, ${index})`, }; } return null; } generate(ast: AST.Program): string { const lines: string[] = []; for (const stmt of ast.body) { const code = this.statement(stmt); if (code) lines.push(code); } return lines.join("\n\n"); } private statement(node: AST.Statement | AST.Comment): string { switch (node.type) { case "Comment": // Skip comments in generated output (or could emit as JS comments) return ""; case "ExpressionStatement": return this.line(`${this.expression(node.expression)};`); case "FunctionDeclaration": return this.functionDeclaration(node); case "PackageDeclaration": return this.packageDeclaration(node); case "DatablockDeclaration": return this.datablockDeclaration(node); case "ObjectDeclaration": return this.line(`${this.objectDeclaration(node)};`); case "IfStatement": return this.ifStatement(node); case "ForStatement": return this.forStatement(node); case "WhileStatement": return this.whileStatement(node); case "DoWhileStatement": return this.doWhileStatement(node); case "SwitchStatement": return this.switchStatement(node); case "ReturnStatement": return this.returnStatement(node); case "BreakStatement": return this.line("break;"); case "ContinueStatement": return this.line("continue;"); case "BlockStatement": return this.blockStatement(node); default: throw new Error(`Unknown statement type: ${(node as any).type}`); } } private functionDeclaration(node: AST.FunctionDeclaration): string { const nameInfo = parseMethodName(node.name.name); if (nameInfo) { // Method: Class::method - runtime handles case normalization const className = nameInfo.namespace; const methodName = nameInfo.method; this.currentClass = className.toLowerCase(); this.currentFunction = methodName.toLowerCase(); const body = this.functionBody(node.body, node.params); this.currentClass = null; this.currentFunction = null; return `${this.line(`${this.runtime}.registerMethod(${JSON.stringify(className)}, ${JSON.stringify(methodName)}, function() {`)}\n${body}\n${this.line(`});`)}`; } else { // Standalone function - runtime handles case normalization const funcName = node.name.name; this.currentFunction = funcName.toLowerCase(); const body = this.functionBody(node.body, node.params); this.currentFunction = null; return `${this.line(`${this.runtime}.registerFunction(${JSON.stringify(funcName)}, function() {`)}\n${body}\n${this.line(`});`)}`; } } private functionBody( node: AST.BlockStatement, params: AST.Variable[], ): string { this.indentLevel++; const lines: string[] = []; lines.push(this.line(`const ${this.locals} = ${this.runtime}.locals();`)); for (let i = 0; i < params.length; i++) { lines.push( this.line( `${this.locals}.set(${JSON.stringify(params[i].name)}, arguments[${i}]);`, ), ); } for (const stmt of node.body) { lines.push(this.statement(stmt)); } this.indentLevel--; return lines.join("\n"); } private packageDeclaration(node: AST.PackageDeclaration): string { // Runtime handles case normalization const pkgName = JSON.stringify(node.name.name); this.indentLevel++; const body = node.body.map((s) => this.statement(s)).join("\n\n"); this.indentLevel--; return `${this.line(`${this.runtime}.package(${pkgName}, function() {`)}\n${body}\n${this.line(`});`)}`; } private datablockDeclaration(node: AST.DatablockDeclaration): string { // Runtime handles case normalization const className = JSON.stringify(node.className.name); const instanceName = node.instanceName ? JSON.stringify(node.instanceName.name) : "null"; const parentName = node.parent ? JSON.stringify(node.parent.name) : "null"; const props = this.objectBody(node.body); return this.line( `${this.runtime}.datablock(${className}, ${instanceName}, ${parentName}, ${props});`, ); } private objectDeclaration(node: AST.ObjectDeclaration): string { // Runtime handles case normalization const className = node.className.type === "Identifier" ? JSON.stringify(node.className.name) : this.expression(node.className); const instanceName = node.instanceName === null ? "null" : node.instanceName.type === "Identifier" ? JSON.stringify(node.instanceName.name) : this.expression(node.instanceName); // Separate properties and child objects const props: AST.Assignment[] = []; const children: AST.ObjectDeclaration[] = []; for (const item of node.body) { if (item.type === "Assignment") { props.push(item); } else { children.push(item); } } const propsStr = this.objectBody(props); if (children.length > 0) { const childrenStr = children .map((c) => this.objectDeclaration(c)) .join(",\n"); return `${this.runtime}.create(${className}, ${instanceName}, ${propsStr}, [\n${childrenStr}\n])`; } return `${this.runtime}.create(${className}, ${instanceName}, ${propsStr})`; } private objectBody(items: AST.ObjectBodyItem[]): string { if (items.length === 0) return "{}"; const props: string[] = []; for (const item of items) { if (item.type === "Assignment") { const value = this.expression(item.value); if (item.target.type === "Identifier") { // Simple property: fieldName = value const key = item.target.name; if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { props.push(`${key}: ${value}`); } else { props.push(`[${JSON.stringify(key)}]: ${value}`); } } else if (item.target.type === "IndexExpression") { // Indexed property: stateName[0] = value // This sets a property on the object being defined, not an external variable const propKey = this.objectPropertyKey(item.target); props.push(`[${propKey}]: ${value}`); } else { // Other computed property key const computedKey = this.expression(item.target); props.push(`[${computedKey}]: ${value}`); } } } // Format: single line for 1 prop, multiline for 2+ if (props.length <= 1) { return `{ ${props.join(", ")} }`; } const innerIndent = this.indent.repeat(this.indentLevel + 1); const outerIndent = this.indent.repeat(this.indentLevel); return `{\n${innerIndent}${props.join(",\n" + innerIndent)}\n${outerIndent}}`; } /** * Generate a property key for an indexed expression inside an object/datablock body. * stateName[0] -> $.key("stateName", 0) * arr[i, j] -> $.key("arr", i, j) */ private objectPropertyKey(node: AST.IndexExpression): string { // Get the base name - should be an identifier for datablock properties const baseName = node.object.type === "Identifier" ? JSON.stringify(node.object.name) : this.expression(node.object); // Get the indices const indices = Array.isArray(node.index) ? node.index.map((i) => this.expression(i)).join(", ") : this.expression(node.index); return `${this.runtime}.key(${baseName}, ${indices})`; } private ifStatement(node: AST.IfStatement): string { const test = this.expression(node.test); const consequent = this.statementAsBlock(node.consequent); if (node.alternate) { if (node.alternate.type === "IfStatement") { // else if const alternate = this.ifStatement(node.alternate).replace(/^\s*/, ""); return this.line(`if (${test}) ${consequent} else ${alternate}`); } else { const alternate = this.statementAsBlock(node.alternate); return this.line(`if (${test}) ${consequent} else ${alternate}`); } } return this.line(`if (${test}) ${consequent}`); } private forStatement(node: AST.ForStatement): string { const init = node.init ? this.expression(node.init) : ""; const test = node.test ? this.expression(node.test) : ""; const update = node.update ? this.expression(node.update) : ""; const body = this.statementAsBlock(node.body); return this.line(`for (${init}; ${test}; ${update}) ${body}`); } private whileStatement(node: AST.WhileStatement): string { const test = this.expression(node.test); const body = this.statementAsBlock(node.body); return this.line(`while (${test}) ${body}`); } private doWhileStatement(node: AST.DoWhileStatement): string { const body = this.statementAsBlock(node.body); const test = this.expression(node.test); return this.line(`do ${body} while (${test});`); } private switchStatement(node: AST.SwitchStatement): string { if (node.stringMode) { // switch$ requires runtime helper for case-insensitive matching return this.switchStringStatement(node); } const discriminant = this.expression(node.discriminant); this.indentLevel++; const cases: string[] = []; for (const c of node.cases) { cases.push(this.switchCase(c)); } this.indentLevel--; return `${this.line(`switch (${discriminant}) {`)}\n${cases.join("\n")}\n${this.line("}")}`; } private switchCase(node: AST.SwitchCase): string { const lines: string[] = []; // Handle "or" syntax: case 1 or 2 or 3: if (node.test === null) { lines.push(this.line("default:")); } else if (Array.isArray(node.test)) { for (const t of node.test) { lines.push(this.line(`case ${this.expression(t)}:`)); } } else { lines.push(this.line(`case ${this.expression(node.test)}:`)); } this.indentLevel++; for (const stmt of node.consequent) { lines.push(this.statement(stmt)); } lines.push(this.line("break;")); this.indentLevel--; return lines.join("\n"); } private switchStringStatement(node: AST.SwitchStatement): string { // switch$ uses case-insensitive string matching - emit runtime call const discriminant = this.expression(node.discriminant); const cases: string[] = []; for (const c of node.cases) { if (c.test === null) { cases.push(`default: () => { ${this.blockContent(c.consequent)} }`); } else if (Array.isArray(c.test)) { for (const t of c.test) { cases.push( `${this.expression(t)}: () => { ${this.blockContent(c.consequent)} }`, ); } } else { cases.push( `${this.expression(c.test)}: () => { ${this.blockContent(c.consequent)} }`, ); } } return this.line( `${this.runtime}.switchStr(${discriminant}, { ${cases.join(", ")} });`, ); } private returnStatement(node: AST.ReturnStatement): string { if (node.value) { return this.line(`return ${this.expression(node.value)};`); } return this.line("return;"); } private blockStatement(node: AST.BlockStatement): string { this.indentLevel++; const content = node.body.map((s) => this.statement(s)).join("\n"); this.indentLevel--; return `{\n${content}\n${this.line("}")}`; } private statementAsBlock(node: AST.Statement): string { if (node.type === "BlockStatement") { return this.blockStatement(node); } // Wrap single statement in block this.indentLevel++; const content = this.statement(node); this.indentLevel--; return `{\n${content}\n${this.line("}")}`; } private blockContent(stmts: AST.Statement[]): string { return stmts.map((s) => this.statement(s).trim()).join(" "); } // =========================================================================== // Expressions // =========================================================================== private expression(node: AST.Expression): string { switch (node.type) { case "Identifier": return this.identifier(node); case "Variable": return this.variable(node); case "NumberLiteral": return String(node.value); case "StringLiteral": return JSON.stringify(node.value); case "BooleanLiteral": return String(node.value); case "BinaryExpression": return this.binaryExpression(node); case "UnaryExpression": return this.unaryExpression(node); case "PostfixExpression": return this.postfixExpression(node); case "AssignmentExpression": return this.assignmentExpression(node); case "ConditionalExpression": return `(${this.expression(node.test)} ? ${this.expression(node.consequent)} : ${this.expression(node.alternate)})`; case "CallExpression": return this.callExpression(node); case "MemberExpression": return this.memberExpression(node); case "IndexExpression": return this.indexExpression(node); case "TagDereferenceExpression": return `${this.runtime}.deref(${this.expression(node.argument)})`; case "ObjectDeclaration": return this.objectDeclaration(node); case "DatablockDeclaration": // Datablocks as expressions are rare but possible - runtime handles case normalization return `${this.runtime}.datablock(${JSON.stringify(node.className.name)}, ${node.instanceName ? JSON.stringify(node.instanceName.name) : "null"}, ${node.parent ? JSON.stringify(node.parent.name) : "null"}, ${this.objectBody(node.body)})`; default: throw new Error(`Unknown expression type: ${(node as any).type}`); } } private identifier(node: AST.Identifier): string { const info = parseMethodName(node.name); if (info && info.namespace.toLowerCase() === "parent") { return node.name; } if (info) { return `${this.runtime}.nsRef(${JSON.stringify(info.namespace)}, ${JSON.stringify(info.method)})`; } return JSON.stringify(node.name); } private variable(node: AST.Variable): string { if (node.scope === "global") { return `${this.globals}.get(${JSON.stringify(node.name)})`; } return `${this.locals}.get(${JSON.stringify(node.name)})`; } private binaryExpression(node: AST.BinaryExpression): string { const left = this.expression(node.left); const right = this.expression(node.right); const op = node.operator; // Integer operations need runtime helpers if (INTEGER_OPERATORS.has(op)) { const helper = OPERATOR_HELPERS[op]; return `${helper}(${left}, ${right})`; } // String concat operators const concat = this.concatExpression(left, op, right); if (concat) return concat; // String comparison operators if (op === "$=") { return `${this.runtime}.streq(${left}, ${right})`; } if (op === "!$=") { return `!${this.runtime}.streq(${left}, ${right})`; } // Logical operators (short-circuit, pass through) if (op === "&&" || op === "||") { return `(${left} ${op} ${right})`; } // Arithmetic operators use runtime helpers for proper numeric coercion if (ARITHMETIC_OPERATORS.has(op)) { const helper = OPERATOR_HELPERS[op]; return `${helper}(${left}, ${right})`; } // Comparison operators use runtime helpers for proper numeric coercion if (COMPARISON_OPERATORS.has(op)) { const helper = OPERATOR_HELPERS[op]; return `${helper}(${left}, ${right})`; } // Fallback (shouldn't reach here with valid TorqueScript) return `(${left} ${op} ${right})`; } private unaryExpression(node: AST.UnaryExpression): string { if (node.operator === "++" || node.operator === "--") { const access = this.getAccessInfo(node.argument); if (access) { const delta = node.operator === "++" ? 1 : -1; // Prefix: set and return the new value return access.setter(`${this.runtime}.add(${access.getter}, ${delta})`); } } const arg = this.expression(node.argument); if (node.operator === "~") { return `${this.runtime}.bitnot(${arg})`; } if (node.operator === "-") { return `${this.runtime}.neg(${arg})`; } // ! passes through (JS boolean coercion works correctly) return `${node.operator}${arg}`; } private postfixExpression(node: AST.PostfixExpression): string { const access = this.getAccessInfo(node.argument); if (access) { const helper = node.operator === "++" ? access.postIncHelper : access.postDecHelper; if (helper) { return helper; } } return `${this.expression(node.argument)}${node.operator}`; } private assignmentExpression(node: AST.AssignmentExpression): string { const value = this.expression(node.value); const op = node.operator; const access = this.getAccessInfo(node.target); if (!access) { throw new Error(`Unhandled assignment target type: ${node.target.type}`); } if (op === "=") { // Simple assignment return access.setter(value); } else { // Compound assignment: +=, -=, etc. const baseOp = op.slice(0, -1); const newValue = this.compoundAssignmentValue( access.getter, baseOp, value, ); return access.setter(newValue); } } private callExpression(node: AST.CallExpression): string { const args = node.arguments.map((a) => this.expression(a)).join(", "); if (node.callee.type === "Identifier") { const name = node.callee.name; const info = parseMethodName(name); if (info && info.namespace.toLowerCase() === "parent") { if (this.currentClass) { return `${this.runtime}.parent(${JSON.stringify(this.currentClass)}, ${JSON.stringify(info.method)}, arguments[0]${args ? ", " + args : ""})`; } else if (this.currentFunction) { return `${this.runtime}.parentFunc(${JSON.stringify(this.currentFunction)}${args ? ", " + args : ""})`; } else { throw new Error("Parent:: call outside of function context"); } } if (info) { return `${this.runtime}.nsCall(${JSON.stringify(info.namespace)}, ${JSON.stringify(info.method)}${args ? ", " + args : ""})`; } return `${this.functions}.call(${JSON.stringify(name)}${args ? ", " + args : ""})`; } if (node.callee.type === "MemberExpression") { const obj = this.expression(node.callee.object); const method = node.callee.property.type === "Identifier" ? JSON.stringify(node.callee.property.name) : this.expression(node.callee.property); return `${this.runtime}.call(${obj}, ${method}${args ? ", " + args : ""})`; } const callee = this.expression(node.callee); return `${callee}(${args})`; } private memberExpression(node: AST.MemberExpression): string { const obj = this.expression(node.object); if (node.computed || node.property.type !== "Identifier") { return `${this.runtime}.prop(${obj}, ${this.expression(node.property)})`; } return `${this.runtime}.prop(${obj}, ${JSON.stringify(node.property.name)})`; } private indexExpression(node: AST.IndexExpression): string { const indices = Array.isArray(node.index) ? node.index.map((i) => this.expression(i)) : [this.expression(node.index)]; if (node.object.type === "Variable") { const baseName = JSON.stringify(node.object.name); const store = node.object.scope === "global" ? this.globals : this.locals; return `${store}.get(${baseName}, ${indices.join(", ")})`; } const obj = this.expression(node.object); if (indices.length === 1) { return `${this.runtime}.getIndex(${obj}, ${indices[0]})`; } return `${this.runtime}.getIndex(${obj}, ${this.runtime}.key(${indices.join(", ")}))`; } private line(code: string): string { return this.indent.repeat(this.indentLevel) + code; } private concatExpression( left: string, op: string, right: string, ): string | null { switch (op) { case "@": return `${this.runtime}.concat(${left}, ${right})`; case "SPC": return `${this.runtime}.concat(${left}, " ", ${right})`; case "TAB": return `${this.runtime}.concat(${left}, "\\t", ${right})`; case "NL": return `${this.runtime}.concat(${left}, "\\n", ${right})`; default: return null; } } private compoundAssignmentValue( getter: string, op: string, value: string, ): string { // Integer operators need runtime helpers if (INTEGER_OPERATORS.has(op)) { const helper = OPERATOR_HELPERS[op]; return `${helper}(${getter}, ${value})`; } // String concat operators const concat = this.concatExpression(getter, op, value); if (concat) return concat; // Arithmetic operators need runtime helpers for proper numeric coercion if (ARITHMETIC_OPERATORS.has(op)) { const helper = OPERATOR_HELPERS[op]; return `${helper}(${getter}, ${value})`; } // Fallback (shouldn't reach here with valid TorqueScript) return `(${getter} ${op} ${value})`; } } export function generate(ast: AST.Program, options?: GeneratorOptions): string { const generator = new CodeGenerator(options); return generator.generate(ast); }