[Accepted with modifications] SE-0453: InlineArray (formerly: Vector, a fixed-size array)

The type proposed does not allow for uninitialized state. Every element within the InlineArray must be initialized at all times, so you can't do what you proposed here:

4 Likes

It's worth noting that when your element type is POD, as in audio sample data, the distinction doesn't matter much; you can use a fixed-count array to build the data structure you want with minimal extra bookkeeping. The place you really get into trouble is when the objects stored in the container are non-trivial (representing system resources, etc).

7 Likes

Yes you can. Initialize everything to .zero. Audio samples are numbers.

2 Likes

Agreed, this is just one usecase for a real InlineArray where the Element: BitwiseCopyable but there are many many more where the element type is not.

If I may, your concern doesn’t strike me as a naming objection, but rather a more fundamental design issue. The problem doesn’t go away if we call this type InlineBuffer or Frobble. It has invariants which make it not directly usable for your use case. However, it is pretty easy to adapt to your use case by filling it with .zero and keeping track of the Range that is ā€œvalidā€ as far as your application is concerned.

3 Likes

It's important to keep in mind that this is not viable when the element type is non-POD.

1 Like

Yes, that’s why I say this is an objection to the fundamental design, not to the naming.

It's not an objection to the fundamental design. Rauhul wants to have both types. He's advancing the concern that squatting on the name that's (in his opinion) most appropriate for the other type may be a problem in the future.

16 Likes

The problem with wanting an Inline type that has uninitialized values is that Swift very much does not want values to have uninitialized members. The whole Inline nomenclature means the elements are treated as members of the value itself. Swift does not currently support the notion of a value type with uninitialized non-padding memory.

2 Likes

Thank you, this is exactly what I intended to convey.

At the moment yes, but it's not unreasonable to say that a future version of Swift allows these kinds of constructs with specialized copy/move/destroy mechanics that don't attempt to work on the uninitialized parts.

1 Like

That isn’t the language we have today, and that would be a substantial change and discussion. What @rauhul wants is simply not possible to express in Swift.

It also matters if you do copy the buffer - if only part of its contents are meaningful, you don't need to copy the entire capacity as though it were all meaningful.

As for custom copy/move constructors, IIUC we can import C++ types which have those, and they are a part of value witness tables (including generics and existentials) already. You can't write them in Swift yet, but a lot of the machinery seems to be in place.

1 Like

I don't actually think that's true. While it's not possible to write in user code, the standard library already seems to have developed the necessary machinery for the Synchronization model.

import Builtin

@frozen
@usableFromInline
@_rawLayout(like: Value, movesAsLike)
public struct MaybeUninit<Value: ~Copyable>: ~Copyable {
  @_alwaysEmitIntoClient
  @_transparent
  internal var _address: UnsafeMutablePointer<Value> {
    UnsafeMutablePointer<Value>(_rawAddress)
  }

  @available(SwiftStdlib 6.0, *)
  @_alwaysEmitIntoClient
  @_transparent
  internal var _rawAddress: Builtin.RawPointer {
    Builtin.addressOfRawLayout(self)
  }

  @_alwaysEmitIntoClient
  @_transparent
  @unsafe
  public mutating func initialize(to initialValue: consuming Value) {
    _address.initialize(to: initialValue)
  }

  @_alwaysEmitIntoClient
  @_transparent
  public init() { }

  @_alwaysEmitIntoClient
  @_transparent
  @unsafe
  public mutating func take() -> Value {
    _address.move()
  }

  @_alwaysEmitIntoClient
  @_transparent
  @unsafe
  public var value: Value {
    _read {
      yield _address.pointee
    }
    _modify {
     yield &_address.pointee
   }
  }
}

// This might already be implied by movesAsLike
extension MaybeUninit: Copyable where Value: Copyable { }
extension MaybeUninit: BitwiseCopyable where Value: BitwiseCopyable {}

Then you can implement a fixed capacity array as a count followed by an InlineArray<Capacity, MaybeUninit<Element>>.

Although, it's possible that the empty init doesn't compile. I haven't tested it yet. If it doesn't, I think adding a way to explicitly set a @_rawLayout type to an uninitialized state is a perfectly reasonable extension for an underscored attribute meant for writing abstractions over very low level code inside the standard library.

EDIT: There is one other problem, the FixedCapacityArray would need a deinit, and I don't know how to do that and make the type conditionally Copyable. It'd probably need a clone method.

2 Likes

It is expressible in Swift (at a fixed capacity) with an enum type with cases holding different numbers of associated values. I’m not sure how well it would optimize, but it is definitely not impossible to express.

2 Likes

Enums are tagged, so you'd either need a free bit inside the value type to distinguish between uninitialized and the payload, or live with an extra byte for the discriminant. Either way, you've just exactly re-implemented Optional.

He got the point! Was this consideration missed during design phase? :thinking:

If the base type is fixed capacity we can add a new type on top of it that adds size, but we will still copy the whole thing on assignments instead of copying potentially much smaller part. Choosing the base type with "fixed capacity and the size up to capacity" seems more optimal in that regard.

As for the name - it is perfect IMHO.

This doesn't actually work because movesAsLike requires an initialized state at all times. You could remove it and it would "work", but it limits the types you can put in MaybeUninit to bitwise borrowable types.

1 Like

In that case, we could initialize the _rawAddress to an empty tuple in the init() and when we take() from it, and rebind it to Value when we initialize it. Which is basically what Rust does, except they have this as part of the type system instead of an attribute.

No, this still doesn't work. The move constructor in the value witness table that the compiler creates for you implicitly assumes the state is initialized and calls the underlying Value move constructor when moving the raw layout type, hence movesAsLike. A valid instance of Value must be present at all times with movesAsLike.

2 Likes