Files
swift-mirror/utils/swift-xcodegen/Sources/SwiftXcodeGen/Ninja/NinjaParser.swift
Hamish Knight 03d8ea5248 Introduce swift-xcodegen
This is a tool specifically designed to generate
Xcode projects for the Swift repo (as well as a
couple of adjacent repos such as LLVM and Clang).
It aims to provide a much more user-friendly experience
than the CMake Xcode generation (`build-script --xcode`).
2024-11-05 22:42:10 +00:00

327 lines
7.9 KiB
Swift

//===--- NinjaParser.swift ------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
struct NinjaParser {
private var lexer: Lexer
private init(_ input: UnsafeRawBufferPointer) throws {
self.lexer = Lexer(ByteScanner(input))
}
static func parse(_ input: Data) throws -> NinjaBuildFile {
try input.withUnsafeBytes { bytes in
var parser = try Self(bytes)
return try parser.parse()
}
}
}
fileprivate enum NinjaParseError: Error {
case badAttribute
case expected(NinjaParser.Lexeme)
}
fileprivate extension ByteScanner {
mutating func consumeUnescaped(
while pred: (Byte) -> Bool
) -> String? {
let bytes = consume(using: { consumer in
guard let c = consumer.peek, pred(c) else { return false }
// Ninja uses '$' as the escape character.
if c == "$" {
switch consumer.peek(ahead: 1) {
case let c? where c.isSpaceOrTab:
fallthrough
case "$", ":":
// Skip the '$' and take the unescaped character.
consumer.skip()
return consumer.eat()
case let c? where c.isNewline:
// This is a line continuation, skip the newline, and strip any
// following space.
consumer.skip(untilAfter: \.isNewline)
consumer.skip(while: \.isSpaceOrTab)
return true
default:
// Unknown escape sequence, treat the '$' literally.
break
}
}
return consumer.eat()
})
return bytes.isEmpty ? nil : String(utf8: bytes)
}
}
fileprivate extension NinjaParser {
typealias BuildRule = NinjaBuildFile.BuildRule
typealias Attribute = NinjaBuildFile.Attribute
struct ParsedAttribute: Hashable {
var key: String
var value: String
}
enum Lexeme: Hashable {
case attribute(ParsedAttribute)
case element(String)
case build
case newline
case colon
case equal
case pipe
case doublePipe
}
struct Lexer {
private var input: ByteScanner
private(set) var lexeme: Lexeme?
private(set) var isAtStartOfLine = true
private(set) var leadingTriviaCount = 0
init(_ input: ByteScanner) {
self.input = input
self.lexeme = lex()
}
}
var peek: Lexeme? { lexer.lexeme }
@discardableResult
mutating func tryEat(_ lexeme: Lexeme) -> Bool {
guard peek == lexeme else { return false }
eat()
return true
}
mutating func tryEatElement() -> String? {
guard case .element(let str) = peek else { return nil }
eat()
return str
}
@discardableResult
mutating func eat() -> Lexeme? {
defer {
lexer.eat()
}
return peek
}
}
fileprivate extension Byte {
var isNinjaOperator: Bool {
switch self {
case ":", "|", "=":
true
default:
false
}
}
}
fileprivate extension NinjaBuildFile.Attribute {
init?(_ parsed: NinjaParser.ParsedAttribute) {
// Ignore unknown attributes for now.
guard let key = Key(rawValue: parsed.key) else { return nil }
self.init(key: key, value: parsed.value)
}
}
extension NinjaParser.Lexer {
typealias Lexeme = NinjaParser.Lexeme
private mutating func consumeOperator() -> Lexeme {
switch input.eat() {
case ":":
return .colon
case "=":
return .equal
case "|":
if input.tryEat("|") {
return .doublePipe
}
return .pipe
default:
fatalError("Invalid operator character")
}
}
private mutating func consumeElement() -> String? {
input.consumeUnescaped(while: { char in
switch char {
case let c where c.isNinjaOperator || c.isSpaceTabOrNewline:
false
default:
true
}
})
}
private mutating func tryConsumeAttribute(key: String) -> Lexeme? {
input.tryEating { input in
input.skip(while: \.isSpaceOrTab)
guard input.tryEat("=") else { return nil }
input.skip(while: \.isSpaceOrTab)
guard let value = input.consumeUnescaped(while: { !$0.isNewline }) else {
return nil
}
return .attribute(.init(key: key, value: value))
}
}
private mutating func lex() -> Lexeme? {
while true {
isAtStartOfLine = input.previous?.isNewline ?? true
leadingTriviaCount = input.eat(while: \.isSpaceOrTab)?.count ?? 0
guard let c = input.peek else { return nil }
if c == "#" {
input.skip(untilAfter: \.isNewline)
continue
}
if c.isNewline {
input.skip(untilAfter: \.isNewline)
if isAtStartOfLine {
// Ignore empty lines, newlines are only semantically meaningful
// when they delimit non-empty lines.
continue
}
return .newline
}
if c.isNinjaOperator {
return consumeOperator()
}
if isAtStartOfLine && input.tryEat(utf8: "build") {
return .build
}
guard let element = consumeElement() else { return nil }
// If we're on a newline, check to see if we can lex an attribute.
if isAtStartOfLine {
if let attr = tryConsumeAttribute(key: element) {
return attr
}
}
return .element(element)
}
}
@discardableResult
mutating func eat() -> Lexeme? {
defer {
lexeme = lex()
}
return lexeme
}
}
fileprivate extension NinjaParser {
mutating func skipLine() {
while let lexeme = eat(), lexeme != .newline {}
}
mutating func parseAttribute() throws -> ParsedAttribute? {
guard case let .attribute(attr) = peek else { return nil }
eat()
tryEat(.newline)
return attr
}
mutating func parseBuildRule() throws -> BuildRule? {
let indent = lexer.leadingTriviaCount
guard tryEat(.build) else { return nil }
var outputs: [String] = []
while let str = tryEatElement() {
outputs.append(str)
}
// Ignore implicit outputs for now.
if tryEat(.pipe) {
while tryEatElement() != nil {}
}
guard tryEat(.colon) else {
throw NinjaParseError.expected(.colon)
}
var isPhony = false
var inputs: [String] = []
while let str = tryEatElement() {
if str == "phony" {
isPhony = true
} else {
inputs.append(str)
}
}
if isPhony {
skipLine()
return .phony(for: outputs, inputs: inputs)
}
var dependencies: [String] = []
while true {
if let str = tryEatElement() {
dependencies.append(str)
continue
}
if tryEat(.pipe) || tryEat(.doublePipe) {
// Currently we don't distinguish between implicit and explicit deps.
continue
}
break
}
// We're done with the line, skip to the next.
skipLine()
var attributes: [Attribute.Key: Attribute] = [:]
while indent < lexer.leadingTriviaCount, let attr = try parseAttribute() {
if let attr = Attribute(attr) {
attributes[attr.key] = attr
}
}
return BuildRule(
inputs: inputs,
outputs: outputs,
dependencies: dependencies,
attributes: attributes
)
}
mutating func parse() throws -> NinjaBuildFile {
var buildRules: [BuildRule] = []
var attributes: [Attribute.Key: Attribute] = [:]
while peek != nil {
if let rule = try parseBuildRule() {
buildRules.append(rule)
continue
}
if let attr = try parseAttribute() {
if let attr = Attribute(attr) {
attributes[attr.key] = attr
}
continue
}
// Ignore unknown bits of syntax like 'include' for now.
eat()
}
return NinjaBuildFile(attributes: attributes, buildRules: buildRules)
}
}