You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

754 lines
24 KiB

import luaparse from 'luaparse'
type Block =
| luaparse.IfClause
| luaparse.ElseifClause
| luaparse.ElseClause
| luaparse.WhileStatement
| luaparse.DoStatement
| luaparse.RepeatStatement
| luaparse.FunctionDeclaration
| luaparse.ForNumericStatement
| luaparse.ForGenericStatement
| luaparse.Chunk
const isBlock = (node: luaparse.Node): node is Block =>
node.type === 'IfClause' ||
node.type === 'ElseifClause' ||
node.type === 'ElseClause' ||
node.type === 'WhileStatement' ||
node.type === 'DoStatement' ||
node.type === 'RepeatStatement' ||
node.type === 'FunctionDeclaration' ||
node.type === 'ForNumericStatement' ||
node.type === 'ForGenericStatement' ||
node.type === 'Chunk'
class MemExpr extends String {
public base: string | MemExpr
public property: string | MemExpr
public constructor(base: string | MemExpr, property: string | MemExpr) {
super()
this.base = base
this.property = property
}
public get(): string {
return `__lua.get(${this.base}, ${this.property})`
}
public set(value: string | MemExpr): string {
return `${this.base}.set(${this.property}, ${value})`
}
public setFn(): string {
return `${this.base}.setFn(${this.property})`
}
public toString(): string {
return this.get()
}
public valueOf(): string {
return this.get()
}
}
const UNI_OP_MAP = {
not: 'not',
'-': 'unm',
'~': 'bnot',
'#': 'len'
}
const BIN_OP_MAP = {
'+': 'add',
'-': 'sub',
'*': 'mul',
'%': 'mod',
'^': 'pow',
'/': 'div',
'//': 'idiv',
'&': 'band',
'|': 'bor',
'~': 'bxor',
'<<': 'shl',
'>>': 'shr',
'..': 'concat',
'~=': 'neq',
'==': 'eq',
'<': 'lt',
'<=': 'le',
'>': 'gt',
'>=': 'ge'
}
const generate = (node: luaparse.Node): string | MemExpr => {
switch (node.type) {
case 'LabelStatement': {
return `case '${node.label.name}': label = undefined`
}
case 'BreakStatement': {
return 'break'
}
case 'GotoStatement': {
return `label = '${node.label.name}'; continue`
}
case 'ReturnStatement': {
const args = parseExpressions(node.arguments)
return `return ${args}`
}
case 'IfStatement': {
const clauses = node.clauses.map(clause => generate(clause))
return clauses.join(' else ')
}
case 'IfClause':
case 'ElseifClause': {
const condition = expression(node.condition)
const body = parseBody(node)
return `if (__lua.bool(${condition})) {\n${body}\n}`
}
case 'ElseClause': {
const body = parseBody(node)
return `{\n${body}\n}`
}
case 'WhileStatement': {
const condition = expression(node.condition)
const body = parseBody(node)
return `while(${condition}) {\n${body}\n}`
}
case 'DoStatement': {
const body = parseBody(node)
return `\n${body}\n`
}
case 'RepeatStatement': {
const condition = expression(node.condition)
const body = parseBody(node)
return `do {\n${body}\n} while (!(${condition}))`
}
case 'LocalStatement': {
return parseAssignments(node)
}
case 'AssignmentStatement': {
return parseAssignments(node)
}
case 'CallStatement': {
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 `(${argsStr}) => {\n${body}${returnStr}\n}`
}
const params = node.parameters.map(param => {
if (param.type === 'VarargLiteral') {
return `$${nodeToScope.get(param)}.setVarargs(args)`
}
return `$${nodeToScope.get(param)}.setLocal('${param.name}', args.shift())`
})
// anonymous function
if (node.identifier === null) return getFuncDef(params)
if (node.identifier.type === 'Identifier') {
const scope = nodeToScope.get(node.identifier)
const setStr = node.isLocal ? 'setLocal' : 'set'
return `$${scope}.${setStr}('${node.identifier.name}', ${getFuncDef(params)})`
}
const identifier = generate(node.identifier) as MemExpr
if (node.identifier.indexer === ':') {
params.unshift(`$${nodeToScope.get(node)}.setLocal('self', args.shift())`)
}
return identifier.set(getFuncDef(params))
}
case 'ForNumericStatement': {
const varName = node.variable.name
const start = expression(node.start)
const end = expression(node.end)
const step = node.step === null ? 1 : expression(node.step)
const init = `let ${varName} = ${start}, end = ${end}, step = ${step}`
const cond = `step > 0 ? ${varName} <= end : ${varName} >= end`
const after = `${varName} += step`
const varInit = `$${nodeToScope.get(node.variable)}.setLocal('${varName}', ${varName});`
const body = parseBody(node, varInit)
return `for (${init}; ${cond}; ${after}) {\n${body}\n}`
}
case 'ForGenericStatement': {
const iterators = parseExpressions(node.iterators)
const variables = node.variables
.map((variable, index) => {
return `$${nodeToScope.get(variable)}.setLocal('${variable.name}', res[${index}])`
})
.join(';\n')
const body = parseBody(node, variables)
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 `'use strict'\nconst $0 = __lua.globalScope\nlet vars\nlet vals\nlet label\n\n${body}`
}
case 'Identifier': {
return `$${nodeToScope.get(node)}.get('${node.name}')`
}
case 'StringLiteral': {
const S = node.value
.replace(/([^\\])?\\(\d{1,3})/g, (_, pre, dec) => `${pre || ''}${String.fromCharCode(dec)}`)
.replace(/\\/g, '\\\\')
return `\`${S}\``
}
case 'NumericLiteral': {
return node.value.toString()
}
case 'BooleanLiteral': {
return node.value ? 'true' : 'false'
}
case 'NilLiteral': {
return 'undefined'
}
case 'VarargLiteral': {
return `$${nodeToScope.get(node)}.getVarargs()`
}
// inside TableConstructorExpression
// case 'TableKey': {}
// case 'TableKeyString': {}
// case 'TableValue': {}
case 'TableConstructorExpression': {
if (node.fields.length === 0) return 'new __lua.Table()'
const fields = node.fields
.map((field, index, arr) => {
if (field.type === 'TableKey') {
return `t.rawset(${generate(field.key)}, ${expression(field.value)})`
}
if (field.type === 'TableKeyString') {
return `t.rawset('${field.key.name}', ${expression(field.value)})`
}
if (field.type === 'TableValue') {
if (index === arr.length - 1 && ExpressionReturnsArray(field.value)) {
return `t.insert(...${generate(field.value)})`
}
return `t.insert(${expression(field.value)})`
}
})
.join(';\n')
return `new __lua.Table(t => {\n${fields}\n})`
}
case 'UnaryExpression': {
const operator = UNI_OP_MAP[node.operator]
const argument = expression(node.argument)
if (!operator) {
throw new Error(`Unhandled unary operator: ${node.operator}`)
}
return `__lua.${operator}(${argument})`
}
case 'BinaryExpression': {
const left = expression(node.left)
const right = expression(node.right)
const operator = BIN_OP_MAP[node.operator]
if (!operator) {
throw new Error(`Unhandled binary operator: ${node.operator}`)
}
return `__lua.${operator}(${left}, ${right})`
}
case 'LogicalExpression': {
const left = expression(node.left)
const right = expression(node.right)
const operator = node.operator
if (operator === 'and') {
return `(!__lua.bool(${left})?${left}:${right})`
}
if (operator === 'or') {
return `(__lua.bool(${left})?${left}:${right})`
}
throw new Error(`Unhandled logical operator: ${node.operator}`)
}
case 'MemberExpression': {
const base = expression(node.base)
return new MemExpr(base, `'${node.identifier.name}'`)
}
case 'IndexExpression': {
const base = expression(node.base)
const index = expression(node.index)
return new MemExpr(base, index)
}
case 'CallExpression':
case 'TableCallExpression':
case 'StringCallExpression': {
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}`)
}
}
const parseBody = (node: Block, header = ''): string => {
const scope = nodeToScope.get(node)
const scopeDef = scope === undefined ? '' : `const $${scope} = $${scopeToParentScope.get(scope)}.extend();`
const body = node.body.map(statement => generate(statement)).join(';\n')
const goto = nodeToGoto.get(node)
if (goto === undefined) return `${scopeDef}\n${header}\n${body}`
const gotoHeader = `L${goto}: do { switch(label) { case undefined:`
const gotoParent = gotoToParentGoto.get(goto)
const def = gotoParent === undefined ? '' : `break; default: continue L${gotoParent}\n`
const footer = `${def}} } while (label)`
return `${scopeDef}\n${gotoHeader}\n${header}\n${body}\n${footer}`
}
const expression = (node: luaparse.Expression): string | MemExpr => {
const v = generate(node)
if (ExpressionReturnsArray(node)) return `${v}[0]`
return v
}
const parseExpressions = (expressions: luaparse.Expression[]): string | MemExpr => {
// return the `array` directly instead of `[...array]`
if (expressions.length === 1 && ExpressionReturnsArray(expressions[0])) {
return generate(expressions[0])
}
return `[${parseExpressionList(expressions).join(', ')}]`
}
const parseExpressionList = (expressions: luaparse.Expression[]): (string | MemExpr)[] => {
return expressions.map((node, index, arr) => {
const value = generate(node)
if (ExpressionReturnsArray(node)) {
return index === arr.length - 1 ? `...${value}` : `${value}[0]`
}
return value
})
}
const parseAssignments = (node: luaparse.LocalStatement | luaparse.AssignmentStatement): string => {
const lines: (string | MemExpr)[] = []
const valFns: string[] = []
const useTempVar = node.variables.length > 1 && node.init.length > 0 && !node.init.every(isLiteral)
for (let i = 0; i < node.variables.length; i++) {
const K = node.variables[i]
const V = node.init[i]
const initStr =
// eslint-disable-next-line no-nested-ternary
useTempVar ? `vars[${i}]` : V === undefined ? 'undefined' : expression(V)
if (K.type === 'Identifier') {
const setStr = node.type === 'LocalStatement' ? 'setLocal' : 'set'
lines.push(`$${nodeToScope.get(K)}.${setStr}('${K.name}', ${initStr})`)
} else {
const name = generate(K) as MemExpr
if (useTempVar) {
lines.push(`vals[${valFns.length}](${initStr})`)
valFns.push(name.setFn())
} else {
lines.push(name.set(initStr))
}
}
}
// push remaining CallExpressions
for (let i = node.variables.length; i < node.init.length; i++) {
const init = node.init[i]
if (isCallExpression(init)) {
lines.push(generate(init))
}
}
if (useTempVar) {
lines.unshift(`vars = ${parseExpressions(node.init)}`)
if (valFns.length > 0) {
lines.unshift(`vals = [${valFns.join(', ')}]`)
}
}
return lines.join(';\n')
}
const isCallExpression = (
node: luaparse.Expression
): node is luaparse.CallExpression | luaparse.StringCallExpression | luaparse.TableCallExpression => {
return node.type === 'CallExpression' || node.type === 'StringCallExpression' || node.type === 'TableCallExpression'
}
const ExpressionReturnsArray = (node: luaparse.Expression): boolean => {
return isCallExpression(node) || node.type === 'VarargLiteral'
}
const isLiteral = (node: luaparse.Expression): boolean => {
return (
node.type === 'StringLiteral' ||
node.type === 'NumericLiteral' ||
node.type === 'BooleanLiteral' ||
node.type === 'NilLiteral' ||
node.type === 'TableConstructorExpression'
)
}
const checkGoto = (ast: luaparse.Chunk): void => {
const gotoInfo: {
type: 'local' | 'label' | 'goto'
name: string
scope: number
last?: boolean
}[] = []
let gotoScope = 0
const gotoScopeMap = new Map<number, number>()
const getNextGotoScope = (() => {
let id = 0
return () => {
id += 1
return id
}
})()
const check = (node: luaparse.Node): void => {
if (isBlock(node)) {
createGotoScope()
for (let i = 0; i < node.body.length; i++) {
const n = node.body[i]
switch (n.type) {
case 'LocalStatement': {
gotoInfo.push({
type: 'local',
name: n.variables[0].name,
scope: gotoScope
})
break
}
case 'LabelStatement': {
if (
gotoInfo.find(
node => node.type === 'label' && node.name === n.label.name && node.scope === gotoScope
)
) {
throw new Error(`label '${n.label.name}' already defined`)
}
gotoInfo.push({
type: 'label',
name: n.label.name,
scope: gotoScope,
last:
node.type !== 'RepeatStatement' &&
node.body.slice(i).every(n => n.type === 'LabelStatement')
})
break
}
case 'GotoStatement': {
gotoInfo.push({
type: 'goto',
name: n.label.name,
scope: gotoScope
})
break
}
case 'IfStatement': {
n.clauses.forEach(n => check(n))
break
}
default: {
check(n)
}
}
}
destroyGotoScope()
}
}
check(ast)
function createGotoScope(): void {
const parent = gotoScope
gotoScope = getNextGotoScope()
gotoScopeMap.set(gotoScope, parent)
}
function destroyGotoScope(): void {
gotoScope = gotoScopeMap.get(gotoScope)
}
for (let i = 0; i < gotoInfo.length; i++) {
const goto = gotoInfo[i]
if (goto.type === 'goto') {
const label = gotoInfo
.filter(node => node.type === 'label' && node.name === goto.name && node.scope <= goto.scope)
.sort((a, b) => Math.abs(goto.scope - a.scope) - Math.abs(goto.scope - b.scope))[0]
if (!label) {
throw new Error(`no visible label '${goto.name}' for <goto>`)
}
const labelI = gotoInfo.findIndex(n => n === label)
if (labelI > i) {
const locals = gotoInfo
.slice(i, labelI)
.filter(node => node.type === 'local' && node.scope === label.scope)
if (!label.last && locals.length > 0) {
throw new Error(`<goto ${goto.name}> jumps into the scope of local '${locals[0].name}'`)
}
}
}
}
}
const visitNode = (
node: luaparse.Node,
visitProp: (node: luaparse.Node, prevScope: number, prevGoto: number) => void,
nextScope: number,
isNewScope: boolean,
nextGoto: number
): void => {
const VP = (node: luaparse.Node | luaparse.Node[], partOfBlock = true): void => {
if (!node) return
const S = partOfBlock === false && isNewScope ? scopeToParentScope.get(nextScope) : nextScope
if (Array.isArray(node)) {
node.forEach(n => visitProp(n, S, nextGoto))
} else {
visitProp(node, S, nextGoto)
}
}
switch (node.type) {
case 'LocalStatement':
case 'AssignmentStatement':
VP(node.variables)
VP(node.init)
break
case 'UnaryExpression':
VP(node.argument)
break
case 'BinaryExpression':
case 'LogicalExpression':
VP(node.left)
VP(node.right)
break
case 'FunctionDeclaration':
VP(node.identifier, false)
VP(node.parameters)
VP(node.body)
break
case 'ForGenericStatement':
VP(node.variables)
VP(node.iterators, false)
VP(node.body)
break
case 'IfClause':
case 'ElseifClause':
case 'WhileStatement':
case 'RepeatStatement':
VP(node.condition, false)
/* fall through */
case 'Chunk':
case 'ElseClause':
case 'DoStatement':
VP(node.body)
// VK(node.globals)
// VK(node.comments)
break
case 'ForNumericStatement':
VP(node.variable)
VP(node.start, false)
VP(node.end, false)
VP(node.step, false)
VP(node.body)
break
case 'ReturnStatement':
VP(node.arguments)
break
case 'IfStatement':
VP(node.clauses)
break
case 'MemberExpression':
VP(node.base)
VP(node.identifier)
break
case 'IndexExpression':
VP(node.base)
VP(node.index)
break
case 'LabelStatement':
VP(node.label)
break
case 'CallStatement':
VP(node.expression)
break
case 'GotoStatement':
VP(node.label)
break
case 'TableConstructorExpression':
VP(node.fields)
break
case 'TableKey':
case 'TableKeyString':
VP(node.key)
/* fall through */
case 'TableValue':
VP(node.value)
break
case 'CallExpression':
VP(node.base)
VP(node.arguments)
break
case 'TableCallExpression':
VP(node.base)
VP(node.arguments)
break
case 'StringCallExpression':
VP(node.base)
VP(node.argument)
// break
// case 'Identifier':
// case 'NumericLiteral':
// case 'BooleanLiteral':
// case 'StringLiteral':
// case 'NilLiteral':
// case 'VarargLiteral':
// case 'BreakStatement':
// case 'Comment':
// break
// default:
// throw new Error(`Unhandled ${node.type}`)
}
}
const scopeToParentScope = new Map<number, number>()
const nodeToScope = new Map<luaparse.Node, number>()
const gotoToParentGoto = new Map<number, number>()
const nodeToGoto = new Map<luaparse.Node, number>()
const setExtraInfo = (ast: luaparse.Chunk): void => {
let scopeID = 0
let gotoID = 0
const visitProp = (node: luaparse.Node, prevScope: number, prevGoto: number): void => {
let nextScope = prevScope
let nextGoto = prevGoto
if (isBlock(node)) {
// set scope info
if (
node.body.findIndex(
n => n.type === 'LocalStatement' || (n.type === 'FunctionDeclaration' && n.isLocal)
) !== -1 ||
(node.type === 'FunctionDeclaration' &&
(node.parameters.length > 0 || (node.identifier && node.identifier.type === 'MemberExpression'))) ||
node.type === 'ForNumericStatement' ||
node.type === 'ForGenericStatement'
) {
scopeID += 1
nextScope = scopeID
nodeToScope.set(node, scopeID)
scopeToParentScope.set(scopeID, prevScope)
}
// set goto info
if (node.body.findIndex(s => s.type === 'LabelStatement' || s.type === 'GotoStatement') !== -1) {
nextGoto = gotoID
nodeToGoto.set(node, gotoID)
if (node.type !== 'Chunk' && node.type !== 'FunctionDeclaration') {
gotoToParentGoto.set(gotoID, prevGoto)
}
gotoID += 1
}
}
// set scope info
else if (node.type === 'Identifier' || node.type === 'VarargLiteral') {
nodeToScope.set(node, prevScope)
}
visitNode(node, visitProp, nextScope, prevScope !== nextScope, nextGoto)
}
visitProp(ast, scopeID, gotoID)
}
const parse = (data: string): string => {
const ast = luaparse.parse(data.replace(/^#.*/, ''), {
scope: false,
comments: false,
2 years ago
luaVersion: '5.3',
encodingMode: 'x-user-defined'
})
checkGoto(ast)
setExtraInfo(ast)
return generate(ast).toString()
}
export { parse }