ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

Scala 词法语法完全指南:字符分类、标识符、字面量与换行规则 编程语言编译器标准库语言运行时【免费下载链接】scalaScala 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至9Nd括号( ) [ ] { }—分隔符 . ; ,—运算符字符除上述集合外的全部可打印 ASCII 字符\u0020–\u007E、数学符号Sm、其他符号SoSm、So这份分类在编译器中有直接实现。Chars.scala 中的isIdentifierStart/isIdentifierPart基于Character.isUnicodeIdentifierStart/Part判定字母类字符isOperatorPart显式枚举~ ! # % ^ * - ? : | / \等 ASCII 运算符字符并叠加isSpecialUnicodeMATH_SYMBOL与OTHER_SYMBOLisScalaLetter则对应规范中的字母类。词法分析器 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.isBiDiCharacterChars.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_bobdef被分解为三个标识符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.scalaisIdentifierStart与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 tokencatch 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后缀的浮点字面量为FloatIEEE 754 32 位单精度否则为DoubleIEEE 754 64 位双精度。若浮点字面量后紧跟一个以字母开头的 token两者之间必须有至少一个空白字符。注意1.toString会解析为三个 token整数1、.、标识符toString而1.不是合法浮点字面量.后缺少必需的数字。0.0 1e30f 3.14159f 1.0e-100 .1布尔字面量booleanLiteral :: true | falsetrue与false均为Boolean类型的成员。字符字面量characterLiteral :: (charNoQuoteOrNewline | escapeSeq) 字符字面量是单引号内的单个字符可以是除单引号定界符、\u000ALF、\u000DCR之外的任意 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 StringinterpolatedString :: 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实现其中InvalidUnicodeEscapeExceptionStringContext.scala用于校验字符串中 Unicode 转义的合法性。转义序列Escape Sequences字符与字符串字面量中识别下列字符转义序列字符转义Unicode名称字符\b\u0008退格backspaceBS\t\u0009水平制表符HT\n\u000a换行linefeedLF\f\u000c换页form feedFF\r\u000d回车CR\s\u0020空格\\u0022双引号\\u0027单引号\\\u005c反斜杠\此外字符与字符串字面量还识别\uxxxx形式的 Unicode 转义每个x为十六进制数字。若字符或字符串字面量中的反斜杠不能构成合法转义序列则为编译错误。对应实现见 Scanners.scala 的uEscape与非法转义、八进制转义octal escape literals are unsupported的报错逻辑。符号字面量Symbol LiteralssymbolLiteral :: plainid符号字面量x是表达式scala.Symbol(x)的弃用简写。Symbol伴生对象的apply方法会缓存符号的弱引用从而保证相同符号字面量在引用相等reference equality意义上等价。空白与注释Whitespace and Commentstoken 之间可由空白字符和/或注释分隔。注释有两种形式单行注释以//开始延伸到行尾。多行注释介于/*与*/之间。多行注释可以嵌套但必须正确嵌套因此/* /* */会因注释未闭合而被拒绝。扫描器中与此对应的逻辑位于 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 titleThe 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赞分享编程语言编译器标准库语言运行时【免费下载链接】scalaScala 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点击查看免费下载相关推荐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 资源提取 与开发工具逆向工程游戏开发上一篇OpenProject 工作包工作流配置详解基于矩阵的状态迁移权限控制与批量复制下一篇如何在5分钟内用Turbo Boost Switcher控制Mac过热完整温度管理指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表