news 2026/9/22 11:15:23

Scala 词法语法完全指南:字符分类、标识符、字面量与换行规则

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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

项目地址:https://gitcode.com/gh_mirrors/sc/scala
点击查看免费下载

本指南以 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)、其他符号(SoSmSo

这份分类在编译器中有直接实现。Chars.scala 中的isIdentifierStart/isIdentifierPart基于Character.isUnicodeIdentifierStart/Part判定字母类字符,isOperatorPart显式枚举~ ! @ # % ^ * + - < > ? : = & | / \等 ASCII 运算符字符并叠加isSpecial(UnicodeMATH_SYMBOLOTHER_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 insteadChars.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'

三种构成形式

  1. 字母开头的标识符:以一个字母开头,后接任意字母与数字序列;其后可跟随一个或多个下划线_,再接一段由字母数字或运算符字符组成的字符串。例如big_bobp2pempty_?dot_product_*_MAX_LEN_
  2. 运算符开头的标识符:以一个运算符字符开头,后接任意运算符字符序列,如+++=。以上两种统称 plain(普通)标识符。
  3. 反引号包裹的标识符:任意字符串(反引号除外)置于反引号内,如`yield``://`。宿主系统可能对合法标识符施加额外限制。

规范特别指出:虽然/是运算符字符,但///*会开启注释,因此要用作标识符时必须以反引号包裹,例如:

def `://`(s: String): URI def `*/*`(d: Double): Double

最长匹配规则

与多数语言一致,切词遵循最长匹配(longest match)规则。例如字符串

big_bob++=`def`

被分解为三个标识符:big_bob++=defdef虽为保留字,但此处指反引号外的裸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:isIdentifierStartisIdentifierPart均允许$参与标识符。

保留字(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

  1. 换行之前紧邻的 token能够终止一条语句;
  2. 换行之后紧邻的 token能够开始一条语句;
  3. 该 token 位于启用换行的区域。

能够终止语句的 token:字面量、标识符,以及下列分隔符与保留字:

this null true false return type <xml-start> _ ) ] }

能够开始语句的 token:除下列分隔符与保留字之外的所有 Scala token:

catch else extends finally forSome match with yield , . ; : = => <- <: <% >: # [ ) ] }

casetoken 只有后接classobject时才可开始一条语句。

换行启用与禁用的区域

  • 启用换行:整个 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)为ByteShortChar,且数值落在对应范围内,则字面量自动转换并采用该类型。范围如下:
类型范围
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'

truefalse均为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 定义了内置的srawf三个插值器(在 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

项目地址:https://gitcode.com/gh_mirrors/sc/scala
点击查看免费下载

相关推荐

上一篇:OpenProject 工作包工作流配置详解:基于矩阵的状态迁移权限控制与批量复制
下一篇:如何在5分钟内用Turbo Boost Switcher控制Mac过热:完整温度管理指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 11:15:19

5个致命坑:东城会技术认证避坑指南与最佳实践

5个致命坑:东城会技术认证避坑指南与最佳实践 刚拿到“东城会”技术认证的报名通知,是不是兴奋之余又有点慌?别急,我见过太多新人栽在第一步。很多人以为只要把官方文档里的代码复制粘贴进去就能过,结果一运行全是红字报错,或者跑通了但性能慢得让人想摔键盘。这种“复制来的代码跑不通不知道怎么调”的绝望感,是初…

作者头像 李华
网站建设 2026/9/22 11:14:33

产品网络推广方案保姆级教程:3步搞定部署

产品网络推广方案保姆级教程:3步搞定部署 看着满屏红色的 StackTrace 报错,是不是脑子直接炸了?别慌,很多刚接触这块的兄弟都卡在第一步。今天这篇 保姆级教程 ,我不讲虚的,直接带你把【产品网络推广方案】这套东西跑通。…

作者头像 李华
网站建设 2026/9/22 11:14:15

面试突击:训练什么手写实现,看这份完整示例

面试突击:训练什么手写实现,看这份完整示例 刚拿到 Offer 还没捂热,入职第一周就让你手写一个“训练什么”的底层逻辑?别慌,这题不是考你会背多少框架 API,而是看你能不能把复制来的代码跑通。很多人卡在 loss.backward()…

作者头像 李华
网站建设 2026/9/22 11:14:10

牛俊杰源码解析:3个实战项目教你搞定性能瓶颈

牛俊杰源码解析:3个实战项目教你搞定性能瓶颈 官方文档太长抓不住重点?别慌。我见过太多新手对着几页 API 文档发呆,最后代码写得像天书。今天不聊虚的,直接拆解牛俊杰在几个高并发实战项目里踩过的坑。这些代码片段来自 CSDN…

作者头像 李华
网站建设 2026/9/22 11:14:07

ESP32跨开发板固件适配实战:从引脚映射到硬件配置

上个月我把同一套小智语音固件从一块 ESP32 DevKitC 挪到另一块 ESP32-S3-DevKitC 上&#xff0c;原本想着项目源码是通用的&#xff0c;最多改个引脚定义就能编译烧录。结果呢&#xff1f;开机串口日志里全是警告&#xff0c;I2S 麦克风一点声音都采不到&#xff0c;按键触发错…

作者头像 李华
网站建设 2026/9/22 11:14:00

3个坑让阿尔泰数据采集卡性能优化失效,选型避坑指南

3个坑让阿尔泰数据采集卡性能优化失效,选型避坑指南 刚把C语言指针玩明白,转头面对阿尔泰数据采集卡(Altai DAQ)的驱动层,是不是瞬间懵了?很多人以为学会了底层API调用就能直接上项目,结果一跑就是数据丢包、延迟抖动,甚至系统死锁。 学会语法却不知怎么搭项目…

作者头像 李华