Skip to content

Commit

Permalink
Introduce Swift Subprocess
Browse files Browse the repository at this point in the history
  • Loading branch information
iCharlesHu committed Feb 26, 2024
1 parent 0e23938 commit 93fb0b9
Show file tree
Hide file tree
Showing 13 changed files with 2,637 additions and 1 deletion.
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ let package = Package(
exact: "0.0.5"),
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "509.0.2")
from: "509.0.2"),
.package(
url: "https://github.com/apple/swift-system",
from: "1.0.0")
],
targets: [
// Foundation (umbrella)
Expand Down Expand Up @@ -63,6 +66,7 @@ let package = Package(
"_CShims",
"FoundationMacros",
.product(name: "_RopeModule", package: "swift-collections"),
.product(name: "SystemPackage", package: "swift-system"),
],
cSettings: [
.define("_GNU_SOURCE", .when(platforms: [.linux]))
Expand Down
168 changes: 168 additions & 0 deletions Sources/FoundationEssentials/AsyncLineSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2019 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
//
//===----------------------------------------------------------------------===//

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public struct AsyncLineSequence<Base: AsyncSequence>: AsyncSequence where Base.Element == UInt8 {
public typealias Element = String

var base: Base

public struct AsyncIterator: AsyncIteratorProtocol {
public typealias Element = String

var byteSource: Base.AsyncIterator
var buffer: Array<UInt8> = []
var leftover: UInt8? = nil

internal init(underlyingIterator: Base.AsyncIterator) {
byteSource = underlyingIterator
}

// We'd like to reserve flexibility to improve the implementation of
// next() in the future, so aren't marking it @inlinable. Manually
// specializing for the common source types helps us get back some of
// the performance we're leaving on the table.
@_specialize(where Base == Subprocess.AsyncBytes)
public mutating func next() async rethrows -> String? {
/*
0D 0A: CR-LF
0A | 0B | 0C | 0D: LF, VT, FF, CR
E2 80 A8: U+2028 (LINE SEPARATOR)
E2 80 A9: U+2029 (PARAGRAPH SEPARATOR)
*/
let _CR: UInt8 = 0x0D
let _LF: UInt8 = 0x0A
let _NEL_PREFIX: UInt8 = 0xC2
let _NEL_SUFFIX: UInt8 = 0x85
let _SEPARATOR_PREFIX: UInt8 = 0xE2
let _SEPARATOR_CONTINUATION: UInt8 = 0x80
let _SEPARATOR_SUFFIX_LINE: UInt8 = 0xA8
let _SEPARATOR_SUFFIX_PARAGRAPH: UInt8 = 0xA9

func yield() -> String? {
defer {
buffer.removeAll(keepingCapacity: true)
}
if buffer.isEmpty {
return nil
}
return String(decoding: buffer, as: UTF8.self)
}

func nextByte() async throws -> UInt8? {
defer { leftover = nil }
if let leftover = leftover {
return leftover
}
return try await byteSource.next()
}

while let first = try await nextByte() {
switch first {
case _CR:
let result = yield()
// Swallow up any subsequent LF
guard let next = try await byteSource.next() else {
return result //if we ran out of bytes, the last byte was a CR
}
if next != _LF {
leftover = next
}
if let result = result {
return result
}
continue
case _LF..<_CR:
guard let result = yield() else {
continue
}
return result
case _NEL_PREFIX: // this may be used to compose other UTF8 characters
guard let next = try await byteSource.next() else {
// technically invalid UTF8 but it should be repaired to "\u{FFFD}"
buffer.append(first)
return yield()
}
if next != _NEL_SUFFIX {
buffer.append(first)
buffer.append(next)
} else {
guard let result = yield() else {
continue
}
return result
}
case _SEPARATOR_PREFIX:
// Try to read: 80 [A8 | A9].
// If we can't, then we put the byte in the buffer for error correction
guard let next = try await byteSource.next() else {
buffer.append(first)
return yield()
}
guard next == _SEPARATOR_CONTINUATION else {
buffer.append(first)
buffer.append(next)
continue
}
guard let fin = try await byteSource.next() else {
buffer.append(first)
buffer.append(next)
return yield()

}
guard fin == _SEPARATOR_SUFFIX_LINE || fin == _SEPARATOR_SUFFIX_PARAGRAPH else {
buffer.append(first)
buffer.append(next)
buffer.append(fin)
continue
}
if let result = yield() {
return result
}
continue
default:
buffer.append(first)
}
}
// Don't emit an empty newline when there is no more content (e.g. end of file)
if !buffer.isEmpty {
return yield()
}
return nil
}

}

public func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(underlyingIterator: base.makeAsyncIterator())
}

internal init(underlyingSequence: Base) {
base = underlyingSequence
}
}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension AsyncLineSequence : Sendable where Base : Sendable {}
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension AsyncLineSequence.AsyncIterator : Sendable where Base.AsyncIterator : Sendable {}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public extension AsyncSequence where Self.Element == UInt8 {
/**
A non-blocking sequence of newline-separated `Strings` created by decoding the elements of `self` as UTF8.
*/
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
var lines: AsyncLineSequence<Self> {
AsyncLineSequence(underlyingSequence: self)
}
}

0 comments on commit 93fb0b9

Please sign in to comment.