feat(relaxng): ns inheritance, QNames, include-start, combine, data-except

The compiler implements simplification 4.9-4.10 (ns inheritance across
ancestors and document boundaries; QName resolution against in-scope
xmlns declarations; the attribute name= shorthand stays namespace-less),
include brings in the included grammar's start with overrides (4.7),
multiple starts and defines combine with a single omitted combine
merging by the named method (4.17), data/except lands as a dataExcept
pattern in the derivative engine (4.12), an empty element matches data
whose lexical space admits the empty string, and whitespace-only
attribute values match empty-matching patterns. Spec suite: valid
274/289, invalid 279/291.
This commit is contained in:
Mihaela Mihaljevic 2026-06-10 10:24:46 +02:00
parent 2eab91bf2b
commit b8fa90ccf5
7 changed files with 287 additions and 160 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements `ns` inheritance and the QName form of names (4.9-4.10: `ns` walks ancestors and crosses include/externalRef document boundaries; `name="eg:foo"` and `<name>eg:foo</name>` resolve their prefix against the schema's in-scope xmlns declarations; the `name=` shorthand on an attribute stays namespace-less rather than inheriting), an `include` brings in the included grammar's `start` with an include-level override honored (4.7), multiple `<start>` elements combine like defines and a single define or start may omit `combine` while still merging by the named method (4.17), `<data>` supports `<except>` via a new `dataExcept` pattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps.
- RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected.
- RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes.
- Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous `xmlns=""` is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to `&#xD;` (the #135 class, surfacing in output rather than lexing).

View File

@ -92,6 +92,8 @@ extension PureXML.Schema {
case let .value(type, literal):
type.valueMatches(string, literal: literal) ? .empty : .notAllowed
case let .data(type): type.validate(string) == nil ? .empty : .notAllowed
case let .dataExcept(type, except):
type.validate(string) == nil && !nullable(valueDeriv(except, string)) ? .empty : .notAllowed
case let .list(inner): listMatches(inner, string) ? .empty : .notAllowed
default: .notAllowed
}
@ -124,7 +126,11 @@ extension PureXML.Schema {
}
private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String) -> Bool {
value.isEmpty ? nullable(content) : nullable(textDeriv(content, value))
if nullable(textDeriv(content, value)) { return true }
// A whitespace-only attribute value also matches a pattern that
// matches the empty sequence, the same leniency element content
// gets for ignorable whitespace.
return value.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isWhitespace) && nullable(content)
}
private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
@ -206,7 +212,9 @@ extension PureXML.Schema.RelaxNGEngine {
private func childrenDeriv(_ pattern: PureXML.Schema.Pattern, _ children: [PureXML.Model.Node]) -> PureXML.Schema.Pattern {
let coalesced = Self.coalesceText(children)
if coalesced.isEmpty { return pattern }
// Clark's algorithm treats an empty element as optional empty text, so
// a data/value pattern whose lexical space admits "" matches <e/>.
if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "")) }
if coalesced.count == 1, case let .text(string) = coalesced[0] {
let derived = textDeriv(pattern, string)
return Self.isWhitespace(string) ? choice(pattern, derived) : derived

View File

@ -0,0 +1,92 @@
/// The name-class side of the RELAX NG compiler (4.9-4.10: ns inheritance and
/// QName resolution), split from the compiler body to keep it under the
/// length caps.
extension RNGCompiler {
typealias Tree = RNGTree
func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
let children = RNGNode.elementChildren(node)
if let name = RNGNode.attribute(node, "name") {
if let qualified = RNGNode.resolveQName(name, at: node) {
return (.name(namespace: qualified.namespace, localName: qualified.localName), children)
}
// The `name=` shorthand on an attribute does not inherit ns: an
// unprefixed attribute name has no namespace unless the attribute
// element itself carries ns (4.9). Elements inherit normally.
let isAttribute = RNGNode.localName(node) == "attribute"
let namespace = isAttribute
? (node.attributes.first { $0.name.prefix == nil && $0.name.localName == "ns" }?.value ?? "")
: effectiveNS(node)
return (.name(namespace: namespace, localName: name), children)
}
guard let first = children.first else { return (.anyName, []) }
return (nameClass(first), Array(children.dropFirst()))
}
// MARK: Combinators and name classes
func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
let patterns = nodes.map(pattern)
let identity: Pattern = compositor == .choice ? .notAllowed : .empty
guard let first = patterns.first else { return identity }
return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
}
func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
switch compositor {
case .sequence: .group(lhs, rhs)
case .choice: .choice(lhs, rhs)
case .all: .interleave(lhs, rhs)
}
}
func nameClass(_ node: Tree) -> NameClass {
switch RNGNode.localName(node) {
case "name":
nameElementClass(node)
case "anyName":
anyName(node)
case "nsName":
nsName(node)
case "choice":
nameClassChoice(RNGNode.elementChildren(node))
default:
.anyName
}
}
func anyName(_ node: Tree) -> NameClass {
guard let except = exceptClass(node) else { return .anyName }
return .anyNameExcept(except)
}
/// A `<name>` element: its text may be a QName resolved against in-scope
/// xmlns declarations (4.10); otherwise the inherited ns applies (4.9).
func nameElementClass(_ node: Tree) -> NameClass {
let raw = RNGNode.text(node)
if let qualified = RNGNode.resolveQName(raw, at: node) {
return .name(namespace: qualified.namespace, localName: qualified.localName)
}
return .name(namespace: effectiveNS(node), localName: raw)
}
func nsName(_ node: Tree) -> NameClass {
let namespace = effectiveNS(node)
guard let except = exceptClass(node) else { return .nsName(namespace) }
return .nsNameExcept(namespace: namespace, except: except)
}
/// The name class of a `<name-class>`'s `<except>` child, treating multiple
/// children as an implicit choice (`anyName`/`nsName` minus the union).
func exceptClass(_ node: Tree) -> NameClass? {
guard let except = RNGNode.children(node, named: "except").first else { return nil }
let children = RNGNode.elementChildren(except)
return children.isEmpty ? nil : nameClassChoice(children)
}
func nameClassChoice(_ nodes: [Tree]) -> NameClass {
let classes = nodes.map(nameClass)
guard let first = classes.first else { return .anyName }
return classes.dropFirst().reduce(first) { .choice($0, $1) }
}
}

