Technique for pinning type inference of closure parameter without reformatting entire code block?

scenario: you have a closure-based DSL that has some very complex content written against the $0 shorthand:

$0[.section]
{
    $0[.h2] = "Blah"
    $0[.ol]
    {
        for item:Item in self.items
        {
            $0[.li] = item
        }
    }

    ...
}

you have made a small edit somewhere that causes type checking to break down completely, and for some reason, $0 is being inferred to the completely wrong type, and is therefore giving completely irrelevant diagnostics.

you know you could get a better compiler diagnostic if you “pinned” the $0 parameter to some more-explicit type (e.g. HTML.ContentEncoder). but then you have to reformat the entire code block to use named parameters instead of anonymous parameters.

does anyone know of a better technique to force the compiler to emit diagnostics assuming a particular type for $0?

simply coercing the parameter inside the closure does not work:

let _:HTML.ContentEncoder = $0
cannot convert value of type 'HTML.AttributeEncoder' to specified type 'HTML.ContentEncoder'sourcekitd
1 Like

Would it work to as-coerce the closure literal, like { ... $0 ... } as (TypeOfDollarZero) -> ReturnType ?

1 Like

it sometimes does, but i find you have to apply it to every closure in the nested stack, and it still involves a lot of reformatting since it is incompatible with trailing closure syntax. sometimes it never really helps.

a real example, from the code that renders the swiftinit.org home page:

