跳到主要内容

3. 语法 (Syntax)

本节展示 CDDL 的整体语法, 并给出一些仅用于说明语法的示例. (该定义并不试图做到过度形式化; 细节见 Appendix B.)

3.1. 通用约定 (General Conventions)

基本语法受 ABNF [RFC5234] 启发, 并采用以下约定:

  • 规则 (rules), 无论定义的是组 (groups) 还是类型 (types), 都由一个名称定义, 后跟等号 "=" 和按该定义对应语法规则给出的实际定义.

  • 名称可以由集合 {"A" to "Z", "a" to "z", "0" to "9", "", "-", "@", ".", "$"} 中的任意字符组成, 以字母字符 (包括 "@", "", "$") 开头, 并以此类字符或数字结尾.

    • 名称区分大小写.
    • 首选风格是以小写字母开始名称.
    • 连字符优先于下划线 (但在 "bareword" (Section 3.5.1) 中例外, 其语义实际上可能要求使用下划线).
    • 对较大的规范而言, 句点可能有助于表达某种模块结构 (如 "tcp.throughput" 与 "udp.throughput").
    • CDDL prelude 中预定义了若干名称, 如 Appendix D 所列.
    • 规则名 (类型或组) 不出现在实际 CBOR 编码中, 但在成员键中作为 "barewords" 使用的名称会出现.
  • 注释以 ";" (semicolon) 字符开始, 并在行尾结束 (LF 或 CRLF).

  • 除字符串内部外, 空白 (空格, 换行和注释) 用于分隔语法元素以提高可读性 (并分隔彼此相邻的标识符, 范围操作符或数字); 除此之外, 空白完全是可选的.

  • 十六进制数以 "0x" (不含引号) 为前缀, 且大小写不敏感. 类似地, 二进制数以 "0b" 为前缀.

  • 文本字符串由双引号 '"' 字符括起. 它们遵循 [RFC8259] Section 7 中定义的字符串约定. (ABNF 用户可能需要注意, CDDL 不支持文本字符串中的大小写不敏感概念; 如有必要, 可以使用正则表达式 (Section 3.8.3).)

  • 字节字符串由单引号 "'" 字符括起, 并且可以带有 "h" 或 "b64" 前缀. 如果没有前缀, 该字符串按文本字符串解释, 但单引号必须转义, 且生成的 UTF-8 字节会被标记为字节字符串 (major type 2). 如果前缀为 "h" 或 "b64", 则该字符串分别解释为十六进制数字对序列 (base16; 见 [RFC4648] Section 8) 或 base64(url) 字符串 ([RFC4648] Section 4 或 Section 5) (与 [RFC7049] Section 6 中的诊断表示法相同; cf. Appendix G.2); 在带前缀的情况下, 字符串内部出现的任何空白 (包括注释) 都会被忽略.

  • CDDL 使用 UTF-8 [RFC3629] 作为其编码. CDDL 的处理不涉及 Unicode 规范化过程.

示例:

; This is a comment
person = { g }

g = (
"name": tstr,
age: int, ; "age" is a bareword
)

3.2. 出现次数 (Occurrence)

可以在组条目前给出一个可选的 occurrence 指示符. 它可以是 (1) 字符 "?" (可选), "" (零个或多个) 或 "+" (一个或多个) 之一, 也可以是 (2) nm 形式, 其中 n 和 m 是可选的无符号整数, n 是出现次数下限 (默认 0), m 是出现次数上限 (默认无上限).

如果未指定出现次数指示符, 该组条目必须恰好出现一次 (就像指定了 1*1 一样). 带有出现次数指示符的组条目匹配由若干个基本组条目所匹配序列串接而成的名称/值对序列, 其中串接数量必须为该出现次数指示符所允许.

注意, 除了可能定义的任何指令/注解之外, CDDL 并不规定数组或映射使用定长还是不定长编码. 也就是说, 在规范中让数组大小保持 "open", 与它随后以定长或不定长形式交换之间没有关联.

