65.9K
CodeProject 正在变化。 阅读更多。
Home

为 DLR 生成 AST

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (12投票s)

2008年8月1日

CPOL

7分钟阅读

viewsIcon

49132

downloadIcon

382

了解如何使用抽象语法树为 DLR 生成您自己的代码。

引言

动态语言运行时 (DLR) 是 .NET Framework 3.5 之上的一层,旨在帮助在 .NET 中构建动态语言。DLR 是许多由 Microsoft 提供的动态语言的核心:IronPython、IronRuby、Managed JScript、VBX 等。DLR 以两个 DLL 的形式提供:Microsoft.Scripting.dllMicrosoft.Scripting.Core.dll

astdlrtest

DLR 的主要功能之一是它允许您轻松构建一组语句并将其生成为 IL 代码。因此,在本文中,我将向您展示如何使用抽象语法树,通过 DLR 生成您自己的代码。

抽象语法树

抽象语法树,简称 AST,是一种在内存中分层表示程序的方式。大多数编译器或解释器都使用 AST 来临时存储从源文件读取的程序。在 AST 中,每个节点都是一个元素:语句、运算符或值。这是一个我稍后将使用的 AST 的简单示例

astdlrtest2.png

顾名思义,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 语言和一个宿主进程。让我们总结一下整个过程的工作原理,以解释原因

  1. 一个宿主 .NET 应用程序需要运行脚本,它调用 DLR。此宿主可以是 Silverlight、ASP.NET 或任何其他进程。脚本可以用任何基于 DLR 的动态语言编写。
  2. DLR 为脚本中使用的语言调用正确的引擎。
  3. 此引擎解析脚本,将其转换为 AST,然后将其传回 DLR。
  4. 最后,DLR 将生成的 AST 转换为 IL 代码并运行它。

因此,本文随附的源代码定义了一种名为 ASTDLRTest 的新语言。为此,我实现了两个类:语言上下文和绑定器。请参阅下面的类图

astdlrtest3.png

我在这里只提供了一个非常简单的实现。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,最终版本可能会出现一些更改。

© . All rights reserved.