func body(_ body:inout HTML.ContentEncoder, format:Swiftinit.RenderFormat)
{
    body[.section, { $0.class = "docs" }]
    {
        $0[.h2] = "Recent Docs"
        $0[.ol]
        {
            for item:Unidoc.DB.DocsFeed.Activity<Unidoc.VolumeMetadata> in
                self.docs
            {
                $0[.li]
                {
                    let dynamicAge:Duration.DynamicFormat = .init(seconds: 0)

                    $0[.p, { $0.class = "edition"}]
// value of type 'HTML.Attribute.Factory' has no subscripts
                    {
                        $0[.span] = "\(item.volume.symbol.package)"
// cannot infer contextual base in reference to member 'span'
                        $0[.a]
                        {
                            $0.href = "\(Swiftinit.Docs[item.volume])"
                        } = item.volume.symbol.version
                    }

                    $0[.p] { $0.class = "age" } = dynamicAge.units != .seconds
                        ? "\(dynamicAge.short) ago"
                        : "just now"
                }
            }
        }
    }
body[.section, { $0.class = "docs" }]
{
    $0[.h2] = "Recent Docs"
    $0[.ol]
    {
        for item:Unidoc.DB.DocsFeed.Activity<Unidoc.VolumeMetadata> in
            self.docs
        {
            $0[.li]
            {
                let dynamicAge:Duration.DynamicFormat = .init(seconds: 0)

+               $0[.p, { $0.class = "edition"},
+ // value of type 'HTML.Attribute.Factory' has no subscripts
+ // cannot infer contextual base in reference to member 'p'
                {
                    $0[.span] = "\(item.volume.symbol.package)"
                    $0[.a]
                    {
                        $0.href = "\(Swiftinit.Docs[item.volume])"
                    } = item.volume.symbol.version
+               } as (inout HTML.ContentEncoder) -> ()]

                $0[.p] { $0.class = "age" } = dynamicAge.units != .seconds
                    ? "\(dynamicAge.short) ago"
                    : "just now"
            }
        }
    }
}
body[.section, { $0.class = "docs" }]
+ // generic parameter 'Renderable' could not be inferred
{
    $0[.h2] = "Recent Docs"
+ // cannot infer contextual base in reference to member 'h2'
    $0[.ol]
    {
        for item:Unidoc.DB.DocsFeed.Activity<Unidoc.VolumeMetadata> in
            self.docs
        {
+           $0[.li,
            {
                let dynamicAge:Duration.DynamicFormat = .init(seconds: 0)

                $0[.p, { $0.class = "edition"},
                {
                    $0[.span] = "\(item.volume.symbol.package)"
                    $0[.a]
                    {
                        $0.href = "\(Swiftinit.Docs[item.volume])"
                    } = item.volume.symbol.version
                } as (inout HTML.ContentEncoder) -> ()]

                $0[.p] { $0.class = "age" } = dynamicAge.units != .seconds
                    ? "\(dynamicAge.short) ago"
                    : "just now"
+           }  as (inout HTML.ContentEncoder) -> ()]
        }
    }
}
+ body[.section, { $0.class = "docs" },
+ type 'SVG.Embedded' has no member 'section'
{
    $0[.h2] = "Recent Docs"
    $0[.ol]
    {
        for item:Unidoc.DB.DocsFeed.Activity<Unidoc.VolumeMetadata> in
            self.docs
        {
            $0[.li,
            {
                let dynamicAge:Duration.DynamicFormat = .init(seconds: 0)

                $0[.p, { $0.class = "edition"},
                {
                    $0[.span] = "\(item.volume.symbol.package)"
                    $0[.a]
                    {
                        $0.href = "\(Swiftinit.Docs[item.volume])"
                    } = item.volume.symbol.version
                } as (inout HTML.ContentEncoder) -> ()]

                $0[.p] { $0.class = "age" } = dynamicAge.units != .seconds
                    ? "\(dynamicAge.short) ago"
                    : "just now"
            }  as (inout HTML.ContentEncoder) -> ()]
        }
    }
+ } as (inout HTML.ContentEncoder) -> ()]
+ // cannot convert value of type '(inout HTML.ContentEncoder) -> ()' to expected argument type '(inout SVG.ContentEncoder) -> ()

what was the actual problem, you wonder?

body[.section, { $0.class = "docs" }]
{
    $0[.h2] = "Recent Docs"
    $0[.ol]
    {
        for item:Unidoc.DB.DocsFeed.Activity<Unidoc.VolumeMetadata> in
            self.docs
        {
            $0[.li]
            {
                let dynamicAge:Duration.DynamicFormat = .init(seconds: 0)

                $0[.p, { $0.class = "edition"}]
                {
                    $0[.span] = "\(item.volume.symbol.package)"
                    $0[.a]
                    {
                        $0.href = "\(Swiftinit.Docs[item.volume])"
                    } = item.volume.symbol.version
                }

-               $0[.p] { $0.class = "age" } = dynamicAge.units != .seconds
+               $0[.p] { $0.class = "age" } = dynamicAge.unit != .seconds
                    ? "\(dynamicAge.short) ago"
                    : "just now"
            }
        }
    }
}

i know this example is very specific to this particular DSL, but in the general case, it would be helpful to give the compiler “hints” about what type checking strategy to use, because it is unhelpfully exploring a lot of irrelevant possibilities here.

in particular, it should have been sufficient to explicitly-type the <li> element, but for some reason that type annotation was not considered authoritative enough, and the compiler kept working its way up the call stack.

could i have avoided this by rigorously adhering to a coding style that always uses named parameters with explicit type annotations? probably. but that would defeat the purpose of having a DSL.

i was able to isolate a small instance of poor diagnostic like the one that was bedeviling me earlier.

enum E
{
    case a, b
}
struct S
{
    var referer:Int? = nil

    init()
    {
    }
}
func f<T>(_ f:() -> T?) -> T?
{
    f()
}
func g(_ e:E) -> Int?
{
    f
    {
        var s:S = .init()
        switch e
        {
        case .a:
            return nil
//  error: 'nil' is not compatible with closure result type 'Int'

        case .b:
            s.referrer = 1
            return s
        }
    }
}

on 5.10, the compiler zeroes in on the return nil statement, but the actual problem was that referer is misspelled elsewhere in the same function.

1 Like