Skip to content

Commit ea8ab58

Browse files
committed
Extend Accessors doc with tentative getForMutation design.
The intent here is to expose an internal mechanism to the standard library in order to allow it to take advantage of the runtime's "pinning" mechanism to enable efficient divide-and-conquer mutation using slices.
1 parent ddf916e commit ea8ab58

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

docs/proposals/Accessors.rst

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,139 @@ Notably, it breaks swapping two array elements::
549549
release(newArrayBuffer_j)
550550
release(newArrayBuffer_i)
551551

552+
get- and setForMutation
553+
~~~~~~~~~~~~~~~~~~~~~~~
554+
555+
Some collections need finer-grained control over the entire mutation
556+
process. For instance, to support divide-and-conquer algorithms using
557+
slices, sliceable collections must "pin" and "unpin" their buffers
558+
while a slice is being mutation to grant permission for the slice
559+
to mutate the collection in-place while sharing ownership. This
560+
flexibility can be exposed by a pair of accessors that are called
561+
before and after a mutation. The "get" stage produces both the
562+
value to mutate, and a state value (whose type must be declared) to
563+
forward to the "set" stage. A pinning accessor can then look something
564+
like this::
565+
566+
extension Array {
567+
subscript(range: Range<Int>) -> Slice<Element> {
568+
// `getForMutation` must declare its return value, a pair of both
569+
// the value to mutate and a state value that is passed to
570+
// `setForMutation`.
571+
getForMutation() -> (Slice<Element>, PinToken) {
572+
let slice = _makeSlice(range)
573+
let pinToken = _pin()
574+
return (slice, pinToken)
575+
}
576+
577+
// `setForMutation` receives two arguments--the result of the
578+
// mutation to write back, and the state value returned by
579+
// `getForMutation`.
580+
setForMutation(slice, pinToken) {
581+
_unpin(pinToken)
582+
_writeSlice(slice, backToRange: range)
583+
}
584+
}
585+
}
586+
587+
``getForMutation`` and ``setForMutation`` must appear as a pair;
588+
neither one is valid on its own.
589+
When the compiler has visibility that storage is implemented in
590+
terms of ``getForMutation`` and ``setForMutation``, it lowers a mutable
591+
projection using those accessors as follows::
592+
593+
// A mutation like this (assume `reverse` is a mutating method):
594+
array[0...99].reverse()
595+
// Decomposes to:
596+
let index = 0...99
597+
(var slice, let state) = array.`subscript.getForMutation`(index)
598+
slice.reverse()
599+
array.`subscript.setForMutation`(index, slice, state)
600+
601+
To support the conservative access pattern,
602+
a `materializeForSet` accessor can be generated from `getForMutation`
603+
and `setForMutation` in an obvious fashion: perform `getForMutation`
604+
and store the state result in its scratch space, and return a
605+
callback that loads the state and hands it off to `setForMutation`.
606+
607+
The beacon of hope for a user-friendly future: Inversion of control
608+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
609+
610+
Addressors and ``{get,set}ForMutation`` expose important optimizations
611+
to the standard library, but are undeniably fiddly and unsafe constructs
612+
to expose to users. A more natural model would be to
613+
recognize that a compound mutation is a composition of nested scopes, and
614+
express it in the language that way. A strawman model might look something
615+
like this::
616+
617+
var foo: T {
618+
get { return getValue() }
619+
set { setValue(newValue) }
620+
621+
// Perform a full in-out mutation. The `next` continuation is of
622+
// type `(inout T) -> ()` and must be called exactly once
623+
// with the value to hand off to the nested mutation operation.
624+
mutate(next) {
625+
var value = getValue()
626+
next(&value)
627+
setValue(value)
628+
}
629+
}
630+
631+
This presents a natural model for expressing the lifetime extension concerns
632+
of addressors, and the state maintenance necessary for pinning ``getForMutation``
633+
accessors::
634+
635+
// An addressing mutator
636+
mutate(next) {
637+
withUnsafePointer(&resource) {
638+
next(&$0.memory)
639+
}
640+
}
641+
642+
// A pinning mutator
643+
mutate(next) {
644+
var slice = makeSlice()
645+
let token = pin()
646+
next(&slice)
647+
unpin(token)
648+
writeBackSlice(slice)
649+
}
650+
651+
For various semantic and implementation efficiency reasons, we don't want to
652+
literally implement every access as a nesting of closures like this. Doing so
653+
would allow for semantic surprises (a mutate() operation never invoking its
654+
continuation, or doing so multiple times would be disastrous), and would
655+
interfere with the ability for `inout` and `mutating` functions to throw or
656+
otherwise nonlocally exit. However, we could present this model using
657+
*inversion of control*, similar to Python generators or async-await.
658+
A `mutate` operation could `yield` the `inout` reference to its inner value,
659+
and the compiler could enforce that a `yield` occurs exactly once on every
660+
control flow path::
661+
662+
// An addressing, yielding mutator
663+
mutate {
664+
withUnsafePointer(&resource) {
665+
yield &$0.memory
666+
}
667+
}
668+
669+
// A pinning mutator
670+
mutate {
671+
var slice = makeSlice()
672+
let token = pin()
673+
yield &slice
674+
unpin(token)
675+
writeBackSlice(slice)
676+
}
677+
678+
This obviously requires more implementation infrastructure than we currently
679+
have, and raises language and library design issues (in particular,
680+
lifetime-extending combinators like ``withUnsafePointer`` would need either
681+
a ``reyields`` kind of decoration, or to become macros), but represents a
682+
promising path toward exposing the full power of the accessor model to
683+
users in an elegant way.
684+
552685
Acceptability
553686
-------------
554687

0 commit comments

Comments
 (0)