还请注意, CDDL 可以描述目标表示的数据模型本身并不具备的灵活性. 对 JSON 而言这一点相当明显, 但它同样适用于 CBOR:

apartment = {
kitchen: size,
* bedroom: size,
}
size = float ; in m2

前面的规范并不意味着 CBOR 被改为允许多次使用键 "bedroom". 换言之, 由于数据模型施加的限制, 第三行基本上变成:

? bedroom: size,

(对于允许多种键的组, 大于一次的出现次数指示符在映射中仍然有用.)

3.3. 类型的预定义名称 (Predefined Names for Types)

CDDL 预定义了若干名称. 本小节汇总这些名称, 但精确定义请见 Appendix D.

定义了以下原始数据类型关键字:

"bool": 布尔值 (major type 7, additional information 20 or 21).

"uint": 无符号整数 (major type 0).

"nint": 负整数 (major type 1).

"int": 无符号整数或负整数.

"float16": 可表示为半精度浮点数 [IEEE754] 的数 (major type 7, additional information 25).

"float32": 可表示为单精度浮点数 [IEEE754] 的数 (major type 7, additional information 26).

"float64": 可表示为双精度浮点数 [IEEE754] 的数 (major type 7, additional information 27).

"float": float16, float32 或 float64 之一.

"bstr" or "bytes": 字节字符串 (major type 2).

"tstr" or "text": 文本字符串 (major type 3).

(注意, 数组或映射没有预定义名称; 它们使用下面给出的语法定义.)

此外, prelude 中还定义了若干与 CBOR 标签关联的类型, 如 "tdate", "bigint", "regexp" 等.

3.4. 数组 (Arrays)

数组定义使用方括号包围一个组.

每个条目都允许使用 Section 3.2 中指定的出现次数指示符.

例如:

unlimited-people = [* person]
one-or-two-people = [1*2 person]
at-least-two-people = [2* person]
person = (
name: tstr,
age: uint,
)

组 "person" 的定义方式使得它每在数组中重复一次, 就生成交替出现的姓名和年龄, 因此以下四个值都是类型 "unlimited-people" 的数据项的有效值:

["roundlet", 1047, "psychurgy", 2204, "extrarhythmical", 2231]
[]
["aluminize", 212, "climograph", 4124]
["penintime", 1513, "endocarditis", 4084, "impermeator", 1669,
"coextension", 865]

3.5. 映射 (Maps)

指定映射的语法值得特别关注, 也包含若干优化和便利写法, 因为它很可能是许多采用 CDDL 的规范的重点. 虽然该语法并不严格区分映射的 struct 用法和 table 用法, 但它专门照顾了这两类用法.

但首先, 让我们重申 CBOR 从 JSON 继承来的一个特性: CBOR 映射中的键/值对没有固定顺序. (可以想象固定顺序可能有用的场景. 例如, 解码器可以查找与整数键 1, 3 和 7 相关的值. 如果顺序是固定的, 并且解码器在遇到键 3 之前遇到了键 4, 它就可以得出键 3 不可用的结论, 而不需要进行更复杂的记账. 遗憾的是, JSON 和 CBOR 都不支持这一点, 因此 CDDL 也没有尝试支持它.)

3.5.1. 结构 (Structs)

映射的 "struct" 用法类似于许多 JSON 应用中使用 JSON 对象的方式.

映射的定义方式与数组定义相同 (见 Section 3.4), 只是使用花括号 "{}" 而不是方括号 "[]".

每个组条目都允许使用 Section 3.2 中指定的出现次数指示符.

下面是一个嵌入了结构的记录示例:

Geography = [
city : tstr,
gpsCoordinates : GpsCoordinates,
]

GpsCoordinates = {
longitude : uint, ; degrees, scaled by 10^7
latitude : uint, ; degrees, scaled by 10^7
}

