第 11 章 OCaml 语言

1 词法约定

空格

以下字符被视为空格:空格、水平制表符、回车符、换行符和换页符。空格会被忽略,但它们会分隔相邻的标识符、字面量和关键字,否则这些标识符、字面量或关键字会被误认为一个整体。

注释

注释以两个字符 (*(中间没有空格)开头,并以字符 *)(中间没有空格)结束。注释被视为空格字符。注释不会出现在字符串或字符字面量内部。嵌套注释会被正确处理。

(* 单行注释 *) (* 多行注释,注释掉程序的一部分,并包含一个嵌套注释:let f = function | 'A'..'Z' -> "Uppercase" (* 稍后添加其他情况... *) *)

标识符

ident::= (字母 ∣ _) { 字母 ∣ 09 ∣ _ ∣ ' } 
 
大写标识符::= (AZ) { 字母 ∣ 09 ∣ _ ∣ ' } 
 
小写标识符::=(az ∣ _) { 字母 ∣ 09 ∣ _ ∣ ' } 
 
字母::=AZ ∣ az

标识符是由字母、数字、_(下划线字符)和'(单引号)组成的序列,以字母或下划线开头。字母至少包含 ASCII 集中的 52 个小写和大写字母。当前实现还将 ISO 8859-1 集中的一些字符识别为字母(字符 192–214 和 216–222 作为大写字母;字符 223–246 和 248–255 作为小写字母)。此功能已弃用,应避免使用,以确保将来兼容性。

标识符中的所有字符都有意义。当前实现接受长度最长达 16000000 个字符的标识符。

在许多地方,OCaml 区分以大写字母开头的标识符和以小写字母开头的标识符。为此,下划线字符被视为小写字母。

整数字面量

integer-literal::=[-] (09) { 09 ∣ _ }
  [-] (0x ∣ 0X) (09 ∣ AF ∣ af) { 09 ∣ AF ∣ af ∣ _ }
  [-] (0o ∣ 0O) (07) { 07 ∣ _ }
  [-] (0b ∣ 0B) (01) { 01 ∣ _ }
 
int32-literal::=整数字面量l
 
int64-literal::=整数字面量L
 
nativeint-literal::=整数字面量n

整数字面量是一个或多个数字的序列,前面可以可选地带有一个减号。默认情况下,整数字面量为十进制(基数 10)。以下前缀选择不同的基数

前缀基数
0x0X十六进制(基数 16)
0o0O八进制(基数 8)
0b0B二进制(基数 2)

(初始 0 是数字零;八进制的 O 是字母 O。)整数字面量后面可以跟一个字母 lLn,以表示该整数的类型分别为 int32int64nativeint,而不是整数字面量的默认类型 int。超出可表示整数范围的整数字面量的解释是未定义的。

为方便起见并提高可读性,下划线字符 (_) 在整数字面量中被接受(并被忽略)。

# let house_number = 37 let million = 1_000_000 let copyright = 0x00A9 let counter64bit = ref 0L;;
val house_number : int = 37 val million : int = 1000000 val copyright : int = 169 val counter64bit : int64 ref = {contents = 0L}

浮点数字面量

float-literal::=[-] (09) { 09 ∣ _ } [. { 09 ∣ _ }] [(e ∣ E) [+ ∣ -] (09) { 09 ∣ _ }]
  [-] (0x ∣ 0X) (09 ∣ AF ∣ af) { 09 ∣ AF ∣ af ∣ _ }  [. { 09 ∣ AF ∣ af ∣ _ }] [(p ∣ P) [+ ∣ -] (09) { 09 ∣ _ }]

浮点十进制字面量由整数部分、小数部分和指数部分组成。整数部分是一个或多个数字的序列,前面可以可选地带有一个减号。小数部分是一个小数点,后面跟着零个、一个或多个数字。指数部分是字符 eE,后面跟着一个可选的 +- 符号,后面跟着一个或多个数字。它被解释为 10 的幂。小数部分或指数部分可以省略,但不能同时省略,以避免与整数字面量产生歧义。超出可表示浮点范围的浮点字面量的解释是未定义的。

浮点十六进制字面量用 0x0X 前缀表示。语法类似于浮点十进制字面量,但有以下区别。整数部分和小数部分使用十六进制数字。指数部分以字符 pP 开头。它以十进制编写,并被解释为 2 的幂。

为方便起见并提高可读性,下划线字符 (_) 在浮点字面量中被接受(并被忽略)。

# let pi = 3.141_592_653_589_793_12 let small_negative = -1e-5 let machine_epsilon = 0x1p-52;;
val pi : float = 3.14159265358979312 val small_negative : float = -1e-05 val machine_epsilon : float = 2.22044604925031308e-16

字符字面量

char-literal::= '普通字符'
 '转义序列'
 
转义序列::= \ (\ ∣ " ∣ ' ∣ n ∣ t ∣ b ∣ r ∣ 空格)
 \ (09) (09) (09)
 \x (09 ∣ AF ∣ af) (09 ∣ AF ∣ af)
 \o (03) (07) (07)

字符字面量由 '(单引号)字符分隔。这两个单引号包含一个与 '\ 不同的字符,或以下转义序列之一

序列表示的字符
\\反斜杠 (\)
\"双引号 (")
\'单引号 (')
\n换行符 (LF)
\r回车符 (CR)
\t水平制表符 (TAB)
\b退格符 (BS)
\空格空格 (SPC)
\dddASCII 码为十进制ddd的字符
\xhhASCII 码为十六进制hh的字符
\ooooASCII 码为八进制ooo的字符
# let a = 'a' let single_quote = '\'' let copyright = '\xA9';;
val a : char = 'a' val single_quote : char = '\'' val copyright : char = '\169'

字符串字面量

字符串字面量::= " { string-character } "
   {quoted-string-id| { newline
 any-char } |quoted-string-id}
 
带引号的字符串标识符::={ a...z ∣ _ }
 
字符串字符::= 普通字符串字符
 转义序列
 \u{ { 09 ∣ AF ∣ af }+}
 换行
 \newline { space ∣ tab }

字符串字面量由"(双引号)字符分隔。这两个双引号包含一系列字符,这些字符要么与"\不同,要么是上面字符字面量表中给出的转义序列,要么是 Unicode 字符转义序列。

Unicode 字符转义序列由指定 Unicode 标量值的 UTF-8 编码替换。Unicode 标量值(范围在 0x0000...0xD7FF 或 0xE000...0x10FFFF 之间的整数)使用 1 到 6 个十六进制数字定义;允许前导零。

# let greeting = "Hello, World!\n" let superscript_plus = "\u{207A}";;
val greeting : string = "Hello, World!\n" val superscript_plus : string = "⁺"

换行序列是一个换行符,前面可选地带有一个回车符。从 OCaml 5.2 开始,字符串字面量中出现的换行序列被规范化为单个换行符。

为了允许跨行拆分长字符串字面量,序列\换行 ‍空格或制表符(一行末尾的反斜杠后跟下一行开头任意数量的空格和水平制表符)在字符串字面量中被忽略。

# let longstr = "Call me Ishmael. Some years ago — never mind how long \ precisely — having little or no money in my purse, and \ nothing particular to interest me on shore, I thought I\ \ would sail about a little and see the watery part of t\ he world.";;
val longstr : string = "Call me Ishmael. Some years ago — never mind how long precisely — having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world."

转义的换行符提供了比非转义的换行符更方便的行为,因为缩进不被视为字符串字面量的一部分。

# let contains_unexpected_spaces = "This multiline literal contains three consecutive spaces." let no_unexpected_spaces = "This multiline literal \n\ uses a single space between all words.";;
val contains_unexpected_spaces : string = "This multiline literal\n contains three consecutive spaces." val no_unexpected_spaces : string = "This multiline literal \nuses a single space between all words."

带引号的字符串字面量为字符串字面量提供了另一种词法语法。它们用于表示任意内容的字符串,无需转义。带引号的字符串由一对匹配的{ quoted-string-id || quoted-string-id }分隔,两侧使用相同的quoted-string-id。带引号的字符串不会以特殊方式解释任何字符1,但要求序列| quoted-string-id }不会出现在字符串本身中。标识符quoted-string-id是(可能是空的)小写字母和下划线的序列,可以自由选择以避免此类问题。

# let quoted_greeting = {|"Hello, World!"|} let nested = {ext|hello {|world|}|ext};;
val quoted_greeting : string = "\"Hello, World!\"" val nested : string = "hello {|world|}"

当前的实现对字符串字面量的长度几乎没有限制。

命名标签

为了避免歧义,表达式中命名标签不能仅仅在语法上定义为三个标记~ident:的序列,而必须在词法级别定义。

标签名::=小写标识符
 
标签::=~label-name:
 
可选标签::=?label-name:

命名标签有两种类型:label 用于普通参数,optlabel 用于可选参数。它们只是通过其第一个字符(~?)来区分。

尽管labeloptlabel 是表达式中的词法实体,但它们的扩展~ label-name :? label-name : 将用于语法,以提高可读性。还要注意,在类型表达式内部,此扩展可以按字面意思理解,实际上有 3 个标记,它们之间可选地有空格。

前缀和中缀符号

中缀符号::=(core-operator-char ∣ % ∣ <) { operator-char }
 # { operator-char }+
 
前缀符号::= ! { operator-char }
  (? ∣ ~) { operator-char }+
 
操作符字符::= ~ ∣ ! ∣ ? ∣ core-operator-char ∣ % ∣ < ∣ : ∣ .
 
核心操作符字符::= $ ∣ & ∣ * ∣ + ∣ - ∣ / ∣ = ∣ > ∣ @ ∣ ^ ∣ |

另请参阅以下语言扩展:扩展操作符扩展索引操作符绑定操作符

“操作符字符”序列,例如<=>!!,作为infix-symbolprefix-symbol 类中的单个标记读取。这些符号在表达式内部作为前缀和中缀操作符进行解析,但在其他情况下则表现得像普通标识符。

关键字

以下标识符作为关键字保留,不能以其他方式使用。

      and         as          assert      asr         begin       class
      constraint  do          done        downto      else        end
      exception   external    false       for         fun         function
      functor     if          in          include     inherit     initializer
      land        lazy        let         lor         lsl         lsr
      lxor        match       method      mod         module      mutable
      new         nonrec      object      of          open        or
      private     rec         sig         struct      then        to
      true        try         type        val         virtual     when
      while       with


以下字符序列也是关键字。

    !=    #     &     &&    '     (     )     *     +     ,     -
    -.    ->    .     ..    .~    :     ::    :=    :>    ;     ;;
    <     <-    =     >     >]    >}    ?     [     [<    [>    [|
    ]     _     `     {     {<    |     |]    ||    }     ~

请注意,以下标识符是现已不再维护的 Camlp4 系统的关键字,出于向后兼容性的原因,应避免使用。

    parser    value    $     $$    $:    <:    <<    >>    ??

歧义

词法歧义根据“最长匹配”规则解决:当字符序列可以以几种不同的方式分解成两个标记时,保留的分解方式是第一个标记最长的那个。

行号指令

行号指令::= # { 09 }+" { string-character } "

生成OCaml源代码的预处理器可以在其输出中插入行号指令,以便编译器生成的错误消息包含行号和文件名,这些文件名指的是预处理前的源文件,而不是预处理后的源文件。行号指令从行首开始,由一个#(井号)开头,后跟一个正整数(源代码行号),再后跟一个字符字符串(源文件名)。在词法分析期间,行号指令被视为空格。

1
除了前面提到的将换行序列规范化为单一换行符之外。