- 编程语言
- 编译器
- 标准库
- 语言运行时
【免费下载链接】scala
Scala 2 compiler and standard library. Scala 2 bugs at https://github.com/scala/bug; Scala 3 at https://github.com/scala/scala3
本指南以 Scala 官方语言规范(spec/01-lexical-syntax.md)第一章「词法语法」为骨架,系统讲解 Scala 源码如何从 Unicode 文本被切分为 token:包括字符分类体系、三种标识符形态、保留字清单、语句结束的换行判定规则、各类字面量的完整语法,以及注释、尾随逗号和 XML 模式的特殊处理。文中同时结合本仓库编译器与标准库实现(Scanners.scala、Chars.scala、StringContext.scala)给出源码级佐证。读完本文,你将能够准确预判任意一段 Scala 代码会被如何切词、为何1.toString是三个 token、为何yield不能直接用作变量名,以及多行字符串与插值字符串的边界行为。
源文件与字符分类
Scala 源码由 Unicode 文本构成。程序文本按照本章描述的方式被切分为 token(词法单元)。为构造 token,字符被划分为以下几类(括号内为 Unicode 通用类别):
| 类别 | 包含字符 | Unicode 类别 |
|---|---|---|
| 空白字符 | \u0020(空格)、\u0009(水平制表符)、\u000D(回车)、\u000A(换行) | — |
| 字母 | 小写字母Ll、大写字母Lu、标题字母Lt、其他字母Lo、修饰字母Lm、字母数字Nl,外加\u0024($)与\u005F(_) | 多类 |
| 数字 | '0'至'9' | Nd |
| 括号 | ( ) [ ] { } | — |
| 分隔符 | ` ' " . ; , | — |
| 运算符字符 | 除上述集合外的全部可打印 ASCII 字符(\u0020–\u007E)、数学符号(Sm)、其他符号(So) | Sm、So |
这份分类在编译器中有直接实现。Chars.scala 中的isIdentifierStart/isIdentifierPart基于Character.isUnicodeIdentifierStart/Part判定字母类字符,isOperatorPart显式枚举~ ! @ # % ^ * + - < > ? : = & | / \等 ASCII 运算符字符并叠加isSpecial(UnicodeMATH_SYMBOL与OTHER_SYMBOL),isScalaLetter则对应规范中的字母类。词法分析器 Scanners.scala 正是通过getIdentRest/getOperatorRest与这些谓词协同完成切词(见下文)。
禁止出现的双向控制字符
规范规定九个 Unicode 双向显式格式化字符被禁止出现在源码文件中:\u202a–\u202e与\u2066–\u2069(含端点)。这些字符在字符串和字符字面量中可用 Unicode 转义(\uXXXX)表示。编译器在 Scanners.scala 中对应报错:found unicode bidirectional character '\uXXXX'; in a string or character literal, use a unicode escape instead;Chars.isBiDiCharacter(Chars.scala)则精确匹配这九个码位。这一限制是为了防止利用双向文本特性制造「视觉与逻辑不一致」的恶意源码(bidi attack)。
标识符(Identifiers)
标识符的 EBNF 语法如下:
op ::= opchar {opchar} varid ::= lower idrest boundvarid ::= varid | '`' varid '`' plainid ::= upper idrest | varid | op id ::= plainid | '`' { charNoBackQuoteOrNewline | escapeSeq } '`' idrest ::= {letter | digit} ['_' op] escapeSeq ::= UnicodeEscape | charEscapeSeq UnicodeEscape ::= '\' 'u' {'u'} hexDigit hexDigit hexDigit hexDigit hexDigit ::= '0' | … | '9' | 'A' | … | 'F' | 'a' | … | 'f'三种构成形式
- 字母开头的标识符:以一个字母开头,后接任意字母与数字序列;其后可跟随一个或多个下划线
_,再接一段由字母数字或运算符字符组成的字符串。例如big_bob、p2p、empty_?、dot_product_*、_MAX_LEN_。 - 运算符开头的标识符:以一个运算符字符开头,后接任意运算符字符序列,如
+、++=。以上两种统称 plain(普通)标识符。 - 反引号包裹的标识符:任意字符串(反引号除外)置于反引号内,如
`yield`、`://`。宿主系统可能对合法标识符施加额外限制。
规范特别指出:虽然/是运算符字符,但//与/*会开启注释,因此要用作标识符时必须以反引号包裹,例如:
def `://`(s: String): URI def `*/*`(d: Double): Double最长匹配规则
与多数语言一致,切词遵循最长匹配(longest match)规则。例如字符串
big_bob++=`def`被分解为三个标识符:big_bob、++=、def(def虽为保留字,但此处指反引号外的裸def不构成标识符的一部分)。读者可以从扫描器实现观察到这一点:getIdentRest 循环吞入字母、数字与$,遇到_转入getIdentOrOperatorRest继续吞并随后的字母数字或运算符片段;getOperatorRest 则连续吞入运算符字符,直到遇到空白或不可作为运算符的字符为止。
变量标识符与常量标识符
模式匹配规则进一步区分变量标识符(以小写字母或_开头)与常量标识符(其他情形)。这里的「小写字母」不仅包含 a–z,还包含 UnicodeLl类别以及具有Other_Lowercase贡献属性的字母;但Nl(字母数字)类别的字符绝不视为小写。变量标识符示例:
x maxIndex p2p empty_? `yield` αρετη _y dot_product_* __system _MAX_LEN_ ªpple ʰelper常量标识符示例:
+ Object $reserved Džul ǂnûm ⅰ_ⅲ Ⅰ_Ⅲ ↁelerious ǃqhàà ʹthatsaletter注意$字符被保留给编译器合成的标识符,用户程序不应定义包含$的标识符。这一约定同样体现在 Chars.scala:isIdentifierStart与isIdentifierPart均允许$参与标识符。
保留字(Reserved Words)
以下名称是保留字,不属于词法标识符id的成员:
abstract case catch class def do else extends false final finally for forSome if implicit import lazy macro match new null object override package private protected return sealed super this throw trait try true type val var while with yield _ : = => <- <: <% >: # @此外,Unicode 运算符\u21D2(⇒,ASCII 等价=>)与\u2190(←,ASCII 等价<-)同样被保留。
访问 Java 保留字的方式:当需要引用恰好是 Scala 保留字的 Java 标识符时,使用反引号包裹。例如Thread.yield()非法(yield是 Scala 保留字),但Thread.`yield`()是合法写法。
换行字符(Newline Characters)与语句结束
Scala 是面向行的语言,语句可由分号或换行终止。换行的判定规则如下:
semi ::= ';' | nl {nl}换行何时被当作 nl token
源码文本中的换行只有在同时满足以下三个条件时才被当作特殊 tokennl:
- 换行之前紧邻的 token能够终止一条语句;
- 换行之后紧邻的 token能够开始一条语句;
- 该 token 位于启用换行的区域。
能够终止语句的 token:字面量、标识符,以及下列分隔符与保留字:
this null true false return type <xml-start> _ ) ] }能够开始语句的 token:除下列分隔符与保留字之外的所有 Scala token:
catch else extends finally forSome match with yield , . ; : = => <- <: <% >: # [ ) ] }casetoken 只有后接class或object时才可开始一条语句。
换行启用与禁用的区域
- 启用换行:整个 Scala 源文件(除嵌套的禁用区域外);以及匹配的
{与}之间的区间(除嵌套的禁用区域外)。 - 禁用换行:匹配的
(与)之间、匹配的[与]之间(均除外层嵌套的启用区域);case与其匹配的=>之间;以及所有 XML 模式区域。
注意:XML 与字符串字面量中{...}转义的括号字符不是 token,因此不会构成启用换行的区域。
连续换行与空行
正常情况下,两个不同行上的相邻非换行 token 之间只会插入一个nltoken,即使中间隔了多行。但如果两个 token 之间隔着至少一个完全空白的行(不含任何可打印字符的行),则会插入两个nltoken。
可选 nl 的位置(不终止语句)
完整的 Scala 语法(见 13-syntax-summary.md)中,某些产生式允许可选的nl(但不允许分号),这意味着这些位置的换行不会终止表达式或语句:
- 条件表达式或 while 循环的条件与下一个表达式之间;
- for 推导式的枚举器与下一个表达式之间;
- 类型定义或声明中初始
type关键字之后。
允许单个换行 token 的位置:
- 在
{之前(若该{是当前语句或表达式的合法延续); - 在 infix 运算符之后(若下一行第一个 token 能开始一个表达式);
- 在参数列表之前;
- 在注解之后。
规范给出了多组「有无额外换行导致语义迥异」的对照示例:
// 合法:换行不终止语句 if (x > 0) x = x - 1 while (x > 0) x = x / 2 for (x <- 1 to 10) println(x) type IntList = List[Int]// 一个换行:对象创建后紧跟一个块 new Iterator[Int] { private var x = 0 def hasNext = true def next = { x += 1; x } } // 空行分隔后:对象创建 + 独立的局部块 new Iterator[Int] { private var x = 0 def hasNext = true def next = { x += 1; x } }// 一个换行:单个表达式 x < 0 || x > 10 // 空行分隔后:两个表达式 x < 0 || x > 10// 合法:换行出现在参数列表前 def func(x: Int) (y: Int) = x + y // 空行分隔后:抽象函数定义 + 非法语句 def func(x: Int) (y: Int) = x + y// 合法:注解与定义之间换行 @serializable protected class Data { ... } // 空行分隔后:注解 + 独立语句(语法非法) @serializable protected class Data { ... }这些示例表明:单换行在语法允许位置被吸收,而空行产生的双nl则一定会终止语句。这也是 Scala 代码风格中「空行即分隔」的底层机制。
字面量(Literals)
字面量涵盖整数、浮点数、字符、布尔值、符号、字符串,其语法与 Java 一致:
Literal ::= ['-'] integerLiteral | ['-'] floatingPointLiteral | booleanLiteral | characterLiteral | stringLiteral | interpolatedString | symbolLiteral | 'null'整数字面量
integerLiteral ::= (decimalNumeral | hexNumeral | binaryNumeral) ['L' | 'l'] decimalNumeral ::= digit {digit} hexNumeral ::= '0' ('x' | 'X') hexDigit {hexDigit} binaryNumeral ::= '0' ('b' | 'B') binaryDigit {binaryDigit}Int覆盖 $-2^{31}$ 到 $2^{31}-1$;Long覆盖 $-2^{63}$ 到 $2^{63}-1$。超出范围的整数字面量是编译错误。- 整数字面量默认为
Int,带L/l后缀则为Long(小写l因可读性原因被弃用)。 - 若字面量的期望类型(pt)为
Byte、Short或Char,且数值落在对应范围内,则字面量自动转换并采用该类型。范围如下:
| 类型 | 范围 |
|---|---|
Byte | $-2^7$ 到 $2^7-1$ |
Short | $-2^{15}$ 到 $2^{15}-1$ |
Char | $0$ 到 $2^{16}-1$ |
- 数字字面量中可用任意多个下划线分隔数字以增强可读性:
0 21_000 0x7F -42L 0xFFFF_FFFF浮点字面量
floatingPointLiteral ::= digit {digit} '.' digit {digit} [exponentPart] [floatType] | '.' digit {digit} [exponentPart] [floatType] | digit {digit} exponentPart [floatType] | digit {digit} [exponentPart] floatType exponentPart ::= ('E' | 'e') ['+' | '-'] digit {digit} floatType ::= 'F' | 'f' | 'D' | 'd'- 带
F/f后缀的浮点字面量为Float(IEEE 754 32 位单精度),否则为Double(IEEE 754 64 位双精度)。 - 若浮点字面量后紧跟一个以字母开头的 token,两者之间必须有至少一个空白字符。
- 注意:
1.toString会解析为三个 token:整数1、.、标识符toString;而1.不是合法浮点字面量(.后缺少必需的数字)。
0.0 1e30f 3.14159f 1.0e-100 .1布尔字面量
booleanLiteral ::= 'true' | 'false'true与false均为Boolean类型的成员。
字符字面量
characterLiteral ::= ''' (charNoQuoteOrNewline | escapeSeq) '''字符字面量是单引号内的单个字符,可以是除单引号定界符、\u000A(LF)、\u000D(CR)之外的任意 Unicode 字符,或由转义序列表示的任意 Unicode 字符:
'a' '\u0041' '\n' '\t'字符串字面量
stringLiteral ::= '"' {stringElement} '"' stringElement ::= charNoDoubleQuoteOrNewline | escapeSeq- 字符串字面量是双引号内的字符序列,可为除双引号定界符、LF、CR 之外的任意 Unicode 字符,或由转义序列表示。
- 字符串内含双引号必须转义为
"\""。 - 字符串字面量的值是一个
String实例:
"Hello, world!\n" "\"Hello,\" replied the world."多行字符串字面量
stringLiteral ::= '"""' multiLineChars '"""' multiLineChars ::= {['"'] ['"'] charNoDoubleQuote} {'"'}- 多行字符串以三引号
""" ... """包裹,除结尾外不得出现三个及以上连续引号字符。 - 字符不必可打印,换行与控制字符均允许。
- 转义序列不处理,但 Unicode 转义仍被处理(自 2.13.2 起这一行为被弃用)。编译器侧 Scanners.scala 的
replaceUnicodeEscapesInTriple会对三引号字符串中的 Unicode 转义发出弃用警告(提示 Scala 3 或-Xsource-features:unicode-escapes-raw下将忽略这些转义)。
"""the present string spans three lines."""结果为:
the present string spans three lines.标准库的stripMargin(定义于scala.collection.StringOps)可去除多行字符串的前导空白:
"""the present string |spans three |lines.""".stripMargin求值结果为:
the present string spans three lines.插值字符串(Interpolated String)
interpolatedString ::= alphaid '"' {['\'] interpolatedStringPart | '\\' | '\"'} '"' | alphaid '"""' {['"'] ['"'] char \ ('"' | '$') | escape} {'"'} '"""' interpolatedStringPart ::= printableChar \ ('"' | '$' | '\') | escape escape ::= '$$' | '$"' | '$' alphaid | '$' BlockExpr alphaid ::= upper idrest | varid- 插值字符串由「以字母开头的标识符 + 紧随其后的字符串字面量」组成,两者之间不得有空白或注释。字符串字面量可为普通单引号形式或三引号多行形式。
- 插值字符串内不解释通常的转义字符(普通与多行形式皆然);
\"不会关闭普通字符串字面量。 $转义有三种形式:- 最通用形式
$后跟{与}包裹的表达式${expr}。其中的表达式属于BlockExpr语法类别,可含多条语句,换行有意义。单独的$不允许出现;要得到字面$需写$$;要得到字面"需写\$"。 - 简单形式
$后跟一个以字母开头、仅含字母数字与下划线的标识符($id),等价于${id}。
- 最通用形式
- 展开后的表达式正常做类型检查;
StringContext通常解析为 scala 包中的默认实现,也可由用户自定义,还可通过内置scala.StringContext的隐式转换添加新的插值器。例如:
implicit class StringInterpolation(s: StringContext) { def id(args: Any*) = ??? }标准库中的 StringContext.scala 定义了内置的s、raw、f三个插值器(在 2.13 中均通过宏 fast-track 到scala.tools.reflect.FastStringInterpolator实现),其中InvalidUnicodeEscapeException(StringContext.scala)用于校验字符串中 Unicode 转义的合法性。
转义序列(Escape Sequences)
字符与字符串字面量中识别下列字符转义序列:
| 字符转义 | Unicode | 名称 | 字符 |
|---|---|---|---|
\b | \u0008 | 退格(backspace) | BS |
\t | \u0009 | 水平制表符 | HT |
\n | \u000a | 换行(linefeed) | LF |
\f | \u000c | 换页(form feed) | FF |
\r | \u000d | 回车 | CR |
\s | \u0020 | 空格 | |
\" | \u0022 | 双引号 | " |
\' | \u0027 | 单引号 | ' |
\\ | \u005c | 反斜杠 | \ |
此外,字符与字符串字面量还识别\uxxxx形式的 Unicode 转义(每个x为十六进制数字)。若字符或字符串字面量中的反斜杠不能构成合法转义序列,则为编译错误。对应实现见 Scanners.scala 的uEscape与非法转义、八进制转义(octal escape literals are unsupported)的报错逻辑。
符号字面量(Symbol Literals)
symbolLiteral ::= ''' plainid符号字面量'x是表达式scala.Symbol("x")的弃用简写。Symbol伴生对象的apply方法会缓存符号的弱引用,从而保证相同符号字面量在引用相等(reference equality)意义上等价。
空白与注释(Whitespace and Comments)
token 之间可由空白字符和/或注释分隔。注释有两种形式:
- 单行注释:以
//开始,延伸到行尾。 - 多行注释:介于
/*与*/之间。多行注释可以嵌套,但必须正确嵌套;因此/* /* */会因注释未闭合而被拒绝。
扫描器中与此对应的逻辑位于 getOperatorRest:运算符字符'/'后若紧跟*或/,则转入skipComment跳过注释;否则'/'仍作为普通运算符字符参与标识符切分。
多行表达式中的尾随逗号(Trailing Commas)
若一个逗号,后(忽略空白)紧跟换行与右括号)、右方括号]或右花括号},则该逗号被视为「尾随逗号」并被忽略。例如:
foo( 23, "bar", true, )这一特性极大方便了多行参数列表、元组、集合字面量的增删与版本管理。
XML 模式(XML Mode)
为允许在源码中直接嵌入 XML 片段,词法分析会在下列情形从 Scala 模式切换到 XML 模式:遇到<且该<前是空白、左括号(或左花括号{,并且紧随其后是一个能开启 XML 名称的字符:
( whitespace | '(' | '{' ) '<' (XNameStart | '!' | '?') XNameStart ::= '_' | BaseChar | Ideographic // 同 W3C XML,但不含 ':'扫描器在以下两种情况下切回 Scala 模式:
- 由初始
<开始的 XML 表达式或 XML 模式被成功解析;或 - 解析器遇到内嵌的 Scala 表达式或模式,强制扫描器回到普通模式,直至该 Scala 表达式或模式解析完成。由于代码与 XML 片段可以互相嵌套,解析器需要维护一个栈来准确反映 XML 与 Scala 的嵌套层次。
在 XML 模式下不构造任何 Scala token,注释也被解释为文本。规范示例——一个含两处内嵌 Scala 表达式的 XML 字面量:
val b = <book> <title>The Scala Language Specification</title> <version>{scalaBook.version}</version> <authors>{scalaBook.authors.mkList("", ", ", "")}</authors> </book>延伸阅读
- 完整 Scala 语法汇总见 spec/13-syntax-summary.md
- 条件表达式与 while 循环的换行规则见 spec/06-expressions.md
- 类型定义与类型别名(
type关键字后的换行)见 spec/04-basic-declarations-and-definitions.md - 注解相关语法见 spec/11-annotations.md
- 编译器词法分析实现见 Scanners.scala,字符分类工具见 Chars.scala,字符串插值器实现见 StringContext.scala
- 编程语言
- 编译器
- 标准库
- 语言运行时
【免费下载链接】scala
Scala 2 compiler and standard library. Scala 2 bugs at https://github.com/scala/bug; Scala 3 at https://github.com/scala/scala3
相关推荐
Carbon 语言词法单元 "Word" 完全指南:关键字、标识符、类型字面量与 Raw 标识符
Carbon 语言词法单元 "Word" 完全指南:关键字、标识符、类型字面量与 Raw 标识符 本文基于 docs/design/lexical_conven
编程语言编译器标准库Swift 词法结构(Lexical Structure)完全解析:从标记、标识符到字面量与运算符的底层语法规则
Swift 词法结构(Lexical Structure)完全解析:从标记、标识符到字面量与运算符的底层语法规则 本篇技术指南以《The Swift Progr
文档教程AssetRipper 实战教程:Unity 资源提取与资产导出的最短路径
AssetRipper 实战教程:Unity 资源提取与资产导出的最短路径 AssetRipper 是一个开源的 GUI 工具,专门做 Unity 资源提取 与
开发工具逆向工程游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考