编码时, Geography 记录使用包含两个成员的 CBOR 数组编码 (组条目的键会被忽略), 而 GpsCoordinates 结构则编码为包含两个键/值对的 CBOR 映射.

结构中使用的类型可以在单独规则中定义, 也可以就地定义 (可能放在括号内, 例如用于选择). 例如:

located-samples = {
sample-point: int,
samples: [+ float],
}

其中 "located-samples" 是引用该 struct 时要使用的数据类型, "sample-point" 和 "samples" 是要使用的键. 这实际上是一个完整示例: 后跟冒号的标识符可以直接用作成员键的文本字符串 (我们称之为 "bareword" 成员键), 双引号字符串或数字也可以这样使用. (当其他类型, 特别是包含多个值的类型, 用作键的类型时, 它们后面要跟双箭头; 见下文.)

如果文本字符串键不符合标识符语法 (或者规范编写者恰好偏好使用双引号), 也可以在成员键位置使用文本字符串语法, 后跟冒号. 因此, 上面的示例也可以写成在成员键位置使用带引号的字符串.

更一般地, 对于不属于上述几种情况的类型指定方式, 可以通过在其后跟双箭头来把它们用于键类型位置. 尤其是, 如果一个类型由标识符命名, 则双箭头是必要的 (该标识符后跟冒号时会被解释为 "bareword" 并转换成文本字符串). 字面文本字符串也会产生一个类型 (它只包含一个值, 即给定字符串), 因此此示例的另一种形式是:

located-samples = {
"sample-point" => int,
"samples" => [+ float],
}

关于这里描述的冒号 (":") 快捷写法还会添加某些隐含语义, 见下面的 Section 3.5.4.

演示双箭头用法的更好方式可能是:

located-samples = {
sample-point: int,
samples: [+ float],
* equipment-type => equipment-tolerances,
}
equipment-type = [name: tstr, manufacturer: tstr]
equipment-tolerances = [+ [float, float]]

下面的示例定义了一个带可选条目的 struct: 显示名称 (作为文本字符串), 名称组成部分 first name 和 family name (作为文本字符串), 以及年龄信息 (作为无符号整数).

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

注意, NameComponents 的组定义不会生成另一个映射; 相反, 所有四个键都直接位于 PersonalData 构建的 struct 中.

在此示例中, 从 CDDL 的角度看, 所有键/值对都是可选的. 如果没有出现次数指示符, 条目就是必需的.

如果希望添加当前规范未指定的更多条目, 可以显式加入这种可能性:

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
* tstr => any
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

图 7: 个人数据: 可扩展性示例

Appendix F 中描述的 CDDL 工具为该规范生成了以下可接受实例之一:

{"familyName": "agust", "antiforeignism": "pretzel",
"springbuck": "illuminatingly", "exuviae": "ephemeris",
"kilometrage": "frogfish"}

(关于显式标识扩展点的一种方式, 见 Section 3.9.)

3.5.2. 表 (Tables)

可以通过定义一个映射来指定表, 其中条目的键类型允许的不只是单个值; 例如:

square-roots = {* x => y}
x = int
y = float

这里, 每个键/值对中的键具有数据类型 x (定义为 int), 值具有数据类型 y (定义为 float).

如果规范不需要限制 x 或 y 中的某一个 (即应用可以按条目自由选择), 则可以用预定义名称 "any" 替代它.

另一个示例是, 以下内容可用作从整数或浮点数转换为字符串的转换表:

tostring = {* mynumber => tstr}
mynumber = int / float

3.5.3. 非确定性顺序 (Non-deterministic Order)

虽然数组的匹配方式完全由 PEG 形式体系决定 (见 Appendix A), 但映射的匹配更复杂, 因为映射没有固有顺序. 对于 PEG 算法会尝试的每个候选名称/值对, 都会从整个映射中选出一个匹配成员. 对某些组表达式而言, 映射中可能有多个成员匹配. 多数情况下这无关紧要, 因为组表达式往往会消耗所有匹配项:

