- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
本文以 Unison 开源仓库中的转写(transcript)测试文档no-hash-in-term-declaration.output.md为线索,深入剖析 Unison 语言的一条核心语法约束:Term(项)声明的名称——无论是类型签名还是类型定义中的名称——均不允许包含哈希限定符(hash qualifier,如x##Nat)。读完本文,你将理解这一规则的作用范围、它在词法/语法解析层的落地方式、违反规则时产生的错误行为,以及如何通过仓库中的转写测试机制验证这条约束。
一、规则速览:Term 声明中不允许出现哈希
unison-src/transcripts/no-hash-in-term-declaration.md(其转写输出即本文关联文档 no-hash-in-term-declaration.output.md)用一句话定义了该规则:
There should not be hashes in the names used in term declarations, either in the type signature or the type definition. (Term 声明中所使用的名称不应包含哈希,无论是在类型签名中还是在类型定义中。)
也就是说,规则覆盖两种位置:
| 位置 | 说明 | 示例 |
|---|---|---|
| 类型签名(type signature) | x : SomeType声明中的被声明名称 | x##Nat : Int -> Int -> Boolean |
| 类型定义/函数体(type definition) | 函数定义x = ...左部(head)中的绑定名称 | x##Nat = 5 |
作为对照,哈希限定名(hash-qualified name)在 Unison 的其他语法位置上是被正常支持的:例如在表达式、类型引用中,可以通过Name#hash的形式精确定位某个名称对应的特定 hash(如引用base##Nat、.foo.bar#xyz)。被禁止的只是"在声明的位置上把名称写成带哈希的形式"。
二、转写测试:如何验证这条规则
仓库中的unison-src/transcripts/目录存放 Unison 的转写测试:每个.md文件包含一段"指令 + Unison 代码"的交互式脚本,由转写框架执行后生成对应的.output.md期望输出文件。
no-hash-in-term-declaration.md的完整内容为:
# No Hashes in Term Declarations There should not be hashes in the names used in term declarations, either in the type signature or the type definition. ``` unison :hide-all :error x##Nat : Int -> Int -> Boolean x##Nat = 5 ```这里的关键是指令标记:hide-all :error:
:hide-all表示该代码块中的所有输出(包括错误信息)都不写入转写输出文件;:error则表明该代码块预期以"出错"的方式结束——即这段代码应当解析失败。
因此对应的期望输出文件 no-hash-in-term-declaration.output.md 只保留了标题与规则说明两行,没有任何代码或错误文本:这正是"代码因含哈希声明名而失败、且错误被隐藏"的预期结果。如果未来某天 Unison 改变了该语法规则(例如允许哈希声明名),这段转写测试的.output.md将不再匹配实际输出,测试即可立刻暴露行为变化。
三、源码实现:解析器为何拒绝哈希
要理解x##Nat = 5与x##Nat : Int -> Int -> Boolean为何失败,需要沿 Unison 的"词法 → 语法"两层实现追查。
3.1 词法层:x##Nat可以被正常切分为哈希限定名
首先需要说明:x##Nat这样的写法在词法层面是合法的。在unison-syntax/src/Unison/Syntax/Lexer/Unison.hs中,identifierP解析一个标识符后,会通过P.optional shortHashP尝试读取可选的短哈希部分:
identifierP :: (Monad m) => P.ParsecT (Token Err) String m (HQ'.HashQualified Name) identifierP = do P.label "identifier (ex: abba1, snake_case, .foo.bar#xyz, .foo.++#xyz, or 🌻)" do name <- PI.withParsecT (fmap nameSegmentParseErrToErr) Name.nameP P.optional shortHashP <&> \case Nothing -> HQ'.fromName name Just shorthash -> HQ'.HashQualified name shorthash即:若标识符后面跟了#...短哈希,就构造HQ'.HashQualified name shorthash;否则是纯名称HQ'.fromName name。所以x##Nat会被词法器产出一个携带 hash 的HashQualified Name词元(lexeme),对应文档注释中的合法示例.foo.bar#xyz、.foo.++#xyz。
3.2 语法层:prefixTermName明确拒绝带哈希的名称
问题出在语法解析层。在unison-syntax/src/Unison/Syntax/Parser.hs中,定义了两个行为不同的前缀标识符解析器:
-- | Parse a prefix identifier e.g. Foo or (+), discarding any hash prefixDefinitionName :: (Var v) => P v m (L.Token v) prefixDefinitionName = wordyDefinitionName <|> parenthesize symbolyDefinitionName -- | Parse a prefix identifier e.g. Foo or (+), rejecting any hash -- This is useful for term declarations, where type signatures and term names should not have hashes. prefixTermName :: (Var v) => P v m (L.Token v) prefixTermName = wordyTermName <|> parenthesize symbolyTermName where wordyTermName = queryToken \case L.WordyId (HQ'.NameOnly n) -> Just $ Name.toVar n _ -> Nothing symbolyTermName = queryToken \case L.SymbolyId (HQ'.NameOnly n) -> Just $ Name.toVar n _ -> Nothing源码注释直接点明了设计意图:"This is useful for term declarations, where type signatures and term names should not have hashes"(该解析器用于 term 声明,其中类型签名与 term 名称不应含有哈希)。其实现核心是模式匹配:
- 只有词元是
HQ'.NameOnly n(即未携带哈希的纯名称)时才接受; - 一旦词元是
HQ'.HashQualified ...(携带哈希),两个分支都会落入_ -> Nothing,解析即失败。
与之形成对照的是prefixDefinitionName/wordyDefinitionName,后者通过Name.toVar (HQ'.toName n)丢弃哈希而只取名称,因此foo#abc在类型/构造器定义等位置是允许出现的:
-- | Parse a wordy identifier e.g. Foo, discarding any hash wordyDefinitionName :: (Var v) => P v m (L.Token v) wordyDefinitionName = queryToken \case L.WordyId n -> Just $ Name.toVar (HQ'.toName n) _ -> Nothing3.3 两个声明位置的实际调用点
prefixTermName在parser-typechecker/src/Unison/Syntax/TermParser.hs中被用于 term 声明的两类关键位置:
(1)类型签名(typedecl)——x : T形式的声明头部,对应文档中x##Nat : Int -> Int -> Boolean的失败场景:
typedecl :: (Monad m, Var v) => P v m (L.Token v, Type v Ann) typedecl = (,) <$> P.try (prefixTermName <* reserved ":") <*> TypeParser.valueType <* semiprefixTermName <* reserved ":"意味着:签名左侧的名称必须是纯名称,其后紧跟冒号。x##Nat因携带哈希而无法通过prefixTermName,该声明解析失败。
(2)函数定义左部(definition head)——对应文档中x##Nat = 5的失败场景。在 term 定义解析时,左部(lhs)由中缀形式与前缀形式组成:
let infixLhs = do (arg1, op) <- P.try $ (,) <$> prefixDefinitionName <*> symbolyDefinitionName arg2 <- prefixDefinitionName pure (ann arg1, op, [arg1, arg2]) let prefixLhs = do v <- prefixTermName vs <- many prefixTermName pure (ann v, v, vs) let lhs :: P v m (Ann, L.Token v, [L.Token v]) lhs = infixLhs <|> prefixLhs注意prefixLhs中绑定名(v)与全部函数参数(vs)都使用prefixTermName解析;infixLhs中用于中缀操作符两侧操作数的prefixDefinitionName虽然会丢弃哈希,但中缀形式要求第一个 token 为符号型标识符(symbolyDefinitionName),与x##Nat的词形也不匹配。因此x##Nat = 5中定义头x##Nat无法作为合法的绑定名被接受。
四、设计动机:为什么声明名要禁止哈希
从源码注释与周边设计可以推断,这条约束服务于两个目的:
- 声明的确定性:一个 term 声明是在"引入一个新名称",而哈希限定名表示"指向某个已存在、特定 hash 的实体"。允许在声明位置写
x##Nat会造成语义混淆——究竟是定义新名称x,还是断言x对应某个既有 hash? - 与声明名称的其他约束一致:声明名还有额外的合法性要求。例如
parser-typechecker/src/Unison/Syntax/TermParser.hs中的verifyRelativeName'会拒绝以.开头的绝对名称(DisallowedAbsoluteName);声明名称必须是相对名称且无哈希,保证绑定名始终是"干净、可被命名空间解析的名称"。
五、给使用者的实践要点
- 在
.u源文件中声明 term 时,类型签名x : T与函数定义x = ...中的x必须是纯名称(可含命名空间段,如foo.bar),不要加#hash后缀。 - 若确实需要引用某个特定 hash 的既有定义(例如跨库重名消歧),请把哈希限定名用在引用位置(表达式中),而不是声明位置。
- 符号型标识符(如
(+))同样受此约束:(+)##a : ...这类写法会被symbolyTermName的HQ'.NameOnly匹配拒绝。 - 想要回归验证该行为,可直接运行仓库的转写测试:
no-hash-in-term-declaration.md使用:hide-all :error断言"带哈希的 term 声明必然解析失败",其期望输出保存在 no-hash-in-term-declaration.output.md,与unison-src/transcripts/目录下的其他转写用例共同构成对 Unison 语法规则的自动化守护。
六、小结
"Term 声明名称禁止携带哈希"是 Unison 语法设计中的一条小而关键的铁律:它由词法器(identifierP支持产出哈希限定词元)与语法器(prefixTermName只接受NameOnly)的配合实现,覆盖类型签名(typedecl)与函数定义头(prefixLhs/infixLhs)两类位置,并有对应的转写测试持续验证。对使用者而言,记住一句话即可:声明要干净,引用可带哈希。
- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
相关推荐
Unison 语言中的哈希与 HMAC 内置函数:crypto 模块 API 详解与测试实践
Unison 语言中的哈希与 HMAC 内置函数:crypto 模块 API 详解与测试实践 导读 Unison 是一门「来自未来」的编程语言,其标准库在 cr
编程语言编译器语言运行时开发工具Unison 字节字面量下划线分隔符完全指南:语法规则、词法实现与测试验证
Unison 字节字面量下划线分隔符完全指南:语法规则、词法实现与测试验证 字节字面量(bytes literal)是 Unison 语言中表示 Bytes 类
编程语言编译器语言运行时开发工具Action! 语言 ANTLR4 语法实现解析:从语法规则、类型系统到示例验证
Action! 语言 ANTLR4 语法实现解析:从语法规则、类型系统到示例验证 导读 本文围绕 action/ https://link.gitcode.co
编程语言编译器开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考