跳到正文豫言技术文档

豫言 / 技术规范

本篇描述 4cdeca1 的实现。版本与适用边界见基线说明。

词法与语法

本篇定义普通源码的解析输入及树构造。下文 式 是语法层表达式,是否为合法类型/值由下一篇决定。混合位置操作符有歧义消解规则,不能把本篇 EBNF 直接交给任意优先级默认的解析器。

词一:源文本扫描

按 UTF-8 字符顺序扫描。普通源码外层显式忽略 U+0020 空格、U+3000 全角空格、U+0009 制表、U+000A 换行。表达式可以跨行。标识符、字符串、注释内部的字符原样保留。

文件首行以 #! 开头时(脚本的解释器行,如 #!/usr/bin/env -S 豫构 运行),整行当作空白,后面各行的行号照旧。

扫描器至少有五种状态:外层、名称、字符串、字符串转义、注释。外层遇 「: 先进入注释;否则 「 进入名称;『 进入字符串;其他非空白字符保存为具体字符令牌。

名称一直读到第一个 」,内容原样保留,不做反斜线转义或 Unicode 正规化。空名称能在扫描阶段形成,但不能因此承诺它在表达式或绑定位置合法;正式接口应使用非空名称;空名称在不同上下文的接受边界不作为常规可移植源码约定。字符串一直读到未转义的 』,允许实际换行及其他标点。未闭合名称、字符串、转义或注释必须拒绝。

字符串中的 「:内容:」 是转义,而非注释。允许的内容恰为:

原文 结果
「:换行:」 U+000A
「:制表:」 U+0009
「:』:」 字符 』
「:「::」 两字符 「:

不支持 \n 这类反斜杠转义的特殊解释;不识别的上述转义内容报错。注释支持深度计数:遇 「: 加一,遇 :」 减一;外层结束时出状态。注释对运行值无影响,但保留文字及前后方向:当前行已有语法元素则后置,否则前置;换行会清除该行状态。

词二:名称到字面量

语法绑定已经建立之后,只对自由名称做以下识别;已转为局部绑定索引的引用不能再误改为数字。

  1. 阳、阴、元 为保留内建值。
  2. 《《内建类型:字符串》》、整数、小数、动态分类值、有、爻、元类型 这七个完整内建类型拼写(均带同样前后定界)为内建类型节点。
  3. 完整内建值拼写为 《《内建爻:阳》》、《《内建爻:阴》》、《《内建有:元》》。内建函数拼写及类型见类型篇。
  4. 名称非空且每字符属于 0123456789.零一二三四五六七八九点 时尝试数字解析;.、点 都是小数分隔符。无分隔符按十进制逐位累加;单分隔符把两侧作为整数部和小数部;多分隔符不识别成数字。名称的第一个字是 负、其余部分按本条是含数字字的数字时,得到对应的负数:负一二 是 −12,负一点五 是 −1.5;只认一个 负,负 本身仍是名称。
  5. 其余仍是自由名称,等待名称解析。

汉字是逐位数字,一二三=123;十、-1、1e3 不由上述数值规则识别。空分隔片段的兼容情况须按分隔算法和用例判断,不把“词法扫描成功”视为数值有效。数字越界不是任意精度契约。小数字面量保存规范化阿拉伯整/小部分及宿主浮点近似值;生成常量时使用前者,不使用默认六位打印值。

能读成数字的名称(上面第 4 条,含 点、负一 这类)只用作数字字面量。令、复令、得、设、得、凡、化 引入的局部名(含带标签函数的形参名)若是这样的名称,编译器报错“数字名不能用作局部名:「名」(文件)”,所以局部名要含至少一个非数字字,如 项一、点位。模式匹配里作为常量去匹配的数(如 或有「零」则 …)是数字字面量,照旧可用。

词三:卦码

卦曰「名」 写一个位模式(提案 00030)。卦曰 是前缀关键词,紧跟一个直接写成 「…」 的名称;名称里的字按下文的记号读成位,只由本节的规则识读。结果是整数:值与爻数(位宽)由名称唯一确定,在具体语法树里记成卦码节点,展开时成为整数节点,此后各阶段不变。

