我们编写编程语言,第3部分:转换器架构。 语言结构和数学表达式的分析

图片

引言


我欢迎您,有兴趣的阅读开发人员,无论我以何种语言使用这些文章以及我重视他们的支持和观点。

首先,根据既定传统,我将提供指向先前文章的链接:

第1部分:编写语言VM
第2部分:程序的中间表示

为了使您对我们在这些文章中写的内容有一个完整的了解,您应该事先熟悉前面的部分。

另外,我应该立即发布一个链接,该链接指向有关我先前编写的项目的文章,整个总结工作基于该文章: Clack syudy 。 也许值得首先了解它。

关于该项目的一点:

小项目现场
GitHub存储库

好吧,我还要立即说,所有内容都是用Object Pascal(即FPC)编写的。

因此,让我们开始吧。

大多数翻译员的工作原理


首先,值得理解的是,如果不先熟悉一堆理论材料和许多雕像,我将无法撰写任何有价值的东西。 我将用几个词来描述主要内容。

转换器的任务是首先准备要进行分析的代码(例如,从其中删除注释)并将代码分解为标记(标记是对语言有意义的最小字符集)。

接下来,通过分析和转换它,我们需要将代码解析为某个中间表示形式,然后组装应用程序以供执行或……一般需要收集什么。

是的,但是我对这些文字并没有说什么,但是-现在该任务分为几个子任务。

让我们跳过代码如何准备执行,因为 描述一个过程太无聊了。 假设我们有一组令牌可供分析。

代码分析


您可能听说过构建代码树及其分析,甚至更深入的东西。 与往常一样,这仅是使简单的可怕术语变得混乱。 通过代码分析,我的意思是要简单得多的一组操作。 任务是遍历令牌列表并解析代码(每个代码的构造)。

通常,在命令式语言中,代码已经以结构形式的树的形式呈现。

您必须承认,在循环“ B”的主体中开始循环“ A”,然后在循环“ B”的主体之外结束循环是不可接受的。 代码是由一组构造组成的结构。
每个设计都有什么? 没错-起点和终点(也许中间还有其他东西,但不是重点)。

因此,代码解析可以单次通过,而实际上无需构建树。
为此,您需要一个遍历代码的循环和一个庞大的切换案例,以执行主代码分析和分析。

即 我们遍历标记,我们有一个标记(例如,让它成为……)“如果”-我真的怀疑这样的标记是否可以像这样在代码中出现->这是构建的开始if..then [.. else] .. end!

我们分别分析所有后续标记,以使用我们的语言构建条件。

关于代码错误的一点


在结构分析和代码执行阶段,最好不要对错误处理进行评分。 这是有用的翻译器功能。 如果在分析结构期间发生错误,那么这是合乎逻辑的-结构构建不正确,您应该通知开发人员。

现在关于Mash。 语言是如何解析的?


上面,我描述了翻译器设备的一般概念。 现在是时候谈论我的工作了。

实际上,事实证明该翻译器与上述翻译器非常相似。 但是对我而言,它不会将代码分解为一堆令牌以供进一步分析。

在开始解析之前,将以更漂亮的形式呈现代码。 如果注释在多行中进行了描述,则删除注释并将所有结构组合成长行。

因此,在每个单独的行中都有语言结构或其一部分。 这很酷,现在我们可以解析大型切换案例中的每一行,而不用在令牌集中查找这些构造。 而且,这里的优点是生产线有一个末端,并且使用这种方法更容易确定构造中的错误。

因此,对单个结构的分析是通过单独的方法进行的,这些方法返回结构或其部分代码的中间表示。

附言 在上一篇文章中,我描述了从中间语言到VM字节码的转换器的构造。 实际上-这种中间语言是中间表示。

应当理解,结构可以包括几个更简单的结构。 因为 由于我们通过不同的方法分析每个结构,因此在分析每个结构时可以轻松地相互调用它们。

图片

