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

使用 Aphid(一种可嵌入的脚本语言)使 .NET 应用程序可脚本化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (38投票s)

2013年10月17日

GPL3

24分钟阅读

viewsIcon

105962

downloadIcon

983

本文详细介绍了如何使用 Aphid(一种可嵌入的脚本语言)使 .NET 应用程序可脚本化。

重要更新

2018年4月22日更新

Aphid 在过去一年中经历了大量开发,自上次更新以来进行了超过 500 次提交。Aphid 已不再是最初计划的嵌入式脚本语言,而是一种功能齐全的 .NET 编程语言,具有完全推断的无缝 CLR 互操作性,包括对泛型和委托的支持。还支持显式泛型类型引用,允许实例化诸如 List<T>Dictionary<TKey, TValue> 之类的类型,而无需借助反射。另外值得注意的是许多开发工具的改进,包括经过大修的 REPL,具有语法高亮显示和详细的自动完成功能,用于语法高亮显示和调试的 Visual Studio Code 扩展,以及现在兼容 VS2012 到 2017 的 Visual Studio 语法高亮显示和错误检查。

有关此重大修订引入的更改的更详细列表,请参阅本文末尾的历史记录部分

2016年4月26日更新

此项目已移至 GitHub:https://github.com/John-Leitch/Aphid/

此页面上附加的版本可能不是最新版本。要获取最新版本的 Aphid,请访问GitHub 发布页面。

注意:由于技术原因,在线编辑器在一段时间内将不会看到更新。当前版本在线是为了兼容旧版,不建议使用。

出于评估目的,已创建了一个在线编辑器。可以在这里找到它:http://autosectools.com/Try-Aphid-Online

目录

  1. 引言
  2. 为什么要再搞一种脚本语言?
  3. 语法
    1. 基础
    2. 程序结构
    3. 控制结构
  4. 类型系统和类型 
  5. Hello World
  6. 列表
  7. 函数
    1. 高阶函数
    2. 局部函数应用
    3. 管道
    4. 扩展方法
  8. 处理解析器异常
  9. .NET 互操作性
    1. 从 Aphid 访问 .NET 变量
    2. 从 Aphid 调用 .NET 函数
    3. 从 .NET 调用 Aphid 函数
    4. 对象互操作性
    5. 无缝 .NET 互操作性
  10. 内部
    1. Mantispid:强大的词法分析器和解析器生成器
    2. 词法分析
  11. 资源
  12. 历史

引言

Aphid 是一种可嵌入、跨平台、多范式且高度可互操作的 .NET 脚本语言。它是一种 C 风格的语言,大量借鉴了 JavaScript,但也受到 C#、F#、Python、Perl 和其他语言的启发。Aphid 解释器完全用 C# 和 Aphid 实现,目标是完全自举。本文旨在介绍 Aphid,因此只涵盖了一些可用功能。

该项目目前处于测试阶段,因此随着它的发展,本文也会随之变化和增长。要获取最新版本的 Aphid,请访问GitHub 发布页面。

为什么要再搞一种脚本语言?

目前,适用于 .NET 平台的易于嵌入的脚本语言很少。在现有语言中,许多都存在多个依赖项,需要包含各种程序集。还有一些语言缺乏互操作性,需要大量的连接代码。Aphid 旨在通过提供一种易于嵌入、高度可互操作的脚本语言来解决这些问题,该语言包含在一个 DLL 中。

此外,大多数语言实现都是黑盒。Aphid 的设计和构建旨在在每个层面上都是白盒;从词法分析器到解释器,所有内部结构都通过干净的对象模型暴露。这使得 Aphid 可以作为快速开发领域特定语言的框架。

语法

作为一种大量借鉴 JavaScript 的 C 风格语言,Aphid 的语法应该为大多数程序员所熟悉。

基础

语句

在大多数情况下,语句是一个表达式后跟一个分号 (;)。但是,对于某些语句,例如 if/elseswitch,分号 (;) 会被省略。

// A statement terminated by a semicolon.
foo = true;

// An if statement without a semicolon.
if (foo) {
    print('bar');
}

与许多其他 C 风格语言不同,Aphid 不容忍多余的分号 (;)。

x = 1;; // This semicolon will cause a syntax error.

if (foo) {
    print('bar');
}; // As will this one.

数字

数字字面量可以十进制、十六进制或二进制形式书写。

x = 1;

x = 3.14159265359;

x = 0xDEADBEEF;

x = 0b01100000;

布尔值

布尔值可以使用 truefalse 关键字指定。

x = true;

x = false;

字符串

字符串字面量可以使用单引号或双引号书写。反斜杠字符 (\) 用于转义序列。

x = "Hello world";
    
x = 'foobar\r\n';

列表

列表由用方括号 ([ ... ]) 括起来的逗号 (,) 分隔的元素组成。允许使用尾随逗号。

x = [ 5, 2, 3, 0 ];

可以通过使用方括号 ([ ... ]) 进行索引来访问列表元素。

y = x[1];

函数

函数可以使用典型的 C 风格语法调用。

func('test');

