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:
            // 根据值进行乘法
    }
}

节点会逐层递归求值,每一层的求值结果作为上一层节点的子树继续求值。最终返回的不是原始的string、int、any等类型,而是包装成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,回到全局环境
相关推荐
海绵宝宝转agent3 小时前
MySql高频面试八股开源笔记总结
mysql·面试·开源
niyongsheng7 小时前
后端零改动,给若依换一套现代化前端
vue.js·开源·node.js
EasyBr指纹浏览器7 小时前
抖音创作者数据导出:怎样留下可比较的日报?
数据分析·开源·抖音·数据导出
Sweet锦8 小时前
不调 Python,不装向量库:我用纯 Java 写了一套以图搜图引擎
java·人工智能·开源·图搜索
网络毒刘9 小时前
开源 AI 编程助手横向对比(Cursor / Continue / Aider):能力边界与 AtomGit 落地建议
人工智能·开源
盘古开天16669 小时前
windows鱼塘可交互鱼群动态桌面资源
人工智能·chatgpt·开源
枫叶丹410 小时前
从一次推理请求出发:模型、显存、网络与服务系统如何共同决定性能
网络·人工智能·chatgpt·开源·agent·codex
deepseek2311 小时前
Kolibri 拆解:计算像 3.5B、显存要 78GB 的德国主权模型,把合规做进六个架构决策
开源·大模型·moe·aiagent·ai架构
EasyBr指纹浏览器11 小时前
抖音账号诊断:公开作品样本能说明什么?
数据分析·开源·内容运营·抖音