Hulo 编程语言开发 —— 解释器

书接上回,在《Hulo 编程语言开发 ------ 包管理与模块解析》一文中,我们介绍了Hulo编程语言的模块系统。今天,让我们深入探讨编译流程中的第三个关键环节------解释器。

作为大杂烩语言的集大成者,Hulo吸收了Zig语言的comptime语法糖。在comptime { ... }表达式的包裹下,代码会在编译的时候执行,就像传统的解释型语言一样。这也为Hulo的元编程提供了强大的支撑,使得Hulo可以实现类似Rust过程宏、编译期反射、直接操作AST等强大功能。

编译时执行

假设我们现在有如下代码:

bash 复制代码
let a = comptime {
    let sum = 0
    loop $i := 0; $i < 10; $i++ {
        echo $i;
        $sum += $i;
    }
    return $sum
}

在翻译成目标语法的时候会以 let a = 45 进行翻译,中间的一大串代码都会被提前执行。这个执行的过程其实就是解释。

对象化 & 求值

求值就是解释器执行代码的过程。在Hulo中,解释器需要能够执行各种类型的表达式和语句。

对象化

在Hulo中,所有的值都被"对象化"处理。这意味着无论是数字、字符串还是函数,都被包装成统一的对象接口。

下面是Hulo代码中关于对象系统的设计:

go 复制代码
// 定义类型的基本行为
type Type interface {
	Name() string // 获取类型名称

	Text() string // 获取类型的文本表示

	Kind() ObjKind // 获取类型种类(如基本类型、对象类型等)

	Implements(u Type) bool // 检查是否实现了某个接口

	AssignableTo(u Type) bool // 检查是否可以赋值给某个类型

	ConvertibleTo(u Type) bool // 检查是否可以转换为某个类型
}

// 继承Type接口,定义对象的行为
type Object interface {
	Type
	NumMethod() int // 获取方法数量
	Method(i int) Method // 根据索引获取方法
	MethodByName(name string) Method // 根据名称获取方法
	NumField() int // 获取字段数量
	Field(i int) Type // 根据索引获取字段
	FieldByName(name string) Type // 根据名称获取字段
}

// 定义值的基本行为
type Value interface {
    Type() Type        // 获取值的类型
    Text() string      // 获取值的文本表示
    Interface() any    // 获取底层的Go值
}

通过这段代码不难看出,这有点类似于Golang的反射系统。实际上,对象系统的实现上的确参考了反射机制,所有的单元测试接口甚至也和反射的测试如出一辙。可以说,Hulo的解释器在抽象AST的过程中就是将值与类型转换成反射操作,通过统一的接口来操作不同类型的值。

求值过程

在对象化的基础上,解释器通过遍历AST节点来执行代码,根据节点类型执行相应的操作。

假设这个我们有1 + 2 * 3这样一个表达式,它的AST结构和求值步骤如下:

hulo 复制代码
BinaryExpr {
    X: Literal(1),
    Op: PLUS,
    Y: BinaryExpr {
        X: Literal(2),
        Op: MULTIPLY,
        Y: Literal(3)
    }
}
  1. 访问根节点 BinaryExpr(PLUS)
  2. 先求值左子树 Literal(1) → 1
  3. 先求值右子树 BinaryExpr(MULTIPLY):
    • 求值左子树 Literal(2) → 2
    • 求值右子树 Literal(3) → 3
    • 执行乘法 2 * 3 → 6
  4. 执行加法 1 + 6 → 7

而这个求值的过程,我们可以用伪代码表示为:

go 复制代码
func (interp *Interpreter) Eval(node ast.Node) Object {
    switch node := node.(type) {
        case *ast.Literal:
            return interp.evalLiteral(node)
        case *ast.BinaryExpr:
            return interp.evalBinaryExpr(node)
        // ...
    }
}

func (interp *Interpreter) evalLiteral(node *ast.Literal) Object {
    // 简化复杂度,我们假设字面量类型都是 number 类型
    return &object.NumberValue{Value: node.Value}
}

func (interp *Interpreter) evalBinaryExpr(node *ast.BinaryExpr) Object {
    lhs := interp.Eval(node.Lhs) // 计算左值
    rhs := interp.Eval(node.Rhs) // 计算右值

    // 由 evalLiteral 可知 lhs、rhs 都是 *object.NumberValue,并假设 NumberValue 的类型为 NumberType
    switch node.Op {
        case token.PLUS: // 根据值进行加法
            // 假设 NumberType 有 add 方法可以直接运算
            return lhs.Type().(*object.NumberType).MethodByName("add").call(rhs)
        case token.MULTIPLY:
            // 根据值进行乘法
    }
}

