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,回到全局环境
相关推荐
芝士加3 小时前
一个有趣的搜索神器:pinyin-match
前端·javascript·开源
HelloGitHub6 小时前
直击痛点的开源项目「GitHub 热点速览」
开源·github
时序数据说11 小时前
时序数据库市场前景分析
大数据·数据库·物联网·开源·时序数据库
曼妥思19 小时前
PosterKit:跨框架海报生成工具
前端·开源
爱喝奶茶的企鹅19 小时前
Ethan独立开发新品速递 | 2025-08-18
人工智能·程序员·开源
程序媛Dev20 小时前
还在 SSH 连服务器敲 SQL?我用 Sealos 把数据库管理后台搬进了浏览器!
开源·github
猫头虎1 天前
猫头虎AI分享|一款Coze、Dify类开源AI应用超级智能体Agent快速构建工具:FastbuildAI
人工智能·开源·github·aigc·ai编程·ai写作·ai-native
何贤1 天前
😲我写出了 Threejs 版城市天际线?!(官推转发🥳+ 源码分享🚀)
前端·开源·three.js
猫头虎1 天前
猫头虎AI分享|一款Coze、Dify类开源AI应用超级智能体快速构建工具:FastbuildAI
人工智能·开源·prompt·github·aigc·ai编程·ai-native