在一个简单任务有数百个依赖项并且其中大部分没有记录的世界中,一切都会陷入混乱。单一统治决定了你的构建时间必须很慢,这样他们(依赖者)才能通过你在编译期间玩的视频游戏赢得你的芳心。一个人对公司免费使用他们的字符串填充库感到愤怒,然后整个互联网爆炸了几天。这是不可持续的。
hlang 是打破这种复杂性并为您提供真正无与伦比的开发体验的大锤。
如果这些都没有任何意义,请阅读本系列的其余部分。这有望帮助某些事情变得有意义。
不过,hlang 在过去存在一个重大缺陷。它本身就是一个空壳,已经腐烂到时间的杀戮和箭矢。 playground 停止工作,所以人们无法通过玩它来了解 hlang 的纯粹威力。
瞧,一个新的编译器诞生了。在本文中,我将介绍 nguh 编译器以及它如何彻底改变您在专业和个人用途中使用 hlang 的方式。
老编译器
旧的编译器是一个 HACK。它的主要工作方式是将程序源代码作为字符串提供给这个Go 模板:
(module (import "h" "h" (func $h (param i32))) (func $h_main (local i32 i32 i32) (local.set 0 (i32.const 10)) (local.set 1 (i32.const 104)) (local.set 2 (i32.const 39)) (call $h (get_local 0)) (call $h (get_local 1)) (call $h (get_local 2)) (call $h (get_local 0)) ) (export "h" (func $h_main)) )
该模板的工作原理是将程序输入作为字符串并循环遍历每个字符以决定要做什么。如果它是一个空格,它会打印一个换行符。如果它是一个h
,它会打印h
。如果它是'
,它会打印'
。其他任何内容都将被忽略。
然而,这意味着解析器几乎被忽略了。解析器规范在 gzip 压缩后编译为 117 字节,这意味着它可以装在 T 恤上。
此外,这将使用命令wat2wasm
将其编译为 WebAssembly 文件,而不是直接执行。这与 get_local 指令在过去 2 年的某个时候以文本格式local.get
get_local
事实相结合意味着我的编译器不仅是 hacky,它不再工作了。
不用说,这可以通过对源文件执行简单s/get_local/local\.get/g
来解决,但这并不好玩。你知道什么是真正的乐趣吗?对流上的二进制文件进行逆向工程,并在代码中重新组装相同的副本。蛮好玩的。
nguh 编译器
2022 年 12 月 31 日,我在stream 上编写了 nguh 编译器。 nguh(nguh 提供 u hlang 或 Next-Generation Universal Hlang 编译器,无论您喜欢哪个)编译器直接输出 WebAssembly 字节码,而不是使用wat2wasm
作为中间人。
nguh 应该与-ing
和uh
一起粉碎的最后声音一起发音。它在英语中不是语音有效的。正确地说出来需要一些练习。我不遗憾。如果你能读懂国际音标,它的发音是/ŋə/。这个名字来自 youtuber Agma Schwa 关于名为 /ŋə/ 的 conlang 的节目。
为了帮助您理解 nguh 的架构,了解有关 WebAssembly 文件工作原理的一些上下文会很有帮助。
WebAssembly 文件如何工作
什么是 WebAssembly?
WebAssembly 是一种标准,它指定了一种以沙盒方式在任意硬件上运行程序的方法。它主要用于网络浏览器,为 YouTube 的播放器组件、Twitch 流观看等功能提供支持,并且在开发人员需要将代码块放入网站而无需用 JavaScript 重写时使用。
我是一个缓慢增长的开发人员团队的一员,他们希望在服务器上运行 WebAssembly 代码,这样您就可以使用相同的.wasm
文件并在任何硬件上运行它,而无需源代码和有效的编译器设置。
hlang 被编译为 WebAssembly 没有特别的原因。
在高层次上,WebAssembly 模块中有很多部分。每个部分都包含有关模块导出的函数、导入函数的类型、模块需要多少内存、默认情况下内存中应包含的内容以及代码的函数体等信息。这是 hlang 二进制文件的注释反汇编:
0x00, 0x61, 0x73, 0x6d, // \0asm wasm magic number 0x01, 0x00, 0x00, 0x00, // version 1
0x01, // type section 0x08, // 8 bytes long 0x02, // 2 entries 0x60, 0x01, 0x7f, 0x00, // function type 0, 1 i32 param, 0 return 0x60, 0x00, 0x00, // function type 1, 0 param, 0 return
0x02, // import section 0x07, // 7 bytes long 0x01, // 1 entry 0x01, 0x68, // module h 0x01, 0x68, // name h 0x00, // type index 0x00, // function number
0x03, // func section 0x02, // 2 bytes long 0x01, // function 1 0x01, // type 1
0x07, // export section 0x05, // 5 bytes long 0x01, // 1 entry 0x01, 0x68, // "h" 0x00, 0x01, // function 1
0x0a, // code section 0x1b, // 27 bytes long 0x01, // 1 entry 0x19, // 25 bytes long 0x01, // 1 local declaration 0x03, 0x7f, // 3 i32 values - (local i32 i32 i32) 0x41, 0x0a, // i32.const 10 (newline) 0x21, 0x00, // local.set 0 0x41, 0xe8, 0x00, // i32.const 104 (h) 0x21, 0x01, // local.set 1 0x41, 0x27, // i32.const 39 (') 0x21, 0x02, // local.set 2 0x20, 0x01, // local.get 1 push h 0x10, 0x00, // call 0 (putchar) 0x20, 0x00, // local.get 0 push newline 0x10, 0x00, // call 0 (putchar) 0x0b // end of function
在高层次上,nguh 只是获取所有需要的部分并将它们放入目标二进制文件中。大多数部分都是从我上面粘贴的反汇编中逐字复制的,因为它们不需要任何修改即可使二进制文件工作。
令人兴奋的部分发生在 hlang 语法树中的各个节点被编译为 WebAssembly 字节码时。树中的每个节点可能都有要打印的字符,也可能有一个子节点列表。如果程序中有一个字符,则 hlang 的语法树可能如下所示:
input: h H("h")
或者如果程序中有多个字符,它可能看起来像这样:
input: hhh H{ "h", "h", "h", }
这意味着我需要这样的东西:
// compile AST to wasm if len (tree. Kids ) == 0 { if err := compileOneNode (funcBuf, tree); err != nil { return nil , err } } else { for _ , node := range tree. Kids { if err := compileOneNode (funcBuf, node); err != nil { return nil , err } } }
这将从树的根部或树的所有子节点读取以编译整个程序。 compileOneNode
函数会将与节点关联的文本转换为相关的 WASM 字节码(将相关字符压入堆栈,然后调用hh
( putchar
) 函数)。
最后,它将生成函数的结尾,包括尾随换行符并结束.wasm
文件。
h
的 hlang 程序生成的二进制文件是 69 个字节。 如果您觉得这很有趣,这里有一个 base-64 编码的 hlang 二进制文件:
AGFzbQEAAAABCAJgAX8AYAAAAgcBAWgBaAAAAwIB AQcFAQFoAAEKHQEbAQN/QQohAEHoACEBQSchAiAB EAAgABAAAQEL
如果您想玩 hlang,请前往h.within.lgbt的新家。如果你想见证诸如此类的现场创建,请在 twitch上关注我或在我的 VTuber 商业帐户@[email protected]上关注我。