全部:构建您自己的通用语言解释器(谁需要理由?!)






4.92/5 (33投票s)
通过将 GOLD 语法映射到语义节点,使用新的 BSN 引擎来实现语言解释器。
引言
我不确定是什么让我对编程语言如此着迷,但不知为何,我似乎无法摆脱对它们的痴迷。也许是因为那些相对简单的算法和技术,能够将对普通观察者来说像是“黑魔法”的事情变成现实。它们如何处理输入代码,这些代码可以以任意方式构建,长度几乎不限,从而按照用户的(程序员的)意愿执行?我将向您展示如何使用 Gold Parser Builder 和 Arsène von Wyss 全新推出的 BSN-Engine 来构建我自己的解释器。
背景
所有 解释器 通常遵循相同的 基本步骤。本质上,某些输入流被转换为代表其要执行的不同操作的节点的分层树。在构建了这个被称为抽象语法树(AST)的分层结构之后,每个节点都会从下往上进行评估,直到到达根节点,“程序”完成。这些节点可以作为表达式返回给它们的父调用者值,也可以作为语句,仅仅产生“副作用”—对执行环境的更改。(副作用的例子包括设置变量值或向输出缓冲区打印文本。)
我刚刚描述的过程的第一步称为翻译。输入流由一个称为“扫描器”的单元进行处理。当一段输入匹配已知模式时,它就被“识别”为一个称为标记(token)的块。(如果一段输入的模式未能匹配语言中定义的任何标记,则这种情况是一种语法错误,称为标记错误。)然后,标记的顺序和排列会与一系列预期的标记配置模式(称为“规则”)进行比较,直到找到“匹配”。如果在语言的规则中找不到给定标记序列的匹配项,则会出现另一种语法错误,称为“规则错误”。规则错误可能是最常遇到的语法错误类型。它字面上的意思是,在定义的语言语法(通常由语法描述,我们稍后会讲到)中,没有一个规则能够识别当前的标记排列。
乍一看,Devin Cook 的 Gold Parser Builder (GPB) 似乎提供了一个简单的解决方案。它是一个完整的语言语法开发环境。您可以在 IDE 中设计和测试完整的语言语法。甚至还有一个方便的小功能,可以生成骨架代码……对于像我这样偶然发现这个工具包的人来说,它几乎好得令人难以置信。在某种程度上,它确实如此!除了极少数情况,GPB 生成的代码骨架距离任何可以实际称为解释器或编译器的东西还差得很远。用户仍然需要完成一项艰巨的任务,即将来自 GOLD 引擎及其相应代码模板的标记和规则归约转换为 AST,然后转换为可执行形式。您可以选择“硬碰硬”的方式,将每个非终结符规则实现为一个 AST 节点,并“手动”连接它们,这在网上已经有了无数的演示。(CodeProject 上甚至还有一些!)但是,如果您只是想了解它是如何工作的,或者仅仅是为了说您做到了,这需要大量的工作,以至于您可能会感到非常沮丧。也许您甚至会放弃,并感叹道:“好吧,够了。我明白了。我只是不想去做。”(是的,这是我自己在 2006 年左右第一次经历的略带自传色彩的引用。)但是,如果发生这种情况,或者您只是看着这一切,已经开始感到紧张了,请稍等片刻!现在有一个新的方法了!
通常,我避免订阅邮件列表,甚至在公共论坛上使用我的“真实”电子邮件地址时也很谨慎。但我一直坚信 GOLD Parser,自从我第一次发现它以来。我偶尔会收到列表中的邮件。最近,一则来自 Arsène 的特别公告引起了我的注意,他宣布了新的 BSN 引擎的到来。
什么是引擎?
GOLD Parser 系统的工作方式是,您使用 GPB 应用程序创建 LALR(1) 语法,该语法介于 BNF 范式 和 EBNF 范式 之间。它实际上非常易读(对人类也是如此),即使与类似用途的 YACC/Bison 工具相比也是如此。语法在 GPB IDE 中经过验证和测试后,可以用于导出 DFA 和 LALR 解析表(用于识别定义的语言)的二进制表示。这个被称为已编译语法表(CGT)的东西,然后被任意数量的“引擎”或支持系统用来在各种不同的宿主平台上实现生成的解析器。例如,有 C++、Java、D 语言以及 .NET 的引擎。通常,它们都消耗 CGT 并生成一个由语言(或其他基于解析器的工具)实现者处理的产生式堆栈或流(标记的有序排列)。当然,在这一点上,许多繁重的工作已经由构建器和引擎完成了,但仍然有很多工作要做。
引入 BSN
BSN 引擎会消耗和分析 CGT,并从输入流生成标记,但用户(这里用户是指实现语言的程序员)永远不会直接看到标记和产生式!用户看到的所有内容都是被封装到紧凑对象中的标记,这些对象只代表了被翻译的 AST 中最重要的活动部分。其余部分由引擎抽象掉了(引擎本身可能更适合被称为框架。如果您想知道,我不想在评论中争论这个术语的定义。谢谢。)此时,我无法在没有示例的情况下真正描述它,所以我们来举一个例子。
我采用了一个最初由 Devin Cook 设计用于测试 GOLD 引擎的示例语法,进行了一些添加和更改,并将其实现为一个读-求值-打印循环(REPL)解释器。在我将其塑造成的这种形式下,它非常像您以前见过的任何其他命令式语言。让我们先看看语法。
警告:我不会详细解释语法的全部内容,所以如果您需要回顾您的 GOLD 范式,请访问 GOLD 官方网站 http://devincook.com。
语法
"Name" = 'Simple'
"Author" = 'Devin Cook (dave dolan added stuff)'
"Version" = '3.0'
"About" = 'This is a very simple grammar designed for use in examples'
"Case Sensitive" = False
"Start Symbol" = <Statements>
{String Ch 1} = {Printable} - ['']
{String Ch 2} = {Printable} - ["]
Id = {Letter}{AlphaNumeric}*
! String allows either single or double quotes
StringLiteral = '' {String Ch 1}* ''
| '"' {String Ch 2}* '"'
NumberLiteral = {Digit}+('.'{Digit}+)?
<Statements> ::= <StatementOrBlock> <Statements>
| <StatementOrBlock>
<StatementOrBlock> ::= <Statement>
| begin <Statements> end
<Statement> ::= print <Expression>
| print <Expression> read ID
| ID '=' <Expression>
| loop <Statements> while <Expression>
| for '(' <Statement> ';' <Expression> ';' <Statement> ')' do <Statements> end
| while <Expression> do <Statements> end
| if <Expression> then <Statements> end
| if <Expression> then <Statements> else <Statements> end
| function ID '(' <OptionalParamList> ')' begin <Statements> end
| return <Expression>
<OptionalParamList> ::= <ParamList>
|
<ParamList> ::= ID ',' <ParamList>
| ID
<Expression> ::= <Expression> '>' <Add Exp>
| <Expression> '<' <Add Exp>
| <Expression> '<=' <Add Exp>
| <Expression> '>=' <Add Exp>
| <Expression> '==' <Add Exp>
| <Expression> '<>' <Add Exp>
| <Add Exp>
<Add Exp> ::= <Add Exp> '+' <Mult Exp>
| <Add Exp> '-' <Mult Exp>
| <Add Exp> '&' <Mult Exp>
| <Mult Exp>
<Mult Exp> ::= <Mult Exp> '*' <Negate Exp>
| <Mult Exp> '/' <Negate Exp>
| <Negate Exp>
<Negate Exp> ::= '-' <Value>
| <Value>
<Value> ::= ID
| StringLiteral
| NumberLiteral
| ID '(' <OptionalArgumentList> ')'
| '(' <Expression> ')'
<OptionalArgumentList> ::= <ArgumentList>
|
<ArgumentList> ::= <Expression> ',' <ArgumentList>
| <Expression>
是的,就这些。这一点就定义了整个语言的语法。您可以用 Simple 3 编写的任何程序都编码在这么小的空间里。`::=` 左边的称为产生式,右边的称为规则。如果解析器匹配了产生式的任何一个规则,标记流以及它可能包含的其他产生式就会被“归约”到该产生式。如果您想执行一个产生式,您必须知道它内部所有“有分量”的细节。例如,查看 `
现在,这部分的编码实际上非常基础。首先,我必须构建一些支持结构来充实我刚才所说的内容。
语句可以被执行,但返回任何值
public abstract class Statement : Simple3Token{
public abstract void Execute(Simple3ExecutionContext ctx);
}
表达式返回一个值
public abstract class Expression : Simple3Token{
public abstract object GetValue(Simple3ExecutionContext ctx);
}
由于 Statement 和 Expression 类代表产生式,而不是可以归约到它们的特定规则实现的数量,所以我们使用 `abstract` 类和 `abstract` 方法来实现所需行为。
什么是执行上下文?
如果您已经知道什么是执行上下文,您会认为它听起来就像它的字面意思。但是,如果您对此类内容不熟悉,请不用担心,它很容易理解。基本上,它是用户代码执行的环境。在“现实世界”中,执行上下文可能是非常复杂的庞然大物,充满了有助于支持优化和各种其他功能的特性。对于我的 Simple 3 示例,它是一个仅包含一个哈希表来存储所有变量,一个用于函数调用的返回值堆栈,一个指向父上下文的引用,以及指向输入和输出流的引用的结构。主程序有一个全局执行上下文,每个函数调用都有自己的执行上下文。所有执行上下文共享一个指向单个全局返回值堆栈的引用,尽管它们有自己的变量存储。当引用一个变量时,它首先在当前执行上下文中进行检查。如果找不到变量,则将其委托给父上下文。如果一直回溯到最顶层,仍然找不到引用的变量,则会抛出一个运行时异常,表明这是一个未定义的变量。(需要明确的是,在此语言中定义变量的方法是为其赋值。在此语言中,定义变量不应与“变量声明”的概念混淆,后者在许多其他语言中都能找到。您不能在 Simple 3 中声明变量。)
我们如何将语法中的规则转化为调用这些微小对象的代码?
在以前的 GOLD 时代(指向 BSN 引擎出现之前的世界),人们会编写一大段代码来消耗 GOLD 引擎向下传递的产生式列表,并为每个终结符和非终结符创建容器对象。每个非终结符容器本身就是一个包含更多子对象的容器,这些子对象必须以相同的方式被消耗。子对象被称为抽象语法节点。在您使用大多数 GOLD 引擎时,构建这个容器的容器及其子容器是之前提到的 AST。在能够消耗 AST 之前构建 AST 是工作的主要部分。要从 AST 创建解释器(这是通往工作语言实现的阻力最小的路径),您需要添加一些方法来执行或评估每个节点(相对于执行上下文)。这本质上是创建“可执行 AST”。这就是我在 Simple 3 示例中所做的,节点从标记创建的过程仍然像我描述的那样发生,但 BSN 引擎将其中大部分隐藏起来,用户(语言实现者)看不到。使用 BSN 引擎,树的构建在设计时由用户“映射”,方法是为那些“做事情”的节点添加注解,用它们代表的语法片段来简化版本。(行动节点通常在其他文献中被称为“语义动作”。)
仔细查看 AST
AST 并不复杂。它只是一个表示“程序”的节点图。
在 AST 中,操作或语义动作是节点(非终结符节点),操作数是子节点。操作数可以由其他非终结符节点组成,或者由终结符节点组成,终结符节点是代表值、标点符号或关键字的具体语法元素。最深的子节点(叶节点)被求值,并将它们的值作为操作数传递给父操作,直到最终执行最顶层的操作。
让我们从小处着手,看看表达式 1 + 2 * 3 的 AST。
简单的 AST
当这个表达式被求值时,最深的非终结符是 '*',其叶节点是 '2' 和 '3'。当然,这将是 2 * 3,求值后是 6。将其传递给上一级 '+' 运算符作为第二个操作数,此时整个树变为
当然,现在是 1 + 6,很容易看出它将求值为 7。
非终结符表示的操作可能比简单的算术运算符复杂得多,但这正是 AST 的生命力和运作方式。
一旦您使用 GPB 从语法生成 CGT,BSN 引擎就能非常轻松地生成 AST。
从简单开始
我曾经见过的几乎所有关于如何构建编译器或解释器的例子都使用简单的算术表达式,然后就停止了。这并不难想象,我是说,从已归约的产生式堆栈中连接节点确实很麻烦。但我决定我不会成为那样的人,而是要处理整个语言。
我知道所有二元运算符都有两个操作数,一个左操作数和一个右操作数。这确实是所有二元运算符共有的唯一特性,所以我创建了一个 `abstract` 类 `BinaryOperator`,它有一个抽象方法。我可能也不想让求值过程试图寻求类型一致性,因为我知道某种类型的运算符总是有一个可预测类型的操作数。为此,我将有一个标志,允许跳过类型检查器。
public abstract class BinaryOperator : Simple3Token{
public virtual bool SkipConversion{
get { return false; }
}
public abstract object Evaluate(object left, object right);
}
这些运算符的具体实例通过用 `Terminal` 属性进行装饰,映射到语法终结符(这意味着它们不能归约成包含其他构造的更深层次的构造)。例如,“+”运算符如下所示
[Terminal("+")]
public class PlusOperator : BinaryOperator{
public override bool SkipConversion{
get { return true; }
}
public override object Evaluate(object left, object right){
return Convert.ToDouble(left) + Convert.ToDouble(right);
}
}
该属性是 BSN 引擎在为特定语法构建节点类时用来抓取的“句柄”。基本上,它的意思是“当您看到一个‘+’终结符时,创建此类的实例并将其放入 AST 中。”
这让我们对 BSN 引擎在构建 AST 过程中带来的便利性有了一些了解。我们只需声明当您遇到此终结符时将实例化此类。完成。其余的都由引擎完成,对我们进行了抽象。
这很不错,但真正感觉像黑魔法的部分是我们进入非终结符的时候。非终结符节点不装饰类本身,而是装饰构造函数,并带有引起其被调用的语法规则的实际文本表示。如下所示,您可以为多个可能的变体串联许多这样的规则。
BinaryOperation 类
public class BinaryOperation : Expression{
private readonly Expression _left;
private readonly BinaryOperator _op;
private readonly Expression _right;
[Rule(@"<Expression> ::= <Expression> '>' <Add Exp>")]
[Rule(@"<Expression> ::= <Expression> '<' <Add Exp>")]
[Rule(@"<Expression> ::= <Expression> '<=' <Add Exp>")]
[Rule(@"<Expression> ::= <Expression> '>=' <Add Exp>")]
[Rule(@"<Expression> ::= <Expression> '==' <Add Exp>")]
[Rule(@"<Expression> ::= <Expression> '<>' <Add Exp>")]
[Rule(@"<Add Exp> ::= <Add Exp> '+' <Mult Exp>")]
[Rule(@"<Add Exp> ::= <Add Exp> '-' <Mult Exp>")]
[Rule(@"<Add Exp> ::= <Add Exp> '&' <Mult Exp>")]
[Rule(@"<Mult Exp> ::= <Mult Exp> '*' <Negate Exp>")]
[Rule(@"<Mult Exp> ::= <Mult Exp> '/' <Negate Exp>")]
public BinaryOperation(Expression left, BinaryOperator op, Expression right){
_left = left;
_op = op;
_right = right;
}
public override object GetValue(Simple3ExecutionContext ctx){
object lStart = _left.GetValue(ctx);
object rStart = _right.GetValue(ctx);
object lFinal;
object rFinal;
if (!_op.SkipConversion){
TypeChecker.GreatestCommonType gct = TypeChecker.GCT(lStart, rStart);
switch (gct){
case TypeChecker.GreatestCommonType.StringType:
lFinal = Convert.ToString(lStart);
rFinal = Convert.ToString(rStart);
break;
case TypeChecker.GreatestCommonType.BooleanType:
lFinal = Convert.ToBoolean(lStart);
rFinal = Convert.ToBoolean(rStart);
break;
case TypeChecker.GreatestCommonType.NumericType:
lFinal = Convert.ToDouble(lStart);
rFinal = Convert.ToDouble(rStart);
break;
default:
throw new TypeMismatchException( );
}
}
else{
// let's not put square pegs in round holes.
// Sometimes the operators can 'universally convert'
// like the '&' operator will always convert to a string. Always.
lFinal = lStart;
rFinal = rStart;
}
return _op.Evaluate(lFinal, rFinal);
}
}
引擎将通过匹配具有适当规则的类来确定为“left”和“right”参数填写哪种类型的表达式。这是递归完成的,直到整个树构建完毕。我们所要做的就是指定与右侧构造函数中的类匹配的规则,并实现最少数量的非终结符节点。
哪些非终结符需要实现为类?
一些非终结符产生式包含纯粹由其他非终结符组成的规则,因此不需要具体的类。这些被称为:瞬时非终结符。它们用于解决冲突,或将组合的非终结符粘合在一起。这些瞬时非终结符不代表任何特定的可执行代码,所以我们不需要在源代码中对它们做任何事情。在 GOLD 引擎的术语中,一个省略瞬时非终结符的已归约产生式堆栈被称为已修剪。如果您查看 GPB 中的语法测试器工具,您会看到一个“Trim Reductions”选项。通过勾选此框,您可以看到它的作用,最终的产生式堆栈将不包含瞬时标记。
当然,在使用 BSN 引擎实现节点时,没有需要勾选的框。只需“忘记”实现它们,它们就会被视为瞬时(或者,如果它们确实不是瞬时的,则会出错,我们稍后会看到。)
我们如何确定肯定完成了?
BSN 引擎的一个标志性功能是,当它初始化并加载语法时,它会全面分析所有节点的结构,并会吐出任何发现错误的报告。为了演示,我将注释掉 `PlusOperator` 的 `Terminal` 属性并再次尝试运行代码。
语义引擎发现错误:终结符 '+' 缺少语义标记
由规则 '+' 使用的类型 SimpleREPL.Simple3.BinaryOperation 的工厂期望在索引 1 处接收一个 SimpleREPL.Simple3.BinaryOperator,但接收的是 SimpleREPL.Simple3.Simple3Token。
“哇,哦,是的,我忘了为 `PlusOperator` 类添加 `Terminal` 属性了!”节省了我大量时间!找到您遗漏的标记并不是该分析所能实现的唯一技巧。您还可以对新的 CGT 文件运行它,它会好心地打印出您必须实现才能使其工作的类的列表。有点像飞行前检查表,您可以将其划掉(或者重新运行程序生成新列表),然后您就知道何时可以开始工作了。这是 REPL 开头用于以正确方式初始化所有内容的代码,以便在出现问题时将此列表打印到控制台。
// this is lifted directly (almost) from the example code that comes with the engine
CompiledGrammar grammar = CompiledGrammar.Load(typeof (Simple3Token), "Simple3.cgt");
var actions = new SemanticTypeActions<Simple3Token>(grammar);
try{
actions.Initialize(true);
}
catch (InvalidOperationException ex){
Console.Write(ex.Message);
Console.ReadKey(true);
return;
}
那么“序列”非终结符呢?
很高兴您问到序列!序列是非终结符标记,它们可以选择性地包含零到 n 个子标记的数组。例如
<Statements> ::= <StatementOrBlock> <Statements>
| <StatementOrBlock>
多个语句标记连续出现,或者仅仅一个孤立的语句标记匹配 `
有一个特殊的模式构造,von Wyss 在 BSN 项目网站的 wiki 部分 中进行了说明,称为序列模式。对于 Simple 3,所有序列(列表类型标记)都映射到此类的构造函数。
Sequence 类
public class Sequence<T> : Simple3Token, IEnumerable<T> where T : Simple3Token{
private readonly T item;
private readonly Sequence<T> next;
public Sequence()
: this(null, null){
}
[Rule(@"<ParamList> ::= Id", typeof (Identifier))]
[Rule(@"<ArgumentList> ::= <Expression>", typeof (Expression))]
[Rule(@"<Statements> ::= <DelimitedStatement>", typeof (Statement))]
public Sequence(T item)
: this(item, null){
}
[Rule(@"<ParamList> ::= Id ~',' <ParamList>", typeof (Identifier))]
[Rule(@"<ArgumentList> ::= <Expression> ~',' <ArgumentList>", typeof (Expression)]
[Rule(@"<Statements> ::= <DelimitedStatement> <Statements>", typeof (Statement))]
public Sequence(T item, Sequence<T> next){
this.item = item;
this.next = next;
}
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator(){
for (Sequence<T> sequence = this; sequence != null; sequence = sequence.next){
if (sequence.item != null){
yield return sequence.item;
}
}
}
IEnumerator IEnumerable.GetEnumerator(){
return GetEnumerator();
}
#endregion
}
您可能会注意到 `Rule` 属性现在有更复杂的参数。
在此示例中,由于 `ArgumentLists` 中有一个逗号分隔每个组成表达式,所以我们想跳过它。规则中只有对我们正在构建的节点有贡献的部分是第一个 `
[Rule(@"<ArgumentList> ::= <Expression> ~',' <ArgumentList>", typeof (Expression)]
再说一遍,类型检查器是做什么的?
在这种情况下,类型检查器会查看多个参数,确定它们的“最大公约数类型”或它们都可以转换为用于操作的类型。如果您尝试将数字 12 添加到字符串“12
”,它会识别在这种情况下最大公约数类型是“数字”,因为两者都可以转换为数字。Simple 3 执行此操作的方式并不是一个适用于所有语言的万无一失的计划,但它足以满足此解释器的目的。(Simple 3 支持非常有限的类型集,并且不允许用户创建自己的类型。)
那么,我们示例语言中的“代码”看起来是什么样的?
这里有一些一个人必须使用的典型示例来证明一种语言确实是一种语言。
Hello world
print "hello world"
99瓶啤酒
x = 99
while (x > 0) do
print x & " bottles of beer on the wall. Take one down and pass it around..."
x = x - 1
end
或者
for ( x = 99; x > 0; x = x - 1) do
print x & " bottles of beer on the wall. Take one down and pass it around..."
end
如果您愿意,可以将这些东西写在一行上。这很酷,当然,这是一种我们可能并不真正需要但确实是一种语言,但关键是我们有一个有效的解释器,而且它运行良好!玩这个的最佳方式是运行下载中附带的 REPL 应用程序。(只需将项目文件加载到 Visual Studio 2010 中 – 即使是 Express Edition 也能正常工作,或者 MonoDevelop 2.4 或更高版本,然后进行调试,您就可以开始了。)
REPL
您上面下载的 REPL 解释器实际上使用单个执行上下文作为“全局”环境。它读取一行文本并将其聚合到一行输入中,直到遇到两个空行。当遇到两个空行时,前面的文本将被作为输入代码进行处理(解析和求值)。您可以将任何内容拆分成多行,但不能在它们之间留有空行,否则 REPL 会在您完成代码片段之前开始处理,并会抛出语法错误。
> 表示“输入开始”,而“Ok.”表示在执行之前,您刚刚输入的内容已被读取为有效的代码。
注意:您可以通过输入“timer”来切换计时器功能,顾名思义,该功能将在开启时进行计时。特别是,解析和执行(每个单独计时。)
> timer
Timer toggled to ON.
> print "hello world"
Parse: Elapsed: 1ms
Ok.
hello world
Execution: Elapsed: 1ms
> timer
Timer toggled to OFF.
>
伪(但技术上真实)语言的奇怪特性
您可以声明函数并调用它们。您可以在函数内部声明函数。如果您给一个函数起了一个在父作用域中已经使用的名字,那么您就“隐藏”了该作用域内的父函数。本质上,这意味着最深层的函数获胜。
> function abc(x, y)
begin
function abc(g,h)
begin return g + x + h end
return x + y + abc(x,y)
end
Ok.
> print abc(1,2)
Ok.
7
>
是不是很疯狂?我不确定您为什么要这样做,但这就是语言的工作方式。
为什么(以及如何?)
每个函数调用,就像任何语句或表达式一样,都在一个执行上下文中执行。在 Simple 3 中,当您调用一个函数时,会创建一个新的上下文,该上下文与父上下文共享输入/输出流,共享返回值堆栈,但拥有自己的函数定义集和自己的变量集。当它找不到引用的函数或变量时,它会将引用传递给父上下文进行检查……以此类推,直到最顶层。如果到最顶层仍然找不到,则会抛出异常。这种行为自然会导致“最深层的获胜”,而无需任何特殊考虑。
看看代码。看看引擎网站。玩玩 Gold Parser Builder,看看您能想出什么。这很有趣,信不信由您,理解这个过程如何工作为您提供了很多基本的见解,这些见解通常适用于计算的许多领域。您将了解解析器如何识别文本。您将了解运算符优先级是如何实现的。您将可视化函数调用是什么。过了一段时间,您会发现如何在您的代码的故障排除和调试过程中,利用这种对编译器如何安排您的代码到可执行细节的新视角。
我的语言,正如其名称所示,以及这个解释器示例实现,都确实很简单,而且它们没有针对可用性、可读性或执行速度进行优化;但是,从概念上讲,所有部分都存在:输入识别、转换为 AST,以及最终的代码执行。
要了解某个语言特性是如何实现的,我建议您在节点代码中设置一些断点,并在其运行时进行查看。这可能是完全理解每个功能如何工作的最佳方式。如果您想改进 Simple 3,或者觉得我应该以不同的方式展示某些内容,请告诉我,我会更新这篇文章和代码(也许)。
关于代码
对任何特定 IDE 都没有实际依赖,但项目文件是使用 VS 2010 构建的。它以 .NET 3.5(Mono 2.6)为目标,并且也可以在 MonoDevelop 2.4 版本中构建和运行。(它可能在早期版本的 MonoDevelop 中也能正常工作,但在 Linux 主机上使用 2.4 版本进行了测试。)
注意:此项目的源代码已添加到 Google Code 存储库,并将在此处更新。
历史
- 2010 年 11 月 22 日:第一版,热乎乎出炉