用程序表示程序非常简单

在程序中表示程序非常简单

假设您想创建一种能够改变世界的全新编程语言。或者您想要自动格式化以正确地缩进。或者您可能想为现有语言编写一个预处理器,以将一些缺失的功能融入其中。

不管是什么,编写将其他程序作为数据进行操作的程序有很多原因,第一步是弄清楚如何在程序中表示该数据。

我在本文中的目标是解释最标准的方法之一(抽象语法树)并展示它们实际上如何非常容易理解和实现。

本文有很多C语言的代码示例。

我们定义什么样的语言?

让我们从一个简单的例子开始:一种带有变量、数字和加法的语言。请注意,该语言没有语句,只有计算结果为简单值的表达式。这意味着我们将能够表示 01+1x+y 甚至 a+(b+1) 等表达式。

我们将为每种类型的表达式使用不同的数据类型。这些数据类型需要存储足够的信息来编码表达式的行为,而不是完整的文本表示。

也就是说,我们将仅存储执行此代码所需的信息,但不足以重建相应的字符串。

这是一个设计决定。如果您正在编写代码格式化程序,您还需要存储此类信息!

让我们从整数 字面值 开始。我们需要什么样的信息?yes。我们只需要存储文字的实际值,因为这足以对此类表达式的行为进行编码。

那么,最直接的表示可能是这样的:

c 复制代码
struct int_literal {
    int value;
};

这对于我们的目的来说完全足够了(事实上,这种表示可以让你走得很远,甚至是一个完整的编译器)。

对于 变量,假设它的名字足以代表它。我们可以存储更多信息,例如它是否是全局变量、指向其声明的指针(如果我们的语言有这样的东西)等等,但我们在这里尽量保持简单。

c 复制代码
struct variable {
    char* name;
};

现在,最后一块拼图是加法。表示 其他两个表达式的总和。我们需要存储什么?嗯,正是另外两个表达方式。

这是我们遇到一些问题的时候。加法可以使用任何类型的表达式作为其操作数。它可以是一个文字加一个文字、一个变量加一个变量、两者的组合,或者更重要的是,它可以有其他添加作为操作数。

为了处理这个问题,我们需要一些可以表示任何类型表达式的数据类型。既然我们现在还没有,那就假装我们有,然后尝试创造它。

c 复制代码
struct addition {
    struct expression* left_term;
    struct expression* right_term;
};

这往往是大多数人感到困惑的一点。我们希望加法有两个字段,这两个字段可以是各种不同的类型,具体取决于运行时提供的数据(也许我们可以说这些字段是多态的),并且字段的类型有时会是加法本身(也许我们可以说数据是递归的)。

为了实现这个所谓的 struct expression ,我们需要设计一个可以容纳 struct int_literalstruct variablestruct addition .

这看起来似乎不可能,但让我们看看这实际上是如何很容易做到的,即使在 C 中也是如此。在 C 中做到这一点的一个巧妙方法是使用标记联合(tagged union)。

Unions in C

C 中的联合(union)只是一块足够大的内存块,可以存储一组数据类型中的任何一种。例如,如果我们有一个 10 字节大的类型 A 和一个 15 字节大的类型 B ,那么这两种类型的并集将是 15 字节。

这个想法是,这种类型的对象有时会保存 A 类型的值,有时会保存 B 类型的值。让我们看一个人为的例子:

c 复制代码
// 12 bytes
struct a {
    char data[12];
};
// 8 bytes in a 32-bit machine, 16 bytes in a 64-bit machine
struct b {
    void* x;
    void* y;
};
// sizeof(union c) is the greater of sizeof(struct a) and sizeof(struct b)
// 12 bytes in a 32-bit machine, 16 bytes in a 64-bit machine
union c {
    struct a as_a;
    struct b as_b;
};

Tagged Unions

尽管 union c 类型的对象有时会保存 struct a ,有时会保存 struct b ,但无法在运行时查询它保存的是哪一个。为此,我们添加一个辅助标签值,用于存储此信息。

c 复制代码
union c {
    struct a as_a;
    struct b as_b;
};
enum c_tag { C_A, C_B };
struct tagged_c {
    enum c_tag tag;
    union c data;
};

构造这些值很简单,但有点冗长。

c 复制代码
struct tagged_c make_a(void* x, void* y) {
    struct tagged_c result;
    result.tag = C_A;
    result.data.as_a.x = x;
    result.data.as_a.y = y;
    return result;
}
struct tagged_c make_b(char* data) {
    // ... analogous code ...
}

然后,我们可以通过查看标签来编写处理 As 或 Bs 的代码:

c 复制代码
void process(struct tagged_c* data) {
    switch(data->tag) {
    case C_A:
        process_a(data->data.as_a);
        break;
    case C_B:
        break;
        process_b(data->data.as_b);
    }
}

