任何编程语言之间Transpiling(第一部分)

||TranspilersANTLR罗盘BSON教学工具聚合管道编译器工程

输入经由ANTLR解析树和代码生成输出变换

输入经由ANTLR解析树和代码生成输出变换

188金宝搏手机客户端安卓下载MongoDB的指南针,MongoDB的用户界面,最近推出的一对功能,使其开发者更容易的工作在他们所选择的语言。现在,用户可以在UI他们建立查询和汇总导出到他们的首选语言。很快,他们也将能够输入他们自己的语言。允许开发人员能够使用北斗当多个输入语言和多输出语言之间进行选择需要我们建立在许多一对多transpiler的形式定制的解决方案。大多数编译器是一个对一个,或不太常用,一个一对多或多对一的一个。有几乎没有任何许多一对多transpilers。为了避免从头开始,我们利用开源分析工具ANTLR这与已有的语法为我们所需要的语言以及一套编译工具为我们提供。我们通过想出一个创意组的类层次是减少需要从N²到n的工作量最小化成功的额外的复杂性的量。

动机

188金宝搏手机客户端安卓下载MongoDB的罗盘,提供了一个用户界面的数据库,并帮助开发者来迭代开发的聚合和查询的应用程序。在构建查询,应用程序目前需要在一个名为MongoDB的壳牌基于JavaScript的查询语言来进行输入。188金宝搏手机客户端安卓下载

北斗聚合管道建设者

北斗聚合管道建设者

为了让开发人员在开发聚集管道和查询时使用他们喜欢的编程语言,我们希望在两个部分添加功能。首先,我们希望允许开发人员熟悉的MongoDB壳牌自己所需(Python和Java等)的语言创建出口查询。188金宝搏手机客户端安卓下载

其次,我们希望允许开发人员使用他们的语言选择,而建设一个查询。为了同时实现,并允许用户最大的灵活性,我们的系统因此需要接受多种输入语言,也可以产生高效的方式多输出语言。

指南针导出到语言可以导出管道在您所选择的语言

指南针导出到语言可以导出管道在您所选择的语言

在这些功能的基础是先进的编译器技术在transpiler的形式。一个transpiler是源到源编译器这需要写入一个编程语言作为其输入的程序的源代码,并且产生的等效源代码中另一种编程语言。由于我们目前transpiler支持扩展JSON,也被称为BSON,我们称之为BSON transpiler。虽然我们目前只支持每种编程语言的一个子集,该transpiler设计的方式,使我们能够扩展支持,包括整个语言的语法。

设计方法

指南针应用程序被设计成一个可扩展的插件架构,使我们能够建立transpiler作为一个独立的插件。与电子架构我们的应用程序是基于工作中,我们需要的插件是在JavaScript执行。有很多在JavaScript不同transpilers这是我们考虑的。然而,为我所用的情况下,我们所需要的任何语言的任何一种语言转换功能,支持BSON,这意味着我们需要一个定制的解决方案。

罗盘查询总是采取两种BSON文件(聚合流水线级)或BSON文档(其他查询)的阵列的形式含有的MongoDB查询语言。188金宝搏手机客户端安卓下载虽然这一约束减少了BSON transpiler问题的范围,在语言子集,我们需要支持庞大和复杂,以至于我们决定把这个问题好像我们是在加入全语言支持。

朴素的方式构建的源极到源极的编译器支持多语言会导致由于语言的组合的数量的努力多项式量是输入和输出语言的数量的乘积。我们需要建立一个可持续发展的设计,这样增加新的输入和输出语言只需要构建O(1)每语言组件。这为的语言数n减小到O(N)的整个问题。解析树:我们通过抽象问题成松散地通过接口耦合到共享,中间,存储器内数据结构独立的输入和输出级来实现这一点。输入语言阶段只需要建立树,输出语言阶段只需要读它。

大多数编译器有两个主要阶段:分析和代码生成。解析阶段是负责将一个程序的文字文本成表示其意义的抽象树,和代码生成阶段行走该树和产生可被执行的输出 - 通常是机或虚拟机指令的二进制。一个关键的发现是,源极到源极的编译器可被看作是一个专门的编译器,其中,代码生成阶段生成程序文本,在另一用户友好的语言,而不是机器代码。我们transpiler的设计从概念茎。

解析

为了处理一些源代码,诸如串新NumberDecimal(5),一个词法分析器或词法分析器获得原始代码,并将其分成标记(此过程称为词法分析)。令牌是表示文本的对应于该语言的语法的原始部件之一的块的对象。这可能是一个号码,标签,标点符号,操作员等

