Skip to content

Commit b6e679f

Browse files
committed
[ResultBuilders] buildBlock(combining:into:) for pairwise combination.
Allow a user-defined `buildBlock(combining:into:)` to combine subexpressions in a block pairwise top to bottom. To use `buildBlock(_combining:into:)`, the user also needs to provide a unary `buildBlock(_:)` as a base case. The feature is being gated under frontend flag `-enable-experimental-pairwise-build-block`. This will enable use cases in `RegexBuilder` in experimental declarative string processing, where we need to concatenate tuples and conditionally skip captureless regexes. For example: ```swift let regex = Regex { "a" // Regex<Substring> OneOrMore("b").capture() // Regex<(Substring, Substring)> "c" // Regex<Substring> Optionally("d".capture()) // Regex<(Substring, Substring?)> } // Regex<Tuple3<Substring, Substring, Substring?>> let result = "abc".firstMatch(of: regex) // MatchResult<(Substring, Substring, Substring?)> ``` In this example, patterns `"a"` and `"c"` have no captures, so we need to skip them. However with the existing result builder `buildBlock()` feature that builds a block wholesale from all subexpressions, we had to generate `2^arity` overloads accounting for any occurrences of captureless regexes. There are also other complexities such as having to drop-first from the tuple to obtain the capture type. Though these features could in theory be supported via variadic generics, we feel that allowing result builders to pairwise combine subexpressions in a block is a much simpler and potentially more useful approach. With `buildBlock(_combining:into:)`, the regex builders can be defined as the following, assuming we have variadic generics: ```swift enum RegexBuilder { static func buildBlock() -> Regex<Substring> static func buildBlock<Match>(_ x: Regex<Match>) -> Regex<Match> static func buildBlock< ExistingWholeMatch, NewWholeMatch, ExistingCaptures..., NewCaptures... >( _combining next: Regex<(NewWholeMatch, NewCaptures...)>, into combined: Regex<(ExistingWholeMatch, ExistingCaptures...)> ) -> Regex<Substring, ExistingCaptures..., NewCaptures...> } ``` Before we have variadic generics, we can define overloads of `buildBlock(_combining:into:)` for up to a certain arity. These overloads will be much fewer than `2^arity`.
1 parent 37c4198 commit b6e679f

File tree

6 files changed

+248
-4
lines changed

6 files changed

+248
-4
lines changed

include/swift/AST/KnownIdentifiers.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ IDENTIFIER_WITH_NAME(code_, "_code")
4747
IDENTIFIER(CodingKeys)
4848
IDENTIFIER(codingPath)
4949
IDENTIFIER(combine)
50+
IDENTIFIER(combining)
5051
IDENTIFIER_(Concurrency)
5152
IDENTIFIER(container)
5253
IDENTIFIER(Context)

include/swift/Basic/LangOptions.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ namespace swift {
335335
/// Enable experimental 'move only' features.
336336
bool EnableExperimentalMoveOnly = false;
337337

338+
/// Enable experimental pairwise `buildBlock` for result builders.
339+
bool EnableExperimentalPairwiseBuildBlock = false;
340+
338341
/// Disable the implicit import of the _Concurrency module.
339342
bool DisableImplicitConcurrencyModuleImport =
340343
!SWIFT_IMPLICIT_CONCURRENCY_IMPORT;

include/swift/Option/FrontendOptions.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ def enable_experimental_eager_clang_module_diagnostics :
295295
Flag<["-"], "enable-experimental-eager-clang-module-diagnostics">,
296296
HelpText<"Enable experimental eager diagnostics reporting on the importability of all referenced C, C++, and Objective-C libraries">;
297297

298+
def enable_experimental_pairwise_build_block :
299+
Flag<["-"], "enable-experimental-pairwise-build-block">,
300+
HelpText<"Enable experimental pairwise 'buildBlock' for result builders">;
301+
298302
def enable_resilience : Flag<["-"], "enable-resilience">,
299303
HelpText<"Deprecated, use -enable-library-evolution instead">;
300304
}

lib/Frontend/CompilerInvocation.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
454454
Opts.EnableExperimentalMoveOnly |=
455455
Args.hasArg(OPT_enable_experimental_move_only);
456456

