mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 12:14:47 +00:00
682 lines
17 KiB
JavaScript
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_]
|