☰
用 ANTLR v4 解析 Scala 3:grammars-v4 中 Scala3 语法的设计、覆盖率与已知限制
2026/9/25 3:31:47 网站建设 项目流程
  • 编程语言
  • 编译器
  • 开发工具

【免费下载链接】grammars-v4

Grammars written for ANTLR v4; expectation that the grammars are free of actions.

项目地址:https://gitcode.com/gh_mirrors/gr/grammars-v4
点击查看免费下载

本文面向需要为 Scala 3 构建词法/语法分析工具的开发者,完整介绍 grammars-v4 仓库scala/scala3/目录下这套无 action(free of actions)的 Scala 3 ANTLR v4 语法:包括它的 EBNF 来源与对齐策略、--3.0-migration兼容选项、对 Scala 3 显著缩进(indentation-sensitive)语法的 INDENT/DEDENT 注入原理、基于 Trash Toolkittrcover的规则覆盖率(98.3%)以及 13 个不可达调用点的成因,并逐条给出文档承认的已知语法限制。读完本文,你将能够理解这套语法的内部设计取舍,知道如何用它做覆盖率分析、如何开启 Scala 2 兼容模式,以及哪些输入它会有意地"过宽"接受。

一、设计基线:EBNF 来源与 Dotty 对齐

1.1 为什么选用 docs 版 EBNF

该语法文件的直接依据是 Scala 3 官方文档站(docs.scala-lang.org/scala3/reference/syntax.html)的语法摘要。作者在 readme.md 中明确记录了选择的理由:语言规范 3.4 版本的13-syntax-summary页面存在明显缺漏(例如缺少Import相关规则),因此放弃了规范版,改用 docs 版本作为蓝本。

1.2 不盲从 EBNF,对齐 Dotty 编译器

更重要的是,文档指出 docs 版 EBNF 在换行(newlines)、分号(semicolons)和语句切分(statements)上存在若干问题。因此实现的指导原则是"尽量镜像 Dotty 编译器(Scala 3 官方编译器)的实际行为,而非盲从人工誊录的 EBNF"。这一点在源码中有直接印证:

  • Scala3LexerBase.cs 与 Scala3LexerBase.java 的注释明确写着Modelled on Dotty (Scala 3 compiler) Scanners.scala handleNewLine logic,其换行处理逻辑直接移植自 Dotty 扫描器的handleNewLine;
  • 两组 Base 类中的 token 集合谓词(CanEndStat/CanStartStat/IsStatContinuation/CanStartIndent)也在注释中标注为mirroring Dotty Tokens.scala;
  • 语法规则中多处保留了"移除某替代以避免与另一规则歧义"的设计注释,例如 Scala3Parser.g4 中block规则明确记录了曾删除| USCORE ...与| id ...两个替代的理由——它们已被blockResult的funParams完全包含,会制造真正歧义并触发深层 ALL(*) 前瞻。

从源码结构看,这套语法是一个"以官方 EBNF 为骨架、以编译器实现为行为基准"的参考实现,而非对规范文本的机械转写。

二、仓库中的模块构成

2.1 语法文件与目标语言

scala/scala3/目录包含:

