Add SwiftTL
This commit is contained in:
parent
2907ddbe46
commit
22e58eb5e4
72 changed files with 13931 additions and 0 deletions
15
build-system/SwiftTL/Package.swift
Normal file
15
build-system/SwiftTL/Package.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftTL",
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "SwiftTL",
|
||||
dependencies: []),
|
||||
]
|
||||
)
|
||||
3
build-system/SwiftTL/README.md
Normal file
3
build-system/SwiftTL/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SwiftTL
|
||||
|
||||
|
||||
986
build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift
Normal file
986
build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift
Normal file
|
|
@ -0,0 +1,986 @@
|
|||
import Foundation
|
||||
|
||||
private let reservedIdentifiers: [String] = [
|
||||
"protocol",
|
||||
"private"
|
||||
]
|
||||
|
||||
private extension String {
|
||||
var camelCased: String {
|
||||
var result = ""
|
||||
|
||||
var capitalizeNext = false
|
||||
for c in self {
|
||||
if c == "_" {
|
||||
capitalizeNext = true
|
||||
} else {
|
||||
if capitalizeNext {
|
||||
capitalizeNext = false
|
||||
result.append(c.uppercased())
|
||||
} else {
|
||||
result.append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var camelCasedAndEscaped: String {
|
||||
var result = self.camelCased
|
||||
|
||||
if reservedIdentifiers.contains(result) {
|
||||
result = "`\(result)`"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private struct CodeWriter {
|
||||
private var code: String = ""
|
||||
private var indentLevel: Int = 0
|
||||
private let indentString: String = " "
|
||||
|
||||
private var currentIndent: String {
|
||||
String(repeating: indentString, count: indentLevel)
|
||||
}
|
||||
|
||||
mutating func indent() {
|
||||
indentLevel += 1
|
||||
}
|
||||
|
||||
mutating func dedent() {
|
||||
indentLevel = max(0, indentLevel - 1)
|
||||
}
|
||||
|
||||
mutating func line(_ text: String = "") {
|
||||
if text.isEmpty {
|
||||
code += "\n"
|
||||
} else {
|
||||
code += currentIndent + text + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
mutating func lines(_ text: String) {
|
||||
for line in text.split(separator: "\n", omittingEmptySubsequences: false) {
|
||||
if line.isEmpty {
|
||||
code += "\n"
|
||||
} else {
|
||||
code += currentIndent + line + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func output() -> String {
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
/*private extension QualifiedName {
|
||||
var camelCased: String {
|
||||
if let namespace = self.namespace {
|
||||
return "\(namespace).\(self.value.camelCased)"
|
||||
} else {
|
||||
return self.value.camelCased
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private func typeReferenceRepresentation(_ type: Resolver.TypeReference) -> String {
|
||||
switch type {
|
||||
case .int32:
|
||||
return "Int32"
|
||||
case .int64:
|
||||
return "Int64"
|
||||
case .int256:
|
||||
return "Int256"
|
||||
case .double:
|
||||
return "Double"
|
||||
case .bytes:
|
||||
return "Buffer"
|
||||
case .string:
|
||||
return "String"
|
||||
case .bool:
|
||||
return "Bool"
|
||||
case .boolTrue:
|
||||
return "bool"
|
||||
case let .bareVector(elementType):
|
||||
return "[\(typeReferenceRepresentation(elementType))]"
|
||||
case let .boxedVector(elementType):
|
||||
return "[\(typeReferenceRepresentation(elementType))]"
|
||||
case let .bareConstructor(typeName, _):
|
||||
return "Api.\(typeName)"
|
||||
case let .boxedType(typeName):
|
||||
return "Api.\(typeName)"
|
||||
}
|
||||
}
|
||||
|
||||
private extension Resolver.SumType {
|
||||
func hasDirectReference(to otherTypes: [Resolver.SumType], typeMap: [QualifiedName: Resolver.SumType]) throws -> Bool {
|
||||
for (_, constructor) in self.constructors {
|
||||
for argument in constructor.arguments {
|
||||
switch argument.type {
|
||||
case .int32:
|
||||
break
|
||||
case .int64:
|
||||
break
|
||||
case .int256:
|
||||
break
|
||||
case .double:
|
||||
break
|
||||
case .bytes:
|
||||
break
|
||||
case .string:
|
||||
break
|
||||
case .bool:
|
||||
break
|
||||
case .boolTrue:
|
||||
break
|
||||
case .bareVector:
|
||||
break
|
||||
case .boxedVector:
|
||||
break
|
||||
case .bareConstructor(let typeName, _), .boxedType(let typeName):
|
||||
for otherType in otherTypes {
|
||||
if typeName == otherType.name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
guard let referencedType = typeMap[typeName] else {
|
||||
throw CodeGenerator.CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
|
||||
var mergedTypes = otherTypes
|
||||
if !mergedTypes.contains(where: { $0.name == self.name }) {
|
||||
mergedTypes.append(self)
|
||||
}
|
||||
|
||||
if try referencedType.hasDirectReference(to: mergedTypes, typeMap: typeMap) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private extension Sequence where Iterator.Element: Hashable {
|
||||
func unique() -> [Iterator.Element] {
|
||||
var seen: Set<Iterator.Element> = []
|
||||
return filter { seen.insert($0).inserted }
|
||||
}
|
||||
}
|
||||
|
||||
enum CodeGenerator {
|
||||
struct CodeGenerationError: Error, CustomStringConvertible {
|
||||
var text: String
|
||||
|
||||
var description: String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
static func generate(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)], typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])], stubFunctions: Bool = false) throws -> [String: String] {
|
||||
var files: [String: String] = [:]
|
||||
|
||||
var functions = functions
|
||||
functions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: UInt32(bitPattern: -1058929929), arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool"))))
|
||||
|
||||
files["Api0.swift"] = try generateMainFile(types: types, functions: functions, constructorOrder: constructorOrder)
|
||||
|
||||
for index in 0 ..< typeOrder.count {
|
||||
files["Api\(index + 1).swift"] = try generateImplFile(types: types, functions: functions, typeOrder: typeOrder[index], stubFunctions: stubFunctions)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
private static func generateMainFile(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)]) throws -> String {
|
||||
var writer = CodeWriter()
|
||||
|
||||
writer.line()
|
||||
|
||||
var namespaces = Set<String>()
|
||||
for type in types {
|
||||
if let namespace = type.name.namespace {
|
||||
namespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
var functionNamespaces = Set<String>()
|
||||
for function in functions {
|
||||
if let namespace = function.name.namespace {
|
||||
functionNamespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("public enum Api {")
|
||||
writer.indent()
|
||||
for namespace in namespaces.sorted(by: { $0 < $1 }) {
|
||||
writer.line("public enum \(namespace) {}")
|
||||
}
|
||||
writer.line("public enum functions {")
|
||||
writer.indent()
|
||||
for namespace in functionNamespaces.sorted(by: { $0 < $1 }) {
|
||||
writer.line("public enum \(namespace) {}")
|
||||
}
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
|
||||
var typeMap: [QualifiedName: Resolver.SumType] = [:]
|
||||
for type in types {
|
||||
typeMap[type.name] = type
|
||||
}
|
||||
|
||||
writer.line("fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {")
|
||||
writer.indent()
|
||||
writer.line("var dict: [Int32 : (BufferReader) -> Any?] = [:]")
|
||||
writer.line("dict[-1471112230] = { return $0.readInt32() }")
|
||||
writer.line("dict[570911930] = { return $0.readInt64() }")
|
||||
writer.line("dict[571523412] = { return $0.readDouble() }")
|
||||
writer.line("dict[0x0929C32F] = { return parseInt256($0) }")
|
||||
writer.line("dict[-1255641564] = { return parseString($0) }")
|
||||
|
||||
for (typeName, constructorName) in constructorOrder {
|
||||
guard let type = typeMap[typeName] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
|
||||
var found = false
|
||||
for (_, constructor) in type.constructors {
|
||||
if constructor.name.value == constructorName {
|
||||
found = true
|
||||
writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return Api.\(type.name).parse_\(constructor.name.value)($0) }")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
throw CodeGenerationError(text: "Constructor \(constructorName) not found")
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("return dict")
|
||||
writer.dedent()
|
||||
writer.line("}()")
|
||||
|
||||
writer.line()
|
||||
|
||||
writer.line("public extension Api {")
|
||||
writer.indent()
|
||||
|
||||
writer.line("static func parse(_ buffer: Buffer) -> Any? {")
|
||||
writer.indent()
|
||||
writer.line("let reader = BufferReader(buffer)")
|
||||
writer.line("if let signature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("return parse(reader, signature: signature)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
|
||||
writer.line("static func parse(_ reader: BufferReader, signature: Int32) -> Any? {")
|
||||
writer.indent()
|
||||
writer.line("if let parser = parsers[signature] {")
|
||||
writer.indent()
|
||||
writer.line("return parser(reader)")
|
||||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("telegramApiLog(\"Type constructor \\(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found\")")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
|
||||
writer.line("static func parseVector<T>(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? {")
|
||||
writer.indent()
|
||||
writer.line("if let count = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("var array = [T]()")
|
||||
writer.line("var i: Int32 = 0")
|
||||
writer.line("while i < count {")
|
||||
writer.indent()
|
||||
writer.line("var signature = elementSignature")
|
||||
writer.line("if elementSignature == 0 {")
|
||||
writer.indent()
|
||||
writer.line("if let unboxedSignature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("signature = unboxedSignature")
|
||||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("if elementType == Buffer.self {")
|
||||
writer.indent()
|
||||
writer.line("if let item = parseBytes(reader) as? T {")
|
||||
writer.indent()
|
||||
writer.line("array.append(item)")
|
||||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("if let item = Api.parse(reader, signature: signature) as? T {")
|
||||
writer.indent()
|
||||
writer.line("array.append(item)")
|
||||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("i += 1")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return array")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
|
||||
writer.line("static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) {")
|
||||
writer.indent()
|
||||
writer.line("switch object {")
|
||||
|
||||
let typeOrder = constructorOrder.map(\.typeName).unique()
|
||||
|
||||
for typeName in typeOrder {
|
||||
guard let type = typeMap[typeName] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
|
||||
writer.line("case let _1 as Api.\(type.name):")
|
||||
writer.indent()
|
||||
writer.line("_1.serialize(buffer, boxed)")
|
||||
writer.dedent()
|
||||
}
|
||||
|
||||
writer.line("default:")
|
||||
writer.indent()
|
||||
writer.line("break")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
return writer.output()
|
||||
}
|
||||
|
||||
private static func generateImplFile(types: [Resolver.SumType], functions: [Resolver.Function], typeOrder: (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]), stubFunctions: Bool) throws -> String {
|
||||
var writer = CodeWriter()
|
||||
|
||||
var typeMap: [QualifiedName: Resolver.SumType] = [:]
|
||||
for type in types {
|
||||
typeMap[type.name] = type
|
||||
}
|
||||
|
||||
for (typeName, constructorNames) in typeOrder.types {
|
||||
writer.line("public extension Api\(typeName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.indent()
|
||||
|
||||
guard let type = typeMap[typeName] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
|
||||
let indirectPrefix = try type.hasDirectReference(to: [type], typeMap: typeMap) ? "indirect " : ""
|
||||
writer.line("\(indirectPrefix)enum \(typeName.value): TypeConstructorDescription {")
|
||||
writer.indent()
|
||||
|
||||
var sortedConstructors: [Resolver.SumType.Constructor] = []
|
||||
for constructorName in constructorNames {
|
||||
var foundConstructor: Resolver.SumType.Constructor?
|
||||
for (_, constructor) in type.constructors {
|
||||
if constructor.name.value == constructorName {
|
||||
foundConstructor = constructor
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let constructor = foundConstructor else {
|
||||
throw CodeGenerationError(text: "Constructor \(constructorName) -> \(typeName) not found")
|
||||
}
|
||||
sortedConstructors.append(constructor)
|
||||
}
|
||||
|
||||
let useStructPattern = true
|
||||
|
||||
if useStructPattern {
|
||||
for constructor in sortedConstructors {
|
||||
var fieldsString = ""
|
||||
var initParamsString = ""
|
||||
var initBodyString = ""
|
||||
var descriptionFieldsString = ""
|
||||
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
let fieldName = argument.name.camelCasedAndEscaped
|
||||
let fieldType = typeReferenceRepresentation(argument.type) + (argument.condition != nil ? "?" : "")
|
||||
|
||||
if !fieldsString.isEmpty {
|
||||
fieldsString.append("\n")
|
||||
}
|
||||
fieldsString.append("public var \(fieldName): \(fieldType)")
|
||||
|
||||
if !initParamsString.isEmpty {
|
||||
initParamsString.append(", ")
|
||||
}
|
||||
initParamsString.append("\(fieldName): \(fieldType)")
|
||||
|
||||
if !initBodyString.isEmpty {
|
||||
initBodyString.append("\n")
|
||||
}
|
||||
initBodyString.append("self.\(fieldName) = \(fieldName)")
|
||||
|
||||
if !descriptionFieldsString.isEmpty {
|
||||
descriptionFieldsString.append(", ")
|
||||
}
|
||||
descriptionFieldsString.append("(\"\(fieldName)\", ConstructorParameterDescription(self.\(fieldName)))")
|
||||
}
|
||||
|
||||
if !fieldsString.isEmpty {
|
||||
writer.line("public class Cons_\(constructor.name.value): TypeConstructorDescription {")
|
||||
writer.indent()
|
||||
writer.lines(fieldsString)
|
||||
writer.line("public init(\(initParamsString)) {")
|
||||
writer.indent()
|
||||
writer.lines(initBodyString)
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {")
|
||||
writer.indent()
|
||||
writer.line("return (\"\(constructor.name.value)\", [\(descriptionFieldsString)])")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
let hasFields = constructor.arguments.contains { if case .boolTrue = $0.type { return false } else { return true } }
|
||||
|
||||
if useStructPattern && hasFields {
|
||||
writer.line("case \(constructor.name.value)(Cons_\(constructor.name.value))")
|
||||
} else {
|
||||
var argumentsString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if !argumentsString.isEmpty {
|
||||
argumentsString.append(", ")
|
||||
}
|
||||
|
||||
argumentsString.append(argument.name.camelCased)
|
||||
argumentsString.append(": ")
|
||||
argumentsString.append(typeReferenceRepresentation(argument.type))
|
||||
if argument.condition != nil {
|
||||
argumentsString.append("?")
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("case \(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))")")
|
||||
}
|
||||
}
|
||||
|
||||
writer.line()
|
||||
writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
writer.line("switch self {")
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
let hasFields = constructor.arguments.contains { if case .boolTrue = $0.type { return false } else { return true } }
|
||||
|
||||
if useStructPattern && hasFields {
|
||||
writer.line("case .\(constructor.name.value)(let _data):")
|
||||
writer.indent()
|
||||
writer.line("if boxed {")
|
||||
writer.indent()
|
||||
writer.line("buffer.appendInt32(\(Int32(bitPattern: constructor.id)))")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
var argumentAccessor = "_data.\(argument.name.camelCasedAndEscaped)"
|
||||
if let condition = argument.condition {
|
||||
writer.line("if Int(_data.\(condition.fieldName.camelCasedAndEscaped)) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
argumentAccessor.append("!")
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
}
|
||||
}
|
||||
writer.line("break")
|
||||
writer.dedent()
|
||||
} else {
|
||||
var argumentsString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if !argumentsString.isEmpty {
|
||||
argumentsString.append(", ")
|
||||
}
|
||||
|
||||
argumentsString.append("let ")
|
||||
argumentsString.append(argument.name.camelCasedAndEscaped)
|
||||
}
|
||||
|
||||
writer.line("case .\(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))"):")
|
||||
writer.indent()
|
||||
writer.line("if boxed {")
|
||||
writer.indent()
|
||||
writer.line("buffer.appendInt32(\(Int32(bitPattern: constructor.id)))")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
var argumentAccessor = "\(argument.name.camelCasedAndEscaped)"
|
||||
if let condition = argument.condition {
|
||||
writer.line("if Int(\(condition.fieldName)) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
argumentAccessor.append("!")
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
}
|
||||
}
|
||||
writer.line("break")
|
||||
writer.dedent()
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("}")
|
||||
} // end if !stubFunctions
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
writer.line("public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
writer.line("switch self {")
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
let hasFields = constructor.arguments.contains { if case .boolTrue = $0.type { return false } else { return true } }
|
||||
|
||||
if useStructPattern && hasFields {
|
||||
var argumentSerializationString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if !argumentSerializationString.isEmpty {
|
||||
argumentSerializationString.append(", ")
|
||||
}
|
||||
argumentSerializationString.append("(\"\(argument.name.camelCasedAndEscaped)\", ConstructorParameterDescription(_data.\(argument.name.camelCasedAndEscaped)))")
|
||||
}
|
||||
|
||||
writer.line("case .\(constructor.name.value)(let _data):")
|
||||
writer.indent()
|
||||
writer.line("return (\"\(constructor.name.value)\", [\(argumentSerializationString)])")
|
||||
writer.dedent()
|
||||
} else {
|
||||
var argumentsString = ""
|
||||
var argumentSerializationString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if !argumentsString.isEmpty {
|
||||
argumentsString.append(", ")
|
||||
}
|
||||
if !argumentSerializationString.isEmpty {
|
||||
argumentSerializationString.append(", ")
|
||||
}
|
||||
|
||||
argumentsString.append("let ")
|
||||
argumentsString.append(argument.name.camelCasedAndEscaped)
|
||||
|
||||
argumentSerializationString.append("(\"\(argument.name.camelCasedAndEscaped)\", \(argument.name.camelCasedAndEscaped) as Any)")
|
||||
}
|
||||
|
||||
writer.line("case .\(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))"):")
|
||||
writer.indent()
|
||||
writer.line("return (\"\(constructor.name.value)\", [\(argumentSerializationString)])")
|
||||
writer.dedent()
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("}")
|
||||
} // end if !stubFunctions for descriptionFields
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
writer.line("public static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(typeName.value)? {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
|
||||
if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) {
|
||||
var argumentIndex = 0
|
||||
var argumentCheckString = ""
|
||||
var argumentCollectionString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(argument.type))?")
|
||||
|
||||
if let condition = argument.condition {
|
||||
guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
|
||||
writer.line("if Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
}
|
||||
|
||||
if !argumentCheckString.isEmpty {
|
||||
argumentCheckString.append(" && ")
|
||||
}
|
||||
argumentCheckString.append("_c\(argumentIndex + 1)")
|
||||
|
||||
if !argumentCollectionString.isEmpty {
|
||||
argumentCollectionString.append(", ")
|
||||
}
|
||||
argumentCollectionString.append("\(argument.name.camelCased): _\(argumentIndex + 1)")
|
||||
if argument.condition == nil {
|
||||
argumentCollectionString.append("!")
|
||||
}
|
||||
|
||||
argumentIndex += 1
|
||||
}
|
||||
|
||||
var checkIndex = 0
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if let condition = argument.condition {
|
||||
guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
|
||||
writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil")
|
||||
} else {
|
||||
writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil")
|
||||
}
|
||||
|
||||
checkIndex += 1
|
||||
}
|
||||
|
||||
writer.line("if \(argumentCheckString) {")
|
||||
writer.indent()
|
||||
if useStructPattern && !argumentCollectionString.isEmpty {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)(Cons_\(constructor.name.value)(\(argumentCollectionString)))")
|
||||
} else {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")")
|
||||
}
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)")
|
||||
}
|
||||
|
||||
} // end if !stubFunctions
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
|
||||
if !typeOrder.functions.isEmpty {
|
||||
for functionName in typeOrder.functions {
|
||||
writer.line("public extension Api.functions\(functionName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.indent()
|
||||
|
||||
var foundFunction: Resolver.Function?
|
||||
for function in functions {
|
||||
if function.name == functionName {
|
||||
foundFunction = function
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let function = foundFunction else {
|
||||
throw CodeGenerationError(text: "Function \(functionName) not found")
|
||||
}
|
||||
|
||||
var argumentsString = ""
|
||||
for argument in function.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
if !argumentsString.isEmpty {
|
||||
argumentsString.append(", ")
|
||||
}
|
||||
|
||||
argumentsString.append(argument.name.camelCasedAndEscaped)
|
||||
argumentsString.append(": ")
|
||||
argumentsString.append(typeReferenceRepresentation(argument.type))
|
||||
if argument.condition != nil {
|
||||
argumentsString.append("?")
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("static func \(function.name.value)(\(argumentsString)) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<\(typeReferenceRepresentation(function.result))>) {")
|
||||
writer.indent()
|
||||
writer.line("let buffer = Buffer()")
|
||||
writer.line("buffer.appendInt32(\(Int32(bitPattern: function.id)))")
|
||||
|
||||
var argumentSerializationString = ""
|
||||
for argument in function.arguments {
|
||||
if case .boolTrue = argument.type {
|
||||
continue
|
||||
}
|
||||
|
||||
var argumentAccessor = "\(argument.name.camelCasedAndEscaped)"
|
||||
if let condition = argument.condition {
|
||||
guard let _ = function.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
|
||||
writer.line("if Int(\(condition.fieldName)) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
argumentAccessor.append("!")
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
}
|
||||
|
||||
if !argumentSerializationString.isEmpty {
|
||||
argumentSerializationString.append(", ")
|
||||
}
|
||||
|
||||
argumentSerializationString.append("(\"\(argument.name.camelCasedAndEscaped)\", ConstructorParameterDescription(\(argument.name.camelCasedAndEscaped)))")
|
||||
}
|
||||
|
||||
writer.line("return (FunctionDescription(name: \"\(function.name)\", parameters: [\(argumentSerializationString)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> \(typeReferenceRepresentation(function.result))? in")
|
||||
writer.indent()
|
||||
writer.line("let reader = BufferReader(buffer)")
|
||||
writer.line("var result: \(typeReferenceRepresentation(function.result))?")
|
||||
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: Resolver.Argument(name: "result", type: function.result, condition: nil), argumentAccessor: "result")
|
||||
|
||||
writer.line("return result")
|
||||
writer.dedent()
|
||||
writer.line("})")
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
}
|
||||
|
||||
return writer.output()
|
||||
}
|
||||
|
||||
private static func generateFieldSerialization(writer: inout CodeWriter, argument: Resolver.Argument, argumentAccessor: String) {
|
||||
switch argument.type {
|
||||
case .int32:
|
||||
writer.line("serializeInt32(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .int64:
|
||||
writer.line("serializeInt64(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .int256:
|
||||
writer.line("serializeInt256(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .double:
|
||||
writer.line("serializeDouble(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .bytes:
|
||||
writer.line("serializeBytes(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .string:
|
||||
writer.line("serializeString(\(argumentAccessor), buffer: buffer, boxed: false)")
|
||||
case .bool:
|
||||
preconditionFailure()
|
||||
case .boolTrue:
|
||||
preconditionFailure()
|
||||
case .bareVector(let elementType), .boxedVector(let elementType):
|
||||
if case .boxedVector = argument.type {
|
||||
writer.line("buffer.appendInt32(481674261)")
|
||||
}
|
||||
writer.line("buffer.appendInt32(Int32(\(argumentAccessor).count))")
|
||||
writer.line("for item in \(argumentAccessor) {")
|
||||
writer.indent()
|
||||
generateFieldSerialization(writer: &writer, argument: Resolver.Argument(name: "item", type: elementType, condition: nil), argumentAccessor: "item")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
case .bareConstructor:
|
||||
writer.line("\(argumentAccessor).serialize(buffer, false)")
|
||||
case .boxedType:
|
||||
writer.line("\(argumentAccessor).serialize(buffer, true)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func generateFieldParsing(writer: inout CodeWriter, typeMap: [QualifiedName: Resolver.SumType], argument: Resolver.Argument, argumentAccessor: String) throws {
|
||||
switch argument.type {
|
||||
case .int32:
|
||||
writer.line("\(argumentAccessor) = reader.readInt32()")
|
||||
case .int64:
|
||||
writer.line("\(argumentAccessor) = reader.readInt64()")
|
||||
case .int256:
|
||||
writer.line("\(argumentAccessor) = parseInt256(reader)")
|
||||
case .double:
|
||||
writer.line("\(argumentAccessor) = reader.readDouble()")
|
||||
case .bytes:
|
||||
writer.line("\(argumentAccessor) = parseBytes(reader)")
|
||||
case .string:
|
||||
writer.line("\(argumentAccessor) = parseString(reader)")
|
||||
case .bool:
|
||||
preconditionFailure()
|
||||
case .boolTrue:
|
||||
preconditionFailure()
|
||||
case .bareVector(let elementType), .boxedVector(let elementType):
|
||||
var elementSignature: Int32 = 0
|
||||
|
||||
switch elementType {
|
||||
case .int32:
|
||||
elementSignature = -1471112230
|
||||
case .int64:
|
||||
elementSignature = 570911930
|
||||
case .int256:
|
||||
elementSignature = 0x0929C32F
|
||||
case .double:
|
||||
elementSignature = 571523412
|
||||
case .bytes:
|
||||
elementSignature = -1255641564
|
||||
case .string:
|
||||
elementSignature = -1255641564
|
||||
case .bool:
|
||||
elementSignature = 0
|
||||
case .boolTrue:
|
||||
elementSignature = 0
|
||||
case .bareVector:
|
||||
elementSignature = 0
|
||||
case .boxedVector:
|
||||
elementSignature = 0
|
||||
case let .bareConstructor(typeName, name):
|
||||
guard let type = typeMap[typeName] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
guard let constructor = type.constructors[name] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
elementSignature = Int32(bitPattern: constructor.id)
|
||||
case .boxedType:
|
||||
elementSignature = 0
|
||||
}
|
||||
|
||||
if case .boxedVector = argument.type {
|
||||
writer.line("if let _ = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)")
|
||||
}
|
||||
case let .bareConstructor(typeName, name):
|
||||
guard let type = typeMap[typeName] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
guard let constructor = type.constructors[name] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
writer.line("\(argumentAccessor) = Api.parse(reader, signature: \(Int32(bitPattern: constructor.id)) as? \(typeReferenceRepresentation(argument.type))")
|
||||
case .boxedType:
|
||||
writer.line("if let signature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("\(argumentAccessor) = Api.parse(reader, signature: signature) as? \(typeReferenceRepresentation(argument.type))")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
228
build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift
Normal file
228
build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import Foundation
|
||||
|
||||
enum DescriptionParser {
|
||||
enum TypeReferenceDescription {
|
||||
case generic(name: String, argumentType: QualifiedName)
|
||||
case type(name: QualifiedName)
|
||||
}
|
||||
|
||||
struct ArgumentDescription {
|
||||
struct ConditionDescription {
|
||||
var fieldName: String
|
||||
var bitIndex: Int
|
||||
}
|
||||
|
||||
var name: String
|
||||
var type: TypeReferenceDescription
|
||||
var condition: ConditionDescription?
|
||||
}
|
||||
|
||||
struct ConstructorDescription {
|
||||
var name: QualifiedName
|
||||
var explicitId: UInt32?
|
||||
var arguments: [ArgumentDescription]
|
||||
var type: TypeReferenceDescription
|
||||
}
|
||||
|
||||
static func parse(data: String) throws -> (constructors: [ConstructorDescription], functions: [ConstructorDescription]) {
|
||||
let lines = data.components(separatedBy: "\n")
|
||||
|
||||
var typeLines: [String] = []
|
||||
var functionLines: [String] = []
|
||||
|
||||
let skipPrefixes: [String] = [
|
||||
//"boolFalse#bc799737 = Bool;",
|
||||
//"boolTrue#997275b5 = Bool;",
|
||||
"true#3fedd339 = True;",
|
||||
"vector#1cb5c415 {t:Type} # [ t ] = Vector t;",
|
||||
"error#c4b9f9bb code:int text:string = Error;",
|
||||
"null#56730bcc = Null;"
|
||||
]
|
||||
|
||||
let skipContains: [String] = [
|
||||
"{X:Type}"
|
||||
]
|
||||
|
||||
var isParsingFunctions = false
|
||||
loop: for line in lines {
|
||||
if line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty {
|
||||
// skip
|
||||
} else if line == "---functions---" {
|
||||
isParsingFunctions = true
|
||||
} else {
|
||||
for string in skipPrefixes {
|
||||
if line.hasPrefix(string) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
for string in skipContains {
|
||||
if line.contains(string) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
if isParsingFunctions {
|
||||
functionLines.append(line)
|
||||
} else {
|
||||
typeLines.append(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var constructors: [ConstructorDescription] = []
|
||||
var functions: [ConstructorDescription] = []
|
||||
|
||||
for line in typeLines {
|
||||
do {
|
||||
let constructor = try self.parseConstructor(string: line)
|
||||
constructors.append(constructor)
|
||||
} catch let e {
|
||||
print("Error while parsing line:\n\(line)\n")
|
||||
print("\(e)")
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
for line in functionLines {
|
||||
do {
|
||||
let constructor = try parseConstructor(string: line)
|
||||
functions.append(constructor)
|
||||
} catch let e {
|
||||
print("Error while parsing line:\n\(line)\n")
|
||||
print("\(e)")
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return (constructors, functions)
|
||||
}
|
||||
|
||||
private static func parseConstructor(string: String) throws -> ConstructorDescription {
|
||||
let parseIdentifier = Parse {
|
||||
Prefix<Substring>(minLength: 1, while: { $0.isLetter || $0.isNumber || $0 == "_" })
|
||||
}.map { String($0) }
|
||||
|
||||
let parseConditionDescription = Parse {
|
||||
parseIdentifier
|
||||
"."
|
||||
Int.parser(of: Substring.self)
|
||||
"?"
|
||||
}.map { fieldName, bitIndex -> ArgumentDescription.ConditionDescription in
|
||||
ArgumentDescription.ConditionDescription(fieldName: fieldName, bitIndex: bitIndex)
|
||||
}
|
||||
|
||||
let parseQualifiedName = Parse {
|
||||
parseIdentifier
|
||||
Optionally {
|
||||
"."
|
||||
parseIdentifier
|
||||
}
|
||||
}.map { first, second -> QualifiedName in
|
||||
if let second = second {
|
||||
return QualifiedName(namespace: first, value: second)
|
||||
} else {
|
||||
return QualifiedName(namespace: nil, value: first)
|
||||
}
|
||||
}
|
||||
|
||||
let parseGenericTypeReference = Parse {
|
||||
parseIdentifier
|
||||
"<"
|
||||
parseQualifiedName
|
||||
">"
|
||||
}.map { name, argumentType -> TypeReferenceDescription in
|
||||
return .generic(name: name, argumentType: argumentType)
|
||||
}
|
||||
|
||||
let parseDirectTypeReference = Parse {
|
||||
parseQualifiedName
|
||||
}.map { name -> TypeReferenceDescription in
|
||||
return .type(name: name)
|
||||
}
|
||||
|
||||
let parseFlagsTypeReference = Parse {
|
||||
"#"
|
||||
}.map { () -> TypeReferenceDescription in
|
||||
return .type(name: QualifiedName(namespace: nil, value: "int"))
|
||||
}
|
||||
|
||||
let parseTypeReference = Parse {
|
||||
OneOf {
|
||||
parseFlagsTypeReference
|
||||
parseGenericTypeReference
|
||||
parseDirectTypeReference
|
||||
}
|
||||
}
|
||||
|
||||
let parseArgument = Parse {
|
||||
parseIdentifier
|
||||
":"
|
||||
Optionally {
|
||||
parseConditionDescription
|
||||
}
|
||||
parseTypeReference
|
||||
}.map { name, condition, type -> ArgumentDescription in
|
||||
return ArgumentDescription(name: name, type: type, condition: condition)
|
||||
}
|
||||
|
||||
let parseExplicitId = Parse {
|
||||
"#"
|
||||
Prefix<Substring> { $0.isHexDigit }
|
||||
}.map { UInt32($0, radix: 16)! }
|
||||
|
||||
let optionalExplicitId = Optionally {
|
||||
parseExplicitId
|
||||
}
|
||||
|
||||
let manyArguments = Many {
|
||||
parseArgument
|
||||
} separator: {
|
||||
Whitespace()
|
||||
}
|
||||
|
||||
let nameAndConstructor = Parse {
|
||||
parseQualifiedName
|
||||
optionalExplicitId
|
||||
Whitespace()
|
||||
}.map { name, explicitId, _ -> (name: QualifiedName, explicitId: UInt32?) in
|
||||
return (name, explicitId)
|
||||
}
|
||||
|
||||
let typeSeparator = Parse {
|
||||
Whitespace()
|
||||
"="
|
||||
Whitespace()
|
||||
}
|
||||
|
||||
let trailerParser = Parse {
|
||||
Whitespace()
|
||||
";"
|
||||
Whitespace()
|
||||
End()
|
||||
}.map { _ -> Void in
|
||||
}
|
||||
|
||||
let parseConstructor = Parse {
|
||||
nameAndConstructor
|
||||
manyArguments
|
||||
typeSeparator
|
||||
parseTypeReference
|
||||
trailerParser
|
||||
}.map { nameAndConstructor, arguments, _, type -> ConstructorDescription in
|
||||
return ConstructorDescription(
|
||||
name: nameAndConstructor.name,
|
||||
explicitId: nameAndConstructor.explicitId,
|
||||
arguments: arguments,
|
||||
type: type
|
||||
)
|
||||
}
|
||||
|
||||
var data = string[...]
|
||||
let result = try parseConstructor.parse(&data)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
128
build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift
Normal file
128
build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import Foundation
|
||||
|
||||
enum LegacyOrderParser {
|
||||
struct LegacyOrderParsingError: Error, CustomStringConvertible {
|
||||
var text: String
|
||||
|
||||
var description: String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
static func parseConstructorOrder(data: String) throws -> [(typeName: QualifiedName, constructorName: String)] {
|
||||
let lines = data.split(separator: "\n")
|
||||
|
||||
var result: [(typeName: QualifiedName, constructorName: String)] = []
|
||||
|
||||
for line in lines {
|
||||
if let startRange = line.range(of: " = { return Api."), let endRange = line.range(of: "($0) }", options: [.backwards], range: nil) {
|
||||
let parseString = line[startRange.upperBound ..< endRange.lowerBound]
|
||||
let components = parseString.components(separatedBy: ".parse_")
|
||||
if components.count != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
result.append((QualifiedName(string: components[0]), components[1]))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func parseTypeOrder(data: String) throws -> (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]) {
|
||||
var resultTypes: [(typeName: QualifiedName, constructorNames: [String])] = []
|
||||
var resultFunctions: [QualifiedName] = []
|
||||
|
||||
let namespaces = data.components(separatedBy: "public extension Api {\n")
|
||||
|
||||
enum ParseSection {
|
||||
case types
|
||||
case functions
|
||||
}
|
||||
|
||||
for namespaceData in namespaces {
|
||||
if namespaceData.isEmpty {
|
||||
continue
|
||||
}
|
||||
guard let firstNewline = namespaceData.range(of: "\n") else {
|
||||
throw LegacyOrderParsingError(text: "No newline in the beginning of the namespace section")
|
||||
}
|
||||
let namespaceName: String?
|
||||
let namespaceContentData: String
|
||||
let parseSection: ParseSection
|
||||
if let prefixRange = namespaceData.range(of: " public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .types
|
||||
} else if let prefixRange = namespaceData.range(of: " indirect public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .types
|
||||
} else if let prefixRange = namespaceData.range(of: " public struct functions {", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.upperBound == firstNewline.lowerBound {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .functions
|
||||
} else {
|
||||
guard let prefixRange = namespaceData.range(of: "public struct ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex else {
|
||||
throw LegacyOrderParsingError(text: "Missing header prefix in the beginning of the namespace section")
|
||||
}
|
||||
guard let trailerRange = namespaceData.range(of: " {", options: [], range: prefixRange.upperBound ..< firstNewline.lowerBound) else {
|
||||
throw LegacyOrderParsingError(text: "Missing trailing suffix in the beginning of the namespace section")
|
||||
}
|
||||
namespaceName = String(namespaceData[prefixRange.upperBound ..< trailerRange.lowerBound])
|
||||
namespaceContentData = String(namespaceData[firstNewline.upperBound...])
|
||||
parseSection = .types
|
||||
}
|
||||
|
||||
let namespaceContentLines = namespaceContentData.split(separator: "\n")
|
||||
|
||||
switch parseSection {
|
||||
case .types:
|
||||
var currentType: (typeName: QualifiedName, constructorNames: [String])?
|
||||
for line in namespaceContentLines {
|
||||
if let typePrefixRange = line.range(of: " public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex {
|
||||
let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound])
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
currentType = (QualifiedName(namespace: namespaceName, value: typeName), [])
|
||||
} else if let typePrefixRange = line.range(of: " indirect public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex {
|
||||
let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound])
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
currentType = (QualifiedName(namespace: namespaceName, value: typeName), [])
|
||||
} else if currentType != nil, let constructorPrefixRange = line.range(of: " case "), constructorPrefixRange.lowerBound == line.startIndex {
|
||||
let constructorName: String
|
||||
if let bracketRange = line.range(of: "(") {
|
||||
constructorName = String(line[constructorPrefixRange.upperBound ..< bracketRange.lowerBound])
|
||||
} else {
|
||||
constructorName = String(line[constructorPrefixRange.upperBound...])
|
||||
}
|
||||
currentType?.constructorNames.append(constructorName)
|
||||
}
|
||||
}
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
case .functions:
|
||||
var currentNamespace: String?
|
||||
for line in namespaceContentLines {
|
||||
if let namespacePrefixRange = line.range(of: " public struct "), namespacePrefixRange.lowerBound == line.startIndex, let namespaceSuffixRange = line.range(of: " {"), namespaceSuffixRange.upperBound == line.endIndex {
|
||||
currentNamespace = String(line[namespacePrefixRange.upperBound ..< namespaceSuffixRange.lowerBound])
|
||||
} else if let functionPrefixRange = line.range(of: " public static func "), functionPrefixRange.lowerBound == line.startIndex {
|
||||
let functionName: String
|
||||
if let bracketRange = line.range(of: "(") {
|
||||
functionName = String(line[functionPrefixRange.upperBound ..< bracketRange.lowerBound])
|
||||
} else {
|
||||
functionName = String(line[functionPrefixRange.upperBound...])
|
||||
}
|
||||
resultFunctions.append(QualifiedName(namespace: currentNamespace, value: functionName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (resultTypes, resultFunctions)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/// A custom parameter attribute that constructs parsers from closures. The constructed parser
|
||||
/// runs each parser in the closure, one after another, till one succeeds with an output.
|
||||
///
|
||||
/// The ``OneOf`` parser acts as an entry point into `@OneOfBuilder` syntax where you can list
|
||||
/// all of the parsers you want to try. For example, to parse a currency symbol into an enum
|
||||
/// of supported currencies:
|
||||
///
|
||||
/// ```swift
|
||||
/// enum Currency { case eur, gbp, usd }
|
||||
/// let currency = OneOf {
|
||||
/// "€".map { Currency.eur }
|
||||
/// "£".map { Currency.gbp }
|
||||
/// "$".map { Currency.usd }
|
||||
/// }
|
||||
/// let money = Parse { currency; Int.parser() }
|
||||
///
|
||||
/// try currency.parse("$100") // (.usd, 100)
|
||||
/// ```
|
||||
@resultBuilder
|
||||
public enum OneOfBuilder {
|
||||
/// Provides support for `for`-`in` loops in ``OneOfBuilder`` blocks.
|
||||
///
|
||||
/// Useful for building up a parser from a dynamic source, like for a case-iterable enum:
|
||||
///
|
||||
/// ```swift
|
||||
/// enum Role: String, CaseIterable {
|
||||
/// case admin
|
||||
/// case guest
|
||||
/// case member
|
||||
/// }
|
||||
///
|
||||
/// let roleParser = Parse {
|
||||
/// for role in Role.allCases {
|
||||
/// status.rawValue.map { role }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildArray<P>(_ parsers: [P]) -> Parsers.OneOfMany<P> {
|
||||
.init(parsers)
|
||||
}
|
||||
|
||||
/// Provides support for specifying a parser in ``OneOfBuilder`` blocks.
|
||||
@inlinable
|
||||
static public func buildBlock<P: Parser>(_ parser: P) -> P {
|
||||
parser
|
||||
}
|
||||
|
||||
/// Provides support for `if`-`else` statements in ``OneOfBuilder`` blocks, producing a
|
||||
/// conditional parser for the `if` branch.
|
||||
///
|
||||
/// ```swift
|
||||
/// OneOf {
|
||||
/// if useLegacyParser {
|
||||
/// LegacyParser()
|
||||
/// } else {
|
||||
/// NewParser()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildEither<TrueParser, FalseParser>(
|
||||
first parser: TrueParser
|
||||
) -> Parsers.Conditional<TrueParser, FalseParser> {
|
||||
.first(parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if`-`else` statements in ``OneOfBuilder`` blocks, producing a
|
||||
/// conditional parser for the `if` branch.
|
||||
///
|
||||
/// ```swift
|
||||
/// OneOf {
|
||||
/// if useLegacyParser {
|
||||
/// LegacyParser()
|
||||
/// } else {
|
||||
/// NewParser()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildEither<TrueParser, FalseParser>(
|
||||
second parser: FalseParser
|
||||
) -> Parsers.Conditional<TrueParser, FalseParser> {
|
||||
.second(parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if` statements in ``OneOfBuilder`` blocks, producing an optional parser.
|
||||
///
|
||||
/// ```swift
|
||||
/// let whitespace = OneOf {
|
||||
/// if shouldParseNewlines {
|
||||
/// "\r\n"
|
||||
/// "\r"
|
||||
/// "\n"
|
||||
/// }
|
||||
///
|
||||
/// " "
|
||||
/// "\t"
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildIf<P>(_ parser: P?) -> OptionalOneOf<P> {
|
||||
.init(wrapped: parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if #available` statements in ``OneOfBuilder`` blocks, producing an
|
||||
/// optional parser.
|
||||
@inlinable
|
||||
public static func buildLimitedAvailability<P>(_ parser: P?) -> OptionalOneOf<P> {
|
||||
.init(wrapped: parser)
|
||||
}
|
||||
|
||||
/// A parser that parses output from an optional parser.
|
||||
///
|
||||
/// You won't typically construct this parser directly, but instead will use standard `if`
|
||||
/// statements in a ``OneOfBuilder`` block to automatically build optional parsers:
|
||||
///
|
||||
/// ```swift
|
||||
/// let whitespace = OneOf {
|
||||
/// if shouldParseNewlines {
|
||||
/// "\r\n"
|
||||
/// "\r"
|
||||
/// "\n"
|
||||
/// }
|
||||
///
|
||||
/// " "
|
||||
/// "\t"
|
||||
/// }
|
||||
/// ```
|
||||
public struct OptionalOneOf<Wrapped: Parser>: Parser {
|
||||
@usableFromInline
|
||||
let wrapped: Wrapped?
|
||||
|
||||
@usableFromInline
|
||||
init(wrapped: Wrapped?) {
|
||||
self.wrapped = wrapped
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Wrapped.Input) throws -> Wrapped.Output {
|
||||
guard let wrapped = self.wrapped
|
||||
else { throw ParsingError.manyFailed([], at: input) }
|
||||
return try wrapped.parse(&input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/// A custom parameter attribute that constructs parsers from closures. The constructed parser
|
||||
/// runs a number of parsers, one after the other, and accumulates their outputs.
|
||||
///
|
||||
/// The ``Parse`` parser acts as an entry point into `@ParserBuilder` syntax, where you can list
|
||||
/// all of the parsers you want to run. For example, to parse two comma-separated integers:
|
||||
///
|
||||
/// ```swift
|
||||
/// try Parse {
|
||||
/// Int.parser()
|
||||
/// ","
|
||||
/// Int.parser()
|
||||
/// }
|
||||
/// .parse("123,456") // (123, 456)
|
||||
/// ```
|
||||
@resultBuilder
|
||||
public enum ParserBuilder {
|
||||
/// Provides support for specifying a parser in ``ParserBuilder`` blocks.
|
||||
@inlinable
|
||||
public static func buildBlock<P: Parser>(_ parser: P) -> P {
|
||||
parser
|
||||
}
|
||||
|
||||
/// Provides support for `if`-`else` statements in ``ParserBuilder`` blocks, producing a
|
||||
/// conditional parser for the `if` branch.
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// "Hello"
|
||||
/// if shouldParseComma {
|
||||
/// ", "
|
||||
/// } else {
|
||||
/// " "
|
||||
/// }
|
||||
/// Rest()
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildEither<TrueParser, FalseParser>(
|
||||
first parser: TrueParser
|
||||
) -> Parsers.Conditional<TrueParser, FalseParser> {
|
||||
.first(parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if`-`else` statements in ``ParserBuilder`` blocks, producing a
|
||||
/// conditional parser for the `else` branch.
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// "Hello"
|
||||
/// if shouldParseComma {
|
||||
/// ", "
|
||||
/// } else {
|
||||
/// " "
|
||||
/// }
|
||||
/// Rest()
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildEither<TrueParser, FalseParser>(
|
||||
second parser: FalseParser
|
||||
) -> Parsers.Conditional<TrueParser, FalseParser> {
|
||||
.second(parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if` statements in ``ParserBuilder`` blocks, producing an optional
|
||||
/// parser.
|
||||
@inlinable
|
||||
public static func buildIf<P: Parser>(_ parser: P?) -> P? {
|
||||
parser
|
||||
}
|
||||
|
||||
/// Provides support for `if` statements in ``ParserBuilder`` blocks, producing a void parser for
|
||||
/// a given void parser.
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// "Hello"
|
||||
/// if shouldParseComma {
|
||||
/// ","
|
||||
/// }
|
||||
/// " "
|
||||
/// Rest()
|
||||
/// }
|
||||
/// ```
|
||||
@inlinable
|
||||
public static func buildIf<P>(_ parser: P?) -> Parsers.OptionalVoid<P> {
|
||||
.init(wrapped: parser)
|
||||
}
|
||||
|
||||
/// Provides support for `if #available` statements in ``ParserBuilder`` blocks, producing an
|
||||
/// optional parser.
|
||||
@inlinable
|
||||
public static func buildLimitedAvailability<P: Parser>(_ parser: P?) -> P? {
|
||||
parser
|
||||
}
|
||||
|
||||
/// Provides support for `if #available` statements in ``ParserBuilder`` blocks, producing a void
|
||||
/// parser for a given void parser.
|
||||
@inlinable
|
||||
public static func buildLimitedAvailability<P>(_ parser: P?) -> Parsers.OptionalVoid<P> {
|
||||
.init(wrapped: parser)
|
||||
}
|
||||
}
|
||||
6317
build-system/SwiftTL/Sources/SwiftTL/Parser/Builders/Variadics.swift
Normal file
6317
build-system/SwiftTL/Sources/SwiftTL/Parser/Builders/Variadics.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,132 @@
|
|||
# Backtracking
|
||||
|
||||
Learn what backtracking is, how it affects the performance of your parsers, and how to avoid it when
|
||||
unnecessary.
|
||||
|
||||
## Overview
|
||||
|
||||
Backtracking is the process of restoring an input to its original value when parsing fails. While it
|
||||
can be very useful, backtracking can lead to more complicated parser logic than necessary, and
|
||||
backtracking too often can lead to performance issues. For this reason, most parsers are not
|
||||
required to backtrack, and can therefore fail _and_ still consume from the input.
|
||||
|
||||
The primary way to make use of backtracking in your parsers is through the ``OneOf`` parser, which
|
||||
tries many parsers on an input and chooses the first that succeeds. This allows you to try many
|
||||
parsers on the same input, regardless of how much each parser consumes:
|
||||
|
||||
```swift
|
||||
enum Currency { case eur, gbp, usd }
|
||||
|
||||
let currency = OneOf {
|
||||
"€".map { Currency.eur }
|
||||
"£".map { Currency.gbp }
|
||||
"$".map { Currency.usd }
|
||||
}
|
||||
```
|
||||
|
||||
## When to backtrack in your parsers?
|
||||
|
||||
If you only use the parsers and operators that ship with this library, and in particular you do not
|
||||
create custom conformances to the ``Parser`` protocol, then you never need to worry about explicitly
|
||||
backtracking your input because it will be handled for you automatically. The primary way to allow
|
||||
for backtracking is via the ``OneOf`` parser, but there are a few other parsers that also backtrack
|
||||
internally.
|
||||
|
||||
One such example is the ``Optionally`` parser, which transforms any parser into one that cannot fail
|
||||
by catching any thrown errors and returning `nil`:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
"Hello,"
|
||||
Optionally { " "; Bool.parser() }
|
||||
" world!"
|
||||
}
|
||||
|
||||
try parser.parse("Hello, world!") // nil
|
||||
try parser.parse("Hello, true world!") // true
|
||||
```
|
||||
|
||||
If the parser captured inside ``Optionally`` fails then it backtracks the input to its state before
|
||||
the parser ran. In particular, if the `Bool.parser()` fails then it will make sure to undo
|
||||
consuming the leading space " " so that later parsers can try.
|
||||
|
||||
Another example of a parser that internally backtracks is the ``Parser/replaceError(with:)``
|
||||
operator, which coalesces any error thrown by a parser into a default output value:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
"Hello,"
|
||||
Optionally { " "; Bool.parser() }
|
||||
.replaceError(with: false)
|
||||
" world!"
|
||||
}
|
||||
|
||||
try parser.parse("Hello, world!") // false
|
||||
try parser.parse("Hello, true world!") // true
|
||||
```
|
||||
|
||||
It backtracks the input to its original value when the parser fails so that later parsers can try.
|
||||
|
||||
The only time you need to worry about explicitly backtracking input is when making your own
|
||||
``Parser`` conformances. As a general rule of thumb, if your parser recovers from all failures
|
||||
in the `parse` method then it should backtrack the input to its state before the error was thrown.
|
||||
This is exactly how ``OneOf``, ``Optionally`` and ``Parser/replaceError(with:)`` work.
|
||||
|
||||
## Performance
|
||||
|
||||
If used naively, backtracking can lead to less performant parsing code. For example, if we wanted to
|
||||
parse two integers from a string that were separated by either a dash "-" or slash "/", then we
|
||||
could write this as:
|
||||
|
||||
```swift
|
||||
OneOf {
|
||||
Parse { Int.parser(); "-"; Int.parser() } // 1️⃣
|
||||
Parse { Int.parser(); "/"; Int.parser() } // 2️⃣
|
||||
}
|
||||
```
|
||||
|
||||
However, parsing slash-separated integers is not going to be performant because it will first run
|
||||
the entire 1️⃣ parser until it fails, then backtrack to the beginning, and run the 2️⃣ parser. In
|
||||
particular, the first integer will get parsed twice, unnecessarily repeating that work.
|
||||
|
||||
On the other hand, we can factor out the common work of the parser and localize the backtracking
|
||||
``OneOf`` work to make a much more performant parser:
|
||||
|
||||
```swift
|
||||
Parse {
|
||||
Int.parser()
|
||||
OneOf { "-"; "/" }
|
||||
Int.parser()
|
||||
}
|
||||
```
|
||||
|
||||
We can even write a benchmark to measure the performance difference:
|
||||
|
||||
```swift
|
||||
let first = OneOf {
|
||||
Parse { Int.parser(); "-"; Int.parser() }
|
||||
Parse { Int.parser(); "/"; Int.parser() }
|
||||
}
|
||||
benchmark("First") {
|
||||
precondition(try! first.parse("100/200") == (100, 200))
|
||||
}
|
||||
let second = Parse {
|
||||
Int.parser()
|
||||
OneOf { "-"; "/" }
|
||||
Int.parser()
|
||||
}
|
||||
benchmark("Second") {
|
||||
precondition(try! second.parse("100/200") == (100, 200))
|
||||
}
|
||||
```
|
||||
|
||||
Running this produces the following results:
|
||||
|
||||
```
|
||||
name time std iterations
|
||||
----------------------------------------
|
||||
First 1500.000 ns ± 19.75 % 856753
|
||||
Second 917.000 ns ± 15.89 % 1000000
|
||||
```
|
||||
|
||||
The second parser takes only 60% of the time to run that the first parser does.
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# Design
|
||||
|
||||
Learn how the library is designed, including its use of protocols, result builders and operators.
|
||||
|
||||
## Protocol
|
||||
|
||||
The design of the library is largely inspired by the Swift standard library and Apple's Combine
|
||||
framework. A parser is represented as a protocol that many types conform to, and then parser
|
||||
transformations (also known as "combinators") are methods that return concrete types conforming to
|
||||
the parser protocol.
|
||||
|
||||
For example, to parse all the characters from the beginning of a substring until you encounter a
|
||||
comma you can use the `Prefix` parser:
|
||||
|
||||
```swift
|
||||
let parser = Prefix { $0 != "," }
|
||||
|
||||
var input = "Hello,World"[...]
|
||||
try parser.parse(&input) // "Hello"
|
||||
input // ",World"
|
||||
```
|
||||
|
||||
The type of this parser is:
|
||||
|
||||
```swift
|
||||
Prefix<Substring>
|
||||
```
|
||||
|
||||
We can `.map` on this parser in order to transform its output, which in this case is the string
|
||||
"Hello":
|
||||
|
||||
```swift
|
||||
let parser = Prefix { $0 != "," }
|
||||
.map { $0 + "!!!" }
|
||||
|
||||
var input = "Hello,World"[...]
|
||||
try parser.parse(&input) // "Hello!!!"
|
||||
input // ",World"
|
||||
```
|
||||
|
||||
The type of this parser is now:
|
||||
|
||||
```swift
|
||||
Parsers.Map<Prefix<Substring>, Substring>
|
||||
```
|
||||
|
||||
Notice that the type of the parser encodes the operations that we performed. This adds a bit of
|
||||
complexity when using these types, but comes with some performance benefits because Swift can
|
||||
usually inline and optimize away the creation of those nested types.
|
||||
|
||||
## Result Builders
|
||||
|
||||
The library takes advantage of Swift's `@resultBuilder` feature to make constructing complex parsers
|
||||
as fluent as possible, and should be reminiscent of how views are constructed in SwiftUI. The main
|
||||
entry point into building a parser is the `Parse` builder:
|
||||
|
||||
```swift
|
||||
Parse {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In this builder block you can specify parsers that will be run one after another. For example, if
|
||||
you wanted to parse an integer, then a comma, and then a boolean from a string, you can simply do:
|
||||
|
||||
```swift
|
||||
Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `String` type conforms to the ``Parser`` protocol, and represents a parser that
|
||||
consumes that exact string from the beginning of an input if it matches, and otherwise fails.
|
||||
|
||||
Many of the parsers and operators that come with the library are configured with parser builders
|
||||
to maximize readability of the parsers. For example, to parse accounting syntax of numbers, where
|
||||
parenthesized numbers are negative, we can use the ``OneOf`` parser builder:
|
||||
|
||||
```swift
|
||||
let digits = Prefix { $0 >= "0" && $0 <= "9" }.compactMap(Int.init)
|
||||
|
||||
let accountingNumber = OneOf {
|
||||
digits
|
||||
|
||||
Parse {
|
||||
"("; digits; ")"
|
||||
}
|
||||
.map { -$0 }
|
||||
}
|
||||
|
||||
try accountingNumber.parse("100") // 100
|
||||
try accountingNumber.parse("(100)") // -100
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
Parser operators (also called "combinators") are methods defined on the ``Parser`` protocol that
|
||||
return a parser. For example, the ``Parser/map(_:)`` operator is a method that returns something
|
||||
called a ``Parsers/Map``:
|
||||
|
||||
```swift
|
||||
extension Parser {
|
||||
public func map<NewOutput>(
|
||||
_ transform: @escaping (Output) -> NewOutput
|
||||
) -> Parsers.Map<Self, NewOutput> {
|
||||
.init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And ``Parsers/Map`` is a dedicated type that implements the logic of the map operation. In
|
||||
particular, in runs the upstream parser and then transforms its output:
|
||||
|
||||
```swift
|
||||
extension Parsers {
|
||||
public struct Map<Upstream: Parser, NewOutput>: Parser {
|
||||
public let upstream: Upstream
|
||||
public let transform: (Upstream.Output) -> NewOutput
|
||||
|
||||
public func parse(_ input: inout Upstream.Input) rethrows -> NewOutput {
|
||||
self.transform(try self.upstream.parse(&input))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Types that conform to the ``Parser`` protocol but are not constructed directly, and instead are
|
||||
constructed via operators, are housed in the ``Parsers`` type. It's just an empty enum that
|
||||
serves as a namespace for such parsers.
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
# Error messages
|
||||
|
||||
Learn how the library reports parsing errors and how to integrate your own custom error messages
|
||||
into parsers.
|
||||
|
||||
## Overview
|
||||
|
||||
When a parser fails it throws an error containing information about what went wrong. The actual
|
||||
error thrown by the parsers shipped with this library is internal, and so it should be considered
|
||||
opaque. To get a human-readable debug description of the error message you can stringify the error.
|
||||
For example, the following `UInt8` parser fails to parse a string that would cause it to overflow:
|
||||
|
||||
```swift
|
||||
do {
|
||||
var input = "1234 Hello"[...]
|
||||
let number = try UInt8.parser().parse(&input))
|
||||
} catch {
|
||||
print(error)
|
||||
|
||||
// error: failed to process "UInt8"
|
||||
// --> input:1:1-4
|
||||
// 1 | 1234 Hello
|
||||
// | ^^^^ overflowed 255
|
||||
}
|
||||
```
|
||||
|
||||
When the ``OneOf`` parser is used and fails, there are multiple errors that can be shown. ``OneOf``
|
||||
prioritizes the error messages based on which parser got the furthest along. For example, consider
|
||||
a parser that can parse accounting style of numbers, i.e. plain numbers are considered positive
|
||||
and numbers in parentheses are considered negative:
|
||||
|
||||
```swift
|
||||
let digits = Prefix { $0 >= "0" && $0 <= "9" }.compactMap(Int.init)
|
||||
|
||||
let accountingNumber = OneOf {
|
||||
digits
|
||||
|
||||
Parse {
|
||||
"("; digits; ")"
|
||||
}
|
||||
.map { -$0 }
|
||||
}
|
||||
|
||||
try accountingNumber.parse("100") // 100
|
||||
try accountingNumber.parse("(100)") // -100
|
||||
```
|
||||
|
||||
If we try parsing something erroneous, such as "(100]", we get multiple error messages, but the
|
||||
second parser's error shows first since it was able to get the furthest:
|
||||
|
||||
```swift
|
||||
do {
|
||||
try accountingNumber.parse("(100]")
|
||||
} catch {
|
||||
print(error)
|
||||
|
||||
// error: multiple failures occurred
|
||||
//
|
||||
// error: unexpected input
|
||||
// --> input:1:5
|
||||
// 1 | (100]
|
||||
// | ^ expected ")"
|
||||
//
|
||||
// error: unexpected input
|
||||
// --> input:1:1
|
||||
// 1 | (100]
|
||||
// | ^ expected integer
|
||||
}
|
||||
```
|
||||
|
||||
## Improving error messages
|
||||
|
||||
The quality of error messages emitted by a parser can depend on the manner in which the parser was
|
||||
constructed. Some parser operators are powerful and convenient, but can cause the quality of error
|
||||
messaging to degrade.
|
||||
|
||||
For example, we could construct a parser that consumes a single uncommented line from an input
|
||||
(_i.e._, a line that does not begin with "//") by using ``Parser/compactMap(_:)`` to check the line
|
||||
for a prefix:
|
||||
|
||||
```swift
|
||||
let uncommentedLine = Prefix { $0 != "\n" }
|
||||
.compactMap { $0.starts(with: "//") ? nil : $0 }
|
||||
|
||||
try uncommentedLine.parse("// let x = 1")
|
||||
|
||||
// error: failed to process "Substring" from "// let x = 1"
|
||||
// --> input:1:1-12
|
||||
// 1 | // let x = 1
|
||||
// | ^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
However, when this parser fails it can only highlight the entire line as having a problem because
|
||||
it cannot know that the only thing that failed was that the first two characters were slashes.
|
||||
|
||||
We can rewrite this parser in a different, but equivalent, way by using the ``Not`` parser to first
|
||||
confirm that the line does not begin with "//", and then consume the entire line:
|
||||
|
||||
```swift
|
||||
let uncommentedLine = Parse {
|
||||
Not { "//" }
|
||||
Prefix { $0 != "\n" }
|
||||
}
|
||||
|
||||
try uncommentedLine.parse("// let x = 1")
|
||||
|
||||
// error: unexpected input
|
||||
// --> input:1:1-2
|
||||
// 1 | // let x = 1
|
||||
// | ^^ expected not to be processed
|
||||
```
|
||||
|
||||
This provides better error messaging because ``Not`` knows exactly what matched that we did not want
|
||||
to match, and so it can highlight those specific characters.
|
||||
|
||||
When using the `Many` parser you can improve error messaging by supplying a "terminator" parser,
|
||||
which is an optional argument. The terminator parser is run after the element and separator
|
||||
parsers have consumed as much as they can, and allows you to assert on exactly what is left
|
||||
afterwards.
|
||||
|
||||
For example, if a parser is run on an input that has a typo in the last row of data, and a
|
||||
terminator is not specified, the parser will succeed without consuming that last row and we won't
|
||||
know what went wrong:
|
||||
|
||||
```swift
|
||||
struct User {
|
||||
var id: Int
|
||||
var name: String
|
||||
var isAdmin: Bool
|
||||
}
|
||||
|
||||
let user = Parse(User.init(id:name:isAdmin:)) {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }.map(String.init)
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
|
||||
let users = Many {
|
||||
user
|
||||
} separator: {
|
||||
"\n"
|
||||
}
|
||||
|
||||
var input = """
|
||||
1,Blob,true
|
||||
2,Blob Jr.,false
|
||||
3,Blob Sr.,tru
|
||||
"""[...]
|
||||
|
||||
let output = try users.parse(&input)
|
||||
output.count // 2
|
||||
input // "\n3,Blob Sr.,tru"
|
||||
```
|
||||
|
||||
However, by adding a terminator to this `users` parser an error will be throw that points to the
|
||||
exact spot where the typo occurred:
|
||||
|
||||
```swift
|
||||
let users = Many {
|
||||
user
|
||||
} separator: {
|
||||
"\n"
|
||||
} terminator: {
|
||||
End()
|
||||
}
|
||||
|
||||
let output = try users.parse(&input)
|
||||
|
||||
// error: unexpected input
|
||||
// --> input:3:11
|
||||
// 3 | 3,Blob Jr,tru
|
||||
// | ^ expected "true" or "false"
|
||||
```
|
||||
|
||||
## Throwing your own errors
|
||||
|
||||
Although the error type thrown by the parsers that ship in this library is currently internal, and
|
||||
so should be thought of as opaque, it is still possible to throw your own errors. Your errors will
|
||||
automatically be reformatted and contextualized to show exactly where the error occurred.
|
||||
|
||||
For example, suppose we wanted a parser that only parsed the digits 0-9 from the beginning of a
|
||||
string and transformed it into an integer. This is subtly different from `Int.parser()` which
|
||||
supports negative numbers, exponential formatting, and more.
|
||||
|
||||
Constructing a `Digits` parser is easy enough, and we can introduce a custom struct error for
|
||||
customizing the message displayed:
|
||||
|
||||
```swift
|
||||
struct DigitsError: Error {
|
||||
let message = "Expected a prefix of digits 0-9"
|
||||
}
|
||||
|
||||
struct Digits: Parser {
|
||||
func parse(_ input: inout Substring) throws -> Int {
|
||||
let digits = input.prefix { $0 >= "0" && $0 <= "9" }
|
||||
guard let output = Int(digits)
|
||||
else {
|
||||
throw DigitsError()
|
||||
}
|
||||
input.removeFirst(digits.count)
|
||||
return output
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If we swap out the `Int.parser()` for a `Digits` parser in `user`:
|
||||
|
||||
```swift
|
||||
let user = Parse(User.init) {
|
||||
Digits()
|
||||
","
|
||||
Prefix { $0 != "," }.map(String.init)
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
```
|
||||
|
||||
And we introduce an incorrect value into the input:
|
||||
|
||||
```swift
|
||||
let input = """
|
||||
1,Blob,true
|
||||
-2,Blob Jr.,false
|
||||
3,Blob Sr.,true
|
||||
"""[...]
|
||||
```
|
||||
|
||||
Then when running the parser we get a nice error message that shows exactly what went wrong:
|
||||
|
||||
```swift
|
||||
try user.parse(&input)
|
||||
|
||||
// error: DigitsError(message: "Expected a prefix of digits 0-9")
|
||||
// --> input:2:1
|
||||
// 2 | -2,Blob Sr,false
|
||||
// | ^
|
||||
```
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
# Getting Started
|
||||
|
||||
Learn how to integrate Parsing into your project and write your first parser.
|
||||
|
||||
## Adding Parsing as a dependency
|
||||
|
||||
To use the Parsing library in a SwiftPM project, add it to the dependencies of your Package.swift
|
||||
and specify the `Parsing` product in any targets that need access to the library:
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.7.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "<target-name>",
|
||||
dependencies: [.product(name: "Parsing", package: "swift-parsing")]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Your first parser
|
||||
|
||||
Suppose you have a string that holds some user data that you want to parse into an array of `User`s:
|
||||
|
||||
```swift
|
||||
let input = """
|
||||
1,Blob,true
|
||||
2,Blob Jr.,false
|
||||
3,Blob Sr.,true
|
||||
"""
|
||||
|
||||
struct User {
|
||||
var id: Int
|
||||
var name: String
|
||||
var isAdmin: Bool
|
||||
}
|
||||
```
|
||||
|
||||
A naive approach to this would be a nested use of `.split(separator:)`, and then a little bit of
|
||||
extra work to convert strings into integers and booleans:
|
||||
|
||||
```swift
|
||||
let users = input
|
||||
.split(separator: "\n")
|
||||
.compactMap { row -> User? in
|
||||
let fields = row.split(separator: ",")
|
||||
guard
|
||||
fields.count == 3,
|
||||
let id = Int(fields[0]),
|
||||
let isAdmin = Bool(String(fields[2]))
|
||||
else { return nil }
|
||||
|
||||
return User(id: id, name: String(fields[1]), isAdmin: isAdmin)
|
||||
}
|
||||
```
|
||||
|
||||
Not only is this code a little messy, but it is also inefficient since we are allocating arrays for
|
||||
the `.split` and then just immediately throwing away those values.
|
||||
|
||||
It would be more straightforward and efficient to instead describe how to consume bits from the
|
||||
beginning of the input and convert that into users. This is what this parser library excels at 😄.
|
||||
|
||||
We can start by describing what it means to parse a single row, first by parsing an integer off the
|
||||
front of the string, and then parsing a comma. We can do this by using the ``Parse`` type, which acts
|
||||
as an entry point into describing a list of parsers that you want to run one after the other to
|
||||
consume from an input:
|
||||
|
||||
```swift
|
||||
let user = Parse {
|
||||
Int.parser()
|
||||
","
|
||||
}
|
||||
```
|
||||
|
||||
Already this can consume the leading integer and comma from the beginning of the input:
|
||||
|
||||
```swift
|
||||
// Use a mutable substring to verify what is consumed
|
||||
var input = input[...]
|
||||
|
||||
try user.parse(&input) // 1
|
||||
input // "Blob,true\n2,Blob Jr.,false\n3,Blob Sr.,true"
|
||||
```
|
||||
|
||||
Next we want to take everything up until the next comma for the user's name, and then consume the
|
||||
comma:
|
||||
|
||||
```swift
|
||||
let user = Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }
|
||||
","
|
||||
}
|
||||
```
|
||||
|
||||
And then we want to take the boolean at the end of the row for the user's admin status:
|
||||
|
||||
```swift
|
||||
let user = Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
```
|
||||
|
||||
Currently this will parse a tuple `(Int, Substring, Bool)` from the input, and we can `.map` on
|
||||
that to turn it into a `User`:
|
||||
|
||||
```swift
|
||||
let user = Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
.map { User(id: $0, name: String($1), isAdmin: $2) }
|
||||
```
|
||||
|
||||
To make the data we are parsing to more prominent, we can instead pass the transform closure as the
|
||||
first argument to `Parse`:
|
||||
|
||||
```swift
|
||||
let user = Parse {
|
||||
User(id: $0, name: String($1), isAdmin: $2)
|
||||
} with: {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
```
|
||||
|
||||
Or we can pass the `User` initializer to `Parse` in a point-free style by first transforming the
|
||||
`Prefix` parser's output from a `Substring` to a `String`:
|
||||
|
||||
```swift
|
||||
let user = Parse(User.init(id:name:isAdmin:)) {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }.map(String.init)
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
```
|
||||
|
||||
That is enough to parse a single user from the input string, leaving behind a newline and the final
|
||||
two users:
|
||||
|
||||
```swift
|
||||
try user.parse(&input) // User(id: 1, name: "Blob", isAdmin: true)
|
||||
input // "\n2,Blob Jr.,false\n3,Blob Sr.,true"
|
||||
```
|
||||
|
||||
To parse multiple users from the input we can use the `Many` parser to run the user parser many
|
||||
times:
|
||||
|
||||
```swift
|
||||
let users = Many {
|
||||
user
|
||||
} separator: {
|
||||
"\n"
|
||||
}
|
||||
|
||||
try users.parse(&input) // [User(id: 1, name: "Blob", isAdmin: true), ...]
|
||||
input // ""
|
||||
```
|
||||
|
||||
Now this parser can process an entire document of users, and the code is simpler and more
|
||||
straightforward than the version that uses `.split` and `.compactMap`.
|
||||
|
||||
Even better, it's more performant. We've written [benchmarks][benchmarks-readme] for these two
|
||||
styles of parsing, and the `.split`-style of parsing is more than twice as slow:
|
||||
|
||||
```
|
||||
name time std iterations
|
||||
------------------------------------------------------------------
|
||||
README Example.Parser: Substring 3426.000 ns ± 63.40 % 385395
|
||||
README Example.Ad hoc 7631.000 ns ± 47.01 % 169332
|
||||
```
|
||||
|
||||
Further, if you are willing write your parsers against `UTF8View` instead of `Substring`, you can
|
||||
eke out even more performance, more than doubling the speed:
|
||||
|
||||
```
|
||||
name time std iterations
|
||||
------------------------------------------------------------------
|
||||
README Example.Parser: Substring 3693.000 ns ± 81.76 % 349763
|
||||
README Example.Parser: UTF8 1272.000 ns ± 128.16 % 999150
|
||||
README Example.Ad hoc 8504.000 ns ± 59.59 % 151417
|
||||
```
|
||||
|
||||
See the article <doc:StringAbstractions> for more info on how to write parsers against different
|
||||
string abstraction levels.
|
||||
|
||||
We can also compare these times to a tool that Apple's Foundation gives us: `Scanner`. It's a type
|
||||
that allows you to consume from the beginning of strings in order to produce values, and provides
|
||||
a nicer API than using `.split`:
|
||||
|
||||
```swift
|
||||
var users: [User] = []
|
||||
while scanner.currentIndex != input.endIndex {
|
||||
guard
|
||||
let id = scanner.scanInt(),
|
||||
let _ = scanner.scanString(","),
|
||||
let name = scanner.scanUpToString(","),
|
||||
let _ = scanner.scanString(","),
|
||||
let isAdmin = scanner.scanBool()
|
||||
else { break }
|
||||
|
||||
users.append(User(id: id, name: name, isAdmin: isAdmin))
|
||||
_ = scanner.scanString("\n")
|
||||
}
|
||||
```
|
||||
|
||||
However, the `Scanner` style of parsing is more than 5 times as slow as the substring parser written
|
||||
written above, and more than 15 times slower than the UTF-8 parser:
|
||||
|
||||
```
|
||||
name time std iterations
|
||||
-------------------------------------------------------------------
|
||||
README Example.Parser: Substring 3481.000 ns ± 65.04 % 376525
|
||||
README Example.Parser: UTF8 1207.000 ns ± 110.96 % 1000000
|
||||
README Example.Ad hoc 8029.000 ns ± 44.44 % 163719
|
||||
README Example.Scanner 19786.000 ns ± 35.26 % 62125
|
||||
```
|
||||
|
||||
Not only are parsers built with the library more succinct and many times more performant than ad hoc
|
||||
parsers, but they can also be easier to evolve to accommodate more features. For example, right now
|
||||
our parser does not work correctly when the user's name contains a comma, such as "Blob, Esq.":
|
||||
|
||||
```swift
|
||||
try user.parse("1,Blob, Esq.,true")
|
||||
|
||||
// error: unexpected input
|
||||
// --> input:1:8
|
||||
// 1 | 1,Blob, Esq.,true
|
||||
// | ^ expected "true" or "false"
|
||||
```
|
||||
|
||||
The problem is that we are using the comma as a reserved identifier for delineating between fields,
|
||||
and so a field cannot contain a comma. We can enhance the CSV format to allow for quoting fields
|
||||
so that they can contain quotes:
|
||||
|
||||
```
|
||||
1,"Blob, Esq.",true
|
||||
```
|
||||
|
||||
To parse quoted fields we can first try parsing a quote, then everything up to the next quote, and
|
||||
then the trailing quote:
|
||||
|
||||
```swift
|
||||
let quotedField = Parse {
|
||||
"\""
|
||||
Prefix { $0 != "\"" }
|
||||
"\""
|
||||
}
|
||||
```
|
||||
|
||||
And then to parse a field, in general, we can first try parsing a quoted field, and if that fails we
|
||||
will just take everything until the next comma. We can do this using the ``OneOf`` parser, which
|
||||
allows us to run multiple parsers on the same input, and it will take the first that succeeds:
|
||||
|
||||
```swift
|
||||
let field = OneOf {
|
||||
quotedField
|
||||
Prefix { $0 != "," }
|
||||
}
|
||||
.map(String.init)
|
||||
```
|
||||
|
||||
We can use this parser in the `user` parser, and now it properly handles quoted and non-quoted
|
||||
fields:
|
||||
|
||||
```swift
|
||||
let user = Parse(User.init) {
|
||||
Int.parser()
|
||||
","
|
||||
field
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
|
||||
try user.parse("1,\"Blob, Esq.\",true") // User(id: 1, name: "Blob, Esq.", admin: true)
|
||||
```
|
||||
|
||||
It was quite straightforward to improve the `user` parser to handle quoted fields. Doing the same
|
||||
with our ad hoc `split`/`compactMap` parser, and even the `Scanner`-based parser, would be a lot
|
||||
more difficult.
|
||||
|
||||
That's the basics of parsing a simple string format, but there's a lot more operators and tricks to
|
||||
learn in order to performantly parse larger inputs. View the [benchmarks][benchmarks] for examples
|
||||
of real-life parsing scenarios.
|
||||
|
||||
[benchmarks-readme]: https://github.com/pointfreeco/swift-parsing/blob/main/Sources/swift-parsing-benchmark/ReadmeExample.swift
|
||||
[benchmarks]: https://github.com/pointfreeco/swift-parsing/tree/main/Sources/swift-parsing-benchmark
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Bool
|
||||
|
||||
A parser that consumes a Boolean value from the beginning of a string.
|
||||
|
||||
This parser only recognizes the literal `"true"` and `"false"` sequence of characters:
|
||||
|
||||
```swift
|
||||
// Parses "true":
|
||||
var input = "true Hello"[...]
|
||||
try Bool.parser().parse(&input) // true
|
||||
input // " Hello"
|
||||
|
||||
// Parses "false":
|
||||
input = "false Hello"[...]
|
||||
try Bool.parser().parse(&input) // false
|
||||
input // " Hello"
|
||||
|
||||
// Otherwise fails:
|
||||
input = "1 Hello"[...]
|
||||
try Bool.parser().parse(&input)
|
||||
|
||||
// error: unexpected input
|
||||
// --> input:1:1
|
||||
// 1 | 1 Hello
|
||||
// ^ expected "true" or "false"
|
||||
```
|
||||
|
||||
The `Bool.parser()` method is overloaded to work on a variety of string representations in order
|
||||
to be as efficient as possible, including `Substring`, `UTF8View`, and more general collections of
|
||||
UTF-8 code units (see <doc:StringAbstractions> for more info).
|
||||
|
||||
Typically Swift can choose the correct overload by using type inference based on what other parsers
|
||||
you are combining `Bool.parser()` with. For example, if you use `Bool.parser()` with a
|
||||
`Substring` parser, say the literal `","` parser (see <doc:String> for more information), Swift
|
||||
will choose the overload that works on substrings:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Bool.parser()
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
|
||||
try parser.parse("true,false") // (true, false)
|
||||
```
|
||||
|
||||
On the other hand, if `Bool.parser()` is used in a context where the input type cannot be inferred,
|
||||
then you will get an compiler error:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Bool.parser()
|
||||
Bool.parser() // 🛑 Ambiguous use of 'parser(of:)'
|
||||
}
|
||||
|
||||
try parser.parse("truefalse")
|
||||
```
|
||||
|
||||
To fix this you can force one of the boolean parsers to be the `Substring` parser, and then the
|
||||
other will figure it out via type inference:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Bool.parser(of: Substring.self)
|
||||
Bool.parser() // ✅
|
||||
}
|
||||
|
||||
try parser.parse("truefalse")
|
||||
```
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
# CaseIterable
|
||||
|
||||
A parser that consumes a case-iterable, raw representable value from the beginning of a string.
|
||||
|
||||
Given a type that conforms to `CaseIterable` and `RawRepresentable` with a `RawValue` of `String`
|
||||
or `Int`, we can incrementally parse a value of it.
|
||||
|
||||
Notably, raw enumerations that conform to `CaseIterable` meet this criteria, so cases of the
|
||||
following type can be parsed with no extra work:
|
||||
|
||||
```swift
|
||||
enum Role: String, CaseIterable {
|
||||
case admin
|
||||
case guest
|
||||
case member
|
||||
}
|
||||
|
||||
try Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Role.parser()
|
||||
}
|
||||
.parse("123,member") // (123, .member)
|
||||
```
|
||||
|
||||
This also works with raw enumerations that are backed by integers:
|
||||
|
||||
```swift
|
||||
enum Role: Int, CaseIterable {
|
||||
case admin = 1
|
||||
case guest = 2
|
||||
case member = 3
|
||||
}
|
||||
|
||||
try Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Role.parser()
|
||||
}
|
||||
.parse("123,1") // (123, .admin)
|
||||
```
|
||||
|
||||
The `parser()` method on `CaseIterable` is overloaded to work on a variety of string representations
|
||||
in order to be as efficient as possible, including `Substring`, `UTF8View`, and more general
|
||||
collections of UTF-8 code units (see <doc:StringAbstractions> for more info).
|
||||
|
||||
Typically Swift can choose the correct overload by using type inference based on what other parsers
|
||||
you are combining `parser()` with. For example, if you use `Role.parser()` with a
|
||||
`Substring` parser, like the literal "," parser in the above examples, Swift
|
||||
will choose the overload that works on substrings.
|
||||
|
||||
On the other hand, if `Role.parser()` is used in a context where the input type cannot be inferred,
|
||||
then you will get an compiler error:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Int.parser()
|
||||
Role.parser() // 🛑 Ambiguous use of 'parser(of:)'
|
||||
}
|
||||
|
||||
try parser.parse("123member")
|
||||
```
|
||||
|
||||
To fix this you can force one of the parsers to be the `Substring` parser, and then the
|
||||
other will figure it out via type inference:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Int.parser(of: Substring.self)
|
||||
Role.parser()
|
||||
}
|
||||
|
||||
try parser.parse("123member") // (123, .member)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# CharacterSet
|
||||
|
||||
A parser that consumes the characters contained in a `CharacterSet` from the beginning of a string.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
Parse {
|
||||
CharacterSet.alphanumerics
|
||||
CharacterSet.punctuationCharacters
|
||||
CharacterSet.alphanumerics
|
||||
}
|
||||
.parse("Hello...World") // ("Hello", "...", "World")
|
||||
```
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Float
|
||||
|
||||
A parser that consumes a floating-point number from the beginning of a string.
|
||||
|
||||
Supports any type that conforms to `BinaryFloatingPoint` and `LosslessStringConvertible`. This
|
||||
includes `Double`, `Float`, `Float16`, and `Float80`.
|
||||
|
||||
Parses the same format parsed by `LosslessStringConvertible.init(_:)` on `BinaryFloatingPoint`.
|
||||
|
||||
```swift
|
||||
var input = "123.45 Hello world"[...]
|
||||
try Double.parser().parse(&input) // 123.45
|
||||
input // " Hello world"
|
||||
|
||||
input = "-123. Hello world"[...]
|
||||
try Double.parser().parse(&input) // -123.0
|
||||
input // " Hello world"
|
||||
|
||||
|
||||
input = "123.123E+2 Hello world"[...]
|
||||
try Double.parser().parse(&input) // 12312.3
|
||||
input // " Hello world"
|
||||
```
|
||||
|
||||
The `parser()` static method is overloaded to work on a variety of string representations in order
|
||||
to be as efficient as possible, including `Substring`, `UTF8View`, and generally collections of
|
||||
UTF-8 code units (see <doc:StringAbstractions> for more info).
|
||||
|
||||
Typically Swift can choose the correct overload by using type inference based on what other parsers
|
||||
you are combining `parser()` with. For example, if you use `Double.parser()` with a `Substring`
|
||||
parser, say the literal `","` parser (see <doc:String> for more information), Swift will choose the
|
||||
overload that works on substrings:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Double.parser()
|
||||
","
|
||||
Double.parser()
|
||||
}
|
||||
|
||||
try parser.parse("1,-2") // (1.0, -2.0)
|
||||
```
|
||||
|
||||
On the other hand, if `Double.parser()` is used in a context where the input type cannot be
|
||||
inferred, then you will get an compiler error:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Double.parser()
|
||||
Double.parser() // 🛑 Ambiguous use of 'parser(of:)'
|
||||
}
|
||||
|
||||
try parser.parse(".1.2")
|
||||
```
|
||||
|
||||
To fix this you can force one of the double parsers to be the `Substring` parser, and then the
|
||||
other will figure it out via type inference:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Double.parser(of: Substring.self)
|
||||
Double.parser() // ✅
|
||||
}
|
||||
|
||||
try parser.parse(".1.2") // (0.1, 0.2)
|
||||
```
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# Int
|
||||
|
||||
A parser that consumes an integer from the beginning of a string.
|
||||
|
||||
Supports any type that conforms to `FixedWidthInteger`. This includes `Int`, `UInt`, `UInt8`, and
|
||||
many more.
|
||||
|
||||
Parses the same format parsed by `FixedWidthInteger.init(_:radix:)`.
|
||||
|
||||
```swift
|
||||
var input = "123 Hello world"[...]
|
||||
try Int.parser().parse(&input) // 123
|
||||
input // " Hello world"
|
||||
|
||||
input = "-123 Hello world"
|
||||
try Int.parser().parse(&input) // -123
|
||||
input // " Hello world"
|
||||
```
|
||||
|
||||
This parser fails when it does not find an integer at the beginning of the collection:
|
||||
|
||||
```swift
|
||||
var input = "+Hello"[...]
|
||||
let number = try Int.parser().parse(&input)
|
||||
// error: unexpected input
|
||||
// --> input:1:2
|
||||
// 1 | +Hello
|
||||
// | ^ expected integer
|
||||
```
|
||||
|
||||
And it fails when the digits extracted from the beginning of the collection would cause the
|
||||
integer type to overflow:
|
||||
|
||||
```swift
|
||||
var input = "9999999999999999999 Hello"[...]
|
||||
let number = try Int.parser().parse(&input)
|
||||
// error: failed to process "Int"
|
||||
// --> input:1:1-19
|
||||
// 1 | 9999999999999999999 Hello
|
||||
// | ^^^^^^^^^^^^^^^^^^^ overflowed 9223372036854775807
|
||||
```
|
||||
|
||||
The static `parser()` method is overloaded to work on a variety of string representations in order
|
||||
to be as efficient as possible, including `Substring`, `UTF8View`, and generally collections of
|
||||
UTF-8 code units (see <doc:StringAbstractions> for more info).
|
||||
|
||||
Typically Swift can choose the correct overload by using type inference based on what other parsers
|
||||
you are combining `parser()` with. For example, if you use `Int.parser()` with a `Substring` parser,
|
||||
say the literal `","` parser (see <doc:String> for more information), Swift will choose the overload
|
||||
that works on substrings:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Int.parser()
|
||||
}
|
||||
|
||||
try parser.parse("123,456") // (123, 456)
|
||||
```
|
||||
|
||||
On the other hand, if `Int.parser()` is used in a context where the input type cannot be inferred,
|
||||
then you will get an compiler error:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Int.parser() // 🛑 Ambiguous use of 'parser(of:radix:)'
|
||||
Bool.parser()
|
||||
}
|
||||
|
||||
try parser.parse("123true")
|
||||
```
|
||||
|
||||
To fix this you can force one of the parsers to be the `Substring` parser, and then the
|
||||
other will figure it out via type inference:
|
||||
|
||||
```swift
|
||||
let parser = Parse {
|
||||
Int.parser() // ✅
|
||||
Bool.parser(of: Substring.self)
|
||||
}
|
||||
|
||||
try parser.parse("123true") // (123, true)
|
||||
```
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# String
|
||||
|
||||
A parser that consumes a string literal from the beginning of a string.
|
||||
|
||||
Many of Swift's string types conform to the ``Parser`` protocol, which allows you to use string types
|
||||
directly in a parser. For example, to parse two integers separated by a comma we can do:
|
||||
|
||||
```swift
|
||||
try Parse {
|
||||
Int.parser()
|
||||
","
|
||||
Int.parser()
|
||||
}
|
||||
.parse("123,456") // (123, 456)
|
||||
```
|
||||
|
||||
The string `","` acts as a parser that consumes a comma from the beginning of an input and fails
|
||||
if the input does not start with a comma.
|
||||
|
||||
Swift's other string representations also conform to ``Parser``, such as `UnicodeScalarView`
|
||||
and `UTF8View`. This allows you to consume strings from the beginning of an input in a more
|
||||
efficient manner than is possible with `Substring` (see <doc:StringAbstractions> for more info).
|
||||
|
||||
For example, we can conver the above parser to work on the level of `UTF8View`s, which is a
|
||||
collection of UTF-8 code units:
|
||||
|
||||
```swift
|
||||
try Parse {
|
||||
Int.parser()
|
||||
",".utf8
|
||||
Int.parser()
|
||||
}
|
||||
.parse("123,456") // (123, 456)
|
||||
```
|
||||
|
||||
Here `",".utf8` is a `String.UTF8View`, which conforms to the ``Parser`` protocol. Also, by type
|
||||
inference, Swift is choosing the overload of `Int.parser()` that now works on `UTF8View`s rather
|
||||
than `Substring`s. See <doc:Int> for more info.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# UUID
|
||||
|
||||
A parser that consumes a `UUID` value from the beginning of a string.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
try Parse {
|
||||
UUID.parser()
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
.parse("deadbeef-dead-beef-dead-beefdeadbeef,true")
|
||||
// (DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF, true)
|
||||
```
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
# String Abstractions
|
||||
|
||||
Learn how to write parsers on different levels of string abstractions, giving you the ability to
|
||||
trade performance for correctness where needed.
|
||||
|
||||
## Levels of abstraction
|
||||
|
||||
The parsers in the library do not work on `String`s directly, and instead operate on _views_ into a
|
||||
string, such as `Substring`, `UnicodeScalarView` and `UTF8View`. Each of these types represents a
|
||||
particular kind of "view" into some subset of a string, which means they are cheap to copy around,
|
||||
and it makes consuming elements from the beginning and end of the string very efficient since only
|
||||
their start and end index need to be mutated to point to different parts of the string.
|
||||
|
||||
However, there are tradeoffs to using each type:
|
||||
|
||||
* `Substring`, like `String`, is a collection of `Character`s, which are extended grapheme
|
||||
clusters that most closely represents a single visual character one can see on the screen. This
|
||||
type is easy to use and hides a lot of the complexities of UTF8 from you (such as multiple byte
|
||||
sequences that represent the same visual character), and as such it is less efficient to use.
|
||||
Its elements are variable width, which means scanning its elements is an O(n) operation.
|
||||
|
||||
* `UnicodeScalarView` is a collection of unicode scalars represented by the `Unicode.Scalar` type.
|
||||
Unicode scalars are 21-bit, and so not variable width like `Substring`, which makes scanning
|
||||
`UnicodeScalarView`s more efficient, but at the cost of some additional complexity in the API.
|
||||
|
||||
For example, complex elements that can be represented by a single `Character`, such as "🇺🇸",
|
||||
are represented by multiple `Unicode.Scalar` elements, "🇺" and "🇸". When put together they
|
||||
form the single extended grapheme cluster of the flag character.
|
||||
|
||||
Further, some `Character`s have multiple representations as collections of unicode scalars. For
|
||||
example, an "e" with an accute accent only has one visual representation, yet there are two
|
||||
different sequences of unicode scalars that can represent that character:
|
||||
|
||||
```swift
|
||||
Array("é".unicodeScalars) // [233]
|
||||
Array("é".unicodeScalars) // [101, 769]
|
||||
```
|
||||
|
||||
You can't tell from looking at the character, but the first "é" is a single unicode scalar
|
||||
called a "LATIN SMALL LETTER E WITH ACUTE" and the second "é" is two scalars, one just a plain
|
||||
"e" and the second a "COMBINING ACUTE ACCENT". Importantly, these two accented e's are equal as
|
||||
`Character`s but unequal as `UnicodeScalarView`s:
|
||||
|
||||
```swift
|
||||
let e1 = "\u{00E9}"
|
||||
let e2 = "e\u{0301}"
|
||||
e1 == e2 // true
|
||||
e1.unicodeScalars.elementsEqual(e2.unicodeScalars) // false
|
||||
```
|
||||
|
||||
So, when parsing on the level of `UnicodeScalarView` you have to be aware of these subtleties in
|
||||
order to form a correct parser.
|
||||
|
||||
* `UTF8View` is a collection of `Unicode.UTF8.CodeUnit`s, which is just a typealias for `UInt8`,
|
||||
_i.e._, a single byte. This is an even lower-level representation of strings than
|
||||
`UnicodeScalarView`, and scanning these collections is quite efficient, but at the cost of even
|
||||
more complexity.
|
||||
|
||||
For example, the non-ASCII characters described above have an even more complex representation
|
||||
has UTF8 bytes:
|
||||
|
||||
```swift
|
||||
Array("é".utf8) // [195, 169]
|
||||
Array("é".utf8) // [101, 204, 129]
|
||||
Array("🇺🇸".utf8) // [240, 159, 135, 186, 240, 159, 135, 184]
|
||||
```
|
||||
|
||||
* There's even `ArraySlice<UInt8>`, which is just a raw collection of bytes. This can be even more
|
||||
efficient to parse than `UTF8View` because it does not require representing a valid UTF-8
|
||||
string, but then you have no guarantees that you can losslessly convert it back into a `String`.
|
||||
|
||||
## Mixing and matching abstraction levels
|
||||
|
||||
It is possible to plug together parsers that work on different abstraction levels so that you can
|
||||
decide where you want to trade correctness for performance and vice-versa.
|
||||
|
||||
For example, suppose you have an enum representing a few cities that you want to parse a string
|
||||
into:
|
||||
|
||||
```swift
|
||||
enum City {
|
||||
case losAngeles
|
||||
case newYork
|
||||
case sanJose
|
||||
}
|
||||
|
||||
let city = OneOf {
|
||||
"Los Angeles".map { City.losAngeles }
|
||||
"New York".map { City.newYork }
|
||||
"San José".map { City.sanJose }
|
||||
}
|
||||
```
|
||||
|
||||
For the most part this parser could work on the level of UTF-8 because it is mostly dealing with
|
||||
plain ASCII characters for which there are not multiple ways of representing the same visual
|
||||
character. The only exception is "San José", which has an accented "e" that can be represented
|
||||
by two different sequences of bytes.
|
||||
|
||||
The `Substring` abstraction is hiding those details from us because this parser will happily parse
|
||||
both representations of "San José" from a string:
|
||||
|
||||
```swift
|
||||
city.parse("San Jos\u{00E9}") // ✅
|
||||
city.parse("San Jose\u{0301}") // ✅
|
||||
```
|
||||
|
||||
But, if we naively convert this parser to work on the level of `UTF8View`:
|
||||
|
||||
```swift
|
||||
let city = OneOf {
|
||||
"Los Angeles".utf8.map { City.losAngeles }
|
||||
"New York".utf8.map { City.newYork }
|
||||
"San José".utf8.map { City.sanJose }
|
||||
}
|
||||
```
|
||||
|
||||
We have accidentally introduced a bug into the parser in which it recognizes one version of
|
||||
"San José", but not the other:
|
||||
|
||||
```swift
|
||||
city.parse("San Jos\u{00E9}".utf8) // ✅
|
||||
city.parse("San Jose\u{0301}".utf8) // ❌
|
||||
```
|
||||
|
||||
One way to fix this would be to add another case to the `OneOf` for this alternate representation
|
||||
of "San José":
|
||||
|
||||
```swift
|
||||
let city = OneOf {
|
||||
"Los Angeles".utf8.map { City.losAngeles }
|
||||
"New York".utf8.map { City.newYork }
|
||||
"San Jos\u{00E9}".utf8.map { City.sanJose }
|
||||
"San Jose\u{0301}".utf8.map { City.sanJose }
|
||||
}
|
||||
|
||||
city.parse("San Jos\u{00E9}".utf8) // ✅
|
||||
city.parse("San Jose\u{0301}".utf8) // ✅
|
||||
```
|
||||
|
||||
This does work, but you are now responsible for understanding the ins and outs of UTF-8
|
||||
normalization. UTF-8 is incredibly complex and Swift does a lot of work to hide that complexity
|
||||
from you.
|
||||
|
||||
However, there's no need to parse everything on the level of `Substring` just because this one
|
||||
parser needs to. We can parse everything on the level of `UTF8View` and then parse just "San José"
|
||||
on the level of `Substring`. We do this by using the ``FromSubstring`` parser, which allows us to
|
||||
temporarily leave the `UTF8View` world to work in the `Substring` world:
|
||||
|
||||
```swift
|
||||
let city = OneOf {
|
||||
"Los Angeles".utf8.map { City.losAngeles }
|
||||
"New York".utf8.map { City.newYork }
|
||||
FromSubstring { "San José" }.map { City.sanJose }
|
||||
}
|
||||
|
||||
city.parse("San Jos\u{00E9}".utf8) // ✅
|
||||
city.parse("San Jose\u{0301}".utf8) // ✅
|
||||
```
|
||||
|
||||
This will run the "San José" parser on the level of `Substring`, meaning it will handle all the
|
||||
complexities of UTF8 normalization so that we don't have to think about it.
|
||||
|
||||
If we want to be _really_ pedantic we can even decide to parse only the "é" character on the
|
||||
level of `Substring` and leave everything else to `UTF8View`:
|
||||
|
||||
```swift
|
||||
let city = OneOf {
|
||||
"Los Angeles".utf8.map { City.losAngeles }
|
||||
"New York".utf8.map { City.newYork }
|
||||
Parse {
|
||||
"San Jos".utf8
|
||||
FromSubstring { "é" }
|
||||
}
|
||||
.map { City.sanJose }
|
||||
}
|
||||
|
||||
city.parse("San Jos\u{00E9}".utf8) // ✅
|
||||
city.parse("San Jose\u{0301}".utf8) // ✅
|
||||
```
|
||||
|
||||
We don't necessarily recommend being this pedantic in general, at least not without benchmarking to
|
||||
make sure it is worth it. But it does demonstrate how you can be very precise with which abstraction
|
||||
levels you want to work on.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# ``Parsing/OneOf``
|
||||
|
||||
## Topics
|
||||
|
||||
### Builder
|
||||
|
||||
- ``OneOfBuilder``
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# ``Parsing/Parse``
|
||||
|
||||
## Topics
|
||||
|
||||
### Builder
|
||||
|
||||
- ``ParserBuilder``
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# ``Parsing/Parser``
|
||||
|
||||
## Topics
|
||||
|
||||
### Diving deeper
|
||||
|
||||
* <doc:GettingStarted>
|
||||
* <doc:Design>
|
||||
* <doc:StringAbstractions>
|
||||
* <doc:ErrorMessages>
|
||||
* <doc:Backtracking>
|
||||
|
||||
### Running a parser
|
||||
|
||||
All of the ways to run a parser on an input.
|
||||
|
||||
- ``parse(_:)-717qw``
|
||||
- ``parse(_:)-6h1d0``
|
||||
- ``parse(_:)-2wzcq``
|
||||
|
||||
### Common parsers
|
||||
|
||||
Some of the most commonly used parsers in the library. Use these parsers with operators in order
|
||||
to build complex parsers from simpler pieces.
|
||||
|
||||
- <doc:Int>
|
||||
- <doc:String>
|
||||
- <doc:Bool>
|
||||
- <doc:Float>
|
||||
- <doc:CharacterSet>
|
||||
- <doc:UUID>
|
||||
- <doc:CaseIterable>
|
||||
- ``Parse``
|
||||
- ``OneOf``
|
||||
- ``Many``
|
||||
- ``Prefix``
|
||||
- ``PrefixThrough``
|
||||
- ``PrefixUpTo``
|
||||
- ``Optionally``
|
||||
- ``Always``
|
||||
- ``End``
|
||||
- ``Rest``
|
||||
- ``Fail``
|
||||
- ``FromSubstring``
|
||||
- ``FromUTF8View``
|
||||
- ``FromUnicodeScalarView``
|
||||
- ``First``
|
||||
- ``Skip``
|
||||
- ``Lazy``
|
||||
- ``Newline``
|
||||
- ``Whitespace``
|
||||
- ``AnyParser``
|
||||
- ``Peek``
|
||||
- ``Not``
|
||||
- ``StartsWith``
|
||||
- ``Stream``
|
||||
|
||||
### Parser operators
|
||||
|
||||
- ``map(_:)``
|
||||
- ``flatMap(_:)``
|
||||
- ``compactMap(_:)``
|
||||
- ``filter(_:)``
|
||||
- ``pipe(_:)``
|
||||
- ``pullback(_:)``
|
||||
- ``replaceError(with:)``
|
||||
- ``eraseToAnyParser()``
|
||||
- ``pipe(_:)``
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# ``Parsing``
|
||||
|
||||
A library for turning nebulous data into well-structured data, with a focus on composition,
|
||||
performance, generality, and ergonomics.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [GitHub Repo](https://github.com/pointfreeco/swift-parsing/)
|
||||
- [Discussions](https://github.com/pointfreeco/swift-parsing/discussions)
|
||||
- [Point-Free Videos](https://www.pointfree.co/collections/parsing)
|
||||
|
||||
## Overview
|
||||
|
||||
Parsing with this library is performed by listing out many small parsers that describe how to
|
||||
incrementally consume small bits from the beginning of an input string. For example, suppose you
|
||||
have a string that holds some user data that you want to parse into an array of `User`s:
|
||||
|
||||
```swift
|
||||
var input = """
|
||||
1,Blob,true
|
||||
2,Blob Jr.,false
|
||||
3,Blob Sr.,true
|
||||
"""
|
||||
|
||||
struct User {
|
||||
var id: Int
|
||||
var name: String
|
||||
var isAdmin: Bool
|
||||
}
|
||||
```
|
||||
|
||||
A parser can be constructed for transforming the input string into an array of users in succinct
|
||||
and fluent API:
|
||||
|
||||
```swift
|
||||
let user = Parse(User.init) {
|
||||
Int.parser()
|
||||
","
|
||||
Prefix { $0 != "," }.map(String.init)
|
||||
","
|
||||
Bool.parser()
|
||||
}
|
||||
|
||||
let users = Many {
|
||||
user
|
||||
} separator: {
|
||||
"\n"
|
||||
} terminator: {
|
||||
End()
|
||||
}
|
||||
|
||||
try users.parse(input) // [User(id: 1, name: "Blob", isAdmin: true), ...]
|
||||
```
|
||||
|
||||
This says that to parse a user we:
|
||||
|
||||
* Parse and consume an integer from the beginning of the input
|
||||
* then a comma
|
||||
* then everything up to the next comma
|
||||
* then another comma
|
||||
* and finally a boolean.
|
||||
|
||||
And to parse an entire array of users we:
|
||||
|
||||
* Run the `user` parser many times
|
||||
* between each invocation of `user` we run the separator parser to consume a newline
|
||||
* and once the `user` and separator parsers have consumed all they can we run the terminator
|
||||
parser to verify there is no more input to consume.
|
||||
|
||||
Further, if the input is malformed, like say we mistyped one of the booleans, then the parser emits
|
||||
an error that describes exactly what went wrong:
|
||||
|
||||
```swift
|
||||
var input = """
|
||||
1,Blob,true
|
||||
2,Blob Jr.,false
|
||||
3,Blob Sr.,tru
|
||||
"""
|
||||
|
||||
try users.parse(input)
|
||||
|
||||
// error: unexpected input
|
||||
// --> input:3:11
|
||||
// 3 | 3,Blob Jr,tru
|
||||
// | ^ expected "true" or "false"
|
||||
```
|
||||
|
||||
That's the basics of parsing a simple string format, but there are a lot more operators and tricks
|
||||
to learn in order to performantly parse larger inputs.
|
||||
|
||||
## Topics
|
||||
|
||||
### Articles
|
||||
|
||||
* <doc:GettingStarted>
|
||||
* <doc:Design>
|
||||
* <doc:StringAbstractions>
|
||||
* <doc:ErrorMessages>
|
||||
* <doc:Backtracking>
|
||||
|
||||
## See Also
|
||||
|
||||
The collecton of videos from [Point-Free](https://www.pointfree.co) that dive deep into the
|
||||
development of the Parsing library.
|
||||
|
||||
* [Point-Free Videos](https://www.pointfree.co/collections/parsing)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
/// Raises a debug breakpoint if a debugger is attached.
|
||||
@inline(__always)
|
||||
@usableFromInline
|
||||
func breakpoint(_ message: @autoclosure () -> String = "") {
|
||||
#if canImport(Darwin)
|
||||
// https://github.com/bitstadium/HockeySDK-iOS/blob/c6e8d1e940299bec0c0585b1f7b86baf3b17fc82/Classes/BITHockeyHelper.m#L346-L370
|
||||
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
|
||||
var info: kinfo_proc = kinfo_proc()
|
||||
var info_size = MemoryLayout<kinfo_proc>.size
|
||||
|
||||
let isDebuggerAttached =
|
||||
sysctl(&name, 4, &info, &info_size, nil, 0) != -1
|
||||
&& info.kp_proc.p_flag & P_TRACED != 0
|
||||
|
||||
if isDebuggerAttached {
|
||||
fputs(
|
||||
"""
|
||||
\(message())
|
||||
|
||||
Caught debug breakpoint. Type "continue" ("c") to resume execution.
|
||||
|
||||
""",
|
||||
stderr
|
||||
)
|
||||
raise(SIGTRAP)
|
||||
}
|
||||
#else
|
||||
assertionFailure(message())
|
||||
#endif
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// NB: Deprecated after 0.6.0
|
||||
|
||||
@available(*, deprecated, renamed: "Parsers.Conditional")
|
||||
public typealias Conditional = Parsers.Conditional
|
||||
204
build-system/SwiftTL/Sources/SwiftTL/Parser/Parser.swift
Normal file
204
build-system/SwiftTL/Sources/SwiftTL/Parser/Parser.swift
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
/// Declares a type that can incrementally parse an `Output` value from an `Input` value.
|
||||
///
|
||||
/// A parser attempts to parse a nebulous piece of data, represented by the `Input` associated type,
|
||||
/// into something more well-structured, represented by the `Output` associated type. The parser
|
||||
/// implements the ``parse(_:)-76tcw`` method, which is handed an `inout Input`, and its job is to
|
||||
/// turn this into an `Output` if possible, or throw an error if it cannot.
|
||||
///
|
||||
/// The argument of the ``parse(_:)-76tcw`` function is `inout` because a parser will usually
|
||||
/// consume some of the input in order to produce an output. For example, we can use an
|
||||
/// `Int.parser()` parser to extract an integer from the beginning of a substring and consume that
|
||||
/// portion of the string:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input: Substring = "123 Hello world"
|
||||
///
|
||||
/// try Int.parser().parse(&input) // 123
|
||||
/// input // " Hello world"
|
||||
/// ```
|
||||
///
|
||||
/// Note that this parser works on `Substring` rather than `String` because substrings expose
|
||||
/// efficient ways of removing characters from its beginning. Substrings are "views" into a string,
|
||||
/// specified by start and end indices. Operations like `removeFirst`, `removeLast` and others can
|
||||
/// be implemented efficiently on substrings because they simply move the start and end indices,
|
||||
/// whereas their implementation on strings must make a copy of the string with the characters
|
||||
/// removed.
|
||||
@rethrows public protocol Parser {
|
||||
/// The type of values this parser parses from.
|
||||
associatedtype Input
|
||||
|
||||
/// The type of values parsed by this parser.
|
||||
associatedtype Output
|
||||
|
||||
/// Attempts to parse a nebulous piece of data into something more well-structured. Typically
|
||||
/// you only call this from other `Parser` conformances, not when you want to parse a concrete
|
||||
/// input.
|
||||
///
|
||||
/// - Parameter input: A nebulous, mutable piece of data to be incrementally parsed.
|
||||
/// - Returns: A more well-structured value parsed from the given input.
|
||||
func parse(_ input: inout Input) throws -> Output
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
/// Parse an input value into an output. This method is more ergonomic to use than
|
||||
/// ``parse(_:)-76tcw`` because the input does not need to be inout.
|
||||
///
|
||||
/// Rather than having to create a mutable input value and feed it to the ``parse(_:)-76tcw``
|
||||
/// method like this:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = ...
|
||||
/// let output = try parser.parse(&input)
|
||||
/// ```
|
||||
///
|
||||
/// You can just feed the input directly:
|
||||
///
|
||||
/// ```swift
|
||||
/// let output = try parser.parse(input)
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter input: A nebulous piece of data to be parsed.
|
||||
/// - Returns: A more well-structured value parsed from the given input.
|
||||
@inlinable
|
||||
public func parse(_ input: Input) rethrows -> Output {
|
||||
var input = input
|
||||
return try self.parse(&input)
|
||||
}
|
||||
|
||||
/// Parse a collection into an output using a parser that works on the collection's `SubSequence`.
|
||||
/// This method is more ergnomic to use than ``parse(_:)-76tcw`` because it accepts a
|
||||
/// collection directly rather than its subsequence, and the input does not need to be `inout`.
|
||||
///
|
||||
/// Rather than having to create a mutable subsequence value, such as a `Substring`, and feed it
|
||||
/// to the ``parse(_:)-76tcw`` method like this:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "123,true"[...]
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ","
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse(&input) // (123, true)
|
||||
/// ```
|
||||
///
|
||||
/// You can just feed a plain `String` input directly:
|
||||
///
|
||||
/// ```swift
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ","
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse("123,true") // (123, true)
|
||||
/// ```
|
||||
///
|
||||
/// This method will fail if the parser does not consume the entirety of the input.
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ","
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse("123,true ")
|
||||
///
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:9
|
||||
/// // 1 | 123,true␣␣␣␣
|
||||
/// // | ^ expected end of input
|
||||
/// ```
|
||||
///
|
||||
/// > Tip: If your input can have trailing whitespace that you would like to consume and discard
|
||||
/// > you can do so like this:
|
||||
/// > ```swift
|
||||
/// > let output = try Parse {
|
||||
/// > Int.parser()
|
||||
/// > ",".utf8
|
||||
/// > Bool.parser()
|
||||
/// > Skip { Whitespace() }
|
||||
/// > }
|
||||
/// > .parse("123,true ") // (123, true)
|
||||
/// > ```
|
||||
///
|
||||
/// - Parameter input: A nebulous collection of data to be parsed.
|
||||
/// - Returns: A more well-structured value parsed from the given input.
|
||||
@inlinable
|
||||
public func parse<C: Collection>(_ input: C) rethrows -> Output
|
||||
where Input == C.SubSequence {
|
||||
try Parse {
|
||||
self
|
||||
End<Input>()
|
||||
}.parse(input[...])
|
||||
}
|
||||
|
||||
/// Parse a `String` into an output using a UTF-8 parser. This method is more ergnomic to use
|
||||
/// than ``parse(_:)-76tcw`` because it accepts a plain string rather than a collection of
|
||||
/// UTF-8 code units, and the input does not need to be `inout`.
|
||||
///
|
||||
/// Rather than having to create a mutable UTF-8 value and feed it to the ``parse(_:)-76tcw``
|
||||
/// method like this:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "123,true"[...].utf8
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ",".utf8
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse(&input) // (123, true)
|
||||
/// ```
|
||||
///
|
||||
/// You can just feed a plain `String` input directly:
|
||||
///
|
||||
/// ```swift
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ",".utf8
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse("123,true") // (123, true)
|
||||
/// ```
|
||||
///
|
||||
/// This method will fail if the parser does not consume the entirety of the input.
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let output = try Parse {
|
||||
/// Int.parser()
|
||||
/// ",".utf8
|
||||
/// Bool.parser()
|
||||
/// }
|
||||
/// .parse("123,true ")
|
||||
///
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:9
|
||||
/// // 1 | 123,true␣␣␣␣
|
||||
/// // | ^ expected end of input
|
||||
/// ```
|
||||
///
|
||||
/// > Tip: If your input can have trailing whitespace that you would like to consume and discard
|
||||
/// > you can do so like this:
|
||||
/// > ```swift
|
||||
/// > let output = try Parse {
|
||||
/// > Int.parser()
|
||||
/// > ",".utf8
|
||||
/// > Bool.parser()
|
||||
/// > Skip { Whitespace() }
|
||||
/// > }
|
||||
/// > .parse("123,true ") // (123, true)
|
||||
/// > ```
|
||||
///
|
||||
/// - Parameter input: A nebulous collection of data to be parsed.
|
||||
/// - Returns: A more well-structured value parsed from the given input.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public func parse<S: StringProtocol>(_ input: S) rethrows -> Output
|
||||
where Input == S.SubSequence.UTF8View {
|
||||
try Parse {
|
||||
self
|
||||
End<Input>()
|
||||
}.parse(input[...].utf8)
|
||||
}
|
||||
}
|
||||
101
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Always.swift
Normal file
101
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Always.swift
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/// A parser that always succeeds with the given value, and does not consume any input.
|
||||
///
|
||||
/// While not very useful on its own, the `Always` parser can be helpful when combined with other
|
||||
/// parsers or operators.
|
||||
///
|
||||
/// When its `Output` is `Void`, it can be used as a "no-op" parser of sorts and be plugged into
|
||||
/// other parser operations. For example, the ``Many`` parser can be configured with separator and
|
||||
/// terminator parsers:
|
||||
///
|
||||
/// ```swift
|
||||
/// Many {
|
||||
/// Int.parser()
|
||||
/// } separator: {
|
||||
/// ","
|
||||
/// } terminator: {
|
||||
/// End()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// But also exposes initializers that omit these parsers when there is no separator or terminator
|
||||
/// to be parsed:
|
||||
///
|
||||
/// ```swift
|
||||
/// Many {
|
||||
/// Prefix { $0 != "\n" }
|
||||
/// "\n"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To support this, `Many` plugs `Always<Input, Void>` into each omitted parser. As a simplified
|
||||
/// example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Many<Element: Parser, Separator: Parser, Terminator: Parser>: Parser
|
||||
/// where Separator.Input == Element.Input, Terminator.Input == Element.Input {
|
||||
/// ...
|
||||
/// }
|
||||
///
|
||||
/// extension Many where Separator == Always<Input, Void>, Terminator == Always<Input, Void> {
|
||||
/// init(@ParserBuilder element: () -> Element) {
|
||||
/// self.element = element()
|
||||
/// self.separator = Always(())
|
||||
/// self.terminator = Always(())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This means the previous example is equivalent to:
|
||||
///
|
||||
/// ```swift
|
||||
/// Many {
|
||||
/// Prefix { $0 != "\n" }
|
||||
/// "\n"
|
||||
/// } separator: {
|
||||
/// Always(())
|
||||
/// } terminator: {
|
||||
/// Always(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > Note: While `Always` can be used as the last alternative of a ``OneOf`` to specify a default
|
||||
/// > output, the resulting parser will be throwing. Instead, prefer ``Parser/replaceError(with:)``,
|
||||
/// > which returns a non-throwing parser.
|
||||
public struct Always<Input, Output>: Parser {
|
||||
public let output: Output
|
||||
|
||||
@inlinable
|
||||
public init(_ output: Output) {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) -> Output {
|
||||
self.output
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func map<NewOutput>(
|
||||
_ transform: @escaping (Output) -> NewOutput
|
||||
) -> Always<Input, NewOutput> {
|
||||
.init(transform(self.output))
|
||||
}
|
||||
}
|
||||
|
||||
extension Always where Input == Substring {
|
||||
@inlinable
|
||||
public init(_ output: Output) {
|
||||
self.output = output
|
||||
}
|
||||
}
|
||||
|
||||
extension Always where Input == Substring.UTF8View {
|
||||
@inlinable
|
||||
public init(_ output: Output) {
|
||||
self.output = output
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Always = SwiftTL.Always // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
extension Parser {
|
||||
/// Wraps this parser with a type eraser.
|
||||
///
|
||||
/// This form of _type erasure_ preserves abstraction across API boundaries, such as different
|
||||
/// modules.
|
||||
///
|
||||
/// When you expose your composed parsers as the ``AnyParser`` type, you can change the underlying
|
||||
/// implementation over time without affecting existing clients.
|
||||
///
|
||||
/// Equivalent to passing `self` to `AnyParser.init(_:)`.
|
||||
///
|
||||
/// - Returns: An ``AnyParser`` wrapping this publisher.
|
||||
@inlinable
|
||||
public func eraseToAnyParser() -> AnyParser<Input, Output> {
|
||||
.init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased parser of `Output` from `Input`.
|
||||
///
|
||||
/// This parser forwards its ``parse(_:)`` method to an arbitrary underlying parser having the same
|
||||
/// `Input` and `Output` types, hiding the specifics of the underlying ``Parser``.
|
||||
///
|
||||
/// Use ``AnyParser`` to wrap a parser whose type has details you don't want to expose across API
|
||||
/// boundaries, such as different modules. When you use type erasure this way, you can change the
|
||||
/// underlying parser over time without affecting existing clients.
|
||||
public struct AnyParser<Input, Output>: Parser {
|
||||
@usableFromInline
|
||||
let parser: (inout Input) throws -> Output
|
||||
|
||||
/// Creates a type-erasing parser to wrap the given parser.
|
||||
///
|
||||
/// Equivalent to calling ``Parser/eraseToAnyParser()`` on the parser.
|
||||
///
|
||||
/// - Parameter parser: A parser to wrap with a type eraser.
|
||||
@inlinable
|
||||
public init<P: Parser>(_ parser: P) where P.Input == Input, P.Output == Output {
|
||||
self.init(parser.parse)
|
||||
}
|
||||
|
||||
/// Creates a parser that wraps the given closure in its ``parse(_:)`` method.
|
||||
///
|
||||
/// - Parameter parse: A closure that attempts to parse an output from an input. `parse` is
|
||||
/// executed each time the ``parse(_:)`` method is called on the resulting parser.
|
||||
@inlinable
|
||||
public init(_ parse: @escaping (inout Input) throws -> Output) {
|
||||
self.parser = parse
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Output {
|
||||
try self.parser(&input)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func eraseToAnyParser() -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias AnyParser = SwiftTL.AnyParser // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
extension Bool {
|
||||
/// A parser that consumes a Boolean value from the beginning of a collection of UTF-8 code units.
|
||||
///
|
||||
/// See <doc:Bool> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - Returns: A parser that consumes a Boolean value from the beginning of a collection of UTF-8
|
||||
/// code units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self
|
||||
) -> Parsers.BoolParser<Input> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a Boolean value from the beginning of a substring's UTF-8 view.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is
|
||||
/// `Substring.UTF8View`.
|
||||
///
|
||||
/// See <doc:Bool> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a Boolean value from the beginning of a substring's UTF-8
|
||||
/// view.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
|
||||
) -> Parsers.BoolParser<Substring.UTF8View> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a Boolean value from the beginning of a substring.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is `Substring`.
|
||||
///
|
||||
/// See <doc:Bool> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a Boolean value from the beginning of a substring.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self
|
||||
) -> FromUTF8View<Substring, Parsers.BoolParser<Substring.UTF8View>> {
|
||||
.init { Parsers.BoolParser<Substring.UTF8View>() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that consumes a Boolean value from the beginning of a collection of UTF-8 code units.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// `Bool.parser()`, which constructs this type.
|
||||
///
|
||||
/// See <doc:Bool> for more information about this parser.
|
||||
public struct BoolParser<Input: Collection>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit
|
||||
{
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Bool {
|
||||
if input.starts(with: [116, 114, 117, 101] /*"true".utf8*/) {
|
||||
input.removeFirst(4)
|
||||
return true
|
||||
} else if input.starts(with: [102, 97, 108, 115, 101] /*"false".utf8*/) {
|
||||
input.removeFirst(5)
|
||||
return false
|
||||
}
|
||||
throw ParsingError.expectedInput("\"true\" or \"false\"", at: input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
extension CaseIterable where Self: RawRepresentable, RawValue == Int {
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of a substring.
|
||||
///
|
||||
/// See <doc:CaseIterable> for more info.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a substring.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> {
|
||||
.init(toPrefix: { String($0) }, areEquivalent: ==)
|
||||
}
|
||||
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of a substring's UTF-8 view.
|
||||
///
|
||||
/// See <doc:CaseIterable> for more info.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a substring's UTF-8 view.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> {
|
||||
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==)
|
||||
}
|
||||
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of UTF-8 code units.
|
||||
///
|
||||
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a collection of UTF-8 code units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View>
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit
|
||||
{
|
||||
.init(toPrefix: { String($0).utf8 }, areEquivalent: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension CaseIterable where Self: RawRepresentable, RawValue == String {
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of a substring.
|
||||
///
|
||||
/// See <doc:CaseIterable> for more info.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a substring.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Substring, Self, String> {
|
||||
.init(toPrefix: { $0 }, areEquivalent: ==)
|
||||
}
|
||||
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of a substring's UTF-8 view.
|
||||
///
|
||||
/// See <doc:CaseIterable> for more info.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a substring's UTF-8 view.
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Substring.UTF8View, Self, String.UTF8View> {
|
||||
.init(toPrefix: { $0.utf8 }, areEquivalent: ==)
|
||||
}
|
||||
|
||||
/// A parser that consumes a case-iterable, raw representable value from the beginning of a
|
||||
/// collection of UTF-8 code units.
|
||||
///
|
||||
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - Returns: A parser that consumes a case-iterable, raw representable value from the beginning
|
||||
/// of a collection of UTF-8 code units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self
|
||||
) -> Parsers.CaseIterableRawRepresentableParser<Input, Self, String.UTF8View>
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit
|
||||
{
|
||||
.init(toPrefix: { $0.utf8 }, areEquivalent: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public struct CaseIterableRawRepresentableParser<
|
||||
Input: Collection, Output: CaseIterable & RawRepresentable, Prefix: Collection
|
||||
>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Output.RawValue: Comparable,
|
||||
Prefix.Element == Input.Element
|
||||
{
|
||||
@usableFromInline
|
||||
let cases: [(case: Output, prefix: Prefix, count: Int)]
|
||||
|
||||
@usableFromInline
|
||||
let areEquivalent: (Input.Element, Input.Element) -> Bool
|
||||
|
||||
@usableFromInline
|
||||
init(
|
||||
toPrefix: @escaping (Output.RawValue) -> Prefix,
|
||||
areEquivalent: @escaping (Input.Element, Input.Element) -> Bool
|
||||
) {
|
||||
self.areEquivalent = areEquivalent
|
||||
self.cases = Output.allCases
|
||||
.map {
|
||||
let prefix = toPrefix($0.rawValue)
|
||||
return ($0, prefix, prefix.count)
|
||||
}
|
||||
.sorted(by: { $0.count > $1.count })
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Output {
|
||||
for (`case`, prefix, count) in self.cases {
|
||||
if input.starts(with: prefix, by: self.areEquivalent) {
|
||||
input.removeFirst(count)
|
||||
return `case`
|
||||
}
|
||||
}
|
||||
throw ParsingError.expectedInput("case of \"\(Output.self)\"", at: input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Foundation
|
||||
|
||||
extension CharacterSet: Parser {
|
||||
@inlinable
|
||||
public func parse(_ input: inout Substring) -> Substring {
|
||||
let output = input.unicodeScalars.prefix(while: self.contains)
|
||||
input.unicodeScalars.removeFirst(output.count)
|
||||
return Substring(output)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
extension Parser {
|
||||
/// Returns a parser that outputs the non-`nil` result of calling the given closure with the
|
||||
/// output of this parser.
|
||||
///
|
||||
/// This method is similar to `Sequence.compactMap` in the Swift standard library, as well as
|
||||
/// `Publisher.compactMap` in the Combine framework.
|
||||
///
|
||||
/// ```swift
|
||||
/// let evenParser = Int.parser().compactMap { $0.isMultiple(of: 2) }
|
||||
///
|
||||
/// var input = "124 hello world"[...]
|
||||
/// try evenParser.parse(&input) // 124
|
||||
/// input // " hello world"
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails when the provided closure returns `nil`. For example, the following parser tries
|
||||
/// to convert two characters into a hex digit, but fails to do so because `"GG"` is not a valid
|
||||
/// hex number:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "GG0000"[...]
|
||||
/// let hex = try Prefix(2).compactMap { Int(String($0), radix: 16) }.parse(&input)
|
||||
/// // error: failed to process "Int" from "GG"
|
||||
/// // --> input:1:1-2
|
||||
/// // 1 | GG0000
|
||||
/// // | ^^
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter transform: A closure that accepts output of this parser as its argument and
|
||||
/// returns an optional value.
|
||||
/// - Returns: A parser that outputs the non-`nil` result of calling the given transformation
|
||||
/// with the output of this parser.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public func compactMap<NewOutput>(
|
||||
_ transform: @escaping (Output) -> NewOutput?
|
||||
) -> Parsers.CompactMap<Self, NewOutput> {
|
||||
.init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that outputs the non-`nil` result of calling the given transformation with the output
|
||||
/// of its upstream parser.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/compactMap(_:)`` operation, which constructs this type.
|
||||
public struct CompactMap<Upstream: Parser, Output>: Parser {
|
||||
public let upstream: Upstream
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) -> Output?
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) throws -> Output {
|
||||
let original = input
|
||||
let output = try self.upstream.parse(&input)
|
||||
guard let newOutput = self.transform(output)
|
||||
else {
|
||||
throw ParsingError.failed(
|
||||
summary: """
|
||||
failed to process "\(Output.self)" from \(formatValue(output))
|
||||
""",
|
||||
from: original,
|
||||
to: input
|
||||
)
|
||||
}
|
||||
return newOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
extension Parsers {
|
||||
/// A parser that can parse output from two types of parsers.
|
||||
///
|
||||
/// This parser is useful for situations where you want to run one of two different parsers based on
|
||||
/// a condition, which typically would force you to perform ``Parser/eraseToAnyParser()`` and incur
|
||||
/// a performance penalty.
|
||||
///
|
||||
/// For example, you can use this parser in a ``Parser/flatMap(_:)`` operation to use the parsed
|
||||
/// output to determine what parser to run next:
|
||||
///
|
||||
/// ```swift
|
||||
/// versionParser.flatMap { version in
|
||||
/// version == "2.0"
|
||||
/// ? Conditional.first(V2Parser())
|
||||
/// : Conditional.second(LegacyParser())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You won't typically construct this parser directly, but instead will use standard `if`-`else`
|
||||
/// statements in a parser builder to automatically build conditional parsers:
|
||||
///
|
||||
/// ```swift
|
||||
/// versionParser.flatMap { version in
|
||||
/// if version == "2.0" {
|
||||
/// V2Parser()
|
||||
/// } else {
|
||||
/// LegacyParser()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
public enum Conditional<First: Parser, Second: Parser>: Parser
|
||||
where
|
||||
First.Input == Second.Input,
|
||||
First.Output == Second.Output
|
||||
{
|
||||
case first(First)
|
||||
case second(Second)
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout First.Input) rethrows -> First.Output {
|
||||
switch self {
|
||||
case let .first(first):
|
||||
return try first.parse(&input)
|
||||
case let .second(second):
|
||||
return try second.parse(&input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/// A parser that succeeds if the input is empty, and fails otherwise.
|
||||
///
|
||||
/// Useful as a final parser in a long sequence of parsers to guarantee that all input has been
|
||||
/// consumed.
|
||||
///
|
||||
/// ```swift
|
||||
/// let parser = Parse {
|
||||
/// "Hello, "
|
||||
/// Prefix { $0 != "!" }
|
||||
/// "!"
|
||||
/// End() // NB: All input should be consumed.
|
||||
/// }
|
||||
///
|
||||
/// var input = "Hello, Blob!"[...]
|
||||
/// try parser.parse(&input) // "Blob"
|
||||
/// ```
|
||||
///
|
||||
/// This parser will fail if there are input elements that have not been consumed:
|
||||
///
|
||||
/// ```swift
|
||||
/// input = "Hello, Blob!!"
|
||||
/// try parser.parse(&input)
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:13
|
||||
/// // 1 | Hello, Blob!!
|
||||
/// // | ^ expected end of input
|
||||
/// ```
|
||||
public struct End<Input: Collection>: Parser {
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws {
|
||||
guard input.isEmpty else {
|
||||
throw ParsingError.expectedInput("end of input", at: input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension End where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/*extension End where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}*/
|
||||
|
||||
extension Parsers {
|
||||
public typealias End = SwiftTL.End // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/// A parser that always fails, no matter the input.
|
||||
///
|
||||
/// While not very useful on its own, this parser can be helpful when combined with other parsers or
|
||||
/// operators.
|
||||
///
|
||||
/// For example, it can be used to conditionally causing parsing to fail when used with
|
||||
/// ``Parser/flatMap(_:)``:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct OddFailure: Error {}
|
||||
///
|
||||
/// let evens = Int.parser().flatMap {
|
||||
/// if $0.isMultiple(of: 2) {
|
||||
/// Always($0)
|
||||
/// } else {
|
||||
/// Fail<Substring, Int>(throwing: OddFailure())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// try evens.parse("42") // 42
|
||||
///
|
||||
/// try evens.parse("123")
|
||||
/// // error: OddFailure()
|
||||
/// // --> input:1:1-3
|
||||
/// // 1 | 123
|
||||
/// // | ^^^
|
||||
/// ```
|
||||
public struct Fail<Input, Output>: Parser {
|
||||
@usableFromInline
|
||||
let error: Error
|
||||
|
||||
/// Creates a parser that throws an error when it runs.
|
||||
///
|
||||
/// - Parameter error: An error to throw when the parser is run.
|
||||
@inlinable
|
||||
public init(throwing error: Error) {
|
||||
self.error = error
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Output {
|
||||
switch self.error {
|
||||
case is ParsingError:
|
||||
throw self.error
|
||||
default:
|
||||
throw ParsingError.wrap(self.error, at: input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Fail {
|
||||
/// Creates a parser that throws an error when it runs.
|
||||
@inlinable
|
||||
public init() {
|
||||
self.init(throwing: DefaultError())
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct DefaultError: Error, CustomDebugStringConvertible {
|
||||
@usableFromInline
|
||||
init() {}
|
||||
|
||||
@usableFromInline
|
||||
var debugDescription: String {
|
||||
"failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Fail = SwiftTL.Fail // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
extension Parser {
|
||||
/// Returns a parser that filters output from this parser when its output does not satisfy the
|
||||
/// given predicate.
|
||||
///
|
||||
/// This method is similar to `Sequence.filter` in the Swift standard library, as well as
|
||||
/// `Publisher.filter` in the Combine framework.
|
||||
///
|
||||
/// This parser fails if the predicate is not satisfied on the output of the upstream parser. For example,
|
||||
/// the following parser consumes only even integers and so fails when an odd integer is used:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "43 Hello, world!"[...]
|
||||
/// let number = try Int.parser().filter { $0.isMultiple(of: 2) }.parse(&input)
|
||||
/// // error: processed value 43 failed to satisfy predicate
|
||||
/// // --> input:1:1-2
|
||||
/// // 1 | 43 Hello, world!
|
||||
/// // | ^^ processed input
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an output from this parser and returns a Boolean
|
||||
/// value indicating whether the output should be returned.
|
||||
/// - Returns: A parser that filters its output.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public func filter(_ predicate: @escaping (Output) -> Bool) -> Parsers.Filter<Self> {
|
||||
.init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that filters the output of an upstream parser when it does not satisfy a predicate.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/filter(_:)`` operation, which constructs this type.
|
||||
public struct Filter<Upstream: Parser>: Parser {
|
||||
public let upstream: Upstream
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
@inlinable
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) throws -> Upstream.Output {
|
||||
let original = input
|
||||
let output = try self.upstream.parse(&input)
|
||||
guard self.predicate(output)
|
||||
else {
|
||||
throw ParsingError.failed(
|
||||
summary: "processed value \(formatValue(output)) failed to satisfy predicate",
|
||||
label: "processed input",
|
||||
from: original,
|
||||
to: input
|
||||
)
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/// A parser that consumes the first element from a collection.
|
||||
///
|
||||
/// This parser is named after `Sequence.first`, and attempts to parse the first element from a
|
||||
/// collection of input by calling this property under the hood.
|
||||
///
|
||||
/// For example, it can parse the leading character off a substring:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Hello"[...]
|
||||
/// try First().parse(&input) // "H"
|
||||
/// input // "ello"
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails if the input collection is empty:
|
||||
///
|
||||
/// ```swift
|
||||
/// input = ""
|
||||
/// try First().parse(&input)
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 |
|
||||
/// // | ^ expected element
|
||||
/// ```
|
||||
public struct First<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Input.Element {
|
||||
guard let first = input.first else {
|
||||
throw ParsingError.expectedInput("element", at: input)
|
||||
}
|
||||
input.removeFirst()
|
||||
return first
|
||||
}
|
||||
}
|
||||
|
||||
extension First where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
extension First where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias First = SwiftTL.First // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
extension Parser {
|
||||
/// Returns a parser that transforms the output of this parser into a new parser.
|
||||
///
|
||||
/// This method is similar to `Sequence.flatMap`, `Optional.flatMap`, and `Result.flatMap` in the
|
||||
/// Swift standard library, as well as `Publisher.flatMap` in the Combine framework.
|
||||
///
|
||||
/// - Parameter transform: A closure that transforms values of this parser's output and returns a
|
||||
/// new parser.
|
||||
/// - Returns: A parser that transforms output from an upstream parser into a new parser.
|
||||
@inlinable
|
||||
public func flatMap<NewParser>(
|
||||
@ParserBuilder _ transform: @escaping (Output) -> NewParser
|
||||
) -> Parsers.FlatMap<NewParser, Self> {
|
||||
.init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that transforms the output of another parser into a new parser.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/flatMap(_:)`` operation, which constructs this type.
|
||||
public struct FlatMap<NewParser: Parser, Upstream: Parser>: Parser
|
||||
where NewParser.Input == Upstream.Input {
|
||||
public let upstream: Upstream
|
||||
public let transform: (Upstream.Output) -> NewParser
|
||||
|
||||
@inlinable
|
||||
public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> NewParser) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) rethrows -> NewParser.Output {
|
||||
let original = input
|
||||
do {
|
||||
return try self.transform(self.upstream.parse(&input)).parse(&input)
|
||||
} catch let ParsingError.failed(reason, context) {
|
||||
throw ParsingError.failed(
|
||||
reason,
|
||||
.init(
|
||||
originalInput: original,
|
||||
remainingInput: input,
|
||||
debugDescription: context.debugDescription,
|
||||
underlyingError: ParsingError.failed(reason, context)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Float.swift
Normal file
172
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Float.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
extension BinaryFloatingPoint where Self: LosslessStringConvertible {
|
||||
/// A parser that consumes a floating-point number from the beginning of a collection of UTF-8
|
||||
/// code units.
|
||||
///
|
||||
/// See <doc:Float> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - Returns: A parser that consumes a floating-point number from the beginning of a collection
|
||||
/// of UTF-8 code units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self
|
||||
) -> Parsers.FloatParser<Input, Self> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a floating-point number from the beginning of a substring's UTF-8 view.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is
|
||||
/// `Substring.UTF8View`.
|
||||
///
|
||||
/// See <doc:Float> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a floating-point number from the beginning of a substring's
|
||||
/// UTF-8 view.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
|
||||
) -> Parsers.FloatParser<Substring.UTF8View, Self> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a floating-point number from the beginning of a substring.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is `Substring`.
|
||||
///
|
||||
/// See <doc:Float> for more information about this parser.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a floating-point number from the beginning of a substring.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self
|
||||
) -> FromUTF8View<Substring, Parsers.FloatParser<Substring.UTF8View, Self>> {
|
||||
.init { Parsers.FloatParser<Substring.UTF8View, Self>() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that consumes a floating-point number from the beginning of a collection of UTF-8
|
||||
/// code units.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the static `parser()` method on the `BinaryFloatingPoint` of your choice, e.g.,
|
||||
/// `Double.parser()`, `Float80.parser()`, etc., all of which construct this type.
|
||||
///
|
||||
/// See <doc:Float> for more information about this parser.
|
||||
public struct FloatParser<Input: Collection, Output: BinaryFloatingPoint>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit,
|
||||
Output: LosslessStringConvertible
|
||||
{
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Output {
|
||||
let original = input
|
||||
let s = input.parseFloat()
|
||||
guard let n = Output(String(decoding: s, as: UTF8.self))
|
||||
else {
|
||||
throw ParsingError.expectedInput("\(Output.self)".lowercased(), from: original, to: input)
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UTF8.CodeUnit {
|
||||
@usableFromInline
|
||||
var isDigit: Bool {
|
||||
(.init(ascii: "0") ... .init(ascii: "9")).contains(self)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
var isHexDigit: Bool {
|
||||
(.init(ascii: "0") ... .init(ascii: "9")).contains(self)
|
||||
|| (.init(ascii: "a") ... .init(ascii: "f")).contains(self)
|
||||
|| (.init(ascii: "A") ... .init(ascii: "F")).contains(self)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
var isSign: Bool {
|
||||
self == .init(ascii: "-") || self == .init(ascii: "+")
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where SubSequence == Self, Element == UTF8.CodeUnit {
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
mutating func parseFloat() -> SubSequence {
|
||||
let original = self
|
||||
if self.first?.isSign == true {
|
||||
self.removeFirst()
|
||||
}
|
||||
if self.first == .init(ascii: "0")
|
||||
&& (self.dropFirst().first == .init(ascii: "x")
|
||||
|| self.dropFirst().first == .init(ascii: "X"))
|
||||
{
|
||||
self.removeFirst(2)
|
||||
let integer = self.prefix(while: { $0.isHexDigit })
|
||||
self.removeFirst(integer.count)
|
||||
if self.first == .init(ascii: ".") {
|
||||
let fractional =
|
||||
self
|
||||
.dropFirst()
|
||||
.prefix(while: { $0.isHexDigit })
|
||||
self.removeFirst(1 + fractional.count)
|
||||
}
|
||||
if self.first == .init(ascii: "p") || self.first == .init(ascii: "P") {
|
||||
var n = 1
|
||||
if self.dropFirst().first?.isSign == true { n += 1 }
|
||||
let exponent =
|
||||
self
|
||||
.dropFirst(n)
|
||||
.prefix(while: { $0.isHexDigit })
|
||||
guard !exponent.isEmpty else { return original[..<self.startIndex] }
|
||||
self.removeFirst(n + exponent.count)
|
||||
}
|
||||
} else if self.first?.isDigit == true || self.first == .init(ascii: ".") {
|
||||
let integer = self.prefix(while: { $0.isDigit })
|
||||
self.removeFirst(integer.count)
|
||||
if self.first == .init(ascii: ".") {
|
||||
let fractional =
|
||||
self
|
||||
.dropFirst()
|
||||
.prefix(while: { $0.isDigit })
|
||||
self.removeFirst(1 + fractional.count)
|
||||
}
|
||||
if self.first == .init(ascii: "e") || self.first == .init(ascii: "E") {
|
||||
var n = 1
|
||||
if self.dropFirst().first?.isSign == true { n += 1 }
|
||||
let exponent =
|
||||
self
|
||||
.dropFirst(n)
|
||||
.prefix(while: { $0.isDigit })
|
||||
guard !exponent.isEmpty else { return original[..<self.startIndex] }
|
||||
self.removeFirst(n + exponent.count)
|
||||
}
|
||||
} else if self.prefix(8).caseInsensitiveElementsEqualLowercase("infinity".utf8) {
|
||||
self.removeFirst(8)
|
||||
} else if self.prefix(3).caseInsensitiveElementsEqualLowercase("inf".utf8)
|
||||
|| self.prefix(3).caseInsensitiveElementsEqualLowercase("nan".utf8)
|
||||
{
|
||||
self.removeFirst(3)
|
||||
}
|
||||
return original[..<self.startIndex]
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
func caseInsensitiveElementsEqualLowercase<S: Sequence>(_ other: S) -> Bool
|
||||
where S.Element == Element {
|
||||
self.elementsEqual(other, by: { $0 == $1 || $0 + 32 == $1 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/// A parser that transforms a parser on `Substring` into a parser on another view.
|
||||
///
|
||||
/// The `FromSubstring` operator allows you to mix and match representation levels of strings
|
||||
/// so that you can maximize how much you parse on the faster, but more complex, lower level
|
||||
/// representations and then switch to slower, but safer, higher level representations for
|
||||
/// when you need that power.
|
||||
///
|
||||
/// For example, to parse "café" as a collection of UTF8 code units you must be careful to parse
|
||||
/// both representations of "é":
|
||||
///
|
||||
/// ```swift
|
||||
/// OneOf {
|
||||
/// "caf\u{00E9}".utf8 // LATIN SMALL LETTER E WITH ACUTE
|
||||
/// "cafe\u{0301}".utf8 // E + COMBINING ACUTE ACCENT
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Alternatively, you can parse the ASCII characters of "caf" as UTF8 code units, and then
|
||||
/// switch to the higher level substring representation to parse "é" so that you don't have
|
||||
/// to worry about UTF8 normalization:
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// "caf".utf8
|
||||
///
|
||||
/// // Parse any recognized "é" character, including:
|
||||
/// // - LATIN SMALL LETTER E WITH ACUTE ("\u{00E9}")
|
||||
/// // - E + COMBINING ACUTE ACCENT ("e\u{0301}")
|
||||
/// FromSubstring { "é" }
|
||||
/// }
|
||||
/// ```
|
||||
public struct FromSubstring<Input, SubstringParser: Parser>: Parser
|
||||
where SubstringParser.Input == Substring {
|
||||
public let substringParser: SubstringParser
|
||||
public let toSubstring: (Input) -> Substring
|
||||
public let fromSubstring: (Substring) -> Input
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) rethrows -> SubstringParser.Output {
|
||||
var substring = self.toSubstring(input)
|
||||
defer { input = self.fromSubstring(substring) }
|
||||
return try self.substringParser.parse(&substring)
|
||||
}
|
||||
}
|
||||
|
||||
extension FromSubstring where Input == ArraySlice<UInt8> {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> SubstringParser) {
|
||||
self.substringParser = build()
|
||||
self.toSubstring = { Substring(decoding: $0, as: UTF8.self) }
|
||||
self.fromSubstring = { ArraySlice($0.utf8) }
|
||||
}
|
||||
}
|
||||
|
||||
extension FromSubstring where Input == Substring.UnicodeScalarView {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> SubstringParser) {
|
||||
self.substringParser = build()
|
||||
self.toSubstring = Substring.init
|
||||
self.fromSubstring = \.unicodeScalars
|
||||
}
|
||||
}
|
||||
|
||||
extension FromSubstring where Input == Substring.UTF8View {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> SubstringParser) {
|
||||
self.substringParser = build()
|
||||
self.toSubstring = Substring.init
|
||||
self.fromSubstring = \.utf8
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
public struct FromUTF8View<Input, UTF8Parser: Parser>: Parser
|
||||
where UTF8Parser.Input == Substring.UTF8View {
|
||||
public let utf8Parser: UTF8Parser
|
||||
public let toUTF8: (Input) -> Substring.UTF8View
|
||||
public let fromUTF8: (Substring.UTF8View) -> Input
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) rethrows -> UTF8Parser.Output {
|
||||
var utf8 = self.toUTF8(input)
|
||||
defer { input = self.fromUTF8(utf8) }
|
||||
return try self.utf8Parser.parse(&utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension FromUTF8View where Input == Substring {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> UTF8Parser) {
|
||||
self.utf8Parser = build()
|
||||
self.toUTF8 = \.utf8
|
||||
self.fromUTF8 = Substring.init
|
||||
}
|
||||
}
|
||||
|
||||
extension FromUTF8View where Input == Substring.UnicodeScalarView {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> UTF8Parser) {
|
||||
self.utf8Parser = build()
|
||||
self.toUTF8 = { Substring($0).utf8 }
|
||||
self.fromUTF8 = { Substring($0).unicodeScalars }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/// A parser that transforms a parser on `Substring.UnicodeScalarView` into a parser on another
|
||||
/// view.
|
||||
public struct FromUnicodeScalarView<Input, UnicodeScalarsParser: Parser>: Parser
|
||||
where UnicodeScalarsParser.Input == Substring.UnicodeScalarView {
|
||||
public let unicodeScalarsParser: UnicodeScalarsParser
|
||||
public let toUnicodeScalars: (Input) -> Substring.UnicodeScalarView
|
||||
public let fromUnicodeScalars: (Substring.UnicodeScalarView) -> Input
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) rethrows -> UnicodeScalarsParser.Output {
|
||||
var unicodeScalars = self.toUnicodeScalars(input)
|
||||
defer { input = self.fromUnicodeScalars(unicodeScalars) }
|
||||
return try self.unicodeScalarsParser.parse(&unicodeScalars)
|
||||
}
|
||||
}
|
||||
|
||||
extension FromUnicodeScalarView where Input == ArraySlice<UInt8> {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> UnicodeScalarsParser) {
|
||||
self.unicodeScalarsParser = build()
|
||||
self.toUnicodeScalars = { Substring(decoding: $0, as: UTF8.self).unicodeScalars }
|
||||
self.fromUnicodeScalars = { ArraySlice(Substring($0).utf8) }
|
||||
}
|
||||
}
|
||||
|
||||
extension FromUnicodeScalarView where Input == Substring {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> UnicodeScalarsParser) {
|
||||
self.unicodeScalarsParser = build()
|
||||
self.toUnicodeScalars = \.unicodeScalars
|
||||
self.fromUnicodeScalars = Substring.init
|
||||
}
|
||||
}
|
||||
|
||||
extension FromUnicodeScalarView where Input == Substring.UTF8View {
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> UnicodeScalarsParser) {
|
||||
self.unicodeScalarsParser = build()
|
||||
self.toUnicodeScalars = { Substring($0).unicodeScalars }
|
||||
self.fromUnicodeScalars = { Substring($0).utf8 }
|
||||
}
|
||||
}
|
||||
163
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Int.swift
Normal file
163
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Int.swift
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
extension FixedWidthInteger {
|
||||
/// A parser that consumes an integer (with an optional leading `+` or `-` sign for signed integer
|
||||
/// types) from the beginning of a collection of UTF-8 code units.
|
||||
///
|
||||
/// See <doc:Int> for more information about this parser.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - radix: The radix, or base, to use for converting text to an integer value. `radix` must be
|
||||
/// in the range `2...36`.
|
||||
/// - Returns: A parser that consumes an integer from the beginning of a collection of UTF-8 code
|
||||
/// units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self,
|
||||
radix: Int = 10
|
||||
) -> Parsers.IntParser<Input, Self> {
|
||||
.init(radix: radix)
|
||||
}
|
||||
|
||||
/// A parser that consumes an integer (with an optional leading `+` or `-` sign for signed integer
|
||||
/// types) from the beginning of a collection of UTF-8 code units.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is
|
||||
/// `Substring.UTF8View`.
|
||||
///
|
||||
/// See <doc:Int> for more information about this parser.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - inputType: The `Substring.UTF8View` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - radix: The radix, or base, to use for converting text to an integer value. `radix` must be
|
||||
/// in the range `2...36`.
|
||||
/// - Returns: A parser that consumes an integer from the beginning of a substring's UTF-8 view.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self,
|
||||
radix: Int = 10
|
||||
) -> Parsers.IntParser<Substring.UTF8View, Self> {
|
||||
.init(radix: radix)
|
||||
}
|
||||
|
||||
/// A parser that consumes an integer (with an optional leading `+` or `-` sign for signed integer
|
||||
/// types) from the beginning of a collection of UTF-8 code units.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is `Substring`.
|
||||
///
|
||||
/// See <doc:Int> for more information about this parser.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - inputType: The `Substring` type. This parameter is included to mirror the interface that
|
||||
/// parses any collection of UTF-8 code units.
|
||||
/// - radix: The radix, or base, to use for converting text to an integer value. `radix` must be
|
||||
/// in the range `2...36`.
|
||||
/// - Returns: A parser that consumes an integer from the beginning of a substring.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self,
|
||||
radix: Int = 10
|
||||
) -> FromUTF8View<Substring, Parsers.IntParser<Substring.UTF8View, Self>> {
|
||||
.init { Parsers.IntParser<Substring.UTF8View, Self>(radix: radix) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that consumes an integer (with an optional leading `+` or `-` sign for signed integer
|
||||
/// types) from the beginning of a collection of UTF8 code units.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the static `parser()` method on the `FixedWidthInteger` of your choice, e.g. `Int.parser()`,
|
||||
/// `UInt8.parser()`, etc., all of which construct this type.
|
||||
///
|
||||
/// See <doc:Int> for more information about this parser.
|
||||
public struct IntParser<Input: Collection, Output: FixedWidthInteger>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit
|
||||
{
|
||||
/// The radix, or base, to use for converting text to an integer value.
|
||||
public let radix: Int
|
||||
|
||||
@inlinable
|
||||
public init(radix: Int = 10) {
|
||||
precondition((2...36).contains(radix), "Radix not in range 2...36")
|
||||
self.radix = radix
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Output {
|
||||
@inline(__always)
|
||||
func digit(for n: UTF8.CodeUnit) -> Output? {
|
||||
let output: Output
|
||||
switch n {
|
||||
case .init(ascii: "0") ... .init(ascii: "9"):
|
||||
output = Output(n - .init(ascii: "0"))
|
||||
case .init(ascii: "A") ... .init(ascii: "Z"):
|
||||
output = Output(n - .init(ascii: "A") + 10)
|
||||
case .init(ascii: "a") ... .init(ascii: "z"):
|
||||
output = Output(n - .init(ascii: "a") + 10)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return output < self.radix ? output : nil
|
||||
}
|
||||
var length = 0
|
||||
var iterator = input.makeIterator()
|
||||
guard let first = iterator.next() else {
|
||||
throw ParsingError.expectedInput("integer", at: input)
|
||||
}
|
||||
let isPositive: Bool
|
||||
let parsedSign: Bool
|
||||
var overflow = false
|
||||
var output: Output
|
||||
switch (Output.isSigned, first) {
|
||||
case (true, .init(ascii: "-")):
|
||||
parsedSign = true
|
||||
isPositive = false
|
||||
output = 0
|
||||
case (true, .init(ascii: "+")):
|
||||
parsedSign = true
|
||||
isPositive = true
|
||||
output = 0
|
||||
case let (_, n):
|
||||
guard let n = digit(for: n) else {
|
||||
throw ParsingError.expectedInput("integer", at: input)
|
||||
}
|
||||
parsedSign = false
|
||||
isPositive = true
|
||||
output = n
|
||||
}
|
||||
let original = input
|
||||
input.removeFirst()
|
||||
length += 1
|
||||
let radix = Output(self.radix)
|
||||
while let next = iterator.next(), let n = digit(for: next) {
|
||||
input.removeFirst()
|
||||
(output, overflow) = output.multipliedReportingOverflow(by: radix)
|
||||
func overflowError() -> Error {
|
||||
ParsingError.failed(
|
||||
summary: "failed to process \"\(Output.self)\"",
|
||||
label: "overflowed \(Output.max)",
|
||||
from: original,
|
||||
to: input
|
||||
)
|
||||
}
|
||||
guard !overflow else { throw overflowError() }
|
||||
(output, overflow) =
|
||||
isPositive
|
||||
? output.addingReportingOverflow(n)
|
||||
: output.subtractingReportingOverflow(n)
|
||||
guard !overflow else { throw overflowError() }
|
||||
length += 1
|
||||
}
|
||||
guard length > (parsedSign ? 1 : 0)
|
||||
else {
|
||||
throw ParsingError.expectedInput("integer", at: input)
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/// A parser that waits for a call to its ``parse(_:)`` method before running the given closure to
|
||||
/// create a parser for the given input.
|
||||
public final class Lazy<LazyParser: Parser>: Parser {
|
||||
@usableFromInline
|
||||
internal var lazyParser: LazyParser?
|
||||
|
||||
public let createParser: () -> LazyParser
|
||||
|
||||
@inlinable
|
||||
public init(@ParserBuilder createParser: @escaping () -> LazyParser) {
|
||||
self.createParser = createParser
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout LazyParser.Input) rethrows -> LazyParser.Output {
|
||||
guard let parser = self.lazyParser else {
|
||||
let parser = self.createParser()
|
||||
self.lazyParser = parser
|
||||
return try parser.parse(&input)
|
||||
}
|
||||
return try parser.parse(&input)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Lazy = SwiftTL.Lazy // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
extension Array: Parser where Element: Equatable {
|
||||
@inlinable
|
||||
public func parse(_ input: inout ArraySlice<Element>) throws {
|
||||
guard input.starts(with: self) else {
|
||||
throw ParsingError.expectedInput(self.debugDescription, at: input)
|
||||
}
|
||||
input.removeFirst(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String: Parser {
|
||||
@inlinable
|
||||
public func parse(_ input: inout Substring) throws {
|
||||
guard input.starts(with: self) else {
|
||||
throw ParsingError.expectedInput(self.debugDescription, at: input)
|
||||
}
|
||||
input.removeFirst(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String.UnicodeScalarView: Parser {
|
||||
@inlinable
|
||||
public func parse(_ input: inout Substring.UnicodeScalarView) throws {
|
||||
guard input.starts(with: self) else {
|
||||
throw ParsingError.expectedInput(String(self).debugDescription, at: input)
|
||||
}
|
||||
input.removeFirst(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String.UTF8View: Parser {
|
||||
@inlinable
|
||||
public func parse(_ input: inout Substring.UTF8View) throws {
|
||||
guard input.starts(with: self) else {
|
||||
throw ParsingError.expectedInput(String(self).debugDescription, at: input)
|
||||
}
|
||||
input.removeFirst(self.count)
|
||||
}
|
||||
}
|
||||
353
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Many.swift
Normal file
353
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Many.swift
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import Foundation
|
||||
|
||||
/// A parser that attempts to run another parser as many times as specified, accumulating the result
|
||||
/// of the outputs.
|
||||
///
|
||||
/// For example, given a comma-separated string of numbers, one could parse out an array of
|
||||
/// integers:
|
||||
///
|
||||
/// ```swift
|
||||
/// let intsParser = Many {
|
||||
/// Int.parser()
|
||||
/// } separator: {
|
||||
/// ","
|
||||
/// }
|
||||
///
|
||||
/// var input = "1,2,3"[...]
|
||||
/// try intsParser.parse(&input) // [1, 2, 3]
|
||||
/// input // ""
|
||||
/// ```
|
||||
///
|
||||
/// In addition to an element and separator parser, a "terminator" parser that is run after the element
|
||||
/// parser has run as many times as possible. This can be useful for proving that the `Many` parser has
|
||||
/// consumed everything you expect:
|
||||
///
|
||||
/// ```swift
|
||||
/// let intsParser = Many {
|
||||
/// Int.parser()
|
||||
/// } separator: {
|
||||
/// ","
|
||||
/// } terminator: {
|
||||
/// "---"
|
||||
/// }
|
||||
///
|
||||
/// var input = "1,2,3---"[...]
|
||||
/// try intsParser.parse(&input) // [1, 2, 3]
|
||||
/// input // ""
|
||||
/// ```
|
||||
///
|
||||
/// The outputs of the element parser do not need to be accumulated in an array. More generally one can
|
||||
/// specify a closure that customizes how outputs are accumulated, much like `Sequence.reduce(into:_)`. We
|
||||
/// could, for example, sum the numbers as we parse them instead of accumulating each value in an array:
|
||||
///
|
||||
/// ```swift
|
||||
/// let sumParser = Many(into: 0, +=) {
|
||||
/// Int.parser()
|
||||
/// } separator: {
|
||||
/// ","
|
||||
/// }
|
||||
///
|
||||
/// var input = "1,2,3"[...]
|
||||
/// try sumParser.parse(&input) // 6
|
||||
/// input // ""
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails if the terminator parser fails. For example, if we required our comma-separated
|
||||
/// integer parser to be terminated by `"---"`, but we parsed a list that contained a non-integer we would
|
||||
/// get an error:
|
||||
///
|
||||
/// ```swift
|
||||
/// let intsParser = Many {
|
||||
/// Int.parser()
|
||||
/// } separator: {
|
||||
/// ","
|
||||
/// } terminator: {
|
||||
/// "---"
|
||||
/// }
|
||||
/// var input = "1,2,Hello---"[...]
|
||||
/// try intsParser.parse(&input)
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:5
|
||||
/// // 1 | 1,2,Hello---
|
||||
/// // | ^ expected integer
|
||||
/// ```
|
||||
public struct Many<Element: Parser, Result, Separator: Parser, Terminator: Parser>: Parser
|
||||
where
|
||||
Separator.Input == Element.Input,
|
||||
Terminator.Input == Element.Input
|
||||
{
|
||||
public let element: Element
|
||||
public let initialResult: Result
|
||||
public let maximum: Int
|
||||
public let minimum: Int
|
||||
public let separator: Separator
|
||||
public let terminator: Terminator
|
||||
public let updateAccumulatingResult: (inout Result, Element.Output) throws -> Void
|
||||
|
||||
/// Initializes a parser that attempts to run the given parser at least and at most the given
|
||||
/// number of times, accumulating the outputs into a result with a given closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value to use as the initial accumulating value.
|
||||
/// - minimum: The minimum number of times to run this parser and consider parsing to be
|
||||
/// successful.
|
||||
/// - maximum: The maximum number of times to run this parser before returning the output.
|
||||
/// - updateAccumulatingResult: A closure that updates the accumulating result with each output
|
||||
/// of the element parser.
|
||||
/// - element: A parser to run multiple times to accumulate into a result.
|
||||
/// - separator: A parser that consumes input between each parsed output.
|
||||
@inlinable
|
||||
public init(
|
||||
into initialResult: Result,
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
_ updateAccumulatingResult: @escaping (inout Result, Element.Output) throws -> Void,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder separator: () -> Separator,
|
||||
@ParserBuilder terminator: () -> Terminator
|
||||
) {
|
||||
self.element = element()
|
||||
self.initialResult = initialResult
|
||||
self.maximum = maximum
|
||||
self.minimum = minimum
|
||||
self.separator = separator()
|
||||
self.terminator = terminator()
|
||||
self.updateAccumulatingResult = updateAccumulatingResult
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Element.Input) throws -> Result {
|
||||
var rest = input
|
||||
var previous = input
|
||||
var result = self.initialResult
|
||||
var count = 0
|
||||
var loopError: Error?
|
||||
while count < self.maximum {
|
||||
let output: Element.Output
|
||||
do {
|
||||
output = try self.element.parse(&input)
|
||||
} catch {
|
||||
loopError = error
|
||||
break
|
||||
}
|
||||
defer { previous = input }
|
||||
count += 1
|
||||
do {
|
||||
try self.updateAccumulatingResult(&result, output)
|
||||
} catch {
|
||||
throw ParsingError.failed(
|
||||
"",
|
||||
.init(
|
||||
originalInput: previous, remainingInput: input, debugDescription: "\(error)",
|
||||
underlyingError: error)
|
||||
)
|
||||
}
|
||||
rest = input
|
||||
do {
|
||||
_ = try self.separator.parse(&input)
|
||||
} catch {
|
||||
loopError = error
|
||||
break
|
||||
}
|
||||
if memcmp(&input, &previous, MemoryLayout<Element.Input>.size) == 0 {
|
||||
throw ParsingError.failed(
|
||||
"expected input to be consumed",
|
||||
.init(remainingInput: input, debugDescription: "infinite loop", underlyingError: nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
input = rest
|
||||
do {
|
||||
_ = try self.terminator.parse(&input)
|
||||
} catch {
|
||||
if let loopError = loopError {
|
||||
throw ParsingError.manyFailed([loopError, error], at: input)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
guard count >= self.minimum else {
|
||||
let atLeast = self.minimum - count
|
||||
throw ParsingError.expectedInput(
|
||||
"""
|
||||
\(atLeast) \(count == 0 ? "" : "more ")value\(atLeast == 1 ? "" : "s") of \
|
||||
"\(Element.Output.self)"
|
||||
""",
|
||||
at: rest
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Separator == Always<Input, Void>, Terminator == Always<Input, Void> {
|
||||
/// Initializes a parser that attempts to run the given parser at least and at most the given
|
||||
/// number of times, accumulating the outputs into a result with a given closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value to use as the initial accumulating value.
|
||||
/// - minimum: The minimum number of times to run this parser and consider parsing to be
|
||||
/// successful.
|
||||
/// - maximum: The maximum number of times to run this parser before returning the output.
|
||||
/// - updateAccumulatingResult: A closure that updates the accumulating result with each output
|
||||
/// of the element parser.
|
||||
/// - element: A parser to run multiple times to accumulate into a result.
|
||||
@inlinable
|
||||
public init(
|
||||
into initialResult: Result,
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
_ updateAccumulatingResult: @escaping (inout Result, Element.Output) throws -> Void,
|
||||
@ParserBuilder element: () -> Element
|
||||
) {
|
||||
self.element = element()
|
||||
self.initialResult = initialResult
|
||||
self.maximum = maximum
|
||||
self.minimum = minimum
|
||||
self.separator = .init(())
|
||||
self.terminator = .init(())
|
||||
self.updateAccumulatingResult = updateAccumulatingResult
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Separator == Always<Input, Void> {
|
||||
@inlinable
|
||||
public init(
|
||||
into initialResult: Result,
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
_ updateAccumulatingResult: @escaping (inout Result, Element.Output) throws -> Void,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder terminator: () -> Terminator
|
||||
) {
|
||||
self.element = element()
|
||||
self.initialResult = initialResult
|
||||
self.maximum = maximum
|
||||
self.minimum = minimum
|
||||
self.separator = .init(())
|
||||
self.terminator = terminator()
|
||||
self.updateAccumulatingResult = updateAccumulatingResult
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Terminator == Always<Input, Void> {
|
||||
@inlinable
|
||||
public init(
|
||||
into initialResult: Result,
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
_ updateAccumulatingResult: @escaping (inout Result, Element.Output) throws -> Void,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder separator: () -> Separator
|
||||
) {
|
||||
self.element = element()
|
||||
self.initialResult = initialResult
|
||||
self.maximum = maximum
|
||||
self.minimum = minimum
|
||||
self.separator = separator()
|
||||
self.terminator = .init(())
|
||||
self.updateAccumulatingResult = updateAccumulatingResult
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Result == [Element.Output] {
|
||||
/// Initializes a parser that attempts to run the given parser at least and at most the given
|
||||
/// number of times, accumulating the outputs in an array.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - minimum: The minimum number of times to run this parser and consider parsing to be
|
||||
/// successful.
|
||||
/// - maximum: The maximum number of times to run this parser before returning the output.
|
||||
/// - element: A parser to run multiple times to accumulate into an array.
|
||||
/// - separator: A parser that consumes input between each parsed output.
|
||||
@inlinable
|
||||
public init(
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder separator: () -> Separator,
|
||||
@ParserBuilder terminator: () -> Terminator
|
||||
) {
|
||||
self.init(
|
||||
into: [],
|
||||
atLeast: minimum,
|
||||
atMost: maximum,
|
||||
{ $0.append($1) },
|
||||
element: element,
|
||||
separator: separator,
|
||||
terminator: terminator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Many
|
||||
where
|
||||
Result == [Element.Output],
|
||||
Separator == Always<Input, Void>,
|
||||
Terminator == Always<Input, Void>
|
||||
{
|
||||
/// Initializes a parser that attempts to run the given parser at least and at most the given
|
||||
/// number of times, accumulating the outputs in an array.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - minimum: The minimum number of times to run this parser and consider parsing to be
|
||||
/// successful.
|
||||
/// - maximum: The maximum number of times to run this parser before returning the output.
|
||||
/// - element: A parser to run multiple times to accumulate into an array.
|
||||
@inlinable
|
||||
public init(
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
@ParserBuilder element: () -> Element
|
||||
) {
|
||||
self.init(
|
||||
into: [],
|
||||
atLeast: minimum,
|
||||
atMost: maximum,
|
||||
{ $0.append($1) },
|
||||
element: element
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Result == [Element.Output], Separator == Always<Input, Void> {
|
||||
@inlinable
|
||||
public init(
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder terminator: () -> Terminator
|
||||
) {
|
||||
self.init(
|
||||
into: [],
|
||||
atLeast: minimum,
|
||||
atMost: maximum,
|
||||
{ $0.append($1) },
|
||||
element: element,
|
||||
terminator: terminator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Many where Result == [Element.Output], Terminator == Always<Input, Void> {
|
||||
@inlinable
|
||||
public init(
|
||||
atLeast minimum: Int = 0,
|
||||
atMost maximum: Int = .max,
|
||||
@ParserBuilder element: () -> Element,
|
||||
@ParserBuilder separator: () -> Separator
|
||||
) {
|
||||
self.init(
|
||||
into: [],
|
||||
atLeast: minimum,
|
||||
atMost: maximum,
|
||||
{ $0.append($1) },
|
||||
element: element,
|
||||
separator: separator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Many = SwiftTL.Many // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
extension Parser {
|
||||
/// Returns a parser that transforms the output of this parser with a given closure.
|
||||
///
|
||||
/// This method is similar to `Sequence.map`, `Optional.map`, and `Result.map` in the Swift
|
||||
/// standard library, as well as `Publisher.map` in the Combine framework.
|
||||
///
|
||||
/// - Parameter transform: A closure that transforms values of this parser's output.
|
||||
/// - Returns: A parser of transformed outputs.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public func map<NewOutput>(
|
||||
_ transform: @escaping (Output) -> NewOutput
|
||||
) -> Parsers.Map<Self, NewOutput> {
|
||||
.init(upstream: self, transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that transforms the output of another parser with a given closure.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/map(_:)`` operation, which constructs this type.
|
||||
public struct Map<Upstream: Parser, NewOutput>: Parser {
|
||||
/// The parser from which this parser receives output.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that transforms output from the upstream parser.
|
||||
public let transform: (Upstream.Output) -> NewOutput
|
||||
|
||||
@inlinable
|
||||
public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> NewOutput) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
public func parse(_ input: inout Upstream.Input) rethrows -> NewOutput {
|
||||
self.transform(try self.upstream.parse(&input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/// A parser that consumes a single newline from the beginning of the input.
|
||||
///
|
||||
/// - Note: This parser only consumes a line feed (`"\n"`) or a carriage returns with line feed
|
||||
/// (`"\r\n"`). If you need richer support that covers all unicode newline characters, use a
|
||||
/// ``Prefix`` parser that operates on the `Substring` level with a predicate that consumes a
|
||||
/// single newline:
|
||||
///
|
||||
/// ```swift
|
||||
/// Prefix(1) { $0.isNewline }
|
||||
/// ```
|
||||
/// It will consume both line feeds (`"\n"`) and carriage returns with line feeds (`"\r\n"`).
|
||||
public struct Newline<Input: Collection, Bytes: Collection>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Bytes.SubSequence == Bytes,
|
||||
Bytes.Element == UTF8.CodeUnit
|
||||
{
|
||||
@usableFromInline
|
||||
let toBytes: (Input) -> Bytes
|
||||
|
||||
@usableFromInline
|
||||
let fromBytes: (Bytes) -> Input
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws {
|
||||
var bytes = self.toBytes(input)
|
||||
if bytes.first == .init(ascii: "\n") {
|
||||
bytes.removeFirst()
|
||||
} else if bytes.first == .init(ascii: "\r"), bytes.dropFirst().first == .init(ascii: "\n") {
|
||||
bytes.removeFirst(2)
|
||||
} else {
|
||||
throw ParsingError.expectedInput(#""\n" or "\r\n""#, at: input)
|
||||
}
|
||||
input = self.fromBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Swift 5.5.2 on Linux and Windows fails to build with a simpler `Bytes == Input` constraint
|
||||
extension Newline where Bytes == Input.SubSequence, Bytes.SubSequence == Input {
|
||||
@inlinable
|
||||
public init() {
|
||||
self.toBytes = { $0 }
|
||||
self.fromBytes = { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
extension Newline where Input == Substring, Bytes == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {
|
||||
self.toBytes = { $0.utf8 }
|
||||
self.fromBytes = Substring.init
|
||||
}
|
||||
}
|
||||
|
||||
extension Newline where Input == Substring.UTF8View, Bytes == Input {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() { self.init() }
|
||||
}
|
||||
|
||||
extension Newline where Input == ArraySlice<UTF8.CodeUnit>, Bytes == Input {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() { self.init() }
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Newline = SwiftTL.Newline // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/// A parser that succeeds if the given parser fails, and does not consume any input.
|
||||
///
|
||||
/// For example, to parse a line from an input that does not start with "//" one can do:
|
||||
///
|
||||
/// ```swift
|
||||
/// let uncommentedLine = Parse {
|
||||
/// Not { "//" }
|
||||
/// PrefixUpTo("\n")
|
||||
/// }
|
||||
///
|
||||
/// try uncommentedLine.parse("let x = 1") // ✅ "let x = 1"
|
||||
///
|
||||
/// try uncommentedLine.parse("// let x = 1") // ❌
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1-2
|
||||
/// // 1 | // let x = 1
|
||||
/// // | ^^ expected not to be processed
|
||||
/// ```
|
||||
public struct Not<Upstream: Parser>: Parser {
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Creates a parser that succeeds if the given parser fails, and does not consume any input.
|
||||
///
|
||||
/// - Parameter build: A parser that causes this parser to fail if it succeeds, or succeed if it
|
||||
/// fails.
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> Upstream) {
|
||||
self.upstream = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) throws {
|
||||
let original = input
|
||||
do {
|
||||
_ = try self.upstream.parse(&input)
|
||||
} catch {
|
||||
input = original
|
||||
return
|
||||
}
|
||||
throw ParsingError.expectedInput("not to be processed", from: original, to: input)
|
||||
}
|
||||
}
|
||||
166
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/OneOf.swift
Normal file
166
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/OneOf.swift
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/// A parser that attempts to run a number of parsers till one succeeds.
|
||||
///
|
||||
/// Use this parser to list out a number of parsers in a ``OneOfBuilder`` result builder block.
|
||||
///
|
||||
/// The following example uses ``OneOf`` to parse an enum value. To do so, it spells out a list of
|
||||
/// parsers to `OneOf`, one for each case:
|
||||
///
|
||||
/// ```swift
|
||||
/// enum Currency { case eur, gbp, usd }
|
||||
///
|
||||
/// let currency = OneOf {
|
||||
/// "€".map { Currency.eur }
|
||||
/// "£".map { Currency.gbp }
|
||||
/// "$".map { Currency.usd }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails if every parser inside fails:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "London, Hello!"[...]
|
||||
/// try OneOf { "New York"; "Berlin" }.parse(&input)
|
||||
///
|
||||
/// // error: multiple failures occurred
|
||||
/// //
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | London, Hello!
|
||||
/// // | ^ expected "New York"
|
||||
/// // | ^ expected "Berlin"
|
||||
/// ```
|
||||
///
|
||||
/// If you are parsing input that should coalesce into some default, avoid using a final ``Always``
|
||||
/// parser, and instead opt for a trailing ``replaceError(with:)``, which returns a parser that
|
||||
/// cannot fail:
|
||||
///
|
||||
/// ```swift
|
||||
/// enum Currency { case eur, gbp, usd, unknown }
|
||||
///
|
||||
/// let currency = OneOf {
|
||||
/// "€".map { Currency.eur }
|
||||
/// "£".map { Currency.gbp }
|
||||
/// "$".map { Currency.usd }
|
||||
/// }
|
||||
/// .replaceError(with: Currency.unknown)
|
||||
///
|
||||
/// currency.parse("$") // Currency.usd
|
||||
/// currency.parse("฿") // Currency.unknown
|
||||
/// ```
|
||||
///
|
||||
/// ## Specificity
|
||||
///
|
||||
/// The order of the parsers in the above ``OneOf`` does not matter because each of "€", "£" and "$"
|
||||
/// are mutually exclusive, i.e. at most one will succeed on any given input.
|
||||
///
|
||||
/// However, that is not always true, and when the parsers are not mutually exclusive (i.e. multiple
|
||||
/// can succeed on a given input) you must order them from most specific to least specific. That is,
|
||||
/// the first parser should succeed on the fewest number of inputs and the last parser should
|
||||
/// succeed on the most number of inputs.
|
||||
///
|
||||
/// For example, suppose you wanted to parse a simple CSV format into a doubly-nested array of
|
||||
/// strings, and the fields in the CSV are allowed to contain commas themselves as long as they
|
||||
/// are quoted:
|
||||
///
|
||||
/// ```swift
|
||||
/// let input = #"""
|
||||
/// lastName,firstName
|
||||
/// McBlob,Blob
|
||||
/// "McBlob, Esq.",Blob Jr.
|
||||
/// "McBlob, MD",Blob Sr.
|
||||
/// """#
|
||||
/// ```
|
||||
///
|
||||
/// Here we have a list of last and first names separated by a comma, and some of the last names are
|
||||
/// quoted because they contain commas.
|
||||
///
|
||||
/// In order to safely parse this we must first try parsing a field as a quoted field, and then only
|
||||
/// if that fails we can parse a plain field that takes everything up until the next comma or
|
||||
/// newline:
|
||||
///
|
||||
/// ```swift
|
||||
/// let quotedField = Parse {
|
||||
/// "\""
|
||||
/// Prefix { $0 != "\"" }
|
||||
/// "\""
|
||||
/// }
|
||||
/// let plainField = Prefix { $0 != "," && $0 != "\n" }
|
||||
///
|
||||
/// let field = OneOf {
|
||||
/// quotedField
|
||||
/// plainField
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Then we can parse many fields to form an array of fields making up a line, and then parse many
|
||||
/// lines to make up a full, doubly-nested array for the CSV:
|
||||
///
|
||||
/// ```swift
|
||||
/// let line = Many { field } separator: { "," }
|
||||
/// let csv = Many { line } separator: { "\n" }
|
||||
/// ```
|
||||
///
|
||||
/// Running this parser on the input shows that it properly isolates each field of the CSV, even
|
||||
/// fields that are quoted and contain a comma:
|
||||
///
|
||||
/// ```swift
|
||||
/// XCTAssertEqual(
|
||||
/// try csv.parse(input),
|
||||
/// [
|
||||
/// ["lastName", "firstName"],
|
||||
/// ["McBlob", "Blob"],
|
||||
/// ["McBlob, Esq.", "Blob Jr."],
|
||||
/// ["McBlob, MD", "Blob Sr."],
|
||||
/// ]
|
||||
/// )
|
||||
/// // ✅
|
||||
/// ```
|
||||
///
|
||||
/// The reason this parser works is because the `quotedField` and `plainField` parsers are listed in
|
||||
/// a very specific order inside the `OneOf`:
|
||||
///
|
||||
/// ```swift
|
||||
/// let field = OneOf {
|
||||
/// quotedField
|
||||
/// plainField
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `quotedField` parser is a _more_ specific parser in that it will succeed on fewer inputs
|
||||
/// than the `plainField` parser does. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try quotedField.parse("Blob Jr.") // ❌
|
||||
/// try plainField.parse("Blob Jr.") // ✅
|
||||
/// ```
|
||||
///
|
||||
/// Whereas the `plainField` parser will happily succeed on anything the `quotedField` parser will
|
||||
/// succeed on:
|
||||
///
|
||||
/// ```swift
|
||||
/// try quotedField.parse("\"Blob, Esq\"") // ✅
|
||||
/// try plainField.parse("\"Blob, Esq\"") // ✅
|
||||
/// ```
|
||||
///
|
||||
/// For this reason the `quotedField` parser must be listed first so that it can try its logic
|
||||
/// first, which succeeds less frequently, before then trying the `plainField` parser, which
|
||||
/// succeeds more often.
|
||||
///
|
||||
/// ## Backtracking
|
||||
///
|
||||
/// The ``OneOf`` parser is the primary tool for introducing backtracking into your parsers,
|
||||
/// which means to undo the consumption of a parser when it fails. For more information, see the
|
||||
/// article <doc:Backtracking>.
|
||||
public struct OneOf<Parsers>: Parser where Parsers: Parser {
|
||||
public let parsers: Parsers
|
||||
|
||||
@inlinable
|
||||
public init(@OneOfBuilder _ build: () -> Parsers) {
|
||||
self.parsers = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Parsers.Input) rethrows -> Parsers.Output {
|
||||
try self.parsers.parse(&input)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
extension Parsers {
|
||||
/// A parser that attempts to run a number of parsers till one succeeds.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually loop
|
||||
/// over each parser in a builder block:
|
||||
///
|
||||
/// ```swift
|
||||
/// enum Role: String, CaseIterable {
|
||||
/// case admin
|
||||
/// case guest
|
||||
/// case member
|
||||
/// }
|
||||
///
|
||||
/// let roleParser = OneOf {
|
||||
/// for role in Role.allCases {
|
||||
/// status.rawValue.map { role }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
public struct OneOfMany<Parsers: Parser>: Parser {
|
||||
public let parsers: [Parsers]
|
||||
|
||||
@inlinable
|
||||
public init(_ parsers: [Parsers]) {
|
||||
self.parsers = parsers
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
public func parse(_ input: inout Parsers.Input) throws -> Parsers.Output {
|
||||
let original = input
|
||||
var count = self.parsers.count
|
||||
var errors: [Error] = []
|
||||
errors.reserveCapacity(count)
|
||||
for parser in self.parsers {
|
||||
do {
|
||||
return try parser.parse(&input)
|
||||
} catch {
|
||||
count -= 1
|
||||
if count > 0 { input = original }
|
||||
errors.append(error)
|
||||
}
|
||||
}
|
||||
throw ParsingError.manyFailed(errors, at: original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
extension Optional: Parser where Wrapped: Parser {
|
||||
public func parse(_ input: inout Wrapped.Input) rethrows -> Wrapped.Output? {
|
||||
guard let self = self
|
||||
else { return nil }
|
||||
|
||||
return try self.parse(&input)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that attempts to run a given void parser, succeeding with void.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// `if` statements in parser builder blocks:
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// "Hello"
|
||||
/// if useComma {
|
||||
/// ","
|
||||
/// }
|
||||
/// " "
|
||||
/// Rest()
|
||||
/// }
|
||||
/// ```
|
||||
public struct OptionalVoid<Wrapped: Parser>: Parser where Wrapped.Output == Void {
|
||||
let wrapped: Wrapped?
|
||||
|
||||
public init(wrapped: Wrapped?) {
|
||||
self.wrapped = wrapped
|
||||
}
|
||||
|
||||
public func parse(_ input: inout Wrapped.Input) rethrows {
|
||||
guard let wrapped = self.wrapped
|
||||
else { return }
|
||||
|
||||
try wrapped.parse(&input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/// A parser that runs the given parser and succeeds with `nil` if it fails.
|
||||
///
|
||||
/// Use this parser when you are parsing into an output data model that contains `nil`.
|
||||
///
|
||||
/// When the wrapped parser fails ``Optionally`` will backtrack any consumption of the input so
|
||||
/// that later parsers can attempt to parser the input:
|
||||
///
|
||||
/// ```swift
|
||||
/// let parser = Parse {
|
||||
/// "Hello,"
|
||||
/// Optionally { " "; Bool.parser() }
|
||||
/// " world!"
|
||||
/// }
|
||||
///
|
||||
/// try parser.parse("Hello, world!") // nil 1️⃣
|
||||
/// try parser.parse("Hello, true world!") // true
|
||||
/// ```
|
||||
///
|
||||
/// If ``Optionally`` did not backtrack then 1️⃣ would fail because it would consume a space,
|
||||
/// causing the `" world!"` parser to fail since there is no longer any space to consume.
|
||||
/// Read the article <doc:Backtracking> to learn more about how backtracking works.
|
||||
///
|
||||
/// If you are optionally parsing input that should coalesce into some default, you can skip the
|
||||
/// optionality and instead use ``replaceError(with:)`` with a default:
|
||||
///
|
||||
/// ```swift
|
||||
/// Optionally { Int.parser() }
|
||||
/// .map { $0 ?? 0 }
|
||||
///
|
||||
/// // vs.
|
||||
///
|
||||
/// Int.parser()
|
||||
/// .replaceError(with: 0)
|
||||
/// ```
|
||||
public struct Optionally<Wrapped: Parser>: Parser {
|
||||
public let wrapped: Wrapped
|
||||
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> Wrapped) {
|
||||
self.wrapped = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Wrapped.Input) -> Wrapped.Output? {
|
||||
let original = input
|
||||
do {
|
||||
return try self.wrapped.parse(&input)
|
||||
} catch {
|
||||
input = original
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/// A parser that attempts to run a number of parsers to accumulate their outputs.
|
||||
///
|
||||
/// A general entry point into ``ParserBuilder`` syntax, which can be used to build complex parsers
|
||||
/// from simpler ones.
|
||||
///
|
||||
/// ```swift
|
||||
/// let point = Parse {
|
||||
/// "("
|
||||
/// Int.parser()
|
||||
/// ","
|
||||
/// Int.parser()
|
||||
/// ")"
|
||||
/// }
|
||||
///
|
||||
/// try point.parse("(2,-4)") // (2, -4)
|
||||
///
|
||||
/// try point.parse("(42,blob)")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:5
|
||||
/// // 1 | (42,blob)
|
||||
/// // | ^ expected integer
|
||||
/// ```
|
||||
public struct Parse<Parsers: Parser>: Parser {
|
||||
public let parsers: Parsers
|
||||
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> Parsers) {
|
||||
self.parsers = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init<Upstream, NewOutput>(
|
||||
_ transform: @escaping (Upstream.Output) -> NewOutput,
|
||||
@ParserBuilder with build: () -> Upstream
|
||||
) where Parsers == SwiftTL.Parsers.Map<Upstream, NewOutput> {
|
||||
self.parsers = build().map(transform)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init<Upstream, NewOutput>(
|
||||
_ output: NewOutput,
|
||||
@ParserBuilder with build: () -> Upstream
|
||||
) where Upstream.Output == Void, Parsers == SwiftTL.Parsers.Map<Upstream, NewOutput> {
|
||||
self.parsers = build().map { output }
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Parsers.Input) rethrows -> Parsers.Output {
|
||||
try self.parsers.parse(&input)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/// A namespace for types that serve as parsers.
|
||||
///
|
||||
/// The various operators defined as extensions on ``Parser`` implement their functionality as
|
||||
/// classes or structures that extend this enumeration. For example, the ``Parser/map(_:)`` operator
|
||||
/// returns a ``Map`` parser.
|
||||
public enum Parsers {}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/// A parser that runs the given parser, but does not consume any input.
|
||||
///
|
||||
/// It lets the upstream parser "peek" into the input without consuming it.
|
||||
///
|
||||
/// For example, identifiers (variables, functions, etc.) in Swift allow the first character to be a
|
||||
/// letter or underscore, but not a digit, but subsequent characters can be digits. _E.g._, `foo123`
|
||||
/// is a valid identifier, but `123foo` is not. We can create an identifier parser by using `Peek`
|
||||
/// to first check if the input starts with a letter or underscore, and if it does, return the
|
||||
/// remainder of the input up to the first character that is not a letter, a digit, or an
|
||||
/// underscore.
|
||||
///
|
||||
/// ```swift
|
||||
/// let identifier = Parse {
|
||||
/// Peek { Prefix(1) { $0.isLetter || $0 == "_" } }
|
||||
/// Prefix { $0.isNumber || $0.isLetter || $0 == "_" }
|
||||
/// }
|
||||
///
|
||||
/// try identifier.parse("foo123") // ✅ "foo123"
|
||||
/// try identifier.parse("_foo123") // ✅ "_foo123"
|
||||
/// try identifier.parse("1_foo123") // ❌
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | 1_foo123
|
||||
/// // | ^ expected 1 element satisfying predicate
|
||||
/// ```
|
||||
public struct Peek<Upstream: Parser>: Parser {
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Construct a parser that runs the given parser, but does not consume any input.
|
||||
///
|
||||
/// - Parameter build: A parser this parser wants to inspect.
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> Upstream) {
|
||||
self.upstream = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) rethrows {
|
||||
let original = input
|
||||
_ = try self.upstream.parse(&input)
|
||||
input = original
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
extension Parser {
|
||||
/// Returns a parser that runs this parser, pipes its output into the given parser, and returns
|
||||
/// the output of the given parser.
|
||||
///
|
||||
/// For example, we can try to parse an integer of exactly 4 digits by piping the output of
|
||||
/// ``Prefix`` into an `Int.parser()`:
|
||||
///
|
||||
/// ```swift
|
||||
/// let year = Prefix(4).pipe { Int.parser() }
|
||||
///
|
||||
/// try year.parse("2022") // 2022
|
||||
/// try year.parse("0123") // 123
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails if either the upstream or downstream parser fails. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try year.parse("123")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:4
|
||||
/// // 1 | 123
|
||||
/// // | ^ expected 1 more element
|
||||
///
|
||||
/// try year.parse("fail!")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1-4
|
||||
/// // 1 | fail!
|
||||
/// // | ^^^^ pipe: expected integer
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter downstream: A parser that parses the output of this parser.
|
||||
/// - Returns: A parser that pipes this parser's output into another parser. @inlinable
|
||||
public func pipe<Downstream>(
|
||||
@ParserBuilder _ build: () -> Downstream
|
||||
) -> Parsers.Pipe<Self, Downstream> {
|
||||
.init(upstream: self, downstream: build())
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that runs this parser, pipes its output into the given parser, and returns the output
|
||||
/// of the given parser.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/pipe(_:)`` operation, which constructs this type.
|
||||
public struct Pipe<Upstream: Parser, Downstream: Parser>: Parser
|
||||
where Upstream.Output == Downstream.Input {
|
||||
public let upstream: Upstream
|
||||
public let downstream: Downstream
|
||||
|
||||
@inlinable
|
||||
public init(upstream: Upstream, downstream: Downstream) {
|
||||
self.upstream = upstream
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) rethrows -> Downstream.Output {
|
||||
let original = input
|
||||
var downstreamInput = try self.upstream.parse(&input)
|
||||
do {
|
||||
return try self.downstream.parse(&downstreamInput)
|
||||
} catch let ParsingError.failed(reason, context) {
|
||||
throw ParsingError.failed(
|
||||
"pipe: \(reason)",
|
||||
.init(
|
||||
originalInput: original,
|
||||
remainingInput: input,
|
||||
debugDescription: context.debugDescription,
|
||||
underlyingError: ParsingError.failed(reason, context)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Prefix.swift
Normal file
286
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/Prefix.swift
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
/// A parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// This parser is named after `Sequence.prefix`, which it uses under the hood to consume a number
|
||||
/// of elements and return them as output. It can be configured with minimum and maximum lengths,
|
||||
/// as well as a predicate.
|
||||
///
|
||||
/// For example, to parse as many numbers off the beginning of a substring:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "123 hello world"[...]
|
||||
/// try Prefix { $0.isNumber }.parse(&input) // "123"
|
||||
/// input // " Hello world"
|
||||
/// ```
|
||||
///
|
||||
/// If you wanted this parser to fail if _no_ numbers are consumed, you could introduce a minimum
|
||||
/// length.
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "No numbers here"[...]
|
||||
/// try Prefix(1...) { $0.isNumber }.parse(&input)
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | No numbers here
|
||||
/// // | ^ expected 1 element satisfying predicate
|
||||
/// ```
|
||||
///
|
||||
/// If a predicate is not provided, the parser will simply consume the prefix within the minimum and
|
||||
/// maximum lengths provided:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Lorem ipsum dolor"[...]
|
||||
/// try Prefix(2).parse(&input) // "Lo"
|
||||
/// input // "rem ipsum dolor"
|
||||
/// ```
|
||||
public struct Prefix<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
public let maxLength: Int?
|
||||
public let minLength: Int
|
||||
public let predicate: ((Input.Element) -> Bool)?
|
||||
|
||||
/// Initializes a parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - minLength: The minimum number of elements to consume for parsing to be considered
|
||||
/// successful.
|
||||
/// - maxLength: The maximum number of elements to consume before the parser will return its
|
||||
/// output.
|
||||
/// - predicate: A closure that takes an element of the input sequence as its argument and
|
||||
/// returns `true` if the element should be included or `false` if it should be excluded. Once
|
||||
/// the predicate returns `false` it will not be called again.
|
||||
@inlinable
|
||||
public init(
|
||||
minLength: Int = 0,
|
||||
maxLength: Int? = nil,
|
||||
while predicate: @escaping (Input.Element) -> Bool
|
||||
) {
|
||||
self.minLength = minLength
|
||||
self.maxLength = maxLength
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
/// Initializes a parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// ```swift
|
||||
/// try Prefix(2...4, while: \.isNumber).parse("123456") // "1234"
|
||||
/// try Prefix(2...4, while: \.isNumber).parse("123") // "123"
|
||||
///
|
||||
/// try Prefix(2...4, while: \.isNumber).parse("1")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | 1
|
||||
/// // | ^ expected 1 more element satisfying predicate
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - length: A closed range that provides a minimum number and maximum of elements to consume
|
||||
/// for parsing to be considered successful.
|
||||
/// - predicate: An optional closure that takes an element of the input sequence as its argument
|
||||
/// and returns `true` if the element should be included or `false` if it should be excluded.
|
||||
/// Once the predicate returns `false` it will not be called again.
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: ClosedRange<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.minLength = length.lowerBound
|
||||
self.maxLength = length.upperBound
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
/// Initializes a parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// ```swift
|
||||
/// try Prefix(4, while: \.isNumber).parse("123456") // "1234"
|
||||
///
|
||||
/// try Prefix(4, while: \.isNumber).parse("123")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | 123
|
||||
/// // | ^ expected 1 more element satisfying predicate
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - length: An exact number of elements to consume for parsing to be considered successful.
|
||||
/// - predicate: An optional closure that takes an element of the input sequence as its argument
|
||||
/// and returns `true` if the element should be included or `false` if it should be excluded.
|
||||
/// Once the predicate returns `false` it will not be called again.
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: Int,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.minLength = length
|
||||
self.maxLength = length
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
/// Initializes a parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// ``` swift
|
||||
/// try Prefix(4..., while: \.isNumber).parse("123456") // "123456"
|
||||
///
|
||||
/// try Prefix(4..., while: \.isNumber).parse("123")
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | 123
|
||||
/// // | ^ expected 1 more element satisfying predicate
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - length: A partial range that provides a minimum number of elements to consume for
|
||||
/// parsing to be considered successful.
|
||||
/// - predicate: An optional closure that takes an element of the input sequence as its argument
|
||||
/// and returns `true` if the element should be included or `false` if it should be excluded.
|
||||
/// Once the predicate returns `false` it will not be called again.
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeFrom<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.minLength = length.lowerBound
|
||||
self.maxLength = nil
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
/// Initializes a parser that consumes a subsequence from the beginning of its input.
|
||||
///
|
||||
/// ```swift
|
||||
/// try Prefix(...4, while: \.isNumber).parse("123456") // "1234"
|
||||
/// try Prefix(...4, while: \.isNumber).parse("123") // "123"
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - length: A partial, inclusive range that provides a maximum number of elements to consume.
|
||||
/// - predicate: An optional closure that takes an element of the input sequence as its argument
|
||||
/// and returns `true` if the element should be included or `false` if it should be excluded.
|
||||
/// Once the predicate returns `false` it will not be called again.
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeThrough<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.minLength = 0
|
||||
self.maxLength = length.upperBound
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
public func parse(_ input: inout Input) throws -> Input {
|
||||
var prefix = maxLength.map(input.prefix) ?? input
|
||||
prefix = predicate.map { prefix.prefix(while: $0) } ?? prefix
|
||||
let count = prefix.count
|
||||
input.removeFirst(count)
|
||||
guard count >= self.minLength else {
|
||||
let atLeast = self.minLength - count
|
||||
throw ParsingError.expectedInput(
|
||||
"""
|
||||
\(self.minLength - count) \(count == 0 ? "" : "more ")element\(atLeast == 1 ? "" : "s")\
|
||||
\(predicate == nil ? "" : " satisfying predicate")
|
||||
""",
|
||||
at: input
|
||||
)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
extension Prefix where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
minLength: Int = 0,
|
||||
maxLength: Int? = nil,
|
||||
while predicate: @escaping (Input.Element) -> Bool
|
||||
) {
|
||||
self.init(minLength: minLength, maxLength: maxLength, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: ClosedRange<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: Int,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeFrom<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeThrough<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Prefix where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
minLength: Int = 0,
|
||||
maxLength: Int? = nil,
|
||||
while predicate: @escaping (Input.Element) -> Bool
|
||||
) {
|
||||
self.init(minLength: minLength, maxLength: maxLength, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: ClosedRange<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: Int,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeFrom<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(
|
||||
_ length: PartialRangeThrough<Int>,
|
||||
while predicate: ((Input.Element) -> Bool)? = nil
|
||||
) {
|
||||
self.init(length, while: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Prefix = SwiftTL.Prefix // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/// A parser that consumes a subsequence from the beginning of its input through a given sequence of
|
||||
/// elements.
|
||||
///
|
||||
/// This parser is named after `Sequence.prefix(through:)`, and uses similar logic under the hood to
|
||||
/// consume and return input through a particular subsequence.
|
||||
///
|
||||
/// ```swift
|
||||
/// let lineParser = PrefixThrough("\n")
|
||||
///
|
||||
/// var input = "Hello\nworld\n"[...]
|
||||
/// try line.parse(&input) // "Hello\n"
|
||||
/// input // "world\n"
|
||||
/// ```
|
||||
public struct PrefixThrough<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
public let possibleMatch: Input
|
||||
public let areEquivalent: (Input.Element, Input.Element) -> Bool
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
_ possibleMatch: Input,
|
||||
by areEquivalent: @escaping (Input.Element, Input.Element) -> Bool
|
||||
) {
|
||||
self.possibleMatch = possibleMatch
|
||||
self.areEquivalent = areEquivalent
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
public func parse(_ input: inout Input) throws -> Input {
|
||||
guard let first = self.possibleMatch.first else { return self.possibleMatch }
|
||||
let count = self.possibleMatch.count
|
||||
let original = input
|
||||
while let index = input.firstIndex(where: { self.areEquivalent(first, $0) }) {
|
||||
input = input[index...]
|
||||
if input.count >= count,
|
||||
zip(input[index...], self.possibleMatch).allSatisfy(self.areEquivalent)
|
||||
{
|
||||
let index = input.index(index, offsetBy: count)
|
||||
input = input[index...]
|
||||
return original[..<index]
|
||||
}
|
||||
input.removeFirst()
|
||||
}
|
||||
throw ParsingError.expectedInput("prefix through \(formatValue(self.possibleMatch))", at: input)
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixThrough where Input.Element: Equatable {
|
||||
@inlinable
|
||||
public init(_ possibleMatch: Input) {
|
||||
self.init(possibleMatch, by: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixThrough where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(_ possibleMatch: String) {
|
||||
self.init(possibleMatch[...])
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixThrough where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(_ possibleMatch: String.UTF8View) {
|
||||
self.init(String(possibleMatch)[...].utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias PrefixThrough = SwiftTL.PrefixThrough // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/// A parser that consumes a subsequence from the beginning of its input up to a given sequence of
|
||||
/// elements.
|
||||
///
|
||||
/// This parser is named after `Sequence.prefix(upTo:)`, and uses similar logic under the hood to
|
||||
/// consume and return input up to a particular subsequence.
|
||||
///
|
||||
/// ```swift
|
||||
/// let lineParser = PrefixUpTo("\n")
|
||||
///
|
||||
/// var input = "Hello\nworld\n"[...]
|
||||
/// try line.parse(&input) // "Hello"
|
||||
/// input // "\nworld\n"
|
||||
/// ```
|
||||
public struct PrefixUpTo<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
public let possibleMatch: Input
|
||||
public let areEquivalent: (Input.Element, Input.Element) -> Bool
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
_ possibleMatch: Input,
|
||||
by areEquivalent: @escaping (Input.Element, Input.Element) -> Bool
|
||||
) {
|
||||
self.possibleMatch = possibleMatch
|
||||
self.areEquivalent = areEquivalent
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
public func parse(_ input: inout Input) throws -> Input {
|
||||
guard let first = self.possibleMatch.first else { return self.possibleMatch }
|
||||
let count = self.possibleMatch.count
|
||||
let original = input
|
||||
while let index = input.firstIndex(where: { self.areEquivalent(first, $0) }) {
|
||||
input = input[index...]
|
||||
if input.count >= count,
|
||||
zip(input[index...], self.possibleMatch).allSatisfy(self.areEquivalent)
|
||||
{
|
||||
return original[..<index]
|
||||
}
|
||||
input.removeFirst()
|
||||
}
|
||||
throw ParsingError.expectedInput("prefix up to \(formatValue(self.possibleMatch))", at: input)
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixUpTo where Input.Element: Equatable {
|
||||
@inlinable
|
||||
public init(_ possibleMatch: Input) {
|
||||
self.init(possibleMatch, by: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixUpTo where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(_ possiblePrefix: String) {
|
||||
self.init(possiblePrefix[...])
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefixUpTo where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init(_ possibleMatch: String.UTF8View) {
|
||||
self.init(String(possibleMatch)[...].utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias PrefixUpTo = SwiftTL.PrefixUpTo // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
extension Parser {
|
||||
/// Transforms the `Input` of a parser.
|
||||
///
|
||||
/// This operator allows you to transform a parser of `Input`s into one on `NewInput`s, via a
|
||||
/// writable key path from `NewInput` to `Input`. Intuitively you can think of this as a way of
|
||||
/// transforming a parser on local data into one on more global data.
|
||||
///
|
||||
/// For example, the parser `Int.parser()` parses `Substring.UTF8View` collections into integers,
|
||||
/// and there's a key path from `Substring.UTF8View` to `Substring`, and so we can `pullback`:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "123 Hello world"[...]
|
||||
/// let output = try Int.parser().pullback(\.utf8).parse(&input) // 123
|
||||
/// input // " Hello world"
|
||||
/// ```
|
||||
///
|
||||
/// This has allowed us to parse `Substring`s with something that is only defined on
|
||||
/// `Substring.UTF8View`.
|
||||
///
|
||||
/// - Parameter keyPath: A key path to pull parsing back along from this parser's input to a new
|
||||
/// input.
|
||||
/// - Returns: A parser that parses new input.
|
||||
@inlinable
|
||||
public func pullback<NewInput>(
|
||||
_ keyPath: WritableKeyPath<NewInput, Input>
|
||||
) -> Parsers.Pullback<Self, NewInput> {
|
||||
.init(downstream: self, keyPath: keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// Transforms the `Input` of a downstream parser.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/pullback(_:)`` operator, which constructs this type.
|
||||
public struct Pullback<Downstream: Parser, Input>: Parser {
|
||||
public let downstream: Downstream
|
||||
public let keyPath: WritableKeyPath<Input, Downstream.Input>
|
||||
|
||||
@inlinable
|
||||
public init(downstream: Downstream, keyPath: WritableKeyPath<Input, Downstream.Input>) {
|
||||
self.downstream = downstream
|
||||
self.keyPath = keyPath
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) rethrows -> Downstream.Output {
|
||||
try self.downstream.parse(&input[keyPath: self.keyPath])
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func pullback<NewInput>(
|
||||
_ keyPath: WritableKeyPath<NewInput, Input>
|
||||
) -> Pullback<Downstream, NewInput> {
|
||||
.init(downstream: self.downstream, keyPath: keyPath.appending(path: self.keyPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
extension Parser {
|
||||
/// A parser that replaces its error with a provided output.
|
||||
///
|
||||
/// Useful for providing a default output for a parser.
|
||||
///
|
||||
/// For example, we could create a parser that parses a plus or minus sign and maps the result to
|
||||
/// a positive or negative multiplier respectively, or else defaults to a positive multiplier:
|
||||
///
|
||||
/// ```swift
|
||||
/// let sign = OneOf {
|
||||
/// "+".map { 1 }
|
||||
/// "-".map { -1 }
|
||||
/// }
|
||||
/// .replaceError(with: 1)
|
||||
/// ```
|
||||
///
|
||||
/// Notably this parser is non-throwing:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "-123"[...]
|
||||
///
|
||||
/// // No `try` required:
|
||||
/// sign.parse(&input) // -1
|
||||
/// input // "123"
|
||||
///
|
||||
/// // Simply returns the default when parsing fails:
|
||||
/// sign.parse(&input) // 1
|
||||
/// ```
|
||||
///
|
||||
/// This means it can be used to turn throwing parsers into non-throwing ones, which is important
|
||||
/// for building up complex parsers that cannot fail.
|
||||
///
|
||||
/// - Parameter output: An output to return should the upstream parser fail.
|
||||
/// - Returns: A parser that never fails.
|
||||
@inlinable
|
||||
public func replaceError(with output: Output) -> Parsers.ReplaceError<Self> {
|
||||
.init(output: output, upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that replaces its error with a provided output.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// the ``Parser/replaceError(with:)`` operation, which constructs this type.
|
||||
public struct ReplaceError<Upstream: Parser>: Parser {
|
||||
@usableFromInline
|
||||
let output: Upstream.Output
|
||||
|
||||
@usableFromInline
|
||||
let upstream: Upstream
|
||||
|
||||
@usableFromInline
|
||||
init(output: Upstream.Output, upstream: Upstream) {
|
||||
self.output = output
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Upstream.Input) -> Upstream.Output {
|
||||
let original = input
|
||||
do {
|
||||
return try self.upstream.parse(&input)
|
||||
} catch {
|
||||
input = original
|
||||
return self.output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/// A parser that consumes everything to the end of the collection and returns this subsequence as
|
||||
/// its output.
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Hello"[...]
|
||||
/// Rest().parse(&input) // "Hello"
|
||||
/// input // ""
|
||||
/// ```
|
||||
///
|
||||
/// This parser fails if there is no input to consume:
|
||||
///
|
||||
/// ```swift
|
||||
/// try Rest().parse("")
|
||||
///
|
||||
/// /// error: unexpected input
|
||||
/// /// --> input:1:1
|
||||
/// /// 1 |
|
||||
/// /// | ^ expected a non-empty input
|
||||
/// ```
|
||||
///
|
||||
/// If you want to allow for the possibility of an empty remaining input you can use the
|
||||
/// ``Optionally`` parser to parse an optional output value, or the ``replaceError(with:)`` method
|
||||
/// to coalesce the error into a default output value.
|
||||
public struct Rest<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> Input {
|
||||
guard !input.isEmpty
|
||||
else { throw ParsingError.expectedInput("a non-empty input", at: input) }
|
||||
let output = input
|
||||
input.removeFirst(input.count)
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
extension Rest where Input == Substring {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
extension Rest where Input == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Rest = SwiftTL.Rest // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/// A parser that discards the output of another parser.
|
||||
public struct Skip<Parsers: Parser>: Parser {
|
||||
/// The parser from which this parser receives output.
|
||||
public let parsers: Parsers
|
||||
|
||||
@inlinable
|
||||
public init(@ParserBuilder _ build: () -> Parsers) {
|
||||
self.parsers = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Parsers.Input) rethrows {
|
||||
_ = try self.parsers.parse(&input)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Skip = SwiftTL.Skip // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/// A parser that parses a sequence of elements from its input.
|
||||
///
|
||||
/// This parser is named after `Sequence.starts(with:)`, and tests that the input it is parsing
|
||||
/// starts with a given subsequence by calling this method under the hood.
|
||||
///
|
||||
/// If `true`, it consumes this prefix and returns `Void`:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Hello, Blob!"[...]
|
||||
///
|
||||
/// StartsWith("Hello, ").parse(&input) // ()
|
||||
/// input // "Blob!"
|
||||
/// ```
|
||||
///
|
||||
/// If `false`, it fails and leaves input intact:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Goodnight, Blob!"[...]
|
||||
/// try StartsWith("Hello, ").parse(&input)
|
||||
/// // error: unexpected input
|
||||
/// // --> input:1:1
|
||||
/// // 1 | Goodnight, Blob!
|
||||
/// // | ^ expected "Hello, "
|
||||
/// ```
|
||||
///
|
||||
/// This parser returns `Void` and _not_ the sequence of elements it consumes because the sequence
|
||||
/// is already known at the time the parser is created (it is the value quite literally passed to
|
||||
/// ``StartsWith/init(_:)``).
|
||||
///
|
||||
/// In many circumstances you can omit the `StartsWith` parser entirely and just use the collection
|
||||
/// as the parser. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "Hello, Blob!"[...]
|
||||
///
|
||||
/// try "Hello, ".parse(&input) // ()
|
||||
/// input // "Blob!"
|
||||
/// ```
|
||||
public struct StartsWith<Input: Collection>: Parser where Input.SubSequence == Input {
|
||||
public let count: Int
|
||||
public let possiblePrefix: AnyCollection<Input.Element>
|
||||
public let startsWith: (Input) -> Bool
|
||||
|
||||
/// Initializes a parser that successfully returns `Void` when the initial elements of its input
|
||||
/// are equivalent to the elements in another sequence, using the given predicate as the
|
||||
/// equivalence test.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - possiblePrefix: A sequence to compare to the start of an input sequence.
|
||||
/// - areEquivalent: A predicate that returns `true` if its two arguments are equivalent;
|
||||
/// otherwise, `false`.
|
||||
@inlinable
|
||||
public init<PossiblePrefix>(
|
||||
_ possiblePrefix: PossiblePrefix,
|
||||
by areEquivalent: @escaping (Input.Element, Input.Element) -> Bool
|
||||
)
|
||||
where
|
||||
PossiblePrefix: Collection,
|
||||
PossiblePrefix.Element == Input.Element
|
||||
{
|
||||
self.count = possiblePrefix.count
|
||||
self.possiblePrefix = AnyCollection(possiblePrefix)
|
||||
self.startsWith = { input in input.starts(with: possiblePrefix, by: areEquivalent) }
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws {
|
||||
guard self.startsWith(input) else {
|
||||
throw ParsingError.expectedInput(formatValue(self.possiblePrefix), at: input)
|
||||
}
|
||||
input.removeFirst(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers.StartsWith where Input.Element: Equatable {
|
||||
/// Initializes a parser that successfully returns `Void` when the initial elements of its input
|
||||
/// are equivalent to the elements in another sequence.
|
||||
///
|
||||
/// - Parameter possiblePrefix: A sequence to compare to the start of an input sequence.
|
||||
@inlinable
|
||||
public init<PossiblePrefix>(_ possiblePrefix: PossiblePrefix)
|
||||
where
|
||||
PossiblePrefix: Collection,
|
||||
PossiblePrefix.Element == Input.Element
|
||||
{
|
||||
self.init(possiblePrefix, by: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias StartsWith = SwiftTL.StartsWith // NB: Convenience type alias for discovery
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/// A parser that can parse streams of input.
|
||||
///
|
||||
/// For example, the following parser can parse an integer followed by a newline from a collection
|
||||
/// of UTF8 bytes:
|
||||
///
|
||||
/// ```swift
|
||||
/// Parse {
|
||||
/// Int.parser(of: ArraySlice<UInt8>.self)
|
||||
/// StartsWith("\n".utf8)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This parser can be transformed into one that processes an incoming stream of UTF8 bytes:
|
||||
///
|
||||
/// ```swift
|
||||
/// Stream {
|
||||
/// Parse {
|
||||
/// Int.parser(of: ArraySlice<UInt8>.self)
|
||||
/// StartsWith("\n".utf8)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And then it can be used on a stream, such as values coming from standard in:
|
||||
///
|
||||
/// ```swift
|
||||
/// var stdin = AnyIterator {
|
||||
/// readLine().map { ArraySlice($0.utf8) }
|
||||
/// }
|
||||
///
|
||||
/// try newlineSeparatedIntegers.parse(&stdin)
|
||||
/// ```
|
||||
public struct Stream<Parsers: Parser>: Parser where Parsers.Input: RangeReplaceableCollection {
|
||||
public let parsers: Parsers
|
||||
|
||||
@inlinable
|
||||
public init(@ParserBuilder build: () -> Parsers) {
|
||||
self.parsers = build()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout AnyIterator<Parsers.Input>) rethrows -> [Parsers.Output] {
|
||||
var buffer = Parsers.Input()
|
||||
var outputs: Output = []
|
||||
while let chunk = input.next() {
|
||||
buffer.append(contentsOf: chunk)
|
||||
outputs.append(try self.parsers.parse(&buffer))
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
public typealias Stream = SwiftTL.Stream // NB: Convenience type alias for discovery
|
||||
}
|
||||
137
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/UUID.swift
Normal file
137
build-system/SwiftTL/Sources/SwiftTL/Parser/Parsers/UUID.swift
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import Foundation
|
||||
|
||||
extension UUID {
|
||||
/// A parser that consumes a hexadecimal UUID from the beginning of a collection of UTF-8 code
|
||||
/// units.
|
||||
///
|
||||
/// ```swift
|
||||
/// var input = "deadbeef-dead-beef-dead-beefdeadbeef,"[...]
|
||||
/// try UUID.parser().parse(&input) // DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF
|
||||
/// input // ","
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter inputType: The collection type of UTF-8 code units to parse.
|
||||
/// - Returns: A parser that consumes a hexadecimal UUID from the beginning of a collection of
|
||||
/// UTF-8 code units.
|
||||
@inlinable
|
||||
public static func parser<Input>(
|
||||
of inputType: Input.Type = Input.self
|
||||
) -> Parsers.UUIDParser<Input> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a hexadecimal UUID from the beginning of a substring's UTF-8 view.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is
|
||||
/// `Substring.UTF8View`.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a hexadecimal UUID from the beginning of a substring's UTF-8
|
||||
/// view.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
|
||||
) -> Parsers.UUIDParser<Substring.UTF8View> {
|
||||
.init()
|
||||
}
|
||||
|
||||
/// A parser that consumes a hexadecimal UUID from the beginning of a substring.
|
||||
///
|
||||
/// This overload is provided to allow the `Input` generic to be inferred when it is `Substring`.
|
||||
///
|
||||
/// - Parameter inputType: The `Substring` type. This parameter is included to mirror the
|
||||
/// interface that parses any collection of UTF-8 code units.
|
||||
/// - Returns: A parser that consumes a hexadecimal UUID from the beginning of a substring.
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public static func parser(
|
||||
of inputType: Substring.Type = Substring.self
|
||||
) -> FromUTF8View<Substring, Parsers.UUIDParser<Substring.UTF8View>> {
|
||||
.init { Parsers.UUIDParser<Substring.UTF8View>() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parsers {
|
||||
/// A parser that consumes a UUID from the beginning of a collection of UTF8 code units.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you will usually use
|
||||
/// `UUID.parser()`, which constructs this type.
|
||||
public struct UUIDParser<Input: Collection>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Input.Element == UTF8.CodeUnit
|
||||
{
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) throws -> UUID {
|
||||
func parseHelp<C>(_ bytes: C) throws -> UUID where C: Collection, C.Element == UTF8.CodeUnit {
|
||||
var prefix = bytes.prefix(36)
|
||||
guard prefix.count == 36
|
||||
else { throw ParsingError.expectedInput("UUID", at: input) }
|
||||
|
||||
@inline(__always)
|
||||
func digit(for n: UTF8.CodeUnit) throws -> UTF8.CodeUnit {
|
||||
switch n {
|
||||
case .init(ascii: "0") ... .init(ascii: "9"):
|
||||
return UTF8.CodeUnit(n - .init(ascii: "0"))
|
||||
case .init(ascii: "A") ... .init(ascii: "F"):
|
||||
return UTF8.CodeUnit(n - .init(ascii: "A") + 10)
|
||||
case .init(ascii: "a") ... .init(ascii: "f"):
|
||||
return UTF8.CodeUnit(n - .init(ascii: "a") + 10)
|
||||
default:
|
||||
throw ParsingError.expectedInput("UUID", at: input)
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func nextByte() throws -> UInt8 {
|
||||
try digit(for: prefix.removeFirst()) * 16 + digit(for: prefix.removeFirst())
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func chompHyphen() throws {
|
||||
guard prefix.removeFirst() == .init(ascii: "-")
|
||||
else { throw ParsingError.expectedInput("UUID", at: input) }
|
||||
}
|
||||
|
||||
let _00 = try nextByte()
|
||||
let _01 = try nextByte()
|
||||
let _02 = try nextByte()
|
||||
let _03 = try nextByte()
|
||||
try chompHyphen()
|
||||
let _04 = try nextByte()
|
||||
let _05 = try nextByte()
|
||||
try chompHyphen()
|
||||
let _06 = try nextByte()
|
||||
let _07 = try nextByte()
|
||||
try chompHyphen()
|
||||
let _08 = try nextByte()
|
||||
let _09 = try nextByte()
|
||||
try chompHyphen()
|
||||
let _10 = try nextByte()
|
||||
let _11 = try nextByte()
|
||||
let _12 = try nextByte()
|
||||
let _13 = try nextByte()
|
||||
let _14 = try nextByte()
|
||||
let _15 = try nextByte()
|
||||
|
||||
input.removeFirst(36)
|
||||
return UUID(
|
||||
uuid: (
|
||||
_00, _01, _02, _03,
|
||||
_04, _05,
|
||||
_06, _07,
|
||||
_08, _09,
|
||||
_10, _11, _12, _13, _14, _15
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return try input.withContiguousStorageIfAvailable(parseHelp) ?? parseHelp(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/// A parser that consumes all ASCII whitespace from the beginning of the input.
|
||||
///
|
||||
/// - Note: This parser only consumes ASCII spaces (`" "`), newlines (`"\n"` and `"\r"`), and tabs
|
||||
/// (`"\t"`). If you need richer support that covers all unicode whitespace, use a ``Prefix``
|
||||
/// parser that operates on the `Substring` level with a predicate that consumes whitespace:
|
||||
///
|
||||
/// ```swift
|
||||
/// Prefix { $0.isWhitespace }
|
||||
/// ```
|
||||
public struct Whitespace<Input: Collection, Bytes: Collection>: Parser
|
||||
where
|
||||
Input.SubSequence == Input,
|
||||
Bytes.SubSequence == Bytes,
|
||||
Bytes.Element == UTF8.CodeUnit
|
||||
{
|
||||
@usableFromInline
|
||||
let toBytes: (Input) -> Bytes
|
||||
|
||||
@usableFromInline
|
||||
let fromBytes: (Bytes) -> Input
|
||||
|
||||
@inlinable
|
||||
public func parse(_ input: inout Input) -> Input {
|
||||
let output = self.toBytes(input).prefix(while: { (byte: UTF8.CodeUnit) in
|
||||
byte == .init(ascii: " ")
|
||||
|| byte == .init(ascii: "\n")
|
||||
|| byte == .init(ascii: "\r")
|
||||
|| byte == .init(ascii: "\t")
|
||||
})
|
||||
input.removeFirst(output.count)
|
||||
return self.fromBytes(output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Whitespace {
|
||||
@inlinable
|
||||
public init() where Bytes == Input {
|
||||
self.toBytes = { $0 }
|
||||
self.fromBytes = { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
extension Whitespace where Input == Substring, Bytes == Substring.UTF8View {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() {
|
||||
self.toBytes = { $0.utf8 }
|
||||
self.fromBytes = Substring.init
|
||||
}
|
||||
}
|
||||
|
||||
/*extension Whitespace where Input == Substring.UTF8View, Bytes == Input {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() { self.init() }
|
||||
}
|
||||
|
||||
extension Whitespace where Input == ArraySlice<UTF8.CodeUnit>, Bytes == Input {
|
||||
@_disfavoredOverload
|
||||
@inlinable
|
||||
public init() { self.init() }
|
||||
}*/
|
||||
|
||||
extension Parsers {
|
||||
public typealias Whitespace = SwiftTL.Whitespace // NB: Convenience type alias for discovery
|
||||
}
|
||||
486
build-system/SwiftTL/Sources/SwiftTL/Parser/ParsingError.swift
Normal file
486
build-system/SwiftTL/Sources/SwiftTL/Parser/ParsingError.swift
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import Foundation
|
||||
|
||||
@usableFromInline
|
||||
enum ParsingError: Error {
|
||||
case failed(String, Context)
|
||||
case manyFailed([Error], Context)
|
||||
|
||||
@usableFromInline
|
||||
static func expectedInput(_ description: String, at remainingInput: Any) -> Self {
|
||||
.failed(
|
||||
summary: "unexpected input",
|
||||
label: "expected \(description)",
|
||||
at: remainingInput
|
||||
)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func expectedInput(
|
||||
_ description: String,
|
||||
from originalInput: Any,
|
||||
to remainingInput: Any
|
||||
) -> Self {
|
||||
.failed(
|
||||
summary: "unexpected input",
|
||||
label: "expected \(description)",
|
||||
from: originalInput,
|
||||
to: remainingInput
|
||||
)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func failed(summary: String, label: String = "", at remainingInput: Any) -> Self {
|
||||
.failed(label, .init(remainingInput: remainingInput, debugDescription: summary))
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func failed(
|
||||
summary: String, label: String = "", from originalInput: Any, to remainingInput: Any
|
||||
) -> Self {
|
||||
.failed(
|
||||
label,
|
||||
.init(
|
||||
originalInput: originalInput,
|
||||
remainingInput: remainingInput,
|
||||
debugDescription: summary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func manyFailed(_ errors: [Error], at remainingInput: Any) -> Self {
|
||||
.manyFailed(errors, .init(remainingInput: remainingInput, debugDescription: ""))
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func wrap(_ error: Error, at remainingInput: Any) -> Self {
|
||||
error as? ParsingError
|
||||
?? .failed(
|
||||
"",
|
||||
.init(
|
||||
remainingInput: remainingInput,
|
||||
debugDescription: formatError(error),
|
||||
underlyingError: error
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func flattened() -> Self {
|
||||
func flatten(_ depth: Int = 0) -> (Error) -> [(depth: Int, error: Error)] {
|
||||
{ error in
|
||||
switch error {
|
||||
case let ParsingError.manyFailed(errors, _):
|
||||
return errors.flatMap(flatten(depth + 1))
|
||||
default:
|
||||
return [(depth, error)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .failed:
|
||||
return self
|
||||
case let .manyFailed(errors, context):
|
||||
return .manyFailed(
|
||||
errors.flatMap(flatten())
|
||||
.sorted {
|
||||
switch ($0.error, $1.error) {
|
||||
case let (lhs as ParsingError, rhs as ParsingError):
|
||||
return lhs.context > rhs.context
|
||||
default:
|
||||
return $0.depth > $1.depth
|
||||
}
|
||||
}
|
||||
.map { $0.error },
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
var context: Context {
|
||||
switch self {
|
||||
case let .failed(_, context), let .manyFailed(_, context):
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct Context {
|
||||
@usableFromInline
|
||||
var debugDescription: String
|
||||
|
||||
@usableFromInline
|
||||
var originalInput: Any
|
||||
|
||||
@usableFromInline
|
||||
var remainingInput: Any
|
||||
|
||||
@usableFromInline
|
||||
var underlyingError: Error?
|
||||
|
||||
@usableFromInline
|
||||
init(
|
||||
originalInput: Any,
|
||||
remainingInput: Any,
|
||||
debugDescription: String,
|
||||
underlyingError: Error? = nil
|
||||
) {
|
||||
self.originalInput = originalInput
|
||||
self.remainingInput = remainingInput
|
||||
self.debugDescription = debugDescription
|
||||
self.underlyingError = underlyingError
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
init(
|
||||
remainingInput: Any,
|
||||
debugDescription: String,
|
||||
underlyingError: Error? = nil
|
||||
) {
|
||||
self.originalInput = remainingInput
|
||||
self.remainingInput = remainingInput
|
||||
self.debugDescription = debugDescription
|
||||
self.underlyingError = underlyingError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ParsingError: CustomDebugStringConvertible {
|
||||
@usableFromInline
|
||||
var debugDescription: String {
|
||||
switch self.flattened() {
|
||||
case let .failed(label, context):
|
||||
return format(labels: [label], context: context)
|
||||
|
||||
case let .manyFailed(errors, context) where errors.isEmpty:
|
||||
return format(labels: [""], context: context)
|
||||
|
||||
case let .manyFailed(errors, _):
|
||||
return debugDescription(for: errors)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func debugDescription(for errors: [Error]) -> String {
|
||||
func failed(_ error: Error) -> (String, Context)? {
|
||||
guard
|
||||
let error = error as? ParsingError,
|
||||
case .failed(let label, let context) = error
|
||||
else { return nil }
|
||||
return (label, context)
|
||||
}
|
||||
|
||||
var count = 0
|
||||
var description = ""
|
||||
|
||||
func append(_ s: String) {
|
||||
count += 1
|
||||
if !description.isEmpty {
|
||||
description.append("\n\n")
|
||||
}
|
||||
description.append(s)
|
||||
}
|
||||
|
||||
var errors = errors[...]
|
||||
while let error = errors.popFirst() {
|
||||
guard case .some((let firstLabel, let firstContext)) = failed(error) else {
|
||||
append(formatError(error))
|
||||
continue
|
||||
}
|
||||
|
||||
var labels = [firstLabel]
|
||||
while case .some((let label, let context)) = errors.first.flatMap({ failed($0) }),
|
||||
firstContext.canGroup(with: context)
|
||||
{
|
||||
errors.removeFirst()
|
||||
labels.append(label)
|
||||
}
|
||||
|
||||
append(format(labels: labels, context: firstContext))
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
description = """
|
||||
error: multiple failures occurred
|
||||
|
||||
\(description)
|
||||
"""
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
extension ParsingError.Context {
|
||||
fileprivate func canGroup(with other: Self) -> Bool {
|
||||
func areSame(_ lhs: Any, _ rhs: Any) -> Bool {
|
||||
switch (normalize(lhs), normalize(rhs)) {
|
||||
case let (lhs as Substring, rhs as Substring):
|
||||
return lhs.startIndex == rhs.startIndex && lhs.endIndex == rhs.endIndex
|
||||
case let (lhs as Slice<[Substring]>, rhs as Slice<[Substring]>):
|
||||
return zip(lhs, rhs).allSatisfy { l, r in
|
||||
l.startIndex == r.startIndex && l.endIndex == r.endIndex
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
(areSame(self.remainingInput, other.remainingInput)
|
||||
&& areSame(self.originalInput, other.originalInput)
|
||||
&& self.underlyingError == nil && other.underlyingError == nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == (label: String, context: ParsingError.Context) {
|
||||
fileprivate func allSame(_ keyPath: KeyPath<ParsingError.Context, Any>) -> Bool {
|
||||
guard let first = self.first else { return true }
|
||||
guard let firstValue = first.context[keyPath: keyPath] as? AnyHashable else { return false }
|
||||
return self.dropFirst().allSatisfy {
|
||||
guard let value = $0.context[keyPath: keyPath] as? AnyHashable else { return false }
|
||||
return value == firstValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func format(labels: [String], context: ParsingError.Context) -> String {
|
||||
func formatHelp<Input>(from originalInput: Input, to remainingInput: Input) -> String {
|
||||
switch (normalize(originalInput), normalize(remainingInput)) {
|
||||
case let (originalInput as Substring, remainingInput as Substring):
|
||||
let substring =
|
||||
originalInput.startIndex == remainingInput.startIndex
|
||||
? originalInput
|
||||
: originalInput.base[originalInput.startIndex..<remainingInput.startIndex]
|
||||
|
||||
let position = substring.base[..<substring.startIndex].reduce(
|
||||
into: (0, 0)
|
||||
) { (position: inout (line: Int, column: Int), character: Character) in
|
||||
if character.isNewline {
|
||||
position.line += 1
|
||||
position.column = 0
|
||||
} else {
|
||||
position.column += 1
|
||||
}
|
||||
}
|
||||
|
||||
let through = substring.reduce(
|
||||
into: position
|
||||
) { (position: inout (line: Int, column: Int), character: Character) in
|
||||
if character.isNewline {
|
||||
position.line += 1
|
||||
position.column = 0
|
||||
} else {
|
||||
position.column += 1
|
||||
}
|
||||
}
|
||||
|
||||
let offset = min(position.column, 20)
|
||||
let selectedLine = substring.base[
|
||||
substring.base.index(substring.startIndex, offsetBy: -offset)...
|
||||
]
|
||||
.prefix { !$0.isNewline }
|
||||
let isStartTruncated = offset != position.column
|
||||
var truncatedLine = selectedLine.prefix(79 - 4 - (isStartTruncated ? 1 : 0))
|
||||
let isEndTruncated = truncatedLine.endIndex != selectedLine.endIndex
|
||||
for index in truncatedLine.indices.reversed() {
|
||||
guard truncatedLine[index].isWhitespace
|
||||
else { break }
|
||||
truncatedLine.replaceSubrange(index...index, with: "␣")
|
||||
}
|
||||
|
||||
let diagnostic =
|
||||
("\(isStartTruncated ? "…" : "")\(truncatedLine)\(isEndTruncated ? "…" : "")\n"
|
||||
+ labels.map { label in
|
||||
"""
|
||||
\(String(repeating: " ", count: offset + (isStartTruncated ? 1 : 0)))\
|
||||
\(String(repeating: "^", count: max(1, substring.count)))\
|
||||
\(label.isEmpty ? "" : " \(label)")
|
||||
"""
|
||||
}.joined(separator: "\n"))
|
||||
|
||||
return formatError(
|
||||
summary: context.debugDescription,
|
||||
location: """
|
||||
input:\(position.line + 1):\(position.column + 1)\
|
||||
\(
|
||||
through.line == position.line
|
||||
? (through.column <= position.column + 1 ? "" : "-\(through.column)")
|
||||
: "-\(through.line + 1):\(through.column + 1)")
|
||||
""",
|
||||
prefix: "\(position.line + 1)",
|
||||
diagnostic: diagnostic
|
||||
)
|
||||
|
||||
case let (originalInput as Slice<[Substring]>, remainingInput as Slice<[Substring]>):
|
||||
let slice =
|
||||
originalInput.startIndex == remainingInput.startIndex
|
||||
? originalInput
|
||||
: Slice(
|
||||
base: originalInput.base,
|
||||
bounds: originalInput.startIndex..<remainingInput.startIndex
|
||||
)
|
||||
|
||||
let expectation: String
|
||||
if let error = context.underlyingError as? ParsingError,
|
||||
case let .failed(elementLabel, elementContext) = error,
|
||||
let originalInput = normalize(elementContext.originalInput) as? Substring,
|
||||
let remainingInput = normalize(elementContext.remainingInput) as? Substring
|
||||
{
|
||||
let substring =
|
||||
originalInput.startIndex == remainingInput.startIndex
|
||||
? originalInput
|
||||
: originalInput.base[originalInput.startIndex..<remainingInput.startIndex]
|
||||
let indent = String(
|
||||
repeating: " ",
|
||||
count: substring.distance(
|
||||
from: substring.base.startIndex,
|
||||
to: substring.startIndex
|
||||
)
|
||||
)
|
||||
expectation = """
|
||||
\(indent)\(String(repeating: "^", count: max(1, substring.count)))\
|
||||
\(elementLabel.isEmpty ? "" : " \(elementLabel)")
|
||||
"""
|
||||
} else {
|
||||
let count = slice.map(formatValue).joined(separator: ", ").count
|
||||
expectation = labels.map { label in
|
||||
"""
|
||||
\(String(repeating: "^", count: max(1, count)))\(label.isEmpty ? "" : " \(label)")
|
||||
"""
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
|
||||
let indent = slice.base[..<slice.startIndex].map { "\(formatValue($0.base)), " }.joined()
|
||||
return formatError(
|
||||
summary: context.debugDescription,
|
||||
location: "input[\(slice.startIndex)]",
|
||||
prefix: "\(slice.startIndex)",
|
||||
diagnostic: """
|
||||
\(slice.base)
|
||||
\(String(repeating: " ", count: indent.count + 1))\(expectation)
|
||||
"""
|
||||
)
|
||||
|
||||
default:
|
||||
return "error: \(context.debugDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
return formatHelp(from: context.originalInput, to: context.remainingInput)
|
||||
}
|
||||
|
||||
private func formatError(_ error: Error) -> String {
|
||||
switch error {
|
||||
case let error as ParsingError:
|
||||
return error.debugDescription
|
||||
|
||||
case let error as LocalizedError:
|
||||
return error.localizedDescription
|
||||
|
||||
default:
|
||||
return "\(error)"
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func formatValue<Input>(
|
||||
_ input: Input
|
||||
) -> String {
|
||||
switch input {
|
||||
case let input as String:
|
||||
return input.debugDescription
|
||||
|
||||
case let input as String.UnicodeScalarView:
|
||||
return String(input).debugDescription
|
||||
|
||||
case let input as String.UTF8View:
|
||||
return String(input).debugDescription
|
||||
|
||||
case let input as Substring:
|
||||
return input.debugDescription
|
||||
|
||||
case let input as Substring.UnicodeScalarView:
|
||||
return Substring(input).debugDescription
|
||||
|
||||
case let input as Substring.UTF8View:
|
||||
return Substring(input).debugDescription
|
||||
|
||||
default:
|
||||
return "\(input)"
|
||||
}
|
||||
}
|
||||
|
||||
private func formatError(
|
||||
summary: String,
|
||||
location: String,
|
||||
prefix: String,
|
||||
diagnostic: String
|
||||
) -> String {
|
||||
let indent = String(repeating: " ", count: prefix.count)
|
||||
var diagnostic =
|
||||
diagnostic
|
||||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
.map { "\(indent) |\($0.isEmpty ? "" : " \($0)")" }
|
||||
.joined(separator: "\n")
|
||||
diagnostic.replaceSubrange(..<indent.endIndex, with: prefix)
|
||||
return """
|
||||
error: \(summary)
|
||||
\(indent)--> \(location)
|
||||
\(diagnostic)
|
||||
"""
|
||||
}
|
||||
|
||||
extension ParsingError.Context {
|
||||
fileprivate static func > (lhs: Self, rhs: Self) -> Bool {
|
||||
switch (normalize(lhs.remainingInput), normalize(rhs.remainingInput)) {
|
||||
case let (lhsInput as Substring, rhsInput as Substring):
|
||||
return lhsInput.endIndex > rhsInput.endIndex
|
||||
|
||||
case let (lhsInput as Slice<[Substring]>, rhsInput as Slice<[Substring]>):
|
||||
guard lhsInput.endIndex != rhsInput.endIndex else {
|
||||
switch (lhs.underlyingError, rhs.underlyingError) {
|
||||
case let (lhs as ParsingError, rhs as ParsingError):
|
||||
return lhs.context > rhs.context
|
||||
case (is ParsingError, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return lhsInput.endIndex > rhsInput.endIndex
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func normalize(_ input: Any) -> Any {
|
||||
// TODO: Use `_openExistential` for `C: Collection where C == C.SubSequence` for index juggling?
|
||||
switch input {
|
||||
case let input as Substring:
|
||||
// NB: We want to ensure we are sliced at a character boundary and not a scalar boundary.
|
||||
let startIndex =
|
||||
input.startIndex == input.base.endIndex
|
||||
? input.startIndex
|
||||
: input.base.indices.last { $0 <= input.startIndex } ?? input.startIndex
|
||||
let endIndex = input.endIndex == input.base.endIndex ? startIndex : input.endIndex
|
||||
|
||||
return input.base[startIndex..<endIndex]
|
||||
|
||||
case let input as Substring.UnicodeScalarView:
|
||||
return normalize(Substring(input))
|
||||
|
||||
case let input as Substring.UTF8View:
|
||||
return normalize(Substring(input))
|
||||
|
||||
case let input as Slice<[Substring]>:
|
||||
return input.endIndex == input.base.endIndex ? input[..<input.startIndex] : input
|
||||
|
||||
default:
|
||||
return input
|
||||
}
|
||||
}
|
||||
284
build-system/SwiftTL/Sources/SwiftTL/Resolution.swift
Normal file
284
build-system/SwiftTL/Sources/SwiftTL/Resolution.swift
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import Foundation
|
||||
|
||||
struct QualifiedName: Hashable, Comparable, CustomStringConvertible {
|
||||
var namespace: String?
|
||||
var value: String
|
||||
|
||||
var description: String {
|
||||
if let namespace = self.namespace {
|
||||
return "\(namespace).\(self.value)"
|
||||
} else {
|
||||
return self.value
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: QualifiedName, rhs: QualifiedName) -> Bool {
|
||||
return lhs.description < rhs.description
|
||||
}
|
||||
}
|
||||
|
||||
extension QualifiedName {
|
||||
init(string: String) {
|
||||
if let dotRange = string.range(of: ".") {
|
||||
self.init(namespace: String(string[string.startIndex ..< dotRange.lowerBound]), value: String(string[dotRange.upperBound...]))
|
||||
} else {
|
||||
self.init(namespace: nil, value: string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Resolver {
|
||||
struct ResolutionError: Error, CustomStringConvertible {
|
||||
var text: String
|
||||
|
||||
var description: String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
indirect enum TypeReference {
|
||||
case int32
|
||||
case int64
|
||||
case int256
|
||||
case double
|
||||
case bytes
|
||||
case string
|
||||
case bool
|
||||
case boolTrue
|
||||
case bareVector(TypeReference)
|
||||
case boxedVector(TypeReference)
|
||||
case bareConstructor(typeName: QualifiedName, name: QualifiedName)
|
||||
case boxedType(QualifiedName)
|
||||
}
|
||||
|
||||
struct Argument {
|
||||
struct Condition {
|
||||
var fieldName: String
|
||||
var bitIndex: Int
|
||||
}
|
||||
|
||||
var name: String
|
||||
var type: TypeReference
|
||||
var condition: Condition?
|
||||
}
|
||||
|
||||
final class SumType {
|
||||
struct Constructor {
|
||||
var name: QualifiedName
|
||||
var id: UInt32
|
||||
var arguments: [Argument]
|
||||
}
|
||||
|
||||
let name: QualifiedName
|
||||
var constructors: [QualifiedName: Constructor] = [:]
|
||||
|
||||
init(name: QualifiedName) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
struct Function {
|
||||
var name: QualifiedName
|
||||
var id: UInt32
|
||||
var arguments: [Argument]
|
||||
var result: TypeReference
|
||||
}
|
||||
|
||||
static func resolveBuiltinType(name: QualifiedName) -> TypeReference? {
|
||||
if name.namespace == nil {
|
||||
if name.value == "int" {
|
||||
return .int32
|
||||
} else if name.value == "long" {
|
||||
return .int64
|
||||
} else if name.value == "int256" {
|
||||
return .int256
|
||||
} else if name.value == "double" {
|
||||
return .double
|
||||
} else if name.value == "string" {
|
||||
return .string
|
||||
} else if name.value == "bytes" {
|
||||
return .bytes
|
||||
} else if name.value == "true" {
|
||||
return .boolTrue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func resolveTypes(constructors: [DescriptionParser.ConstructorDescription]) throws -> [SumType] {
|
||||
var constructedTypes: [QualifiedName: [DescriptionParser.ConstructorDescription]] = [:]
|
||||
var constructorNameToType: [QualifiedName: QualifiedName] = [:]
|
||||
|
||||
for constructorDescription in constructors {
|
||||
switch constructorDescription.type {
|
||||
case let .type(name):
|
||||
if !name.value[name.value.startIndex].isUppercase {
|
||||
throw ResolutionError(text: "Type constructor \(constructorDescription.name) -> \(name): the resulting type name should begin with a capital letter")
|
||||
}
|
||||
|
||||
constructedTypes[name, default: []].append(constructorDescription)
|
||||
|
||||
if let _ = constructorNameToType[constructorDescription.name] {
|
||||
throw ResolutionError(text: "Duplicate type constructor \(constructorDescription.name) found")
|
||||
}
|
||||
constructorNameToType[constructorDescription.name] = name
|
||||
case let .generic(name, argumentType):
|
||||
throw ResolutionError(text: "Type constructor \(constructorDescription.name) can not be used to construct a generic type \(name)<\(argumentType)>")
|
||||
}
|
||||
}
|
||||
|
||||
func resolveTypeReference(description: DescriptionParser.TypeReferenceDescription) throws -> TypeReference {
|
||||
switch description {
|
||||
case let .type(name):
|
||||
if let resolvedBuiltinType = resolveBuiltinType(name: name) {
|
||||
return resolvedBuiltinType
|
||||
}
|
||||
|
||||
if name.value[name.value.startIndex].isUppercase {
|
||||
if let _ = constructedTypes[name] {
|
||||
return .boxedType(name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type \(name)")
|
||||
}
|
||||
} else {
|
||||
if let typeName = constructorNameToType[name] {
|
||||
return .bareConstructor(typeName: typeName, name: name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type constructor \(name)")
|
||||
}
|
||||
}
|
||||
case let .generic(name, argumentType):
|
||||
if name == "vector" {
|
||||
return .bareVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else if name == "Vector" {
|
||||
return .boxedVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved generic type \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveArgument(existingArguments: [Argument], description: DescriptionParser.ArgumentDescription) throws -> Argument {
|
||||
return Argument(
|
||||
name: description.name,
|
||||
type: try resolveTypeReference(description: description.type),
|
||||
condition: try description.condition.flatMap { condition -> Argument.Condition in
|
||||
if !existingArguments.contains(where: { $0.name == condition.fieldName }) {
|
||||
throw ResolutionError(text: "Unresolved conditional field reference to \(condition.fieldName)")
|
||||
}
|
||||
return Argument.Condition(fieldName: condition.fieldName, bitIndex: condition.bitIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var types: [QualifiedName: SumType] = [:]
|
||||
|
||||
for (typeName, constructorDescriptions) in constructedTypes {
|
||||
let type = SumType(name: typeName)
|
||||
|
||||
for constructorDescription in constructorDescriptions {
|
||||
var arguments: [Argument] = []
|
||||
|
||||
for argumentDescription in constructorDescription.arguments {
|
||||
arguments.append(try resolveArgument(existingArguments: arguments, description: argumentDescription))
|
||||
}
|
||||
|
||||
guard let id = constructorDescription.explicitId else {
|
||||
throw ResolutionError(text: "Constructor \(constructorDescription.name) does not have an id")
|
||||
}
|
||||
|
||||
type.constructors[constructorDescription.name] = SumType.Constructor(
|
||||
name: constructorDescription.name,
|
||||
id: id,
|
||||
arguments: arguments
|
||||
)
|
||||
}
|
||||
|
||||
types[type.name] = type
|
||||
}
|
||||
|
||||
return types.values.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
static func resolveFunctions(types: [SumType], functionDescriptions: [DescriptionParser.ConstructorDescription]) throws -> [Function] {
|
||||
var functions: [QualifiedName: Function] = [:]
|
||||
|
||||
var typeMap: [QualifiedName: SumType] = [:]
|
||||
var constructorMap: [QualifiedName: SumType] = [:]
|
||||
|
||||
for type in types {
|
||||
typeMap[type.name] = type
|
||||
|
||||
for (_, constructor) in type.constructors {
|
||||
constructorMap[constructor.name] = type
|
||||
}
|
||||
}
|
||||
|
||||
func resolveTypeReference(description: DescriptionParser.TypeReferenceDescription) throws -> TypeReference {
|
||||
switch description {
|
||||
case let .type(name):
|
||||
if let resolvedBuiltinType = resolveBuiltinType(name: name) {
|
||||
return resolvedBuiltinType
|
||||
}
|
||||
|
||||
if name.value[name.value.startIndex].isUppercase {
|
||||
if let _ = typeMap[name] {
|
||||
return .boxedType(name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type \(name)")
|
||||
}
|
||||
} else {
|
||||
if let type = constructorMap[name] {
|
||||
return .bareConstructor(typeName: type.name, name: name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type constructor \(name)")
|
||||
}
|
||||
}
|
||||
case let .generic(name, argumentType):
|
||||
if name == "vector" {
|
||||
return .bareVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else if name == "Vector" {
|
||||
return .boxedVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved generic type \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveArgument(existingArguments: [Argument], description: DescriptionParser.ArgumentDescription) throws -> Argument {
|
||||
return Argument(
|
||||
name: description.name,
|
||||
type: try resolveTypeReference(description: description.type),
|
||||
condition: try description.condition.flatMap { condition -> Argument.Condition in
|
||||
if !existingArguments.contains(where: { $0.name == condition.fieldName }) {
|
||||
throw ResolutionError(text: "Unresolved conditional field reference to \(condition.fieldName)")
|
||||
}
|
||||
return Argument.Condition(fieldName: condition.fieldName, bitIndex: condition.bitIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
for functionDescription in functionDescriptions {
|
||||
var arguments: [Argument] = []
|
||||
|
||||
for argumentDescription in functionDescription.arguments {
|
||||
arguments.append(try resolveArgument(existingArguments: arguments, description: argumentDescription))
|
||||
}
|
||||
|
||||
let result = try resolveTypeReference(description: functionDescription.type)
|
||||
|
||||
guard let id = functionDescription.explicitId else {
|
||||
throw ResolutionError(text: "Function \(functionDescription.name) does not have an id")
|
||||
}
|
||||
|
||||
functions[functionDescription.name] = Function(
|
||||
name: functionDescription.name,
|
||||
id: id,
|
||||
arguments: arguments,
|
||||
result: result
|
||||
)
|
||||
}
|
||||
|
||||
return functions.values.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
}
|
||||
130
build-system/SwiftTL/Sources/SwiftTL/main.swift
Normal file
130
build-system/SwiftTL/Sources/SwiftTL/main.swift
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import Foundation
|
||||
|
||||
private extension String {
|
||||
var camelCased: String {
|
||||
var result = ""
|
||||
var capitalizeNext = false
|
||||
for c in self {
|
||||
if c == "_" {
|
||||
capitalizeNext = true
|
||||
} else {
|
||||
if capitalizeNext {
|
||||
capitalizeNext = false
|
||||
result.append(c.uppercased())
|
||||
} else {
|
||||
result.append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if CommandLine.arguments.count < 3 {
|
||||
print("Usage: SwiftTL path-to-scheme.tl path-to-output-folder [--stub-functions] [--print-constructors=N-M]")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let schemeFilePath = CommandLine.arguments[1]
|
||||
let outputDirectoryPath = CommandLine.arguments[2]
|
||||
|
||||
var stubFunctions = false
|
||||
var printConstructorsRange: (start: Int, end: Int)? = nil
|
||||
for arg in CommandLine.arguments {
|
||||
if arg == "--stub-functions" {
|
||||
stubFunctions = true
|
||||
}
|
||||
if arg.hasPrefix("--print-constructors=") {
|
||||
let value = String(arg.dropFirst("--print-constructors=".count))
|
||||
let parts = value.split(separator: "-")
|
||||
if parts.count == 2, let start = Int(parts[0]), let end = Int(parts[1]) {
|
||||
if start > end {
|
||||
print("Error: Invalid range for --print-constructors: start (\(start)) must be <= end (\(end))")
|
||||
exit(1)
|
||||
}
|
||||
printConstructorsRange = (start, end)
|
||||
} else {
|
||||
print("Error: Invalid format for --print-constructors. Expected: --print-constructors=N-M (e.g., --print-constructors=0-10)")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let data = try? String(contentsOfFile: schemeFilePath) else {
|
||||
print("Could not open scheme file \(schemeFilePath)")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
do {
|
||||
let parsedData = try DescriptionParser.parse(data: data)
|
||||
let resolvedTypes = try Resolver.resolveTypes(constructors: parsedData.constructors)
|
||||
var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: parsedData.functions)
|
||||
|
||||
resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool"))))
|
||||
|
||||
var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = []
|
||||
var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = []
|
||||
|
||||
let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name })
|
||||
|
||||
if let range = printConstructorsRange {
|
||||
print("--- CONSTRUCTORS ---")
|
||||
for (index, type) in sortedTypes.enumerated() {
|
||||
if index >= range.start && index < range.end {
|
||||
for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) {
|
||||
let storedArguments = constructor.arguments.filter {
|
||||
if case .boolTrue = $0.type { return false }
|
||||
return true
|
||||
}
|
||||
if !storedArguments.isEmpty {
|
||||
let fieldNames = storedArguments.map { $0.name.camelCased }
|
||||
print("\(constructor.name.value):\(fieldNames.joined(separator: ","))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print("--- END CONSTRUCTORS ---")
|
||||
print("Total types: \(sortedTypes.count)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
for type in sortedTypes {
|
||||
for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) {
|
||||
constructorOrder.append((type.name, constructor.name.value))
|
||||
}
|
||||
}
|
||||
|
||||
var totalConstructorCount = 0
|
||||
var currentConstructorCount = 0
|
||||
for type in sortedTypes {
|
||||
if typeOrder.isEmpty || currentConstructorCount >= 32 {
|
||||
typeOrder.append(([], []))
|
||||
currentConstructorCount = 0
|
||||
}
|
||||
|
||||
typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value)))
|
||||
|
||||
currentConstructorCount += type.constructors.count
|
||||
totalConstructorCount += type.constructors.count
|
||||
|
||||
if totalConstructorCount > 40 {
|
||||
}
|
||||
}
|
||||
|
||||
typeOrder.append(([], []))
|
||||
for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) {
|
||||
typeOrder[typeOrder.count - 1].functions.append(function.name)
|
||||
}
|
||||
|
||||
try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let generatedFiles = try CodeGenerator.generate(types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder, stubFunctions: stubFunctions)
|
||||
|
||||
for (name, fileData) in generatedFiles {
|
||||
let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path
|
||||
let _ = try? FileManager.default.removeItem(atPath: filePath)
|
||||
try fileData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
} catch let e {
|
||||
print("\(e)")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue