The summary of tf.shape(x) is a fresh Tensor allocation with no generator, and a fresh allocation the factory refuses at seeding is never seeded, so its result reads ⊥ in the dataflow: this lattice's spelling of "not a tensor". Everything read from it inherits that, and a subscript of it, tf.shape(x)[0], the ordinary way a program obtains a batch or node count as a scalar tensor, reads "not a tensor" at the parameter it is passed to. Measured, with the count that makes it the subject rather than one of a class.
Measured
A probe at the engine's seeding refusal (the IllegalArgumentException the factory throws for a do result it has no generator for), over every analysis in the test module, with an unconditional entry and completion line per analysis so the counts are certified: 1616 analyses entered and 1616 completed, 120803 sources seeded, 310016 refusals, and 113 cases in which the factory returned null rather than throwing (the seeding path stores such a source anyway, and a probe on that path meets the null before it meets the exception). Joined by class against the 180 summary classes in tensorflow.xml whose method allocates a fresh Tensor and returns it: 26 refused classes allocate a fresh tensor. Twenty-three of those are Keras layer classes whose refused invoke is the layer call (__call__/call), unseeded by design and typed by the forward-result dataflow. Of the rest, conv2d (14 refusals) and conv3d (5) have their own engine-side handling. What is left is tensorflow/math/shape, with 3760 refusals, the most refused summary of any kind after user code bodies. One op, reached often.
At a subscript of it, read with a second probe that walks a function's IR value by value and prints whether each value has a dataflow node and whether its state is null, empty or populated (the harness's own dump omits empty states, so it cannot tell ⊥ from "no node"): in the two concrete propagate bodies of a graph neural network library's message-passing layers (a public project, the example here), v22 = tf.shape(node_embeddings) is in the flow graph with an empty state in every context (6 and 10), and so is v21 = v22[0], the nodes_num those bodies pass on. The receiver of the subscript is not ⊤ but ⊥, so the subscript pin's decline for an unranked receiver (#405) never has a member to decline on, and the subscript generator reads an empty receiver. A control in the same program: a num_nodes parameter that is genuinely not a tensor (len(features) at both call sites) reads empty in all 13 of its contexts, correctly.
Why A Generator Rather Than A Summary Change
The summary's own comment leaves the result "at ⊤ (no per-op generator wired up here)", and that is the intent the engine does not carry out: an allocation with no generator is refused, not seeded as ⊤. The result of tf.shape(x) is rank 1 and int32 statically, always. Its single extent is the rank of its input, so when the input's rank is known it is a concrete (r,), and only an unranked input degrades it to (Unresolved,). A Shape generator (input operand at position 0, input) gives that, and with a rank-1 receiver the subscript pin engages by its existing rule and [0] resolves to a rank-0 int32 scalar with no change to the subscript machinery. The registration for that change is wide: tf.shape appears wherever shape arithmetic does, so it wants the full-module value diff and node and edge counts, and the (r,)-when-rank-is-known half is the part most likely to move something unexpected.
What This Does Not Fix
The parameter that prompted the measurement belongs to a function the analysis never enters, because its class inherits it across a module boundary that falls to object (#571, narrowed there). A Shape generator moves nodes_num at the call sites and cannot move that parameter.
The summary of
tf.shape(x)is a freshTensorallocation with no generator, and a fresh allocation the factory refuses at seeding is never seeded, so its result reads ⊥ in the dataflow: this lattice's spelling of "not a tensor". Everything read from it inherits that, and a subscript of it,tf.shape(x)[0], the ordinary way a program obtains a batch or node count as a scalar tensor, reads "not a tensor" at the parameter it is passed to. Measured, with the count that makes it the subject rather than one of a class.Measured
A probe at the engine's seeding refusal (the
IllegalArgumentExceptionthe factory throws for adoresult it has no generator for), over every analysis in the test module, with an unconditional entry and completion line per analysis so the counts are certified: 1616 analyses entered and 1616 completed, 120803 sources seeded, 310016 refusals, and 113 cases in which the factory returned null rather than throwing (the seeding path stores such a source anyway, and a probe on that path meets the null before it meets the exception). Joined by class against the 180 summary classes intensorflow.xmlwhose method allocates a freshTensorand returns it: 26 refused classes allocate a fresh tensor. Twenty-three of those are Keras layer classes whose refused invoke is the layer call (__call__/call), unseeded by design and typed by the forward-result dataflow. Of the rest,conv2d(14 refusals) andconv3d(5) have their own engine-side handling. What is left istensorflow/math/shape, with 3760 refusals, the most refused summary of any kind after user code bodies. One op, reached often.At a subscript of it, read with a second probe that walks a function's IR value by value and prints whether each value has a dataflow node and whether its state is null, empty or populated (the harness's own dump omits empty states, so it cannot tell ⊥ from "no node"): in the two concrete
propagatebodies of a graph neural network library's message-passing layers (a public project, the example here),v22 = tf.shape(node_embeddings)is in the flow graph with an empty state in every context (6 and 10), and so isv21 = v22[0], thenodes_numthose bodies pass on. The receiver of the subscript is not ⊤ but ⊥, so the subscript pin's decline for an unranked receiver (#405) never has a member to decline on, and the subscript generator reads an empty receiver. A control in the same program: anum_nodesparameter that is genuinely not a tensor (len(features)at both call sites) reads empty in all 13 of its contexts, correctly.Why A Generator Rather Than A Summary Change
The summary's own comment leaves the result "at ⊤ (no per-op generator wired up here)", and that is the intent the engine does not carry out: an allocation with no generator is refused, not seeded as ⊤. The result of
tf.shape(x)is rank 1 and int32 statically, always. Its single extent is the rank of its input, so when the input's rank is known it is a concrete(r,), and only an unranked input degrades it to(Unresolved,). AShapegenerator (input operand at position 0,input) gives that, and with a rank-1 receiver the subscript pin engages by its existing rule and[0]resolves to a rank-0 int32 scalar with no change to the subscript machinery. The registration for that change is wide:tf.shapeappears wherever shape arithmetic does, so it wants the full-module value diff and node and edge counts, and the(r,)-when-rank-is-known half is the part most likely to move something unexpected.What This Does Not Fix
The parameter that prompted the measurement belongs to a function the analysis never enters, because its class inherits it across a module boundary that falls to
object(#571, narrowed there). AShapegenerator movesnodes_numat the call sites and cannot move that parameter.