t2-mapper/TorqueScript.pegjs
2025-12-02 22:06:20 -08:00

682 lines
17 KiB
JavaScript

{{
// Collect exec() script paths during parsing (deduplicated)
// These are reset in the per-parse initializer below
let execScriptPathsSet;
let hasDynamicExec;
function buildBinaryExpression(head, tail) {
return tail.reduce((left, [op, right]) => ({
type: 'BinaryExpression',
operator: op,
left,
right
}), head);
}
function buildUnaryExpression(operator, argument) {
return {
type: 'UnaryExpression',
operator,
argument
};
}
function buildCallExpression(callee, args) {
// Check if this is an exec() call
if (callee.type === 'Identifier' && callee.name.toLowerCase() === 'exec') {
if (args.length > 0 && args[0].type === 'StringLiteral') {
execScriptPathsSet.add(args[0].value);
} else {
hasDynamicExec = true;
}
}
return {
type: 'CallExpression',
callee,
arguments: args
};
}
function getExecScriptPaths() {
return Array.from(execScriptPathsSet);
}
}}
// Per-parse initializer - reset state for each parse call
{
execScriptPathsSet = new Set();
hasDynamicExec = false;
}
// Main entry point
Program
= ws items:((Comment / Statement) ws)* {
return {
type: 'Program',
body: items.map(([item]) => item).filter(Boolean),
execScriptPaths: getExecScriptPaths(),
hasDynamicExec
};
}
// Statements
Statement
= PackageDeclaration
/ FunctionDeclaration
/ DatablockStatement
/ ObjectStatement
/ IfStatement
/ ForStatement
/ DoWhileStatement
/ WhileStatement
/ SwitchStatement
/ ReturnStatement
/ BreakStatement
/ ContinueStatement
/ ExpressionStatement
/ BlockStatement
/ Comment
/ _ ";" _ { return null; }
DatablockStatement
= decl:DatablockDeclaration _ ";"? _ { return decl; }
ObjectStatement
= decl:ObjectDeclaration _ ";"? _ { return decl; }
PackageDeclaration
= "package" __ name:Identifier _ "{" ws items:((Comment / Statement) ws)* "}" _ ";"? {
return {
type: 'PackageDeclaration',
name,
body: items.map(([item]) => item).filter(Boolean)
};
}
FunctionDeclaration
= "function" __ name:FunctionName _ "(" _ params:ParameterList? _ ")" _ body:BlockStatement {
return {
type: 'FunctionDeclaration',
name,
params: params || [],
body
};
}
FunctionName
= namespace:Identifier "::" method:Identifier {
return { type: 'MethodName', namespace, method };
}
/ Identifier
ParameterList
= head:Identifier tail:(_ "," _ Identifier)* {
return [head, ...tail.map(([,,,id]) => id)];
}
DatablockDeclaration
= "datablock" __ className:Identifier _ "(" _ instanceName:ObjectName? _ ")" _ parent:(":" _ Identifier)? _ body:("{" _ ObjectBody* _ "}" _)? {
return {
type: 'DatablockDeclaration',
className,
instanceName,
parent: parent ? parent[2] : null,
body: body ? body[2].filter(Boolean) : []
};
}
ObjectDeclaration
= "new" __ className:ClassNameExpression _ "(" _ instanceName:ObjectName? _ ")" _ body:("{" _ ObjectBody* _ "}" _)? {
return {
type: 'ObjectDeclaration',
className,
instanceName,
body: body ? body[2].filter(Boolean) : []
};
}
ClassNameExpression
= "(" _ expr:Expression _ ")" { return expr; }
/ base:Identifier accessors:(_ "[" _ IndexList _ "]")* {
return accessors.reduce((obj, [,,,indices]) => ({
type: 'IndexExpression',
object: obj,
index: indices
}), base);
}
ObjectBody
= obj:ObjectDeclaration _ ";"? _ { return obj; }
/ db:DatablockDeclaration _ ";"? _ { return db; }
/ Assignment
/ Comment
/ Whitespace
ObjectName
= StringConcatExpression
/ Identifier
/ NumberLiteral
Assignment
= _ target:LeftHandSide _ "=" _ value:Expression _ ";"? _ {
return {
type: 'Assignment',
target,
value
};
}
LeftHandSide
= base:CallExpression accessors:Accessor* {
return accessors.reduce((obj, accessor) => {
if (accessor.type === 'property') {
return {
type: 'MemberExpression',
object: obj,
property: accessor.value
};
} else {
return {
type: 'IndexExpression',
object: obj,
index: accessor.value
};
}
}, base);
}
Accessor
= "." _ property:Identifier { return { type: 'property', value: property }; }
/ "[" _ indices:IndexList _ "]" { return { type: 'index', value: indices }; }
IndexList
= head:Expression tail:(_ "," _ Expression)* {
return tail.length > 0 ? [head, ...tail.map(([,,,expr]) => expr)] : head;
}
IfStatement
= "if" _ "(" _ test:Expression _ ")" _ consequent:Statement alternate:(_ "else" _ Statement)? {
return {
type: 'IfStatement',
test,
consequent,
alternate: alternate ? alternate[3] : null
};
}
ForStatement
= "for" _ "(" _ init:Expression? _ ";" _ test:Expression? _ ";" _ update:Expression? _ ")" _ body:Statement {
return {
type: 'ForStatement',
init,
test,
update,
body
};
}
WhileStatement
= "while" _ "(" _ test:Expression _ ")" _ body:Statement {
return {
type: 'WhileStatement',
test,
body
};
}
DoWhileStatement
= "do" _ body:Statement _ "while" _ "(" _ test:Expression _ ")" _ ";"? {
return {
type: 'DoWhileStatement',
test,
body
};
}
SwitchStatement
= "switch$" _ "(" _ discriminant:Expression _ ")" _ "{" ws items:((Comment / SwitchCase) ws)* "}" {
return {
type: 'SwitchStatement',
stringMode: true,
discriminant,
cases: items.map(([item]) => item).filter(i => i && i.type === 'SwitchCase')
};
}
/ "switch" _ "(" _ discriminant:Expression _ ")" _ "{" ws items:((Comment / SwitchCase) ws)* "}" {
return {
type: 'SwitchStatement',
stringMode: false,
discriminant,
cases: items.map(([item]) => item).filter(i => i && i.type === 'SwitchCase')
};
}
SwitchCase
= "case" __ tests:CaseTestList _ ":" ws items:((Comment / Statement) ws)* {
return {
type: 'SwitchCase',
test: tests,
consequent: items.map(([item]) => item).filter(Boolean)
};
}
/ "default" _ ":" ws items:((Comment / Statement) ws)* {
return {
type: 'SwitchCase',
test: null,
consequent: items.map(([item]) => item).filter(Boolean)
};
}
CaseTestList
= head:CaseTestExpression tail:(_ "or" __ CaseTestExpression)* {
return tail.length > 0 ? [head, ...tail.map(([,,,expr]) => expr)] : head;
}
CaseTestExpression
= AdditiveExpression
ReturnStatement
= "return" value:(__ Expression)? _ ";" {
return {
type: 'ReturnStatement',
value: value ? value[1] : null
};
}
BreakStatement
= "break" _ ";" {
return { type: 'BreakStatement' };
}
ContinueStatement
= "continue" _ ";" {
return { type: 'ContinueStatement' };
}
ExpressionStatement
= expr:Expression _ ";" {
return {
type: 'ExpressionStatement',
expression: expr
};
}
BlockStatement
= "{" ws items:((Comment / Statement) ws)* "}" {
return {
type: 'BlockStatement',
body: items.map(([item]) => item).filter(Boolean)
};
}
// Expressions (in order of precedence)
Expression
= AssignmentExpression
AssignmentExpression
= target:LeftHandSide _ operator:AssignmentOperator _ value:AssignmentExpression {
return {
type: 'AssignmentExpression',
operator,
target,
value
};
}
/ ConditionalExpression
AssignmentOperator
= "=" / "+=" / "-=" / "*=" / "/=" / "%=" / "<<=" / ">>=" / "&=" / "|=" / "^="
ConditionalExpression
= test:LogicalOrExpression _ "?" _ consequent:Expression _ ":" _ alternate:Expression {
return {
type: 'ConditionalExpression',
test,
consequent,
alternate
};
}
/ LogicalOrExpression
LogicalOrExpression
= head:LogicalAndExpression tail:(_ "||" _ LogicalAndExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
LogicalAndExpression
= head:BitwiseOrExpression tail:(_ "&&" _ BitwiseOrExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
BitwiseOrExpression
= head:BitwiseXorExpression tail:(_ "|" !"|" _ BitwiseXorExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,,right]) => [op, right]));
}
BitwiseXorExpression
= head:BitwiseAndExpression tail:(_ "^" _ BitwiseAndExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
BitwiseAndExpression
= head:EqualityExpression tail:(_ "&" !"&" _ EqualityExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,,right]) => [op, right]));
}
EqualityExpression
= head:RelationalExpression tail:(_ EqualityOperator _ RelationalExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
EqualityOperator
= "==" / "!="
RelationalExpression
= head:StringConcatExpression tail:(_ RelationalOperator _ StringConcatExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
RelationalOperator
= "<=" / ">=" / "<" / ">"
StringConcatExpression
= head:ShiftExpression tail:(_ StringConcatOperator _ StringConcatRightSide)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
// Right side of string concat: allow assignments or normal expressions, but minimize backtracking
StringConcatRightSide
= target:LeftHandSide _ assignOp:AssignmentOperator _ value:AssignmentExpression {
return {
type: 'AssignmentExpression',
operator: assignOp,
target,
value
};
}
/ ShiftExpression
StringConcatOperator
= "$=" / "!$=" / "@" / "NL" / "TAB" / "SPC"
ShiftExpression
= head:AdditiveExpression tail:(_ ShiftOperator _ AdditiveExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
ShiftOperator
= "<<" / ">>"
AdditiveExpression
= head:MultiplicativeExpression tail:(_ ("+" / "-") _ MultiplicativeExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
MultiplicativeExpression
= head:UnaryExpression tail:(_ ("*" / "/" / "%") _ UnaryExpression)* {
return buildBinaryExpression(head, tail.map(([,op,,right]) => [op, right]));
}
UnaryExpression
= operator:("-" / "!" / "~") _ argument:UnaryOperand {
return buildUnaryExpression(operator, argument);
}
/ operator:("++" / "--") _ argument:UnaryOperand {
return buildUnaryExpression(operator, argument);
}
/ "*" _ argument:UnaryOperand {
return {
type: 'TagDereferenceExpression',
argument
};
}
/ PostfixExpression
// Allow assignment expressions as unary operands without parentheses.
// This matches official TorqueScript behavior where `!%x = foo()` parses as `!(%x = foo())`.
// We can't use full Expression here or it would break precedence (e.g., `!a + b` would
// incorrectly parse as `!(a + b)` instead of `(!a) + b`).
UnaryOperand
= target:LeftHandSide _ operator:AssignmentOperator _ value:AssignmentExpression {
return {
type: 'AssignmentExpression',
operator,
target,
value
};
}
/ UnaryExpression
PostfixExpression
= argument:CallExpression _ operator:("++" / "--") {
return {
type: 'PostfixExpression',
operator,
argument
};
}
/ CallExpression
CallExpression
= base:MemberExpression tail:((_ "(" _ ArgumentList? _ ")") / (_ Accessor))* {
return tail.reduce((obj, item) => {
// Check if it's a function call
if (item[1] === '(') {
const [,,,argList] = item;
return buildCallExpression(obj, argList || []);
}
// Otherwise it's an accessor
const accessor = item[1];
if (accessor.type === 'property') {
return {
type: 'MemberExpression',
object: obj,
property: accessor.value
};
} else {
return {
type: 'IndexExpression',
object: obj,
index: accessor.value
};
}
}, base);
}
MemberExpression
= base:PrimaryExpression accessors:(_ Accessor)* {
return accessors.reduce((obj, [, accessor]) => {
if (accessor.type === 'property') {
return {
type: 'MemberExpression',
object: obj,
property: accessor.value
};
} else {
return {
type: 'IndexExpression',
object: obj,
index: accessor.value
};
}
}, base);
}
ArgumentList
= head:Expression tail:(_ "," _ Expression)* {
return [head, ...tail.map(([,,,expr]) => expr)];
}
PrimaryExpression
= ObjectDeclaration
/ DatablockDeclaration
/ StringLiteral
/ NumberLiteral
/ BooleanLiteral
/ Variable
/ ParenthesizedExpression
ParenthesizedExpression
= "(" _ expr:Expression _ ")" { return expr; }
// Variables
Variable
= LocalVariable
/ GlobalVariable
/ PlainIdentifier
LocalVariable
= "%" name:$([a-zA-Z_][a-zA-Z0-9_]*) {
return {
type: 'Variable',
scope: 'local',
name
};
}
GlobalVariable
= "$" name:$("::"? [a-zA-Z_][a-zA-Z0-9_]* ("::" [a-zA-Z_][a-zA-Z0-9_]*)*) {
return {
type: 'Variable',
scope: 'global',
name
};
}
PlainIdentifier
= name:$("parent" [ \t]* "::" [ \t]* [a-zA-Z_][a-zA-Z0-9_]*) {
return {
type: 'Identifier',
name: name.replace(/\s+/g, '')
};
}
/ name:$("parent" ("::" [a-zA-Z_][a-zA-Z0-9_]*)+) {
return {
type: 'Identifier',
name
};
}
/ name:$([a-zA-Z_][a-zA-Z0-9_]* ("::" [a-zA-Z_][a-zA-Z0-9_]*)*) {
return {
type: 'Identifier',
name
};
}
Identifier
= LocalVariable
/ GlobalVariable
/ PlainIdentifier
// Literals
StringLiteral
= '"' chars:DoubleQuotedChar* '"' {
return {
type: 'StringLiteral',
value: chars.join('')
};
}
/ "'" chars:SingleQuotedChar* "'" {
// Single-quoted strings are "tagged" strings in TorqueScript,
// used for network optimization (string sent once, then only tag ID)
return {
type: 'StringLiteral',
value: chars.join(''),
tagged: true
};
}
DoubleQuotedChar
= "\\" char:EscapeSequence { return char; }
/ [^"\\\n\r]
SingleQuotedChar
= "\\" char:EscapeSequence { return char; }
/ [^'\\\n\r]
EscapeSequence
= "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }
/ "x" hex:$([0-9a-fA-F][0-9a-fA-F]) { return String.fromCharCode(parseInt(hex, 16)); }
// TorqueScript color codes - mapped to byte values that skip \t (9), \n (10), \r (13)
// These are processed by the game's text rendering system
/ "cr" { return String.fromCharCode(0x0F); } // reset color
/ "cp" { return String.fromCharCode(0x10); } // push color
/ "co" { return String.fromCharCode(0x11); } // pop color
/ "c" code:$([0-9]) {
// collapseRemap: \c0-\c9 map to bytes that avoid \t, \n, \r
const collapseRemap = [0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0B, 0x0C, 0x0E];
return String.fromCharCode(collapseRemap[parseInt(code, 10)]);
}
/ char:. { return char; }
NumberLiteral
= hex:$("0" [xX] [0-9a-fA-F]+) !IdentifierChar {
return {
type: 'NumberLiteral',
value: parseInt(hex, 16)
};
}
/ number:$(("-"? [0-9]+ ("." [0-9]+)?) / ("-"? "." [0-9]+)) !IdentifierChar {
return {
type: 'NumberLiteral',
value: parseFloat(number)
};
}
BooleanLiteral
= value:("true" / "false") !IdentifierChar {
return {
type: 'BooleanLiteral',
value: value === "true"
};
}
// Comments
Comment
= SingleLineComment
/ MultiLineComment
SingleLineComment
= "//" text:$[^\n\r]* [\n\r]? {
return {
type: 'Comment',
value: text
};
}
MultiLineComment
= "/*" text:$(!"*/" .)* "*/" {
return {
type: 'Comment',
value: text
};
}
// Whitespace
Whitespace
= [ \t\n\r]+ { return null; }
// Optional whitespace and comments (comments are consumed but not captured)
// Use this between tokens and in expressions
_
= ([ \t\n\r] / SkipComment)*
// Required whitespace (at least one whitespace char, may include comments)
// Use this between keywords that must be separated
__
= [ \t\n\r]+ ([ \t\n\r] / SkipComment)*
// Pure whitespace only (no comments) - used in statement lists where comments are captured separately
ws
= [ \t\n\r]*
// Comments consumed silently (no return value) - used in whitespace rules
SkipComment
= "//" [^\n\r]* [\n\r]?
/ "/*" (!"*/" .)* "*/"
IdentifierChar
= [a-zA-Z0-9_]