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

Hulo 编程语言开发 解释器

  •  
  •   ansurfen 51 天前 2112 次点击
    这是一个创建于 51 天前的主题,其中的信息可能已经有所发展或是发生改变。

    书接上回,在《 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  
       51 天前
    点赞,每次写 sh/bat 都头疼,语法太另类了,但现在有 ai 了,好了一点
    spritecn
        2
    spritecn  
       51 天前
    我有一个意见 ,语法能不能不要大杂烩,要么延用 go,要么延用 python 或 js,学习成本低,并且编辑器提示友好
    llsquaer
        3
    llsquaer  
       51 天前
    Hulo /hjulo/ 是一个现代化的、面向批处理的编程语言,可以编译为 Bash 、PowerShell 和 Vbscript 。它旨在通过简洁一致的 DSL 来统一跨平台的脚本编写。


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

    利益相关:我是 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
       51 天前
    @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
       51 天前
    @llsquaer Hulo 现在的定位是批处理脚本的编译时,因为 Hulo 的完成度不是很高,对外宣传的还是比较保守。Hulo 现在能够基础的解释,未来还会为对接上 LLVM ,设计出自己的字节码。至于为什么这么做?因为很多项目都是 bash 、powershell 写的,可以实现一个提升器(lifter) 或者说是 反编译器,将这些语法向 hulo 转换,而 hulo 就能成为这些语言的 IR ,那他能够打包成其他语言,甚至提供 runtime 、native 机制。最终成为批处理脚本的 LLVM
    vfs
        7
    vfs  
       51 天前
    @w568w 为什么会觉得别人提问的时候没有经过大脑呢? “为什么不直接写一个新的脚本语言?非要翻译干什么。” 我之前就问过类似问题, 那是我思考了之后提问的,也并没有要嘲讽说作者的项目没有用,只是想探讨一下做这样的选择背后的原因而已。
    ansurfen
        8
    ansurfen  
    OP
       51 天前
    @w568w 感谢支持。除了 V2EX 我在 b 站的时候宣传评论区也存在诸多不解。大部分人的第一印象就是 为什么不直接用 python 、js 等脚本语言写什么批处理,就存在这种刻板印象。不过也还好,质疑的声音越多,我就可以试着表达我的想法和告诉他们为什么需要 Hulo 这样的语言,也能带来更多的曝光度。要开发一个跨平台的批处理编译时真的,复杂度太大了。目标语言的语法一坨又一坨 要统一他们只有各种的语法糖(运算符重载、操作 ast 、comptime 条件编译...)。有时候 bash 用户可能没有 bc 要用 awk ,有时候可能只想用 (( )) ,batch 的延迟展开 这些操作 真的蛋疼
    xgdgsc
        9
    xgdgsc  
       51 天前 via Android
    @w568w python js 哪里不跨平台了?
    ansurfen
        10
    ansurfen  
    OP
       51 天前
    @vfs 其实最直接的理由就是,著名是开源项目 Kubernetes 中 bash 2.4 % + powershell 0.2% > python 0.0% 而 Hulo 存在的意义就是要让 2.6% 变成 Hulo 。不过这可能是目前的幻想 hhhh
    w568w
        11
    w568w  
       51 天前
    @vfs 先给你道歉,语气暴躁了点。

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

    虽然 OP 做的工具确实有技术含量,也存在一定的使用场景,但短时间内多次发布,把一个公共论坛当作自己产品的 news page 了。甚至最近几个帖子直接发到了“程序员”节点,但内容没有普适的讨论价值,只是在介绍自己产品的技术细节。
    ansurfen
        17
    ansurfen  
    OP
       51 天前
    @gullitintanni 虽然发这个帖子有一定的推广存在,但是如果把每篇文章串联起来,你会对“一个编程语言是如何开发的?”有比较清晰的认识,也算是一种教程/科普性质的文章。每次文章出现的 Hulo 和标题代的 Hulo 其实可有可无,换成其他语言也是同理,主要是为了浏览器 SEO , 因为这类项目的关注度和推广难度真的很难做起来。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2992 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 35ms UTC 11:40 PVG 19:40 LAX 04:40 JFK 07:40
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86