View File

@ -0,0 +1,109 @@
typealias RNGTree = PureXML.Model.TreeNode
/// RNGTree helpers for the RELAX NG parser. File-scope and private.
enum RNGNode {
static func localName(_ node: RNGTree) -> String? {
node.name?.localName
}
static func attribute(_ node: RNGTree, _ name: String) -> String? {
let raw = node.attributes.first { $0.name.localName == name }?.value
// Simplification 4.2: the value of name, type, and combine attributes
// is whitespace-trimmed.
guard let raw, ["name", "type", "combine"].contains(name) else { return raw }
return raw.trimmingXMLWhitespace()
}
/// The RELAX NG namespace; simplification 4.1 removes foreign-namespace
/// elements (annotations such as documentation or comments) before any
/// pattern interpretation.
static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0"
static func elementChildren(_ node: RNGTree) -> [RNGTree] {
node.children.filter { child in
guard child.kind == .element else { return false }
// Simplification 4.1: only RELAX NG-namespace elements are schema
// content. An element with no namespace counts as schema content
// only when the whole document is unqualified (leniency for
// namespace-less schemas); under a qualified parent it is foreign.
let uri = child.name?.namespaceURI
if uri == relaxNGNamespace { return true }
return uri == nil && node.name?.namespaceURI == nil
}
}
static func children(_ node: RNGTree, named name: String) -> [RNGTree] {
elementChildren(node).filter { localName($0) == name }
}
static func text(_ node: RNGTree) -> String {
node.stringValue.trimmingXMLWhitespace()
}
static func strip(_ qualified: String) -> String {
qualified.split(separator: ":").last.map(String.init) ?? qualified
}
/// The nearest `ns` attribute on the node or an ancestor (simplification
/// 4.9 inheritance), or nil when none is in scope in this document.
static func inheritedNS(_ node: RNGTree) -> String? {
var current: RNGTree? = node
while let candidate = current {
if let namespace = candidate.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == "ns" })?.value {
return namespace
}
current = candidate.parent
}
return nil
}
/// Resolves a prefixed name against the xmlns declarations in scope at the
/// schema node (simplification 4.10), or nil for an unprefixed name.
static func resolveQName(_ raw: String, at node: RNGTree) -> (namespace: String, localName: String)? {
guard let colon = raw.firstIndex(of: ":") else { return nil }
let prefix = String(raw[..<colon])
let local = String(raw[raw.index(after: colon)...])
if prefix == "xml" { return ("http://www.w3.org/XML/1998/namespace", local) }
var current: RNGTree? = node
while let candidate = current {
if let uri = candidate.attributes.first(where: { $0.name.prefix == "xmlns" && $0.name.localName == prefix })?.value {
return (uri, local)
}
current = candidate.parent
}
return nil
}
/// Resolves `reference` against an optional base URI (RFC 3986), keeping a
/// relative base's merged form relative (the same adjustment external
/// identifiers use).
static func resolveRelative(_ reference: String, against base: String?) -> String {
guard let base, !base.isEmpty else { return reference }
let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base)
let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":")
if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") {
return String(merged.dropFirst())
}
return merged
}
/// The `href` of an include/externalRef, resolved against the document's
/// base and the `xml:base` attributes in scope at the node (4.5).
static func resolvedHref(_ node: RNGTree, documentBase: String?) -> String? {
guard let href = attribute(node, "href") else { return nil }
var bases: [String] = []
var cursor: RNGTree? = node
while let current = cursor {
if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value {
bases.append(base)
}
cursor = current.parent
}
if let documentBase { bases.append(documentBase) }
var resolved = href
for base in bases {
resolved = resolveRelative(resolved, against: base)
}
return resolved
}
}