名称由记号连成,记号有四类:

类 位宽 记号与值
爻 1 阴 0、阳 1
四象 2 太阴 0、少阴 1、少阳 2、太阳 3
八象 3 地 0、雷 1、水 2、泽 3、山 4、火 5、风 6、天 7
卦 6 六十四卦,值见下表

六十四卦的值是全名的两位八进制(全名先说上卦、后说下卦):

上\下 地 0 雷 1 水 2 泽 3 山 4 火 5 风 6 天 7
地 0 坤 0 复 1 师 2 临 3 谦 4 明夷 5 升 6 泰 7
雷 1 豫 8 震 9 解 10 归妹 11 小过 12 丰 13 恒 14 大壮 15
水 2 比 16 屯 17 坎 18 节 19 蹇 20 既济 21 井 22 需 23
泽 3 萃 24 随 25 困 26 兑 27 咸 28 革 29 大过 30 夬 31
山 4 剥 32 颐 33 蒙 34 损 35 艮 36 贲 37 蛊 38 大畜 39
火 5 晋 40 噬嗑 41 未济 42 睽 43 旅 44 离 45 鼎 46 大有 47
风 6 观 48 益 49 涣 50 中孚 51 渐 52 家人 53 巽 54 小畜 55
天 7 否 56 无妄 57 讼 58 履 59 遁 60 同人 61 姤 62 乾 63

初爻(最低位)是第零位,名称自左向右由高位写到低位,值是各记号按位宽依次拼成的二进制数。位宽除以六的余数写在最前,称为头,其后每六爻一卦:

余数 头 例 位宽与值
0 无 卦曰「乾乾」 12 位,0xFFF
1 爻 卦曰「阳乾」 7 位,0x7F
2 四象 卦曰「少阳震」 8 位,0x89
3 八象 卦曰「天乾」 9 位,0x1FF
4 爻加八象 卦曰「阴雷师临」 16 位,0x1083
5 四象加八象 卦曰「太阳风」 5 位,30

单个记号也是卦码:卦曰「乾」 是 6 位的 63,卦曰「天」 是 3 位的 7,卦曰「阳」 是 1 位的 1。头上的记号即使是零(太阴、阴地)也写出,所以位宽总在字面上,同值同宽只有一种写法;打印器按值与爻数写回这种规范写法。每个记号都不是另一个记号的开头,连写的名称只有一种切分。卦曰〇 比所有中缀都紧,总是先规约,所以 「加」于卦曰「太阳坤」于「二」 的第一个实参就是卦码。

爻数至多六十四;值按无符号位型存进整数,六十四爻的最高位为阳时值是负数,与十进制写大数一样回绕。名称里有不认得的字、头与卦的次序不合(头之后只能是卦,爻或四象之后至多再接一个八象)、爻数超过六十四,或名称为空时报错;卦曰 之后不是直接写成的名称(如括号、字符串)时也报错。

语一:普通源码形状

带标签的写法有三处:函数类型的参数 化「签」曰 类、匿名函数的参数 得「签」曰「名」、调用的实参 于「签」曰 值;标签位置须为名称,规约关系按不带标签的写法查表,见标签函数类型。可省参数在签名里写 化「签」曰 类 可阙,在定义里写 得「签」曰「名」阙则 值;不带标签的 化 类 可阙 报错,见标签函数类型签六。

引号中的文字为终结符;花括号表示重复,方括号表示可选;这里的 EBNF 元记号不是豫言源码。名 是带 「」 的名称令牌,串 是带 『』 的字符串令牌。

