Skip to content

Commit 93fb0b9

Browse files
committed
Introduce Swift Subprocess
1 parent 0e23938 commit 93fb0b9

13 files changed

+2637
-1
lines changed

Package.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ let package = Package(
3333
exact: "0.0.5"),
3434
.package(
3535
url: "https://github.com/apple/swift-syntax.git",
36-
from: "509.0.2")
36+
from: "509.0.2"),
37+
.package(
38+
url: "https://github.com/apple/swift-system",
39+
from: "1.0.0")
3740
],
3841
targets: [
3942
// Foundation (umbrella)
@@ -63,6 +66,7 @@ let package = Package(
6366
"_CShims",
6467
"FoundationMacros",
6568
.product(name: "_RopeModule", package: "swift-collections"),
69+
.product(name: "SystemPackage", package: "swift-system"),
6670
],
6771
cSettings: [
6872
.define("_GNU_SOURCE", .when(platforms: [.linux]))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
14+
public struct AsyncLineSequence<Base: AsyncSequence>: AsyncSequence where Base.Element == UInt8 {
15+
public typealias Element = String
16+
17+
var base: Base
18+
19+
public struct AsyncIterator: AsyncIteratorProtocol {
20+
public typealias Element = String
21+
22+
var byteSource: Base.AsyncIterator
23+
var buffer: Array<UInt8> = []
24+
var leftover: UInt8? = nil
25+
26+
internal init(underlyingIterator: Base.AsyncIterator) {
27+
byteSource = underlyingIterator
28+
}
29+
30+
// We'd like to reserve flexibility to improve the implementation of
31+
// next() in the future, so aren't marking it @inlinable. Manually
32+
// specializing for the common source types helps us get back some of
33+
// the performance we're leaving on the table.
34+
@_specialize(where Base == Subprocess.AsyncBytes)
35+
public mutating func next() async rethrows -> String? {
36+
/*
37+
0D 0A: CR-LF
38+
0A | 0B | 0C | 0D: LF, VT, FF, CR
39+
E2 80 A8: U+2028 (LINE SEPARATOR)
40+
E2 80 A9: U+2029 (PARAGRAPH SEPARATOR)
41+
*/
42+
let _CR: UInt8 = 0x0D
43+
let _LF: UInt8 = 0x0A
44+
let _NEL_PREFIX: UInt8 = 0xC2
45+
let _NEL_SUFFIX: UInt8 = 0x85
46+
let _SEPARATOR_PREFIX: UInt8 = 0xE2
47+
let _SEPARATOR_CONTINUATION: UInt8 = 0x80
48+
let _SEPARATOR_SUFFIX_LINE: UInt8 = 0xA8
49+
let _SEPARATOR_SUFFIX_PARAGRAPH: UInt8 = 0xA9
50+
51+
func yield() -> String? {
52+
defer {
53+
buffer.removeAll(keepingCapacity: true)
54+
}
55+
if buffer.isEmpty {
56+
return nil
57+
}
58+
return String(decoding: buffer, as: UTF8.self)
59+
}
60+
61+
func nextByte() async throws -> UInt8? {
62+
defer { leftover = nil }
63+
if let leftover = leftover {
64+
return leftover
65+
}
66+
return try await byteSource.next()
67+
}
68+
69+
while let first = try await nextByte() {
70+
switch first {
71+
case _CR:
72+
let result = yield()
73+
// Swallow up any subsequent LF
74+
guard let next = try await byteSource.next() else {
75+
return result //if we ran out of bytes, the last byte was a CR
76+
}
77+
if next != _LF {
78+
leftover = next
79+
}
80+
if let result = result {
81+
return result
82+
}
83+
continue
84+
case _LF..<_CR:
85+
guard let result = yield() else {
86+
continue
87+
}
88+
return result
89+
case _NEL_PREFIX: // this may be used to compose other UTF8 characters
90+
guard let next = try await byteSource.next() else {
91+
// technically invalid UTF8 but it should be repaired to "\u{FFFD}"
92+
buffer.append(first)
93+
return yield()
94+
}
95+
if next != _NEL_SUFFIX {
96+
buffer.append(first)
97+
buffer.append(next)
98+
} else {
99+
guard let result = yield() else {
100+
continue
101+
}
102+
return result
103+
}
104+
case _SEPARATOR_PREFIX:
105+
// Try to read: 80 [A8 | A9].
106+
// If we can't, then we put the byte in the buffer for error correction
107+
guard let next = try await byteSource.next() else {
108+
buffer.append(first)
109+
return yield()
110+
}
111+
guard next == _SEPARATOR_CONTINUATION else {
112+
buffer.append(first)
113+
buffer.append(next)
114+
continue
115+
}
116+
guard let fin = try await byteSource.next() else {
117+
buffer.append(first)
118+
buffer.append(next)
119+
return yield()
120+
121+
}
122+
guard fin == _SEPARATOR_SUFFIX_LINE || fin == _SEPARATOR_SUFFIX_PARAGRAPH else {
123+
buffer.append(first)
124+
buffer.append(next)
125+
buffer.append(fin)
126+
continue
127+
}
128+
if let result = yield() {
129+
return result
130+
}
131+
continue
132+
default:
133+
buffer.append(first)
134+
}
135+
}
136+
// Don't emit an empty newline when there is no more content (e.g. end of file)
137+
if !buffer.isEmpty {
138+
return yield()
139+
}
140+
return nil
141+
}
142+
143+
}
144+
145+
public func makeAsyncIterator() -> AsyncIterator {
146+
return AsyncIterator(underlyingIterator: base.makeAsyncIterator())
147+
}
148+
149+
internal init(underlyingSequence: Base) {
150+
base = underlyingSequence
151+
}
152+
}
153+
154+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
155+
extension AsyncLineSequence : Sendable where Base : Sendable {}
156+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
157+
extension AsyncLineSequence.AsyncIterator : Sendable where Base.AsyncIterator : Sendable {}
158+
159+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
160+
public extension AsyncSequence where Self.Element == UInt8 {
161+
/**
162+
A non-blocking sequence of newline-separated `Strings` created by decoding the elements of `self` as UTF8.
163+
*/
164+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
165+
var lines: AsyncLineSequence<Self> {
166+
AsyncLineSequence(underlyingSequence: self)
167+
}
168+
}

0 commit comments

Comments
 (0)