Repository: hylo-lang/hylo (swift)

Issue:
Support `&&` and `||` operators in compiler conditions
- Support `&&` and `||` operators in compiler conditions

Follow-up to https://github.com/hylo-lang/hylo/pull/1282

Pull request description:
This PR is finishing the work started by @Metalymph on logic operators in compile-time conditions (see #1358). The original PR was #1410.

Patch:
diff --git a/Sources/CodeGen/LLVM/LLVMProgram.swift b/Sources/CodeGen/LLVM/LLVMProgram.swift
index 54b90ee93..b2d57f2a4 100644
--- a/Sources/CodeGen/LLVM/LLVMProgram.swift
+++ b/Sources/CodeGen/LLVM/LLVMProgram.swift
@@ -55,7 +55,9 @@ public struct LLVMProgram {
   /// to `directory`, returning the URL of each written file.
   ///
   /// - Returns: The URL of each written product, one for each module in `self`.
-  public func write(_ type: SwiftyLLVM.CodeGenerationResultType, to directory: URL) throws -> [URL] {
+  public func write(
+    _ type: SwiftyLLVM.CodeGenerationResultType, to directory: URL
+  ) throws -> [URL] {
     precondition(directory.hasDirectoryPath)
     var result: [URL] = []
     for m in llvmModules.values {
diff --git a/Sources/CodeGen/LLVM/Transpilation.swift b/Sources/CodeGen/LLVM/Transpilation.swift
index ba3ea58c8..b31383170 100644
--- a/Sources/CodeGen/LLVM/Transpilation.swift
+++ b/Sources/CodeGen/LLVM/Transpilation.swift
@@ -322,7 +322,8 @@ extension SwiftyLLVM.Module {
     if !implementations.isEmpty {
       tableContents.append(
         SwiftyLLVM.ArrayConstant(
-          of: SwiftyLLVM.StructType([word(), ptr], in: &self), containing: implementations, in: &self))
+          of: SwiftyLLVM.StructType([word(), ptr], in: &self), containing: implementations,
+          in: &self))
     }
 
     let table = SwiftyLLVM.StructConstant(aggregating: tableContents, in: &self)
@@ -927,42 +928,48 @@ extension SwiftyLLVM.Module {
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.sadd.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .unsignedAdditionWithOverflow(let t):
         let l = llvm(s.operands[0])
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.uadd.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .signedSubtractionWithOverflow(let t):
         let l = llvm(s.operands[0])
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.ssub.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .unsignedSubtractionWithOverflow(let t):
         let l = llvm(s.operands[0])
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.usub.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .signedMultiplicationWithOverflow(let t):
         let l = llvm(s.operands[0])
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.smul.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .unsignedMultiplicationWithOverflow(let t):
         let l = llvm(s.operands[0])
         let r = llvm(s.operands[1])
         let f = intrinsic(
           named: Intrinsic.llvm.umul.with.overflow, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [l, r], at: insertionPoint)
 
       case .icmp(let p, _):
         let l = llvm(s.operands[0])
@@ -1046,7 +1053,8 @@ extension SwiftyLLVM.Module {
       case .ctpop(let t):
         let source = llvm(s.operands[0])
         let f = intrinsic(named: Intrinsic.llvm.ctpop, for: [ir.llvm(builtinType: t, in: &self)])!
-        register[.register(i)] = insertCall(SwiftyLLVM.Function(f)!, on: [source], at: insertionPoint)
+        register[.register(i)] = insertCall(
+          SwiftyLLVM.Function(f)!, on: [source], at: insertionPoint)
 
       case .ctlz(let t):
         let source = llvm(s.operands[0])
diff --git a/Sources/CodeGen/LLVM/TypeLowering.swift b/Sources/CodeGen/LLVM/TypeLowering.swift
index 4c640c389..83c1602a4 100644
--- a/Sources/CodeGen/LLVM/TypeLowering.swift
+++ b/Sources/CodeGen/LLVM/TypeLowering.swift
@@ -82,7 +82,9 @@ extension IR.Program {
   /// Returns the LLVM form of `val` in `module`.
   ///
   /// - Requires: `val` is representable in LLVM.
-  func llvm(boundGenericType val: BoundGenericType, in module: inout SwiftyLLVM.Module) -> SwiftyLLVM.IRType {
+  func llvm(
+    boundGenericType val: BoundGenericType, in module: inout SwiftyLLVM.Module
+  ) -> SwiftyLLVM.IRType {
     precondition(val[.isCanonical])
 
     let fields = base.storage(of: val.base).map { (part) in
diff --git a/Sources/Driver/Driver.swift b/Sources/Driver/Driver.swift
index 713db97c6..5e123f79a 100644
--- a/Sources/Driver/Driver.swift
+++ b/Sources/Driver/Driver.swift
@@ -3,8 +3,8 @@ import CodeGenLLVM
 import Foundation
 import FrontEnd
 import IR
-import SwiftyLLVM
 import StandardLibrary
+import SwiftyLLVM
 import Utils
 
 public struct Driver: ParsableCommand {
diff --git a/Sources/FrontEnd/AST/Stmt/ConditionalCompilationStmt.swift b/Sources/FrontEnd/AST/Stmt/ConditionalCompilationStmt.swift
index b186ed1ae..e74ec3620 100644
--- a/Sources/FrontEnd/AST/Stmt/ConditionalCompilationStmt.swift
+++ b/Sources/FrontEnd/AST/Stmt/ConditionalCompilationStmt.swift
@@ -52,6 +52,12 @@ public struct ConditionalCompilationStmt: Stmt {
     /// Holds iff the payload doesn't.
     case not(Condition)
 
+    /// Holds iff both conditions in the payload do.
+    case and(Condition, Condition)
+
+    /// Holds iff either condition in the payload does.
+    case or(Condition, Condition)
+
     /// `true` iff the body of the conditional-compilation shouldn't be parsed.
     public var mayNotNeedParsing: Bool {
       switch self {
@@ -63,6 +69,10 @@ public struct ConditionalCompilationStmt: Stmt {
         return true
       case .not(let c):
         return c.mayNotNeedParsing
+      case .and(let l, let r):
+        return l.mayNotNeedParsing && r.mayNotNeedParsing
+      case .or(let l, let r):
+        return l.mayNotNeedParsing || r.mayNotNeedParsing
       default:
         return false
       }
@@ -89,6 +99,10 @@ public struct ConditionalCompilationStmt: Stmt {
         return comparison.evaluate(for: factors.hyloVersion)
       case .not(let c):
         return !c.holds(for: factors)
+      case .and(let l, let r):
+        return l.holds(for: factors) && r.holds(for: factors)
+      case .or(let l, let r):
+        return l.holds(for: factors) || r.holds(for: factors)
       }
     }
 
diff --git a/Sources/FrontEnd/BuiltinFunction.swift b/Sources/FrontEnd/BuiltinFunction.swift
index 7ca0db7f9..976d3e4f6 100644
--- a/Sources/FrontEnd/BuiltinFunction.swift
+++ b/Sources/FrontEnd/BuiltinFunction.swift
@@ -352,7 +352,9 @@ private func mathFlags(_ stream: inout ArraySlice<Substring>) -> NativeInstructi
 }
 
 /// Returns an overflow behavior parsed from `stream` or `.ignore` if none can be parsed.
-private func overflowBehavior(_ stream: inout ArraySlice<Substring>) -> SwiftyLLVM.OverflowBehavior {
+private func overflowBehavior(
+  _ stream: inout ArraySlice<Substring>
+) -> SwiftyLLVM.OverflowBehavior {
   switch stream.first {
   case "nuw":
     stream.removeFirst()
diff --git a/Sources/FrontEnd/Parse/Parser.swift b/Sources/FrontEnd/Parse/Parser.swift
index 94a833b85..9c975999d 100644
--- a/Sources/FrontEnd/Parse/Parser.swift
+++ b/Sources/FrontEnd/Parse/Parser.swift
@@ -2986,9 +2986,74 @@ public enum Parser {
 
   static let compilerConditionStmt = Apply(parseCompilerConditionStmt(in:))
 
-  private static func parseCompilerConditionStmt(in state: inout ParserState) throws -> AnyStmtID? {
-    let head = state.take(.poundIf)
-    return head != nil ? try parseCompilerConditionTail(head: head!, in: &state) : nil
+  private static func parseCompilerConditionStmt(
+    in state: inout ParserState
+  ) throws -> AnyStmtID? {
+    try state.take(.poundIf).map { (head) in
+      try parseCompilerConditionTail(head: head, in: &state)
+    }
+  }
+
+  /// Parses a logical connective from `state`.
+  private static func parseConnective(in state: inout ParserState) -> Connective? {
+    // Next token must be an operator.
+    guard let t = state.peek(), t.kind == .oper else { return nil }
+
+    // The value of the token must be either `||` or `&&`.
+    var r: Connective
+    switch t.site.text {
+    case "||":
+      r = .disjunction
+    case "&&":
+      r = .conjunction
+    default:
+      return nil
+    }
+
+    // Consume the token and "succeed".
+    _ = state.take()
+    return r
+  }
+
+  /// Parses a `Condition` for a `ConditionalCompilationStmt`.
+  private static func parseCondition(
+    in state: inout ParserState
+  ) throws -> ConditionalCompilationStmt.Condition {
+    return try condition(withInfixConnectiveStrongerOrEqualTo: .disjunction, in: &state)
+
+    /// Parses a condition as a proposition, a negation, or an infix sentence whose operator has
+    /// a precedence at least as strong as `p`.
+    func condition(
+      withInfixConnectiveStrongerOrEqualTo p: Connective,
+      in state: inout ParserState
+    ) throws -> ConditionalCompilationStmt.Condition {
+      var lhs = try parseCompilerCondition(in: &state)
+
+      while true {
+        // Tentatively parse a connective.
+        let backup = state.backup()
+        guard let c = parseConnective(in: &state) else { return lhs }
+
+        // Backtrack if the connective we got hasn't strong enough precedence.
+        if (c.rawValue < p.rawValue) {
+          state.restore(from: backup)
+          return lhs
+        }
+
+        // If we parsed `||` the RHS must be a conjunction. Otherwise it must be a proposition or
+        // negation. In either case we'll come back here to parse the remainder of the expression.
+        switch c {
+        case .disjunction:
+          let rhs = try condition(withInfixConnectiveStrongerOrEqualTo: .conjunction, in: &state)
+          lhs = .or(lhs, rhs)
+        case .conjunction:
+          let rhs = try parseCompilerCondition(in: &state)
+          lhs = .and(lhs, rhs)
+        }
+      }
+
+      return lhs
+    }
   }
 
   /// Parses a compiler condition structure, after the initial token (#if or #elseif).
@@ -2996,7 +3061,7 @@ public enum Parser {
     head: Token, in state: inout ParserState
   ) throws -> AnyStmtID {
     // Parse the condition.
-    let condition = try parseCompilerCondition(in: &state)
+    let condition = try parseCondition(in: &state)
 
     // Parse the body of the compiler condition.
     let stmts: [AnyStmtID]
@@ -3007,28 +3072,28 @@ public enum Parser {
       stmts = try parseConditionalCompilationBranch(in: &state)
     }
 
-    // The next token may be #endif, #else or #elseif.
+    // Parse the other branch(es) of the condition.
     let fallback: [AnyStmtID]
-    if state.take(.poundEndif) != nil {
+
+    switch state.peek()?.kind {
+    case .poundEndif:
       fallback = []
-    } else if state.take(.poundElse) != nil {
-      if condition.mayNotNeedParsing && condition.holds(for: state.ast.compilationConditions) {
-        try skipConditionalCompilationBranch(in: &state, stoppingAtElse: false)
-        fallback = []
-      } else {
-        fallback = try parseConditionalCompilationBranch(in: &state)
-      }
-      // Expect #endif.
       _ = try state.expect("'#endif'", using: { $0.take(.poundEndif) })
-    } else if let head2 = state.take(.poundElseif) {
+
+    case .poundElse, .poundElseif:
+      let h = state.take()!
       if condition.mayNotNeedParsing && condition.holds(for: state.ast.compilationConditions) {
         try skipConditionalCompilationBranch(in: &state, stoppingAtElse: false)
         fallback = []
-      } else {
-        // We continue with another conditional compilation statement.
-        fallback = [try parseCompilerConditionTail(head: head2, in: &state)]
+        _ = try state.expect("'#endif'", using: { $0.take(.poundEndif) })
+      } else if h.kind == .poundElse {
+        fallback = try parseConditionalCompilationBranch(in: &state)
+        _ = try state.expect("'#endif'", using: { $0.take(.poundEndif) })
+      } else{
+        fallback = [try parseCompilerConditionTail(head: h, in: &state)]
       }
-    } else {
+
+    default:
       try fail(.error(expected: "statement, #endif, #else or #elseif", at: state.currentLocation))
     }
 
@@ -3488,6 +3553,17 @@ struct ParameterInterface {
 
 }
 
+/// A conjunction (`&&`) or disjunction (`||`) operator.
+enum Connective: Int {
+
+  /// The logical disjunction.
+  case disjunction
+
+  /// The logical conjunction.
+  case conjunction
+
+}
+
 /// A combinator that parses tokens with a specific kind.
 struct TakeKind: Combinator {
 


Test patch:
diff --git a/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift b/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift
index b7149faa1..3f2ec4f61 100644
--- a/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift
+++ b/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift
@@ -192,7 +192,7 @@ fileprivate struct TestArgument: CustomStringConvertible {
 }
 
 /// Information necessary to generate a test case.
-fileprivate struct TestDescription {
+private struct TestDescription {
 
   /// The name of the method implementing the logic of the test runner.
   let methodName: TestMethod
diff --git a/Tests/EndToEndTests/TestCases/ConditionalCompilation.hylo b/Tests/EndToEndTests/TestCases/ConditionalCompilation.hylo
index e3d6060a0..aa9f7839a 100644
--- a/Tests/EndToEndTests/TestCases/ConditionalCompilation.hylo
+++ b/Tests/EndToEndTests/TestCases/ConditionalCompilation.hylo
@@ -169,6 +169,30 @@ fun testNot() {
   use(b)
 }
 
+fun testMultipleOperandConditions() {
+  var n = 0
+
+  #if compiler_version(>= 0.0)
+    &n += 1
+  #else if true
+    <something that would not be parsed>
+  #endif
+
+  #if os(macOS)
+    &n += 1
+  #elseif os(macOS) && !true
+    &n += 1
+  #elseif os(macOS) && false || !true || false
+    &n += 1
+  #elseif os(macOS) || compiler_version(< 1.0)
+    &n += 1
+  #else
+    &n += 1
+  #endif
+
+  use(n)
+}
+
 public fun main() {
   testTrue()
   testFalse()
@@ -180,4 +204,5 @@ public fun main() {
   testHyloVersion()
   testSkipParsingNested()
   testNot()
+  testMultipleOperandConditions()
 }
diff --git a/Tests/HyloTests/ParserTests.swift b/Tests/HyloTests/ParserTests.swift
index 907520e38..cf9418a47 100644
--- a/Tests/HyloTests/ParserTests.swift
+++ b/Tests/HyloTests/ParserTests.swift
@@ -1710,10 +1710,12 @@ final class ParserTests: XCTestCase {
     XCTAssertEqual(stmt.condition, .operatingSystem("macOs"))
     XCTAssertEqual(stmt.stmts.count, 1)
     XCTAssertEqual(stmt.fallback.count, 1)
+    
     let stmt2 = try XCTUnwrap(ast[stmt.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt2.condition, .operatingSystem("Linux"))
     XCTAssertEqual(stmt2.stmts.count, 1)
     XCTAssertEqual(stmt2.fallback.count, 1)
+    
     let stmt3 = try XCTUnwrap(ast[stmt2.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt3.condition, .operatingSystem("Windows"))
     XCTAssertEqual(stmt3.stmts.count, 1)
@@ -1728,14 +1730,17 @@ final class ParserTests: XCTestCase {
     XCTAssertEqual(stmt.condition, .architecture("x86_64"))
     XCTAssertEqual(stmt.stmts.count, 1)
     XCTAssertEqual(stmt.fallback.count, 1)
+    
     let stmt2 = try XCTUnwrap(ast[stmt.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt2.condition, .architecture("i386"))
     XCTAssertEqual(stmt2.stmts.count, 1)
     XCTAssertEqual(stmt2.fallback.count, 1)
+    
     let stmt3 = try XCTUnwrap(ast[stmt2.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt3.condition, .architecture("arm64"))
     XCTAssertEqual(stmt3.stmts.count, 1)
     XCTAssertEqual(stmt3.fallback.count, 1)
+    
     let stmt4 = try XCTUnwrap(ast[stmt3.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt4.condition, .architecture("arm"))
     XCTAssertEqual(stmt4.stmts.count, 1)
@@ -1851,6 +1856,7 @@ final class ParserTests: XCTestCase {
       .hyloVersion(comparison: .less(SemanticVersion(major: 0, minor: 1, patch: 0))))
     XCTAssertEqual(stmt.stmts.count, 0)  // Body not parsed
     XCTAssertEqual(stmt.fallback.count, 1)
+    
     let stmt2 = try XCTUnwrap(ast[stmt.fallback[0]] as? ConditionalCompilationStmt)
     XCTAssertEqual(stmt2.condition, .operatingSystem("bla"))
     XCTAssertEqual(stmt2.stmts.count, 0)
@@ -1890,6 +1896,7 @@ final class ParserTests: XCTestCase {
       // all good
     }
   }
+
   func testConditionalControlNotOperatorOnFalse() throws {
     let input: SourceFile = "#if !os(abracadabra) foo() #endif"
     let (stmtID, ast) = try apply(Parser.stmt, on: input)
@@ -1905,6 +1912,7 @@ final class ParserTests: XCTestCase {
     // We should expand to nothing.
     XCTAssertEqual(stmt.expansion(for: ConditionalCompilationFactors()).count, 0)
   }
+
   func testConditionalControlNotNot() throws {
     let input: SourceFile = "#if ! !true foo() #endif"
     let (stmtID, ast) = try apply(Parser.stmt, on: input)
@@ -1912,6 +1920,7 @@ final class ParserTests: XCTestCase {
     // We should expand to the body.
     XCTAssertEqual(stmt.expansion(for: ConditionalCompilationFactors()).count, 1)
   }
+
   func testConditionalControlSkipParsingAfterNot() throws {
     let input: SourceFile = "#if !compiler_version(< 0.1) foo() #else <won't parse> #endif"
     let (stmtID, ast) = try apply(Parser.stmt, on: input)
@@ -1920,6 +1929,26 @@ final class ParserTests: XCTestCase {
     XCTAssertEqual(stmt.fallback.count, 0)  // don't parse the #else part
   }
 
+  func testConditionalControlInfix() throws {
+    let input = SourceFile.diagnosableLiteral(
+      """
+      #if os(macOS) || os(Linux) && hylo_version(< 1.0.0) || os(Windows)
+      do_something()
+      #endif
+      """)
+    let (stmtID, ast) = try apply(Parser.stmt, on: input)
+    let stmt = try XCTUnwrap(ast[stmtID] as? ConditionalCompilationStmt)
+    XCTAssertEqual(
+      stmt.condition,
+      .or(
+        .or(
+          .operatingSystem("macOS"),
+          .and(
+            .operatingSystem("Linux"),
+            .hyloVersion(comparison: .less(SemanticVersion(major: 1, minor: 0, patch: 0))))),
+        .operatingSystem("Windows")))
+  }
+
   // MARK: Operators
 
   func testTakeOperator() throws {