文件 = { 声明 "。" | "。" } [ 声明 ] ;
声明 = 导入 | 名 ("者" | "即") 式 "也" | 名 ("乃" | "立") 式 ("也" | "者也") | 式 ;
导入 = ("寻" | "观" | "寻观") 路径 "之书" ;
路径 = 名 { "之" 名 } ;
式 = 名 | 串 | "(" 式 ")"
   | "得" 参名 { "又" "得" 参名 } "而" 式
   | "设" 名 { "又" "设" 名 } "而" 式
   | "化" 参类 { ("又" | "可阙又") "化" 参类 } ("为" | "可阙为") 式
   | "凡" 名 [ "属" 式 ] { "又" "凡" 名 [ "属" 式 ] } "而" 式
   | "令" 名 "为" 式 "而" 式
   | "复令" 名 "属" 式 "即" 式 "而" 式
   | "若" 式 "则" 式 "否则" 式
   | "鉴" 式 "而" 分支 { 分支 }
   | 式 ("且" | "或") 式
   | 式 ("于" | "以" | "又以" | "之" | "中" | "与" | "合" | "属" | "而后" | "接") 式
   | 式 "于" 名 "曰" 式
   | 式 ("加" | "减" | "乘" | "除以" | "大于" | "小于" | "等于" | "不等于") 式
   | "非" 式
   | "卦曰" 名
   | 序列首 "【" [ 式 { "," 式 } [ "," ] ] "】" ;
参名 = 名 [ "曰" 名 ] [ "属" 式 ] [ "阙则" 式 ] ;
参类 = [ 名 "曰" ] [ 名 "属" ] 式 ;
分支 = "或有" 式 "则" 式 ;
序列首 = 名 ;

文件切句在词法令牌上进行,只在圆括号深度零时以具体 。 切分;连续空句忽略,文件末尾未带句号的非空尾句仍解析。字符串/名称中的 。 已被包成令牌。普通圆括号不容内联结构声明;典/阶段扩展不是任意普通运行时语法。

鉴 本身不额外消费终结 也;例子的 也 通常属于外层声明。鉴 的每个分支(含首支)都写 或有(提案 00024);或有 是一个关键词,两字相连时总按一个认;单字 且、或 是短路的爻运算:甲 且 乙 即 若 甲 则 乙 否则 阴,甲 或 乙 即 若 甲 则 阳 否则 乙(提案 00022)。合 不建立联合类型。卦曰 是前缀关键词,后接一个名称,写一个位模式,见词三。

若 的 否则 后可以直接再写一个 若,形成右嵌套的 否则若 链。链的末支可以是完整的 令 x 为 a 而 b;这个 令 的 而 只引入 b,b 结束后的 而 归外层仍需要它的运算符(例如 复令…即 得…而 … 而 … 中归那个 得 与外层 复令),与只有「则/否则」两层时相同。多写一个 而 时,报错说明它接不上,并从期望里去掉这个已经遇到的记号。

语二:操作符解析算法

每个操作符由固定字符串组件、表达式槽 〇、绑定槽 □ 组成。实现保留多个组件匹配候选,不用最长关键词匹配武断消除短候选;只有中缀关键词在同一处能匹配长短几个时留最长的,所以 或有 不拆成 或 与 有。绑定槽只读一个名称并匹配其后固定组件;表达式槽递归参与解析。

可用有共享子树的 chart parser 或具有分支状态的移入规约器:扫描令牌;扩展所有可匹配操作符组件;完成组件模式时形成候选规约;遇两种嵌套/连续规约冲突时查询规约关系表。表返回左规约则先完成左式,右规约则让右式占据其表达式槽;无关系时保留可行状态。规约结果还须经过各操作符的形状约束,例如序列首与投影名称。

最终必须有唯一合法解析;无解析拒绝,多条不等价解析拒绝,不按容器遍历顺序选第一棵。括号强制内部式先形成,括号节点本身不进入类型树。表中没有的组合按规约关系表文末的后备规则决定。且、或 只与彼此有规约关系,与其他中缀操作符混写须加括号,否则报“未定义优先级关系”;遇 或有 时前一分支体先收束,内层 鉴 除外,见规约关系表。

