2024-09-24 16:15:22 +08:00
|
|
|
import type { LuaFunctionBody } from "./ast.ts";
|
2024-09-27 23:09:25 +08:00
|
|
|
import { evalStatement } from "$common/space_lua/eval.ts";
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
export type LuaType =
|
|
|
|
| "nil"
|
|
|
|
| "boolean"
|
|
|
|
| "number"
|
|
|
|
| "string"
|
|
|
|
| "table"
|
|
|
|
| "function"
|
|
|
|
| "userdata"
|
|
|
|
| "thread";
|
|
|
|
|
|
|
|
// These types are for documentation only
|
|
|
|
export type LuaValue = any;
|
|
|
|
export type JSValue = any;
|
|
|
|
|
|
|
|
export interface ILuaFunction {
|
|
|
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ILuaSettable {
|
|
|
|
set(key: LuaValue, value: LuaValue): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ILuaGettable {
|
|
|
|
get(key: LuaValue): LuaValue | undefined;
|
|
|
|
}
|
|
|
|
|
2024-09-27 23:09:25 +08:00
|
|
|
export class LuaEnv implements ILuaSettable, ILuaGettable {
|
2024-09-30 18:50:54 +08:00
|
|
|
variables = new Map<string, LuaValue>();
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
constructor(readonly parent?: LuaEnv) {
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
setLocal(name: string, value: LuaValue) {
|
|
|
|
this.variables.set(name, value);
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
set(key: string, value: LuaValue): void {
|
|
|
|
if (this.variables.has(key) || !this.parent) {
|
|
|
|
this.variables.set(key, value);
|
|
|
|
} else {
|
|
|
|
this.parent.set(key, value);
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
get(name: string): LuaValue | undefined {
|
|
|
|
if (this.variables.has(name)) {
|
|
|
|
return this.variables.get(name);
|
|
|
|
}
|
|
|
|
if (this.parent) {
|
|
|
|
return this.parent.get(name);
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return undefined;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export class LuaMultiRes {
|
2024-10-03 23:55:51 +08:00
|
|
|
values: any[];
|
|
|
|
|
|
|
|
constructor(values: LuaValue[] | LuaValue) {
|
|
|
|
if (values instanceof LuaMultiRes) {
|
|
|
|
this.values = values.values;
|
|
|
|
} else {
|
|
|
|
this.values = Array.isArray(values) ? values : [values];
|
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
unwrap(): any {
|
|
|
|
if (this.values.length !== 1) {
|
|
|
|
throw new Error("Cannot unwrap multiple values");
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return this.values[0];
|
|
|
|
}
|
2024-10-03 23:55:51 +08:00
|
|
|
|
|
|
|
// Takes an array of either LuaMultiRes or LuaValue and flattens them into a single LuaMultiRes
|
|
|
|
flatten(): LuaMultiRes {
|
|
|
|
const result: any[] = [];
|
|
|
|
for (const value of this.values) {
|
|
|
|
if (value instanceof LuaMultiRes) {
|
|
|
|
result.push(...value.values);
|
|
|
|
} else {
|
|
|
|
result.push(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new LuaMultiRes(result);
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function singleResult(value: any): any {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (value instanceof LuaMultiRes) {
|
|
|
|
return value.unwrap();
|
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export class LuaFunction implements ILuaFunction {
|
2024-09-30 18:50:54 +08:00
|
|
|
constructor(private body: LuaFunctionBody, private closure: LuaEnv) {
|
|
|
|
}
|
|
|
|
|
|
|
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
|
|
|
// Create a new environment for this function call
|
|
|
|
const env = new LuaEnv(this.closure);
|
|
|
|
// Assign the passed arguments to the parameters
|
|
|
|
for (let i = 0; i < this.body.parameters.length; i++) {
|
|
|
|
let arg = args[i];
|
|
|
|
if (arg === undefined) {
|
|
|
|
arg = null;
|
|
|
|
}
|
|
|
|
env.set(this.body.parameters[i], arg);
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return evalStatement(this.body.block, env).catch((e: any) => {
|
|
|
|
if (e instanceof LuaReturn) {
|
|
|
|
if (e.values.length === 0) {
|
|
|
|
return;
|
|
|
|
} else if (e.values.length === 1) {
|
|
|
|
return e.values[0];
|
|
|
|
} else {
|
|
|
|
return new LuaMultiRes(e.values);
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export class LuaNativeJSFunction implements ILuaFunction {
|
2024-09-30 18:50:54 +08:00
|
|
|
constructor(readonly fn: (...args: JSValue[]) => JSValue) {
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
// Performs automatic conversion between Lua and JS values
|
2024-09-30 18:50:54 +08:00
|
|
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
|
|
|
const result = this.fn(...args.map(luaValueToJS));
|
|
|
|
if (result instanceof Promise) {
|
|
|
|
return result.then(jsToLuaValue);
|
|
|
|
} else {
|
|
|
|
return jsToLuaValue(result);
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
export class LuaBuiltinFunction implements ILuaFunction {
|
|
|
|
constructor(readonly fn: (...args: LuaValue[]) => LuaValue) {
|
|
|
|
}
|
|
|
|
|
|
|
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
|
|
|
return this.fn(...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-27 23:09:25 +08:00
|
|
|
export class LuaTable implements ILuaSettable, ILuaGettable {
|
2024-09-30 18:50:54 +08:00
|
|
|
// To optimize the table implementation we use a combination of different data structures
|
|
|
|
// When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
|
|
|
|
private stringKeys: Record<string, any>;
|
|
|
|
// Other keys we can support using a Map as a fallback
|
|
|
|
private otherKeys: Map<any, any> | null;
|
|
|
|
// When tables are used as arrays, we use a native JavaScript array for that
|
|
|
|
private arrayPart: any[];
|
|
|
|
|
|
|
|
// TODO: Actually implement metatables
|
|
|
|
private metatable: LuaTable | null;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
// For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
|
|
|
|
this.stringKeys = {};
|
|
|
|
this.arrayPart = [];
|
|
|
|
this.otherKeys = null; // Only create this when needed
|
|
|
|
this.metatable = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
get length(): number {
|
|
|
|
return this.arrayPart.length;
|
|
|
|
}
|
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
keys(): any[] {
|
|
|
|
const keys: any[] = Object.keys(this.stringKeys);
|
|
|
|
for (let i = 0; i < this.arrayPart.length; i++) {
|
|
|
|
keys.push(i + 1);
|
|
|
|
}
|
|
|
|
for (const key of Object.keys(this.stringKeys)) {
|
|
|
|
keys.push(key);
|
|
|
|
}
|
|
|
|
if (this.otherKeys) {
|
|
|
|
for (const key of this.otherKeys.keys()) {
|
|
|
|
keys.push(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return keys;
|
|
|
|
}
|
|
|
|
|
2024-09-30 18:50:54 +08:00
|
|
|
set(key: LuaValue, value: LuaValue) {
|
|
|
|
if (typeof key === "string") {
|
|
|
|
this.stringKeys[key] = value;
|
|
|
|
} else if (Number.isInteger(key) && key >= 1) {
|
|
|
|
this.arrayPart[key - 1] = value;
|
|
|
|
} else {
|
|
|
|
if (!this.otherKeys) {
|
|
|
|
this.otherKeys = new Map();
|
|
|
|
}
|
|
|
|
this.otherKeys.set(key, value);
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get(key: LuaValue): LuaValue | undefined {
|
|
|
|
if (typeof key === "string") {
|
|
|
|
return this.stringKeys[key];
|
|
|
|
} else if (Number.isInteger(key) && key >= 1) {
|
|
|
|
return this.arrayPart[key - 1];
|
|
|
|
} else if (this.otherKeys) {
|
|
|
|
return this.otherKeys.get(key);
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return undefined;
|
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
toJSArray(): JSValue[] {
|
2024-09-30 18:50:54 +08:00
|
|
|
return this.arrayPart;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
toJSObject(): Record<string, JSValue> {
|
2024-09-30 18:50:54 +08:00
|
|
|
const result = { ...this.stringKeys };
|
|
|
|
for (const i in this.arrayPart) {
|
|
|
|
result[parseInt(i) + 1] = this.arrayPart[i];
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return result;
|
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
static fromJSArray(arr: JSValue[]): LuaTable {
|
2024-09-30 18:50:54 +08:00
|
|
|
const table = new LuaTable();
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
table.set(i + 1, arr[i]);
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return table;
|
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
static fromJSObject(obj: Record<string, JSValue>): LuaTable {
|
2024-09-30 18:50:54 +08:00
|
|
|
const table = new LuaTable();
|
|
|
|
for (const key in obj) {
|
|
|
|
table.set(key, obj[key]);
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
return table;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
2024-09-27 15:11:03 +08:00
|
|
|
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
|
|
|
|
|
2024-09-24 16:15:22 +08:00
|
|
|
export function luaSet(obj: any, key: any, value: any) {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (obj instanceof LuaTable) {
|
|
|
|
obj.set(key, value);
|
|
|
|
} else {
|
|
|
|
obj[key] = value;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function luaGet(obj: any, key: any): any {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (obj instanceof LuaTable) {
|
|
|
|
return obj.get(key);
|
|
|
|
} else {
|
|
|
|
return obj[key];
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function luaLen(obj: any): number {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (obj instanceof LuaTable) {
|
2024-10-03 23:55:51 +08:00
|
|
|
return obj.toJSArray().length;
|
2024-09-30 18:50:54 +08:00
|
|
|
} else if (Array.isArray(obj)) {
|
|
|
|
return obj.length;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
2024-09-24 16:15:22 +08:00
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
export function luaTypeOf(val: any): LuaType {
|
|
|
|
if (val === null || val === undefined) {
|
|
|
|
return "nil";
|
|
|
|
} else if (typeof val === "boolean") {
|
|
|
|
return "boolean";
|
|
|
|
} else if (typeof val === "number") {
|
|
|
|
return "number";
|
|
|
|
} else if (typeof val === "string") {
|
|
|
|
return "string";
|
|
|
|
} else if (val instanceof LuaTable) {
|
|
|
|
return "table";
|
|
|
|
} else if (Array.isArray(val)) {
|
|
|
|
return "table";
|
|
|
|
} else if (typeof val === "function") {
|
|
|
|
return "function";
|
|
|
|
} else {
|
|
|
|
return "userdata";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Both `break` and `return` are implemented by exception throwing
|
2024-09-27 15:11:03 +08:00
|
|
|
export class LuaBreak extends Error {
|
|
|
|
}
|
|
|
|
|
2024-09-27 23:09:25 +08:00
|
|
|
export class LuaReturn extends Error {
|
2024-09-30 18:50:54 +08:00
|
|
|
constructor(readonly values: LuaValue[]) {
|
|
|
|
super();
|
|
|
|
}
|
2024-09-27 23:09:25 +08:00
|
|
|
}
|
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
export class LuaRuntimeError extends Error {
|
|
|
|
constructor(
|
|
|
|
readonly message: string,
|
|
|
|
readonly astNode: { from?: number; to?: number },
|
|
|
|
) {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
toString() {
|
|
|
|
return `LuaRuntimeErrorr: ${this.message} at ${this.astNode.from}, ${this.astNode.to}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-27 15:11:03 +08:00
|
|
|
export function luaTruthy(value: any): boolean {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (value === undefined || value === null || value === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (value instanceof LuaTable) {
|
|
|
|
return value.length > 0;
|
|
|
|
}
|
|
|
|
return true;
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
|
|
|
|
2024-10-03 23:55:51 +08:00
|
|
|
export function luaToString(value: any): string {
|
|
|
|
// Implementation to be refined
|
|
|
|
return String(value);
|
|
|
|
}
|
|
|
|
|
2024-09-27 15:11:03 +08:00
|
|
|
export function jsToLuaValue(value: any): any {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (value instanceof LuaTable) {
|
|
|
|
return value;
|
|
|
|
} else if (Array.isArray(value)) {
|
2024-10-03 23:55:51 +08:00
|
|
|
return LuaTable.fromJSArray(value.map(jsToLuaValue));
|
2024-09-30 18:50:54 +08:00
|
|
|
} else if (typeof value === "object") {
|
2024-10-03 23:55:51 +08:00
|
|
|
return LuaTable.fromJSObject(value);
|
2024-09-30 18:50:54 +08:00
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function luaValueToJS(value: any): any {
|
2024-09-30 18:50:54 +08:00
|
|
|
if (value instanceof LuaTable) {
|
|
|
|
// This is a heuristic: if this table is used as an array, we return an array
|
|
|
|
if (value.length > 0) {
|
2024-10-03 23:55:51 +08:00
|
|
|
return value.toJSArray();
|
2024-09-27 15:11:03 +08:00
|
|
|
} else {
|
2024-10-03 23:55:51 +08:00
|
|
|
return value.toJSObject();
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|
2024-09-30 18:50:54 +08:00
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
2024-09-27 15:11:03 +08:00
|
|
|
}
|