为 DLR 生成 AST






4.75/5 (12投票s)
了解如何使用抽象语法树为 DLR 生成您自己的代码。
引言
动态语言运行时 (DLR) 是 .NET Framework 3.5 之上的一层,旨在帮助在 .NET 中构建动态语言。DLR 是许多由 Microsoft 提供的动态语言的核心:IronPython、IronRuby、Managed JScript、VBX 等。DLR 以两个 DLL 的形式提供:Microsoft.Scripting.dll 和 Microsoft.Scripting.Core.dll。
DLR 的主要功能之一是它允许您轻松构建一组语句并将其生成为 IL 代码。因此,在本文中,我将向您展示如何使用抽象语法树,通过 DLR 生成您自己的代码。
抽象语法树
抽象语法树,简称 AST,是一种在内存中分层表示程序的方式。大多数编译器或解释器都使用 AST 来临时存储从源文件读取的程序。在 AST 中,每个节点都是一个元素:语句、运算符或值。这是一个我稍后将使用的 AST 的简单示例
顾名思义,DLR 中的命名空间 Microsoft.Scripting.Ast
包含语言中每个可用元素的 AST 节点。本文将探讨 DLR 中主要的现有 AST 节点。
创建程序
为 DLR 构建 AST 的第一件事是创建一个根 LambdaBuilder
对象。此 LambdaBuilder
对象将包含程序的所有语句,并将转换为 LambdaExpression
// Create a lambda expression builder
LambdaBuilder program = AstUtils.Lambda(typeof(object), "ASTDLRTest");
Lambda
方法是一个工厂方法。您必须为其提供数据类型和名称作为参数,但在此处未使用。
赋值语句
现在让我们看看如何创建一些非常简单的语句来为变量赋值,如下例所示
n = 4;
r = n;
DLR 中的语句由派生自 Expression
类的类表示。您可以使用泛型 List
处理一组语句。
// Create a set of statements
List<Expression> statements = new List<Expression>()
要使用 DLR 处理变量,您可以使用 Lambda 表达式方法 CreateLocalVariable
创建它。此处需要两个参数:变量名和数据类型。
// Create two variables
Expression n = program.CreateLocalVariable("n", typeof(int));
Expression r = program.CreateLocalVariable("r", typeof(int));
所以,现在我可以为这些变量赋值
// n = 4
statements.Add(
Expression.Assign(
n,
Expression.Constant(4)
)
);
// r = n
statements.Add(
Expression.Assign(
r,
n
)
);
此处使用了另外两个工厂方法。Constant
方法用于从字面值创建常量表达式。Assign
方法用于为变量赋值。
表达式
表达式的创建方式非常相似,对每种操作都使用一个工厂方法。让我们看看如何构建以下表达式
r = r * (n - 1);
这是要调用的 DLR 代码
// r = r * (n - 1)
statements.Add(
Expression.Assign(
r,
Expression.Multiply(
r,
Expression.Subtract(
n,
Expression.Constant(1)
)
)
)
);
请注意,AST 不需要使用括号语句,因为在评估此表达式时没有歧义。
调用 .NET 方法
现在显示变量 r
的内容会很棒。像这样
Console.WriteLine(r);
DLR 允许您轻松地与 .NET 和 CLR 互操作。以下是用于调用 CLR 方法的 Expression.Call
工厂的签名
MethodCallExpression Expression.Call(MethodInfo method,
params Expression[] arguments);
如果您已经使用过 .NET 中的反射 API,您可能知道 MethodInfo
是表示方法描述的结构。因此,要检索 Console.WriteLine
描述,您应该像这样使用 .NET 反射 API
MethodInfo consoleWriteLine =
typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) });
然后,您可以使用我们之前的 Expression.Call
工厂来写入变量 r
的内容
statements.Add(
Expression.Call(
consoleWriteLine,
r
)
);
循环
现在让我们看看循环语句的方法,更准确地说,是一个 while
语句。以下是我要生成的语句
while (n>1) { r = r * (n-1); n = n - 1; }
要使用 DLR 编写这些语句,我只需要调用一个新的方法工厂 While
。方法如下
statements.Add(
AstUtils.While(
Expression.GreaterThan(
n,
Expression.Constant(1)
),
Expression.Block(
Expression.Assign(r, Expression.Multiply(r,
Expression.Subtract(n, Expression.Constant(1)))),
Expression.Assign(n, Expression.Subtract(n, AstUtils.Constant(1)))
),
AstUtils.Empty(span)
)
);
这里另一个值得注意的节点是 Block
节点。此节点提供了一种一次性组合多个语句的方法。在这里,它允许我为 while
语句执行内部块。
自定义函数
嘿,等等,我知道前面那组语句的名字:它是阶乘!通过这些语句,我们刚刚计算了数字 n
的阶乘值。现在为它编写一个自定义函数会很棒。
这是一个两步过程
- 创建函数:为函数代码创建 AST。
- 调用函数:创建 AST 以使用所需的参数调用函数。
创建函数
在 DLR 中,函数是一个 Lambda 表达式,因此要创建我的新函数,我首先需要一个新的 Lambda 表达式
// Create lambda expression
LambdaBuilder function = AstUtils.Lambda(typeof(int), "fact");
// Declare parameter
Expression n = function.CreateParameter("n", typeof(int));
这次,Lambda 表达式是使用函数返回类型(整数)和函数名称("fact
")创建的。此外,我需要声明函数的所有参数。这里只有一个:"n
" 是用于计算阶乘的值。
fact
函数的源代码以非常相似的方式构建
// Create statements
List<Expression> statements = new List<Expression>();
// r = n
Expression r = function.CreateLocalVariable("r", typeof(int));
statements.Add(
Expression.Assign(
r,
n
)
);
// while(n>1) { r = r * (n-1); n = n - 1; }
statements.Add(
AstUtils.While(
Expression.GreaterThan(
n,
Expression.Constant(1)
),
Expression.Block(
Expression.Assign(r, Expression.Multiply(r,
Expression.Subtract(n, Expression.Constant(1)))),
Expression.Assign(n, Expression.Subtract(n, AstUtils.Constant(1)))
),
AstUtils.Empty(span)
)
);
// return r
statements.Add(
Expression.Return(r)
);
唯一的新内容是我现在使用 return
语句将计算值返回给调用函数。
然后,我需要将语句分配给 Lambda 表达式并将其转换为 Expression
。
// Add statements
function.Body = AstUtils.Block(span, statements);
// Factorial as expression
Expression factorialExpression = function.MakeLambda();
最后,我需要在程序中声明我的 fact
函数。在 DLR 中,函数只是一个变量,其值为 Lambda 表达式。所以,声明 fact
函数的方法如下
// fact = function(n) { ... }
Expression fact = program.CreateLocalVariable("fact", typeof(object));
statements.Add(
Expression.Assign(
fact,
factorialExpression()
)
);
就是这样。我的程序中现在存在一个新的 fact
函数。
调用函数
调用自定义函数比调用 CLR 方法稍微复杂一些。我必须添加一些 DLR 内容来处理动态调用。方法如下
// fact(5)
Expression.Action.Call(
this.Binder,
typeof(int),
Expression.CodeContext(),
fact,
Expression.Constant(5)
);
请注意,我使用 Expression.Action.Call
工厂,其中包含两个主要参数(最后):fact
变量和要传递给函数的值(此处为 5
)。我还必须添加语言的 Binder
(我们很快就会看到)、调用的返回类型和当前代码上下文。
因为调用 fact
而不检索其结果并不是很有趣,所以我最终将此调用嵌入到打印调用中
statements.Add(
Expression.Call(
consoleWriteLine,
Expression.Action.Call(
this.Binder,
typeof(int),
Expression.CodeContext(),
fact,
Expression.Constant(5)
)
)
);
托管 AST
要在前面的示例中运行 AST,我需要一种新的 DLR 语言和一个宿主进程。让我们总结一下整个过程的工作原理,以解释原因
- 一个宿主 .NET 应用程序需要运行脚本,它调用 DLR。此宿主可以是 Silverlight、ASP.NET 或任何其他进程。脚本可以用任何基于 DLR 的动态语言编写。
- DLR 为脚本中使用的语言调用正确的引擎。
- 此引擎解析脚本,将其转换为 AST,然后将其传回 DLR。
- 最后,DLR 将生成的 AST 转换为 IL 代码并运行它。
因此,本文随附的源代码定义了一种名为 ASTDLRTest
的新语言。为此,我实现了两个类:语言上下文和绑定器。请参阅下面的类图
我在这里只提供了一个非常简单的实现。ParseSourceCode
方法(由 DLR 调用)始终返回前面的 AST,无论输入是什么。
以下是我的宿主程序的源代码
static void Main(string[] args)
{
// Create runtime
ScriptRuntime runtime = ScriptRuntime.Create();
// Load Engine
ScriptEngine engine =
runtime.GetEngine(typeof(ADT.DLR.ADTLanguageContext));
// Execute command
ScriptSource src = engine.CreateScriptSourceFromString("" );
src.Execute();
// Shutdown engine
engine.Shutdown();
}
再次强调,这再简单不过了。它创建 DLR 的 ScriptRuntime
,然后加载我的新语言的引擎,最后执行一个脚本(可以使用任何字符串,因为我的语言总是返回相同的 AST)。
有了这几个类,您现在就可以像前面所有示例中一样,使用 AST 的 DLR 了。
结论
本文解释了如何使用 AST 为 DLR 生成您自己的代码。然而,这只是创建您自己的语言过程的一部分。另一部分是创建您自己的解析器以生成与输入脚本对应的 AST。
如果您想了解更多信息,可以查看 MyJScript,这是一个我从头开始创建的类似于 JavaScript 的 DLR 语言,作为 DLR 教程。另一个有趣的示例是 ToyScript,一种类似于 BASIC 的语言,可与 IronPython 一起下载。
最后,请注意本文基于 DLR beta 3,最终版本可能会出现一些更改。