labeled-values = {
? fritz: number,
* label => value
}
label = text
value = number

这里, 如果存在任何键为 "fritz" 的成员, 它会被组的第一个条目选中; 所有剩余的 text/number 成员会被第二个条目选中 (如果还有任何内容未被选中, 则该映射不匹配).

然而, 可以构造出这样的组表达式: 实际选中哪个成员是不确定的, 但这个选择又确实会影响结果:

do-not-do-this = {
int => int,
int => 6,
}

当该表达式与 "{3: 5, 4: 6}" 匹配时, 第一个组条目可能选走 "3: 5", 留下 "4: 6" 去匹配第二个条目. 或者, 它也可能选走 "4: 6", 从而不给第二个条目留下任何内容. 这种病态非确定性是由在 "更具体" 之前指定 "更一般", 以及由一个通用规则只消耗其能够匹配的映射键/值对的子集所导致的. 这两种情况在现实世界的映射规范中都往往不会出现. 在撰写本文时, CDDL 工具无法自动检测这类情况; 对于当前版本的 CDDL 规范, 只能敦促规范编写者不要编写病态非确定性的规范.

(敏锐的读者可能会想到 Standard Generalized Markup Language (SGML) 中所谓的 "ambiguous content models", 以及 XML 中的 "non-deterministic content models". 那个问题与这里描述的问题相关, 但这里的问题是由映射缺乏顺序这一点特别导致的, 而 XML schema languages 不需要应对这一点. 注意, RELAX NG 的 "interleave" 模式在规范侧显式处理无顺序问题, 而 XML 中的实例始终具有确定顺序.)

3.5.4. 映射中的 Cuts

上面针对 struct 讨论的可扩展性惯用法有一个问题:

extensible-map-example = {
? "optional-key" => int,
* tstr => any
}

在此示例中, 有一个可选键 "optional-key", 它在出现时映射到一个整数. 同时还有一个用于任何未来新增项的通配项.

遗憾的是, 数据项

{ "optional-key": "nonsense" }

确实匹配此规范: 虽然组的第一个条目不匹配, 但第二个条目 (通配项) 匹配. 这很可能是期望行为 (例如, 如果允许未来扩展扩展 "optional-key" 的类型), 但在许多情况下并非如此.

为了预留一个更通用的潜在特性 "cuts", CDDL 允许在映射条目定义中插入 cut "^":

extensible-map-example = {
? "optional-key" ^ => int,
* tstr => any
}

此位置的 cut 意味着, 一旦成员键匹配了带有 cut 的条目的名称部分, 就不再允许该成员键与映射组中后续条目中的其他潜在项匹配. 换言之, 当一个组条目仅基于匹配的键就会选取某个键/值对时, 它会 "锁定" 该选择. 此规则独立于值是否也匹配而适用, 因此当值不匹配时, 整个映射匹配失败. 总之, 上面的示例不再匹配加入 cut 后修改得到的规范.

由于这种排他性匹配的需求非常常见, ":" 快捷写法实际上被定义为包含 cut 语义. 因此, 前面的示例 (包含 cut) 可以更简单地写为:

extensible-map-example = {
? "optional-key": int,
* tstr => any
}

或者更短一些, 对键使用 bareword:

extensible-map-example = {
? optional-key: int,
* tstr => any
}

3.6. 标签 (Tags)

类型可以通过使用表示类型记法来利用 CBOR tag (major type 6), 即给出 #6.nnn(type), 其中 nnn 是给出标签号的无符号整数, "type" 是被加标签的数据项的类型.

例如, CDDL prelude (Appendix D) 中的以下行将 "biguint" 定义为无符号 bignum N 的类型名:

biguint = #6.2(bstr)

[RFC7049] 定义的标签包含在 prelude 中. 自 [RFC7049] 撰写以来注册的其他标签需要按需添加到 CDDL 规范中; 例如, 二进制 Universally Unique Identifier (UUID) 标签可以在定义以下内容后, 在规范中以 "buuid" 引用