它们也可以使用管道运算符 (|>) 调用。

'test' |> func;

函数使用函数运算符 (@) 声明。参数用括号 (( ... )) 括起来并用逗号 (,) 分隔。

add = @(x, y) {
    ret x + y;
};

如果函数体是单个 return 语句,则可以更简洁地将其写为 lambda 表达式。

// This is semantically identical to the previous example.
add = @(x, y) x + y;

如果未指定参数,则可以省略括号。

foo = @{
    /* do something */
};

函数运算符 (@) 也用于执行局部函数应用。

add4 = @add(4);

同样,函数运算符 (@) 也可以用于二元运算符以执行局部运算符应用。

// This:
add4 = @+ 4;
// Is similar to this:
add4 = @(x) x + 4;

当在函数表达式或局部应用中使用时,函数运算符 (@) 可以用于创建隐式管道。

// This:
4 |> @add(2) |> print;
// Is the same as this:
4 @add(2) print;

函数组合运算符 (@>) 可以用于将函数链接在一起。

halfSquare = square @> divideBy2;
10 |> halfSquare |> print;

对象

对象由用大括号 ({ ... }) 括起来的逗号 (,) 分隔的名称-值对组成。名称和值之间用冒号 (:) 分隔。允许使用尾随逗号。支持嵌套对象。

widget = {
    name: 'My Widget',
    location: { x: 10, y: 20 },
    data: [ 0xef, 0xbe, 0xad, 0xde ],
};

在对象初始化器中引用变量时,如果名称匹配,则可以省略名称-值对的右侧。

data = [ 0xef, 0xbe, 0xad, 0xde ];

widget = {
    name: 'My Widget',
    location: { x: 10, y: 20 },
    data,
};

成员访问运算符 (.) 可用于获取和设置成员。

widget.location.x = 10;
print(widget.location.x);

可以使用大括号 ({ ... }) 动态访问成员。

key = 'location';
widget.{key}.y++;
print(widget.{key}.y);

Null (空值)

null 值可以使用 null 关键字指定。

x = null;

程序结构

Aphid 脚本的程序结构取决于初始词法分析器模式。在标准模式下,词法分析器假定文件以 Aphid 代码开头并按此方式解析。但是,当设置为文档模式时,词法分析器假定文件以原始文本开头,并在遇到 gator (<% ... %>) 或 gator emit (<%= ... %>) 标记时在模式之间切换。在解释期间,除非另有指定,否则文本会写入控制台。

在标准模式下标记的典型程序可能看起来像这样

#'std';
x = 10;
x *= 2;
print(x);

而在文档模式下标记的程序可能看起来像以下内容

Hello <% 
    #'std';
    print('world');
%>

gator emit (<%= ... %>) 标记可以用于简洁地发出表达式。

Hello <%= 'world' %>

请注意没有分号;在打开的 gator emit 标记之后预期是表达式,而不是语句。

控制结构

if/else

条件语句可以使用 ifelse 关键字声明。

#'Std';
x = 2;

if (x == 1) {
    print('one');
} else if (x == 2) {
    print('two');
} else {
    print('not one or two');
}

如果块是单个语句,则可以省略大括号 ({ ... })。

#'Std';
x = 2;

if (x == 1)
    print('one');
else if (x == 2)
    print('two');
else
    print('not one or two');

switch

Aphid 的 switch 语法与典型的 C 风格 switch 大相径庭。Aphid 不使用 switchcase 关键字,而是使用冒号 (:) 和大括号 ({ ... }) 以与其他表达式类型保持语法一致性。default 关键字用于指定当没有其他匹配时执行的 case。

#'Std';
x = 20;
switch (x) {
    1: {
        print('One');
    }
    2: {
        print('Two');
    }
    default: print('Default');
}

与其它代码块一样,如果块是单个语句,则可以省略大括号 ({ ... })。

#'Std';
x = 20;
switch (x) {
    1: print('One');
    2: print('Two');
    default: print('Default');
}

for

for 关键字可用于声明具有初始化、条件和后置操作的传统 for 循环。

#'Std';
l = [ 1, 2, 3 ];

for (x = 0; x < l.count(); x++) {
    print(l[x]);
}

for/in

当与 in 关键字结合使用时,for 声明一个 for-each 循环。

#'Std';
l = [ 'a', 'b', 'c' ];

for (x in l) {
    print(x);
}

while

可以使用 while 关键字声明 while 循环。

#'Std';
x = 0;

while (x < 5) {
    x++;
    print(x);
}

条件运算符 (?:)

条件运算符 (?:) 是一个三元运算符,可用于根据条件选择两个值之一。第一个操作数是条件,第二个是条件评估为真时选择的值,第三个是条件为假时选择的值。

#'Std';

x = 10;
print(x == 10 ? 'x is 10' : 'x is not 10');

try/catch/finally

异常处理语句可以使用 trycatchfinally 关键字声明。异常处理语句必须包含 try 块以及 catch 和/或 finally 块。可以为 catch 块指定可选的异常参数。

#'Std';

// A typical try/catch statement.
try {
    1/0;
} catch(e) {
    print(e);
}

