词法与语法
本篇定义普通源码的解析输入及树构造。下文 式 是语法层表达式,是否为合法类型/值由下一篇决定。混合位置操作符有歧义消解规则,不能把本篇 EBNF 直接交给任意优先级默认的解析器。
词一:源文本扫描
按 UTF-8 字符顺序扫描。普通源码外层显式忽略 U+0020 空格、U+3000 全角空格、U+0009 制表、U+000A 换行。表达式可以跨行。标识符、字符串、注释内部的字符原样保留。
文件首行以 #! 开头时(脚本的解释器行,如 #!/usr/bin/env -S 豫构 运行),整行当作空白,后面各行的行号照旧。
扫描器至少有五种状态:外层、名称、字符串、字符串转义、注释。外层遇 「: 先进入注释;否则 「 进入名称;『 进入字符串;其他非空白字符保存为具体字符令牌。
名称一直读到第一个 」,内容原样保留,不做反斜线转义或 Unicode 正规化。空名称能在扫描阶段形成,但不能因此承诺它在表达式或绑定位置合法;正式接口应使用非空名称;空名称在不同上下文的接受边界不作为常规可移植源码约定。字符串一直读到未转义的 』,允许实际换行及其他标点。未闭合名称、字符串、转义或注释必须拒绝。
字符串中的 「:内容:」 是转义,而非注释。允许的内容恰为:
| 原文 | 结果 |
|---|---|
「:换行:」 |
U+000A |
「:制表:」 |
U+0009 |
「:』:」 |
字符 』 |
「:「::」 |
两字符 「: |
不支持 \n 这类反斜杠转义的特殊解释;不识别的上述转义内容报错。注释支持深度计数:遇 「: 加一,遇 :」 减一;外层结束时出状态。注释对运行值无影响,但保留文字及前后方向:当前行已有语法元素则后置,否则前置;换行会清除该行状态。
词二:名称到字面量
语法绑定已经建立之后,只对自由名称做以下识别;已转为局部绑定索引的引用不能再误改为数字。
阳、阴、元为保留内建值。《《内建类型:字符串》》、整数、小数、动态分类值、有、爻、元类型这七个完整内建类型拼写(均带同样前后定界)为内建类型节点。- 完整内建值拼写为
《《内建爻:阳》》、《《内建爻:阴》》、《《内建有:元》》。内建函数拼写及类型见类型篇。 - 名称非空且每字符属于
0123456789.零一二三四五六七八九点时尝试数字解析;.、点都是小数分隔符。无分隔符按十进制逐位累加;单分隔符把两侧作为整数部和小数部;多分隔符不识别成数字。名称的第一个字是负、其余部分按本条是含数字字的数字时,得到对应的负数:负一二是 −12,负一点五是 −1.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;卦曰 之后写括号、字符串等非名称的式子时语法拒绝,名称不合卦码写法时报错,见词三。