buuid = #6.37(bstr)

在以下示例中, 对 URI 使用 tag 32 是可选的:

my_uri = #6.32(tstr) / tstr

3.7. 解包 (Unwrapping)

用于定义映射或数组的组通常可以在另一个映射或数组的定义中复用. 类似地, 定义为标签的类型携带一个人们可能希望引用的内部数据项. 在这些情况下, 直接使用映射, 数组或标签类型的名称作为其内部定义的组或类型的句柄会很方便.

"unwrap" 操作符 (写作在名称前加一个波浪号字符 "~") 可用于剥去名称所定义类型的一层, 暴露底层组 (对于映射和数组) 或底层类型 (对于标签).

例如, 应用可能希望定义 basic header 和 advanced header. 如果不使用解包, 可以如下完成:

basic-header-group = (
field1: int,
field2: text,
)

basic-header = [ basic-header-group ]

advanced-header = [
basic-header-group,
field3: bytes,
field4: number, ; as in the tagged type "time"
]

解包可将其简化为:

basic-header = [
field1: int,
field2: text,
]

advanced-header = [
~basic-header,
field3: bytes,
field4: ~time,
]

(注意, 如果在后一个示例中省略第一个 unwrap 操作符, 将导致 basic-header 在 advanced-header 内部以其自己的数组形式嵌套; 而使用解包后的 basic-header 时, basic-header 内部组的定义实际上会在 advanced-header 内部重复, 从而得到单个数组. 这可用于许多通常由编程语言中的继承来解决的应用. 解包的效果也可以描述为将被引用类型内部的组或类型 "threading in", 这提示了类似线的 "~" 字符.)

3.8. 控制 (Controls)

control 允许通过 control operatortarget 类型与 controller 类型关联起来.

控制类型的语法是 "target .control-operator controller", 其中控制操作符是以点为前缀的特殊标识符. (注意, targetcontroller 可能需要加括号.)

目前已定义若干控制操作符. 后续控制操作符可以由本规范的新版本定义, 或按 Section 6.1 中的过程注册.

3.8.1. 控制操作符 .size

".size" 控制通过控制类型来控制目标的字节大小. 该控制为文本字符串和字节字符串定义, 在这些类型上它直接控制字符串中的字节数. 它也为无符号整数定义 (见下文). 图 8 展示了字节字符串的用法示例.

full-address = [[+ label], ip4, ip6]
ip4 = bstr .size 4
ip6 = bstr .size 16
label = bstr .size (1..63)

图 8: 以字节为单位的大小控制

当应用于无符号整数时, ".size" 控制通过给出该无符号整数在计算机表示中应当需要的最大字节数来限制该整数的范围. 换言之, "uint .size N" 等价于 "0...BYTES_N", 其中 BYTES_N == 256**N.

audio_sample = uint .size 3 ; 24-bit, equivalent to 0...16777216

图 9: 以字节为单位的整数大小控制

注意, 与 CDDL 中的值限制一样, 此控制不是表示约束; 能装入更少字节的数字仍然可以以那种形式表示, 而低效实现也可以使用更长形式 (除非 CDDL 之外的某些格式约束限制了这一点, 例如 [RFC7049] Section 3.9 中的规则).

3.8.2. 控制操作符 .bits

字节字符串上的 ".bits" 控制表示, 在目标中, 只允许设置编号为控制类型中某个数字的位. (位按通常方式计数, 位号 "n" 在 "str" 中被设置意味着 "(str[n >> 3] & (1 << (n & 7))) != 0".) 类似地, 无符号整数 "i" 上的 ".bits" 控制表示, 对于所有满足 "(i & (1 << n)) != 0" 的无符号整数 "n", "n" 必须位于控制类型中.