// A try/catch statement with a finally block.
try {
    1/0;
} catch(e) {
    print(e);
} finally {
    print('done');
}

// try/catch without an exception argument
try {
    1/0;
} catch {
    print('error');
}

类型系统和类型

Aphid 使用多模式类型系统;在处理内置 Aphid 类型时使用鸭子类型,而在与 .NET 类型互操作时使用强类型。Aphid 类型系统将进行有限的类型强制转换,这更接近 C# 而不是 JavaScript 和 PHP 等典型的脚本语言。例如,在表达式 'foo' + 10 中,number 字面量 10 将自动转换为 string。但是,在大多数其他情况下,表达式的类型必须匹配。这是为了防止常见的编程错误,并且对于 boolean 表达式尤其有用。

有七种内置类型:stringnumberbooleanlistobjectfunctionnull。下表显示了 Aphid 类型如何映射到 .NET 类型。

Aphid 类型 .NET 类型
字符串 System.String
number System.Decimal
布尔值 System.Boolean
列表 System.Collections.Generic.List<AphidObject>
object AphidObject : System.Collections.Generic.Dictionary<string, AphidObject>
function AphidFunction
null null

Aphid 的基本类型以及 list 是不言自明的,任何问题都可能在 .NET 类型的 MSDN 页面上找到答案。AphidObject 类因其行为和广泛使用而特别受关注。除了互操作 .NET 类型之外,Aphid 中的每种类型都是 AphidObject。对象的实际值包含在 AphidObject.Value 属性中,该属性是一个 System.Object,而其成员则存储为字典项。除了值之外,AphidObject 还用于管理词法作用域。在解析期间,AphidObject.Parent 属性(它也是 AphidObject 类型)用于搜索外部作用域。

AphidFunction 类用于表示 Aphid 函数。与其他函数式语言一样,函数在 Aphid 中是值。事实上,甚至没有函数声明语句语法;所有函数都是表达式。

你好,世界

Aphid 的入门只需要最少的设置。首先,添加对 Components.Aphid.dll 的引用。接下来,实例化 AphidInterpreter。最后,调用实例方法 AphidInterpreter.Interpret 以执行 Aphid 脚本。轻松,对吧?下面列出了一个完整的 C#/Aphid“Hello world”程序,见列表 1。

列表 1. 一个简单的 C#/Aphid hello world 程序
using Components.Aphid.Interpreter;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                print('Hello, world');
            ");
        }
    }
}

应用程序的 C# 部分应该是不言自明的。然而,Aphid 程序需要稍作解释。该程序由两个语句组成。

第一个是加载脚本语句,由加载脚本运算符 (#) 和 string 操作数 'Std' 组成。默认情况下,Aphid 加载器首先搜索 Components.Aphid.dll 所在目录的 Library 子目录。加载器会自动将 ALX 扩展名附加到传入的脚本名称,因此在此实例中它会查找 <dll>\Library\Std.alx。假设一切正常,它应该会找到并加载该文件,该文件是标准的 Aphid 库,包含用于操作字符串、打印控制台输出等的有用函数。

第二个语句是一个调用表达式,它调用 print,这是一个 Aphid 标准库中的函数。这行代码应该是不言自明的。

当程序运行时,输出如预期所示(列表 2)。

列表 2. hello world 程序的输出
Hello, world
Press any key to continue . . .

列表

list 类型与底层 System.Collections.Generic.List<T> 类型行为大致相同。可以使用方括号 ([ ... ]) 按索引访问元素,或者使用 for/in 语句枚举。列表是可变的,因此可以在创建后修改。需要注意的是,列表是引用类型,因此在传递时不会进行复制。对列表的修改将影响所有引用。

下面的示例演示了几个 list 操作。首先,使用方括号 ([ ... ]) 创建一个 list。正如初始化元素所演示的,列表可以包含混合类型。创建后,调用 add 方法将另一个 string 添加到 list。接下来,使用 countcontains 方法打印有关 list 的信息。最后,使用 for/in 语句枚举并打印 list

一个演示列表类型的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace ListSample
{
    class Program
    {
        static void Main(string[] args)
        {
            new AphidInterpreter().Interpret(@"
                #'std';
                list = [ 10, 20, 'foo' ];
                list.add('bar');

                printf(
                    'Count: {0}, Contains foo: {1}',
                    list.count(),
                    list.contains('foo'));
                
                for (x in list)
                    print(x);
            ");
        }
    }
}

函数

Aphid 函数通过使用函数运算符 (@) 定义。由于函数在 Aphid 中是一等公民,它们可以存储在变量中(列表 3)。

列表 3. 一个定义和调用 Aphid 函数的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace FunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';

                add = @(x, y) {
                    ret x + y;
                };

                print(add(3, 7));
            ");
        }
    }
}

程序的输出如列表 4 所示。

列表 4. 函数示例的输出
10
Press any key to continue . . .

我们的 add 函数很好,但是可以使用一些人可能熟悉的语言特性使其更简洁:lambda 表达式。

Lambda 表达式

