V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
ansurfen
V2EX  ›  程序员

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

  •  
  •   ansurfen · 16 小时 12 分钟前 · 1069 次点击

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

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

    编译时执行

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

    let a = comptime {
        let sum = 0
        loop $i := 0; $i < 10; $i++ {
            echo $i;
            $sum += $i;
        }
        return $sum
    }
    

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

    对象化 & 求值

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

    对象化

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

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

    // 定义类型的基本行为
    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 结构和求值步骤如下:

    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

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

    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)来存储变量,但为什么要环境管理?这涉及到作用域和变量查找的问题。

    为什么需要环境管理?

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

    作用域链

    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
    

    环境链实现

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

    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() 创建一个新的环境,这个新环境就是一个栈帧:

    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 ,回到全局环境
    17 条回复    2025-08-19 19:29:55 +08:00
    spritecn
        1
    spritecn  
       14 小时 55 分钟前
    点赞,每次写 sh/bat 都头疼,语法太另类了,但现在有 ai 了,好了一点
    spritecn
        2
    spritecn  
       14 小时 53 分钟前
    我有一个意见 ,语法能不能不要大杂烩,要么延用 go,要么延用 python 或 js,学习成本低,并且编辑器提示友好
    llsquaer
        3
    llsquaer  
       14 小时 27 分钟前
    Hulo /ˈhjuːloʊ/ 是一个现代化的、面向批处理的编程语言,可以编译为 Bash 、PowerShell 和 VBScript 。它旨在通过简洁一致的 DSL 来统一跨平台的脚本编写。


    感觉定位脚本语言就好了嘛。为啥非得装饰上编程语言。
    w568w
        4
    w568w  
       13 小时 56 分钟前
    为啥楼主每次发帖都有人问不经大脑的问题,冷嘲热讽之前不先思考一下吗?

    利益相关:我是 amber-lang 的 contributor ,和楼主这个项目的定位类似。

    ----

    下面是一些常见问题:

    Q:为什么不直接写 Python/JS/VBScript/JScript/Lua/bat/...?
    A:你说的这些都不跨平台(尤其是臭名昭著的 Windows ),脚本编写者需要学习多门语言、维护多套脚本,增加维护成本。

    Q:为什么不直接写一个新的脚本语言?非要翻译干什么。
    A:不用在用户设备上安装解释器,便携性强。

    Q:哎呀,没听说过脚本都需要跨平台,需要时再写不就行了?
    A:相同项目确实少见,那不同项目的脚本呢?比如说,今天可能要给 Windows 客户机写批处理,明天要给 Linux 服务器写脚本,后天又给自己的 mac 写小脚本,用中间语言就不需要同时学三门语言了。

    Q:脚本语言有什么难学的?我 [此处填时长] 就学会了。
    A:脚本语言的问题是(通常为了向后兼容)有非常多的 quirks 。比如 Bash ,很多人喜欢用的 set -o pipefail 和 -e 其实都是有问题的,在计算数值非 0 时也可能当作错误退出;另外,它的 local scope 是 dynamic scope 而不是 lexical scope ,如果不为每个函数重命名变量,会导致意外的变量覆盖;此外还有 string interpolation 和转义的各种复杂规则。Windows bat 也有诸如参数传递和变量展开延迟这些问题。你不碰到自然没事,碰到就会很难受。使用中间语言有助于以统一的方式解决怪癖,提高 QoL 。

    Q:为什么要重新发明新语言?直接给现有语言写编译器不行吗?
    A:脚本语言的逻辑差异很大,比如命令( command )语法,现有语言不支持。另外脚本语言大多是弱类型或动态类型,支持的语法特性很少,很难写一个 1:1 的编译器。

    Q:现在 AI 都能写了,你这根本没用。
    A:AI 写稍微复杂一点的脚本逻辑,依然会漏洞百出。另外后续可维护性会极差(亲身经历)。

    Q1:你用的 AI 不行 / 你自己提示水平不行,我的 AI 从没出问题。
    Q2:AI 很快就全面解放人类劳动了,写这些东西根本没有意义。
    Q3:古法手工编程的传统码农又来啦 / 写代码写出优越感了
    A:¯\_(ツ)_/¯
    ansurfen
        5
    ansurfen  
    OP
       12 小时 36 分钟前
    @spritecn 每个语法的设计都有考量的,整体上语法是师从 typescript + rust 的。1. 在接口的定义和实现上抄了 rust 的 impl, 分离具体的实现类和接口之间的耦合,这样可以在任何位置实现,比如说 impl findstr for grep, impl grep for findstr ,不同平台命令可以相互实现。2. comptime 这个语法糖他是一个表达式,可以塞在任何地方,所以他和传统的宏还不太一样,他可以耍无赖的放在函数的参数里面,比如说 echo ( true, comptime { ... }) 至于这要做有什么用,就是后面 hulo 会开发的,hulo 的 comptime 可以操作 ast ,但是不同于传统的编程语言 只能操作包裹的子类节点,hulo 的 comptime 能够直接操作父类,也就是说 经过 comptime { ... } 的执行,外层的 echo 可能被替换成 Write-host ,之所以会这样设计 也是为了跨平台的考量。如果是 go, python 的语法糖压根达不到这样的水平。3. hulo 的命令是基础类型,可以对命令进行组合相互实现,例如 use grep = find & findstr 这样的,有点类似 ts 的类型体操,但是 hulo 的机制更加复杂,因为命令有不同的 options 还需要更细粒度的组合,因此 hulo 还参考了 css 的选择器,支持对命令进行更精细化的提取。总的来说,Hulo 的这些语法糖就是为了统一这些平台的语言设计的。
    ansurfen
        6
    ansurfen  
    OP
       12 小时 31 分钟前
    @llsquaer Hulo 现在的定位是批处理脚本的编译时,因为 Hulo 的完成度不是很高,对外宣传的还是比较保守。Hulo 现在能够基础的解释,未来还会为对接上 LLVM ,设计出自己的字节码。至于为什么这么做?因为很多项目都是 bash 、powershell 写的,可以实现一个提升器(lifter) 或者说是 反编译器,将这些语法向 hulo 转换,而 hulo 就能成为这些语言的 IR ,那他能够打包成其他语言,甚至提供 runtime 、native 机制。最终成为批处理脚本的 LLVM
    vfs
        7
    vfs  
       12 小时 25 分钟前
    @w568w 为什么会觉得别人提问的时候没有经过大脑呢? “为什么不直接写一个新的脚本语言?非要翻译干什么。” 我之前就问过类似问题, 那是我思考了之后提问的,也并没有要嘲讽说作者的项目没有用,只是想探讨一下做这样的选择背后的原因而已。
    ansurfen
        8
    ansurfen  
    OP
       12 小时 18 分钟前
    @w568w 感谢支持。除了 V2EX 我在 b 站的时候宣传评论区也存在诸多不解。大部分人的第一印象就是 为什么不直接用 python 、js 等脚本语言写什么批处理,就存在这种刻板印象。不过也还好,质疑的声音越多,我就可以试着表达我的想法和告诉他们为什么需要 Hulo 这样的语言,也能带来更多的曝光度。要开发一个跨平台的批处理编译时真的,复杂度太大了。目标语言的语法一坨又一坨 要统一他们只有各种的语法糖(运算符重载、操作 ast 、comptime 条件编译...)。有时候 bash 用户可能没有 bc 要用 awk ,有时候可能只想用 (( )) ,batch 的延迟展开 这些操作 真的蛋疼
    xgdgsc
        9
    xgdgsc  
       12 小时 15 分钟前 via Android
    @w568w python js 哪里不跨平台了?
    ansurfen
        10
    ansurfen  
    OP
       12 小时 14 分钟前
    @vfs 其实最直接的理由就是,著名是开源项目 Kubernetes 中 bash 2.4 % + powershell 0.2% > python 0.0% 而 Hulo 存在的意义就是要让 2.6% 变成 Hulo 。不过这可能是目前的幻想 hhhh
    w568w
        11
    w568w  
       12 小时 12 分钟前
    @vfs 先给你道歉,语气暴躁了点。

    上面最后列的这几个问题不是我在 V 站看到的(我也没看过你之前提问的那个帖子),是我自己在用户群和 Reddit 宣传的时候遇到的常见问题。不是说「提这些问题的人 = 不经大脑思考」。如果不小心地图炮到你了,不好意思。
    vfs
        12
    vfs  
       12 小时 4 分钟前
    @ansurfen 这个想法没问题,其实问题在于如果真的使用 hulo 来取代 2.4% 的 bash 和 0.2% 的 powershell ,hulo 能减少多少代码,为了让 hulo 能自动转到这几个语言,hulo 脚本中需要写多少平台相关的代码
    vfs
        13
    vfs  
       12 小时 2 分钟前
    @w568w 嗯嗯,没关系的,正常交流即可。 其实这两种方案本来就伯仲之间,没有好坏,可能别处的提问也只是想知道你们的决策原因。毕竟程序员好奇心都比较强 :)
    ansurfen
        14
    ansurfen  
    OP
       11 小时 59 分钟前
    @vfs 是的,蛋疼就蛋疼在这里,所以 hulo 需要一堆语法糖去实现这个过程,让用户写起来不需要关注平台的封装,Hulo 会将所有复杂性留在标准库里面,而且也不做编译器内的硬编码,所有的转换都在 .hl 文件里面实现
    vfs
        15
    vfs  
       11 小时 32 分钟前
    @ansurfen 这感觉也是必然要面对的事情。将尽可能多的细节藏起来(放在 hulo 提供的标准库中)。 其实比较担心的是将来会长期陷入往标准库中加入更多的语法糖。。。只要能收敛,就没问题
    gullitintanni
        16
    gullitintanni  
       6 小时 53 分钟前
    @Livid 这个主题是不是放推广节点更好一些?

    虽然 OP 做的工具确实有技术含量,也存在一定的使用场景,但短时间内多次发布,把一个公共论坛当作自己产品的 news page 了。甚至最近几个帖子直接发到了“程序员”节点,但内容没有普适的讨论价值,只是在介绍自己产品的技术细节。
    ansurfen
        17
    ansurfen  
    OP
       6 小时 7 分钟前
    @gullitintanni 虽然发这个帖子有一定的推广存在,但是如果把每篇文章串联起来,你会对“一个编程语言是如何开发的?”有比较清晰的认识,也算是一种教程/科普性质的文章。每次文章出现的 Hulo 和标题代的 Hulo 其实可有可无,换成其他语言也是同理,主要是为了浏览器 SEO , 因为这类项目的关注度和推广难度真的很难做起来。
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1368 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 17:37 · PVG 01:37 · LAX 10:37 · JFK 13:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.