Listing stored properties of a type

something that’s been on my wishlist for a while has been the ability to categorize the stored properties of a type separately from the computed properties. it is currently very difficult to sort through all of the instance properties from the generated documentation to understand what a type’s actual representation is.

i did some investigation into this idea today, and discovered that lib/SymbolGraphGen currently classifies all instance properties as stored properties (vp).

it is also not possible to deduce this from the presence of accessors ({ get }, { get set }, etc.) in the emitted symbol declaration, since those also appear for stored public private(set) properties.

what would it take for lib/SymbolGraphGen to emit this information?

cc @QuietMisdreavus

3 Likes

Note that it is very much by design that Swift-the-language does not distinguish stored and computed properties by default. Changing representations is supposed to be a source-compatible change.

4 Likes

right, but when you have a type with a large population of instance properties, it becomes a problem when “important” properties get lost in a long list of computed properties. this is really just about the readability of the generated docs.

nothing about surfacing “storeness” in generated documentation is going to have any impact on source compatibility. if a stored property becomes a computed property (or vice versa), the only observable change in the documentation is the symbol would move to a different heading on the same page.

5 Likes

Hmmm, I think then it would be better to organize the properties into headings regarding which ones are "important" or not.

There could be important computed properties, and unimportant stored ones. I'm not sure if "is stored or not" would be a useful distinction from a documentation perspective. And that matches the OO philosophy on the matter, that storage is an implementation detail which shouldn't be exposed.

Do you have any examples that illustrate the issue?

right, i think this is an important difference between class-heavy styles of programming and struct-heavy styles.

here’s an small example:

from looking at the type’s source code, you can see it is just a glorified (UInt16, UInt16, UInt16) tuple.

but if you browse its documentation, it actually has five instance properties.

  • var description: String
  • var major: UInt16
  • var minor: UInt16
  • var patch: UInt16
  • var rawValue: Int64

but the description and the rawValue are not integral to the type, they are hooks for protocol conformances. as the type matures, it would likely gain many more such “conformance” or “convenience” properties, and the only reason it doesn’t have more already is because i am consciously trying to avoid this problem by limiting the amount of API this type exposes.

1 Like

Could it be organised in sections?

  • var major: UInt16
  • var minor: UInt16
  • var patch: UInt16

protocol CustomStringConvertible:

  • var description: String

protocol RawRepresentable:

  • var rawValue: Int64
1 Like

Somewhat related, I needed to know the actual layout of various structures earlier this evening - e.g. UnsafeMutableBufferPointer - and I ultimately had to dig up some gyb templates buried deep in the Swift stdlib sources (and then read very carefully to ensure I'd found all the stored properties, since their declarations can appear practically anywhere in the file). If you don't know to specifically go to GitHub and specifically search Apple's "Swift" repo and to fiddle with the language settings to get it to actually surface those template files, you'll never find them. Search engines work really hard to not show GitHub source code results.

If they'd been simply listed in the documentation, even if just in some appendix or footnote, I'd have been much happier.

Note that in this case the order is important, too. I need to know the full layout in order to map that to registers and the like.

2 Likes

Can you use Mirror for that? It doesn't return everything but you could figure quite a few things about structs layout:

let x = UnsafeMutableBufferPointer<Int>(start: nil, count: 10)
let m = Mirror(reflecting: x)
print(m.subjectType)            // UnsafeMutableBufferPointer<Int>
print(m.displayStyle!)          // struct
print(m.children.count)         // 2
for child in m.children {
    print(child.label!)         // _position, count
    print(child.value)          // nil, 10
}
print("offset of _position:", MemoryLayout<UnsafeMutableBufferPointer<Int>>.offset(of: \._position)!)
// 🛑 Error: '_position' is inaccessible due to 'internal' protection level
print("offset of count:", MemoryLayout<UnsafeMutableBufferPointer<Int>>.offset(of: \.count)!) // 8

I think that it would make some sense to document the stored properties for frozen types specifically.

2 Likes

Even frozen types don’t necessarily make all of their stored properties public, so having a “stored properties” section there could still seem like it’s providing more information than it really is.

1 Like

What are exact guarantees about frozen types? Could this change happen or not?

// old version
@frozen public struct S {
    var x: Int
}

// new version
@frozen public struct S {
    private var y: Int
    var x: Int {
        get { y &- 1 }
        set { y = newValue &+ 1 }
    }
}

That change is ABI breaking for a frozen type (but admittedly in a weird and subtle way).

2 Likes

And this one?

// new version
@frozen public struct S {
    private var y: Int
    var x: Int {
        get { y }
        set { y = newValue }
    }
}

Perhaps, but it's a bit of a pain to have to write the boilerplate code for it, versus just looking at some simple documentation.

I did think about doing that, but I figured it's still easier to dig up the source on GitHub. At least, where that's available.

it is a basic tradeoff when going from class inheritance to protocol-oriented patterns that you lose the ability to reverse-map declarations to protocol conformances.

for example, a type might expose a description property that witnesses multiple protocol requirements, or might witness a conformance declared in a downstream module.

1 Like

i came across a somewhat more motivating example that doesn’t have anything to do with protocol conformances in the form of this datetime formatter type i have:

Timestamp.Components

var day: Int32
var DD:String
var hh: String
var hour: Int32
var minute: Int32
var mm: String
var MM:String
var month: Int32
var second: Int32
var ss: String
var year: Int32
var yyyymmdd: String
var yyyymmddThhmmssZ: String

Funny enough, I just ran into a case at work (in Ruby) where I had to optimize a class that represents a SemVer Version, which changed its layout.

I changed it from having 3 seperate fields for major/minor/patch, to a single bit-packed Integer. This allowed us to greatly speed up the < operator, which our app used a lot.

This is a perfect example of where decoupling internal storage from API is useful, regardless if you're doing traditional class-oriented OOP or not. If the docs put an emphasis on the stored properties, then the docs for major/minor/patch would have been de-emphasized, and the docs for this random packedInt would have been emphasized instead.

It would be better if major/minor/public fields were highlighted intentionally+explicitly as members of a section, rather than implicitly because they happened to be stored properties instead of computed.

ha, i had a similar issue with an IP address type, except in my case it was the ==(_:_:) operator.

the thing is i don’t think the “Topics” system is mature enough where we can lean on that as a curation mechanism. my main issue with “Topics” lists is that you need to know the symbol hashes to create them, and the symbol hashes are just dreadful to work with, particularly if you’re not using XCode.

probably stored-ness is not the best heuristic to use here. but i think we still do need one.

Exactly, perhaps improving the DX of topics is the better direction here

yeah but that’s fundamentally an IDE/editor issue and any improvements there would have to come from the VSCode plugin side. it’s not apparent to me what documentation tooling can do here.