Skip to content

Commit 1a3f156

Browse files
committed
Avoid evaluating canImport when the result of the #if condition is known
Within the compiler, canImport involves side effects, so we need to follow along with how the compiler works precisely to avoid changing behavior.
1 parent 3f5c9ea commit 1a3f156

File tree

3 files changed

+103
-28
lines changed

3 files changed

+103
-28
lines changed

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -132,52 +132,63 @@ func evaluateIfConfig(
132132
}
133133

134134
// Evaluate the left-hand side.
135-
let (lhsActive, lhssyntaxErrorsAllowed, lhsDiagnostics) = evaluateIfConfig(
135+
let (lhsActive, lhsSyntaxErrorsAllowed, lhsDiagnostics) = evaluateIfConfig(
136136
condition: binOp.leftOperand,
137137
configuration: configuration,
138138
outermostCondition: false
139139
)
140140

141-
// Short-circuit evaluation if we know the answer and the left-hand side
142-
// was syntaxErrorsAllowed.
143-
if lhssyntaxErrorsAllowed {
144-
switch (lhsActive, op.operator.text) {
145-
case (true, "||"):
146-
return (
147-
active: true,
148-
syntaxErrorsAllowed: lhssyntaxErrorsAllowed,
149-
diagnostics: extraDiagnostics + lhsDiagnostics
150-
)
151-
case (false, "&&"):
152-
return (
153-
active: false,
154-
syntaxErrorsAllowed: lhssyntaxErrorsAllowed,
155-
diagnostics: extraDiagnostics + lhsDiagnostics
156-
)
157-
default:
158-
break
159-
}
141+
// Short-circuit evaluation if we know the answer. We still recurse into
142+
// the right-hand side, but with a dummy configuration that won't have
143+
// side effects, so we only get validation-related errors.
144+
let shortCircuitResult: Bool?
145+
switch (lhsActive, op.operator.text) {
146+
case (true, "||"): shortCircuitResult = true
147+
case (false, "&&"): shortCircuitResult = false
148+
default: shortCircuitResult = nil
149+
}
150+
151+
// If we are supposed to short-circuit and the left-hand side of this
152+
// operator with inactive &&, stop now: we shouldn't evaluate the right-
153+
// hand side at all.
154+
if let isActive = shortCircuitResult, lhsSyntaxErrorsAllowed {
155+
return (
156+
active: isActive,
157+
syntaxErrorsAllowed: lhsSyntaxErrorsAllowed,
158+
diagnostics: extraDiagnostics + lhsDiagnostics
159+
)
160160
}
161161

162162
// Evaluate the right-hand side.
163-
let (rhsActive, rhssyntaxErrorsAllowed, rhsDiagnostics) = evaluateIfConfig(
164-
condition: binOp.rightOperand,
165-
configuration: configuration,
166-
outermostCondition: false
167-
)
163+
let rhsActive: Bool
164+
let rhsSyntaxErrorsAllowed: Bool
165+
let rhsDiagnostics: [Diagnostic]
166+
if shortCircuitResult != nil {
167+
(rhsActive, rhsSyntaxErrorsAllowed, rhsDiagnostics) = evaluateIfConfig(
168+
condition: binOp.rightOperand,
169+
configuration: CanImportSuppressingBuildConfiguration(other: configuration),
170+
outermostCondition: false
171+
)
172+
} else {
173+
(rhsActive, rhsSyntaxErrorsAllowed, rhsDiagnostics) = evaluateIfConfig(
174+
condition: binOp.rightOperand,
175+
configuration: configuration,
176+
outermostCondition: false
177+
)
178+
}
168179

169180
switch op.operator.text {
170181
case "||":
171182
return (
172183
active: lhsActive || rhsActive,
173-
syntaxErrorsAllowed: lhssyntaxErrorsAllowed && rhssyntaxErrorsAllowed,
184+
syntaxErrorsAllowed: lhsSyntaxErrorsAllowed && rhsSyntaxErrorsAllowed,
174185
diagnostics: extraDiagnostics + lhsDiagnostics + rhsDiagnostics
175186
)
176187

177188
case "&&":
178189
return (
179190
active: lhsActive && rhsActive,
180-
syntaxErrorsAllowed: lhssyntaxErrorsAllowed || rhssyntaxErrorsAllowed,
191+
syntaxErrorsAllowed: lhsSyntaxErrorsAllowed || rhsSyntaxErrorsAllowed,
181192
diagnostics: extraDiagnostics + lhsDiagnostics + rhsDiagnostics
182193
)
183194

@@ -674,3 +685,55 @@ extension ExprSyntaxProtocol {
674685
return false
675686
}
676687
}
688+
689+
/// Build configuration adaptor that suppresses calls to canImport, which
690+
/// can have side effects. This is somewhat of a hack for the compiler.
691+
private struct CanImportSuppressingBuildConfiguration<Other: BuildConfiguration>: BuildConfiguration {
692+
var other: Other
693+
694+
func isCustomConditionSet(name: String) throws -> Bool {
695+
return try other.isCustomConditionSet(name: name)
696+
}
697+
698+
func hasFeature(name: String) throws -> Bool {
699+
return try other.hasFeature(name: name)
700+
}
701+
702+
func hasAttribute(name: String) throws -> Bool {
703+
return try other.hasAttribute(name: name)
704+
}
705+
706+
func canImport(importPath: [String], version: CanImportVersion) throws -> Bool {
707+
return false
708+
}
709+
710+
func isActiveTargetOS(name: String) throws -> Bool {
711+
return try other.isActiveTargetOS(name: name)
712+
}
713+
714+
func isActiveTargetArchitecture(name: String) throws -> Bool {
715+
return try other.isActiveTargetArchitecture(name: name)
716+
}
717+
718+
func isActiveTargetEnvironment(name: String) throws -> Bool {
719+
return try other.isActiveTargetEnvironment(name: name)
720+
}
721+
722+
func isActiveTargetRuntime(name: String) throws -> Bool {
723+
return try other.isActiveTargetRuntime(name: name)
724+
}
725+
726+
func isActiveTargetPointerAuthentication(name: String) throws -> Bool {
727+
return try other.isActiveTargetPointerAuthentication(name: name)
728+
}
729+
730+
var targetPointerBitWidth: Int { return other.targetPointerBitWidth }
731+
732+
var targetAtomicBitWidths: [Int] { return other.targetAtomicBitWidths }
733+
734+
var endianness: Endianness { return other.endianness }
735+
736+
var languageVersion: VersionTuple { return other.languageVersion }
737+
738+
var compilerVersion: VersionTuple { return other.compilerVersion }
739+
}

Tests/SwiftIfConfigTest/EvaluateTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,11 @@ public class EvaluateTests: XCTestCase {
387387
)
388388
]
389389
)
390+
391+
assertIfConfig(
392+
"canImport(SwiftSyntax) || canImport(ExplodingModule)",
393+
.active
394+
)
390395
}
391396

392397
func testLikelySimulatorEnvironment() throws {

Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import SwiftSyntax
1515

1616
enum BuildConfigurationError: Error, CustomStringConvertible {
1717
case badAttribute(String)
18+
case badModule(String)
1819

1920
var description: String {
2021
switch self {
2122
case .badAttribute(let attribute):
2223
return "unacceptable attribute '\(attribute)'"
24+
case .badModule(let module):
25+
return "unacceptable module '\(module)'"
2326
}
2427
}
2528
}
@@ -53,11 +56,15 @@ struct TestingBuildConfiguration: BuildConfiguration {
5356
func canImport(
5457
importPath: [String],
5558
version: CanImportVersion
56-
) -> Bool {
59+
) throws -> Bool {
5760
guard let moduleName = importPath.first else {
5861
return false
5962
}
6063

64+
if moduleName == "ExplodingModule" {
65+
throw BuildConfigurationError.badModule(moduleName)
66+
}
67+
6168
guard moduleName == "SwiftSyntax" else { return false }
6269

6370
switch version {

0 commit comments

Comments
 (0)