457+
Opts.EnableExperimentalPairwiseBuildBlock |=
458+
Args.hasArg(OPT_enable_experimental_pairwise_build_block);
459+
457460
Opts.EnableInferPublicSendable |=
458461
Args.hasFlag(OPT_enable_infer_public_concurrent_value,
459462
OPT_disable_infer_public_concurrent_value,

lib/Sema/BuilderTransform.cpp

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,34 @@ class BuilderClosureVisitor
367367
if (!cs || hadError)
368368
return nullptr;
369369

370-
// Call Builder.buildBlock(... args ...)
371-
auto call = buildCallIfWanted(braceStmt->getStartLoc(),
372-
ctx.Id_buildBlock, expressions,
373-
/*argLabels=*/{ });
370+
Expr *call = nullptr;
371+
// If the builder supports `buildBlock(combining:into:)`, use this to
372+
// combine subexpressions pairwise.
373+
if (ctx.LangOpts.EnableExperimentalPairwiseBuildBlock &&
374+
!expressions.empty() &&
375+
builderSupports(ctx.Id_buildBlock, {ctx.Id_combining, ctx.Id_into})) {
376+
// NOTE: The current implementation uses one-way constraints in between
377+
// subexpressions. It's functionally equivalent to the following:
378+
// let v0 = Builder.buildBlock(arg_0)
379+
// let v1 = Builder.buildBlock(combining: arg_1, into: v0)
380+
// ...
381+
// return Builder.buildBlock(combining: arg_n, into: ...)
382+
call = buildCallIfWanted(braceStmt->getStartLoc(), ctx.Id_buildBlock,
383+
{expressions.front()}, /*argLabels=*/{});
384+
for (auto *expr : llvm::drop_begin(expressions)) {
385+
call = buildCallIfWanted(braceStmt->getStartLoc(), ctx.Id_buildBlock,
386+
{expr, new (ctx) OneWayExpr(call)},
387+
{ctx.Id_combining, ctx.Id_into});
388+
}
389+
}
390+
// Otherwise, call `buildBlock` on all subexpressions.
391+
else {
392+
// Call Builder.buildBlock(... args ...)
393+
call = buildCallIfWanted(braceStmt->getStartLoc(),
394+
ctx.Id_buildBlock, expressions,
395+
/*argLabels=*/{ });
396+
}
397+
374398
if (!call)
375399
return nullptr;
376400

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-pairwise-build-block) | %FileCheck %s
2+
// REQUIRES: executable_test
3+
4+
struct Values<T> {
5+
var values: T
6+
7+
init(values: T) {
8+
self.values = values
9+
}
10+
11+
func map<R>(_ f: (T) -> R) -> Values<R> {
12+
.init(values: f(values))
13+
}
14+
}
15+
16+
@resultBuilder
17+
enum NestedTupleBuilder {
18+
static func buildBlock<T>(_ x: T) -> Values<T> {
19+
.init(values: x)
20+
}
21+
22+
static func buildBlock<T, U>(
23+
combining next: U, into combined: Values<T>
24+
) -> Values<(T, U)> {
25+
.init(values: (combined.values, next))
26+
}
27+
}
28+
29+
extension Values {
30+
init(@NestedTupleBuilder nested values: () -> Self) {
31+
self = values()
32+
}
33+
}
34+
35+
let nestedValues = Values(nested: {
36+
1
37+
"2"
38+
3.0
39+
"yes"
40+
})
41+
print(nestedValues)
42+
43+
// CHECK: Values<(((Int, String), Double), String)>(values: (((1, "2"), 3.0), "yes"))
44+
45+
@resultBuilder
46+
enum FlatTupleBuilder {
47+
static func buildExpression<T>(_ x: T) -> Values<T> {
48+
.init(values: x)
49+
}
50+
51+
static func buildBlock<T>(_ x: Values<T>) -> Values<T> {
52+
.init(values: x.values)
53+
}
54+
55+
static func buildBlock<T, N>(
56+
combining new: Values<N>,
57+
into combined: Values<T>
58+
) -> Values<(T, N)> {
59+
.init(values: (combined.values, new.values))
60+
}
61+
62+
static func buildBlock<T0, T1, N>(
63+
combining new: Values<N>,
64+
into combined: Values<(T0, T1)>
65+
) -> Values<(T0, T1, N)> {
66+
.init(values: (combined.values.0, combined.values.1, new.values))
67+
}
68+
69+
static func buildBlock<T0, T1, T2, N>(
70+
combining new: Values<N>,
71+
into combined: Values<(T0, T1, T2)>
72+
) -> Values<(T0, T1, T2, N)> {
73+
.init(values: (combined.values.0, combined.values.1, combined.values.2, new.values))
74+
}
75+
76+
static func buildBlock<T0, T1, T2, T3, N>(
77+
combining new: Values<N>,
78+
into combined: Values<(T0, T1, T2, T3)>
79+
) -> Values<(T0, T1, T2, T3, N)> {
80+
.init(values: (combined.values.0, combined.values.1, combined.values.2, combined.values.3, new.values))
81+
}
82+
83+
static func buildBlock(_ x: Never...) -> Values<()> {
84+
assert(x.isEmpty, "I should never be called unless it's nullary")
85+
return .init(values: ())
86+
}
87+
88+
static func buildEither<T>(first: T) -> T {
89+
first
90+
}
91+
92+
static func buildEither<T>(second: T) -> T {
93+
second
94+
}
95+
96+
static func buildOptional<T>(_ x: Values<T>?) -> Values<T?> {
97+
x?.map { $0 } ?? .init(values: nil)
98+
}
99+
100+
static func buildLimitedAvailability<T>(_ x: Values<T>) -> Values<T> {
101+
x
102+
}
103+
}
104+
105+
extension Values {
106+
init(@FlatTupleBuilder flat values: () -> Self) {
107+
self = values()
108+
}
109+
}
110+
111+
let flatValues0 = Values(flat: {})
112+
print(flatValues0)
113+
// CHECK: Values<()>(values: ())
114+
115+
let flatValues1 = Values(flat: {
116+
1
117+
"2"
118+
3.0
119+
})
120+
print(flatValues1)
121+
// CHECK: Values<(Int, String, Double)>(values: (1, "2", 3.0))
122+
123+
let flatValues2 = Values(flat: {
124+
1
125+
"2"
126+
let y = 3.0 + 4.0
127+
#if false
128+
"not gonna happen"
129+
#endif
130+
if true {
131+
"yes"
132+
} else {
133+
"no"
134+
}
135+
#warning("Beware of pairwise block building")
136+
#if true
137+
if false {
138+
"nah"
139+
}
140+
if #available(SwiftStdlib 5.0, *) {
141+
5.0
142+
}
143+
#endif
144+
})
145+
print(flatValues2)
146+
147+
// CHECK: Values<(Int, String, String, Optional<String>, Optional<Double>)>(values: (1, "2", "yes", nil, Optional(5.0)))
148+
149+
struct Nil: CustomStringConvertible {
150+
var description: String {
151+
"nil"
152+
}
153+
}
154+
struct Cons<Head, Tail>: CustomStringConvertible {
155+
var head: Head
156+
var tail: Tail
157+
158+
var description: String {
159+
"(cons \(String(reflecting: head)) \(tail))"
160+
}
161+
}
162+
163+
@resultBuilder
164+
enum ListBuilder {
165+
static func buildBlock() -> Nil {
166+
Nil()
167+
}
168+
169+
static func buildBlock<T>(_ x: T) -> Cons<T, Nil> {
170+
.init(head: x, tail: Nil())
171+
}
172+
173+
static func buildBlock<New, T>(combining new: New, into combined: T) -> Cons<New, T> {
174+
.init(head: new, tail: combined)
175+
}
176+
177+
static func buildBlock<T>(_ x: T...) -> [T] {
178+
fatalError("I should never be called!")
179+
}
180+
}
181+
182+
func list<T>(@ListBuilder f: () -> T) -> T {
183+
f()
184+
}
185+
186+
let list0 = list {}
187+
print(list0)
188+
// CHECK: nil
189+
190+
let list1 = list { "1" }
191+
print(list1)
192+
// Check: (cons 1 nil)
193+
194+
let list2 = list {
195+
1
196+
2
197+
}
198+
print(list2)
199+
// CHECK: (cons 2 (cons 1 nil))
200+
let list3 = list {
201+
1
202+
list {
203+
2.0
204+
"3"
205+
}
206+
"4"
207+
}
208+
print(list3)
209+
// CHECK: (cons "4" (cons (cons "3" (cons 2.0 nil)) (cons 1 nil)))

0 commit comments

Comments
 (0)