170 lines
6.6 KiB
Swift
170 lines
6.6 KiB
Swift
extension PureXML.Schema {
|
|
struct ContentNFAFragment {
|
|
var start: Int
|
|
var accept: Int
|
|
var counters: [Int]
|
|
var nullable: Bool
|
|
}
|
|
|
|
/// Builds a counted ``ContentNFA`` from a particle.
|
|
struct ContentNFABuilder {
|
|
private var states: [ContentState] = []
|
|
private var counters: [ContentCounterScope] = []
|
|
|
|
static func build(_ particle: PureXML.Schema.Particle) -> ContentNFA {
|
|
var builder = ContentNFABuilder()
|
|
let fragment = builder.particle(particle)
|
|
return ContentNFA(
|
|
states: builder.states,
|
|
counters: builder.counters,
|
|
start: fragment.start,
|
|
accept: fragment.accept,
|
|
)
|
|
}
|
|
|
|
private mutating func addState() -> Int {
|
|
states.append(ContentState())
|
|
return states.count - 1
|
|
}
|
|
|
|
private mutating func addEpsilon(
|
|
from source: Int,
|
|
to target: Int,
|
|
guards: [ContentCounterGuard] = [],
|
|
actions: [ContentCounterAction] = [],
|
|
) {
|
|
states[source].epsilon.append(ContentEpsilonEdge(target: target, guards: guards, actions: actions))
|
|
}
|
|
|
|
private mutating func particle(_ particle: PureXML.Schema.Particle) -> ContentNFAFragment {
|
|
let start = addState()
|
|
let loop = addState()
|
|
let accept = addState()
|
|
let counter = counters.count
|
|
let bodyNullable = termIsNullable(particle.term)
|
|
counters.append(ContentCounterScope(range: particle.occurrenceRange, nullableBody: bodyNullable))
|
|
let body = term(particle.term)
|
|
let resetAll = [ContentCounterAction.reset(counter)] + body.counters.map(ContentCounterAction.reset)
|
|
addEpsilon(from: start, to: loop, actions: resetAll)
|
|
addEpsilon(
|
|
from: loop,
|
|
to: accept,
|
|
guards: [
|
|
.canExit(
|
|
counter: counter,
|
|
minimum: particle.occurrenceRange.minimum,
|
|
nullableBody: bodyNullable,
|
|
),
|
|
],
|
|
)
|
|
addEpsilon(
|
|
from: loop,
|
|
to: body.start,
|
|
guards: [.belowMaximum(counter: counter, maximum: particle.occurrenceRange.maximum)],
|
|
actions: body.counters.map(ContentCounterAction.reset),
|
|
)
|
|
addEpsilon(from: body.accept, to: loop, actions: [.increment(counter)])
|
|
return ContentNFAFragment(
|
|
start: start,
|
|
accept: accept,
|
|
counters: [counter] + body.counters,
|
|
nullable: particleIsNullable(particle),
|
|
)
|
|
}
|
|
|
|
private mutating func term(_ term: PureXML.Schema.Term) -> ContentNFAFragment {
|
|
switch term {
|
|
case let .element(name, type, _, constraint, _, _):
|
|
let fragment = labeled(.name(name))
|
|
states[fragment.start].elementType = type
|
|
states[fragment.start].valueConstraint = constraint
|
|
return fragment
|
|
case let .wildcard(wildcard):
|
|
return labeled(.wildcard(wildcard))
|
|
case let .group(group):
|
|
return self.group(group)
|
|
}
|
|
}
|
|
|
|
private mutating func labeled(_ label: TermLabel) -> ContentNFAFragment {
|
|
let start = addState()
|
|
let accept = addState()
|
|
states[start].label = label
|
|
states[start].target = accept
|
|
return ContentNFAFragment(start: start, accept: accept, counters: [], nullable: false)
|
|
}
|
|
|
|
private mutating func group(_ group: PureXML.Schema.Group) -> ContentNFAFragment {
|
|
// `all` is validated by counting, not in the NFA; treat it as a
|
|
// sequence here so a nested occurrence still parses.
|
|
group.compositor == .choice ? choice(group.particles) : sequence(group.particles)
|
|
}
|
|
|
|
private mutating func sequence(_ particles: [PureXML.Schema.Particle]) -> ContentNFAFragment {
|
|
guard !particles.isEmpty else { return labeledEmpty() }
|
|
var first: Int?
|
|
var last: Int?
|
|
var fragmentCounters: [Int] = []
|
|
var nullable = true
|
|
for member in particles {
|
|
let part = particle(member)
|
|
if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start }
|
|
last = part.accept
|
|
fragmentCounters.append(contentsOf: part.counters)
|
|
nullable = nullable && part.nullable
|
|
}
|
|
return ContentNFAFragment(
|
|
start: first ?? addState(),
|
|
accept: last ?? addState(),
|
|
counters: fragmentCounters,
|
|
nullable: nullable,
|
|
)
|
|
}
|
|
|
|
private mutating func choice(_ particles: [PureXML.Schema.Particle]) -> ContentNFAFragment {
|
|
let start = addState()
|
|
let accept = addState()
|
|
var fragmentCounters: [Int] = []
|
|
var nullable = false
|
|
for member in particles {
|
|
let part = particle(member)
|
|
addEpsilon(from: start, to: part.start)
|
|
addEpsilon(from: part.accept, to: accept)
|
|
fragmentCounters.append(contentsOf: part.counters)
|
|
nullable = nullable || part.nullable
|
|
}
|
|
return ContentNFAFragment(
|
|
start: start,
|
|
accept: accept,
|
|
counters: fragmentCounters,
|
|
nullable: nullable,
|
|
)
|
|
}
|
|
|
|
private mutating func labeledEmpty() -> ContentNFAFragment {
|
|
let start = addState()
|
|
let accept = addState()
|
|
addEpsilon(from: start, to: accept)
|
|
return ContentNFAFragment(start: start, accept: accept, counters: [], nullable: true)
|
|
}
|
|
|
|
private func particleIsNullable(_ particle: PureXML.Schema.Particle) -> Bool {
|
|
particle.occurrenceRange.minimum.isZero || termIsNullable(particle.term)
|
|
}
|
|
|
|
private func termIsNullable(_ term: PureXML.Schema.Term) -> Bool {
|
|
switch term {
|
|
case .element, .wildcard:
|
|
false
|
|
case let .group(group):
|
|
switch group.compositor {
|
|
case .sequence, .all:
|
|
group.particles.allSatisfy(particleIsNullable)
|
|
case .choice:
|
|
group.particles.contains(where: particleIsNullable)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|