Miniscript DSL implementation (how to simplify complex solution)

Hi!

I'm working on a DSL for https://bitcoin.sipa.be/miniscript/ and ran into an issue trying to make expressions conform to different result types depending on their inputs.

To abstract the problem I've come up with a basic playground example.

I have an Or construct that takes 2 boolean expressions and returns another boolean expression. I want to identify when the input boolean expressions are different and mark that as an xor result.

There's another expression I called NeedsXOrResult which requires xor results, even If they come from a regular or.

The problem is I can't make Or conditionally conform to ExpXOr for both when A: ExpTrue and B: ExpFalse and viceversa at the same time. I need to choose one, but then I'd be leaving valid results outside of the scope for NeedsXOrResult which would absolutely break the rules of the DSL.

My solution was to create a second ExpXOr_ protocol and then do a bunch of gymnastics to have NeedsXOrResult take either of the 2 flavors of xor results.

This is the best I could come up with. I wonder if there's a better solution involving associated types or result builders or something.

Anyway thanks for taking a look. Here's the playground source code:

protocol Exp: CustomStringConvertible {}
protocol ExpBool: Exp {}
protocol ExpTrue: ExpBool {}
protocol ExpFalse: ExpBool {}

struct True: ExpTrue {
    let description = "true"
}

struct False: ExpFalse {
    let description = "false"
}

struct Or<A: ExpBool, B: ExpBool>: Exp {

    let a: A
    let b: B

    init(_ a: A, _ b: B) {
        self.a = a
        self.b = b
    }

    var description: String { "\(a) or \(b)" }

}

// Nastiness begins…

protocol ExpXOr: Exp {}
protocol ExpXOr_: Exp {}

extension Or: ExpXOr where A: ExpTrue, B: ExpFalse { }
extension Or: ExpXOr_ where A: ExpFalse, B: ExpTrue { }

extension Never: @retroactive CustomStringConvertible {}
extension Never: ExpXOr, ExpXOr_ { public var description: String { fatalError() } }

struct NeedsXOrResult<E: ExpXOr, E_: ExpXOr_>: Exp {
    // What we would really like is `struct NeedsXOrResult<E: ExpXOr>: Exp { … }`. Just one dependant type.

    init(_ a: E, _ dummy: E_? = Never?.none) {
        precondition(dummy == nil)
        self.a = a
        a_ = .none
    }
    init(_ a_: E_, _ dummy: E? = Never?.none) {
        precondition(dummy == nil)
        a = .none
        self.a_ = a_
    }

    let a: E?
    let a_: E_? // The hack cascades all the way to the implementation of the expression.

    var description: String {
        if let a {
            return "\(a)"
        }
        guard let a_ else { preconditionFailure() }
        return "\(a_)"
    }

}

print(NeedsXOrResult(Or(True(), False()))) // true or false
print(NeedsXOrResult(Or(False(), True()))) // false or true
// NeedsXOrResult(Or(True(), True())) // 👍 "No exact matches in call to initializer" 👍

Quick update. For those interested the complete DSL definition can be found here: swift-bitcoin/src/bitcoin-miniscript/Miniscript.swift at develop · swift-bitcoin/swift-bitcoin · GitHub

Official test vectors passing: swift-bitcoin/test/bitcoin-miniscript/Miniscript.swift at develop · swift-bitcoin/swift-bitcoin · GitHub

Usage: Documentation

While all DSL rules are currently being enforced by the Swift compiler I'm still interested in improving the implementation side with Macros, Result Builders or even just additional protocols with associated types. So any ideas/direct contributions are absolutely welcome.

Cheers!