Aphid lambda 表达式是由单个表达式构成的特殊函数。当调用 lambda 表达式时,会评估该表达式并返回其值。

由于前面示例中 add 函数的主体由单个返回语句组成,因此可以将其重构为 lambda 表达式(列表 5)。

列表 5. 一个定义和调用 Aphid lambda 表达式的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace LambdaSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';
                add = @(x, y) x + y;
                print(add(3, 7));
            ");
        }
    }
}

程序的输出如列表 6 所示。

列表 6. lambda 示例的输出
10
Press any key to continue . . .

高阶函数

Aphid 函数是值。这使得高阶函数(即接受和/或返回其他函数的函数)成为可能。列表 7 显示了一个高阶 Aphid 函数。

列表 7. 一个定义和调用高阶 Aphid 函数的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace HigherOrderFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';
                call = @(func) func();
                foo = @() print('foo() called');                
                call(foo);
            ");
        }
    }
}

程序的输出如列表 8 所示。

列表 8. 高阶函数示例的输出
foo() called
Press any key to continue . . .

局部函数应用

局部函数应用可以用于将参数应用于给定函数,从而生成一个接受剩余未应用参数的新函数。局部函数应用使用函数运算符 (@) 执行。列表 9 显示了一个名为 add 的函数,它被局部应用于数字字面量 10 以生成一个新函数 add10。列表 10 显示了示例程序的输出。

列表 9. 一个演示局部函数应用的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace PartialFunctionApplicationSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                add = @(x, y) x + y;
                add10 = @add(10);
                print(add10(20));
            ");
        }
    }
}
列表 10. 局部函数应用示例的输出
30
Press any key to continue . . .

管道

管道操作,通过管道运算符 (|>) 完成,提供了一种调用函数的替代语法。列表 11 展示了管道操作的示例,列表 12 展示了输出。

列表 11. 一个演示局部函数应用的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace PipeliningSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                square = @(x) x * x;
                cube = @(x) x * x * x;
                2 |> square |> cube |> print;
            ");
        }
    }
}
列表 12. 管道示例的输出
64
Press any key to continue . . .

接受多个参数的函数可以通过使用局部函数应用包含在管道中(列表 13)。

列表 13. 一个演示管道和局部函数应用的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace PipeliningSample2
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                square = @(x) x * x;
                cube = @(x) x * x * x;
                add = @(x, y) x + y;
                2 |> square |> cube |> @add(4) |> print;
            ");
        }
    }
}

扩展方法

扩展方法可用于向内置类型添加方法。这是通过 extend 关键字完成的(列表 14)。当调用扩展方法时,调用该方法的实例将作为第一个参数传递(在下面的示例中为 l)。

列表 14. 一个演示扩展方法的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace ExtensionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'std';

                extend number {
                    square: @(l) l * l
                }

                x = 10;

                print(x.square());
            ");
        }
    }
}

Aphid 宏与 C 等语言中的宏有很大不同。与 C 预处理器不同,Aphid 的预处理器使用与核心语言相同的解析器,因此宏必须遵守语言的词法和句法约定。Aphid 宏不是执行简单的字符串操作,而是对抽象语法树 (AST) 进行操作。这使得 Aphid 宏比 C 宏更“重量级”,但同时它也消除了与它们相关的许多陷阱和注意事项。

宏可以在表达式和语句级别使用。它们会递归展开,这意味着它们可以嵌套。

一个演示宏的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace MacroSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'std';
                m1 = macro(@{ 'Hello world' });
                m2 = macro(@(msg) { print(msg) });
                
                m3 = macro(@{
                    m2('foobar');
                    m2(m1());
                });
                
                m3();
            ");
        }
    }
}

如示例所示,宏可以有参数,并且可以代替几乎任何表达式或语句类型使用。请注意,在使用展开为语句的宏时必须小心。如果预期是表达式而宏展开为语句,则会发生异常。

处理解析器异常

ParserErrorMessage.Create 辅助方法可用于将 AphidParserException 转换为友好的错误消息。这特别有用,因为语法错误总是会发生,并且知道错误原因和从何处开始查找错误是很方便的。

下面的示例尝试执行一个故意在传递给 print 的参数中包含语法错误的 Aphid 脚本。语法错误使用 C# try/catch 块捕获,使用 ParserErrorMessage.Create 转换为 string,然后写入控制台。

一个演示友好解析器错误的 C#/Aphid 程序
using Components.Aphid.Interpreter;
using Components.Aphid.Parser;
using System;

namespace ErrorHandlingSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var code = @"
                #'std';
                print('foo'bar');
            ";

            try
            {
                var interpreter = new AphidInterpreter();
                interpreter.Interpret(code);
            }
            catch (AphidParserException e)
            {
                var msg = ParserErrorMessage.Create(code, e);
                Console.WriteLine(msg);
            }
        }
    }
}
友好解析器错误示例的输出
Unexpected identifier bar on line 2

