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,回到全局环境
相关推荐
WPG大大通34 分钟前
从数据到模型:Label Studio 开源标注工具完整实施指南
经验分享·笔记·ai·系统架构·开源·大大通
weixin_5112228044 分钟前
GameObject 常见类型详解 -- 陷阱(TRAP)
开源
weixin_511222801 小时前
GameObject 常见类型详解 -- 傻瓜(GOOBER)
开源
卓码软件测评1 小时前
第三方软件测试公司:【Gatling基于Scala的开源高性能负载测试工具】
测试工具·开源·scala·压力测试·可用性测试·第三方软件测试
weixin_511222804 小时前
物品奖励系统介绍
开源
讓丄帝愛伱5 小时前
阿里开源 Java 诊断神器Arthas
java·linux·开发语言·开源
说私域5 小时前
微商本地化发展模式的借鉴与探讨——以开源AI智能名片链动2+1模式S2B2C商城小程序为例
人工智能·小程序·开源
诗仙&李白5 小时前
HEFrame.WpfUI :一个现代化的 开源 WPF UI库
ui·开源·wpf
javastart5 小时前
Oumi:开源的AI模型一站式开发平台,涵盖训练、评估和部署模型
人工智能·开源·aigc
叶庭云5 小时前
一文了解国产算子编程语言 TileLang,TileLang 对国产开源生态的影响与启示
开源·昇腾·开发效率·tilelang·算子编程语言·deepseek-v3.2·国产 ai 硬件