至少复现这些容易反直觉的结果:f 于 a 于 b 的应用脊为 [a,b];f 于 a 接 b 等价 f 于 (a 接 b);a 或 b 且 c 等价 a 或 (b 且 c);A 合 F 于 X 等价 A 合 (F 于 X);f 于 卦曰「甲」 于 b 的应用脊为 [卦曰「甲」, b];f 于 「列」【a,b】 等价 f 于 (「列」【a,b】),「组值」【…】 属 T 等价 (「组值」【…】) 属 T:序列头与紧挨在前的名称结合最紧,带尾逗号的 【…,】 亦同。词法空白已经删除,所以 寻 观 等空白分隔组件不能按英语词界处理。

语三:树构造与语法糖

解析先得具体语法树(提案 00026):按源码原样保留括号(括号节点)、且与或(短路且、短路或节点)、接(字符串拼接节点)、卦曰「名」(卦码节点,存值与爻数),以及 「列」【…】、「组值」【…】、「组类」【…】(通用序列节点;序列头记成具体字符节点,组值、组类的命名项先析成命名项节点,免被外层同名的绑定当作变量);调用、箭头、隐式量化与元组都按源码二元或单层地记下;数只存数值(全由数字字组成的名字记成整数节点、小数节点,声明名与导入路径除外),拼法由打印器决定。随后展开:自底向上去掉括号、展开这些语法糖,并做下表所列的合并与展平,结果就是下表的树。打印用展开前的具体语法树。句号之后同一行的后置注释归前一条声明,作为它的顶层后置注释。

表面式 树与约束
得 x 而 e 显式无标注函数,x 只绑定 e
得 x 属 A 而 e 显式带参数类型函数,x 不绑定 A
设 a 而 e 隐式类型参数函数
化 A 为 B、化 A 又化 B 为 C 若 B 已是连续箭头 [B1,…,R],前加 A;否则 [A,B];又化 与嵌套的 为 同义
凡 a 而 B、凡 a 又凡 b 而 B 隐式全称类型;相邻同形隐式量化合并计数
化 a 属 T 为 B T 是语法上的 类(或相应内建节点)时报错,类型参数写成 凡 a 而 B;否则造依值箭头(B 提到 a 时)或普通箭头,见依值类型
凡 a 属 T 而 B T 是语法上的 类(或相应内建节点)时为隐式全称类型;否则为隐式值参数,见依值类型;不接受任意同义别名作 类
f 于 x、f 以 A 对 f 的调用脊追加显式/隐式实参;已有调用头不再二叉包裹
e 属 A 类型标注;左对象、右类型;运行时只保留 e
令 x 为 a 而 b 先 a,再在 x 绑定下 b;x 不绑定 a
复令 x 属 A 即 a 而 b 局部绑定递归值;x 分别绑定 a、b,不绑定 A
甲 而后 乙 先求甲、再求乙,结果是乙
a 与 b、A 合 B 右侧已经是元组节点时展平;左侧元组保留为嵌套项
x 中 n n 为整数字面量时存为常量索引;n 为其他名称时是按名投影,类型检查时换成该字段的序号
m 之 n n 必须为名称,变为静态投影字段
a 接 b 依次绑定经字符串检查的 a、b,再调用内建的字符串拼接;临时名须卫生
甲 加 乙 等算术比较中缀、非 甲 即 「加」于 甲 于 乙、「非」于 甲,按作用域找名字
卦曰「名」 名称读成(值,爻数),先记成卦码节点(打印时按爻数写回规范的卦名),展开成整数节点,值原样带过去,见词三
「列」【a,b】(源码须用中文逗号) 缀 于 a 于 (缀 于 b 于 罄);空序列为 罄;模式位置同样展开
「典」【…】 保留静态配置树,由豫构解码;普通表达式类型检查不接受运行时字典字面量
「组值」【…】、「组类」【…】 元组值与元组类型的列表写法,按列表项直接构造元组:写几项就是几项,某项本身是元组(包括括号里的 b 与 c、B 合 C)时作为一项嵌套保留;项写作 「名」者 值 也(组值)或 「名」乃 类型 也(组类)时是命名元组。各项须全部命名或全部不命名,字段名不可重复;空序列为 元、有,单项为一元元组;模式位置同样展开
「《《扩展名》》」【…】 留给对应 IR 阶段解释;普通源码不任意执行扩展
「头」【…】(其余的头) 构造写法(提案 00018 议十九):阐述记成构造节点,名称解析按普通名字找头,类型检查按构造调用约定降为带标签调用,见《标签函数类型》签七