(1)                 #'std';
(2)                 print('foo'bar');
(3)


Press any key to continue . . .

到目前为止所示的 Aphid 脚本并没有真正与其宿主语言交互。让我们来看看一些互操作功能。

.NET 互操作性

Aphid 提供了两种与 .NET 互操作的方法。首先描述显式方法,它涉及直接使用 AphidObject 并使用属性修饰 .NET 方法和属性。Aphid 的后续版本添加了隐式互操作,这将在本节末尾介绍。

从 .NET 访问 Aphid 变量

从 .NET 获取和设置 Aphid 变量是通过访问 AphidInterpreter 实例的 CurrentScope 属性来完成的。CurrentScope 只是一个 AphidObject,它本身派生自 Dictionary<string, AphidObject>。获取 Aphid 变量的示例显示在列表 15 中,设置变量的示例显示在列表 16 中。

列表 15. 一个演示使用 C# 获取 Aphid 变量的互操作程序
using Components.Aphid.Interpreter;
using System;

namespace VariableGetSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.Interpret("x = 'foo';");
            Console.WriteLine(interpreter.CurrentScope["x"].Value);
        }
    }
}
列表 16. 一个演示使用 C# 设置 Aphid 变量的互操作程序
using Components.Aphid.Interpreter;
using System;

namespace VariableSetSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.CurrentScope.Add("x", new AphidObject("foo"));

            interpreter.Interpret(@"
                #'Std';
                print(x);
            ");
        }
    }
}

从 Aphid 脚本调用 .NET 函数

将 .NET 函数暴露给 Aphid 非常简单。首先,所选的 .NET 函数必须用 AphidInteropFunctionAttribute 修饰(列表 17)。AphidInteropFunctionAttribute 的构造函数接受一个 string,该 string 指定了 Aphid 看到的函数名称。这可以是一个简单的标识符(例如 foo)或一个成员访问表达式(例如 foo.bar.x.y)。如果是后者,Aphid 将在导入函数时根据需要构造对象或向现有对象添加成员。

列表 17. 一个 Aphid 互操作函数
using Components.Aphid.Interpreter;

namespace InteropFunctionSample
{
    public static class AphidMath
    {
        [AphidInteropFunction("math.add")]
        public static decimal Add(decimal x, decimal y)
        {
            return x + y;
        }
    }
}

请注意,被修饰的函数既是 public 又是 static;这是所有 Aphid 互操作函数的必需条件。现在我们已经创建了互操作函数,我们可以继续编写一个脚本来导入和调用它(列表 18)。

列表 18. 一个演示加载库并调用互操作函数的 Aphid 程序
#'Std';
##'InteropFunctionSample.AphidMath';
print(math.add(3, 7));

第一行,加载脚本语句,在“Hello, world”部分已描述。然而,第二行略有不同。加载库操作符 (##) 在 Aphid 模块中(稍后会详细介绍)搜索由字符串操作数指定的类。您可能会认识到操作数是我们之前编写的互操作函数的容器类的完全限定名称。

那么 Aphid 解释器如何知道在哪里找到 InteropFunctionSample.AphidMath 呢?很简单:我们将相应的 .NET 程序集添加到 Aphid 加载器的模块列表中。相关代码如列表 19 所示。

列表 19. 一个演示加载库并调用互操作函数的 C#/Aphid 程序
using Components.Aphid.Interpreter;
using System.Reflection;

namespace InteropFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interprer = new AphidInterpreter();
            interprer.Loader.LoadModule(Assembly.GetExecutingAssembly());
            
            interprer.Interpret(@"
                #'Std';
                ##'InteropFunctionSample.AphidMath';
                print(math.add(3, 7));
            ");
        }
    }
}

运行时,应用程序会产生预期的输出(列表 20)。

列表 20. 互操作示例的输出
10
Press any key to continue . . .

现在,让我们反过来。

从 .NET 调用 Aphid 函数

在某些场景中,您可能需要从 .NET 代码调用 Aphid 函数。这可以通过调用 AphidInterpreter.CallFunction 实例方法来实现,该方法接受一个函数名和要传递的参数(列表 21)。

列表 21. 一个演示从 .NET 调用 Aphid 函数的 C#/Aphid 程序
using Components.Aphid.Interpreter;
using System;

namespace CallAphidFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.Interpret("add = @(x, y) x + y;");
            var x = interpreter.CallFunction("add", 3, 7).Value;
            Console.WriteLine(x);
        }
    }
}

当运行时,Aphid 函数 add 被调用(列表 22)。

列表 22. 互操作示例的输出
10
Press any key to continue . . .

对象互操作性

某些场景可能需要 Aphid 和 .NET 之间来回传递对象。这可以通过手动创建和操作 AphidObject 实例,或者使用 AphidObject.ConvertTo<t></t>AphidObject.ConvertFrom 方法来完成(列表 23)。旨在通过 ConvertToConvertFrom 在 Aphid 和 .NET 之间传递的类属性应使用 AphidPropertyAttribute 进行修饰。

列表 23. 一个演示 Aphid 对象互操作性的 C#/Aphid 程序
using Components.Aphid.Interpreter;
using System;

namespace ObjectSample
{
    public class Point
    {
        [AphidProperty("x")]
        public int X { get; set; }