回到表达式

这些知识可以直接应用于表达式。我们只需要创建一个可以容纳三种表达式类型中任何一种的tagged union。

c 复制代码
enum expression_tag {
    EXPRESSION_INT_LITERAL,
    EXPRESSION_VARIABLE,
    EXPRESSION_ADDITION,
};
struct expression {
    enum expression_tag tag;
    union {
        struct int_literal as_int_literal;
        struct variable as_variable;
        struct addition as_addition;
    };
};

这里我使用了一些语法糖来避免声明单独的联合类型。

这还允许我们直接访问 as_xxx 字段,而不是像以前那样使用中间 data 字段。

为了方便构建这些值,我建议编写一些如下所示的辅组函数:

c 复制代码
struct expression* addition(struct expression* left_term, struct expression* right_term) {
    struct expression* result = malloc(sizeof(struct expression));
    result->tag = EXPRESSION_ADDITION;
    result->as_addition.left_term = left_term;
    result->as_addition.right_term = right_term;
    return result;
}
struct expression* int_literal(int value) { /* analogous code */ }
struct expression* variable(char* name) { /* analogous code */ }

然后,我们就可以自由创建表达式了:

c 复制代码
// 0
struct expression* example1 = int_literal(0);
// 1+1
struct expression* example2 = addition(int_literal(1), int_literal(1));
// x+y
struct expression* example3 = addition(variable("x"), variable("y"));
// a+(b+1)
struct expression* example4 = addition(variable("a"), addition(variable("b"), int_literal(1)));

我们如何使用这些表达方式?

正如前面提到的,这些表达式是递归数据类型。这意味着在处理表达式时使用递归通常是最方便的。

例如,让我们编写一个简单的表达式打印方法。

c 复制代码
void print(struct expression* e) {
    switch (e->tag) {
    case EXPRESSION_INT_LITERAL:
        printf("%d", e->as_int_literal.value);
        break;
    case EXPRESSION_VARIABLE:
        printf("%s", e->as_variable.name);
        break;
    case EXPRESSION_ADDITION:
        printf("(");
        print(e->as_addition.left_term);
        printf("+");
        print(e->as_addition.right_term);
        printf(")");
        break;
    }
}

虽然这可能需要一段时间才能习惯,但必须注意的是,AST 上的大多数递归都是相当机械的。它通常以"进行一些预处理,对所有子表达式进行递归,进行一些后处理"的形式出现,并且代码最终非常统一,因此即使没有完全理解递归,也可以很快地掌握它。

这能扩展吗?

这个基本设计可以走得很远。创建新类型的表达式并将它们添加到tagged union中并不困难:

c 复制代码
struct anonymous_function {
    // array of parameters
    int parameter_count;
    char** parameters;
    struct expression* body;
};
struct function_call {
    struct expression* target;
    // array of arguments
    int argument_count;
    struct expression** arguments;
};
enum expression_tag {
    // .. old tags ..
    EXPRESSION_FUNCTION_CALL,
    EXPRESSION_ANONYMOUS_FUNCTION,
};
struct expression {
    enum expression_tag tag;
    union {
        // .. old types ..
        struct anonymous_function as_anonymous_function;
        struct function_call as_function_call;
    };
};

我们可以轻松扩展任何使用标记联合的函数,就像之前的打印方法一样:

c 复制代码
void print(struct expression* e) {
    switch (e->tag) {
    // .. old cases ..
    case EXPRESSION_ANONYMOUS_FUNCTION:
        printf("fun (");
        for (int i = 0; i < e->as_anonymous_function.parameter_count; ++i) {
            if (i > 0) printf(", ");
            print(e->as_anonymous_function.parameters[i]);
        }
        printf(") => ");
        print(e->as_anonymous_function.body);
        break;
    case EXPRESSION_FUNCTION_CALL:
        print(e->as_function_call.target);
        printf("(");
        for (int i = 0; i < e->as_function_call.argument_count; ++i) {
            if (i > 0) printf(", ");
            print(e->as_function_call.arguments[i]);
        }
        printf(")");
        break;
    }
}

结论

尽管程序具有本质上递归的不太常见的特征,但这可以以机械方式简单地处理。在我看来,这意味着在程序中表示程序并不比任何其他数据建模任务更困难。

我希望这个博客能够鼓励您编写操作程序的程序!


[EN] Representing Programs Within Programs is Surprisingly Easy

相关推荐
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
幸运小圣2 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
前端SkyRain3 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁3 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
老猿讲编程3 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
黄小耶@3 小时前
python如何使用Rabbitmq
分布式·后端·python·rabbitmq
宅小海4 小时前
Scala-List列表
开发语言·后端·scala
蔚一5 小时前
Javaweb—Ajax与jQuery请求
前端·javascript·后端·ajax·jquery