tcpflagbytes = bstr .bits flags
flags = &(
fin: 8,
syn: 9,
rst: 10,
psh: 11,
ack: 12,
urg: 13,
ece: 14,
cwr: 15,
ns: 0,
) / (4..7) ; data offset bits

rwxbits = uint .bits rwx
rwx = &(r: 2, w: 1, x: 0)

图 10: 可设置位的控制

Appendix F 中描述的 CDDL 工具为 "tcpflagbytes" 生成了以下十个示例实例:

h'906d' h'01fc' h'8145' h'01b7' h'013d' h'409f' h'018e' h'c05f'
h'01fa' h'01fe'

这些示例没有体现出上面的 CDDL 规范并未显式指定两字节大小: 标志字节的有效全清零实例也可以是 "h''", "h'00'", 甚至 "h'000000'".

3.8.3. 控制操作符 .regexp

".regexp" 控制表示作为目标给出的文本字符串需要匹配作为控制类型中的值给出的 XML Schema Definition (XSD) 正则表达式. XSD 正则表达式定义见 [W3C.REC-xmlschema-2-20041028] Appendix F.

nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+"

图 11: 使用 XSD regexp 的控制

匹配此正则表达式的示例:

3.8.3.1. 使用注意事项 (Usage Considerations)

注意, XSD 正则表达式不支持通常用于以十六进制表示字节或 Unicode 码点的 \x 或 \u 转义. 不过, 在 CDDL 中, XSD 正则表达式包含在文本字符串中, 而文本字符串的字面量记法提供 \u 转义; 对大多数为文本字符串使用正则表达式的应用而言, 这应当已经足够. (注意, 这也意味着在应用 XSD 转义规则之前, 还存在一层字符串转义.)

XSD 正则表达式支持字符类减法, 这是正则表达式库中通常没有的特性; 规范编写者可能需要谨慎使用该特性. 类似考虑也适用于 Unicode 字符类; 如果使用这些字符类, 采用 CDDL 的规范 SHOULD 标识所针对的 Unicode 版本.

不常使用 XSD 正则表达式的用户还可能遇到以下意外之处:

  • 不直接支持大小写不敏感. 虽然大小写不敏感在协议设计中大多已经不再流行, 但有时仍然需要, 此时就需要像 "[Cc][Aa][Ss][Ee]" 这样手动表达.

  • 对 \w 和 \d 等常见字符类的支持基于 Unicode 字符属性; 这通常不是基于 ASCII 的协议所期望的, 因而可能带来意外. (\s 和 \S 确实具有其更常规的含义, 而 "." 匹配除行结束字符 \r 或 \n 之外的任何字符.)

3.8.3.2. 讨论 (Discussion)

编程社区中使用着多种风格的正则表达式. 例如, Perl-Compatible Regular Expressions (PCREs) 被广泛使用, 并且可能比 XSD 正则表达式更有用. 但是, 当前文档没有可用于 PCREs 的规范性引用. 因此, 我们目前选择 XSD 正则表达式. IETF 中已有这种选择的先例, 例如 YANG [RFC7950].

注意, CDDL 使用 controls 作为其主要扩展点. 如果需要, 这提供了在此处引用的格式之外添加更多正则表达式格式的机会. 作为示例, [CDDL-Freezer] 中定义了一个 ".pcre" 控制提案.

3.8.4. 控制操作符 .cbor 和 .cborseq

字节字符串上的 ".cbor" 控制表示该字节字符串携带一个 CBOR 编码的数据项. 解码后, 该数据项匹配作为右侧实参给出的类型 (以下示例中的 type1).

bytes .cbor type1

类似地, 字节字符串上的 ".cborseq" 控制表示该字节字符串携带一串 CBOR 编码的数据项. 当这些数据项被视为数组时, 该数组匹配作为右侧实参给出的类型 (以下示例中的 type2).

bytes .cborseq type2

(例如, 可以通过将字节字符串包装在 0x9f 和 0xff 这两个字节之间, 并将包装后的字节字符串作为 CBOR 编码数据项解码, 来实现从编码序列到数组的转换.)