在解析阶段这些令牌然后转化到一个树结构,不仅描述所述输入代码的分离的片,而且它们之间的相互关系。此时编译器能够识别的语言结构,如变量声明,语句,表达式,等等。这种树的叶子是由词法分析中找到的标记。当树叶从左向右读,顺序是一样的,在输入文本。

编译器处理的阶段:输入通过词法分析成令牌转化,令牌通过语法分析变换成用于产生输出码的AST

编译器处理的阶段:输入通过词法分析成令牌转化,令牌通过语法分析变换成用于产生输出码的AST

我们不想写我们自己的解析器和词法分析器,因为这是令人难以置信的时间,即使是单一语言耗时,我们必须支持多个。幸运的是,有许多“分析器生成”工具,有效地从一组规则,称为语法生成语法树。这些工具需要输入的语法,这是分层次和高度结构化,解析基于该语法输入字符串,并将其转换成一个树状结构。

使用解析器发电机的最棘手的部分是写入语法的繁琐和容易出错的过程。从头开始编写语法要求其所有的边缘情况下,输入语言的详细知识。如果transpiler需要支持多种编程语言,我们会写语法为每个输入语言这将是一个巨大的任务。

源到源变换与ANTLR

源到源变换与ANTLR

这就是为什么我们决定使用ANTLR,一个强大的解析器生成器,最重要的是,已经有感兴趣的几乎所有编程语言的语法。ANTLR也有一个JavaScript运行,使我们能够在我们的Node.js的项目中使用它。我们考虑使用LLVM-IR,一组不同的编译器技术的编译成一个中间的高级别表示。然后,该方法将需要一个单独的步骤来编译所述中间表示成目标语言。这是多平台编译器的通用模式,像锵/LLVM项目。不幸的是,目前还没有从中间表示回用户的编程语言去很多现有的编译器。我们将不得不写这些编译器自己,所以最终使用LLVM就不用救了我们很大的努力。

下面的代码段示出了用于构建的ECMAScript(JavaScript)的输入源代码解析树的程序的基本结构。此代码进口辅助词法和语法分析器文件,并允许ANTLR拉字符从输入字符串,创建一个字符流,将其转换为符号流和最终建立一个解析树。

//只需几行从字符串输入到去完全解析穿越树!常量antlr4 =要求( 'antlr4');常量ECMAScriptLexer =要求(” ./ LIB / ANTLR / ECMAScriptLexer.js');常量ECMAScriptParser =要求(” ./ LIB / ANTLR / ECMAScriptParser.js');常量输入= '新NumberDecimal(5)';常量字符=新antlr4.InputStream(输入);常量词法分析器=新ECMAScriptLexer.ECMAScriptLexer(字符);常量令牌=新antlr4.CommonTokenStream(词法分析);常量解析器=新ECMAScriptParser.ECMAScriptParser(令牌);常量树= parser.program();

从ANTLR定义所得到的解析树继承分析树类,给它一个统一的方式来运行。

请注意,分析阶段和解析树中由输入语言决定;他们是完全独立的,我们将产生我们所追求翻译的源代码输出语言的阶段。这种独立性在我们的设计使我们能够减少我们需要编写覆盖从O(N²)我们的输入和输出语言为O(n)的部件数量。

代码生成

树类型

使用ANTLR其预建文法库需要在我们的设计略有妥协。要理解为什么,就必须要明白,是相关的,有时可以互换使用两个词的区别:解析树和抽象语法树(AST)。从概念上讲,这些树木是相似的,因为它们都表示的源代码的片段的语法;所不同的是抽象的水平。一个AST还没有有关输入信息标记本身仍然被完全抽象的地步。正因为如此,代表着相同的指令AST的是无法区分的,不管是什么语言产生它们。相比之下,解析树包含了低级别的输入令牌的信息,所以不同的语言会产生不同的解析树,即使他们做同样的事情。

抽象语法树和解析树给定的比较器的输入“的新NumberDecimal(5)”

抽象语法树和解析树给定的比较器的输入“的新NumberDecimal(5)”

理想情况下,我们的代码生成阶段将上一个AST,而不是一个解析树操作,因为不必考虑特定语言的解析树引入了复杂性,我们宁愿避免。ANTLR4,但是,只有生产只读解析树。但是,使用ANTLR和现成的语法的优点是非常值得的是权衡。

游客

解析树遍历

解析树遍历

