diff --git a/.gitignore b/.gitignore index 3a8ec2b..6f32772 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist -package-lock.json \ No newline at end of file +package-lock.json +.idea \ No newline at end of file diff --git a/src/Thread.ts b/src/Thread.ts index 1ac255b..9bf734d 100644 --- a/src/Thread.ts +++ b/src/Thread.ts @@ -1,8 +1,6 @@ import { LuaError } from './LuaError' import { LuaType } from './utils' -export type ThreadStatus = 'running' | 'suspended' | 'dead' - type Gen = Generator type GenFn = (...args: LuaType[]) => Gen @@ -46,9 +44,10 @@ class Thread { } // eslint-disable-next-line @typescript-eslint/no-empty-function -const mainThread = new Thread((function* () {}) as unknown as GenFn) +const mainThread = new Thread((function*() {} as unknown) as GenFn) mainThread.status = 'running' Thread.main = mainThread Thread.current = mainThread export { Thread } +export type ThreadStatus = 'running' | 'suspended' | 'dead' diff --git a/src/index.ts b/src/index.ts index 4ebd2e4..4d7b72f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,36 +7,36 @@ import { Table } from './Table' import { LuaError } from './LuaError' import { libMath } from './lib/math' import { libTable } from './lib/table' -import { libString, metatable as stringMetatable } from './lib/string' -import { getLibOS } from './lib/os' -import { getLibPackage } from './lib/package' -import { libCoroutine } from './lib/coroutine' -import { LuaType, ensureArray, Config } from './utils' -import { Thread } from './Thread' +import { libString, metatable as stringMetatable } from './lib/string' +import { getLibOS } from './lib/os' +import { getLibPackage } from './lib/package' +import { libCoroutine } from './lib/coroutine' +import { LuaType, ensureArray, Config } from './utils' +import { Thread } from './Thread' import { parse as parseScript } from './parser' interface Script { exec: () => LuaType } -const call = (f: Function | Table | Thread, ...args: LuaType[]): LuaType[] => { - if (f instanceof Thread) return f.resume(...args) - - if (f instanceof Function) { - const res = f(...args) - if (res && typeof res.next === 'function') { - let r = res.next() - while (!r.done) r = res.next() - return ensureArray(r.value) - } - return ensureArray(res as LuaType) - } - - const mm = f instanceof Table && f.getMetaMethod('__call') - if (mm) return ensureArray(mm(f, ...args)) - - throw new LuaError(`attempt to call an uncallable type`) -} +const call = (f: Function | Table | Thread, ...args: LuaType[]): LuaType[] => { + if (f instanceof Thread) return f.resume(...args) + + if (f instanceof Function) { + const res = f(...args) + if (res && typeof res.next === 'function') { + let r = res.next() + while (!r.done) r = res.next() + return ensureArray(r.value) + } + return ensureArray(res as LuaType) + } + + const mm = f instanceof Table && f.getMetaMethod('__call') + if (mm) return ensureArray(mm(f, ...args)) + + throw new LuaError(`attempt to call an uncallable type`) +} const stringTable = new Table() stringTable.metatable = stringMetatable @@ -49,22 +49,22 @@ const get = (t: Table | string, v: LuaType): LuaType => { } const execChunk = (_G: Table, chunk: string, chunkName?: string): LuaType[] => { - const exec = new Function(`return ${chunk}`)() as (lua: unknown) => Generator - const globalScope = new Scope(_G.strValues).extend() - if (chunkName) globalScope.setVarargs([chunkName]) - const iterator = exec({ - globalScope, - ...operators, - Table, - call, - get - }) - let res = iterator.next() - while (!res.done) { - res = iterator.next() - } - return res.value === undefined ? [undefined] : res.value -} + const exec = new Function(`return ${chunk}`)() as (lua: unknown) => Generator + const globalScope = new Scope(_G.strValues).extend() + if (chunkName) globalScope.setVarargs([chunkName]) + const iterator = exec({ + globalScope, + ...operators, + Table, + call, + get + }) + let res = iterator.next() + while (!res.done) { + res = iterator.next() + } + return res.value === undefined ? [undefined] : res.value +} function createEnv( config: Config = {} @@ -97,9 +97,9 @@ function createEnv( loadLib('package', libPackage) loadLib('math', libMath) loadLib('table', libTable) - loadLib('string', libString) - loadLib('os', getLibOS(cfg)) - loadLib('coroutine', libCoroutine) + loadLib('string', libString) + loadLib('os', getLibOS(cfg)) + loadLib('coroutine', libCoroutine) _G.rawset('require', _require) diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 4d3ff66..2d438fb 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -1,407 +1,408 @@ -import { parse } from '../parser' -import { Table } from '../Table' -import { LuaError } from '../LuaError' -import { - LuaType, - Config, - type, - tostring, - posrelat, - coerceToNumber, - coerceToString, - coerceToBoolean, - coerceArgToNumber, - coerceArgToString, - coerceArgToTable, - hasOwnProperty -} from '../utils' -import { metatable as stringMetatable } from './string' - -const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - -function ipairsIterator(table: Table, index: number): LuaType[] { - if (index === undefined) { - throw new LuaError('Bad argument #2 to ipairs() iterator') - } - - const nextIndex = index + 1 - const numValues = table.numValues - - if (!numValues[nextIndex] || numValues[nextIndex] === undefined) return undefined - return [nextIndex, numValues[nextIndex]] -} - -const _VERSION = 'Lua 5.3' - -function assert(v: LuaType, m?: LuaType): [unknown, unknown] { - if (coerceToBoolean(v)) return [v, m] - - const msg = m === undefined ? 'Assertion failed!' : coerceArgToString(m, 'assert', 2) - throw new LuaError(msg) -} - -function collectgarbage(): [] { - // noop - return [] -} - -function error(message: LuaType): void { - const msg = coerceArgToString(message, 'error', 1) - throw new LuaError(msg) -} - -/** - * If object does not have a metatable, returns nil. - * Otherwise, if the object's metatable has a __metatable field, returns the associated value. - * Otherwise, returns the metatable of the given object. - */ -function getmetatable(table: LuaType): Table { - if (table instanceof Table && table.metatable) { - const mm = table.metatable.rawget('__metatable') as Table - return mm ? mm : table.metatable - } - if (typeof table === 'string') { - return stringMetatable - } -} - -/** - * Returns three values (an iterator function, the table t, and 0) so that the construction - * - * `for i,v in ipairs(t) do body end` - * - * will iterate over the key–value pairs (1,t[1]), (2,t[2]), ..., up to the first nil value. - */ -function ipairs(t: LuaType): [Function, Table, number] { - const table = coerceArgToTable(t, 'ipairs', 1) - const mm = table.getMetaMethod('__pairs') || table.getMetaMethod('__ipairs') - return mm ? mm(table).slice(0, 3) : [ipairsIterator, table, 0] -} - -/** - * Allows a program to traverse all fields of a table. - * Its first argument is a table and its second argument is an index in this table. - * next returns the next index of the table and its associated value. - * When called with nil as its second argument, next returns an initial index and its associated value. - * When called with the last index, or with nil in an empty table, next returns nil. - * If the second argument is absent, then it is interpreted as nil. - * In particular, you can use next(t) to check whether a table is empty. - * - * The order in which the indices are enumerated is not specified, even for numeric indices. - * (To traverse a table in numerical order, use a numerical for.) - * - * The behavior of next is undefined if, during the traversal, you assign any value to a non-existent field in the table. - * You may however modify existing fields. In particular, you may clear existing fields. - */ -function next(table: LuaType, index?: LuaType): [number | string, LuaType] { - const TABLE = coerceArgToTable(table, 'next', 1) - - // SLOOOOOOOW... - let found = index === undefined - - if (found || (typeof index === 'number' && index > 0)) { - const numValues = TABLE.numValues - const keys = Object.keys(numValues) - let i = 1 - - if (!found) { - const I = keys.indexOf(`${index}`) - if (I >= 0) { - found = true - i += I - } - } - - if (found) { - for (i; keys[i] !== undefined; i++) { - const key = Number(keys[i]) - const value = numValues[key] - if (value !== undefined) return [key, value] - } - } - } - - for (const i in TABLE.strValues) { - if (hasOwnProperty(TABLE.strValues, i)) { - if (!found) { - if (i === index) found = true - } else if (TABLE.strValues[i] !== undefined) { - return [i, TABLE.strValues[i]] - } - } - } - - for (const i in TABLE.keys) { - if (hasOwnProperty(TABLE.keys, i)) { - const key = TABLE.keys[i] - - if (!found) { - if (key === index) found = true - } else if (TABLE.values[i] !== undefined) { - return [key, TABLE.values[i]] - } - } - } -} - -/** - * If t has a metamethod __pairs, calls it with t as argument and returns the first three results from the call. - * - * Otherwise, returns three values: the next function, the table t, and nil, so that the construction - * - * `for k,v in pairs(t) do body end` - * - * will iterate over all key–value pairs of table t. - * - * See function next for the caveats of modifying the table during its traversal. - */ -function pairs(t: LuaType): [Function, Table, undefined] { - const table = coerceArgToTable(t, 'pairs', 1) - const mm = table.getMetaMethod('__pairs') - return mm ? mm(table).slice(0, 3) : [next, table, undefined] -} - -/** - * Calls function f with the given arguments in protected mode. - * This means that any error inside f is not propagated; - * instead, pcall catches the error and returns a status code. - * Its first result is the status code (a boolean), which is true if the call succeeds without errors. - * In such case, pcall also returns all results from the call, after this first result. - * In case of any error, pcall returns false plus the error message. - */ -function pcall(f: LuaType, ...args: LuaType[]): [false, string] | [true, ...LuaType[]] { - if (typeof f !== 'function') { - throw new LuaError('Attempt to call non-function') - } - - try { - return [true, ...f(...args)] - } catch (e) { - return [false, e && e.toString()] - } -} - -/** - * Checks whether v1 is equal to v2, without invoking the __eq metamethod. Returns a boolean. - */ -function rawequal(v1: LuaType, v2: LuaType): boolean { - return v1 === v2 -} - -/** - * Gets the real value of table[index], without invoking the __index metamethod. - * table must be a table; index may be any value. - */ -function rawget(table: LuaType, index: LuaType): LuaType { - const TABLE = coerceArgToTable(table, 'rawget', 1) - return TABLE.rawget(index) -} - -/** - * Returns the length of the object v, which must be a table or a string, without invoking the __len metamethod. - * Returns an integer. - */ -function rawlen(v: LuaType): number { - if (v instanceof Table) return v.getn() - - if (typeof v === 'string') return v.length - - throw new LuaError('attempt to get length of an unsupported value') -} - -/** - * Sets the real value of table[index] to value, without invoking the __newindex metamethod. - * table must be a table, index any value different from nil and NaN, and value any Lua value. - * - * This function returns table. - */ -function rawset(table: LuaType, index: LuaType, value: LuaType): Table { - const TABLE = coerceArgToTable(table, 'rawset', 1) - if (index === undefined) throw new LuaError('table index is nil') - - TABLE.rawset(index, value) - return TABLE -} - -/** - * If index is a number, returns all arguments after argument number index; - * a negative number indexes from the end (-1 is the last argument). - * Otherwise, index must be the string "#", and select returns the total number of extra arguments it received. - */ -function select(index: number | '#', ...args: LuaType[]): LuaType[] | number { - if (index === '#') { - return args.length - } - - if (typeof index === 'number') { - const pos = posrelat(Math.trunc(index), args.length) - return args.slice(pos - 1) - } - - throw new LuaError(`bad argument #1 to 'select' (number expected, got ${type(index)})`) -} - -/** - * Sets the metatable for the given table. - * (To change the metatable of other types from Lua code,you must use the debug library (§6.10).) - * If metatable is nil, removes the metatable of the given table. - * If the original metatable has a __metatable field, raises an error. - * - * This function returns table. - */ -function setmetatable(table: LuaType, metatable: LuaType): Table { - const TABLE = coerceArgToTable(table, 'setmetatable', 1) - - if (TABLE.metatable && TABLE.metatable.rawget('__metatable')) { - throw new LuaError('cannot change a protected metatable') - } - - TABLE.metatable = metatable === null || metatable === undefined ? null : coerceArgToTable(metatable, 'setmetatable', 2) - return TABLE -} - -/** - * When called with no base, tonumber tries to convert its argument to a number. - * If the argument is already a number or a string convertible to a number, - * then tonumber returns this number; otherwise, it returns nil. - * - * The conversion of strings can result in integers or floats, - * according to the lexical conventions of Lua (see §3.1). - * (The string may have leading and trailing spaces and a sign.) - * - * When called with base, then e must be a string to be interpreted as an integer numeral in that base. - * The base may be any integer between 2 and 36, inclusive. - * In bases above 10, the letter 'A' (in either upper or lower case) represents 10, - * 'B' represents 11, and so forth, with 'Z' representing 35. - * If the string e is not a valid numeral in the given base, the function returns nil. - */ -function tonumber(e: LuaType, base: LuaType): number { - const E = coerceToString(e).trim() - const BASE = base === undefined ? 10 : coerceArgToNumber(base, 'tonumber', 2) - - if (BASE !== 10 && E === 'nil') { - throw new LuaError("bad argument #1 to 'tonumber' (string expected, got nil)") - } - - if (BASE < 2 || BASE > 36) { - throw new LuaError(`bad argument #2 to 'tonumber' (base out of range)`) - } - - if (E === '') return - if (BASE === 10) return coerceToNumber(E) - - const pattern = new RegExp(`^${BASE === 16 ? '(0x)?' : ''}[${CHARS.substr(0, BASE)}]*$`, 'gi') - - if (!pattern.test(E)) return // Invalid - return parseInt(E, BASE) -} - -/** - * This function is similar to pcall, except that it sets a new message handler msgh. - */ -function xpcall(f: LuaType, msgh: LuaType, ...args: LuaType[]): [false, string] | [true, ...LuaType[]] { - if (typeof f !== 'function' || typeof msgh !== 'function') { - throw new LuaError('Attempt to call non-function') - } - - try { - return [true, ...f(...args)] - } catch (e) { - return [false, msgh(e)[0]] - } -} - -function createG(cfg: Config, execChunk: (_G: Table, chunk: string) => LuaType[]): Table { - function print(...args: LuaType[]): void { - const output = args.map(arg => tostring(arg)).join('\t') - cfg.stdout(output) - } - - function load( - chunk: LuaType, - _chunkname?: string, - _mode?: 'b' | 't' | 'bt', - env?: Table - ): [undefined, string] | (() => LuaType[]) { - let C = '' - if (chunk instanceof Function) { - let ret = ' ' - while (ret !== '' && ret !== undefined) { - C += ret - ret = chunk()[0] - } - } else { - C = coerceArgToString(chunk, 'load', 1) - } - - let parsed: string - try { - parsed = parse(C) - } catch (e) { - return [undefined, e.message] - } - - return () => execChunk(env || _G, parsed) - } - - function dofile(filename?: LuaType): LuaType[] { - const res = loadfile(filename) - - if (Array.isArray(res) && res[0] === undefined) { - throw new LuaError(res[1]) - } - - const exec = res as () => LuaType[] - return exec() - } - - function loadfile( - filename?: LuaType, - mode?: 'b' | 't' | 'bt', - env?: Table - ): [undefined, string] | (() => LuaType[]) { - const FILENAME = filename === undefined ? cfg.stdin : coerceArgToString(filename, 'loadfile', 1) - - if (!cfg.fileExists) { - throw new LuaError('loadfile requires the config.fileExists function') - } - - if (!cfg.fileExists(FILENAME)) return [undefined, 'file not found'] - - if (!cfg.loadFile) { - throw new LuaError('loadfile requires the config.loadFile function') - } - - return load(cfg.loadFile(FILENAME), FILENAME, mode, env) - } - - const _G = new Table({ - _VERSION, - assert, - dofile, - collectgarbage, - error, - getmetatable, - ipairs, - load, - loadfile, - next, - pairs, - pcall, - print, - rawequal, - rawget, - rawlen, - rawset, - select, - setmetatable, - tonumber, - tostring, - type, - xpcall - }) - - return _G -} - -export { tostring, createG } +import { parse } from '../parser' +import { Table } from '../Table' +import { LuaError } from '../LuaError' +import { + LuaType, + Config, + type, + tostring, + posrelat, + coerceToNumber, + coerceToString, + coerceToBoolean, + coerceArgToNumber, + coerceArgToString, + coerceArgToTable, + hasOwnProperty +} from '../utils' +import { metatable as stringMetatable } from './string' + +const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + +function ipairsIterator(table: Table, index: number): LuaType[] { + if (index === undefined) { + throw new LuaError('Bad argument #2 to ipairs() iterator') + } + + const nextIndex = index + 1 + const numValues = table.numValues + + if (!numValues[nextIndex] || numValues[nextIndex] === undefined) return undefined + return [nextIndex, numValues[nextIndex]] +} + +const _VERSION = 'Lua 5.3' + +function assert(v: LuaType, m?: LuaType): [unknown, unknown] { + if (coerceToBoolean(v)) return [v, m] + + const msg = m === undefined ? 'Assertion failed!' : coerceArgToString(m, 'assert', 2) + throw new LuaError(msg) +} + +function collectgarbage(): [] { + // noop + return [] +} + +function error(message: LuaType): void { + const msg = coerceArgToString(message, 'error', 1) + throw new LuaError(msg) +} + +/** + * If object does not have a metatable, returns nil. + * Otherwise, if the object's metatable has a __metatable field, returns the associated value. + * Otherwise, returns the metatable of the given object. + */ +function getmetatable(table: LuaType): Table { + if (table instanceof Table && table.metatable) { + const mm = table.metatable.rawget('__metatable') as Table + return mm ? mm : table.metatable + } + if (typeof table === 'string') { + return stringMetatable + } +} + +/** + * Returns three values (an iterator function, the table t, and 0) so that the construction + * + * `for i,v in ipairs(t) do body end` + * + * will iterate over the key–value pairs (1,t[1]), (2,t[2]), ..., up to the first nil value. + */ +function ipairs(t: LuaType): [Function, Table, number] { + const table = coerceArgToTable(t, 'ipairs', 1) + const mm = table.getMetaMethod('__pairs') || table.getMetaMethod('__ipairs') + return mm ? mm(table).slice(0, 3) : [ipairsIterator, table, 0] +} + +/** + * Allows a program to traverse all fields of a table. + * Its first argument is a table and its second argument is an index in this table. + * next returns the next index of the table and its associated value. + * When called with nil as its second argument, next returns an initial index and its associated value. + * When called with the last index, or with nil in an empty table, next returns nil. + * If the second argument is absent, then it is interpreted as nil. + * In particular, you can use next(t) to check whether a table is empty. + * + * The order in which the indices are enumerated is not specified, even for numeric indices. + * (To traverse a table in numerical order, use a numerical for.) + * + * The behavior of next is undefined if, during the traversal, you assign any value to a non-existent field in the table. + * You may however modify existing fields. In particular, you may clear existing fields. + */ +function next(table: LuaType, index?: LuaType): [number | string, LuaType] { + const TABLE = coerceArgToTable(table, 'next', 1) + + // SLOOOOOOOW... + let found = index === undefined + + if (found || (typeof index === 'number' && index > 0)) { + const numValues = TABLE.numValues + const keys = Object.keys(numValues) + let i = 1 + + if (!found) { + const I = keys.indexOf(`${index}`) + if (I >= 0) { + found = true + i += I + } + } + + if (found) { + for (i; keys[i] !== undefined; i++) { + const key = Number(keys[i]) + const value = numValues[key] + if (value !== undefined) return [key, value] + } + } + } + + for (const i in TABLE.strValues) { + if (hasOwnProperty(TABLE.strValues, i)) { + if (!found) { + if (i === index) found = true + } else if (TABLE.strValues[i] !== undefined) { + return [i, TABLE.strValues[i]] + } + } + } + + for (const i in TABLE.keys) { + if (hasOwnProperty(TABLE.keys, i)) { + const key = TABLE.keys[i] + + if (!found) { + if (key === index) found = true + } else if (TABLE.values[i] !== undefined) { + return [key, TABLE.values[i]] + } + } + } +} + +/** + * If t has a metamethod __pairs, calls it with t as argument and returns the first three results from the call. + * + * Otherwise, returns three values: the next function, the table t, and nil, so that the construction + * + * `for k,v in pairs(t) do body end` + * + * will iterate over all key–value pairs of table t. + * + * See function next for the caveats of modifying the table during its traversal. + */ +function pairs(t: LuaType): [Function, Table, undefined] { + const table = coerceArgToTable(t, 'pairs', 1) + const mm = table.getMetaMethod('__pairs') + return mm ? mm(table).slice(0, 3) : [next, table, undefined] +} + +/** + * Calls function f with the given arguments in protected mode. + * This means that any error inside f is not propagated; + * instead, pcall catches the error and returns a status code. + * Its first result is the status code (a boolean), which is true if the call succeeds without errors. + * In such case, pcall also returns all results from the call, after this first result. + * In case of any error, pcall returns false plus the error message. + */ +function pcall(f: LuaType, ...args: LuaType[]): [false, string] | [true, ...LuaType[]] { + if (typeof f !== 'function') { + throw new LuaError('Attempt to call non-function') + } + + try { + return [true, ...f(...args)] + } catch (e) { + return [false, e && e.toString()] + } +} + +/** + * Checks whether v1 is equal to v2, without invoking the __eq metamethod. Returns a boolean. + */ +function rawequal(v1: LuaType, v2: LuaType): boolean { + return v1 === v2 +} + +/** + * Gets the real value of table[index], without invoking the __index metamethod. + * table must be a table; index may be any value. + */ +function rawget(table: LuaType, index: LuaType): LuaType { + const TABLE = coerceArgToTable(table, 'rawget', 1) + return TABLE.rawget(index) +} + +/** + * Returns the length of the object v, which must be a table or a string, without invoking the __len metamethod. + * Returns an integer. + */ +function rawlen(v: LuaType): number { + if (v instanceof Table) return v.getn() + + if (typeof v === 'string') return v.length + + throw new LuaError('attempt to get length of an unsupported value') +} + +/** + * Sets the real value of table[index] to value, without invoking the __newindex metamethod. + * table must be a table, index any value different from nil and NaN, and value any Lua value. + * + * This function returns table. + */ +function rawset(table: LuaType, index: LuaType, value: LuaType): Table { + const TABLE = coerceArgToTable(table, 'rawset', 1) + if (index === undefined) throw new LuaError('table index is nil') + + TABLE.rawset(index, value) + return TABLE +} + +/** + * If index is a number, returns all arguments after argument number index; + * a negative number indexes from the end (-1 is the last argument). + * Otherwise, index must be the string "#", and select returns the total number of extra arguments it received. + */ +function select(index: number | '#', ...args: LuaType[]): LuaType[] | number { + if (index === '#') { + return args.length + } + + if (typeof index === 'number') { + const pos = posrelat(Math.trunc(index), args.length) + return args.slice(pos - 1) + } + + throw new LuaError(`bad argument #1 to 'select' (number expected, got ${type(index)})`) +} + +/** + * Sets the metatable for the given table. + * (To change the metatable of other types from Lua code,you must use the debug library (§6.10).) + * If metatable is nil, removes the metatable of the given table. + * If the original metatable has a __metatable field, raises an error. + * + * This function returns table. + */ +function setmetatable(table: LuaType, metatable: LuaType): Table { + const TABLE = coerceArgToTable(table, 'setmetatable', 1) + + if (TABLE.metatable && TABLE.metatable.rawget('__metatable')) { + throw new LuaError('cannot change a protected metatable') + } + + TABLE.metatable = + metatable === null || metatable === undefined ? null : coerceArgToTable(metatable, 'setmetatable', 2) + return TABLE +} + +/** + * When called with no base, tonumber tries to convert its argument to a number. + * If the argument is already a number or a string convertible to a number, + * then tonumber returns this number; otherwise, it returns nil. + * + * The conversion of strings can result in integers or floats, + * according to the lexical conventions of Lua (see §3.1). + * (The string may have leading and trailing spaces and a sign.) + * + * When called with base, then e must be a string to be interpreted as an integer numeral in that base. + * The base may be any integer between 2 and 36, inclusive. + * In bases above 10, the letter 'A' (in either upper or lower case) represents 10, + * 'B' represents 11, and so forth, with 'Z' representing 35. + * If the string e is not a valid numeral in the given base, the function returns nil. + */ +function tonumber(e: LuaType, base: LuaType): number { + const E = coerceToString(e).trim() + const BASE = base === undefined ? 10 : coerceArgToNumber(base, 'tonumber', 2) + + if (BASE !== 10 && E === 'nil') { + throw new LuaError("bad argument #1 to 'tonumber' (string expected, got nil)") + } + + if (BASE < 2 || BASE > 36) { + throw new LuaError(`bad argument #2 to 'tonumber' (base out of range)`) + } + + if (E === '') return + if (BASE === 10) return coerceToNumber(E) + + const pattern = new RegExp(`^${BASE === 16 ? '(0x)?' : ''}[${CHARS.substr(0, BASE)}]*$`, 'gi') + + if (!pattern.test(E)) return // Invalid + return parseInt(E, BASE) +} + +/** + * This function is similar to pcall, except that it sets a new message handler msgh. + */ +function xpcall(f: LuaType, msgh: LuaType, ...args: LuaType[]): [false, string] | [true, ...LuaType[]] { + if (typeof f !== 'function' || typeof msgh !== 'function') { + throw new LuaError('Attempt to call non-function') + } + + try { + return [true, ...f(...args)] + } catch (e) { + return [false, msgh(e)[0]] + } +} + +function createG(cfg: Config, execChunk: (_G: Table, chunk: string) => LuaType[]): Table { + function print(...args: LuaType[]): void { + const output = args.map(arg => tostring(arg)).join('\t') + cfg.stdout(output) + } + + function load( + chunk: LuaType, + _chunkname?: string, + _mode?: 'b' | 't' | 'bt', + env?: Table + ): [undefined, string] | (() => LuaType[]) { + let C = '' + if (chunk instanceof Function) { + let ret = ' ' + while (ret !== '' && ret !== undefined) { + C += ret + ret = chunk()[0] + } + } else { + C = coerceArgToString(chunk, 'load', 1) + } + + let parsed: string + try { + parsed = parse(C) + } catch (e) { + return [undefined, e.message] + } + + return () => execChunk(env || _G, parsed) + } + + function dofile(filename?: LuaType): LuaType[] { + const res = loadfile(filename) + + if (Array.isArray(res) && res[0] === undefined) { + throw new LuaError(res[1]) + } + + const exec = res as () => LuaType[] + return exec() + } + + function loadfile( + filename?: LuaType, + mode?: 'b' | 't' | 'bt', + env?: Table + ): [undefined, string] | (() => LuaType[]) { + const FILENAME = filename === undefined ? cfg.stdin : coerceArgToString(filename, 'loadfile', 1) + + if (!cfg.fileExists) { + throw new LuaError('loadfile requires the config.fileExists function') + } + + if (!cfg.fileExists(FILENAME)) return [undefined, 'file not found'] + + if (!cfg.loadFile) { + throw new LuaError('loadfile requires the config.loadFile function') + } + + return load(cfg.loadFile(FILENAME), FILENAME, mode, env) + } + + const _G = new Table({ + _VERSION, + assert, + dofile, + collectgarbage, + error, + getmetatable, + ipairs, + load, + loadfile, + next, + pairs, + pcall, + print, + rawequal, + rawget, + rawlen, + rawset, + select, + setmetatable, + tonumber, + tostring, + type, + xpcall + }) + + return _G +} + +export { tostring, createG } diff --git a/src/operators.ts b/src/operators.ts index 5a91df4..5096d4b 100644 --- a/src/operators.ts +++ b/src/operators.ts @@ -38,13 +38,13 @@ const bool = (value: LuaType): boolean => coerceToBoolean(value) // logical const and = (l: () => LuaType, r: () => LuaType): LuaType => { - const lv = l() - return coerceToBoolean(lv) ? r() : lv + const lv = l() + return coerceToBoolean(lv) ? r() : lv } const or = (l: () => LuaType, r: () => LuaType): LuaType => { - const lv = l() - return coerceToBoolean(lv) ? lv : r() + const lv = l() + return coerceToBoolean(lv) ? lv : r() } // unary @@ -191,7 +191,7 @@ const operators = { lt, le, gt, - ge, + ge } export { operators } diff --git a/src/parser.ts b/src/parser.ts index ecbe020..f08b8cc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -149,15 +149,15 @@ const generate = (node: luaparse.Node): string | MemExpr => { return generate(node.expression) } - case 'FunctionDeclaration': { - const getFuncDef = (params: string[]): string => { - const paramStr = params.join(';\n') - const body = parseBody(node, paramStr) - const argsStr = params.length === 0 ? '' : '...args' - const returnStr = - node.body.findIndex(node => node.type === 'ReturnStatement') === -1 ? '\nreturn []' : '' - return `function* (${argsStr}) {\n${body}${returnStr}\n}` - } + case 'FunctionDeclaration': { + const getFuncDef = (params: string[]): string => { + const paramStr = params.join(';\n') + const body = parseBody(node, paramStr) + const argsStr = params.length === 0 ? '' : '...args' + const returnStr = + node.body.findIndex(node => node.type === 'ReturnStatement') === -1 ? '\nreturn []' : '' + return `function* (${argsStr}) {\n${body}${returnStr}\n}` + } const params = node.parameters.map(param => { if (param.type === 'VarargLiteral') { @@ -210,10 +210,10 @@ const generate = (node: luaparse.Node): string | MemExpr => { return `for (let [iterator, table, next] = ${iterators}, res = __lua.call(iterator, table, next); res[0] !== undefined; res = __lua.call(iterator, table, res[0])) {\n${body}\n}` } - case 'Chunk': { - const body = parseBody(node) - return `function* (__lua) {\n'use strict'\nconst $0 = __lua.globalScope\nlet vars\nlet vals\nlet label\n\n${body}\n}` - } + case 'Chunk': { + const body = parseBody(node) + return `function* (__lua) {\n'use strict'\nconst $0 = __lua.globalScope\nlet vars\nlet vals\nlet label\n\n${body}\n}` + } case 'Identifier': { return `$${nodeToScope.get(node)}.get('${node.name}')` @@ -321,34 +321,34 @@ const generate = (node: luaparse.Node): string | MemExpr => { return new MemExpr(base, index) } - case 'CallExpression': - case 'TableCallExpression': - case 'StringCallExpression': { - if ( - node.type === 'CallExpression' && - node.base.type === 'MemberExpression' && - node.base.base.type === 'Identifier' && - node.base.base.name === 'coroutine' && - node.base.identifier.type === 'Identifier' && - node.base.identifier.name === 'yield' && - node.base.indexer === '.' - ) { - const args = parseExpressions(node.arguments) - return `yield ${args}` - } - - const functionName = expression(node.base) - const args = - node.type === 'CallExpression' - ? parseExpressionList(node.arguments).join(', ') - : expression(node.type === 'TableCallExpression' ? node.arguments : node.argument) - - if (functionName instanceof MemExpr && node.base.type === 'MemberExpression' && node.base.indexer === ':') { - return `__lua.call(${functionName}, ${functionName.base}, ${args})` - } - - return `__lua.call(${functionName}, ${args})` - } + case 'CallExpression': + case 'TableCallExpression': + case 'StringCallExpression': { + if ( + node.type === 'CallExpression' && + node.base.type === 'MemberExpression' && + node.base.base.type === 'Identifier' && + node.base.base.name === 'coroutine' && + node.base.identifier.type === 'Identifier' && + node.base.identifier.name === 'yield' && + node.base.indexer === '.' + ) { + const args = parseExpressions(node.arguments) + return `yield ${args}` + } + + const functionName = expression(node.base) + const args = + node.type === 'CallExpression' + ? parseExpressionList(node.arguments).join(', ') + : expression(node.type === 'TableCallExpression' ? node.arguments : node.argument) + + if (functionName instanceof MemExpr && node.base.type === 'MemberExpression' && node.base.indexer === ':') { + return `__lua.call(${functionName}, ${functionName.base}, ${args})` + } + + return `__lua.call(${functionName}, ${args})` + } default: throw new Error(`No generator found for: ${node.type}`) diff --git a/src/utils.ts b/src/utils.ts index 5810aa2..ee2a8e6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,8 @@ -import { LuaError } from './LuaError' -import { Table } from './Table' -import { Thread } from './Thread' +import { LuaError } from './LuaError' +import { Table } from './Table' +import { Thread } from './Thread' -type LuaType = undefined | boolean | number | string | Function | Table | Thread // userdata +type LuaType = undefined | boolean | number | string | Function | Table | Thread // userdata interface Config { LUA_PATH?: string @@ -19,7 +19,7 @@ const FLOATING_POINT_PATTERN = /^[-+]?[0-9]*\.?([0-9]+([eE][-+]?[0-9]+)?)?$/ /** Pattern to identify a hex string value that can validly be converted to a number in Lua */ const HEXIDECIMAL_CONSTANT_PATTERN = /^(-)?0x([0-9a-fA-F]*)\.?([0-9a-fA-F]*)$/ -function type(v: LuaType): 'string' | 'number' | 'boolean' | 'function' | 'nil' | 'table' | 'thread' { +function type(v: LuaType): 'string' | 'number' | 'boolean' | 'function' | 'nil' | 'table' | 'thread' { const t = typeof v switch (t) { @@ -32,12 +32,12 @@ function type(v: LuaType): 'string' | 'number' | 'boolean' | 'function' | 'nil' case 'function': return t - case 'object': - if (v instanceof Table) return 'table' - if (v instanceof Function) return 'function' - if (v instanceof Thread) return 'thread' - } -} + case 'object': + if (v instanceof Table) return 'table' + if (v instanceof Function) return 'function' + if (v instanceof Thread) return 'thread' + } +} function tostring(v: LuaType): string { if (v instanceof Table) { @@ -47,13 +47,13 @@ function tostring(v: LuaType): string { return valToStr(v, 'table: 0x') } - if (v instanceof Function) { - return valToStr(v, 'function: 0x') - } - - if (v instanceof Thread) { - return valToStr(v, 'thread: 0x') - } + if (v instanceof Function) { + return valToStr(v, 'function: 0x') + } + + if (v instanceof Thread) { + return valToStr(v, 'thread: 0x') + } return coerceToString(v) @@ -190,29 +190,29 @@ function coerceArgToTable(value: LuaType, funcName: string, index: number): Tabl } } -function coerceArgToFunction(value: LuaType, funcName: string, index: number): Function { - if (value instanceof Function) { - return value - } else { - const typ = type(value) - throw new LuaError(`bad argument #${index} to '${funcName}' (function expected, got ${typ})`) - } -} - -function coerceArgToThread(value: LuaType, funcName: string, index: number): Thread { - if (value instanceof Thread) { - return value - } - const typ = type(value) - throw new LuaError(`bad argument #${index} to '${funcName}' (thread expected, got ${typ})`) -} +function coerceArgToFunction(value: LuaType, funcName: string, index: number): Function { + if (value instanceof Function) { + return value + } else { + const typ = type(value) + throw new LuaError(`bad argument #${index} to '${funcName}' (function expected, got ${typ})`) + } +} + +function coerceArgToThread(value: LuaType, funcName: string, index: number): Thread { + if (value instanceof Thread) { + return value + } + const typ = type(value) + throw new LuaError(`bad argument #${index} to '${funcName}' (thread expected, got ${typ})`) +} const ensureArray = (value: T | T[]): T[] => (value instanceof Array ? value : [value]) const hasOwnProperty = (obj: Record | unknown[], key: string | number): boolean => Object.prototype.hasOwnProperty.call(obj, key) -export { +export { LuaType, Config, type, @@ -223,9 +223,9 @@ export { coerceToString, coerceArgToNumber, coerceArgToString, - coerceArgToTable, - coerceArgToFunction, - coerceArgToThread, + coerceArgToTable, + coerceArgToFunction, + coerceArgToThread, ensureArray, hasOwnProperty }