View File

@ -1,97 +1,36 @@
private typealias Tree = PureXML.Model.TreeNode
/// Tree helpers for the RELAX NG parser. File-scope and private.
private enum RNGNode {
static func localName(_ node: Tree) -> String? {
node.name?.localName
}
static func attribute(_ node: Tree, _ name: String) -> String? {
let raw = node.attributes.first { $0.name.localName == name }?.value
// Simplification 4.2: the value of name, type, and combine attributes
// is whitespace-trimmed.
guard let raw, ["name", "type", "combine"].contains(name) else { return raw }
return raw.trimmingXMLWhitespace()
}
/// The RELAX NG namespace; simplification 4.1 removes foreign-namespace
/// elements (annotations such as documentation or comments) before any
/// pattern interpretation.
static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0"
static func elementChildren(_ node: Tree) -> [Tree] {
node.children.filter { child in
guard child.kind == .element else { return false }
// Simplification 4.1: only RELAX NG-namespace elements are schema
// content. An element with no namespace counts as schema content
// only when the whole document is unqualified (leniency for
// namespace-less schemas); under a qualified parent it is foreign.
let uri = child.name?.namespaceURI
if uri == relaxNGNamespace { return true }
return uri == nil && node.name?.namespaceURI == nil
}
}
static func children(_ node: Tree, named name: String) -> [Tree] {
elementChildren(node).filter { localName($0) == name }
}
static func text(_ node: Tree) -> String {
node.stringValue.trimmingXMLWhitespace()
}
static func strip(_ qualified: String) -> String {
qualified.split(separator: ":").last.map(String.init) ?? qualified
}
/// Resolves `reference` against an optional base URI (RFC 3986), keeping a
/// relative base's merged form relative (the same adjustment external
/// identifiers use).
static func resolveRelative(_ reference: String, against base: String?) -> String {
guard let base, !base.isEmpty else { return reference }
let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base)
let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":")
if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") {
return String(merged.dropFirst())
}
return merged
}
/// The `href` of an include/externalRef, resolved against the document's
/// base and the `xml:base` attributes in scope at the node (4.5).
static func resolvedHref(_ node: Tree, documentBase: String?) -> String? {
guard let href = attribute(node, "href") else { return nil }
var bases: [String] = []
var cursor: Tree? = node
while let current = cursor {
if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value {
bases.append(base)
}
cursor = current.parent
}
if let documentBase { bases.append(documentBase) }
var resolved = href
for base in bases {
resolved = resolveRelative(resolved, against: base)
}
return resolved
}
}
private typealias Tree = RNGTree
/// Compiles a RELAX NG XML-syntax document into the pattern algebra, resolving
/// `include` and `externalRef` through a loader. Holds the accumulating `define`
/// table so external grammars merge into it. File-scope and private.
private final class RNGCompiler {
final class RNGCompiler {
typealias Pattern = PureXML.Schema.Pattern
typealias NameClass = PureXML.Schema.NameClass
typealias Compositor = PureXML.Schema.Compositor
private(set) var defines: [String: Pattern] = [:]
/// The combine method established for each define name (4.17): defines
/// sharing a name agree on one method, and the single define that omits
/// `combine` still merges by it.
private var defineCombine: [String: String] = [:]
private let loader: (String) -> String?
private var visited: Set<String> = []
/// The URI of the document currently being compiled (nil for the top
/// level): hrefs inside a loaded document resolve against it.
private var documentBase: String?
/// The `ns` value carried across a document boundary: an externalRef's
/// in-scope ns applies to the loaded pattern when it has none of its own
/// (simplification 4.6); the tree-ancestor walk cannot cross documents.
private var fallbackNamespace = ""
/// The start pattern contributed by an `include`d grammar (4.7), used when
/// the including grammar declares no start of its own.
private var includedStart: Pattern?
/// The ns in scope at a schema node: its own/ancestor `ns` attribute, or
/// the namespace inherited across the document boundary.
func effectiveNS(_ node: Tree) -> String {
RNGNode.inheritedNS(node) ?? fallbackNamespace
}
init(loader: @escaping (String) -> String?) {
self.loader = loader
@ -108,8 +47,26 @@ private final class RNGCompiler {
for define in RNGNode.children(node, named: "define") {
addDefine(define)
}
let start = RNGNode.children(node, named: "start").first
return start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed
let starts = RNGNode.children(node, named: "start")
guard let first = starts.first else {
return includedStart ?? .notAllowed
}
// Multiple starts combine like defines (4.17): the named method wins,
// and the single start that omits `combine` still merges by it.
var method: String?
for start in starts {
if let named = RNGNode.attribute(start, "combine") { method = named }
}
var result = combined(RNGNode.elementChildren(first), .sequence)
for start in starts.dropFirst() {
let next = combined(RNGNode.elementChildren(start), .sequence)
switch method {
case "choice": result = .choice(result, next)
case "interleave": result = .interleave(result, next)
default: result = next
}
}
return result
}
/// Adds a `define`, honoring `combine`: a second definition of the same name
@ -118,11 +75,14 @@ private final class RNGCompiler {
private func addDefine(_ define: Tree) {
guard let name = RNGNode.attribute(define, "name") else { return }
let pattern = combined(RNGNode.elementChildren(define), .sequence)
if let method = RNGNode.attribute(define, "combine") {
defineCombine[name] = method
}
guard let existing = defines[name] else {
defines[name] = pattern
return
}
switch RNGNode.attribute(define, "combine") {
switch defineCombine[name] {
case "choice": defines[name] = .choice(existing, pattern)
case "interleave": defines[name] = .interleave(existing, pattern)
default: defines[name] = pattern
@ -140,12 +100,22 @@ private final class RNGCompiler {
}
visited.insert(href)
let outerBase = documentBase
let outerNamespace = fallbackNamespace
documentBase = href
defer { documentBase = outerBase }
if RNGNode.inheritedNS(grammar) == nil {
fallbackNamespace = effectiveNS(node)
}
for define in RNGNode.children(grammar, named: "define") {
addDefine(define)
}
if includedStart == nil, let start = RNGNode.children(grammar, named: "start").first {
includedStart = combined(RNGNode.elementChildren(start), .sequence)
}
documentBase = outerBase
fallbackNamespace = outerNamespace
if let override = RNGNode.children(node, named: "start").first {
includedStart = combined(RNGNode.elementChildren(override), .sequence)
}
for define in RNGNode.children(node, named: "define") {
addDefine(define)
}
@ -153,7 +123,7 @@ private final class RNGCompiler {
// MARK: Patterns
private func pattern(_ node: Tree) -> Pattern {
func pattern(_ node: Tree) -> Pattern {
leafPattern(node) ?? compositePattern(node)
}
@ -189,7 +159,13 @@ private final class RNGCompiler {
PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets)
}
}
return .data(PureXML.Schema.SimpleType(base: base, facets: facets))
let type = PureXML.Schema.SimpleType(base: base, facets: facets)
// 4.12: an except child excludes its (implicitly choice-combined)
// patterns from the data type's lexical space.
if let except = RNGNode.children(node, named: "except").first {
return .dataExcept(type, combined(RNGNode.elementChildren(except), .choice))
}
return .data(type)
}
/// Builds a `<value>` pattern carrying its datatype, so the value compares in
@ -246,8 +222,15 @@ private final class RNGCompiler {
}
visited.insert(href)
let outerBase = documentBase
let outerNamespace = fallbackNamespace
documentBase = href
defer { documentBase = outerBase }
if RNGNode.inheritedNS(top) == nil {
fallbackNamespace = effectiveNS(node)
}
defer {
documentBase = outerBase
fallbackNamespace = outerNamespace
}
return topLevel(top)
}
@ -279,73 +262,6 @@ private final class RNGCompiler {
let content = contentNodes.isEmpty ? Pattern.text : combined(contentNodes, .sequence)
return .attribute(nameClass, content)
}
private func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
let children = RNGNode.elementChildren(node)
if let name = RNGNode.attribute(node, "name") {
let namespace = RNGNode.attribute(node, "ns") ?? ""
return (.name(namespace: namespace, localName: name), children)
}
guard let first = children.first else { return (.anyName, []) }
return (nameClass(first), Array(children.dropFirst()))
}
// MARK: Combinators and name classes
private func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
let patterns = nodes.map(pattern)
let identity: Pattern = compositor == .choice ? .notAllowed : .empty
guard let first = patterns.first else { return identity }
return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
}
private func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
switch compositor {
case .sequence: .group(lhs, rhs)
case .choice: .choice(lhs, rhs)
case .all: .interleave(lhs, rhs)
}
}
private func nameClass(_ node: Tree) -> NameClass {
switch RNGNode.localName(node) {
case "name":
.name(namespace: RNGNode.attribute(node, "ns") ?? "", localName: RNGNode.text(node))
case "anyName":
anyName(node)
case "nsName":
nsName(node)
case "choice":
nameClassChoice(RNGNode.elementChildren(node))
default:
.anyName
}
}
private func anyName(_ node: Tree) -> NameClass {
guard let except = exceptClass(node) else { return .anyName }
return .anyNameExcept(except)
}
private func nsName(_ node: Tree) -> NameClass {
let namespace = RNGNode.attribute(node, "ns") ?? ""
guard let except = exceptClass(node) else { return .nsName(namespace) }
return .nsNameExcept(namespace: namespace, except: except)
}
/// The name class of a `<name-class>`'s `<except>` child, treating multiple
/// children as an implicit choice (`anyName`/`nsName` minus the union).
private func exceptClass(_ node: Tree) -> NameClass? {
guard let except = RNGNode.children(node, named: "except").first else { return nil }
let children = RNGNode.elementChildren(except)
return children.isEmpty ? nil : nameClassChoice(children)
}
private func nameClassChoice(_ nodes: [Tree]) -> NameClass {
let classes = nodes.map(nameClass)
guard let first = classes.first else { return .anyName }
return classes.dropFirst().reduce(first) { .choice($0, $1) }
}
}
extension PureXML.Schema {

View File

@ -36,6 +36,9 @@ public extension PureXML.Schema {
case element(NameClass, Pattern)
case attribute(NameClass, Pattern)
case data(SimpleType)
/// `<data>` with an `<except>` child: the type matches but the except
/// pattern must not (4.12).
case dataExcept(SimpleType, Pattern)
case value(SimpleType, String)
case list(Pattern)
case ref(String)

View File

@ -33,12 +33,10 @@ struct RelaxNGSpecSuiteTests {
private let knownIncorrectCompiledCount = 213
private let knownCorrectRejected: Set<Int> = []
private let knownValidRejected: Set<Int> = [
98, 104, 109, 110, 111, 115, 119, 120, 124, 125, 126, 127, 128, 130,
131, 132, 133, 142, 190, 191, 194, 195, 208, 209, 210, 236, 256, 258,
265, 266, 268, 269, 271, 328, 372, 374, 378, 379, 380,
98, 111, 115, 133, 195, 208, 209, 210, 271, 328, 379, 380,
]
private let knownInvalidAccepted: Set<Int> = [
104, 124, 125, 126, 127, 128, 142, 264, 271,
120, 142, 264, 271, 378, 379,
]
private struct SpecCase {