3.8.5. 控制操作符 .within 和 .and

类型上的 ".and" 控制表示数据项同时匹配左侧类型和作为右侧给出的类型. (形式上, 结果类型是给出的两个类型的交集.)

type1 .and type2

".and" 控制的一个变体是 ".within" 控制, 它表达了一个额外意图: 左侧类型意在成为右侧类型的子集.

type1 .within type2

虽然两种形式具有相同的形式语义 (交集), 但 ".within" 形式的意图是, 右侧为左侧允许的类型提供指导, 而左侧通常是 socket (Section 3.9):

message = $message .within message-structure
message-structure = [message_type, *message_option]
message_type = 0..255
message_option = any

$message /= [3, dough: text, topping: [* text]]
$message /= [4, noodles: text, sauce: text, parmesan: bool]

对于 ".within", 如果 type1 允许了 type2 不允许的数据项, 工具可能标记错误. 相比之下, 对于 ".and", 并不期望 type1 已经是 type2 的子集.

3.8.6. 控制操作符 .lt, .le, .gt, .ge, .eq, .ne 和 .default

控制 .lt, .le, .gt, .ge, .eq 和 .ne 为左侧类型指定约束, 要求其值小于, 小于或等于, 大于, 大于或等于, 等于, 或不等于作为右侧类型给出的值 (该右侧类型仅包含那个单一值). 在当前规范中, 前四个控制 (.lt, .le, .gt 和 .ge) 仅为数值类型定义, 因为这些类型具有自然的顺序关系.

speed = number .ge 0  ; unit: m/s

.ne 和 .eq 同时为数值和其他类型的值定义. 如果其中一个值不是数值类型, 则按如下方式确定相等性: 如果文本字符串逐字节相同, 则它们相等 (满足 .eq / 不满足 .ne); 字节字符串同理. 如果数组具有相同数量的元素, 且两个数组中所有元素按顺序逐对相等, 则数组相等. 如果映射具有相同数量的键/值对, 且两个映射之间的键/值对逐对相等, 则映射相等. 如果带标签值具有相同标签且值相等, 则它们相等. 简单类型的值如果是相同值则匹配. 出现在数组, 映射或带标签值中的数值类型, 如果其数值相等且二者同为整数或同为浮点值, 则相等. 所有其他情况均不相等 (例如, 将文本字符串与字节字符串比较).

".ne" 控制的一个变体是 ".default" 控制, 它表达了一个额外意图: 右侧类型指定的值意在作为给定左侧类型的默认值, 而隐含的 .ne 控制用于防止该值在线路上传送. 此控制仅当控制类型用于可选上下文时才有意义; 否则就无法利用默认值.

timer = {
time: uint,
? displayed-step: (number .gt 0) .default 1
}

3.9. Socket/Plug

对于类型选择和组选择, CDDL 都定义了一种机制, 便于从空选择开始, 并在之后组装它们, 这些组装内容可能位于单独文件中, 再通过串接构建完整规范.

按照约定, CDDL 扩展点用前导美元符号 (类型) 或两个前导美元符号 (组) 标记. 工具会遵循该约定: 如果这样的类型或组完全没有定义, 也不报告错误; 此时该符号被视为空的类型选择 (组选择), 即没有可用选择.

tcp-header = {seq: uint, ack: uint, * $$tcp-option}

; later, in a different file

$$tcp-option //= (
sack: [+(left: uint, right: uint)]
)

; and, maybe in another file

$$tcp-option //= (
sack-permitted: true
)

以单个 "$" 开头的名称是 "type sockets", 起始为空类型, 并意图通过 "/=" 扩展. 以双 "$$" 开头的名称是 "group sockets", 起始为空组选择, 并意图通过 "//=" 扩展. 无论哪种情况, 如果某个 socket 完全没有定义, 也不是错误; 这意味着没有办法满足该规则 (即选择为空).