最喜欢的编译器,该BSON transpiler使用访问者模式遍历解析树。ANTLR不仅构建了一个解析树,但它也编程产生的骨架访问者类。这个访问者类包含用于遍历解析树(每种类型树的节点之一访问方法)方法。所有这些方法开始访问并与节点的名称结尾,这将访问 - 例如visitFuncCall()要么visitAdditiveExpression()。节点名称是直接从输入语言的语法文件拍摄,所以每个访问者类和它的方法是量身定做的输入语言的语法。在ANTLR生成的访问者类,这些方法没有做任何事情,除了子节点上递归。为了让我们的访问者能够transpile代码,我们需要继承生成的访问者类并覆盖每个访问方法来定义如何处理每种类型的节点做。

由于BSON transpiler是建立在支持多种输入语言,并且每种语言会产生不同的解析树,我们需要创建一个由罗盘支持的每个输入语言一个定义访问者。但是,只要我们避免构建自定义访问者对每个组合输入和输出的语言,我们仍然只能建立O(n)的成分。

通过这种设计,每个访问者负责遍历一个单一的语言解析树。因为它访问的每个节点,并返回该节点的原始文本,也可以改造这个文本,我们需要的方式,来人来电功能。从根开始,来人来电参观递归方法,在深度优先顺序递减的叶子。在一路下滑,游客装饰用元数据节点,如类型信息。在途中,它返回transpiled代码。

发电机

用蛮力的解决方案,访问*访问者的方法将包含用于生成输出语言文字的代码。为了产生多种输出语言,我们就必须专注根据当前的输出语言每种方法。总体而言,这种方法将继承每一种语言特定访问者类一次,每输出语言,或者更糟糕的是,把一个巨大的switch语句在每个访问*与每个输出语言的情况下的方法。无论这些选项是易碎的,并要求O(N²)发力。因此,我们选择了去耦的代码从生成输出的代码横穿特定语言的树。我们通过封装代码生成的每种语言到了一组类称为发电机,它实现一个家庭的做到了这一点发射*方法,如emitDateemitNumber用于产生输出码。

阶级成分

类依赖图

类依赖图

我们的设计是因为需要通知的访问者能够调用生成方法,而不需要知道他们正在使用该发电机。由于代码生成实际上有很多的共同点,无论输出语言,我们要实现一个系统,我们可以抽象的默认行为尽可能离开发电机只处理边缘情形。

我们选择由具有从访问者类发电机类继承利用JavaScript的动态力学继承和方法分派。因为JavaScript并不需要被定义的方法被调用之前,游客可拨打发射这是在发电机实际定义和发电机可以调用访问者的方法来继续遍历树本身的方法。使用输出语言和访问者类从输入语言决定确定的生成器类,我们能够组成一个transpiler上即时,因为它是出口。发电机类似于一个抽象的接口,除了有在JavaScript中没有的经典界面。

如该代码所示片断下面,对于每一种语言组合我们的应用程序创建相应的访问者和发电机类组成的专门transpiler实例。当我们的应用程序接收来自用户的一段代码,它创建了一个解析树。然后transpiler访问解析树,用ParseTreeVisitor的从我们定制的访问者子类继承的访问方法和语言特有的,ANTLR生成的访问类(如ECMAScriptVisitor)。

//每个复合transpiler实例必须遍历解析树//用于与其特定语言的能力“访问*”的方法,并为//其另一种语言输出代码“EMIT *”的方法。常量getJavascriptVisitor =要求( './ codegeneration / JavaScript的/游客');常量getJavaGenerator =要求( './ codegeneration / JAVA /发电机');常量getPythonGenerator =要求( './ codegeneration /蟒/发电机');...常量loadJSTree =(输入)=> {/ *文法和解析所述用户输入* / ...};/ ** *撰写transpiler并返回将使用该transpiler *参观树,并返回生成的代码编译的方法。* * @参数{函数} Known Issues已知的 - 该方法需要在用户输入,并返回一棵树。* @参数{}参观游客 - 输入语言的游客。* @参数{}函数发生器 - 返回发电机,从它的ARG继承。* * @Returns {函数}编译函数来导出* /常量composeTranspiler =(Known Issues已知的,访客,发电机)=> {常量Transpiler =发生器(访客); const transpiler = new Transpiler(); return { compile: (input) => { const tree = loadTree(input); return transpiler.start(tree); } }; } module.exports = { javascript: { java: composeTranspiler( loadJSTree, getJavascriptVisitor(JavascriptANTLRVisitor), // Visitor + ANTLR visitor getJavaGenerator // Method that takes in a superclass, i.e. the visitor ), python: composeTranspiler( loadJSTree, getJavascriptVisitor(JavascriptANTLRVisitor)), getPythonGenerator ), ... }, ... }

树遍历实例

简单的节点

