将一种语言转换为另一种语言的一般方法是什么?

What is a general approach for transpiling one language to another?

我想将 JavaScript 转换成 LinkScript。我是这样开始的:

const acorn = require('acorn')
const fs = require('fs')

const input = fs.readFileSync('./tmp/parse.in.js', 'utf-8')

const jst = acorn.parse(input, {
  ecmaVersion: 2021,
  sourceType: 'module'
})

fs.writeFileSync('tmp/parse.out.js.json', JSON.stringify(jst, null, 2))

const linkScriptText = generateLinkScriptText(convertToLinkScriptAst(jst))

fs.writeFileSync('tmp/parse.out.link', linkScriptText)

function convertToLinkScriptAst(jst) {
  const lst = {}
  switch (jst.type) {
    case 'Program':
      convertProgram(jst, lst)
      break
  }
  return lst
}

function convertProgram(jst, lst) {
  lst.zones = []
  jst.body.forEach(node => {
    switch (node.type) {
      case 'VariableDeclaration':
        convertVariableDeclaration(node).forEach(vnode => {
          lst.zones.push(vnode)
        })
        break
      case 'ExpressionStatement':

        break
      default: throw JSON.stringify(node)
    }
  })
}

function convertVariableDeclaration(jst) {
  return jst.declarations.map(dec => {
    switch (dec.type) {
      case 'VariableDeclarator':
        return convertVariableDeclarator(jst.kind, dec)
        break
      default: throw JSON.stringify(dec)
    }
  })
}

function convertVariableDeclarator(kind, jst) {
  return {
    type: 'host',
    immutable: kind === 'const',
    name: jst.id.name,
    value: convertVariableValue(jst.init)
  }
}

function convertVariableValue(jst) {
  if (!jst) return

  switch (jst.type) {
    case 'Literal':
      return convertLiteral(jst)
      break
  }
}

function convertLiteral(jst) {
  switch (typeof jst.value) {
    case 'string':
      return {
        type: 'string',
        value: jst.value
      }
    case 'number':
      return {
        type: 'number',
        value: jst.value
      }
    default: throw JSON.stringify(jst)
  }
}

function generateLinkScriptText(lst) {
  const text = []
  lst.zones.forEach(zone => {
    switch (zone.type) {
      case 'host':
        generateHost(zone).forEach(line => {
          text.push(line)
        })
        break
    }
  })
  return text.join('\n')
}

function generateHost(lst) {
  const text = []
  if (lst.value) {
    switch (lst.value.type) {
      case 'string':
        text.push(`host ${lst.name}, text <${lst.value.value}>`)
        break
      case 'number':
        text.push(`host ${lst.name}, size ${lst.value.value}`)
        break
    }
  } else {
    text.push(`host ${lst.name}`)
  }
  return text
}

基本上,您将 JS 解析为 AST,然后以某种方式将此 AST 转换为目标语言(在本例中为 LinkScript)的 AST。然后将输出的 AST 转换为文本。问题是,这样做的一般策略是什么?好像挺难的。

更详细地说,我需要知道您可以在 JavaScript 中创建的所有结构类型,以及您可以在 LinkScript 中创建的所有结构类型,以及一种结构如何映射到另一种结构。在我的脑海中,看着 JS,我可以手动弄清楚相应的 LinkScript 应该是什么样子。但尝试以编程方式执行此操作是另一回事,我对执行此操作应该采用的一般方法有点迷茫。

首先,尽管我已经做了 10 多年 JavaScript,但我对 JS AST 并不是很了解。我计划编写一些示例代码片段并使用 acorn 查看 AST 的外观。第二,似乎有太多的组合,让人不知所措。

我是否继续沿着上面开始的这条路走下去?还是有更结构化或更有纪律的方法?我如何更好地将问题分解为更易于管理的块?

此外,它并不总是像进行简单的一对一映射那样容易。有时事情的顺序会改变。例如,在 JS 中你可能有:

a = x + y

但在 LinkScript 中,这将是:

call add
  bind a, link x
  bind b, link y
  save a

所以赋值表达式有点颠倒了。在其他情况下会变得更加复杂。

所以就好像我需要研究每种类型的映射,并就如何进行映射提出详细的计划或算法。那么似乎我需要研究成千上万种可能的 transformation/mapping 类型。所以从这个意义上说,这似乎是一个非常耗时的问题,需要精神上的解决。

有没有更简单的方法?

很长一段时间(几年?)我一直想这样做,但就像我暗示的那样,这似乎总是一项极其艰巨的任务。我认为这是因为我没有清楚地看到所有不同的东西 ways/angles 我可以收到 AST,而且我不知道如何将它归结为我可以看到的东西。

除了弄清楚如何执行每种类型的 mapping/transformation 之外,我还应该有一些可以扩展的不错的代码。这通常是我的强项(用简单的 API 编写干净的代码),但在这里我很挣扎,因为是的,我还没有看到完整的画面。

编写转译器是一项非常艰巨的工作...但是,由于各种原因,JavaScript 工作流程中已经充满了转译器,因此有很多工具可以提供帮助。

如果您的目标语言看起来像 JavaScript,那么您可以将您的转译器编写为 Babel 的插件:https://babeljs.io/

否则,也许从 jscodeshift 开始,这将为您提供易于访问的 AST。

许多开源 javascript 工具,例如 eslint,也有 javscript 解析器,您可以稍加努力就可以将其提取出来。

另见 AST Explorer

一旦你有了 AST,你通常会递归地处理它,可能遵循访问者模式,将每个 AST 节点转换成等效的目标结构。然后可能优化窥孔以简化生成的 AST。然后最后序列化它。 jscodeshift 带有一个 javascript 序列化器,您可以用自己的序列化器替换它。