Now I have the complete picture. Here is the precise answer.
The exact location and mechanism
referencedVariables is the HashMap<String, VariableNode<D, C, F>> declared at the top of Expression.java. Variable nodes are inserted into it inside VariableNode.resolveReference() — specifically via expression.registerVariableReference(this) which does:
referencedVariables.put(reference.name, this);
The stream that feeds declareFieldsForUpstreamInputPropagation is built from that map:
upstreamInputVariableEntryStream().forEach(entry -> {
String varName = entry.getKey();
VariableNode varNode = entry.getValue();
Class<?> varType = varNode.type(); // ← called HERE
...
Compiler.declareField(classVisitor, varName, varType);
declaredVariables.add(varName);
});
upstreamInputVariableEntryStream() filters referencedVariables through the predicate field, which excludes the independent variable, the upstream independent variable name, and any variable already in context. What passes through are variables whose VariableNode.upstreamInput == true — those are the p, p0, p1, q, r, μ parameters from the enclosing expression's scope.
The bug is: VariableNode.resolveReference() is called during define() (parse time) and correctly puts these nodes into referencedVariables. But VariableNode.type() on those nodes returns null until after the enclosing BinaryOperationNode.type() has been called on their parent node — because VariableNode.type() delegates to reference.type, which is set by the parent binary op's type() method when it resolves the operand type hierarchy.
When traceNodes = true, BinaryOperationNode.generate() calls formatGenerationParameters() which calls left.type() and right.type() — this triggers type resolution on the subtree before declareFields runs. When traceNodes = false, that call never happens, so at the time declareFieldsForUpstreamInputPropagation iterates referencedVariables and calls varNode.type() on each upstream-input variable node, reference.type is still null — varType comes back null, the assert fires or is silently skipped, and the field is never declared.
The fix must force type() to be called on every VariableNode whose upstreamInput == true during optimize(), before generate() calls declareFields. The precise location is the end of optimize(), after eliminateCommonSubexpressions(), with a pass:
rootNode.accept(node -> {
if (node instanceof VariableNode<?,?,?> v && v.upstreamInput) {
v.type(); // prime reference.type from parent context
}
});
Or alternatively, in VariableNode.resolveReference() itself, ensure reference.type is set to upstreamExpression.getIndependentVariable().reference.type at the point the node is registered, rather than relying on the parent binary op's deferred type() call to back-fill it.
Originally posted by @crowlogic in #1021