在最顺利的情况下,认为JavaScript片段文本“你好,世界”,自定义访问者类需要做的第一件事就是指定树的遍历的入口点。因为在不同的语言进入节点有不同的名称(即file_input在Python,但程序在JavaScript),我们定义在每个访问者的方法称为开始这要求该输入语言根节点的访问方法。这样,我们的编译器可以简单地调用开始,而不必担心哪根节点被称为每个访问者。

//切入点树遍历类访问者扩展ECMAScriptVisitor {开始(CTX){回报this.visitProgram(CTX);}}

的ANTLR访问方法的默认行为是复发每个子节点上,并在数组中返回结果。如果节点没有任何孩子,那么这次访问方法将返回节点本身。所以,如果我们不覆盖任何的ANTLR方法,则返回值我们开始方法将是节点的数组。要返回节点转到我们简单的返回一个字符串“你好,世界”举例来说,我们首先覆盖visitTerminal方法,使叶节点将返回该节点中存储的原始文本。然后,我们修改visitChildren方法,这样不是把来访的每个子节点到一个数组的结果,结果弄串连成一个字符串。这两个变化都足以让我们“你好,世界”例如要被充分转化为使用相同的字符串表示,像Python语言。

')visitTerminal(' //重写方法类访客延伸ECMAScriptVisitor {开始(CTX){返回this.visitProgram(CTX);} //访问的叶节点,并返回字符串visitTerminal(CTX){返回ctx.getText();} //串联重复上子节点visitChildren(CTX)的结果{返回ctx.children.reduce((代码,子)=>`$ {代码} $ {this.visit(子)}`, '');}}

转换

然而,我们不能总是正好连接终端节点的文本值形成的结果。相反,我们必须在不丢失任何细节改造浮点数,以及在不同的数字系统的数字。对于字符串常量,我们需要思考的问题单,双引号,转义序列,评论,空格和空行。这种类型的转换逻辑可以被应用于任何类型的节点。

