Confusing error message: "'subscript(_:)' requires the types 'String.Index' and 'Int' be equivalent"

Hi there. I was writing a function that prints a string using ncurses, truncating to avoid line breaking:

static func write(_ s: String, to loc: Point) {
    assert(Console.inBounds(loc))
    let maxCount = width - loc.x
    if (s.count < maxCount) {
        mvaddstr(Int32(loc.y), Int32(loc.x), s)
    } else {
        let start = s.startIndex
        let end   = s.index(start, offsetBy: maxCount)
        mvaddstr(Int32(loc.y), Int32(loc.x), s[start..<end])
    }
}

That last mvaddstr's last argument is wrong -- it should be String(s[start..<end]). The error that swiftc generated had me scratching my head, however:

src/Console.swift:38:51: error: subscript 'subscript(_:)' requires the types 'String.Index' and 'Int' be equivalent
...
Swift.String.subscript:3:10: note: where 'R.Bound' = 'String.Index'

I was wondering if anyone could help me understand what the compiler is trying to do? As far as I understand it, the last parameter will be something akin to UnsafePointer<Int8>!, and String has an implicit conversion an UnsafePointer to make C calls like this more ergonomic. I guess due to the fact that mvaddstr doesn't just take a String, the compiler has to try a bunch of weird stuff?

1 Like

I think that's essentially it. There's special treatment for Strings but not Substrings, hence compiler is confused and tries various paths before failing with not so obvious error. If you pass Substring(s[start..<end]) the error is totally sane:

Cannot convert value of type 'Substring' to expected argument type 'UnsafePointer<CChar>' (aka 'UnsafePointer<Int8>')

I don’t know about the error message, but I want to point out that calling count on a string is an O(n) operation that walks the entire length of the string. (Well, I’m pretty sure it does. Can someone better versed in the implementation confirm or refute this?)

To avoid that, you can do something like:

let end = s.index(s.startIndex, offsetBy: maxCount, limitedBy: s.endIndex)

let stringToWrite = if (end == s.endIndex) { s } else {
  String(s[..<end])
}

mvaddstr(Int32(loc.y), Int32(loc.x), stringToWrite)

That way instead of counting all the characters in the string, it only counts at most the number of characters that will fit on the line.

1 Like

Ooh, that's very surprising and good to know if true. My mental model was that the length would be just a stored property and not computed from the string data. And the limitedBy version of index is very useful, thank you.

This seems like it may a type checker bug. The overload it has chosen is:

extension String {
    @available(*, unavailable, message: "cannot subscript String with an integer range, use a String.Index range instead.")
    public subscript(bounds: some RangeExpression<Int>) -> String { ... }
}

But given that overload is always unavailable, it should not have selected it, especially since the parameter type does not match. It seems that having a matching return type is prioritized over having a matching argument type. Here’s the reduced code sample I created. I suspect it would be possible to make a version that does not depend on any standard library types, if someone wants to give that a try and file a bug (or file with this example).

func take(_: UnsafePointer<CChar>) {}
func write(_ s: String) {
    let start = s.startIndex
    let end = s.endIndex
    take(s[start..<end])
}
2 Likes