按照约定, socket 名称的所有定义 (plugs) 必须是增补, 即分别必须使用 "/=" 和 "//=".

继续使用图 7 所示示例, socket/plug 机制可以如图 12 所示使用:

PersonalData = {
? displayName: tstr,
NameComponents,
? age: uint,
* $$personaldata-extensions
}

NameComponents = (
? firstName: tstr,
? familyName: tstr,
)

; The above already works as is.
; But then, we can add later:

$$personaldata-extensions //= (
favorite-salsa: tstr,
)

; and again, somewhere else:

$$personaldata-extensions //= (
shoesize: uint,
)

图 12: Personal Data 示例: 使用 Socket/Plug 可扩展性

3.10. 泛型 (Generics)

使用尖括号时, 规则左侧可以在被定义名称之后添加形式参数, 如:

messages = message<"reboot", "now"> / message<"sleep", 1..100>
message<t, v> = {type: t, value: v}

使用泛型规则时, 在该泛型规则的作用域内, 形式参数会绑定到提供的实际实参 (同样使用尖括号), 就像存在一条 parameter = argument 形式的规则.

泛型规则可用于为类型和组建立名称.

(目前, Appendix F 中描述的 CDDL 工具对泛型嵌套存在一些限制.)

3.11. 操作符优先级 (Operator Precedence)

与任何具有前缀和中缀操作符等多种语法特性的语言一样, CDDL 中有些操作符的绑定比其他操作符更紧. 与 ABNF 等相比, 这会变得更复杂, 因为 CDDL 同时具有类型和组, 并且有专门用于这些概念的操作符. 类型操作符 (如用于类型选择的 "/") 作用于类型, 而组操作符 (如用于组选择的 "//") 作用于组. 类型可以直接用于组中, 但组需要加括号 (作为数组或映射) 才能成为类型. 因此, 类型操作符自然比组操作符绑定得更紧.

例如, 在

t = [group1]
group1 = (a / b // c / d)
a = 1 b = 2 c = 3 d = 4

中, group1 是 a 与 b 的类型选择和 c 与 d 的类型选择之间的组选择. 一旦加入成员键和/或出现次数, 这一点会变得更相关:

t = {group2}
group2 = (? ab: a / b // cd: c / d)
a = 1 b = 2 c = 3 d = 4

是类型为 a 或 b 的可选成员 "ab" 与类型为 c 或 d 的成员 "cd" 之间的组选择. 注意, 可选性附着在第一个选择 ("ab") 上, 而不是第二个选择.

类似地, 在

t = [group3]
group3 = (+ a / b / c)
a = 1 b = 2 c = 3

中, group3 是 a, b 和 c 之间类型选择的重复; 如果只有 a 要可重复, 则需要用组选择来限定出现次数的作用范围:

t = [group4]
group4 = (+ a // b / c)
a = 1 b = 2 c = 3

group4 是可重复的 a 与单个 b 或 c 之间的组选择.

有人评论说 group3 的语义可能不符合直觉. 一般而言, 与许多其他具有操作符优先级规则的语言一样, 鼓励规范编写者不要依赖这些规则, 而是充分插入括号, 以引导不熟悉 CDDL 优先级规则的读者:

t = [group4a]
group4a = ((+ a) // (b / c))
a = 1 b = 2 c = 3

操作符优先级按从松到紧的绑定顺序在 Appendix B 中定义, 并汇总于表 1. (给出的元数中, 1 表示一元前缀操作符, 2 表示二元中缀操作符.)

OperatorArityOperates onPrecedence
=2name = type, name = group1
/=2name /= type1
//=2name //= group1
//2group // group2
,2group, group3
*1* group4
n*m1n*m group4
+1+ group4
?1? group4
=>2type => type5
:2name: type5
/2type / type6
..2type..type7
...2type...type7
.ctrl2type .ctrl type7
&1&group8
~1~type8

表 1: 操作符优先级汇总