让我们看一个具体的例子:在Python中,对象属性名必须用引号括起来({'你好,世界'});在JavaScript中这是可选的({你好,世界'})。在这个特定的情况下,这是唯一的一个修改,我们需要以变换的JavaScript代码片段插入到Python代码。

//转化的JavaScript代码插入Python代码类访客延伸ECMAScriptVisitor {... visitPropertyExpressionAssignment(CTX){const的键= this.visit(ctx.propertyName());常数值= this.visit(ctx.singleExpression());如果( 'emitPropertyExpressionAssignment' 在此){返回此[ 'emitPropertyExpressionAssignment'];}回`$ {}键:$ {值}`;}}

propertyExpressionAssignment节点有两个子节点(propertyName的singleExpression)。为了得到这两个子节点的值,我们需要分别穿过它们的左侧和右侧的子树。遍历子树返回子节点的原始或转换值。然后,我们可以建立使用检索到的值在这里补转化代码片段的新的字符串。相反,在直接访问者这样做的,我们检查,如果相应的EMIT方法存在。

如果访客找到合适的EMIT方法,将委托改造过程中的生成器类。通过这样做,我们解放我们从寂寂输出语言什么游客。我们姑且认为有一些发电机类,知道如何处理输出语言。

然而,如果这个方法不存在,访问者将返回原始的字符串,没有任何转变。在我们的例子中,我们假设emitPropertyExpressionAssignment供给,这将返回转化JavaScript字符串。

处理

在更复杂的情况下,我们必须做一些预处理的游客才可以调用任何EMIT方法。

例如,日期表达是一个复杂的情况下,因为日期有广泛的跨不同的编程语言可以接受的参数格式。我们需要做一些预处理的游客,所以我们可以保证所有信号发送的方法发送相同的信息,无论输入语言。在这种情况下的日期节点的,以表示日期信息的最简单的方法是构造一个JavaScript Date对象,并把它传递给发电机。节点类型需要预处理必须有处理*方法在访问者定义来处理该预处理。在这个例子中它会被称为processDate

//“processDate()”来创建日期对象将其传递到所述发送方法processDate(节点){让文本= node.getText();此节点让利日期//原始文本输入;尝试{日期= this.executeJavascript(文本);//构造在沙箱中的日期对象}赶上(错误){抛出新BsonTranspilersError(返回Error.message);}如果( 'emitDate' 在此){返回this.emitDate(节点,日期);} ...}

为了这processDate方法,因为我们正在编写JavaScript和transpiler是用JavaScript编写的,我们采取了一个快捷方式:在执行用户输入构造日期。因为它已经被符号化,我们确切地知道该代码包含因此它是安全的在沙箱中执行。对于其他语言处理日期,我们将代替解析结果,并通过参数构造日期对象。完成后,将处理方法将再调用相应发射*方法,emitDate并传递给它的构造日期作为参数。现在,我们可以调用从访问者的访问适当的方法所需的过程,并发出方法。

//这是一个用于Python生成代码的发电机。//在 'emitDate()' 方法在发电机定义,并从所述访客module.exports称为=(超)=>类发电机延伸超{emitDate(节点,日期){常量dateStr = [date.getUTCFullYear(),date.getUTCMonth()+ 1,date.getUTCDate(),date.getUTCHours(),date.getUTCMinutes(),date.getUTCSeconds()]。加入( '');返回`datetime.datetime($ {dateStr},tzinfo = datetime.timezone.utc)`;}};

鉴于输入字符串日期(“2019-02-12T11:31:14.828Z”),解析树的根将是一个FuncCallExpression节点。此节点的访问方法称为visitFuncCallExpression()

//延伸ECMAScript的语法类访客延伸ECMAScriptVisitor {/ ** *访问表示一个函数调用的节点的访问者类的实施例。* * @参数{FuncCallExpression}节点 - 树节点* @返回{字符串}  - 生成的代码* / visitFuncCallExpression(节点){常量LHS = this.visit(node.functionName());常量RHS = this.visit(node.arguments());如果(`过程$ {LHS}`在此){返回此[`过程$ {LHS}`](节点);}如果(`发射$ {LHS}`在此){返回此[`EMIT $ {LHS}`](节点);}回`$ {LHS} $ {RHS}`;} ...}

访问方法所做的第一件事就是它的两个子节点递归。左手孩子表示函数名节点,即日期。右手边的孩子代表论点节点,即2019-02-12T11:31:14.828Z。一旦方法检索功能的名称,它可以检查,看看是否能功能需要任何预处理。它检查processDate方法被定义和,如若不然检查,是否一emitDate方法被定义。尽管emitDate方法是在发电机中定义,由于访问者和发生器是由为一类,访问者对待发射的方法,好像他们是它自己的类的方法。如果没有方法存在,访问*方法将返回子节点上的递归结果的串联。

每个输入语言都有自己的游客,可以包含处理逻辑和每个输出语言都有自己的发电机,它包含特定语言所需的转换逻辑。作为一项规则,所有输出语言需要转换会发生处理逻辑,而其他所有的转换在发电机发生。通过这种设计,根据不同游客的不同transpilers可以使用相同的发电机的方法。这样一来,因为我们添加的每个输入语言,我们只需要定义一个访客。同样,对于我们添加的每个输出语言,我们只需要定义一个发电机。对于ñ我们要支持的语言,我们现在有O(ñ工作不必编写一个访问者产生的每一种语言的组合)量。

结论

指南针BSON transpiler插件有任何编程语言解析和生成MongoDB的查询和聚合的潜力。188金宝搏手机客户端安卓下载当前版本支持多种输入(MongoDB的外壳,使用Javascript和Python)和输188金宝搏手机客户端安卓下载出(Java,C#,Python和MongoDB的壳牌和JavaScript)的语言。

所述BSON transpiler插件被构建为一个独立的Node.js模块,并且可以在任何基于浏览器的或Node.js的应用一起使用NPM安装BSON-transpilers。正如许多其他的MongoDB188金宝搏手机客户端安卓下载项目,BSON transpiler插件是开源的,你可以去回购我们欢迎的贡献。如果您想参与北斗BSON transpiler,请查看我们的GitHub上起作用的区段

当写BSON transpiler,我们被一般编译器的设计原理(词法分析,句法分析,树遍历)引导。我们使用ANTLR减少手工作业的解析感兴趣的输入语言所需要的量,这允许我们主要集中在模块化的代码生成过程。模块化的语言定义的一个主要好处是,用户可以在不需要了解当前支持的输入语言什么促成一个新的输出语言。该规则同样适用于添加新的输入语言:你应该能够定义你的访客,而无需关心现有的发电机。

该BSON transpiler插件的最新版本是更复杂,比已经覆盖当前的博客帖子强大。它支持更宽范围的语法,通过使用一个符号表的。它还包括整个BSON库,带有参数的函数和类型的验证,以及信息的错误消息调用。最重要的是,我们通过使用字符串模板抽象很多代码生成添加优化的高水平。所有这些事态发展将在以后的博客文章来描述。

撰稿安娜·赫利希阿莲娜Khineika,&伊琳娜Shestak。插图伊琳娜Shestak

延伸阅读