节点会逐层递归求值,每一层的求值结果作为上一层节点的子树继续求值。最终返回的不是原始的stringintany等类型,而是包装成Object接口的对象,体现了"一切皆对象"的设计理念。

环境管理

解释器维护一个环境(Environment)来存储变量,但为什么要环境管理?这涉及到作用域和变量查找的问题。

为什么需要环境管理?

hulo 复制代码
var globalVar = 100  // 全局变量

fn test() {
    let localVar = 200  // 局部变量
    echo $globalVar     // 可以访问全局变量
    echo $localVar      // 可以访问局部变量
}

fn another() {
    echo $globalVar     // 可以访问全局变量
    echo $localVar      // ❌ 错误!无法访问test函数的局部变量
}

作用域链

Hulo采用词法作用域,变量查找遵循"就近原则":

hulo 复制代码
let x = 1  // 全局作用域

fn outer() {
    let x = 2  // 局部作用域,遮蔽了全局的x

    fn inner() {
        let x = 3  // 更内层的作用域
        echo $x    // 输出3,找到最近的x
    }

    echo $x  // 输出2,找到outer函数中的x
}

echo $x  // 输出1,找到全局的x

环境链实现

环境通过链表结构实现作用域链:

go 复制代码
type Environment struct {
    store map[string]Value  // 当前作用域的变量
    outer *Environment      // 外层环境(父作用域)
}

func (e *Environment) Get(name string) (Value, bool) {
    // 先从当前环境查找
    obj, ok := e.store[name]
    if ok {
        return obj, true
    }

    // 如果没找到,继续在外层环境查找
    if e.outer != nil {
        return e.outer.Get(name)
    }

    // 所有环境都没找到
    return nil, false
}

// Fork创建新的环境,类似于函数调用的栈帧
func (e *Environment) Fork() *Environment {
    env := NewEnvironment()  // 创建新的环境
    env.outer = e           // 将当前环境作为外层环境
    return env              // 返回新环境
}

Ps. 这个代码只是用于展示的最小实现,实际Hulo的实现将更为复杂。

环境创建过程

栈帧(Stack Frame) 是函数调用时在调用栈上分配的一块内存,用于存储函数的局部变量、参数和返回地址。

在Hulo中,每次函数调用都会通过 Fork() 创建一个新的环境,这个新环境就是一个栈帧:

hulo 复制代码
fn outer() {
    let x = 10
    fn inner() {
        let y = 20
        echo $x + $y  // 30
    }
    inner()
}

执行过程:

  1. 全局环境 {}
  2. 调用outer()Fork() → 创建栈帧1 {x: 10, outer: 全局环境}
  3. 调用inner()Fork() → 创建栈帧2 {y: 20, outer: 栈帧1}
  4. 执行echo → 在栈帧2中查找变量
    • 查找y:栈帧2中找到 20
    • 查找x:栈帧2没有 → 栈帧1中找到 10
  5. inner()返回 → 销毁栈帧2,回到栈帧1
  6. outer()返回 → 销毁栈帧1,回到全局环境
相关推荐
快乐的学习16 小时前
开源相关术语及提交commit关键字总结
驱动开发·开源
NocoBase1 天前
8 个最佳 Google Sheets 替代方案(附成本与能力分析)
低代码·开源·github
8***v2571 天前
开源模型应用落地-FastAPI-助力模型交互-进阶篇-中间件(四)
开源·交互·fastapi
用户84316489494321 天前
pve ui 绝美ui 拒绝原生ui
开源
l***77521 天前
开源的不需要写代码的爬虫maxun
爬虫·开源
隐语SecretFlow1 天前
【隐语Secretflow】一文速通隐私计算节点Domain
开源·资讯
HelloGitHub1 天前
《HelloGitHub》第 116 期
开源·github
周杰伦_Jay1 天前
【 2025年必藏】8个开箱即用的优质开源智能体(Agent)项目
人工智能·机器学习·架构·开源
K***72842 天前
开源模型应用落地-工具使用篇-Spring AI-Function Call(八)
人工智能·spring·开源
ajassi20002 天前
开源 Linux 服务器与中间件(十三)FRP服务器、客户端安装和测试
linux·服务器·开源