实现依据:官网技术规范基线 4cdeca1。每题末尾标明章节与规则;具体布局和限制不自动适用于后续版本。更新:2026-09-10。
没有找到相关问答。可以缩短关键词、切换主题,或清除筛选后重新查找。
01初识豫言
001豫言是一门怎样的语言?+
豫言是一门静态类型的中文函数式编程语言,以函数、类型和模块组织程序。中文是源码的表达方式,程序仍须遵守精确的语法和类型规则。
002中文自然语言描述能直接当源码运行吗?+
不能把任意中文句子当作豫言源码。源码必须先被扫描和解析,再通过静态检查;描述需求与编写合法程序是两个步骤。
003为什么通过类型检查还不等于程序正确?+
类型检查约束输入、输出和表达式的组合,但不能替你证明业务逻辑正确。一般递归不保证终止,越界、不安全转换等也不能因此视为安全。
004初学者应该从哪里开始?+
先从官网“在线体验”运行问候示例,再读《豫言赋》教程;遇到具体规则时查技术文档。想研究编译器实现,再循技术文档中的源码依据阅读。
005什么叫自托管编译器?+
意思是豫言编译器由豫言实现。阅读和修改它仍需要可运行的编译工具链;独立实现则可以采用不同的前端或中间表示。
006这 100 问的实现细节适用于哪个版本?+
涉及语言算法和二进制布局的回答以官网技术规范的 4cdeca1 基线为准。它们用于解释这份固定规范,不能自动视为以后每个版本的承诺。
007源码兼容和 ABI 兼容有什么区别?+
源码兼容关注程序的接受边界、求值行为和结果。ABI 兼容还要求同一目标下的数据布局、调用协议、符号身份和 GC 根约定相合,才能互相调用对象文件。
008编译器通过自举就证明 ABI 兼容了吗?+
没有。只打印相同结果或只通过自举,都不足以证明旧对象文件与新实现能互操作;还要验证跨边界调用、返回、异常和回收后的行为。
009ABI 兼容会让编译结果逐字节相同吗?+
不会自动保证。逐字节复现还受工具链、路径、编号、优化选项和调试信息影响;内部临时名或寄存器分配不同,并不必然破坏 ABI。
010独立实现豫言要重写整个标准库和操作系统封装吗?+
不必。标准库是豫言源码,可由新编译器编译;系统原语可以链接基线 C 运行时。若连运行时也要替换,就须实现对应的真实 C 签名和行为并验证互操作。
02文字与字面量
011名称和字符串为什么使用不同的引号?+
「」包围名称,『』包围字符串。扫描器分别进入名称和字符串状态,所以字符串里的句号等标点不会被当作外层语法操作符。
012换行会结束一条语句吗?+
不会。换行属于被忽略的空白,普通文件在圆括号深度为零时用句号切句;字符串和名称内部的句号已经封装在各自令牌中。
013任何看起来像空白的字符都会被忽略吗?+
不能这样假定。基线明确忽略普通空格、制表和换行,不保证 BOM、回车或全角空格自动消失;复制源码后莫名解析失败时,应检查这些字符。
014豫言注释可以嵌套吗?+
可以。注释以「:开始、以:」结束,扫描器记录嵌套深度;未闭合的注释必须报错。字符串内部同样的起始形式用于转义,不是注释。
015字符串里的反斜线 n 会产生换行吗?+
不会按 C 风格转义解释。豫言规定的换行写法是「:换行:」,也允许字符串中出现实际换行;不能把其他语言的转义规则直接搬过来。
016字符串如何包含它自己的结束引号?+
在字符串内部用「:』:」表示字符 』。这样扫描器会把它当作内容,而不是终止字符串;不被支持的转义内容会报错。
017数字为什么也放在「」里?+
基线从自由名称识别数字,所以「123」可进入数值识别,裸写 123 不形成数字表达式。若名称已解析为局部绑定,则不会再被误改成字面量。
018「一二三」和「一百二十三」都表示 123 吗?+
前者表示 123,后者不按该数值规则识别。汉字数字按十进制逐位累加,支持零到九,不使用十、百、千这样的单位词。
019能直接写负数和科学计数法字面量吗?+
基线数值识别不接受负一、-1 或 1e3 这些名称作为数字。需要负值时使用整数运算构造;这不表示整数类型本身只能存非负数。
020数字中的点有什么规则?+
名称里的 . 和 点 都可作为小数分隔符;多个分隔符不会被识别成数字。整数不是任意精度契约,小数字面量还会保留规范化的十进制片段供 LLVM 生成使用。
03表达式与语法糖
021乃和者分别做什么?+
常见的“「名」乃类型也”声明类型,紧接的同名“「名」者表达式也”给出定义。基线将这两句合并为一项,签名和定义之间不能插入其他声明。
022只有类型签名而没有定义可以吗?+
基线不接受文件末尾独立的签名;签名后跟不同名的定义也会拒绝。把签名与它的同名定义紧挨着写,可以避免这类预处理错误。
023虑会让新名称在自己的定义中可见吗?+
不会。虑先求绑定的定义,再在后续表达式中引入新名称;如果需要在定义中引用自身,应使用带类型标注的递归绑定形式。
024若的两个分支都会执行吗?+
不会。条件先求得爻,只执行被选择的分支;不过类型检查时,两支都要符合共同的结果类型,未执行不等于可以写类型错误。
025与和合有什么区别?+
与用于组合元组值,合用于组合元组类型。合并不建立联合类型;把多个可能的数据形态表示成一种类型,应阅读名义类型与构造器规则。
026元组的括号为什么会改变嵌套结构?+
元组只展平右侧已有的元组。因此 a 与 b 与 c 是三项,而(a 与 b)与 c 的第一项仍是二元组;不能把左右两边一律递归展平。
027中可以用变量动态选择元组成员吗?+
不可以。中右侧必须在语法阶段识别为整数字面量,它保存的是常量索引;是否真是元组以及索引对应的类型,还需后续类型检查。
028附是普通标点还是字符串拼接?+
附是字符串拼接语法糖。编译器先后绑定并检查左右操作数,再调用字符串拼接原语;结合方向不允许改变左右副作用的先后次序。
029列【…】会生成什么数据结构?+
列表语法展开成标准库的缀和罄构造器:每个元素接到后续列表,空列表就是罄。它不是普通函数调用语法,也不能据此当成 C 数组布局。
030典【…】是运行时字典字面量吗?+
不是。典保留静态配置树,供豫构解码;普通表达式类型检查不把它当成运行时字典。不要把包配置的写法直接用于一般计算表达式。
04函数与多态
031为什么会参数而函数体有时无法推断类型?+
无参数类型的会表达式需要期望函数类型来检查,不能总是单独合成类型。给函数写签名,或使用带参数类型的遇形式,可以提供所需信息。
032遇和会的主要区别是什么?+
遇显式携带参数类型,检查后可扩展环境并合成函数体类型;会依赖外部期望箭头取得参数类型。二者都创建函数,而不是立刻执行函数体。
033承和受如何配合表达多态?+
承描述隐式全称类型,受引入对应的隐式类型参数函数。这里的参数用于类型,并不是任意运行时值参数;也不等于一般依赖类型系统。
034授以传递的是值还是类型?+
授以向函数提供显式写出的隐式类型实参。实参必须形成合法类型,不能把整数值一作为类型实参;普通运行时参数使用于传递。
035连续两段化意味着可以只传第一个参数吗?+
不意味着。连续箭头会形成一组参数,非空调用必须给足当前组;二参数函数不能因为写作 f 于 a 于 b,就认定 f 于 a 一定合法。
036为什么把字面量先绑定到变量可能帮助推断?+
基线的实参类型预读只直接读取局部变量或文件定义引用,不递归合成所有复杂表达式。先绑定可让类型变得可读;另一种办法是明确授以类型或提供期望结果类型。
037隐式类型参数能插在普通参数之间吗?+
当前参数组不允许这样写。调用先读取前面的授以,再顺序消费该组显参;开始消费显参后,在它们之间插入隐参会被拒绝。
038局部绑定会自动获得任意多态类型吗?+
不会。基线不做自动 let 泛化,不能把其他语言的 Hindley–Milner 推断习惯直接套用。需要多态时,应按承、受和类型签名的规则表达。
039所有隐参都能通过反复搜索推出来吗?+
不能。基线按顺序做有限结构匹配,最后仍有未解类型参数就拒绝;它不是全局反复求解器,也不会随意利用后面 lambda 的线索补齐未知。
040类型参数会增加运行时函数参数数量吗?+
一般隐式类型参数在检查后擦除,不执行也不占运行时槽位。计算调用协议时,必须区分源码显参、隐参以及闭包自身这几种不同角色。
05类型与模式匹配
041类型名字相似就代表同一种类型吗?+
不代表。普通值不会因为名字像类型就合法;名义类型有自己的声明身份,不同名义声明即使结构相同也不相等。
042透明类型别名如何参与类型比较?+
比较前从检查后的接口展开透明类型引用,再比较对应结构。编译器不会为了类型相等而随意执行普通值函数体。
043元类型之上还有无限层类型宇宙吗?+
基线没有引入这样的宇宙层级。元类型本身也被接受为类型;这是一项具体实现规则,不应据此宣称豫言拥有任意形式的依赖类型计算。
044能把任意函数调用放进类型里计算吗?+
不能。类型应用头要解析到名义构造器,并按其声明消费合法类型参数;运行时值、普通 lambda 和未解析投影不能充当任意类型计算头。
045豫言会自动做宽度子类型转换吗?+
基线没有普遍的宽度子类型或协变自动提升。普通对象的所谓子类型检查调用的是类型相等,不能期待字段更多的结构自动被接受为字段更少的类型。
046模式匹配中的新名称有什么含义?+
未被识别为构造器的新名称会引入局部绑定,其类型来自分析对象。已解析为构造器的名称则按构造器模式检查,不能一律把所有名称当变量。
047模式能调用任意函数反推输入吗?+
不能。应用模式必须以构造器为头,不是把普通函数倒过来求解。模式测试只做规定的标签、常量或字段判别,不执行任意用户函数。
048没有覆盖所有模式会在编译时被拒绝吗?+
不一定。基线并非对所有匹配做静态穷尽证明,缺少分支会生成运行时报错路径。编写匹配时仍应主动考虑所有形态和默认处理。
049一个模式分支绑定的名字能在另一个分支使用吗?+
不能。每个分支独立建立绑定环境,名字只在本分支结果中可见。不要把同名重复模式绑定自动理解成相等性守卫。
050为什么调整首个匹配分支可能影响类型推断?+
在合成模式下,基线从首支合成结果类型,再检查其他分支;它不会任意换一支来推断。若首支无法合成,可考虑提供明确的期望类型,而不是依赖分支猜测。
06名称、模块与包
051局部变量同名时使用哪一个?+
局部绑定优先采用最新者,也优先于命名空间和打开文件的名称。但顶层定义列表有不同规则,不能把局部覆盖习惯推广到所有场景。
052能在文件前面引用后面才定义的普通名称吗?+
不能仅因后面存在定义就认定合法。顶层按声明次序处理,先解析定义体再加入后续环境;有签名的自递归另有专门规则。
053打开两个含同名定义的模块,谁优先?+
基线每次打开文件会前插,所以后打开的文件在这类查找中优先。为避免顺序影响阅读,可以使用命名空间投影明确指向所需定义。
054寻、观、诵有什么区别?+
寻建立以路径末段命名的空间;观让目标接口参与未限定名称查找;诵重导出目标接口。组合形式按这三个效果展开,观不自动等于寻。
055寻观标准库会自动读取包里的所有文件吗?+
不会。单段包导入定位到包的总集。豫,不自动聚合整个目录。需要具体子模块时,要按包根下的模块路径定位。
056编译器如何确定包根?+
包根是“名字。包。豫”所在目录,直接依赖由包说明声明。子包构成边界,不能沿父包路径越过子包边界来绕开包定位。
057模块的之投影会在运行时查字典吗?+
不会按运行时字典查找。模块投影在名称解析阶段消除并定位到文件定义引用;它与普通运行时数据结构查找是不同机制。
058循环导入能靠先加载半份接口解决吗?+
基线不允许这样绕过。自身导入和循环依赖不能靠部分接口蒙混通过;拆分共享定义并整理依赖方向,是排查这类问题的思路。
059重导出会把定义身份改成当前文件吗?+
不会。重导出必须保留原文件和定义身份,否则可能改变类型身份与链接符号。它不是在当前文件虚构一份新的源码定义。
060只有对象文件就能检查依赖它的新源码吗?+
不能。对象文件能供链接,却不足以恢复类型别名、隐参和构造器身份;前端还需要依赖源码或等价的检查后接口记录。
07求值、递归与异常
061定义一个函数会立即执行它的函数体吗?+
不会。函数形成产生代码与词法捕获环境,调用时才执行函数体。这是闭包保存定义时环境的基础。
062调用会先算完所有参数再进入函数吗?+
基线普通调用先求函数,再求第一个运行时实参并应用,之后才求下一个实参。把所有实参提前求完可能改变中间函数体的副作用次序。
063传递引用会复制整份数据吗?+
不会复制引用内容,而是复制位置身份。多个别名会观察到同一次写;普通值参数按值传递不意味着深拷贝所有引用对象。
064普通布尔函数天然具有短路行为吗?+
不天然具有。若是普通函数,短路取决于参数是否以延迟操作等形式组织;条件表达式本身只执行选中分支,不能把这条特殊规则套到所有布尔调用。
065模式匹配会重复计算被分析的表达式吗?+
规范要求只求一次分析对象,然后测试模式并执行选中分支。编译器可以选择不同的决策树,但不能因此重复原表达式的副作用。
066递归闭包怎样引用自己?+
通用转换先分配单槽递归单元,再让闭包捕获这个单元,最后把闭包写回单元。若捕获的是初始化前的单位值而非单元,自引用就会失效。
067递归数据会自动成为惰性的无限结构吗?+
不会。一般递归值若在填槽前读取自身,可能看到未初始化占位或导致错误;基线没有因此提供惰性无限数据结构语义。
068依赖模块会按什么顺序初始化?+
依赖先于使用者初始化,同一程序初始化序列不重复执行同一模块。模块入口自身却没有任意重复调用的幂等保证,不能绕过程序顺序反复调用它。
069异常处理函数自己又抛异常时谁来处理?+
尝试运行安装的新处理器会先恢复旧处理器,再执行当前处理逻辑。因此处理器内部再次抛出的字符串异常交给外层处理,而不是反复进入自身。
070提前返回的续延能保存下来任意多次调用吗?+
没有这样的通用契约。基线续延受动态调用范围约束,原生和 WasmGC 都不保证恢复已经销毁的调用栈;它主要服务于该动态范围内的提前返回。
08编译器内部
071编译器大致经过哪些阶段?+
从扫描、解析和名称解析开始,进入类型检查,再做模式展开、类型擦除、递归与闭包转换等,最终生成 LLVM 或 WasmGC。实现可以合并阶段,但必须保持各阶段的不变量。
072类型检查会执行用户程序来判断类型吗?+
静态检查不执行一般用户函数。合法类型和隐参要在静态阶段确定;无法合成类型不能退化成“先作为动态值跑起来再说”。
073类型擦除会删除哪些东西?+
擦除去掉类型标注、类型值、隐式类型参数及抽象,保留运行时计算。它不能改变源码显式参数的求值顺序,也不能随意压缩公开定义的定序。
074为什么编译器要做闭包转换?+
嵌套函数可以引用外层局部值,提升后的代码需要明确携带这些捕获值。闭包转换将它们组织成代码引用和捕获元组,让调用者把闭包自身一并传入。
075ANF 是什么,为什么给中间结果起名字?+
ANF 把复杂子表达式转成命名的中间值,使求值顺序和调用边界显式化。它必须先规范化函数再处理实参,并把条件的继续计算接到合并点,不能先算两支。
076非尾调用为什么要保存局部值?+
调用返回后仍要使用的值必须存入豫言帧槽,返回后重新加载。这同时帮助 GC 找到存活值;所有被扫描的槽都要初始化,不能留下非法游标。
077尾调用靠什么避免不断增加栈帧?+
尾位置复用当前帧。原生生成 musttail,WasmGC 使用 return_call_indirect;普通 C 调用后接 return 并不能自动保证恒定栈空间。
078加入临时变量会让原本的尾调用失效吗?+
正确的 ANF 转换不应如此。“令 x 为调用结果,然后返回 x”仍是尾位置;条件分支末尾和函数体末尾也要保留尾调用性质。
079优化器是不是程序能够运行的必要条件?+
规范要求检查后的结果能交给简单的不优化后端。优化器不能被用来掩盖非法或未完成的中间树;跨模块优化可以跳过,但公开入口契约仍须遵守。
080独立编译器必须复制原编译器的 JSON 缓存格式吗?+
ABI 兼容本身不要求复制缓存格式。只有要读取原编译器阶段文件时,才需要额外遵守阶段标识、定义数组、绑定索引和版本等格式约定;阶段文件应当作为数据读取。
09原生运行时与 GC
081豫言的整数是 128 位整数吗?+
原生值容器是 128 位,但整数数值位宽为 64 位。其余位承载标签等信息;容器宽度不能直接当作整数精度,也不能据此承诺任意精度算术。
082原生语言函数的两个机器参数就是源码参数吗?+
不是。LLVM 入口统一为 void(i128, i128),传的是豫言栈帧地址和当前堆分配地址。源码实参放在豫言帧中,不能随意改成两个裸指针签名。
083闭包调用和直接调用的首个参数槽相同吗?+
不同。闭包调用的槽 5 放闭包自身,槽 6 才放显式参数;直接调用不传闭包,首参位于槽 5。是否传 self 取决于入口契约,不能只看相同的 LLVM 函数类型。
084原生闭包在内存里长什么样?+
它使用普通元组:第零字段是代码指针,后面是捕获值。调用者读取代码并传递整个闭包,不应依赖另一个编译器私有的捕获字段顺序;无捕获闭包仍有代码槽。
085构造器编号是整个文件统一递增的吗?+
不是。编号在同一文件、相同最终返回类型头的构造声明中从 1 计数,不同类型分别计数。它与文件定义表的零基定序是两套数字。
086构造器有很多载荷字段也能安全使用旧原生 GC 吗?+
不能仅凭前端接受就保证安全。基线前端允许至 250 个载荷,但 C 运行时读取的子类型只保留四位;超过 15 个载荷不能当作已安全兼容的原生 GC 对象。
087原生字符串为什么有隐藏的末尾零?+
基线存储长度包含额外的末尾零,逻辑内容长度不包含它。静态字符串直接指向字节,堆字符串另有头槽;与 C 互调时要核对接口要求的长度是否含末零。
088字符串中间能包含零字节吗?+
逻辑内容可以含零,按长度处理的原语能够保留它。但 strlen、按 %s 打印等 C 文本操作会按零终止,所以不能推断所有字符串接口都能完整处理嵌零内容。
089GC 会扫描 C 栈和所有机器寄存器吗?+
不会。旧 GC 扫描已登记的值地址以及豫言栈的有效槽,不扫描普通 C 栈和任意寄存器。跨安全点仍需使用的值必须按根与帧协议保存,移动后重新读取。
090为什么不能用裸地址作为引用的永久身份?+
基线 GC 会搬迁堆对象并更新根值,原始机器地址可能改变。引用共享和循环通过转发标记保留;把旧地址藏在未登记位置会绕开更新,不能作为稳定引用使用。
10网页汇编与互操作
091WASI 和 WasmGC 使用同一种值布局吗?+
不是。LLVM/WASI 继续使用逻辑 i128 值和豫言帧,WasmGC 使用引擎引用、数组和结构。不能把两条路径都当成原生指针 ABI。
092能把原生对象文件直接接到 WasmGC 模块里吗?+
不能。原生对象和 WasmGC 的值型、调用栈与模块组织不同。基线 WasmGC 生成合并模块,不提供任意拼接独立模块的原生式对象 ABI。
093WASI 的线性内存地址能当宿主裸指针吗?+
不能。它表示 guest 线性内存偏移,宿主必须检查边界后复制数据。i128 参数也要交给 LLVM/clang 做目标 ABI 降级,不能自行猜导出函数的机器签名。
094WasmGC 如何表示小整数和大整数?+
在 -2^30 到 2^30-1 范围内用 ref.i31,并按有符号 i31 解码;其他 i64 整数放进含 i64 字段的 $big 结构。这里的大整数指超出 i31 范围,不是任意精度整数。
095WasmGC 字符串也有原生的隐藏末尾零吗?+
没有。WasmGC 字符串和字节串使用可变 i8 数组,数组长度就是逻辑字节数。移植原生字符串处理代码时,不能机械地多算或少算一个字节。
096WasmGC 闭包里保存的是机器代码地址吗?+
不是原生地址。闭包首项是函数表索引的 i31 表示,再通过相应函数类型进行间接调用;不能把原生代码指针塞进 i31 期待互通。
097WasmGC 内部函数参数数量没有上限吗?+
基线生成 $t0 到 $t16 的函数类型,闭包 self 也计入运行时参数数量。超过这个范围不能宣称已经支持;内部参数组也不能仅由源码箭头数量推定。
098Wasm 的 trap 都能被语言异常处理器捕获吗?+
不能。算术、索引越界或引用 cast 失败产生的 Wasm trap,不会自动成为可捕获的字符串异常。语言提前返回使用另外的异常标签与动态标识机制。
099WasmGC 怎样调用没有内联的系统原语?+
通过 yuyan:gc-host/v1 模块的 call 引用桥,第一参是原语名字节串,第二参是按 C 参数次序排列的元组。宿主用模块暴露的实际类型原型构造返回引用,而不是只凭相似结构猜类型。
100宿主桥能透明传递任意闭包、引用和循环结构吗?+
不能。通用桥支持有限值型,递归转换有 512 层上限,不通传任意构造器、语言闭包或原生指针。逐次复制不保证循环与共享身份,因此引用读写等身份敏感操作留在 guest 内部。