元组的展平具有方向性:a 与 b 与 c 是三项;(a 与 b)与 c 的首项是二元组;a 与(b 与 c) 仍被展平。函数箭头的右展平同理,括号不会凭空增加可部分调用边界。

展平按右侧的字面形式判断,值与类型用同一条规则:右侧字面上是元组(b 与 c、B 合 C、「组值」【…】,括号不影响)时展平;右侧是变量、调用、令 等求值为元组的式子时,值保留为嵌套的一项。所以值 a 与 x(x 是二元组变量)是二元组,它的类型要写成 「组类」【A,(B 合 C)】 或借类型别名写成 A 合 P(P 即 B 合 C);写成 A 合(B 合 C) 会展平成三元组而对不上。

元组的展平只属于 与、合。列表写法按项构造:「组值」【a,(b 与 c)】 是二元组,第二项是二元组 b 与 c;「组值」【a,b 与 c】 与之相同;「组类」【A,(B 合 C)】 同理。a 与「组值」【b,c】 的右侧已是元组节点,按 与 的规则展平为三项。

语四:声明预处理

一条 乃 声明与紧随同名 者/即 定义合成一项,末尾独立签名、不同名或中间夹其他声明均拒绝。六种文件是例外,没有同名定义的签名(无体声明)在合并前补一个定义,由文件后缀决定谁来提供:。内建。豫 由编译器提供,补的函数体调用内部原语,类型检查按原语表核对签名;。接口。豫 由宿主提供(构建时绑了适配,适配的同名 。豫 先被找到,接口文件就不编译),补的函数体调用宿主导入,代码生成写成带类型的 Wasm 导入,宿主没有实现时调用即报“接口函数未绑定:模块.字段”;。应用接口。豫 由应用提供,补的函数体调用应用接口,入口定义了同名函数时编成导出(见网页汇编接口网五),入口没有实现却被调用时抛出可捕获的异常;底层内建声明 。底层内建。豫 按底层原语表核对后就地生成指令,底层接口 。底层接口。豫 编成 Wasm 导入,模块名是接口文件名(见《底层模式》七);着色内建声明 。着色器。接口。豫 补占位定义,其值按声明类型抛出“接口函数未绑定:名”异常。补的函数体逐个接收参数,见类型与模块类六。所以这些文件都可以被导入和类型检查。合并后先对定义体建立同名抽象;若声明类型为箭头/隐式全称,或定义为函数,则加递归节点,否则去掉该顶层绑定。无签名定义不获得这条自动递归规则。

所有普通命名声明初始公开;无普通私有声明关键字。顶层裸式获内部匿名名但仍占有序定义位置;它只在测试、脚本与黄金样例里合法,普通源文件的顶层只有定义(见执行与编译行一)。文件操作按导入、打开次序展开为内部项;不能忽略这些项后继续用源码声明序数作为 ABI 序号。

语五:最小解析用例

「一」属「数」 的树为标注,不是声明;「数」中「一」 语法可形成,类型检查会失败。「甲」中「序数」 若序数不是数字则语法拒绝;「甲」【「一」】 的头是「列」「典」「组值」「组类」与「《《扩展名》》」以外的名字时为构造写法,按《标签函数类型》签七降为带标签调用;头找不到时报“未定义的构造「甲」”。裸 1 不形成数字表达式;「1」 才进入数值识别。整数字面量检查为「小数」时当作小数字面量(提案 00018),如「秒」的参数是小数,(「秒」于「三」) 与 (「秒」于「一点五」) 都可写。未闭合定界符须在对应扫描状态报告错误,不能继续吞到下一个文件。卦曰「少阳震」 展开成整数 137;卦曰 之后写括号、字符串等非名称的式子时语法拒绝,名称不合卦码写法时报错,见词三。