在代码上进行热身


首先,翻译人员应迅速熟悉代码,仔细阅读并注意一些设计。

此时,您可以处理全局变量,使用构造以及导入,过程和函数以及OOP构造。

最好对多个对象生成一个中间视图以进行存储,以便
在初始化之后但在main()开始之前粘贴用于全局变量的代码。

可以在末尾插入OOP构造的代码。

精致的设计


好的,我们找到了简单的设计。 现在是时候进行棘手了。 我不认为您有两个周期就忘记了这个例子。 众所周知,结构通常以树的形式出现。 这意味着我们可以使用堆栈来解析复杂的结构。

堆栈与它有什么关系? 而且。

首先,我们描述将推入堆栈的类。 解析复杂的构造时,我们可以为该块的开头和结尾生成一个中间表示,例如,解析for,while,直到循环,if构造,方法,甚至使用Mash语言。

此类需要用于存储中间表示形式,元信息(对于某些变量构造)以及用于存储块类型的字段。

我只给出整个代码,因为它没有很多:

unit u_prep_codeblock; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TBlockEntryType = (btProc, btFunc, btIf, btFor, btWhile, btUntil, btTry, btClass, btSwitch, btCase); TCodeBlock = class(TObject) public bType: TBlockEntryType; mName, bMeta, bMCode, bEndCode: string; constructor Create(bt: TBlockEntryType; MT, MC, EC: string); end; implementation constructor TCodeBlock.Create(bt: TBlockEntryType; MT, MC, EC: string); begin inherited Create; bType := bt; bMeta := MT; bMCode := MC; bEndCode := EC; end; end. 

好吧,堆栈是一个简单的TList,在这里重新构造轮子只是愚蠢的。

因此,在解析构造时,我们可以说相同的while循环如下所示:

 function ParseWhile(s: string; varmgr: TVarManager): string; var WhileNum, ExprCode: string; begin Delete(s, 1, 5); //"while" Delete(s, Length(s), 1); //":" s := Trim(s); //   ,       ... //        :) WhileNum := '__gen_while_' + IntToStr(WhileBlCounter); Inc(WhileBlCounter); //   while   // ,        if IsExpr(s) then ExprCode := PreprocessExpression(s, varmgr) else ExprCode := PushIt(s, varmgr); //  ExprCode     //        //    //(      ) Result := WhileNum + ':' + sLineBreak + 'pushcp ' + WhileNum + '_end' + sLineBreak + ExprCode + sLineBreak + 'jz' + sLineBreak + 'pop'; //        //  -        //   break BlockStack.Add(TCodeBlock.Create(btWhile, '', 'pushcp ' + WhileNum + sLineBreak + 'jp' + sLineBreak + WhileNum + '_end:', WhileNum + '_end')); end; 

关于数学表达式


您可能没有注意到这一点,但是数学/逻辑表达式也是结构化代码。

我以堆叠的方式实施了他们的分析。 首先,将表达式的所有各个元素都压入堆栈,然后通过几次传递生成中间表示形式的代码。

几次-因为 有优先级的数学运算,例如乘法。
我在这里看不到重点,因为 它很多而且很无聊。

附言 /lang/u_prep_expressions.pas-在此完全完整地展示给您查看。

总结


因此,我们实现了可以进行转换的转换器。例如,这是代码:

 proc PrintArr(arr): for(i ?= 0; i < len(arr); i++): PrintLn("arr[", i, "] = ", arr[i]) end end proc main(): var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] PrintArr(arr) InputLn() end 

我们的语言缺少什么? 对,支持OOP。 我们将在下一篇文章中讨论。

多谢您读完为止。

如果您不清楚某些事情,那么我正在等待您的评论。 或在论坛上提问,是的...是的,有时我会检查一下。

现在进行一次小型民意测验(以便我对其进行研究并享受我文章的重要性):

Source: https://habr.com/ru/post/zh-CN435520/


All Articles