Swift Full Modified

0 views
Skip to first unread message

Jon Levatte

unread,
Aug 4, 2024, 5:55:28 PM8/4/24
to toilingsterin
Hasanyone encountered that issue or knows what to do? I tried toggling the checkbox "Based on dependency analysis", but that didn't help. I didn't have that issue when using Xcode 12. Interesting to note is the duplicate error message despite having only one build phase generating that particular file.

Edit: Here are my build phases:The three marked build phases all generate one such file. All of them fail occasionally. I do not know if that makes a difference, but these are defined for only one target (Notification Service Extension) which is a dependency of my main app target, so it gets triggered only once when I build the app.


To check if this is happening for you, add this to your build script: say $ACTION

On Xcode 12, it only says "build", but in Xcode 13 I hear it saying "indexbuild" during compilation, whenever I save .swift files or otherwise navigate through my code.


EDIT:There is an issue with the original solution. Xcode 12.5 users who try to build the test target will get the error Build input file cannot be found... as the script that generates GeneratedMocks.swift might be run after the build starts.


But I got an official answer from apple that this error occurs as expected because the input file is modified during the build job. As I understood that happens because the build phases can run in parallel. Before Xcode 13 the parallel execution was avoided when the output file definition fit to the input file of the next phase with the same file. This seems to be broken in Xcode 13.


Nevertheless the answer contained a hint about how to solve the problem for not Cuckoo setups. It recommends to use build rules, which are executed during the compile build phase. Here the link with a time marker for this topic: =379


The build rule can be defined as matching the source file by naming convention and then run the script correctly. The output files from the build rules are automatically set to the compile build phase. So this should not be defined there.


What I think might have done the trick was making the build phase run script input and output files definition homogeneous. Previously, we set the inputs via .xcfilelist and outputs via the Xcode UI. Now we use a .xcfilelist for both.


The bodies of modify implementations will be coroutines, and they will introduce a new contextual keyword, yield, that will be used to yield a value to be modified back to the caller. Control will resume after the yield when the caller returns.


Swift's get/set syntax allows users to expose computed properties and subscripts that behave as l-values. This powerful feature allows for the creation of succinct idiomatic APIs, such as this use of Dictionary's defaulting subscript:


However, while this provides the illusion of "in-place" mutation, this is actually implemented as three separate operations: a get of a copy of the value, the mutation on that returned value, and finally a set replacing the original value with the mutated copy.


This simulation of in-place mutation works well for user ergnomics, but has a major performance shortcoming. This can be seen in even our simple GetSet type above. Strings in Swift are "non-trivial" types. Once they grow beyond a small fixed size, they allocate a reference-counted buffer to hold their contents. Mutation is handled via the usual copy-on-write technique: when you make a copy of a string, only the reference to the buffer is copied, not the buffer itself. Then, when the string is mutated, it checks if the buffer is uniquely referenced. If it isn't (because the string has been copied), it first makes a full copy of the buffer before then mutating the buffer, preserving the value semantics of string while avoiding unnecessary eager copies.


So, despite looking like in-place mutation, every mutating operation on x made through property is actually causing a full copy of x's backing buffer. This is a linear operation. If we were doing something like appending to this property in a loop, this loop would end up being quadratic in complexity. This is likely very surprising to the user and can be a major performance pitfall.


it is also possible to supply both a modify and a set. The set will be called in the case of straight assignment, which may be more efficient than first fetching/creating a value to then be overwritten:


Because the fetch and update code are all contained in one block, the isEmpty check is not duplicated (unlike with a get/set pair). Instead, the state of whether the array was empty or not is captured by the program location in the coroutine when the element is yielded. Notice that there are two yields in this modify implementation, for the empty and non-empty branches.


The rules for accessor yields are similar to that of deferred initialization of let variables: it must be possible for the compiler to guarantee there is exactly one yield on every path. The call must not contain any path with either zero or more than one yield. This is the case here, as there is a yield in both the if and the else. More complex cases where the compiler cannot guarantee this will need refactoring, or use of fatalError() to assert unreachable code paths.


The optional return value of first in the code above means that, even with a modify, we have introduced the problem of triggering copy-on-write when mutating via our first property. We cannot yield the value in the array's buffer directly because it needs to be placed inside an optional. That act of placing inside the optional creates a copy.


We can work around this with some lower-level unsafe code. If the implementation of Array.first has access to its underlying buffer, it can move that value directly into the optional, yield it, and then move it back:


During the yield to the caller, the array is in an invalid state: the memory location where the first element is stored is left uninitialized, and must not be accessed. This is safe due to Swift's rules preventing conflicting access to memory. For the full duration of the coroutine, the call to modify has exclusive access to the array. Unlike a get, the modify is guaranteed to have an opportunity to put the element back (or to remove the invalid memory if the entry is set to nil) after the caller returns from the yield, restoring the array to a valid state in all circumstances before any other code can access it.


When throwingMutatingOp throws, control returns back to the outer caller. The body of Array.first modify terminates, and the code after the yield does not execute. This would result in the yielded element being discarded, and the memory location for it in the array being left in an uninitialized state (leaving the array corrupted).


The body of the modify does not get to participate in the error handling. This is not like nested function calls, where an inner call could catch and potentially supress or alter an error. The modify has no knowledge to what operation it is yielding and whether it could throw, any more than a get/set pair would.


Note that our efficient implementation now handles errors slightly differently to the first version that made a copy. That first version was safe in the face of exceptions, but discarded the update if an error was thrown (because the code to unwrap the optional and replace the value never ran). Whereas in this version, the yielded value is always replaced with whatever value is in the optional when the error was thrown. This means that whether the value in the array is updated depends on whether the caller threw the error before or after updating the value.


The need to place mandatory cleanup inside a defer block is definitely a sharp edge. It would be easy to overlook this and assume that the cleanup code can just be placed after the yield. An alternative design could be to always run subsequent code, even when the caller throws. There are a number of reasons why the defer approach is preferable.


Similar to how any method on a property yielded by modify can throw, anything in the body of the for loop could throw, and be caught outside the loop. In addition, the user may just break out of the loop. In either case, the generateMutably function should immediately terminate, not run to completion. A simplified model where the straight line code just continues is not viable. More fine-grained control of cleanup is also likely needed. The implementation of generateMutably might need cleanup either for each element yielded, or after iteration is finished, or both. Using defer for cleanup provides full control of this.


With both accessors and generators, it might also be desirable to drive different behavior on early vs regular termination. Early termination can be handled in a defer block, while straight-line code can be used for when the implementation was allowed to run to completion.


Given that a yield may be to a throwing caller, there is an argument that the keyword should be try yield to indicate this and remind the user. However, this is likely to just cause noise, and annoy users in the majority of use cases where this does not matter. This use of try would also be inconsistent with the rest of the language: it would not need to be wrapped in a do...catch block nor would try? or try! make sense.


The majority of modify accessors are expected to be simple, directly yielding some storage. Most times mandatory cleanup will be needed will be when dealing with unsafe constucts as seen in our first implementation. As always when using unsafe operations, the user must have a full understanding of exactly how the language operates in order to manually ensure safety. As such, understanding how errors are handled when yielding is unavoidable.


There is a similar possible enhancement for coroutine-based fetching of read-only values: read instead of get. This will also be necessary for containers of move-only values. For example, it would be necessary to implement even the current read-only version of Array.first, similar to the mutating version described here.

3a8082e126
Reply all
Reply to author
Forward
0 new messages