文件作用
Scala3Lexer.g4词法规则,约 585 行,含关键字、字面量、运算符、标识符与 Unicode 分类 fragment
Scala3Parser.g4语法规则,约 948 行,覆盖 compilationUnit 到各类定义与表达式
CSharp/Scala3LexerBase.csC# 词法基类:INDENT/DEDENT 注入、换行判定
CSharp/Scala3ParserBase.csC# 语法基类:migration30()谓词
Java/Scala3LexerBase.javaJava 词法基类(与 C# 版逻辑一致)
Java/Scala3ParserBase.javaJava 语法基类
pom.xmlMaven 构建:ANTLR4 插件生成 + 自动测试插件
desc.xml仓库级描述:目标语言与测试输入

desc.xml 声明了该语法的目标语言为CSharp;Java,并配置了两组测试输入:examples/*.scala与examples/lila/app/*.scala。

2.2 Maven 构建与自动测试

pom.xml 中的配置给出了现成的构建/验证方式:

  • antlr4-maven-plugin从Scala3Lexer.g4与Scala3Parser.g4生成解析器,并开启visitor与listener两种访问模式;
  • antlr4test-maven-plugin以compilationUnit为入口规则,grammarName为Scala3,exampleFiles指向examples/,测试文件扩展名为.scala。

也就是说,在仓库根目录执行 Maven 即可同时完成代码生成与示例驱动的回归测试,无需额外编写测试代码。

2.3 示例语料

examples/下除 41 个按特性编号的示例(如 05_enum.scala、19_inline.scala、41_indentation_syntax.scala)之外,还包含两类"真实世界"语料:

  • examples/lila/:从目录结构看,这是 lila(lichess 开源国际象棋平台)的 Scala 源码树,包含app/controllers/、app/views/等大量真实业务代码(如 LilaController.scala),用于验证语法在大规模真实代码上的健壮性;
  • 2246.scala2:一个以.scala2为后缀的 Apache Spark 源码文件(头部为 Apache License,包名org.apache.spark.storage.memory),专门用于检验--3.0-migration模式对 Scala 2 风格代码的兼容能力。

desc.xml的测试通配符examples/*.scala不含.scala2文件,说明该文件更多是供手动以迁移模式验证用的对照语料。

三、命令行选项:--3.0-migration

这是本语法唯一对外开放的命令行选项,用于在语法层面模拟 Scala 3 编译器在-source:3.0-migration下接受的 Scala 2 兼容语法。

OptionDescription
--3.0-migration启用 Scala 3 编译器在-source:3.0-migration下接受的 Scala 2 兼容语法。当前启用两类构造:._通配导入选择器(如import scala.jdk.CollectionConverters._)与_通配类型实参(如Seq[_])。未开启该标志时上述构造会被拒绝;Scala 3 的等价写法分别是.*与?

3.1 源码级的标志传递

该选项在两个语言实现中通过 Base 类读取:

  • C#:Scala3LexerBase.cs 用静态只读属性Migration30扫描Environment.GetCommandLineArgs(),只要出现忽略大小写匹配的--3.0-migration即置位;
  • Java:Scala3LexerBase.java 通过读取系统属性sun.java.command并切分参数来判断;
  • 两者各自配套的Scala3ParserBase(C# 版、Java 版)都暴露migration30()方法,供 grammar 内的语义谓词(semantic predicate)调用。

3.2 谓词在语法中的两个挂载点

在 Scala3Parser.g4 中,{migration30()}?守卫出现在两处:

  1. wildCardSelector:| {migration30()}? USCORE—— 允许_作为 Scala 2 风格的导入通配符;
  2. 类型参数位置(L207):| {migration30()}? USCORE typeBounds?—— 允许_作为通配类型实参。

这解释了文档表格中的行为:不加该标志时,import foo.bar._与Seq[_]都会被拒绝;Scala 3 原生写法分别为import foo.bar.*(由wildCardSelector : Op中代表*的分支覆盖)与Seq[?]。

四、缩进敏感语法的 INDENT/DEDENT 注入原理

Scala 3 支持"显著缩进"(significant indentation),即用缩进而非花括号界定代码块。ANTLR v4 本身不识别缩进,因此这套语法把最复杂的部分放进了词法基类。

4.1 词法侧的准备

在 Scala3Lexer.g4 中:

  • NEWLINE被定义为真实 token(L238-L240),而不是默认的skip或 hidden,这样基类才能看到换行并测量下一行缩进;
  • WS被发送到隐藏通道(L244-L246),供基类读取行首空白、计算缩进长度,同时不干扰解析;
  • 语法头部声明了合成 tokenINDENT, DEDENT(L16),它们没有对应词法规则,完全由Scala3LexerBase注入。

解析器侧则由end_of_stat : NEWLINE+ | SEMI(Scala3Parser.g4 L31)统一处理"语句分隔"——无论分隔符是保留在默认通道的 NEWLINE 还是真实分号。

4.2 Region 模型

Scala3LexerBase.cs(Java 版 Scala3LexerBase.java 逻辑相同)用栈维护四种"区域":

Region语义换行处理
TopLevel最外层作用域缩进不变且满足语句边界条件时,NEWLINE 留在默认通道作为分隔符
Indented缩进块内(INDENT … DEDENT 之间)同上,按缩进增减决定 INDENT/DEDENT
InBraces{…}内NEWLINE 在语句边界暴露为分隔符;不产生 INDENT/DEDENT
InParens(…)或[…]内换行一律抑制(隐式行连接)

NEWLINE 的处理完全复刻 Dotty 语义:缩进增加且前序 token 允许开启块时,抑制 NEWLINE 并注入 INDENT;缩进不变时按CanEndStat(上一个非隐藏 token) && CanStartStat(下一行首 token) && !IsStatContinuation(下一行首 token)判断是否保留 NEWLINE;缩进减少时抑制 NEWLINE、逐个弹出 DEDENT,若外层上下文仍需要分隔符则再补发一个 NEWLINE 副本。

4.3 四个 token 集合谓词

这四个静态谓词直接镜像 DottyTokens.scala中的同名集合,是"NEWLINE 该不该出现"的判定核心:

  • CanEndStat:上一个 token 能否结束一条语句。包含各类字面量、标识符(Id/Varid/BacktickId)、运算符Op、USCORE/THIS/SUPER/RETURN、TYPE/GIVEN、右括号类 token、DEDENT/NEWLINE,以及 end-marker 关键字(IF/WHILE/FOR/MATCH/TRY/VAL/NEW/EXTENSION,因为end if、end while等语句以这些关键字 token 结尾);
  • CanStartStat:下一行首 token 能否开启新语句。在标识符/字面量/左括号基础上,还包括NEW/THROW、各种控制流关键字、修饰符关键字(ABSTRACT/FINAL/PRIVATE/PROTECTED/OVERRIDE/SEALED)、定义关键字(CLASS/TRAIT/OBJECT/ENUM/DEF/VAL/VAR/TYPE/GIVEN/IMPORT/EXPORT/PACKAGE/INLINE/LAZY/IMPLICIT/EXTENSION),以及上下文关键字(OPEN/INFIX/TRANSPARENT/OPAQUE/AS/DERIVES/USING)——后者可作普通标识符使用,因此也能开启语句;
  • IsStatContinuation:下一行首 token 属于语句延续集合(THEN/ELSE/DO/CATCH/FINALLY/YIELD/MATCH),即使CanEndStat成立也抑制 NEWLINE——例如if cond then\n expr中then后的换行不应被当作语句分隔;
  • CanStartIndent:前一个 token 允许开启新的缩进块(THEN/ELSE/DO/CATCH/FINALLY/YIELD/MATCH/COLON/WITH/ASSIGN/ARROW/CTXARROW/LARROW/WHILE/TRY/FOR/IF/THROW/RETURN)。

4.4 若干工程化的边界处理

Base 类里还处理了若干容易出错的边界情形,值得注意:

  • 缩进计算:空格每 1 列、制表符对齐到 8 列((length / 8 + 1) * 8)、\f归零——即 tab 按"制表位"而非固定宽度计算;
  • .续行:下一行以DOT开头时视为方法链续行,抑制 NEWLINE 与 INDENT,但仍会按缩进变化发出 DEDENT;
  • RPAREN开启缩进:extension (params)这类以)结尾、后接缩进方法体的构造,允许RPAREN触发 INDENT,但限定在外层是Indented/TopLevel且下一行不以extends/with开头时(后者是类模板续行而非新块);
  • COMMA/RPAREN时的 DEDENT 排空:同一行内f: u => expr,这类"冒号实参在括号内开块却未遇到换行"的场景,会在逗号/右括号之前主动弹出Indented区域并补发 DEDENT,保证括号闭合法则正常;
  • 空行与注释行:整行空白或仅注释的行全部抑制,不影响缩进判定;
  • EOF:文件结束时为所有未闭合的Indented区域补发 DEDENT,再输出 EOF。

这些细节使得for推导式、match表达式、given ... with等"既可用花括号又可用缩进"的构造在两种书写方式下都能被正确处理。

五、用 trcover 度量规则覆盖率

5.1 覆盖率的含义

文档说明,这套语法使用 Trash Toolkit 的trcover工具做覆盖率验证:它会对 ANTLR4 语法做插桩,统计examples/中的示例输入实际走到了哪些"规则调用点"(rule call sites,即一条语法规则引用另一条规则的每个位置)。覆盖数字就是解析过程中被触达的调用点数量。

5.2 重新生成覆盖报告

在向examples/添加或修改示例后,可执行:

cd Generated-CSharp dotnet trash cover ../examples/*.scala

说明:Generated-CSharp是 ANTLR 插件生成 C# 解析器后的输出目录;该命令会生成cover.html——一份对 grammar 做了高亮标注的副本,被触达的调用点有高亮,未被触达的调用点则没有高亮。

5.3 当前覆盖数据

750 of 763 rule call sites covered(98.3%)。13 个未覆盖调用点分布在 8 条 grammar 替代行上(同一替代行上的多个规则引用各计一个调用点,因此 8 行对应 13 个点)。

六、13 个不可达调用点的成因

文档逐一给出了这 13 个调用点"当前语法与解析器下永久不可达"的结构性原因。下面按当前仓库 Scala3Parser.g4 的行号整理(原文档行号为评估时快照,与当前文件有少量偏移):

Grammar location(当前行号)Reason unreachable
funParamClause/typedFunParam(约 L173-L185,4 个调用点)simpleType_: LPAREN nameAndType RPAREN会先吞掉(x: Int),使得funTypeArgs中根本轮不到funParamClause;ANTLR 总是优先走infixType替代。对应规则见 funTypeArgs、funParamClause
INLINE infixExpr matchClause(L347,2 个调用点)postfixExpr ascription?(L348)排在前面,先把inline当作普通标识符消费;剩下的x match { … }再被解析成独立语句
LPAREN namedExprInParens … RPAREN/namedExprInParens(L395、L426-L428,3 个调用点)LPAREN exprsInParens RPAREN(L394)在simpleExpr中位置更靠前且总是先赢;命名实参f(x = 1)会被exprsInParens经expr1: id ASSIGN expr吸收
变长实参LPAREN … postfixExpr Op RPAREN(L434,2 个调用点)LPAREN exprsInParens RPAREN先匹配;args*被当作exprsInParens内部的 postfix 表达式解析
defSig (COLON type_)?抽象声明(约 L762,2 个调用点)该替代确实会被抽象方法声明执行,但覆盖率工具无法独立追踪它:多个以defSig (COLON type_)?开头的defDef替代共享同一 ATN 前缀,命中被归到第一个替代名下。defDef定义见 L757-L763

一个值得注意的细节:前四类"不可达"意味着这些调用点虽然在语法中存在,但被更早出现的替代"遮蔽",属于 ANTLR 文法结构中常见的自然遮蔽(shadowing),而非示例不足;第五类则恰恰相反——代码路径存在且被执行,只是工具无法单独计数。

七、已知语法限制

以下限制是作者有意为之的简化,目的是让语法保持自包含、易维护,代价是接受比严格 Scala 3 语法更宽泛的一小类输入。

7.1importSelectors:不支持混用命名选择器与通配选择器

importSelectors : namedSelector (COMMA importSelectors)? | wildCardSelector (COMMA wildCardSelector)* ;

合法 Scala 3 允许在一条 import 中混用命名选择器和通配符,例如import foo.{bar, given, *}。上述规则只接受"全部是namedSelector"或"全部是wildCardSelector"两种列表,不接受两者混排。该规则在 Scala3Parser.g4 L106-L109,同时wildCardSelector(L100-L104)支持*、迁移模式下的_,以及given [Type]三种形式。

7.2wildCardSelector、negation、variance用Op匹配单字符运算符

词法器不为*、+、-单独设 token——所有连续运算符字符都被合并为一个Optoken(Scala3Lexer.g4 L215-L217)。因此下面三条规则都用Op代替"仅允许的特定单字符":

RuleIntended operatorAlso accepted (over-broadly)
wildCardSelector : Op(L101)*导入通配符任意运算符序列
negation : Op(L147)数值字面量前的-任意运算符序列
variance : Op(L611)类型参数型变的+或-任意运算符序列

文档指出,若要严格化,就需要新增STAR/MINUS/PLUS等单字符 token,而这会迫使运算符词法在整个语法中碎片化,对一份参考语法而言不值得。每条规则的注释都记录了其意图中的限制(如 negation 的注释标注"must be-")。

八、结合示例验证这些边界

examples/中的文件既是覆盖率语料,也是理解边界行为的活教材:

  • 41_indentation_syntax.scala 全面覆盖冒号+缩进写法:缩进体 class/object/trait、缩进 match、缩进 for/yield、extension (n: Int):冒号形式的扩展方法、缩进 enum、given ... with等;
  • 25_wildcard_given_import.scala 覆盖wildCardSelector的given形式(import scala.math.{given})、namedSelector AS USCORE(ArrayList as _)以及super[Base]类限定符;
  • 40_coverage_gaps.scala 顶部注释明确列出它要触达的目标:blockStat importDecl、usingParamClause构造器、extMethods、funParamClause/typedFunParam(type DepFn = (x: Int) => String走funTypeArgs的第三替代)、ascription(x: String)、givenConditional等——可作为阅读不可达表格时的对照用例(注意其中funParamClause的覆盖尝试与第六节表格"不可达"的判定存在张力,恰好说明同一构造在不同上下文中的可达性差异)。

九、运行与验证方式小结

  1. 生成 + 测试:在仓库根目录执行 Maven(参考 pom.xml),antlr4-maven-plugin生成 Java/C# 解析器与 visitor/listener,antlr4test-maven-plugin自动以compilationUnit为入口跑完examples/下所有.scala文件;
  2. 覆盖率:生成 C# 解析器后,在Generated-CSharp目录执行dotnet trash cover ../examples/*.scala并查看cover.html;
  3. 迁移模式验证:以--3.0-migration参数启动解析程序,可用 2246.scala2 这类.scala2语料人工验证 Scala 2 兼容语法;
  4. 手动试跑:仓库 grun.sh 与_scripts/antlr4-tools/提供通用的 ANTLR 工具链,可结合Scala3Parser.g4的compilationUnit入口对单文件做 tree 输出。

十、结语

scala/scala3/是 grammars-v4 中少数需要同时处理"缩进敏感语法"与"编译器级换行语义"的语法之一。它把最困难的部分(INDENT/DEDENT 注入、语句分隔判定)下沉到词法基类并忠实复刻 Dotty 的handleNewLine逻辑,用语义谓词支撑--3.0-migration迁移模式,再用trcover把规则覆盖钉在 98.3%,最后以文档形式公开承认五处"过宽接受"或"结构不可达"的取舍。对于想为 Scala 3 构建工具链、或想学习"如何为缩进敏感语言编写 ANTLR 语法"的读者,这套语法及其 readme.md 是一份可直接参考的完整样例。

  • 编程语言
  • 编译器
  • 开发工具

【免费下载链接】grammars-v4

Grammars written for ANTLR v4; expectation that the grammars are free of actions.

项目地址:https://gitcode.com/gh_mirrors/gr/grammars-v4
点击查看免费下载
上一篇:BabyAI模仿学习实战:如何用Bot生成演示训练AI智能体
下一篇:如何永久备份微信聊天记录:WeChatMsg完整指南与实战教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询