A subscript's shape is pinned against the receiver leak in the dataflow analysis, but a generator that reads the subscript result as an argument queries the points-to set instead and still sees the receiver's allocation. The two disagree about the same value.
Witness
With #824's slice fix in place:
def propagate_direct(node_embeddings, adjacency):
edge_sources = adjacency[:, 0]
consume_direct_indices(edge_sources) # int32 (30,) correct
edge_source_states = tf.gather(node_embeddings, edge_sources)
consume_direct(edge_source_states) # float32 (30, 2, 16)
edge_sources reports (30,) at the sink, which is right. One instruction later tf.gather composes ids.shape + params.shape[1:] and produces (30, 2, 16), which is (30, 2) + (16,). So the gather read the receiver's rank-2 shape for the same value the sink reports as rank 1.
Reading
PythonTensorAnalysisEngine routes subscript results through setCalls, so SetShapeOp replaces predecessor types rather than unioning them and the receiver's pre-subscript shape cannot leak in through the assignment graph. That pin governs TensorTypeAnalysis state, which is what the sink reads.
A generator resolving an argument does not read that state. It calls getShapes on the argument's value, which walks the points-to set, and the points-to set still aliases the subscript result with the receiver's allocation — that aliasing is exactly what the pin exists to neutralize, and it is untouched.
So the pin is applied at one of the two places that consume a subscript result. Any generator that reads a sliced value as an argument sees the unpinned shape.
Scope
This is not specific to tf.gather; it is a property of the pin, so every generator that resolves an argument through the points-to set is affected whenever that argument is a subscript result. tf.gather merely makes it visible, because it composes the argument's rank into its own result rather than only reading extents.
A subscript's shape is pinned against the receiver leak in the dataflow analysis, but a generator that reads the subscript result as an argument queries the points-to set instead and still sees the receiver's allocation. The two disagree about the same value.
Witness
With
#824's slice fix in place:edge_sourcesreports(30,)at the sink, which is right. One instruction latertf.gathercomposesids.shape + params.shape[1:]and produces(30, 2, 16), which is(30, 2) + (16,). So the gather read the receiver's rank-2 shape for the same value the sink reports as rank 1.Reading
PythonTensorAnalysisEngineroutes subscript results throughsetCalls, soSetShapeOpreplaces predecessor types rather than unioning them and the receiver's pre-subscript shape cannot leak in through the assignment graph. That pin governsTensorTypeAnalysisstate, which is what the sink reads.A generator resolving an argument does not read that state. It calls
getShapeson the argument's value, which walks the points-to set, and the points-to set still aliases the subscript result with the receiver's allocation — that aliasing is exactly what the pin exists to neutralize, and it is untouched.So the pin is applied at one of the two places that consume a subscript result. Any generator that reads a sliced value as an argument sees the unpinned shape.
Scope
This is not specific to
tf.gather; it is a property of the pin, so every generator that resolves an argument through the points-to set is affected whenever that argument is a subscript result.tf.gathermerely makes it visible, because it composes the argument's rank into its own result rather than only reading extents.