编程语言来自各种不同的范式。即便如此,往往有两个主要阵营 语言前面。有静态语言,有动态语言。避免太多的历史和任何类型的历史 深入分析,本文将简化具有许多假设的东西。我们将假设我们将制作 可能并不总是保持真实是以静态语言编写的程序编译为机器代码,而 以动态语言编写的程序在解释器中运行。
测试语言静态或动态的定义可以更多地说明这一点。用静态语言, 变量和过程具有顽固的类型。尝试访问尚未命名的值 相关范围导致编译时发出的语法错误。编程语言以这样的方式组织 即,在源电平分析它时,所有变量和函数的位置是由词汇知识的 地址:即给定相关范围,可以通过定义的顺序来识别变量。
这将是静态语言的原型。当首次声明变量时,它们必须与a配对 类型。为了与动态类型的语言进行比较,让'查看Python的示例。
在此示例中,未对这些变量声明类型。变量' num1'可以用作任何类型 为其分配任何值。此外,声明步骤完全不必要。存储值的方式 包括类型信息,因此仅在声明范围时才需要声明。
除了Python之外的动态类型语言的其他例子包括JavaScript,Ruby,Lua,Clojure,Scheme和 Lisp,以及其他一些。除了语法和表达程度之外,这些编程语言 由于它们的类型系统而言,在很大程度上。所以称为动态类型的语言有时被称为 鸭子类型或鸭语言。
采用许多这些识别功能,并且真的作为构建来自基本零件的语言的练习, 我们将从地上建造鸭子编程语言。我们将探讨此过程而不考虑其 务实或实用性。实际上,在我们完成了这个过程后,才会出现任何效用的想法。
作为公平的警告,我觉得我应该提到即将到来的部分可能包含一些复杂的材料,即使是 它是明白的,无论何处都是如此。因此,我会推荐读者做一些研究 论踏上这一旅程之前编程语言设计的主题。有了那么说,我们正在路上。
此时,它可能很容易拼接在一起句法表达式的文档,初学者如何进行教程, Wiki,或语法指南作为语言的解释,并完全放弃主题,继续前进 生产任务。有时它似乎是一种语言的设计,到它的真正的语法,是更多的东西 关于参考手册的衬垫笔记而不是存在的东西,这些内容随着时间的推移编写,制作或改变。 这种解释的目的不是表明你的编程语言应该被凿成石头和 这就是概念如何实现的。相反,忠于我们任务的性质,我们将在每一步 看看如何改变,扩展的方式,并随着时间的推移而发展。什么可能适合一个 任务不是每个问题的解决方案,并有一种方法来改变事物或重用我们的努力总是很好。
考虑到这一点,我们将对鸭子语言的基本机制处于原子的基本机制的光泽 等级。相反,我们将探讨本质是什么。我们希望发现语言本身的意图。 如果我们能够进入心灵,风水,或方案语言的基本态度,那将是 足够的。尽管如此,编程语言是我们用来表达想法的系统。以及任何通信方式 有用,我们需要有一些共同的地面。
我们将创建一种对程序员背景非常中性的语言。我们也将在一个 势在必行的风格。虽然某些功能性质的想法可能会在最终渗透,但我们正试图建立 公共程序员的基本语言。
找到我们可以从中工作的根概念,让' s从语句开始。有表明操作的陈述。 给定两个值,将它们添加在一起并分配结果。如果表达式为真,请执行一块语句。 评估表达式,然后使用参数进行过程调用。等等。将描述陈述块 就声明列表而言。鉴于函数声明,一种语句,函数有一个名称,a 参数名称列表,以及构成正文的语句列表。如果/ else语句具有类似的性质。它 是包含其他陈述的陈述。
提供一点点框架来从,写下一些这些想法可能看起来像的东西。
这就是我们的基本情况,如果语句看起来像。考虑到我们经常想要执行陈述 条件是假的,我们也更好地为别人的陈述添加支持。
如果某些条件那么 ;这是一块陈述 ;如果条件为真,则运行 别的 ;这是一块陈述 ;如果条件为假 结尾
看着我们所拥有的语法,我们似乎已经借用了Lua的结束块。我想这只是 巧合。在基本并终止与结束时的陈述之后,我们可以轻松调整我们的语言 而不是简单地关键字结束。我没有对此有任何真正的理由。
但是,我将证明是在语法中使用真实单词。我认为这是平淡的,更容易可读 或者比使用大量的句法符号更简单到描绘块和其他控制 结构。所以无论我们在哪里,我们都会在我们的语言中使用整个单词。
就像我们在我们介绍了函数定义的想法或函数声明之前,让'在那个看起来的东西 喜欢。
函数us_new_function(参数1,parameter2) ;在这里,我们有工作的陈述 ;如果此函数返回值,那么我们可能会有 返回OuRSult. ;在末尾 结尾
我们赢得了对这些的局限性的任何限制。可以在功能内定义函数。在这种情况下 他们将在定位的地方本地。我们不想在形成我们的定义时介绍局限性 从一开始,因为它们可能是基于aren' t true的期望。如果我们强加的限制 我们自己是人为的,那么我们将使时间和努力实施人工局限性,而且 变得浪费了努力。在函数上,使用结束作为关键字似乎更加合理,因为它更短 而不是结束功能。一个良好的中间地面可能是Endf。这取决于我们是否希望结束成为一个 终止运行程序的特定命令。我一直认为这可能是一个有用的指导,但它' s 通常不包括在语言中。相反,我们可能有一个可以从我们的quit()的系统命令 程序。
我们的语言必须具有现有语言的一些共同点,所以我们将使用熟悉的构造等循环 而循环。对于循环将以明确的方式使用范围。将有一个开始值和终点值,而且 将为它们两者和每个值执行循环体。这是假设我们从中开始 整数,或可能浮点数,但总是通过一个增量增加。
对于j = 1到100做 ;这打印了数字1到100 println(j) 环形
我们将允许我们预期的所有基本操作。即添加,减法,乘法和 师,以及模数(我们可以实现自己的东西),为方便起见,串联串联, 否定,而不是和布尔表达式。这些是基本面。
我们正试图完成创造最灵活和最可编程的语言,所以我们还必须想到 我们可以添加的明显动态元素。阵列从根本上很有用。让'允许使用的阵列 任何索引,无需指定容器大小。让&#39展开此数组功能以允许任何类型的值 用作索引。此外,使用熟悉的对象表示法创建字典类型。这些将是 类似于相关语言的类或结构。为了提高灵活性,这些将是相同的对象 内部作为阵列,并且每个互换的语法可互换使用。
arr1 = [] ARR2 = [1,2,3,4] dict2 = {firstarray:arr1,secondarray:arr2} dict1 = {" a&#34 ;: 1," b&#34 ;: 2," c&#34 ;: 3} ARR2 [4] = 5 dict2.arr2 [5] = 6 dict1.d = 4.
提高灵活性的另一个功能允许函数成为一流的对象。这意味着我们 可以使用函数作为参数,将它们作为值返回,并将它们分配给命名变量。我们可以重命名一个函数 并使用原始名称进行其他内容。我们可以将函数附加到对象并将其用作类。虽然 传递函数作为一流的对象是一个功能的想法,这里它与动态语言的想法重叠 所以我们将采用此功能。为了跟踪对象和复杂类型,实现有意义 内存管理通过垃圾收集,而不是将大责任传递给程序员 适应使用我们的语言。
截至目前,可能对休闲读者可能不会有兴趣开发自己的语言。他们 可能对所涉及的力学更感兴趣。作为一个相当精致的练习,我邀请任何人试图 创建自己的解释编程语言。所说,我即将概述这个过程 我曾经创建鸭子编程语言的工具,即使我识别出多种方式 走这个过程。
例如,我选择在C编程语言中写一切。这真的比它更艰巨 必须是,但我会进一步解释我的决定。开发人员可能更容易创建一个 任何其他环境的语言。一个真正专用的工程师可能已经针对平台并决定 从get-go与手写汇编语言开始。这不是一个非常便携的解决方案。另一个努力 开发人员可能从像Java或C#这样的现代语言开始。这些是伟大的选择,我会鼓励采摘 无论您熟悉的任何工具。出于可能或可能不明显的原因,我建议塑造时尚 我们在静态主机环境中创建的这种动态语言。所以,一种像这样的语言将是彼此之一 方案。我知道许多课程通过实施方案或LISP解释器来教导编程语言。 除了风格和语法的巨大差异,我们所做的就是没有遥远。通常这些 课程要求在语言本身中写语言解释器,或涉及类似的任务。这 我建议远离本课程的原因是因为它开始刻意不切实际 解决方案。您可以从自主托管环境中获得的性能实际上只有在那里的一半,以及任何一个 我们正在创造的解决方案的好处往往被带走。用于语言的特殊前端可以非常 有用,有些方法可以通过创建一个特殊方言来提高生产力和性能,但我们是什么 去是一种完全新的语言。
我会注意到,以解释或脚本语言实现这种动态语言有一种方法 可能会有所帮助,这将在交叉编译的情况下。 IE。让我们的鸭代码减少到其他一些 在被执行之前,语言如JavaScript或Python。那没关系,但我们正在处理代码 编译和其他复杂性最佳地解决了本系列第II节。
它是便携式的。在几乎任何操作系统上都可以部署在C中编写的程序。它很容易 编译任何设备,可以在几乎任何微处理器上执行。尤其是当此代码写入时 思想的标准。我还会选择避免语言中的任何新功能,以最大限度地提高兼容性。
它是低级别的。在不漂亮进入装配和机器代码本身,C代表了目标的近距离 机器本身。不知道CPU我们的代码将在寄存器上执行或确切的寄存器布局,我们想知道 将执行哪些指令以及我们的程序将如何在内存中奠定。
它不是收集的垃圾。虽然这一点可以辩论,但这个项目的目的是以创造的方式 新鲜玩意。因此,创建具有更高级别功能的新语言,更容易感受成就感 当我们正在研究功能时,我们并从一开始就没有。这也有助于我们控制运行时 对于我们的最终口译员,我们正在为自己处理内存管理的责任而。
它&#39很难。这几乎没有资格作为一个原因。我会说一个原因会是'因为它很快,'但 了解写作过程已经消失,它既不快速实施,也不一定是最快的结果 代码。但是,形成这种练习将是一个挑战,所以它也可能是一个好的。
现在,这一并非特别有资格作为一种语言关注,而是作为一个额外的挑战,该项目是存在的 从上面建造的,以逐步的自身引导的方式构建。这意味着,如您所见的那样 在解析器和Lexer周围的发展融合在一起,两个语言的基本组成部分,这些都将是 手工造成的零件和工具。将解释解析器发生器的内部工作。这完全 忽略可能有关于使用野牛,Flex,YACC或Lex的工具的任何辩论。我的许多工具 很少了解。
我们也从我们自己的前端和我们自己的后端工作。这意味着我们aren' t插入llvm或捆绑 用克兰或类似的东西。我们正在使用具有CMake脚本的ANSI编译器。
如果你自己正在与这个项目和自己的项目并行开发一些东西,我会建议使用所有 这些伟大的工具和资源,它将显着加速您的旅程,您可以与您的方式完成 在完成读取本指南之前的项目。
即使考虑到我们的目标平台和我们即将跳过的技术规格,也会很少 提供或直接引用的实际代码。可能附加资源,但是,一般情况下,将概述 算法方法或在伪代码中描述。逐行上没有理由进行注释 基础。
Lexer的目标是拍摄目标源文件并创建一个我们将调用Lexer令牌的lexemes流。 在内部,我们将表示为链接的元素列表,每个元素都跟踪识别内容的整数 令牌的种类是(符号,关键字或标识符为示例),原始源字符串及其长度以及行 令牌在源文件中显示令牌。
将源文件置于输入时,Lexer将创建一个新的缓冲区并删除冗余空格,单行 评论和多线评论。连续的空格字符将用一个空间替换,除非 Stream包括换行符,在这种情况下,将使用换行符。这个过程用于'条带清洁'来源 在我们直接处理程序源之前的文件。现在,我们将以字符为孤立和孤立的人物 识别构成鸭源代码的令牌。
在这一点上,我们已经有关键字和符号表开始,如果我们不和谐,这是我们的 将生成一个我们可以保存并存储在Lexer中的下一步骤中的一个。无论如何,我们可以列出 我们想要用于我们的编程语言的所有关键字和符号。在这种情况下,这些关键字 是:
导入,包括,返回,休息,继续,抛出,函数,结束,如果,否则,否则,do,do,do,循环,步骤,in, 让,开始,尝试,填写,捕获,对象,静态,运算符,这,以及,或者,不是,是,新的,true,false
其中一些包括我们尚未讨论实施的特征的关键字。这很好,我们可以用它们 保留言语,同时我们考虑添加的添加我们可以在路上的语言。
我们的Lexer将首先查看下一个输入字符。在这里开始,我们会看看第一个 我们的源文件中的非评论字符' s源文本。它可能是一封信,一个数字,另一种类型的字形 像括号或加号,空白字符或换行符。如果它是我们的空白字符 可以忽略它。此时,我们只使用空格作为分隔符。它标志着令牌之间的边界 必须分开,如关键字和标识符名称。并非所有代币都将被空格除以 某些字符可能遵循彼此形成不同的标记。
让&#39假设我们遇到字母表中的一个字符。然后我们可以看一个标识符(我们的东西 在程序中使用以标记变量,过程或字段)或关键字。遵循命名的约定 我们语言中的标识符,我们将允许任何以字母开头的东西并继续使用任何组合 字母,数字或下划线命名变量。所以我们将继续扫描,直到我们达到一个角色 不匹配或直到我们到达输入结束;此时,我们将添加标识符或关键字 我们的lexemes列表。我们必须检查我们的关键字列表以查看此标识符是否实际上是关键字。在 这种情况,我们的Lexer发出了A'令牌'相反,找到此关键字的正确令牌常量,其索引 我们的名单。否则,我们向Lexemes添加标识符令牌并附上标识符的字符串文字。
如果我们遇到一个数字,我们将继续在我们看到数字或小数点之前继续,直到我们看到另一个角色或 到达输入结束。如果此序列包含小数点,那么我们将假设这是一个实数或者 浮点常数。否则,我们将作为整数将此数字解释。我们为整数添加一个令牌 或者漂浮到我们的Lexer标记,并包括当我们需要访问该时序列的文字字符串 价值。
如果我们达到符号字形,我们将用来明确地指的是不是字母数字的字符, WhiteSpace或Newlines,那么我们将找到正确匹配预设集合的最长序列 令牌并将该令牌添加到我们的令牌列表中。当我谈论预设的令牌集合时,我指的是 遵循集合,我们将在下一步中尽快生成。
,=()[] + - * /。 ==!=< > <> =! {}:
注意到我们显着错过了引号,我们必须返回并将字符串添加到我们的集合 Lex,主要是因为我们不想在引用字符串的内部举行' s'我们当然不想要 要解析它们。所以,如果我们遇到一个字符串,或者特别是我们遇到一个字符 这是一个打开的引号,所以一个或双引用,然后我们必须扫描输入,直到找到输入 匹配结束报价。我们'然后是一个
......