        [AphidProperty("y")]
        public int Y { get; set; }

        public override string ToString()
        {
            return string.Format("{0}, {1}", X, Y);
        }
    }

    public class Widget
    {
        [AphidProperty("name")]
        public string Name { get; set; }

        [AphidProperty("location")]
        public Point Location { get; set; }

        public override string ToString()
        {
            return string.Format("{0} ({1})", Name, Location);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                
                ret {
                    name: 'My Widget',
                    location: { x: 10, y: 20 }
                };
            ");

            var widget = interpreter.GetReturnValue().ConvertTo<pWidget>();
            Console.WriteLine(widget);
            widget.Location.X = 40;
            var aphidWidget = AphidObject.ConvertFrom(widget);
            interpreter.CurrentScope.Add("w", aphidWidget);
            interpreter.Interpret(@"printf('New X value: {0}', w.location.x);");
        }
    }
}
列表 24. 互操作示例的输出
My Widget (10, 20)
New X value: 40
Press any key to continue . . .

无缝 .NET 互操作性

Aphid 的最新版本增加了无需任何连接或胶水代码即可与 .NET 互操作的功能。虽然这些功能尚未完善,但已经可以实例化和操作类,使用一些泛型类型,并执行许多其他操作。

已添加了一个类似于 C# 的 using 关键字。它可用于将指定的命名空间导入作用域。 .NET 类的静态方法可以在 Aphid 代码中作为一等公民使用。它们可以被调用、作为值传递、局部应用等。

一个演示 Aphid 无缝 .NET 静态方法互操作能力的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace SeamlessInteropSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                using System;
                Console.WriteLine('Hello world');
                print = Console.WriteLine;
                print('{0}', 'foo');
                printBar = @Console.WriteLine('{0}bar');
                printBar('foo');
            ");
        }
    }
}

如示例所示,即使使用部分应用,Aphid 运行时也会解析重载。

.NET 类的实例化可以使用一元运算符 new 执行。支持重载构造函数。

一个演示 Aphid 无缝 .NET 类互操作能力的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace SeamlessInteropSample2
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                using System;
                using System.Text;
                sb = new StringBuilder('Hello');
                sb.Append(' world');
                
                Console.WriteLine(
                    'Length={0}, Capacity={1}', 
                    sb.Length,
                    sb.Capacity);

                sb |> Console.WriteLine;
            ");
        }
    }
}

.NET 实例是 Aphid 中的一等公民,其成员使用标准语法访问。

已添加 load 运算符,允许 Aphid 脚本在运行时加载 .NET 程序集。程序集通过部分名称指定,通常是文件名,不带 dll 扩展名。在以下示例中,System.Web.Extensions.dll 使用 load 运算符加载。

一个演示 Aphid 无缝 .NET 程序集互操作能力的 C#/Aphid 程序
using Components.Aphid.Interpreter;

namespace SeamlessInteropSample3
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                load System.Web.Extensions;
                using System;
                using System.Web.Script.Serialization;
                serializer = new JavaScriptSerializer();
                
                obj = 
                    '{ ""x"":52, ""y"":30 }' 
                    |> serializer.DeserializeObject;
                
                Console.WriteLine(
                    'x={0}, y={1}',
                    obj.get_Item('x'),
                    obj.get_Item('y'));
            ");
        }
    }
}

内部

Aphid 的语言实现分为三层:词法分析、解析和解释。每一层都代表 Aphid 程序执行的一个阶段。第一阶段,词法分析,读取脚本文件并对其进行标记化。然后,解析器获取标记序列并使用它构建抽象语法树 (AST)。最后,解释器遍历 AST,维护状态并执行每个节点。

Mantispid:强大的词法分析器和解析器生成器

Aphid 是使用自定义语言实现工具链构建的。词法分析器和递归下降解析器都是使用名为 Mantispid 的工具生成的,Mantispid 本身是 Aphid 的另一个前端。Mantispid 接受脚本作为输入,并输出词法分析器和解析器以及支持类,全部在一个 C# 文件中。

要使用 Mantispid,请使用 Visual Studio 从源代码构建它。该项目可以在 \Mantispid 文件夹中找到。构建完成后,在命令行中不带任何参数运行它。

C:\source\Aphid\Mantispid\bin\Debug>Mantispid.exe
mantispid [Parser Script] [Output File]

C:\source\Aphid\Mantispid\bin\Debug>

Mantispid 构建完成后,可以从位于 \Components.Aphid\Aphid.alx 的输入文件重新生成 Aphid 的词法分析器和解析器,并输出到 \Components.Aphid\Parser\AphidParser.g.cs

C:\source\Aphid\Components.Aphid>..\Mantispid\bin\Debug\Mantispid.exe Aphid.alx Parser\AphidParser.g.cs
Parsing input file
Generating parser
Parser written to 'Parser\AphidParser.g.cs'

C:\source\Aphid\Components.Aphid>

生成的 C# 文件相当大,但在使用 Visual Studio 格式化后(Ctrl+k, Ctrl+d)具有适度的可读性。幸运的是,不需要直接使用它;除了少数部分类之外,对词法分析器和解析器的更改都专门通过 Mantispid 输入文件进行。

