mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-02-15 04:33:49 +00:00
add TorqueScript transpiler and runtime
This commit is contained in:
parent
c8391a1056
commit
7d10fb7dee
49 changed files with 12324 additions and 2075 deletions
233
src/torqueScript/README.md
Normal file
233
src/torqueScript/README.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# TorqueScript Transpiler
|
||||
|
||||
Transpiles TorqueScript (`.cs`/`.mis` files) to JavaScript. Includes a runtime
|
||||
that implements TorqueScript semantics and built-ins.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { parse, transpile } from "./index";
|
||||
import { createRuntime } from "./runtime";
|
||||
|
||||
// Parse and transpile
|
||||
const { code, ast } = transpile(source);
|
||||
|
||||
// Create runtime and execute
|
||||
const runtime = createRuntime();
|
||||
const script = await runtime.loadFromSource(source);
|
||||
script.execute();
|
||||
|
||||
// Access results
|
||||
runtime.$g.get("myGlobal"); // Get global variable
|
||||
runtime.$.call(obj, "method"); // Call method on object
|
||||
```
|
||||
|
||||
## Why Transpile to JavaScript?
|
||||
|
||||
- No TypeScript compiler needed at runtime
|
||||
- Can dynamically transpile and execute in the browser
|
||||
- The transpiler and runtime are written in TypeScript, but output is plain JS
|
||||
|
||||
## Key Differences from JavaScript
|
||||
|
||||
TorqueScript has semantics that don't map cleanly to JavaScript. The transpiler
|
||||
and runtime handle these differences.
|
||||
|
||||
### Case Insensitivity
|
||||
|
||||
All identifiers are case-insensitive: functions, methods, variables, object
|
||||
names, and properties. The runtime uses `CaseInsensitiveMap` for lookups.
|
||||
|
||||
### Namespaces, Not Classes
|
||||
|
||||
TorqueScript has no `class` keyword. The `::` in `function Player::onKill` is
|
||||
a naming convention that registers a function in a **namespace**—it doesn't
|
||||
define or reference a class.
|
||||
|
||||
```torquescript
|
||||
function Item::onPickup(%this) {
|
||||
echo("Picked up: " @ %this.getName());
|
||||
}
|
||||
```
|
||||
|
||||
This registers a function named `onPickup` in the `Item` namespace. You can
|
||||
define functions in any namespace—`Item`, `MyGame`, `Util`—whether or not
|
||||
objects of that "type" exist.
|
||||
|
||||
When you call a method on an object (`%obj.onPickup()`), the engine searches
|
||||
for a matching function through a **namespace chain**. Every object has an
|
||||
associated namespace (typically its C++ class name like `Item` or `Player`),
|
||||
and namespaces are chained to parent namespaces. The engine walks up this
|
||||
chain until it finds a function or fails.
|
||||
|
||||
```torquescript
|
||||
new Item(HealthPack) { };
|
||||
HealthPack.onPickup(); // Searches: Item -> SimObject -> ...
|
||||
```
|
||||
|
||||
The `%this` parameter receives the object handle automatically—it's not magic
|
||||
OOP binding, just a calling convention.
|
||||
|
||||
### `Parent::` is Package-Based
|
||||
|
||||
`Parent::method()` does NOT call a superclass. It calls the previous definition
|
||||
of the same function before the current package was activated. Packages are
|
||||
layers that override functions:
|
||||
|
||||
```torquescript
|
||||
function DefaultGame::onKill(%game, %client) {
|
||||
// base behavior
|
||||
}
|
||||
|
||||
package CTFGame {
|
||||
function DefaultGame::onKill(%game, %client) {
|
||||
Parent::onKill(%game, %client); // calls the base version
|
||||
// CTF-specific behavior
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The runtime maintains a stack of function definitions per name. `Parent::` calls
|
||||
the previous entry in that stack.
|
||||
|
||||
### Numeric Coercion
|
||||
|
||||
All arithmetic and comparison operators coerce operands to numbers. Empty
|
||||
strings and undefined variables become `0`. This differs from JavaScript's
|
||||
behavior:
|
||||
|
||||
```torquescript
|
||||
$x = "5" + "3"; // 8, not "53"
|
||||
$y = $undefined + 1; // 1, not NaN
|
||||
```
|
||||
|
||||
### Integer vs Float Operations
|
||||
|
||||
TorqueScript uses different numeric types internally:
|
||||
|
||||
| Operator | Type | JavaScript Equivalent |
|
||||
| ---------------------- | --------------- | ---------------------- |
|
||||
| `+` `-` `*` `/` | 64-bit float | Direct (with coercion) |
|
||||
| `%` | 32-bit signed | `$.mod(a, b)` |
|
||||
| `&` `\|` `^` `<<` `>>` | 32-bit unsigned | `$.bitand()` etc. |
|
||||
|
||||
Division by zero returns `0` (not `Infinity`).
|
||||
|
||||
### String Operators
|
||||
|
||||
| TorqueScript | JavaScript |
|
||||
| ------------ | ---------------------------------- |
|
||||
| `%a @ %b` | `$.concat(a, b)` |
|
||||
| `%a SPC %b` | `$.concat(a, " ", b)` |
|
||||
| `%a TAB %b` | `$.concat(a, "\t", b)` |
|
||||
| `%a $= %b` | `$.streq(a, b)` (case-insensitive) |
|
||||
|
||||
### Array Variables
|
||||
|
||||
TorqueScript "arrays" are string-keyed, implemented via variable name
|
||||
concatenation:
|
||||
|
||||
```torquescript
|
||||
$items[0] = "first"; // Sets $items0
|
||||
$items["key"] = "named"; // Sets $itemskey
|
||||
$arr[%i, %j] = %val; // Sets $arr{i}_{j}
|
||||
```
|
||||
|
||||
### Switch Statements
|
||||
|
||||
TorqueScript `switch` has implicit break (no fallthrough). `switch$` does
|
||||
case-insensitive string matching. The `or` keyword combines cases:
|
||||
|
||||
```torquescript
|
||||
switch (%x) {
|
||||
case 1 or 2 or 3:
|
||||
doSomething(); // No break needed
|
||||
default:
|
||||
doOther();
|
||||
}
|
||||
```
|
||||
|
||||
## Generated Code Structure
|
||||
|
||||
The transpiler emits JavaScript that calls into a runtime API:
|
||||
|
||||
```javascript
|
||||
// Function registration
|
||||
$.registerFunction("myFunc", function() { ... });
|
||||
$.registerMethod("Player", "onKill", function() { ... });
|
||||
|
||||
// Variable access via stores
|
||||
const $l = $.locals(); // Per-function local store
|
||||
$l.set("x", value); // Set local
|
||||
$l.get("x"); // Get local
|
||||
$g.set("GlobalVar", value); // Set global
|
||||
$g.get("GlobalVar"); // Get global
|
||||
|
||||
// Object/method operations
|
||||
$.create("SimGroup", "MissionGroup", { ... });
|
||||
$.call(obj, "method", arg1, arg2);
|
||||
$.parent("CurrentClass", "method", thisObj, ...args);
|
||||
|
||||
// Operators with proper coercion
|
||||
$.add(a, b); $.sub(a, b); $.mul(a, b); $.div(a, b);
|
||||
$.mod(a, b); $.bitand(a, b); $.shl(a, b); // etc.
|
||||
```
|
||||
|
||||
## Runtime API
|
||||
|
||||
The runtime exposes three main objects:
|
||||
|
||||
- **`$`** (`RuntimeAPI`): Object/method system, operators, property access
|
||||
- **`$f`** (`FunctionsAPI`): Call standalone functions by name
|
||||
- **`$g`** (`GlobalsAPI`): Global variable storage
|
||||
|
||||
Key methods on `$`:
|
||||
|
||||
```typescript
|
||||
// Registration
|
||||
registerMethod(className, methodName, fn)
|
||||
registerFunction(name, fn)
|
||||
package(name, bodyFn)
|
||||
|
||||
// Object creation
|
||||
create(className, instanceName, props, children?)
|
||||
datablock(className, instanceName, parentName, props)
|
||||
deleteObject(obj)
|
||||
|
||||
// Property access (case-insensitive)
|
||||
prop(obj, name)
|
||||
setProp(obj, name, value)
|
||||
|
||||
// Method dispatch
|
||||
call(obj, methodName, ...args)
|
||||
nsCall(namespace, method, ...args)
|
||||
parent(currentClass, methodName, thisObj, ...args)
|
||||
```
|
||||
|
||||
## Script Loading
|
||||
|
||||
The runtime supports `exec()` for loading dependent scripts:
|
||||
|
||||
```typescript
|
||||
const runtime = createRuntime({
|
||||
loadScript: async (path) => {
|
||||
// Return script source or null if not found
|
||||
return await fetch(path).then((r) => r.text());
|
||||
},
|
||||
});
|
||||
|
||||
// Dependencies are resolved before execution
|
||||
const script = await runtime.loadFromPath("scripts/main.cs");
|
||||
script.execute();
|
||||
```
|
||||
|
||||
- Scripts are executed once; subsequent `exec()` calls are no-ops
|
||||
- Circular dependencies are handled (each script runs once)
|
||||
- Paths are normalized (backslashes → forward slashes, lowercased)
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
The runtime implements common TorqueScript built-ins like `echo`, `exec`,
|
||||
`schedule`, `activatePackage`, string functions (`getWord`, `strLen`, etc.),
|
||||
math functions (`mFloor`, `mSin`, etc.), and vector math (`vectorAdd`,
|
||||
`vectorDist`, etc.). See `createBuiltins()` in `runtime.ts` for the full list.
|
||||
250
src/torqueScript/ast.ts
Normal file
250
src/torqueScript/ast.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
export interface BaseNode {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Program extends BaseNode {
|
||||
type: "Program";
|
||||
body: Statement[];
|
||||
comments?: Comment[];
|
||||
execScriptPaths: string[];
|
||||
hasDynamicExec: boolean;
|
||||
}
|
||||
|
||||
export type Statement =
|
||||
| ExpressionStatement
|
||||
| FunctionDeclaration
|
||||
| PackageDeclaration
|
||||
| DatablockDeclaration
|
||||
| ObjectDeclaration
|
||||
| IfStatement
|
||||
| ForStatement
|
||||
| WhileStatement
|
||||
| DoWhileStatement
|
||||
| SwitchStatement
|
||||
| ReturnStatement
|
||||
| BreakStatement
|
||||
| ContinueStatement
|
||||
| BlockStatement;
|
||||
|
||||
export interface ExpressionStatement extends BaseNode {
|
||||
type: "ExpressionStatement";
|
||||
expression: Expression;
|
||||
}
|
||||
|
||||
export interface FunctionDeclaration extends BaseNode {
|
||||
type: "FunctionDeclaration";
|
||||
name: Identifier;
|
||||
params: Variable[];
|
||||
body: BlockStatement;
|
||||
}
|
||||
|
||||
export interface PackageDeclaration extends BaseNode {
|
||||
type: "PackageDeclaration";
|
||||
name: Identifier;
|
||||
body: Statement[];
|
||||
comments?: Comment[];
|
||||
}
|
||||
|
||||
export interface DatablockDeclaration extends BaseNode {
|
||||
type: "DatablockDeclaration";
|
||||
className: Identifier;
|
||||
instanceName: Identifier | null;
|
||||
parent: Identifier | null;
|
||||
body: ObjectBodyItem[];
|
||||
}
|
||||
|
||||
export interface ObjectDeclaration extends BaseNode {
|
||||
type: "ObjectDeclaration";
|
||||
className: Identifier | Expression;
|
||||
instanceName: Identifier | Expression | null;
|
||||
body: ObjectBodyItem[];
|
||||
}
|
||||
|
||||
export type ObjectBodyItem = Assignment | ObjectDeclaration;
|
||||
|
||||
export interface Assignment extends BaseNode {
|
||||
type: "Assignment";
|
||||
target: Identifier | IndexExpression;
|
||||
value: Expression;
|
||||
}
|
||||
|
||||
export interface IfStatement extends BaseNode {
|
||||
type: "IfStatement";
|
||||
test: Expression;
|
||||
consequent: Statement;
|
||||
alternate: Statement | null;
|
||||
}
|
||||
|
||||
export interface ForStatement extends BaseNode {
|
||||
type: "ForStatement";
|
||||
init: Expression | null;
|
||||
test: Expression | null;
|
||||
update: Expression | null;
|
||||
body: Statement;
|
||||
}
|
||||
|
||||
export interface WhileStatement extends BaseNode {
|
||||
type: "WhileStatement";
|
||||
test: Expression;
|
||||
body: Statement;
|
||||
}
|
||||
|
||||
export interface DoWhileStatement extends BaseNode {
|
||||
type: "DoWhileStatement";
|
||||
test: Expression;
|
||||
body: Statement;
|
||||
}
|
||||
|
||||
export interface SwitchStatement extends BaseNode {
|
||||
type: "SwitchStatement";
|
||||
stringMode: boolean;
|
||||
discriminant: Expression;
|
||||
cases: SwitchCase[];
|
||||
}
|
||||
|
||||
export interface SwitchCase extends BaseNode {
|
||||
type: "SwitchCase";
|
||||
test: Expression | Expression[] | null; // null = default, array = "or" syntax
|
||||
consequent: Statement[];
|
||||
}
|
||||
|
||||
export interface ReturnStatement extends BaseNode {
|
||||
type: "ReturnStatement";
|
||||
value: Expression | null;
|
||||
}
|
||||
|
||||
export interface BreakStatement extends BaseNode {
|
||||
type: "BreakStatement";
|
||||
}
|
||||
|
||||
export interface ContinueStatement extends BaseNode {
|
||||
type: "ContinueStatement";
|
||||
}
|
||||
|
||||
export interface BlockStatement extends BaseNode {
|
||||
type: "BlockStatement";
|
||||
body: Statement[];
|
||||
comments?: Comment[];
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| Identifier
|
||||
| Variable
|
||||
| NumberLiteral
|
||||
| StringLiteral
|
||||
| BooleanLiteral
|
||||
| BinaryExpression
|
||||
| UnaryExpression
|
||||
| PostfixExpression
|
||||
| AssignmentExpression
|
||||
| ConditionalExpression
|
||||
| CallExpression
|
||||
| MemberExpression
|
||||
| IndexExpression
|
||||
| TagDereferenceExpression
|
||||
| ObjectDeclaration
|
||||
| DatablockDeclaration;
|
||||
|
||||
export interface Identifier extends BaseNode {
|
||||
type: "Identifier";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Variable extends BaseNode {
|
||||
type: "Variable";
|
||||
scope: "local" | "global";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface NumberLiteral extends BaseNode {
|
||||
type: "NumberLiteral";
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface StringLiteral extends BaseNode {
|
||||
type: "StringLiteral";
|
||||
value: string;
|
||||
tagged?: boolean;
|
||||
}
|
||||
|
||||
export interface BooleanLiteral extends BaseNode {
|
||||
type: "BooleanLiteral";
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export interface BinaryExpression extends BaseNode {
|
||||
type: "BinaryExpression";
|
||||
operator: string;
|
||||
left: Expression;
|
||||
right: Expression;
|
||||
}
|
||||
|
||||
export interface UnaryExpression extends BaseNode {
|
||||
type: "UnaryExpression";
|
||||
operator: string;
|
||||
argument: Expression;
|
||||
}
|
||||
|
||||
export interface PostfixExpression extends BaseNode {
|
||||
type: "PostfixExpression";
|
||||
operator: string;
|
||||
argument: Expression;
|
||||
}
|
||||
|
||||
export interface AssignmentExpression extends BaseNode {
|
||||
type: "AssignmentExpression";
|
||||
operator: string;
|
||||
target: Expression;
|
||||
value: Expression;
|
||||
}
|
||||
|
||||
export interface ConditionalExpression extends BaseNode {
|
||||
type: "ConditionalExpression";
|
||||
test: Expression;
|
||||
consequent: Expression;
|
||||
alternate: Expression;
|
||||
}
|
||||
|
||||
export interface CallExpression extends BaseNode {
|
||||
type: "CallExpression";
|
||||
callee: Expression;
|
||||
arguments: Expression[];
|
||||
}
|
||||
|
||||
export interface MemberExpression extends BaseNode {
|
||||
type: "MemberExpression";
|
||||
object: Expression;
|
||||
property: Identifier | Expression;
|
||||
computed?: boolean;
|
||||
}
|
||||
|
||||
export interface IndexExpression extends BaseNode {
|
||||
type: "IndexExpression";
|
||||
object: Expression;
|
||||
index: Expression | Expression[]; // Single or multi-index access: $arr[i] or $arr[i, j]
|
||||
}
|
||||
|
||||
export interface TagDereferenceExpression extends BaseNode {
|
||||
type: "TagDereferenceExpression";
|
||||
argument: Expression;
|
||||
}
|
||||
|
||||
export interface Comment extends BaseNode {
|
||||
type: "Comment";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function isMethodName(name: Identifier): boolean {
|
||||
return name.name.includes("::");
|
||||
}
|
||||
|
||||
export function parseMethodName(
|
||||
name: string,
|
||||
): { namespace: string; method: string } | null {
|
||||
const idx = name.indexOf("::");
|
||||
if (idx === -1) return null;
|
||||
return {
|
||||
namespace: name.slice(0, idx),
|
||||
method: name.slice(idx + 2),
|
||||
};
|
||||
}
|
||||
690
src/torqueScript/builtins.ts
Normal file
690
src/torqueScript/builtins.ts
Normal file
|
|
@ -0,0 +1,690 @@
|
|||
import type { BuiltinsContext, TorqueFunction } from "./types";
|
||||
import { normalizePath } from "./utils";
|
||||
|
||||
function parseVector(v: any): [number, number, number] {
|
||||
const parts = String(v ?? "0 0 0")
|
||||
.split(" ")
|
||||
.map(Number);
|
||||
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
||||
}
|
||||
|
||||
// TorqueScript unit delimiters (from SDK source):
|
||||
// - Words: space, tab, newline (" \t\n")
|
||||
// - Fields: tab, newline ("\t\n")
|
||||
// - Records: newline ("\n")
|
||||
const FIELD_DELIM = /[\t\n]/;
|
||||
const FIELD_DELIM_CHAR = "\t"; // Use tab when joining
|
||||
|
||||
/**
|
||||
* Default TorqueScript built-in functions.
|
||||
*
|
||||
* Names are lowercased to optimize lookup, since TorqueScript is case-insensitive.
|
||||
*/
|
||||
export function createBuiltins(
|
||||
ctx: BuiltinsContext,
|
||||
): Record<string, TorqueFunction> {
|
||||
const { runtime } = ctx;
|
||||
return {
|
||||
// Console
|
||||
echo(...args: any[]): void {
|
||||
console.log(...args.map((a) => String(a ?? "")));
|
||||
},
|
||||
warn(...args: any[]): void {
|
||||
console.warn(...args.map((a) => String(a ?? "")));
|
||||
},
|
||||
error(...args: any[]): void {
|
||||
console.error(...args.map((a) => String(a ?? "")));
|
||||
},
|
||||
call(funcName: any, ...args: any[]): any {
|
||||
return runtime().$f.call(String(funcName ?? ""), ...args);
|
||||
},
|
||||
eval(_code: any): any {
|
||||
throw new Error(
|
||||
"eval() not implemented: requires runtime parsing and execution",
|
||||
);
|
||||
},
|
||||
collapseescape(str: any): string {
|
||||
// Single-pass replacement to correctly handle sequences like \\n
|
||||
return String(str ?? "").replace(/\\([ntr\\])/g, (_, char) => {
|
||||
if (char === "n") return "\n";
|
||||
if (char === "t") return "\t";
|
||||
if (char === "r") return "\r";
|
||||
return "\\";
|
||||
});
|
||||
},
|
||||
expandescape(str: any): string {
|
||||
return String(str ?? "")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\n/g, "\\n")
|
||||
.replace(/\t/g, "\\t")
|
||||
.replace(/\r/g, "\\r");
|
||||
},
|
||||
export(pattern: any, filename?: any, append?: any): void {
|
||||
console.warn(`export(${pattern}): not implemented`);
|
||||
},
|
||||
quit(): void {
|
||||
console.warn("quit(): not implemented in browser");
|
||||
},
|
||||
trace(_enable: any): void {
|
||||
// Enable/disable function call tracing
|
||||
},
|
||||
|
||||
// Type checking
|
||||
isobject(obj: any): boolean {
|
||||
return runtime().$.isObject(obj);
|
||||
},
|
||||
typeof(obj: any): string {
|
||||
if (obj == null) return "";
|
||||
if (typeof obj === "object" && obj._class) return obj._className;
|
||||
return typeof obj;
|
||||
},
|
||||
|
||||
// Object lookup
|
||||
nametoid(name: string): number {
|
||||
return runtime().$.nameToId(name);
|
||||
},
|
||||
isfunction(name: string): boolean {
|
||||
return runtime().$.isFunction(name);
|
||||
},
|
||||
|
||||
// String functions
|
||||
strlen(str: any): number {
|
||||
return String(str ?? "").length;
|
||||
},
|
||||
strchr(str: any, char: any): string {
|
||||
// Returns remainder of string starting at first occurrence of char, or ""
|
||||
const s = String(str ?? "");
|
||||
const c = String(char ?? "")[0] ?? "";
|
||||
const idx = s.indexOf(c);
|
||||
return idx >= 0 ? s.substring(idx) : "";
|
||||
},
|
||||
strpos(haystack: any, needle: any, offset?: any): number {
|
||||
const s = String(haystack ?? "");
|
||||
const n = String(needle ?? "");
|
||||
const o = Number(offset) || 0;
|
||||
return s.indexOf(n, o);
|
||||
},
|
||||
strcmp(a: any, b: any): number {
|
||||
const sa = String(a ?? "");
|
||||
const sb = String(b ?? "");
|
||||
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
||||
},
|
||||
stricmp(a: any, b: any): number {
|
||||
const sa = String(a ?? "").toLowerCase();
|
||||
const sb = String(b ?? "").toLowerCase();
|
||||
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
||||
},
|
||||
strstr(haystack: any, needle: any): number {
|
||||
return String(haystack ?? "").indexOf(String(needle ?? ""));
|
||||
},
|
||||
getsubstr(str: any, start: any, len?: any): string {
|
||||
const s = String(str ?? "");
|
||||
const st = Number(start) || 0;
|
||||
if (len === undefined) return s.substring(st);
|
||||
return s.substring(st, st + (Number(len) || 0));
|
||||
},
|
||||
getword(str: any, index: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
const i = Number(index) || 0;
|
||||
return words[i] ?? "";
|
||||
},
|
||||
getwordcount(str: any): number {
|
||||
const s = String(str ?? "").trim();
|
||||
if (s === "") return 0;
|
||||
return s.split(/\s+/).length;
|
||||
},
|
||||
getfield(str: any, index: any): string {
|
||||
const fields = String(str ?? "").split(FIELD_DELIM);
|
||||
const i = Number(index) || 0;
|
||||
return fields[i] ?? "";
|
||||
},
|
||||
getfieldcount(str: any): number {
|
||||
const s = String(str ?? "");
|
||||
if (s === "") return 0;
|
||||
return s.split(FIELD_DELIM).length;
|
||||
},
|
||||
setword(str: any, index: any, value: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
const i = Number(index) || 0;
|
||||
words[i] = String(value ?? "");
|
||||
return words.join(" ");
|
||||
},
|
||||
setfield(str: any, index: any, value: any): string {
|
||||
const fields = String(str ?? "").split(FIELD_DELIM);
|
||||
const i = Number(index) || 0;
|
||||
fields[i] = String(value ?? "");
|
||||
return fields.join(FIELD_DELIM_CHAR);
|
||||
},
|
||||
firstword(str: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
return words[0] ?? "";
|
||||
},
|
||||
restwords(str: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
return words.slice(1).join(" ");
|
||||
},
|
||||
trim(str: any): string {
|
||||
return String(str ?? "").trim();
|
||||
},
|
||||
ltrim(str: any): string {
|
||||
return String(str ?? "").replace(/^\s+/, "");
|
||||
},
|
||||
rtrim(str: any): string {
|
||||
return String(str ?? "").replace(/\s+$/, "");
|
||||
},
|
||||
strupr(str: any): string {
|
||||
return String(str ?? "").toUpperCase();
|
||||
},
|
||||
strlwr(str: any): string {
|
||||
return String(str ?? "").toLowerCase();
|
||||
},
|
||||
strreplace(str: any, from: any, to: any): string {
|
||||
return String(str ?? "")
|
||||
.split(String(from ?? ""))
|
||||
.join(String(to ?? ""));
|
||||
},
|
||||
filterstring(str: any, _replacementChars?: any): string {
|
||||
// Filters profanity/bad words from the string (requires bad word dictionary)
|
||||
// Since we don't have a bad word filter, just return the string unchanged
|
||||
return String(str ?? "");
|
||||
},
|
||||
stripchars(str: any, chars: any): string {
|
||||
// Removes all characters in `chars` from the string
|
||||
const s = String(str ?? "");
|
||||
const toRemove = new Set(String(chars ?? "").split(""));
|
||||
return s
|
||||
.split("")
|
||||
.filter((c) => !toRemove.has(c))
|
||||
.join("");
|
||||
},
|
||||
getfields(str: any, start: any, end?: any): string {
|
||||
const fields = String(str ?? "").split(FIELD_DELIM);
|
||||
const s = Number(start) || 0;
|
||||
const e = end !== undefined ? Number(end) + 1 : 1000000;
|
||||
return fields.slice(s, e).join(FIELD_DELIM_CHAR);
|
||||
},
|
||||
getwords(str: any, start: any, end?: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
const s = Number(start) || 0;
|
||||
const e = end !== undefined ? Number(end) + 1 : 1000000;
|
||||
return words.slice(s, e).join(" ");
|
||||
},
|
||||
removeword(str: any, index: any): string {
|
||||
const words = String(str ?? "").split(/\s+/);
|
||||
const i = Number(index) || 0;
|
||||
words.splice(i, 1);
|
||||
return words.join(" ");
|
||||
},
|
||||
removefield(str: any, index: any): string {
|
||||
const fields = String(str ?? "").split(FIELD_DELIM);
|
||||
const i = Number(index) || 0;
|
||||
fields.splice(i, 1);
|
||||
return fields.join(FIELD_DELIM_CHAR);
|
||||
},
|
||||
getrecord(str: any, index: any): string {
|
||||
const records = String(str ?? "").split("\n");
|
||||
const i = Number(index) || 0;
|
||||
return records[i] ?? "";
|
||||
},
|
||||
getrecordcount(str: any): number {
|
||||
const s = String(str ?? "");
|
||||
if (s === "") return 0;
|
||||
return s.split("\n").length;
|
||||
},
|
||||
setrecord(str: any, index: any, value: any): string {
|
||||
const records = String(str ?? "").split("\n");
|
||||
const i = Number(index) || 0;
|
||||
records[i] = String(value ?? "");
|
||||
return records.join("\n");
|
||||
},
|
||||
removerecord(str: any, index: any): string {
|
||||
const records = String(str ?? "").split("\n");
|
||||
const i = Number(index) || 0;
|
||||
records.splice(i, 1);
|
||||
return records.join("\n");
|
||||
},
|
||||
nexttoken(_str: any, _tokenVar: any, _delim: any): string {
|
||||
// nextToken modifies a variable to store the remainder of the string,
|
||||
// which cannot be implemented correctly from a builtin function.
|
||||
throw new Error(
|
||||
"nextToken() is not implemented: it requires variable mutation",
|
||||
);
|
||||
},
|
||||
strtoplayername(str: any): string {
|
||||
// Sanitizes a string to be a valid player name
|
||||
return String(str ?? "")
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.trim();
|
||||
},
|
||||
|
||||
// Math functions
|
||||
mabs(n: any): number {
|
||||
return Math.abs(Number(n) || 0);
|
||||
},
|
||||
mfloor(n: any): number {
|
||||
return Math.floor(Number(n) || 0);
|
||||
},
|
||||
mceil(n: any): number {
|
||||
return Math.ceil(Number(n) || 0);
|
||||
},
|
||||
msqrt(n: any): number {
|
||||
return Math.sqrt(Number(n) || 0);
|
||||
},
|
||||
mpow(base: any, exp: any): number {
|
||||
return Math.pow(Number(base) || 0, Number(exp) || 0);
|
||||
},
|
||||
msin(n: any): number {
|
||||
return Math.sin(Number(n) || 0);
|
||||
},
|
||||
mcos(n: any): number {
|
||||
return Math.cos(Number(n) || 0);
|
||||
},
|
||||
mtan(n: any): number {
|
||||
return Math.tan(Number(n) || 0);
|
||||
},
|
||||
masin(n: any): number {
|
||||
return Math.asin(Number(n) || 0);
|
||||
},
|
||||
macos(n: any): number {
|
||||
return Math.acos(Number(n) || 0);
|
||||
},
|
||||
matan(rise: any, run: any): number {
|
||||
// SDK: mAtan(rise, run) - always requires 2 args, returns atan2
|
||||
return Math.atan2(Number(rise) || 0, Number(run) || 0);
|
||||
},
|
||||
mlog(n: any): number {
|
||||
return Math.log(Number(n) || 0);
|
||||
},
|
||||
getrandom(a?: any, b?: any): number {
|
||||
// SDK behavior:
|
||||
// - 0 args: returns float 0-1
|
||||
// - 1 arg: returns int 0 to a
|
||||
// - 2 args: returns int a to b
|
||||
if (a === undefined) {
|
||||
return Math.random();
|
||||
}
|
||||
if (b === undefined) {
|
||||
return Math.floor(Math.random() * (Number(a) + 1));
|
||||
}
|
||||
const min = Number(a) || 0;
|
||||
const max = Number(b) || 0;
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
getrandomseed(): number {
|
||||
throw new Error("getRandomSeed() not implemented");
|
||||
},
|
||||
setrandomseed(_seed: any): void {
|
||||
throw new Error("setRandomSeed() not implemented");
|
||||
},
|
||||
mdegtorad(deg: any): number {
|
||||
return (Number(deg) || 0) * (Math.PI / 180);
|
||||
},
|
||||
mradtodeg(rad: any): number {
|
||||
return (Number(rad) || 0) * (180 / Math.PI);
|
||||
},
|
||||
mfloatlength(n: any, precision: any): string {
|
||||
return (Number(n) || 0).toFixed(Number(precision) || 0);
|
||||
},
|
||||
getboxcenter(box: any): string {
|
||||
// Box format: "minX minY minZ maxX maxY maxZ"
|
||||
const parts = String(box ?? "")
|
||||
.split(" ")
|
||||
.map(Number);
|
||||
const minX = parts[0] || 0;
|
||||
const minY = parts[1] || 0;
|
||||
const minZ = parts[2] || 0;
|
||||
const maxX = parts[3] || 0;
|
||||
const maxY = parts[4] || 0;
|
||||
const maxZ = parts[5] || 0;
|
||||
return `${(minX + maxX) / 2} ${(minY + maxY) / 2} ${(minZ + maxZ) / 2}`;
|
||||
},
|
||||
|
||||
// Vector math (3-component vectors as space-separated strings)
|
||||
vectoradd(a: any, b: any): string {
|
||||
const [ax, ay, az] = parseVector(a);
|
||||
const [bx, by, bz] = parseVector(b);
|
||||
return `${ax + bx} ${ay + by} ${az + bz}`;
|
||||
},
|
||||
vectorsub(a: any, b: any): string {
|
||||
const [ax, ay, az] = parseVector(a);
|
||||
const [bx, by, bz] = parseVector(b);
|
||||
return `${ax - bx} ${ay - by} ${az - bz}`;
|
||||
},
|
||||
vectorscale(v: any, s: any): string {
|
||||
const [x, y, z] = parseVector(v);
|
||||
const scale = Number(s) || 0;
|
||||
return `${x * scale} ${y * scale} ${z * scale}`;
|
||||
},
|
||||
vectordot(a: any, b: any): number {
|
||||
const [ax, ay, az] = parseVector(a);
|
||||
const [bx, by, bz] = parseVector(b);
|
||||
return ax * bx + ay * by + az * bz;
|
||||
},
|
||||
vectorcross(a: any, b: any): string {
|
||||
const [ax, ay, az] = parseVector(a);
|
||||
const [bx, by, bz] = parseVector(b);
|
||||
return `${ay * bz - az * by} ${az * bx - ax * bz} ${ax * by - ay * bx}`;
|
||||
},
|
||||
vectorlen(v: any): number {
|
||||
const [x, y, z] = parseVector(v);
|
||||
return Math.sqrt(x * x + y * y + z * z);
|
||||
},
|
||||
vectornormalize(v: any): string {
|
||||
const [x, y, z] = parseVector(v);
|
||||
const len = Math.sqrt(x * x + y * y + z * z);
|
||||
if (len === 0) return "0 0 0";
|
||||
return `${x / len} ${y / len} ${z / len}`;
|
||||
},
|
||||
vectordist(a: any, b: any): number {
|
||||
const [ax, ay, az] = parseVector(a);
|
||||
const [bx, by, bz] = parseVector(b);
|
||||
const dx = ax - bx;
|
||||
const dy = ay - by;
|
||||
const dz = az - bz;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
},
|
||||
|
||||
// Matrix math - these require full 3D matrix operations with axis-angle/quaternion
|
||||
// conversions that we haven't implemented
|
||||
matrixcreate(_pos: any, _rot: any): string {
|
||||
throw new Error(
|
||||
"MatrixCreate() not implemented: requires axis-angle rotation math",
|
||||
);
|
||||
},
|
||||
matrixcreatefromeuler(_euler: any): string {
|
||||
throw new Error(
|
||||
"MatrixCreateFromEuler() not implemented: requires Euler→Quaternion→AxisAngle conversion",
|
||||
);
|
||||
},
|
||||
matrixmultiply(_a: any, _b: any): string {
|
||||
throw new Error(
|
||||
"MatrixMultiply() not implemented: requires full 4x4 matrix multiplication",
|
||||
);
|
||||
},
|
||||
matrixmulpoint(_mat: any, _point: any): string {
|
||||
throw new Error(
|
||||
"MatrixMulPoint() not implemented: requires full transform application",
|
||||
);
|
||||
},
|
||||
matrixmulvector(_mat: any, _vec: any): string {
|
||||
throw new Error(
|
||||
"MatrixMulVector() not implemented: requires rotation matrix application",
|
||||
);
|
||||
},
|
||||
|
||||
// Simulation
|
||||
getsimtime(): number {
|
||||
return Date.now() - runtime().state.startTime;
|
||||
},
|
||||
getrealtime(): number {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
// Schedule
|
||||
schedule(
|
||||
delay: any,
|
||||
_obj: any,
|
||||
func: any,
|
||||
...args: any[]
|
||||
): ReturnType<typeof setTimeout> {
|
||||
const ms = Number(delay) || 0;
|
||||
const rt = runtime();
|
||||
const timeoutId = setTimeout(() => {
|
||||
rt.state.pendingTimeouts.delete(timeoutId);
|
||||
rt.$f.call(String(func), ...args);
|
||||
}, ms);
|
||||
rt.state.pendingTimeouts.add(timeoutId);
|
||||
return timeoutId;
|
||||
},
|
||||
cancel(id: any): void {
|
||||
clearTimeout(id);
|
||||
runtime().state.pendingTimeouts.delete(id);
|
||||
},
|
||||
iseventpending(id: any): boolean {
|
||||
return runtime().state.pendingTimeouts.has(id);
|
||||
},
|
||||
|
||||
// Script loading
|
||||
exec(path: any): boolean {
|
||||
const pathString = String(path ?? "");
|
||||
console.debug(
|
||||
`exec(${JSON.stringify(pathString)}): preparing to execute…`,
|
||||
);
|
||||
const normalizedPath = normalizePath(pathString);
|
||||
const rt = runtime();
|
||||
const { executedScripts, scripts } = rt.state;
|
||||
|
||||
// Check if already executed
|
||||
if (executedScripts.has(normalizedPath)) {
|
||||
console.debug(
|
||||
`exec(${JSON.stringify(pathString)}): skipping (already executed)`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the pre-parsed AST from the scripts map
|
||||
const ast = scripts.get(normalizedPath);
|
||||
if (ast == null) {
|
||||
console.warn(`exec(${JSON.stringify(pathString)}): script not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as executed before running (handles circular deps)
|
||||
executedScripts.add(normalizedPath);
|
||||
|
||||
console.debug(`exec(${JSON.stringify(pathString)}): executing!`);
|
||||
rt.executeAST(ast);
|
||||
return true;
|
||||
},
|
||||
compile(_path: any): boolean {
|
||||
throw new Error(
|
||||
"compile() not implemented: requires DSO bytecode compiler",
|
||||
);
|
||||
},
|
||||
|
||||
// Misc
|
||||
isdemo(): boolean {
|
||||
// FIXME: Unsure if this is referring to demo (.rec) playback, or a demo
|
||||
// version of the game.
|
||||
return false;
|
||||
},
|
||||
|
||||
// Files
|
||||
isfile(_path: any): boolean {
|
||||
throw new Error("isFile() not implemented: requires filesystem access");
|
||||
},
|
||||
fileext(path: any): string {
|
||||
const s = String(path ?? "");
|
||||
const dot = s.lastIndexOf(".");
|
||||
return dot >= 0 ? s.substring(dot) : "";
|
||||
},
|
||||
filebase(path: any): string {
|
||||
const s = String(path ?? "");
|
||||
const slash = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
||||
const dot = s.lastIndexOf(".");
|
||||
const start = slash >= 0 ? slash + 1 : 0;
|
||||
const end = dot > start ? dot : s.length;
|
||||
return s.substring(start, end);
|
||||
},
|
||||
filepath(path: any): string {
|
||||
const s = String(path ?? "");
|
||||
const slash = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
||||
return slash >= 0 ? s.substring(0, slash) : "";
|
||||
},
|
||||
expandfilename(_path: any): string {
|
||||
throw new Error(
|
||||
"expandFilename() not implemented: requires filesystem path expansion",
|
||||
);
|
||||
},
|
||||
findfirstfile(_pattern: any): string {
|
||||
throw new Error(
|
||||
"findFirstFile() not implemented: requires filesystem directory listing",
|
||||
);
|
||||
},
|
||||
findnextfile(_pattern: any): string {
|
||||
throw new Error(
|
||||
"findNextFile() not implemented: requires filesystem directory listing",
|
||||
);
|
||||
},
|
||||
getfilecrc(_path: any): number {
|
||||
throw new Error(
|
||||
"getFileCRC() not implemented: requires filesystem access",
|
||||
);
|
||||
},
|
||||
iswriteablefilename(path: any): boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
// Package management
|
||||
activatepackage(name: any): void {
|
||||
runtime().$.activatePackage(String(name ?? ""));
|
||||
},
|
||||
deactivatepackage(name: any): void {
|
||||
runtime().$.deactivatePackage(String(name ?? ""));
|
||||
},
|
||||
ispackage(name: any): boolean {
|
||||
return runtime().$.isPackage(String(name ?? ""));
|
||||
},
|
||||
|
||||
// Messaging (stubs - no networking layer)
|
||||
addmessagecallback(_msgType: any, _callback: any): void {
|
||||
// No-op: message callbacks are for multiplayer networking
|
||||
},
|
||||
|
||||
// ===== ENGINE STUBS =====
|
||||
// These functions are called by scripts but require engine features we don't have.
|
||||
// They're implemented as no-ops or return sensible defaults.
|
||||
|
||||
// Audio (OpenAL)
|
||||
alxcreatesource(..._args: any[]): number {
|
||||
return 0;
|
||||
},
|
||||
alxgetwavelen(_source: any): number {
|
||||
return 0;
|
||||
},
|
||||
alxlistenerf(_param: any, _value: any): void {},
|
||||
alxplay(..._args: any[]): number {
|
||||
return 0;
|
||||
},
|
||||
alxsetchannelvolume(_channel: any, _volume: any): void {},
|
||||
alxsourcef(_source: any, _param: any, _value: any): void {},
|
||||
alxstop(_source: any): void {},
|
||||
alxstopall(): void {},
|
||||
|
||||
// Device I/O
|
||||
activatedirectinput(): void {},
|
||||
activatekeyboard(): void {},
|
||||
deactivatedirectinput(): void {},
|
||||
deactivatekeyboard(): void {},
|
||||
disablejoystick(): void {},
|
||||
enablejoystick(): void {},
|
||||
enablewinconsole(_enable: any): void {},
|
||||
isjoystickdetected(): boolean {
|
||||
return false;
|
||||
},
|
||||
lockmouse(_lock: any): void {},
|
||||
|
||||
// Video/Display
|
||||
addmaterialmapping(_from: any, _to: any): void {},
|
||||
flushtexturecache(): void {},
|
||||
getdesktopresolution(): string {
|
||||
return "1920 1080 32";
|
||||
},
|
||||
getdisplaydevicelist(): string {
|
||||
return "OpenGL";
|
||||
},
|
||||
getresolutionlist(_device: any): string {
|
||||
return "640 480\t800 600\t1024 768\t1280 720\t1920 1080";
|
||||
},
|
||||
getvideodriverinfo(): string {
|
||||
return "WebGL";
|
||||
},
|
||||
isdevicefullscreenonly(_device: any): boolean {
|
||||
return false;
|
||||
},
|
||||
isfullscreen(): boolean {
|
||||
return false;
|
||||
},
|
||||
screenshot(_filename: any): void {},
|
||||
setdisplaydevice(_device: any): boolean {
|
||||
return true;
|
||||
},
|
||||
setfov(_fov: any): void {},
|
||||
setinteriorrendermode(_mode: any): void {},
|
||||
setopenglanisotropy(_level: any): void {},
|
||||
setopenglmipreduction(_level: any): void {},
|
||||
setopenglskymipreduction(_level: any): void {},
|
||||
setopengltexturecompressionhint(_hint: any): void {},
|
||||
setscreenmode(
|
||||
_width: any,
|
||||
_height: any,
|
||||
_bpp: any,
|
||||
_fullscreen: any,
|
||||
): void {},
|
||||
setverticalsync(_enable: any): void {},
|
||||
setzoomspeed(_speed: any): void {},
|
||||
togglefullscreen(): void {},
|
||||
videosetgammacorrection(_gamma: any): void {},
|
||||
snaptoggle(): void {},
|
||||
|
||||
// Networking
|
||||
addtaggedstring(_str: any): number {
|
||||
return 0;
|
||||
},
|
||||
buildtaggedstring(_format: any, ..._args: any[]): string {
|
||||
return "";
|
||||
},
|
||||
detag(_tagged: any): string {
|
||||
return String(_tagged ?? "");
|
||||
},
|
||||
gettag(_str: any): number {
|
||||
return 0;
|
||||
},
|
||||
gettaggedstring(_tag: any): string {
|
||||
return "";
|
||||
},
|
||||
removetaggedstring(_tag: any): void {},
|
||||
commandtoclient(_client: any, _func: any, ..._args: any[]): void {},
|
||||
commandtoserver(_func: any, ..._args: any[]): void {},
|
||||
cancelserverquery(): void {},
|
||||
querymasterserver(..._args: any[]): void {},
|
||||
querysingleserver(..._args: any[]): void {},
|
||||
setnetport(_port: any): boolean {
|
||||
return true;
|
||||
},
|
||||
startheartbeat(): void {},
|
||||
stopheartbeat(): void {},
|
||||
gotowebpage(_url: any): void {
|
||||
// Could potentially open URL in browser
|
||||
},
|
||||
|
||||
// Scene/Physics
|
||||
containerboxempty(..._args: any[]): boolean {
|
||||
return true;
|
||||
},
|
||||
containerraycast(..._args: any[]): string {
|
||||
return "";
|
||||
},
|
||||
containersearchcurrdist(): number {
|
||||
return 0;
|
||||
},
|
||||
containersearchnext(): number {
|
||||
return 0;
|
||||
},
|
||||
initcontainerradiussearch(..._args: any[]): void {},
|
||||
calcexplosioncoverage(..._args: any[]): number {
|
||||
return 1;
|
||||
},
|
||||
getcontrolobjectaltitude(): number {
|
||||
return 0;
|
||||
},
|
||||
getcontrolobjectspeed(): number {
|
||||
return 0;
|
||||
},
|
||||
getterrainheight(_pos: any): number {
|
||||
return 0;
|
||||
},
|
||||
lightscene(..._args: any[]): void {},
|
||||
pathonmissionloaddone(): void {},
|
||||
};
|
||||
}
|
||||
756
src/torqueScript/codegen.ts
Normal file
756
src/torqueScript/codegen.ts
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
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<string, string> = {
|
||||
// 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);
|
||||
}
|
||||
46
src/torqueScript/index.ts
Normal file
46
src/torqueScript/index.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import TorqueScript from "@/generated/TorqueScript.cjs";
|
||||
import { generate, type GeneratorOptions } from "./codegen";
|
||||
import type { Program } from "./ast";
|
||||
|
||||
export { generate, type GeneratorOptions } from "./codegen";
|
||||
export type { Program } from "./ast";
|
||||
export { createBuiltins } from "./builtins";
|
||||
export { createRuntime } from "./runtime";
|
||||
export { normalizePath } from "./utils";
|
||||
export type {
|
||||
BuiltinsContext,
|
||||
BuiltinsFactory,
|
||||
RuntimeState,
|
||||
TorqueObject,
|
||||
TorqueRuntime,
|
||||
TorqueRuntimeOptions,
|
||||
} from "./types";
|
||||
|
||||
export interface ParseOptions {
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export type TranspileOptions = ParseOptions & GeneratorOptions;
|
||||
|
||||
export function parse(source: string, options?: ParseOptions): Program {
|
||||
try {
|
||||
return TorqueScript.parse(source);
|
||||
} catch (error: any) {
|
||||
if (options?.filename && error.location) {
|
||||
throw new Error(
|
||||
`${options.filename}:${error.location.start.line}:${error.location.start.column}: ${error.message}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function transpile(
|
||||
source: string,
|
||||
options?: TranspileOptions,
|
||||
): { code: string; ast: Program } {
|
||||
const ast = parse(source, options);
|
||||
const code = generate(ast, options);
|
||||
return { code, ast };
|
||||
}
|
||||
1194
src/torqueScript/runtime.spec.ts
Normal file
1194
src/torqueScript/runtime.spec.ts
Normal file
File diff suppressed because it is too large
Load diff
895
src/torqueScript/runtime.ts
Normal file
895
src/torqueScript/runtime.ts
Normal file
|
|
@ -0,0 +1,895 @@
|
|||
import { generate } from "./codegen";
|
||||
import { parse, type Program } from "./index";
|
||||
import { createBuiltins as defaultCreateBuiltins } from "./builtins";
|
||||
import { CaseInsensitiveMap, normalizePath } from "./utils";
|
||||
import type {
|
||||
BuiltinsContext,
|
||||
FunctionStack,
|
||||
FunctionsAPI,
|
||||
GlobalsAPI,
|
||||
LoadedScript,
|
||||
LoadScriptOptions,
|
||||
LocalsAPI,
|
||||
MethodStack,
|
||||
PackageState,
|
||||
RuntimeAPI,
|
||||
RuntimeState,
|
||||
TorqueFunction,
|
||||
TorqueMethod,
|
||||
TorqueObject,
|
||||
TorqueRuntime,
|
||||
TorqueRuntimeOptions,
|
||||
VariableStoreAPI,
|
||||
} from "./types";
|
||||
|
||||
function normalize(name: string): string {
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
function toU32(value: any): number {
|
||||
return (Number(value) | 0) >>> 0;
|
||||
}
|
||||
|
||||
function toI32(value: any): number {
|
||||
return Number(value) | 0;
|
||||
}
|
||||
|
||||
/** Coerce instance name to string, returning null for empty/null values. */
|
||||
function toName(value: any): string | null {
|
||||
if (value == null) return null;
|
||||
if (typeof value === "string") return value || null;
|
||||
if (typeof value === "number") return String(value);
|
||||
throw new Error(`Invalid instance name type: ${typeof value}`);
|
||||
}
|
||||
|
||||
export function createRuntime(
|
||||
options: TorqueRuntimeOptions = {},
|
||||
): TorqueRuntime {
|
||||
const methods = new CaseInsensitiveMap<CaseInsensitiveMap<MethodStack>>();
|
||||
const functions = new CaseInsensitiveMap<FunctionStack>();
|
||||
const packages = new CaseInsensitiveMap<PackageState>();
|
||||
const activePackages: string[] = [];
|
||||
|
||||
const FIRST_DATABLOCK_ID = 3;
|
||||
const FIRST_DYNAMIC_ID = 1027;
|
||||
let nextDatablockId = FIRST_DATABLOCK_ID;
|
||||
let nextObjectId = FIRST_DYNAMIC_ID;
|
||||
|
||||
const objectsById = new Map<number, TorqueObject>();
|
||||
const objectsByName = new CaseInsensitiveMap<TorqueObject>();
|
||||
const datablocks = new CaseInsensitiveMap<TorqueObject>();
|
||||
const globals = new CaseInsensitiveMap<any>();
|
||||
const executedScripts = new Set<string>();
|
||||
const scripts = new Map<string, Program>();
|
||||
const pendingTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||
let currentPackage: PackageState | null = null;
|
||||
let runtimeRef: TorqueRuntime | null = null;
|
||||
const getRuntime = () => runtimeRef!;
|
||||
const createBuiltins = options.builtins ?? defaultCreateBuiltins;
|
||||
const builtinsCtx: BuiltinsContext = { runtime: getRuntime };
|
||||
const builtins = createBuiltins(builtinsCtx);
|
||||
|
||||
function registerMethod(
|
||||
className: string,
|
||||
methodName: string,
|
||||
fn: TorqueMethod,
|
||||
): void {
|
||||
if (currentPackage) {
|
||||
if (!currentPackage.methods.has(className)) {
|
||||
currentPackage.methods.set(className, new CaseInsensitiveMap());
|
||||
}
|
||||
currentPackage.methods.get(className)!.set(methodName, fn);
|
||||
} else {
|
||||
if (!methods.has(className)) {
|
||||
methods.set(className, new CaseInsensitiveMap());
|
||||
}
|
||||
const classMethods = methods.get(className)!;
|
||||
if (!classMethods.has(methodName)) {
|
||||
classMethods.set(methodName, []);
|
||||
}
|
||||
classMethods.get(methodName)!.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
function registerFunction(name: string, fn: TorqueFunction): void {
|
||||
if (currentPackage) {
|
||||
currentPackage.functions.set(name, fn);
|
||||
} else {
|
||||
if (!functions.has(name)) {
|
||||
functions.set(name, []);
|
||||
}
|
||||
functions.get(name)!.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
function activatePackage(name: string): void {
|
||||
const pkg = packages.get(name);
|
||||
if (!pkg || pkg.active) return;
|
||||
|
||||
pkg.active = true;
|
||||
activePackages.push(pkg.name);
|
||||
|
||||
for (const [className, methodMap] of pkg.methods) {
|
||||
if (!methods.has(className)) {
|
||||
methods.set(className, new CaseInsensitiveMap());
|
||||
}
|
||||
const classMethods = methods.get(className)!;
|
||||
for (const [methodName, fn] of methodMap) {
|
||||
if (!classMethods.has(methodName)) {
|
||||
classMethods.set(methodName, []);
|
||||
}
|
||||
classMethods.get(methodName)!.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [funcName, fn] of pkg.functions) {
|
||||
if (!functions.has(funcName)) {
|
||||
functions.set(funcName, []);
|
||||
}
|
||||
functions.get(funcName)!.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
function deactivatePackage(name: string): void {
|
||||
const pkg = packages.get(name);
|
||||
if (!pkg || !pkg.active) return;
|
||||
|
||||
pkg.active = false;
|
||||
// Find and remove from activePackages (case-insensitive search)
|
||||
const idx = activePackages.findIndex(
|
||||
(n) => n.toLowerCase() === name.toLowerCase(),
|
||||
);
|
||||
if (idx !== -1) activePackages.splice(idx, 1);
|
||||
|
||||
// Remove the specific functions this package added (not just pop!)
|
||||
for (const [className, methodMap] of pkg.methods) {
|
||||
const classMethods = methods.get(className);
|
||||
if (!classMethods) continue;
|
||||
for (const [methodName, fn] of methodMap) {
|
||||
const stack = classMethods.get(methodName);
|
||||
if (stack) {
|
||||
const fnIdx = stack.indexOf(fn);
|
||||
if (fnIdx !== -1) stack.splice(fnIdx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [funcName, fn] of pkg.functions) {
|
||||
const stack = functions.get(funcName);
|
||||
if (stack) {
|
||||
const fnIdx = stack.indexOf(fn);
|
||||
if (fnIdx !== -1) stack.splice(fnIdx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function packageFn(name: string, fn: () => void): void {
|
||||
let pkg = packages.get(name);
|
||||
if (!pkg) {
|
||||
pkg = {
|
||||
name,
|
||||
active: false,
|
||||
methods: new CaseInsensitiveMap(),
|
||||
functions: new CaseInsensitiveMap(),
|
||||
};
|
||||
packages.set(name, pkg);
|
||||
}
|
||||
|
||||
const prevPackage = currentPackage;
|
||||
currentPackage = pkg;
|
||||
fn();
|
||||
currentPackage = prevPackage;
|
||||
|
||||
activatePackage(name);
|
||||
}
|
||||
|
||||
function createObject(
|
||||
className: string,
|
||||
instanceName: string | null,
|
||||
props: Record<string, any>,
|
||||
children?: TorqueObject[],
|
||||
): TorqueObject {
|
||||
const normClass = normalize(className);
|
||||
const id = nextObjectId++;
|
||||
|
||||
const obj: TorqueObject = {
|
||||
_class: normClass,
|
||||
_className: className,
|
||||
_id: id,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
obj[normalize(key)] = value;
|
||||
}
|
||||
|
||||
objectsById.set(id, obj);
|
||||
|
||||
const name = toName(instanceName);
|
||||
if (name) {
|
||||
obj._name = name;
|
||||
objectsByName.set(name, obj);
|
||||
}
|
||||
|
||||
if (children) {
|
||||
for (const child of children) {
|
||||
child._parent = obj;
|
||||
}
|
||||
obj._children = children;
|
||||
}
|
||||
|
||||
const onAdd = findMethod(className, "onAdd");
|
||||
if (onAdd) {
|
||||
onAdd(obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function deleteObject(obj: any): boolean {
|
||||
if (obj == null) return false;
|
||||
|
||||
// Resolve object if given by ID or name
|
||||
let target: TorqueObject | undefined;
|
||||
if (typeof obj === "number") {
|
||||
target = objectsById.get(obj);
|
||||
} else if (typeof obj === "string") {
|
||||
target = objectsByName.get(obj);
|
||||
} else if (typeof obj === "object" && obj._id) {
|
||||
target = obj;
|
||||
}
|
||||
|
||||
if (!target) return false;
|
||||
|
||||
// Call onRemove if it exists
|
||||
const onRemove = findMethod(target._className, "onRemove");
|
||||
if (onRemove) {
|
||||
onRemove(target);
|
||||
}
|
||||
|
||||
// Remove from tracking maps
|
||||
objectsById.delete(target._id);
|
||||
if (target._name) {
|
||||
objectsByName.delete(target._name);
|
||||
}
|
||||
if (target._isDatablock && target._name) {
|
||||
datablocks.delete(target._name);
|
||||
}
|
||||
|
||||
// Remove from parent's children array
|
||||
if (target._parent && target._parent._children) {
|
||||
const idx = target._parent._children.indexOf(target);
|
||||
if (idx !== -1) {
|
||||
target._parent._children.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively delete children
|
||||
if (target._children) {
|
||||
for (const child of [...target._children]) {
|
||||
deleteObject(child);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function datablock(
|
||||
className: string,
|
||||
instanceName: string | null,
|
||||
parentName: string | null,
|
||||
props: Record<string, any>,
|
||||
): TorqueObject {
|
||||
const normClass = normalize(className);
|
||||
const id = nextDatablockId++;
|
||||
|
||||
const obj: TorqueObject = {
|
||||
_class: normClass,
|
||||
_className: className,
|
||||
_id: id,
|
||||
_isDatablock: true,
|
||||
};
|
||||
|
||||
const parentKey = toName(parentName);
|
||||
if (parentKey) {
|
||||
const parentObj = datablocks.get(parentKey);
|
||||
if (parentObj) {
|
||||
for (const [key, value] of Object.entries(parentObj)) {
|
||||
if (!key.startsWith("_")) {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
obj._parent = parentObj;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
obj[normalize(key)] = value;
|
||||
}
|
||||
|
||||
objectsById.set(id, obj);
|
||||
|
||||
const name = toName(instanceName);
|
||||
if (name) {
|
||||
obj._name = name;
|
||||
objectsByName.set(name, obj);
|
||||
datablocks.set(name, obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function prop(obj: any, name: string): any {
|
||||
if (obj == null) return "";
|
||||
return obj[normalize(name)] ?? "";
|
||||
}
|
||||
|
||||
function setProp(obj: any, name: string, value: any): any {
|
||||
if (obj == null) return value;
|
||||
obj[normalize(name)] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function getIndex(obj: any, index: any): any {
|
||||
if (obj == null) return "";
|
||||
return obj[String(index)] ?? "";
|
||||
}
|
||||
|
||||
function setIndex(obj: any, index: any, value: any): any {
|
||||
if (obj == null) return value;
|
||||
obj[String(index)] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function postIncDec(obj: any, key: string, delta: 1 | -1): number {
|
||||
if (obj == null) return 0;
|
||||
const oldValue = toNum(obj[key]);
|
||||
obj[key] = oldValue + delta;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
function propPostInc(obj: any, name: string): number {
|
||||
return postIncDec(obj, normalize(name), 1);
|
||||
}
|
||||
|
||||
function propPostDec(obj: any, name: string): number {
|
||||
return postIncDec(obj, normalize(name), -1);
|
||||
}
|
||||
|
||||
function indexPostInc(obj: any, index: any): number {
|
||||
return postIncDec(obj, String(index), 1);
|
||||
}
|
||||
|
||||
function indexPostDec(obj: any, index: any): number {
|
||||
return postIncDec(obj, String(index), -1);
|
||||
}
|
||||
|
||||
// TorqueScript array indexing: foo[0] -> foo0, foo[0,1] -> foo0_1
|
||||
function key(base: string, ...indices: any[]): string {
|
||||
return base + indices.join("_");
|
||||
}
|
||||
|
||||
function findMethod(
|
||||
className: string,
|
||||
methodName: string,
|
||||
): TorqueMethod | null {
|
||||
const classMethods = methods.get(className);
|
||||
if (classMethods) {
|
||||
const stack = classMethods.get(methodName);
|
||||
if (stack && stack.length > 0) {
|
||||
return stack[stack.length - 1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFunction(name: string): TorqueFunction | null {
|
||||
const stack = functions.get(name);
|
||||
if (stack && stack.length > 0) {
|
||||
return stack[stack.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function call(obj: any, methodName: string, ...args: any[]): any {
|
||||
if (obj == null) return "";
|
||||
|
||||
// Dereference string/number names to actual objects
|
||||
if (typeof obj === "string" || typeof obj === "number") {
|
||||
obj = deref(obj);
|
||||
if (obj == null) return "";
|
||||
}
|
||||
|
||||
const objClass = obj._className || obj._class;
|
||||
|
||||
if (objClass) {
|
||||
const fn = findMethod(objClass, methodName);
|
||||
if (fn) {
|
||||
return fn(obj, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const db = obj._datablock || obj;
|
||||
if (db._parent) {
|
||||
let current = db._parent;
|
||||
while (current) {
|
||||
const parentClass = current._className || current._class;
|
||||
if (parentClass) {
|
||||
const fn = findMethod(parentClass, methodName);
|
||||
if (fn) {
|
||||
return fn(obj, ...args);
|
||||
}
|
||||
}
|
||||
current = current._parent;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function nsCall(namespace: string, method: string, ...args: any[]): any {
|
||||
const fn = findMethod(namespace, method);
|
||||
if (fn) {
|
||||
return (fn as TorqueFunction)(...args);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function nsRef(
|
||||
namespace: string,
|
||||
method: string,
|
||||
): ((...args: any[]) => any) | null {
|
||||
const fn = findMethod(namespace, method);
|
||||
if (fn) {
|
||||
return (...args: any[]) => (fn as TorqueFunction)(...args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parent(
|
||||
currentClass: string,
|
||||
methodName: string,
|
||||
thisObj: any,
|
||||
...args: any[]
|
||||
): any {
|
||||
const classMethods = methods.get(currentClass);
|
||||
if (!classMethods) return "";
|
||||
|
||||
const stack = classMethods.get(methodName);
|
||||
if (!stack || stack.length < 2) return "";
|
||||
|
||||
// Call parent method with the object as first argument
|
||||
return stack[stack.length - 2](thisObj, ...args);
|
||||
}
|
||||
|
||||
function parentFunc(currentFunc: string, ...args: any[]): any {
|
||||
const stack = functions.get(currentFunc);
|
||||
if (!stack || stack.length < 2) return "";
|
||||
|
||||
return stack[stack.length - 2](...args);
|
||||
}
|
||||
|
||||
function toNum(value: any): number {
|
||||
if (value == null || value === "") return 0;
|
||||
const n = Number(value);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function add(a: any, b: any): number {
|
||||
return toNum(a) + toNum(b);
|
||||
}
|
||||
|
||||
function sub(a: any, b: any): number {
|
||||
return toNum(a) - toNum(b);
|
||||
}
|
||||
|
||||
function mul(a: any, b: any): number {
|
||||
return toNum(a) * toNum(b);
|
||||
}
|
||||
|
||||
function div(a: any, b: any): number {
|
||||
const divisor = toNum(b);
|
||||
if (divisor === 0) return 0; // TorqueScript returns 0 for division by zero
|
||||
return toNum(a) / divisor;
|
||||
}
|
||||
|
||||
function neg(a: any): number {
|
||||
return -toNum(a);
|
||||
}
|
||||
|
||||
function lt(a: any, b: any): boolean {
|
||||
return toNum(a) < toNum(b);
|
||||
}
|
||||
|
||||
function le(a: any, b: any): boolean {
|
||||
return toNum(a) <= toNum(b);
|
||||
}
|
||||
|
||||
function gt(a: any, b: any): boolean {
|
||||
return toNum(a) > toNum(b);
|
||||
}
|
||||
|
||||
function ge(a: any, b: any): boolean {
|
||||
return toNum(a) >= toNum(b);
|
||||
}
|
||||
|
||||
function eq(a: any, b: any): boolean {
|
||||
return toNum(a) === toNum(b);
|
||||
}
|
||||
|
||||
function ne(a: any, b: any): boolean {
|
||||
return toNum(a) !== toNum(b);
|
||||
}
|
||||
|
||||
function mod(a: any, b: any): number {
|
||||
const ib = toI32(b);
|
||||
if (ib === 0) return 0;
|
||||
return toI32(a) % ib;
|
||||
}
|
||||
|
||||
function bitand(a: any, b: any): number {
|
||||
return toU32(a) & toU32(b);
|
||||
}
|
||||
|
||||
function bitor(a: any, b: any): number {
|
||||
return toU32(a) | toU32(b);
|
||||
}
|
||||
|
||||
function bitxor(a: any, b: any): number {
|
||||
return toU32(a) ^ toU32(b);
|
||||
}
|
||||
|
||||
function shl(a: any, b: any): number {
|
||||
return toU32(toU32(a) << (toU32(b) & 31));
|
||||
}
|
||||
|
||||
function shr(a: any, b: any): number {
|
||||
return toU32(a) >>> (toU32(b) & 31);
|
||||
}
|
||||
|
||||
function bitnot(a: any): number {
|
||||
return ~toU32(a) >>> 0;
|
||||
}
|
||||
|
||||
function concat(...parts: any[]): string {
|
||||
return parts.map((p) => String(p ?? "")).join("");
|
||||
}
|
||||
|
||||
function streq(a: any, b: any): boolean {
|
||||
return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
function switchStr(
|
||||
value: any,
|
||||
cases: Record<string, () => void> & { default?: () => void },
|
||||
): void {
|
||||
const normValue = String(value ?? "").toLowerCase();
|
||||
|
||||
for (const [caseValue, handler] of Object.entries(cases)) {
|
||||
if (caseValue === "default") continue;
|
||||
if (normalize(caseValue) === normValue) {
|
||||
handler();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (cases.default) {
|
||||
cases.default();
|
||||
}
|
||||
}
|
||||
|
||||
function deref(tag: any): any {
|
||||
if (tag == null || tag === "") return null;
|
||||
return objectsByName.get(String(tag)) ?? null;
|
||||
}
|
||||
|
||||
function nameToId(name: string): number {
|
||||
const obj = objectsByName.get(name);
|
||||
return obj ? obj._id : 0;
|
||||
}
|
||||
|
||||
function isObject(obj: any): boolean {
|
||||
if (obj == null) return false;
|
||||
if (typeof obj === "object" && obj._id) return true;
|
||||
if (typeof obj === "number") return objectsById.has(obj);
|
||||
if (typeof obj === "string") return objectsByName.has(obj);
|
||||
return false;
|
||||
}
|
||||
|
||||
function isFunction(name: string): boolean {
|
||||
return functions.has(name);
|
||||
}
|
||||
|
||||
function isPackage(name: string): boolean {
|
||||
return packages.has(name);
|
||||
}
|
||||
|
||||
function createVariableStore(
|
||||
storage: CaseInsensitiveMap<any>,
|
||||
): VariableStoreAPI {
|
||||
// TorqueScript array indexing: $foo[0] -> $foo0, $foo[0,1] -> $foo0_1
|
||||
function fullName(name: string, indices: any[]): string {
|
||||
return name + indices.join("_");
|
||||
}
|
||||
|
||||
return {
|
||||
get(name: string, ...indices: any[]): any {
|
||||
return storage.get(fullName(name, indices)) ?? "";
|
||||
},
|
||||
set(name: string, ...args: any[]): any {
|
||||
if (args.length === 0) {
|
||||
throw new Error("set() requires at least a value argument");
|
||||
}
|
||||
if (args.length === 1) {
|
||||
storage.set(name, args[0]);
|
||||
return args[0];
|
||||
}
|
||||
const value = args[args.length - 1];
|
||||
const indices = args.slice(0, -1);
|
||||
storage.set(fullName(name, indices), value);
|
||||
return value;
|
||||
},
|
||||
postInc(name: string, ...indices: any[]): number {
|
||||
const key = fullName(name, indices);
|
||||
const oldValue = toNum(storage.get(key));
|
||||
storage.set(key, oldValue + 1);
|
||||
return oldValue;
|
||||
},
|
||||
postDec(name: string, ...indices: any[]): number {
|
||||
const key = fullName(name, indices);
|
||||
const oldValue = toNum(storage.get(key));
|
||||
storage.set(key, oldValue - 1);
|
||||
return oldValue;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createLocals(): LocalsAPI {
|
||||
return createVariableStore(new CaseInsensitiveMap<any>());
|
||||
}
|
||||
|
||||
const $: RuntimeAPI = {
|
||||
registerMethod,
|
||||
registerFunction,
|
||||
package: packageFn,
|
||||
activatePackage,
|
||||
deactivatePackage,
|
||||
create: createObject,
|
||||
datablock,
|
||||
deleteObject,
|
||||
prop,
|
||||
setProp,
|
||||
getIndex,
|
||||
setIndex,
|
||||
propPostInc,
|
||||
propPostDec,
|
||||
indexPostInc,
|
||||
indexPostDec,
|
||||
key,
|
||||
call,
|
||||
nsCall,
|
||||
nsRef,
|
||||
parent,
|
||||
parentFunc,
|
||||
add,
|
||||
sub,
|
||||
mul,
|
||||
div,
|
||||
neg,
|
||||
lt,
|
||||
le,
|
||||
gt,
|
||||
ge,
|
||||
eq,
|
||||
ne,
|
||||
mod,
|
||||
bitand,
|
||||
bitor,
|
||||
bitxor,
|
||||
shl,
|
||||
shr,
|
||||
bitnot,
|
||||
concat,
|
||||
streq,
|
||||
switchStr,
|
||||
deref,
|
||||
nameToId,
|
||||
isObject,
|
||||
isFunction,
|
||||
isPackage,
|
||||
locals: createLocals,
|
||||
};
|
||||
|
||||
const $f: FunctionsAPI = {
|
||||
call(name: string, ...args: any[]): any {
|
||||
const fn = findFunction(name);
|
||||
if (fn) {
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
// Builtins are stored with lowercase keys
|
||||
const builtin = builtins[name.toLowerCase()];
|
||||
if (builtin) {
|
||||
return builtin(...args);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unknown function: ${name}(${args
|
||||
.map((a) => JSON.stringify(a))
|
||||
.join(", ")})`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const $g: GlobalsAPI = createVariableStore(globals);
|
||||
|
||||
const generatedCode = new WeakMap<Program, string>();
|
||||
|
||||
const state: RuntimeState = {
|
||||
methods,
|
||||
functions,
|
||||
packages,
|
||||
activePackages,
|
||||
objectsById,
|
||||
objectsByName,
|
||||
datablocks,
|
||||
globals,
|
||||
executedScripts,
|
||||
scripts,
|
||||
generatedCode,
|
||||
pendingTimeouts,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
function destroy(): void {
|
||||
for (const timeoutId of state.pendingTimeouts) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
state.pendingTimeouts.clear();
|
||||
}
|
||||
|
||||
function getOrGenerateCode(ast: Program): string {
|
||||
let code = generatedCode.get(ast);
|
||||
if (code == null) {
|
||||
code = generate(ast);
|
||||
generatedCode.set(ast, code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function executeAST(ast: Program): void {
|
||||
const code = getOrGenerateCode(ast);
|
||||
const execFn = new Function("$", "$f", "$g", code);
|
||||
execFn($, $f, $g);
|
||||
}
|
||||
|
||||
function createLoadedScript(ast: Program, path?: string): LoadedScript {
|
||||
return {
|
||||
execute(): void {
|
||||
if (path) {
|
||||
const normalized = normalizePath(path);
|
||||
state.executedScripts.add(normalized);
|
||||
}
|
||||
executeAST(ast);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDependencies(
|
||||
ast: Program,
|
||||
loading: Set<string>,
|
||||
): Promise<void> {
|
||||
const loader = options.loadScript;
|
||||
if (!loader) {
|
||||
// No loader, can't resolve dependencies
|
||||
if (ast.execScriptPaths.length > 0) {
|
||||
console.warn(
|
||||
`Script has exec() calls but no loadScript provided:`,
|
||||
ast.execScriptPaths,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ref of ast.execScriptPaths) {
|
||||
const normalized = normalizePath(ref);
|
||||
|
||||
// Skip if already loaded or currently loading (cycle detection)
|
||||
if (state.scripts.has(normalized) || loading.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
loading.add(normalized);
|
||||
|
||||
const source = await loader(ref);
|
||||
if (source == null) {
|
||||
console.warn(`Script not found: ${ref}`);
|
||||
loading.delete(normalized);
|
||||
continue;
|
||||
}
|
||||
|
||||
let depAst: Program;
|
||||
try {
|
||||
depAst = parse(source, { filename: ref });
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse script: ${ref}`, err);
|
||||
loading.delete(normalized);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recursively load this script's dependencies first
|
||||
await loadDependencies(depAst, loading);
|
||||
|
||||
// Store the parsed AST
|
||||
state.scripts.set(normalized, depAst);
|
||||
loading.delete(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromPath(path: string): Promise<LoadedScript> {
|
||||
const loader = options.loadScript;
|
||||
if (!loader) {
|
||||
throw new Error("loadFromPath requires loadScript option to be set");
|
||||
}
|
||||
|
||||
// Check if already loaded (avoid unnecessary fetch)
|
||||
const normalized = normalizePath(path);
|
||||
if (state.scripts.has(normalized)) {
|
||||
return createLoadedScript(state.scripts.get(normalized)!, path);
|
||||
}
|
||||
|
||||
const source = await loader(path);
|
||||
if (source == null) {
|
||||
throw new Error(`Script not found: ${path}`);
|
||||
}
|
||||
|
||||
return loadFromSource(source, { path });
|
||||
}
|
||||
|
||||
async function loadFromSource(
|
||||
source: string,
|
||||
loadOptions?: LoadScriptOptions,
|
||||
): Promise<LoadedScript> {
|
||||
// Check if already loaded
|
||||
if (loadOptions?.path) {
|
||||
const normalized = normalizePath(loadOptions.path);
|
||||
if (state.scripts.has(normalized)) {
|
||||
return createLoadedScript(
|
||||
state.scripts.get(normalized)!,
|
||||
loadOptions.path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ast = parse(source, { filename: loadOptions?.path });
|
||||
return loadFromAST(ast, loadOptions);
|
||||
}
|
||||
|
||||
async function loadFromAST(
|
||||
ast: Program,
|
||||
loadOptions?: LoadScriptOptions,
|
||||
): Promise<LoadedScript> {
|
||||
// Load dependencies
|
||||
const loading = new Set<string>();
|
||||
if (loadOptions?.path) {
|
||||
const normalized = normalizePath(loadOptions.path);
|
||||
loading.add(normalized);
|
||||
state.scripts.set(normalized, ast);
|
||||
}
|
||||
await loadDependencies(ast, loading);
|
||||
|
||||
return createLoadedScript(ast, loadOptions?.path);
|
||||
}
|
||||
|
||||
runtimeRef = {
|
||||
$,
|
||||
$f,
|
||||
$g,
|
||||
state,
|
||||
destroy,
|
||||
executeAST,
|
||||
loadFromPath,
|
||||
loadFromSource,
|
||||
loadFromAST,
|
||||
};
|
||||
return runtimeRef;
|
||||
}
|
||||
30
src/torqueScript/scriptLoader.browser.ts
Normal file
30
src/torqueScript/scriptLoader.browser.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { ScriptLoader } from "./types";
|
||||
import { getUrlForPath } from "../loaders";
|
||||
|
||||
/**
|
||||
* Creates a script loader for browser environments that fetches scripts
|
||||
* using the manifest-based URL resolution.
|
||||
*/
|
||||
export function createScriptLoader(): ScriptLoader {
|
||||
return async (path: string): Promise<string | null> => {
|
||||
let url: string;
|
||||
try {
|
||||
url = getUrlForPath(path);
|
||||
} catch (err) {
|
||||
console.warn(`Script not in manifest: ${path}`, err);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
console.warn(`Script fetch failed: ${path} (${response.status})`);
|
||||
return null;
|
||||
}
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
console.warn(`Script fetch error: ${path}`, err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
28
src/torqueScript/scriptLoader.node.ts
Normal file
28
src/torqueScript/scriptLoader.node.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { ScriptLoader } from "./types";
|
||||
|
||||
export interface CreateScriptLoaderOptions {
|
||||
searchPaths: string[];
|
||||
}
|
||||
|
||||
export function createScriptLoader(
|
||||
options: CreateScriptLoaderOptions,
|
||||
): ScriptLoader {
|
||||
const { searchPaths } = options;
|
||||
|
||||
return async (path: string): Promise<string | null> => {
|
||||
const normalizedPath = path.replace(/\\/g, "/");
|
||||
|
||||
for (const basePath of searchPaths) {
|
||||
const fullPath = join(basePath, normalizedPath);
|
||||
try {
|
||||
return await readFile(fullPath, "utf8");
|
||||
} catch {
|
||||
// File doesn't exist in this search path, try next
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
179
src/torqueScript/types.ts
Normal file
179
src/torqueScript/types.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import type { Program } from "./ast";
|
||||
import type { CaseInsensitiveMap } from "./utils";
|
||||
|
||||
export type TorqueFunction = (...args: any[]) => any;
|
||||
export type TorqueMethod = (this_: TorqueObject, ...args: any[]) => any;
|
||||
|
||||
export interface TorqueObject {
|
||||
_class: string; // normalized class name
|
||||
_className: string; // original class name
|
||||
_id: number;
|
||||
_name?: string;
|
||||
_isDatablock?: boolean;
|
||||
_parent?: TorqueObject;
|
||||
_children?: TorqueObject[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type MethodStack = TorqueMethod[];
|
||||
export type FunctionStack = TorqueFunction[];
|
||||
|
||||
export interface PackageState {
|
||||
name: string;
|
||||
active: boolean;
|
||||
methods: CaseInsensitiveMap<CaseInsensitiveMap<TorqueMethod>>; // class -> method -> fn
|
||||
functions: CaseInsensitiveMap<TorqueFunction>;
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
methods: CaseInsensitiveMap<CaseInsensitiveMap<MethodStack>>;
|
||||
functions: CaseInsensitiveMap<FunctionStack>;
|
||||
packages: CaseInsensitiveMap<PackageState>;
|
||||
activePackages: readonly string[];
|
||||
objectsById: Map<number, TorqueObject>;
|
||||
objectsByName: CaseInsensitiveMap<TorqueObject>;
|
||||
datablocks: CaseInsensitiveMap<TorqueObject>;
|
||||
globals: CaseInsensitiveMap<any>;
|
||||
executedScripts: Set<string>;
|
||||
scripts: Map<string, Program>;
|
||||
generatedCode: WeakMap<Program, string>;
|
||||
pendingTimeouts: Set<ReturnType<typeof setTimeout>>;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
export interface TorqueRuntime {
|
||||
$: RuntimeAPI;
|
||||
$f: FunctionsAPI;
|
||||
$g: GlobalsAPI;
|
||||
state: RuntimeState;
|
||||
destroy(): void;
|
||||
executeAST(ast: Program): void;
|
||||
loadFromPath(path: string): Promise<LoadedScript>;
|
||||
loadFromSource(
|
||||
source: string,
|
||||
options?: LoadScriptOptions,
|
||||
): Promise<LoadedScript>;
|
||||
loadFromAST(ast: Program, options?: LoadScriptOptions): Promise<LoadedScript>;
|
||||
}
|
||||
|
||||
export type ScriptLoader = (path: string) => Promise<string | null>;
|
||||
|
||||
export interface LoadedScript {
|
||||
execute(): void;
|
||||
}
|
||||
|
||||
export interface TorqueRuntimeOptions {
|
||||
loadScript?: ScriptLoader;
|
||||
builtins?: BuiltinsFactory;
|
||||
}
|
||||
|
||||
export interface LoadScriptOptions {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface RuntimeAPI {
|
||||
// Registration
|
||||
registerMethod(className: string, methodName: string, fn: TorqueMethod): void;
|
||||
registerFunction(name: string, fn: TorqueFunction): void;
|
||||
package(name: string, fn: () => void): void;
|
||||
activatePackage(name: string): void;
|
||||
deactivatePackage(name: string): void;
|
||||
|
||||
// Object creation and deletion
|
||||
create(
|
||||
className: string,
|
||||
instanceName: string | null,
|
||||
props: Record<string, any>,
|
||||
children?: TorqueObject[],
|
||||
): TorqueObject;
|
||||
datablock(
|
||||
className: string,
|
||||
instanceName: string | null,
|
||||
parentName: string | null,
|
||||
props: Record<string, any>,
|
||||
): TorqueObject;
|
||||
deleteObject(obj: any): boolean;
|
||||
|
||||
// Property access
|
||||
prop(obj: any, name: string): any;
|
||||
setProp(obj: any, name: string, value: any): any;
|
||||
getIndex(obj: any, index: any): any;
|
||||
setIndex(obj: any, index: any, value: any): any;
|
||||
propPostInc(obj: any, name: string): number;
|
||||
propPostDec(obj: any, name: string): number;
|
||||
indexPostInc(obj: any, index: any): number;
|
||||
indexPostDec(obj: any, index: any): number;
|
||||
key(...parts: any[]): string;
|
||||
|
||||
// Method dispatch
|
||||
call(obj: any, methodName: string, ...args: any[]): any;
|
||||
nsCall(namespace: string, method: string, ...args: any[]): any;
|
||||
nsRef(namespace: string, method: string): ((...args: any[]) => any) | null;
|
||||
parent(currentClass: string, methodName: string, ...args: any[]): any;
|
||||
parentFunc(currentFunc: string, ...args: any[]): any;
|
||||
|
||||
// Arithmetic (numeric coercion)
|
||||
add(a: any, b: any): number;
|
||||
sub(a: any, b: any): number;
|
||||
mul(a: any, b: any): number;
|
||||
div(a: any, b: any): number;
|
||||
neg(a: any): number;
|
||||
|
||||
// Numeric comparison
|
||||
lt(a: any, b: any): boolean;
|
||||
le(a: any, b: any): boolean;
|
||||
gt(a: any, b: any): boolean;
|
||||
ge(a: any, b: any): boolean;
|
||||
eq(a: any, b: any): boolean;
|
||||
ne(a: any, b: any): boolean;
|
||||
|
||||
// Integer math
|
||||
mod(a: any, b: any): number;
|
||||
bitand(a: any, b: any): number;
|
||||
bitor(a: any, b: any): number;
|
||||
bitxor(a: any, b: any): number;
|
||||
shl(a: any, b: any): number;
|
||||
shr(a: any, b: any): number;
|
||||
bitnot(a: any): number;
|
||||
|
||||
// String operations
|
||||
concat(...parts: any[]): string;
|
||||
streq(a: any, b: any): boolean;
|
||||
switchStr(
|
||||
value: any,
|
||||
cases: Record<string, () => void> & { default?: () => void },
|
||||
): void;
|
||||
|
||||
// Special
|
||||
deref(tag: any): any;
|
||||
nameToId(name: string): number;
|
||||
isObject(obj: any): boolean;
|
||||
isFunction(name: string): boolean;
|
||||
isPackage(name: string): boolean;
|
||||
|
||||
// Local variable scope
|
||||
locals(): LocalsAPI;
|
||||
}
|
||||
|
||||
export interface FunctionsAPI {
|
||||
call(name: string, ...args: any[]): any;
|
||||
}
|
||||
|
||||
export interface VariableStoreAPI {
|
||||
get(name: string, ...indices: any[]): any;
|
||||
set(name: string, ...args: any[]): any;
|
||||
postInc(name: string, ...indices: any[]): number;
|
||||
postDec(name: string, ...indices: any[]): number;
|
||||
}
|
||||
|
||||
// Backwards compatibility aliases
|
||||
export type GlobalsAPI = VariableStoreAPI;
|
||||
export type LocalsAPI = VariableStoreAPI;
|
||||
|
||||
export interface BuiltinsContext {
|
||||
runtime: () => TorqueRuntime;
|
||||
}
|
||||
|
||||
export type BuiltinsFactory = (
|
||||
ctx: BuiltinsContext,
|
||||
) => Record<string, TorqueFunction>;
|
||||
94
src/torqueScript/utils.ts
Normal file
94
src/torqueScript/utils.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Map with case-insensitive key lookups, preserving original casing.
|
||||
* The underlying map stores values with original key casing for inspection.
|
||||
*/
|
||||
export class CaseInsensitiveMap<V> {
|
||||
private map = new Map<string, V>();
|
||||
private keyLookup = new Map<string, string>(); // normalized -> original
|
||||
|
||||
constructor(entries?: Iterable<readonly [string, V]> | null) {
|
||||
if (entries) {
|
||||
for (const [key, value] of entries) {
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
get(key: string): V | undefined {
|
||||
const originalKey = this.keyLookup.get(key.toLowerCase());
|
||||
return originalKey !== undefined ? this.map.get(originalKey) : undefined;
|
||||
}
|
||||
|
||||
set(key: string, value: V): this {
|
||||
const norm = key.toLowerCase();
|
||||
const existingKey = this.keyLookup.get(norm);
|
||||
if (existingKey !== undefined) {
|
||||
// Key exists, update value using existing casing
|
||||
this.map.set(existingKey, value);
|
||||
} else {
|
||||
// New key, store with original casing
|
||||
this.keyLookup.set(norm, key);
|
||||
this.map.set(key, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.keyLookup.has(key.toLowerCase());
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
const norm = key.toLowerCase();
|
||||
const originalKey = this.keyLookup.get(norm);
|
||||
if (originalKey !== undefined) {
|
||||
this.keyLookup.delete(norm);
|
||||
return this.map.delete(originalKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.map.clear();
|
||||
this.keyLookup.clear();
|
||||
}
|
||||
|
||||
keys(): IterableIterator<string> {
|
||||
return this.map.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return this.map.values();
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[string, V]> {
|
||||
return this.map.entries();
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[string, V]> {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
|
||||
forEach(
|
||||
callback: (value: V, key: string, map: CaseInsensitiveMap<V>) => void,
|
||||
): void {
|
||||
for (const [key, value] of this.map) {
|
||||
callback(value, key, this);
|
||||
}
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "CaseInsensitiveMap";
|
||||
}
|
||||
|
||||
getOriginalKey(key: string): string | undefined {
|
||||
return this.keyLookup.get(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, "/").toLowerCase();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue