OysterKit

OysterKit

一个用于分词、解析和解释语言的 Swift 框架

Swift Platforms BSD Build Status - Master codecov Documentation Coverage
Twitter

OysterKit 提供原生 Swift 扫描、词法分析和解析功能,作为一个纯 Swift 框架。此软件包还提供了另外两个元素。第一个是第二个框架 STLR,它使用 OysterKit 提供一种名为 STLR(Swift Tool for Language Recognition,Swift 语言识别工具)的纯文本语法规范语言。最后,一个命令行工具 stlr 可以用于自动生成 OysterKit 的 Swift 源代码,用于 STLR 语法,以及将 STLR 语法动态应用于多种使用场景。以下文档可用:

请注意 现在所有开发仅针对 Swift 5.0 及更高版本。如果您希望使用最后一个 Swift 4.1 兼容版本,请使用 swift/4.1 分支

主要特性

示例

创建规则并对字符串进行分词

OysterKit 可用于直接在 Swift 中快速简单地创建和使用语法。以下是一些简单的示例

/// Scanning
let letter = CharacterSet.letters.parse(as:StringToken("letter"))

for token in [letter].tokenize("Hello"){
	print(token)
}

实例 CharacterSetStringNSRegularExpression 都可以直接用作规则。要使规则生成标记,只需使用规则的 parse(as:TokenType) 函数即可。语法只是一个规则数组,您可以使用该语法来标记化字符串。

做出选择

选择规则只是其中包含的任何规则都可以匹配以满足 choice 的规则。 在这种情况下,punctuation 规则可以是多个字符串之一。 然后我们可以标记化

/// Choices
let punctuation = [".",",","!","?"].choice.parse(as: StringToken("punctuation"))

for token in [letter, punctuation].tokenize("Hello!"){
    print(token)
}

跳过内容

您并不总是想为所有内容创建标记。 您可以将对任何规则的修改链接在一起,因为规则具有基于值的语义...您不会更改原始规则。

/// Skipping
let space = CharacterSet.whitespaces.skip()

for token in [letter, punctuation, space].tokenize("Hello, World!"){
    print(token)
}

我们使用所有三个不同的规则来标记化“Hello, World!”,但请注意,我们在 space 规则上调用了 skip()。 这意味着不会创建任何标记(当我们稍后进行更复杂的解析时...这也意味着如果此规则构成另一个更复杂规则的一部分,则开头和结尾跳过的规则将不包含在匹配范围内。 但稍后会详细介绍)。 在此示例中,您只会获得 letterpunctuation 标记,但您仍然会匹配空格。

重复

您还可以告诉规则在生成标记之前必须匹配多少次。 在这里,我们创建一个 word 标记,该标记将我们的 letter 规则重复一次或多次。

let word = letter.require(.oneOrMore).parse(as: StringToken("word"))

for token in [word, punctuation, space].tokenize("Hello, World!"){
    print(token)
}

有标准的 onenoneOrOnenoneOrMoreoneOrMore,您还可以指定闭合或开放范围(例如 .require(2...) 将匹配两次或更多次。

序列

规则可以由其他规则的序列组成。 在此示例中,我们创建一个 properNoun 规则,该规则需要一个大写字母后跟零个或多个小写字母。 请注意,我们从之前的 word 规则创建一个新规则,该规则生成不同的标记 (other)。 然后,我们使我们的新 choice 生成 word 标记。 我们刚刚在语法中创建了一个小层次结构。 word 将匹配 properNounother(我们旧的 word 规则)。 您稍后会明白为什么这很有用。 当您流式传输时,您只会得到 word(而不是 properNounother)。

// Sequences
let properNoun = [CharacterSet.uppercaseLetters, CharacterSet.lowercaseLetters.require(.zeroOrMore)].sequence.parse(as: StringToken("properNoun"))
let classifiedWord = [properNoun,word.parse(as: StringToken("other"))].choice.parse(as: StringToken("word"))

print("Word classification")
for token in [classifiedWord, punctuation, space].tokenize("Jon was here!"){
    print(token)
}

解析 - 超越分词

分词很棒,并且在许多应用程序中它已经足够了(任何语法突出显示?),但是如果您打算尝试构建一种实际的语言,或者想要解析更复杂的数据结构,您将要构建一个抽象语法树。 OysterKit 可以从任何语法构建 HomogenousTree。 等等! 别走。 没那么糟! 这是它的作用。

do {
    print(try [[classifiedWord, punctuation, space].choice].parse("Jon was here!"))

} catch let error as ProcessingError {
    print(error.debugDescription)
}

在这里,我们使用 parse() 而不是 tokenize()。 我们需要将其包装在一个 do-catch 块中,因为在分词时,我们只是在出现问题时停止流式传输,但在解析时我们可以获得更多信息,包括错误。 此代码只是尝试解析(请注意,这次我们正在创建一个单规则语法,但该单规则是我们所有其他规则的 choice)与之前相同的字符串,但这次它生成一棵树。 这是将要打印出来的内容

root 
    word 
	    properNoun - 'Jon'
    word 
	    other - 'was'
    word 
	    other - 'here'
    punctuation - '!'

现在我们可以看到我们的单词分类。

构建复杂的异构抽象语法树

我知道...我又来了...术语。 这很简单。 同质意味着“同一种类”,因此同质树只是树中每个节点都是同一类型的树。 这就是 parse 函数创建的内容。 build 可以创建异构(填充有不同类型的数据的数据结构)的数据结构,例如 Swift 类型。 OysterKit 利用 Swift 的强大功能使这非常简单。

开箱即用,您可以使用 OysterKit 语法 DecodeDecodable 类型(如果您认为这很强大,请等到您查看过 STLR 并自动生成 Swift 源代码,而不是进行您即将看到的所有类型编写!)。

首先,让我们为单词和句子声明一些数据结构。

struct Word : Decodable, CustomStringConvertible {
    let properNoun : String?
    let other : String?
    
    var description: String {
        return properNoun != nil ? "properNoun: \(properNoun!)" : "other: \(other!)"
    }
}

struct Sentence : Decodable {
    let words : [Word]
    let punctuation : String
}

相当简单(如果不准确...通常您拥有的标点符号比结尾的要多)。 现在我们定义一个语法,该语法生成具有与我们的类型属性匹配的名称的标记,OysterKit(和 Swift)将完成其余的工作。

do {
    let words = [classifiedWord, space.require(.zeroOrMore)].sequence.require(.oneOrMore).parse(as:StringToken("words"))
    let sentence = try [ [words, punctuation ].sequence ].build("Jon was here!", as: Sentence.self)

    print(sentence)
} catch let error as ProcessingError {
    print(error.debugDescription)
} catch {
    print(error)
}

我们使用 build() 而不是 parse()。 我们需要一个额外的参数; 您需要告诉 build 您希望将其构建什么。

.build("Jon was here!", as: Sentence.self)

我们在前一个示例中看到的树可以与我们的数据结构完全匹配。

但是,如果我想要一棵异构树,但又不想付出那么多的努力怎么办?

幸运的是,OysterKit 与 STLR 携手并进,STLR 是一种用于编写语法的语言。 您可以动态地将其转换为内存中的 Swift(如果您只想 parse,则非常完美),或者使用三位一体的最后一个成员 stlrc 来生成 Swift,不仅用于规则,还用于 Swift 数据结构。 您可以阅读 此处有关 STLR 的完整文档,但我想给您留下一个示例,其中包含我们完成的语法的 STLR,以及它生成的 Swift,但在此之前,我只想向您展示您唯一需要编写的 Swift

您唯一需要编写的代码

您需要做的就是构建生成的类型的实例...

do {
    let sentence = Sentence.build("Hello Jon!")
} catch {
    print(error)
}

就这么简单。 好的,这是它来自的地方

STLR

grammar Sentence

punctuation = "." | "," | "!" | "?"

properNoun  = .uppercaseLetter .lowercaseLetter*
other       = .letter+

word        = properNoun | other
words       = (word -.whitespace+)+

sentence    = words punctuation

是的。 就是这样。 这是生成的 Swift。 似乎有很多...但请记住,如果您不想看,甚至不需要看! 但是,当您这样做时,您应该在那里看到您学到的所有东西

生成的 Swift

我们现在可以使用 stlrc 将 STLR 编译成 Swift。 这个简单的命令可以做到这一点

stlrc generate -g Sentence.stlr -l swiftIR -ot ./

上面的命令将在当前目录中生成一个 Sentence.swift 文件。 如果您将 -l swiftIR 更改为 -l SwiftPM,您应该会看到它所做的事情...但那是另一个故事。 这是 Sentence.swift 中的内容

import Foundation
import OysterKit

/// Intermediate Representation of the grammar
internal enum SentenceTokens : Int, TokenType, CaseIterable, Equatable {
    typealias T = SentenceTokens

    /// The tokens defined by the grammar
    case `punctuation`, `properNoun`, `other`, `word`, `words`, `sentence`

    /// The rule for the token
    var rule : Rule {
	switch self {
	    /// punctuation
	    case .punctuation:
		return [".", ",", "!", "?"].choice.reference(.structural(token: self))

	    /// properNoun
	    case .properNoun:
		return [CharacterSet.uppercaseLetters,    CharacterSet.lowercaseLetters.require(.zeroOrMore)].sequence.reference(.structural(token: self))

	    /// other
	    case .other:
		return CharacterSet.letters.require(.oneOrMore).reference(.structural(token: self))

	    /// word
	    case .word:
		return [T.properNoun.rule,T.other.rule].choice.reference(.structural(token: self))

	    /// words
	    case .words:
		return [T.word.rule, -CharacterSet.whitespaces.require(.oneOrMore)].sequence.require(.oneOrMore).reference(.structural(token: self))

	    /// sentence
	    case .sentence:
		return [T.words.rule,T.punctuation.rule].sequence.reference(.structural(token: self))
	}
    }

    /// Create a language that can be used for parsing etc
    public static var generatedRules: [Rule] {
	return [T.sentence.rule]
    }
}

public struct Sentence : Codable {

    // Punctuation
    public enum Punctuation : Swift.String, Codable, CaseIterable {
	case period = ".",comma = ",",ping = "!",questionMark = "?"
    }

    // Word
    public enum Word : Swift.String, Codable, CaseIterable {
	case properNoun,other
    }

    public typealias Words = [Word] 

    /// Sentence 
    public struct Sentence : Codable {
	public let words: Words
	public let punctuation: Punctuation
    }
    public let sentence : Sentence
    
    /**
     Parses the supplied string using the generated grammar into a new instance of
     the generated data structure

     - Parameter source: The string to parse
     - Returns: A new instance of the data-structure
     */
    public static func build(_ source : Swift.String) throws ->Sentence{
	let root = HomogenousTree(with: StringToken("root"), matching: source, children: [try AbstractSyntaxTreeConstructor().build(source, using: Sentence.generatedLanguage)])
	// print(root.description)
	return try ParsingDecoder().decode(Sentence.self, using: root)
    }

    public static var generatedLanguage : Grammar {return SentenceTokens.generatedRules}
}

其中有一些有趣(而且我认为相当聪明)的东西。 请注意,对于 Word 类型,STLR 非常聪明,并确定只有几个可能的值,并且这两个值都只是字符串,因此它创建了一个枚举。 对于 Punctuation 也是如此,但这更容易一些,因为它只是简单字符串的选择。 它还确定它实际上不需要为 Words 创建一个新类型,它可以只使用 typealias

我之所以提到这一点是因为您将与此数据结构进行交互,因此我花费了大量时间来确保它生成易于使用的 Swift,它是强类型的。 这将使您在构建完成后更容易使用它。

状态

您会注意到此构建中存在一些警告。 您不应担心这些警告,因为它们主要是指向进一步清理的前向引用,现在 STLR 正在为规则/标记以及自身的数据结构生成 Swift 代码,因此可以完成这些清理。 弃用正在全面展开,因为我开始接近 1.0,并且想要删除旧代码。