词法分析

词法分析器位于 \Components.Aphid\Aphid.Lexer.alx。下面是其简化版本。

Lexer({
    init: @() {
        #'Aphid.Lexer.Tmpl';
        #'Aphid.Lexer.Code';
    },
    name: "Components.Aphid.Lexer.Aphid",
    modes: [ 
        {
            mode: "Aphid",
            tokens: [
                { regex: '%>', type: 'GatorCloseOperator', newMode: "Text" },

                { regex: "#", type: "LoadScriptOperator" },
                { regex: "##", type: "LoadLibraryOperator" },

                { regex: ",", type: "Comma" },
                { regex: ":", type: "ColonOperator" },
                { regex: "@", type: "functionOperator" },
                { regex: "@>", type: "CompositionOperator" },
                
                /* ... */

                { regex: ";", type: "EndOfStatement" },

                { regex: "\\r|\\n|\\t|\\v|\\s", type: "WhiteSpace" },
                { code: idCode },
                { regex: "0", code: getNumber(
                    'NextChar();\r\nstate = 1;', 'return AphidTokenType.Number;') },
                { regex: "0x", code: zeroXCode },
                { regex: "0b", code: zeroBCode },
                { code: getNumber(
                    'state = 0;', 
                    'if (state == 1 || state == 3 || state == 5) { return AphidTokenType.Number; }') },
                getString('"'),
                getString("'"),
                { regex: "//", code: singleLineCommentCode },
                { regex: "/\\*", code: commentCode }
            ],
            keywords: [
                "true",
                "false",
                "null",

                /* ... */

                "try",
                "catch",
                "finally",
            ],
            keywordDefault: getKeywordHelper('Identifier'),
            keywordTail: getKeywordHelper('{Keyword}')            
        },
        {
            mode: "Text",
            tokens: [
                { regex: '<%', type: 'GatorOpenOperator', newMode: "Aphid" },
                { regex: '<%=', type: 'GatorEmitOperator', newMode: "Aphid" },
                { regex: '<', code: textCode },
                { code: textCode },
            ]
        },
    ],
    ignore: [ 
        "WhiteSpace",
        "Comment" 
    ]
});

除了部分模板化的辅助代码外,Aphid 的词法分析器完全在 Aphid.Lexer.alx 中描述,作为传递给特殊 Lexer 函数的 object 表达式。object 表达式的每个属性都具有领域特定的语义,指导 Mantispid 构建词法分析器。

对象的第一个属性 init 是一个函数,在词法分析器生成开始时调用。在这种情况下,它用于以外部代码文件的形式加载模板化代码。

第二个属性 name 是一个 string,用于通过在值后添加“Lexer”来构建词法分析器的类名。

第三个属性 modes 是一个 list,其中包含词法分析器声明的大部分内容。list 的每个元素都是一个 object,代表词法分析器的一种模式。模式具有由 mode 属性指定的名称和一个名为 tokenslisttokens 的元素是使用各种属性为当前词法分析器模式声明不同标记的对象。

标记对象可以具有以下属性:regextypecodenewModeregex 属性指定用于匹配标记的有限正则表达式。请注意,目前只支持正则表达式的一小部分。type 属性指定要匹配的标记类型。code 属性可以代替 type 使用,用于指定当 regex 匹配时执行的代码块,或者当 regex 未定义时没有标记匹配时执行的代码块。newMode 属性用于指定当标记匹配时要转换到的模式。

tokens 属性之后是 keywords,它是一个 string list,定义了 Aphid 语言的关键字。

接下来是 keywordDefault 属性,它指定当关键字匹配失败时运行的代码。原因是字符流最初可能看起来像一个关键字,但中途匹配失败。在许多语言中,直到那时为止的字符可能形成一个有效的标识符,不仅如此,而且必须继续扫描以确保所有可匹配的字符都被获取,根据最大匹配原则。这种情况的一个例子是标识符 cat,它与 Aphid 关键字 catch 部分匹配。一个演示需要继续扫描的例子是标识符 catastrophe,它也与 catch 部分匹配。keywordDefault 属性提供了解决这个常见问题的方案。

keywordDefault 之后是 keywordTail 属性,它的行为类似,再次应用最大匹配原则。在许多语言中,当匹配到关键字时,必须继续扫描以确保该标记不只是以匹配关键字开头的标识符。例如,标识符 catchable,它以关键字 catch 开头。keywordTail 属性旨在通过模板化的代码块处理这种情况。要在模板中引用回退关键字,使用 {Keyword} 标记。

最后,ignore 属性是一个 string list,其中包含在标记化过程中应自动删除的标记类型。

Mantispid 的词法分析生成很复杂,但结果是高性能的词法分析器,具有清晰的对象模型。使用生成的词法分析器对字符串进行标记化所需的代码很少。下面的程序实例化 AphidLexer,使用它标记一个简单程序,然后将标记输出到控制台。

using Components.Aphid.Lexer;
using System;

namespace LexerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var lexer = new AphidLexer(@"
                using System;
                Console.WriteLine('Hello world');
            ");

            foreach (var t in lexer.GetTokens())
            {
                Console.WriteLine(t);
            }
        }
    }
}

输出如下:

[18] usingKeyword: using
[24] Identifier: System
[30] EndOfStatement: ;
[49] Identifier: Console
[56] MemberOperator: .
[57] Identifier: WriteLine
[66] LeftParenthesis: (
[67] String: 'Hello world'
[80] RightParenthesis: )
[81] EndOfStatement: ;
Press any key to continue . . .

资源

历史

  • 2018年4月22日 - 大量修复和改进
    • 改进了跨线程边界共享资源的支持。
    • 现在支持直接类型引用,因此 typeof(StringBuilder) 可以写成 StringBuilder。添加此功能的同时保留了对 System.Type 实例成员和类型声明的静态成员的支持。例如,File.GetMethods() 和 File.ReadAllText() 都能按预期工作。
    • 添加了对显式泛型的支持,因此 List 和 Dictionary<TKey, TValue> 等类无需使用反射即可实例化。
    • 广泛的类型转换更新,包括无需任何显式类型信息即可自动将 Aphid 函数作为 .NET 委托传递的能力。
    • 重写了类型系统,添加了强大的类型推断并支持泛型。
    • REPL 的重大更新,包括语法高亮显示和广泛的自动完成功能。
    • 对扩展支持进行了全面改进,包括静态扩展方法、扩展属性、扩展构造函数和动态扩展成员。
    • 添加了 Shell 支持,包括命令行风格语法、PowerShell Cmdlet 互操作和远程处理。
    • 现在支持 C# 风格的 using 语句来处理 IDisposable 实例。
    • 添加了对数百个新自定义运算符的支持。
    • 支持块级性能分析。
    • 可选的严格模式,要求使用 var 属性明确声明变量。
    • 用基于 Medusa 的视图模型编译器取代了基于 Common Compiler Infrastructure 的 ILWeave,以实现更简洁的视图模型定义。
    • 添加了远程处理功能,能够序列化抽象语法树和词法状态以通过网络发送。
    • 添加了无需 Visual Studio 或其他外部工具即可构建可执行文件的工具。
    • 添加了 Medusa,一个强大的新元编程/白盒语言系统。
    • 改进了运行时错误检查。
    • 序列化的多项修复和更新。
    • 改进了数组互操作支持。
    • 为 Visual Studio Code 添加了调试和语法高亮扩展。
    • Visual Studio 插件的几项更新,包括支持 Visual Studio 2013 到 2017。
    • 对词法作用域支持的多次修复。
    • 由于重写了热路径和类型记忆等优化,性能得到了显著提升。
  • 2016年4月16日 - 主要是文章更新以及一些错误修复。
    • 添加了目录。
    • 添加了列表文档。
    • 添加了解析器错误文档。
    • 类型系统和类型文档已更新。
    • 语法文档已更新。
    • 开始了内部文档编写。
    • 修复了 Aphid 文档支持。
    • 重新排序了部分。
  • 2016年4月11日 - 更改太多,无法一一列出,但下面是一些主要更新。
    • 语法文档已更新。
    • 添加了宏文档。
    • 添加了无缝 .NET 互操作文档。
    • 增加了宏支持。
    • 添加了局部运算符应用。
    • 添加了无缝 .NET 互操作。
    • 添加了 Visual Studio 插件。
    • 添加了隐式管道语法。
    • 添加了输出 Python、PHP 和 Verilog 的编译器前端。
    • 添加了解析器生成器前端,并用它自举 Aphid 解析器。
    • 添加了堆栈跟踪支持
    • 添加了 Aphid 文档支持。
    • 添加了二进制数支持。
    • 改进了解析器错误消息。
    • 添加了单元测试。
    • 多次错误修复。
  • 2013年11月28日 - 代码多次更新和修复。
    • 添加了 switch 支持。
    • 添加了范围运算符。
    • 添加了条件运算符。
    • 添加了查询运算符。
    • 添加了前缀/后缀增量/减量支持。
    • 添加了 num 函数。
    • 添加了 env.processes 函数。
    • 添加了 UDP 库。
    • 添加了 ILWeave 工具。
    • 添加了 WPF AphidRepl 控件。
    • REPL 的多项更新。
    • 用 defined 关键字替换了 exists 运算符。
    • 修复了数字字面量标记化问题。
    • 修复了 AphidObject.ConvertFrom 数字转换错误。
    • 修复了加载器问题。
    • 修复了 string.substring 扩展方法。
  • 2013年11月7日 - 更新了文章,涵盖了控制结构、局部函数应用、管道、扩展方法和对象互操作性。
  • 2013年11月6日 - 添加了 try/catch/finally 支持,添加了 while 循环支持,添加了 * 赋值 (+=, -= 等) 支持,修复了序列化,添加了互操作函数,修复了负数字面量支持。
  • 2013年11月5日 - 添加了基本类型示例,添加了下载链接。
  • 2013年10月16日 - 本文的第一个版本。
© . All rights reserved.