Copilot commented on code in PR #6584: URL: https://github.com/apache/incubator-kie-drools/pull/6584#discussion_r2772636759
########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearJoinNode.java: ########## @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.reteoo.NodeTypeEnums; +import org.drools.core.common.BetaConstraints; +import org.drools.core.common.BiLinearBetaConstraints; +import org.drools.core.reteoo.builder.BuildContext; +import org.drools.core.util.FastIterator; + +public class BiLinearJoinNode extends JoinNode { + + private static final long serialVersionUID = 510l; + + // Cross-network declaration context for variable resolution + protected BiLinearDeclarationContext declarationContext; + + public BiLinearJoinNode() { + } + + public BiLinearJoinNode(final int id, + final LeftTupleSource leftInput, + final BetaRightInput rightInput, + final BetaConstraints constraints, + final BuildContext context) { + super(id, leftInput, rightInput, createBiLinearConstraints(constraints, leftInput), context); + } + + @Override + public void doAttach(BuildContext context) { + super.doAttach(context); + } + + /** + * Override setPartitionId to handle LeftTupleSource as second input instead of ObjectSource. + * BiLinear has LeftTupleSource as second input, not ObjectSource, so we need special handling. + */ + @Override + public void setPartitionId(BuildContext context, org.drools.base.common.RuleBasePartitionId partitionId) { + LeftTupleSource secondInput = getSecondLeftInput(); + if (secondInput != null) { + org.drools.base.common.RuleBasePartitionId parentId = secondInput.getPartitionId(); + if (parentId != org.drools.base.common.RuleBasePartitionId.MAIN_PARTITION && !parentId.equals(partitionId)) { + this.partitionId = parentId; + rightInput.setPartitionId(context, this.partitionId); + context.setPartitionId(this.partitionId); + leftInput.setSourcePartitionId(context, this.partitionId); + return; + } + } + this.partitionId = partitionId; + } + + private static BetaConstraints createBiLinearConstraints(BetaConstraints originalConstraints, + LeftTupleSource leftInput) { + return new BiLinearBetaConstraints(originalConstraints); + } + + private BiLinearDeclarationContext createDeclarationContext(LeftTupleSource leftInput, + LeftTupleSource secondLeftInput) { + int secondNetworkOffset = leftInput != null ? leftInput.getObjectCount() : 0; + + return new BiLinearDeclarationContext( + leftInput, + secondLeftInput, + secondNetworkOffset + ); + } + + public LeftTupleSource getSecondLeftInput() { + return ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).getWrappedSecondLeftInput(); + } + + public int getFirstNetworkSize() { + return getLeftTupleSource().getObjectCount(); + } + + public void linkOutsideLeftInput(LeftTupleSource secondLeftInput) { + + ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).setWrappedSecondLeftInput(secondLeftInput); + + if (secondLeftInput != null && !secondLeftInput.equals(getLeftTupleSource())) { + secondLeftInput.addTupleSink(this); + } + + this.setObjectCount(leftInput.getObjectCount() + secondLeftInput.getObjectCount()); + + this.declarationContext = createDeclarationContext(getLeftTupleSource(), secondLeftInput); + getBiLinearConstraints().setDeclarationContext( + declarationContext + ); Review Comment: The linkOutsideLeftInput method does not handle the case where secondLeftInput is null. If called with a null argument, it will throw a NullPointerException when calling secondLeftInput.getObjectCount(). Consider adding null validation at the start of the method. ```suggestion if (secondLeftInput == null) { // No second network; object count and declaration context are based solely on the primary left input this.setObjectCount(leftInput.getObjectCount()); this.declarationContext = createDeclarationContext(getLeftTupleSource(), null); getBiLinearConstraints().setDeclarationContext(declarationContext); return; } if (!secondLeftInput.equals(getLeftTupleSource())) { secondLeftInput.addTupleSink(this); } this.setObjectCount(leftInput.getObjectCount() + secondLeftInput.getObjectCount()); this.declarationContext = createDeclarationContext(getLeftTupleSource(), secondLeftInput); getBiLinearConstraints().setDeclarationContext( declarationContext ); ``` ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearJoinNode.java: ########## @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.reteoo.NodeTypeEnums; +import org.drools.core.common.BetaConstraints; +import org.drools.core.common.BiLinearBetaConstraints; +import org.drools.core.reteoo.builder.BuildContext; +import org.drools.core.util.FastIterator; + +public class BiLinearJoinNode extends JoinNode { + + private static final long serialVersionUID = 510l; + + // Cross-network declaration context for variable resolution + protected BiLinearDeclarationContext declarationContext; + + public BiLinearJoinNode() { + } + + public BiLinearJoinNode(final int id, + final LeftTupleSource leftInput, + final BetaRightInput rightInput, + final BetaConstraints constraints, + final BuildContext context) { + super(id, leftInput, rightInput, createBiLinearConstraints(constraints, leftInput), context); + } + + @Override + public void doAttach(BuildContext context) { + super.doAttach(context); + } + + /** + * Override setPartitionId to handle LeftTupleSource as second input instead of ObjectSource. + * BiLinear has LeftTupleSource as second input, not ObjectSource, so we need special handling. + */ + @Override + public void setPartitionId(BuildContext context, org.drools.base.common.RuleBasePartitionId partitionId) { + LeftTupleSource secondInput = getSecondLeftInput(); + if (secondInput != null) { + org.drools.base.common.RuleBasePartitionId parentId = secondInput.getPartitionId(); + if (parentId != org.drools.base.common.RuleBasePartitionId.MAIN_PARTITION && !parentId.equals(partitionId)) { + this.partitionId = parentId; + rightInput.setPartitionId(context, this.partitionId); + context.setPartitionId(this.partitionId); + leftInput.setSourcePartitionId(context, this.partitionId); + return; + } + } + this.partitionId = partitionId; + } + + private static BetaConstraints createBiLinearConstraints(BetaConstraints originalConstraints, + LeftTupleSource leftInput) { + return new BiLinearBetaConstraints(originalConstraints); + } + + private BiLinearDeclarationContext createDeclarationContext(LeftTupleSource leftInput, + LeftTupleSource secondLeftInput) { + int secondNetworkOffset = leftInput != null ? leftInput.getObjectCount() : 0; + + return new BiLinearDeclarationContext( + leftInput, + secondLeftInput, + secondNetworkOffset + ); + } + + public LeftTupleSource getSecondLeftInput() { + return ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).getWrappedSecondLeftInput(); Review Comment: The method getSecondLeftInput() casts rightInput to BiLinearLeftInputWrapper without a type check. If rightInput is not of this type (e.g., due to deserialization or incorrect initialization), this will throw a ClassCastException at runtime. Consider adding a type check or document that this method should only be called when the node is properly initialized. ```suggestion if (rightInput instanceof org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) { return ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).getWrappedSecondLeftInput(); } return null; ``` ########## drools-core/src/main/java/org/drools/core/reteoo/CompositeLeftTupleSinkAdapter.java: ########## @@ -67,10 +78,16 @@ public LeftTupleSink[] getSinks() { return sinkArray; } - LeftTupleSink[] sinks = new LeftTupleSink[this.sinks.size()]; + // Count actual nodes in case of list inconsistency (BiLinear shared nodes) + int actualCount = 0; + for ( LeftTupleSinkNode sink = this.sinks.getFirst(); sink != null; sink = sink.getNextLeftTupleSinkNode() ) { + actualCount++; + } + + LeftTupleSink[] sinks = new LeftTupleSink[actualCount]; int i = 0; - for ( LeftTupleSinkNode sink = this.sinks.getFirst(); sink != null; sink = sink.getNextLeftTupleSinkNode() ) { + for ( LeftTupleSinkNode sink = this.sinks.getFirst(); sink != null && i < actualCount; sink = sink.getNextLeftTupleSinkNode() ) { sinks[i++] = sink; } Review Comment: The getSinks method has a potential race condition. It first counts nodes (actualCount), then creates an array of that size, then iterates again to fill it. Between these operations, if the list is modified concurrently, the counts could be inconsistent. Consider synchronizing this method if concurrent access is possible, or document that it's not thread-safe. ########## drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java: ########## @@ -710,19 +723,24 @@ public void propagate(SegmentMemory sourceSegment, TupleSets leftTuples) { } } - private static void processPeers(SegmentMemory sourceSegment, TupleSets leftTuples) { + private void processPeers(SegmentMemory sourceSegment, TupleSets leftTuples) { Review Comment: The processPeers method signature changed from static to non-static, but there's no clear reason why instance state is needed. The method doesn't access any instance variables of RuleNetworkEvaluatorImpl. Consider keeping it static or document why instance access is required. ```suggestion private static void processPeers(SegmentMemory sourceSegment, TupleSets leftTuples) { ``` ########## drools-core/src/main/java/org/drools/core/reteoo/CompositeLeftTupleSinkAdapter.java: ########## @@ -40,7 +40,18 @@ public CompositeLeftTupleSinkAdapter(final RuleBasePartitionId partitionId) { } public void addTupleSink(final LeftTupleSink sink) { - this.sinks.add( (LeftTupleSinkNode) sink ); + // Prevent duplicate BiLinear shared nodes to avoid RETE corruption + LeftTupleSinkNode sinkNode = (LeftTupleSinkNode) sink; + + // Check if this exact sink instance is already registered + for (LeftTupleSinkNode existing = this.sinks.getFirst(); existing != null; existing = existing.getNextLeftTupleSinkNode()) { + if (existing == sinkNode) { + // Duplicate BiLinear shared node - skip to prevent corruption + return; + } + } + + this.sinks.add( sinkNode ); sinkArray = null; Review Comment: In the addTupleSink method, the duplicate detection loop uses object identity (==) comparison at line 48, which is correct. However, if sinks are equal but not identical (same configuration but different instances), they could still be added multiple times. Consider whether semantic equality should also be checked, or document why identity comparison is sufficient. ########## drools-core/src/main/java/org/drools/core/reteoo/builder/RuleOrderOptimizer.java: ########## @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo.builder; + +import org.drools.base.definitions.rule.impl.RuleImpl; +import org.kie.api.definition.rule.Rule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Optimizes rule build order to ensure BiLinear optimization succeeds. + * + * BiLinear optimization requires that rules defining shared patterns + * build before rules that reuse those patterns. This class analyzes + * BiLinear dependencies and reorders rules using topological sorting to satisfy + * these constraints. + * + * The reordering is transparent to users and maintains relative order of rules + * not involved in BiLinear optimization. + */ +public class RuleOrderOptimizer { + + private static final Logger logger = LoggerFactory.getLogger(RuleOrderOptimizer.class); + + public static Collection<? extends Rule> reorderForBiLinear( + Collection<? extends Rule> rules, + BiLinearDetector.BiLinearContext biLinearContext) { + + if (!BiLinearDetector.isBiLinearEnabled()) { + return rules; + } + + if (biLinearContext.sharedChains().isEmpty()) { + return rules; + } + + Map<String, Set<String>> dependencyGraph = buildDependencyGraph(biLinearContext); + + Map<String, RuleImpl> ruleMap = new HashMap<>(); + List<RuleImpl> originalOrder = new ArrayList<>(); + for (Rule rule : rules) { + if (rule instanceof RuleImpl ruleImpl) { + ruleMap.put(rule.getName(), ruleImpl); + originalOrder.add(ruleImpl); + } + } + + return topologicalSort(originalOrder, ruleMap, dependencyGraph); + } + + /** + * Builds a dependency graph from BiLinear pairs. + * + * For each Pair(chainId, consumerRuleName, providerRuleName): + * - providerRule = creates the shared JoinNode (simpler pattern, e.g., C-D) + * - consumerRule = uses BiLinearJoinNode linking to providerRule's JoinNode (complex pattern, e.g., A-B-C-D) + * - providerRule MUST build before consumerRule (so consumerRule can link to providerRule's node) + * + * @param biLinearContext BiLinear context with detected pairs + * @return Adjacency list: consumerRule -> set of providerRules that must build before it + */ + private static Map<String, Set<String>> buildDependencyGraph( + BiLinearDetector.BiLinearContext biLinearContext) { + + Map<String, Set<String>> graph = new HashMap<>(); + + for (Map.Entry<String, List<BiLinearDetector.Pair>> entry : biLinearContext.sharedChains().entrySet()) { + for (BiLinearDetector.Pair pair : entry.getValue()) { + String consumerRule = pair.consumerRuleName(); + String providerRule = pair.providerRuleName(); + + graph.computeIfAbsent(consumerRule, k -> new HashSet<>()).add(providerRule); + + graph.putIfAbsent(providerRule, new HashSet<>()); + } + } + + return graph; + } + + /** + * Algorithm: + * 1. Calculate in-degree for each rule (number of rules that must build before it) + * 2. Start with rules with in-degree 0 (no dependencies) + * 3. Process nodes in order, choosing from available rules based on original order + * 4. When a rule is processed, decrement in-degree of rules that depend on it + * 5. Detect and handle cycles gracefully + * + * Rules not involved in BiLinear are added in their original positions. + * The algorithm preserves relative order as much as possible (stable sort). + * + */ + private static List<Rule> topologicalSort( + List<? extends Rule> originalOrder, + Map<String, RuleImpl> ruleMap, + Map<String, Set<String>> dependencyGraph) { + + Map<String, Integer> inDegree = new HashMap<>(); + Set<String> rulesInGraph = dependencyGraph.keySet(); + + for (String ruleName : rulesInGraph) { + inDegree.put(ruleName, 0); + } + + for (Map.Entry<String, Set<String>> entry : dependencyGraph.entrySet()) { + for (String target : entry.getValue()) { + inDegree.put(target, inDegree.getOrDefault(target, 0) + 1); + } + } Review Comment: The inDegree map is initialized with 0 for all rules in the graph at line 126, but then at line 131 it uses getOrDefault with 0 as the default. This could lead to incorrect in-degree counts if a target node is not in rulesInGraph. The logic should ensure all nodes mentioned as targets are also in rulesInGraph, or handle missing nodes explicitly. ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearTuple.java: ########## @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.rule.Declaration; +import org.drools.core.common.InternalFactHandle; +import org.kie.api.runtime.rule.FactHandle; + +/** + * BiLinearTuple represents a tuple that combines facts from two separate left input networks. + * This specialized tuple enables cross-network variable resolution for BiLinearJoinNode, + * allowing constraints to reference variables from both input networks. + * + * The tuple maintains references to both source network tuples and provides unified + * variable access across networks through enhanced Declaration resolution. + */ +public class BiLinearTuple extends TupleImpl { + + private static final long serialVersionUID = 540l; + + /** First network tuple (primary left input) */ + private final TupleImpl firstNetworkTuple; + + /** Second network tuple (secondary left input) */ + private final TupleImpl secondNetworkTuple; + + /** + * Creates a BiLinearTuple combining tuples from two networks + * + * @param firstNetworkTuple Tuple from the first left input network + * @param secondNetworkTuple Tuple from the second left input network + * @param rightFactHandle Right input fact handle (may be null for some scenarios) + * @param sink The sink node for this tuple + */ + public BiLinearTuple(TupleImpl firstNetworkTuple, + TupleImpl secondNetworkTuple, + InternalFactHandle rightFactHandle, + Sink sink) { + super(rightFactHandle, sink, false); + + this.firstNetworkTuple = firstNetworkTuple; + this.secondNetworkTuple = secondNetworkTuple; + + // Set up parent chain for proper traversal by code generator + // The combined index is firstSize + secondSize - 1 (0-based) + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + + // Create a virtual parent chain that allows proper index traversal + // Start from the end of second network and chain through first network + if (secondSize > 0) { + // Set this tuple's handle to the last fact of second network + this.handle = secondNetworkTuple.getFactHandle(); + } + + setIndex(firstSize + secondSize - 1); + } + + /** + * Override getParent to provide virtual parent chain for code generator traversal. + * Returns a BiLinearParentView that continues the parent chain across both networks. + */ + @Override + public TupleImpl getParent() { + int currentIdx = getIndex(); + if (currentIdx <= 0) { + return null; + } + return new BiLinearParentView(this, currentIdx - 1); + } + + /** + * Inner class that provides a virtual parent view for a specific index. + * This allows the code generator to traverse the parent chain correctly. + */ + private static class BiLinearParentView extends TupleImpl { + private final BiLinearTuple biLinearTuple; + private final int viewIndex; + + BiLinearParentView(BiLinearTuple biLinearTuple, int viewIndex) { + this.biLinearTuple = biLinearTuple; + this.viewIndex = viewIndex; + } + + @Override + public int getIndex() { + return viewIndex; + } + + @Override + public InternalFactHandle getFactHandle() { + return (InternalFactHandle) biLinearTuple.get(viewIndex); + } + + @Override + public TupleImpl getParent() { + if (viewIndex <= 0) { + return null; + } + return new BiLinearParentView(biLinearTuple, viewIndex - 1); + } Review Comment: The BiLinearParentView class creates parent views recursively which could lead to stack overflow for very deep tuple chains. Consider implementing an iterative approach or adding a depth limit to prevent potential stack overflow errors. ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearDeclarationContext.java: ########## @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.rule.Declaration; +import org.drools.base.rule.Pattern; + +import java.util.HashMap; +import java.util.Map; + +/** + * BiLinearDeclarationContext manages variable declarations across two input networks + * for BiLinearJoinNode. It provides unified declaration lookup that can resolve + * variables from either input network and handles potential naming conflicts. + */ +public class BiLinearDeclarationContext { + + private final Map<String, Declaration> firstNetworkDeclarations; + + private final Map<String, Declaration> secondNetworkDeclarations; + + private final Map<String, Declaration> combinedDeclarations; + + private final Map<String, Integer> declarationNetworkMapping; + + private final int secondNetworkOffset; + + public BiLinearDeclarationContext(Map<String, Declaration> firstNetworkDeclarations, + Map<String, Declaration> secondNetworkDeclarations, + int secondNetworkOffset) { + this.firstNetworkDeclarations = new HashMap<>(firstNetworkDeclarations); + this.secondNetworkDeclarations = new HashMap<>(secondNetworkDeclarations); + this.secondNetworkOffset = secondNetworkOffset; + this.combinedDeclarations = new HashMap<>(); + this.declarationNetworkMapping = new HashMap<>(); + + buildCombinedDeclarations(); + } + + public BiLinearDeclarationContext(LeftTupleSource firstNetworkSource, + LeftTupleSource secondNetworkSource, + int secondNetworkOffset) { + this.secondNetworkOffset = secondNetworkOffset; + this.firstNetworkDeclarations = extractDeclarations(firstNetworkSource); + this.secondNetworkDeclarations = extractDeclarations(secondNetworkSource); + this.combinedDeclarations = new HashMap<>(); + this.declarationNetworkMapping = new HashMap<>(); + + buildCombinedDeclarations(); + } + + private void buildCombinedDeclarations() { + // Add first network declarations (no offset needed) + for (Map.Entry<String, Declaration> entry : firstNetworkDeclarations.entrySet()) { + String name = entry.getKey(); + Declaration declaration = entry.getValue(); + + combinedDeclarations.put(name, declaration); + declarationNetworkMapping.put(name, 1); + } + + // Add second network declarations with offset and conflict resolution + for (Map.Entry<String, Declaration> entry : secondNetworkDeclarations.entrySet()) { + String name = entry.getKey(); + Declaration originalDeclaration = entry.getValue(); + + // Check for naming conflicts + if (firstNetworkDeclarations.containsKey(name)) { + // Conflict detected - need to handle this + handleDeclarationConflict(name, originalDeclaration); + } else { + // No conflict - create offset declaration for second network + Declaration offsetDeclaration = createOffsetDeclaration(originalDeclaration); + combinedDeclarations.put(name, offsetDeclaration); + declarationNetworkMapping.put(name, 2); + } + } + } + + /** + * Handles naming conflicts between networks. + * When the same variable name exists in both networks, we use the second network's + * declaration with offset, keeping the original variable name from the rule. + * This allows rules to reference variables by their original names. + */ + private void handleDeclarationConflict(String name, Declaration secondNetworkDeclaration) { + Declaration offsetDeclaration = createOffsetDeclaration(secondNetworkDeclaration); + + // Replace the first network's declaration with the second network's (with offset) + combinedDeclarations.put(name, offsetDeclaration); + declarationNetworkMapping.put(name, 2); + } + + private Declaration createOffsetDeclaration(Declaration original) { + Declaration offsetDeclaration = new Declaration( + original.getIdentifier(), + original.getExtractor(), + createOffsetPattern(original.getPattern()) + ); + + offsetDeclaration.setDeclarationClass(original.getDeclarationClass()); + + return offsetDeclaration; + } + + private Pattern createOffsetPattern(Pattern original) { + if (original == null) { + return null; + } + + Pattern offsetPattern = new Pattern( + original.getPatternId(), + original.getTupleIndex() + secondNetworkOffset, + original.getObjectIndex() + secondNetworkOffset, + original.getObjectType(), + original.getDeclaration() != null ? original.getDeclaration().getIdentifier() : null + ); + + return offsetPattern; + } + + private Map<String, Declaration> extractDeclarations(LeftTupleSource source) { + return new HashMap<>(); Review Comment: The extractDeclarations method returns an empty HashMap regardless of the source parameter. This looks like an incomplete implementation. Either implement proper declaration extraction or document why returning empty is intentional. ```suggestion Map<String, Declaration> result = new HashMap<>(); if (source == null) { return result; } Map<String, Declaration> outerDeclarations = source.getOuterDeclarations(); if (outerDeclarations != null && !outerDeclarations.isEmpty()) { result.putAll(outerDeclarations); } return result; ``` ########## drools-core/src/main/java/org/drools/core/phreak/PhreakBiLinearJoinNode.java: ########## @@ -0,0 +1,471 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.phreak; + +import org.drools.core.common.ReteEvaluator; +import org.drools.core.common.TupleSets; +import org.drools.core.reteoo.BetaMemory; +import org.drools.core.common.BiLinearBetaConstraints; +import org.drools.core.reteoo.BiLinearContextEntry; +import org.drools.base.reteoo.NodeTypeEnums; +import org.drools.core.reteoo.BiLinearJoinNode; +import org.drools.core.reteoo.BiLinearRuleTerminalNodeLeftTuple; +import org.drools.core.reteoo.BiLinearTuple; +import org.drools.core.reteoo.LeftTupleSink; +import org.drools.core.reteoo.TupleImpl; +import org.drools.core.reteoo.TupleMemory; +import org.drools.core.util.FastIterator; + +/** + * Phreak processor for BiLinearJoinNode that handles joining two left input networks. + * Unlike traditional joins that have one left and one right input, BiLinearJoinNode + * has two left inputs that get joined together. + */ +public class PhreakBiLinearJoinNode { + + private final ReteEvaluator reteEvaluator; + + public PhreakBiLinearJoinNode(ReteEvaluator reteEvaluator) { + this.reteEvaluator = reteEvaluator; + } + + public void doNode(BiLinearJoinNode biLinearJoinNode, + LeftTupleSink sink, + BetaMemory bm, + TupleSets srcLeftTuples, + TupleSets stagedLeftTuples, + TupleSets trgLeftTuples) { + + // Get tuples from second left input (stored in "right" memory for BiLinear) + TupleSets srcRightTuples = bm.getStagedRightTuples().takeAll(); + + if (srcRightTuples.getDeleteFirst() != null) { + doRightDeletes(bm, srcRightTuples, trgLeftTuples, stagedLeftTuples); + } + if (srcLeftTuples.getDeleteFirst() != null) { + doLeftDeletes(bm, srcLeftTuples, trgLeftTuples, stagedLeftTuples); + } + + // Process updates from both inputs + if (srcRightTuples.getUpdateFirst() != null) { + PhreakNodeOperations.doUpdatesReorderRightMemory(bm, srcRightTuples); + } + if (srcLeftTuples.getUpdateFirst() != null) { + PhreakNodeOperations.doUpdatesReorderLeftMemory(bm, srcLeftTuples); + } + + if (srcRightTuples.getUpdateFirst() != null) { + doRightUpdates(biLinearJoinNode, sink, bm, srcRightTuples, trgLeftTuples, stagedLeftTuples); + } + if (srcLeftTuples.getUpdateFirst() != null) { + doLeftUpdates(biLinearJoinNode, sink, bm, srcLeftTuples, trgLeftTuples, stagedLeftTuples); + } + + // Process inserts from both inputs + if (srcRightTuples.getInsertFirst() != null) { + doRightInserts(biLinearJoinNode, sink, bm, srcRightTuples, trgLeftTuples); + } + if (srcLeftTuples.getInsertFirst() != null) { + doLeftInserts(biLinearJoinNode, sink, bm, srcLeftTuples, trgLeftTuples); + } + + srcRightTuples.resetAll(); + srcLeftTuples.resetAll(); + } + + /** + * Process left tuple inserts for BiLinearJoinNode. + * This method joins tuples from the primary left input with tuples from the second left input. + */ + public void doLeftInserts(BiLinearJoinNode biLinearJoinNode, + LeftTupleSink sink, + BetaMemory<?> bm, + TupleSets srcLeftTuples, + TupleSets trgLeftTuples) { + + TupleMemory ltm = bm.getLeftTupleMemory(); // Memory for first left input + TupleMemory rtm = bm.getRightTupleMemory(); // Memory for second left input (treated as "right" for memory purposes) + Object contextEntry = bm.getContext(); + + // Get BiLinear constraints wrapper (all BiLinearJoinNode constraints are wrapped) + BiLinearBetaConstraints biLinearConstraints = biLinearJoinNode.getBiLinearConstraints(); + + if (biLinearConstraints == null) { + for (TupleImpl leftTuple = srcLeftTuples.getInsertFirst(); leftTuple != null; ) { + TupleImpl next = leftTuple.getStagedNext(); + leftTuple.clearStaged(); + leftTuple = next; + } + return; + } + + BiLinearContextEntry biLinearContext = contextEntry instanceof BiLinearContextEntry ? + (BiLinearContextEntry) contextEntry : null; + + if (biLinearContext == null) { + for (TupleImpl leftTuple = srcLeftTuples.getInsertFirst(); leftTuple != null; ) { + TupleImpl next = leftTuple.getStagedNext(); + leftTuple.clearStaged(); + leftTuple = next; + } + return; + } Review Comment: The doLeftInserts and doRightInserts methods silently consume all tuples and return early when biLinearConstraints or biLinearContext are null (lines 109-128). This could hide configuration errors where BiLinearJoinNode is not properly initialized. Consider logging a warning or error when this happens, as it indicates a serious misconfiguration. ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearRuleTerminalNodeLeftTuple.java: ########## @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.rule.Declaration; +import org.kie.api.runtime.rule.FactHandle; + +/** + * BiLinearRuleTerminalNodeLeftTuple is a specialized terminal tuple for BiLinearJoinNode. + * It extends RuleTerminalNodeLeftTuple to be compatible with PhreakRuleTerminalNode, + * while also providing cross-network fact access like BiLinearTuple. + * + * This allows rules using BiLinearJoinNode to access variables from both input networks + * in their consequences. + */ +public class BiLinearRuleTerminalNodeLeftTuple extends RuleTerminalNodeLeftTuple { + + private static final long serialVersionUID = 540l; + + private final TupleImpl firstNetworkTuple; + + private final TupleImpl secondNetworkTuple; + + /** Offset for second network tuple indices */ + private final int secondNetworkOffset; + + public BiLinearRuleTerminalNodeLeftTuple(TupleImpl firstNetworkTuple, + TupleImpl secondNetworkTuple, + Sink sink) { + super(); + setSink(sink); + + this.firstNetworkTuple = firstNetworkTuple; + this.secondNetworkTuple = secondNetworkTuple; + + // Calculate offset: second network indices start after first network + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + this.secondNetworkOffset = firstSize; + + // Set the index to the last position in the combined network + setIndex(firstSize + secondSize - 1); + + // Set handle to the last fact in the chain (from second network) + if (secondNetworkTuple != null) { + setFactHandle(secondNetworkTuple.getFactHandle()); + } + } + + /** + * Override get to provide cross-network access. + * Index mapping: + * - 0 to firstNetworkSize-1: First network facts + * - firstNetworkSize to firstNetworkSize+secondNetworkSize-1: Second network facts + */ + @Override + public FactHandle get(int index) { + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + + // First network range + if (index < firstSize) { + return firstNetworkTuple.get(index); + } + + // Second network range + if (index < firstSize + secondSize) { + int secondNetworkIndex = index - firstSize; + return secondNetworkTuple.get(secondNetworkIndex); + } + + throw new IndexOutOfBoundsException("Tuple index " + index + " is out of bounds. " + + "First network size: " + firstSize + ", Second network size: " + secondSize); + } + + @Override + public FactHandle get(Declaration declaration) { + return get(declaration.getTupleIndex()); + } + + @Override + public Object getObject(int index) { + FactHandle handle = get(index); + return handle != null ? handle.getObject() : null; + } + + @Override + public Object getObject(Declaration declaration) { + return getObject(declaration.getTupleIndex()); + } + + @Override + public int size() { + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + return firstSize + secondSize; + } + + @Override + public TupleImpl getParent() { + int currentIdx = getIndex(); + if (currentIdx <= 0) { + return null; + } + return new BiLinearParentView(this, currentIdx - 1); + } + + /** + * Inner class providing virtual parent view for index traversal. + */ + private static class BiLinearParentView extends TupleImpl { + private final BiLinearRuleTerminalNodeLeftTuple biLinearTuple; + private final int viewIndex; + + BiLinearParentView(BiLinearRuleTerminalNodeLeftTuple biLinearTuple, int viewIndex) { + this.biLinearTuple = biLinearTuple; + this.viewIndex = viewIndex; + } + + @Override + public int getIndex() { + return viewIndex; + } + + @Override + public org.drools.core.common.InternalFactHandle getFactHandle() { + return (org.drools.core.common.InternalFactHandle) biLinearTuple.get(viewIndex); + } + + @Override + public TupleImpl getParent() { + if (viewIndex <= 0) { + return null; + } + return new BiLinearParentView(biLinearTuple, viewIndex - 1); Review Comment: Similar to BiLinearTuple, the BiLinearParentView recursion in getParent() could lead to stack overflow for very deep tuple chains. Consider an iterative approach or depth limiting. ########## drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java: ########## @@ -710,19 +723,24 @@ public void propagate(SegmentMemory sourceSegment, TupleSets leftTuples) { } } - private static void processPeers(SegmentMemory sourceSegment, TupleSets leftTuples) { + private void processPeers(SegmentMemory sourceSegment, TupleSets leftTuples) { SegmentMemory firstSmem = sourceSegment.getFirst(); processPeerDeletes( leftTuples.getDeleteFirst(), firstSmem ); processPeerDeletes( leftTuples.getNormalizedDeleteFirst(), firstSmem ); processPeerUpdates( leftTuples, firstSmem ); - processPeerInserts( leftTuples, firstSmem ); + processPeerInserts( leftTuples, firstSmem, sourceSegment ); - firstSmem.getStagedLeftTuples().addAll( leftTuples ); + // Check if target is BiLinearJoinNode receiving from second input + if (shouldRouteToBiLinearRightMemory(sourceSegment, firstSmem)) { + routeToBiLinearRightMemory(firstSmem, leftTuples); + } else { + firstSmem.getStagedLeftTuples().addAll( leftTuples ); + } leftTuples.resetAll(); } - private static void processPeerInserts(TupleSets leftTuples, SegmentMemory firstSmem) { + private static void processPeerInserts(TupleSets leftTuples, SegmentMemory firstSmem, SegmentMemory sourceSegment) { Review Comment: The method signature for processPeerInserts was changed to add a sourceSegment parameter, but this parameter is only used in the BiLinear routing check. Consider renaming the parameter to make its purpose clearer, or refactor to avoid passing unnecessary parameters through the call chain. ########## drools-core/src/main/java/org/drools/core/reteoo/ReteooBuilder.java: ########## @@ -381,16 +382,13 @@ private void updateLeafSet(BaseNode baseNode, NodeSet leafSet) { public static class IdGenerator implements Externalizable { private static final long serialVersionUID = 510l; + private static final int FIRST_ID = 1; private Queue<Integer> recycledIds; private int nextId; public IdGenerator() { - this(1); - } - - public IdGenerator(final int firstId) { - this.nextId = firstId; + this.nextId = FIRST_ID; Review Comment: The IdGenerator constructor parameter was removed and FIRST_ID constant was added, but the constructor that took firstId parameter was also removed. This could break any code that was instantiating IdGenerator with a custom starting ID. Consider deprecating rather than removing, or ensure no callers exist. ```suggestion this(FIRST_ID); } /** * @deprecated Use the no-argument constructor instead. This constructor is retained * for backward compatibility with code that explicitly specifies a starting id. */ @Deprecated public IdGenerator(int firstId) { this.nextId = firstId; ``` ########## drools-core/src/main/java/org/drools/core/reteoo/builder/RuleOrderOptimizer.java: ########## @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo.builder; + +import org.drools.base.definitions.rule.impl.RuleImpl; +import org.kie.api.definition.rule.Rule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Optimizes rule build order to ensure BiLinear optimization succeeds. + * + * BiLinear optimization requires that rules defining shared patterns + * build before rules that reuse those patterns. This class analyzes + * BiLinear dependencies and reorders rules using topological sorting to satisfy + * these constraints. + * + * The reordering is transparent to users and maintains relative order of rules + * not involved in BiLinear optimization. + */ +public class RuleOrderOptimizer { + + private static final Logger logger = LoggerFactory.getLogger(RuleOrderOptimizer.class); + + public static Collection<? extends Rule> reorderForBiLinear( + Collection<? extends Rule> rules, + BiLinearDetector.BiLinearContext biLinearContext) { + + if (!BiLinearDetector.isBiLinearEnabled()) { + return rules; + } + + if (biLinearContext.sharedChains().isEmpty()) { + return rules; + } + + Map<String, Set<String>> dependencyGraph = buildDependencyGraph(biLinearContext); + + Map<String, RuleImpl> ruleMap = new HashMap<>(); + List<RuleImpl> originalOrder = new ArrayList<>(); + for (Rule rule : rules) { + if (rule instanceof RuleImpl ruleImpl) { + ruleMap.put(rule.getName(), ruleImpl); + originalOrder.add(ruleImpl); + } + } + + return topologicalSort(originalOrder, ruleMap, dependencyGraph); + } + + /** + * Builds a dependency graph from BiLinear pairs. + * + * For each Pair(chainId, consumerRuleName, providerRuleName): + * - providerRule = creates the shared JoinNode (simpler pattern, e.g., C-D) + * - consumerRule = uses BiLinearJoinNode linking to providerRule's JoinNode (complex pattern, e.g., A-B-C-D) + * - providerRule MUST build before consumerRule (so consumerRule can link to providerRule's node) + * + * @param biLinearContext BiLinear context with detected pairs + * @return Adjacency list: consumerRule -> set of providerRules that must build before it + */ + private static Map<String, Set<String>> buildDependencyGraph( + BiLinearDetector.BiLinearContext biLinearContext) { + + Map<String, Set<String>> graph = new HashMap<>(); + + for (Map.Entry<String, List<BiLinearDetector.Pair>> entry : biLinearContext.sharedChains().entrySet()) { + for (BiLinearDetector.Pair pair : entry.getValue()) { + String consumerRule = pair.consumerRuleName(); + String providerRule = pair.providerRuleName(); + + graph.computeIfAbsent(consumerRule, k -> new HashSet<>()).add(providerRule); + + graph.putIfAbsent(providerRule, new HashSet<>()); + } + } + + return graph; + } + + /** + * Algorithm: + * 1. Calculate in-degree for each rule (number of rules that must build before it) + * 2. Start with rules with in-degree 0 (no dependencies) + * 3. Process nodes in order, choosing from available rules based on original order + * 4. When a rule is processed, decrement in-degree of rules that depend on it + * 5. Detect and handle cycles gracefully + * + * Rules not involved in BiLinear are added in their original positions. + * The algorithm preserves relative order as much as possible (stable sort). + * + */ + private static List<Rule> topologicalSort( + List<? extends Rule> originalOrder, + Map<String, RuleImpl> ruleMap, + Map<String, Set<String>> dependencyGraph) { + + Map<String, Integer> inDegree = new HashMap<>(); + Set<String> rulesInGraph = dependencyGraph.keySet(); + + for (String ruleName : rulesInGraph) { + inDegree.put(ruleName, 0); + } + + for (Map.Entry<String, Set<String>> entry : dependencyGraph.entrySet()) { + for (String target : entry.getValue()) { + inDegree.put(target, inDegree.getOrDefault(target, 0) + 1); + } + } + + List<Rule> result = new ArrayList<>(); + Set<String> remaining = new HashSet<>(rulesInGraph); + + while (!remaining.isEmpty()) { + List<String> available = new ArrayList<>(); + for (Rule rule : originalOrder) { + String ruleName = rule.getName(); + if (remaining.contains(ruleName) && inDegree.get(ruleName) == 0) { + available.add(ruleName); + } + } + + // If no rules available but rules remain, we have a cycle + if (available.isEmpty()) { + logger.warn("Circular BiLinear dependencies detected. " + + "Remaining rules will be built in original order: {}", remaining); + + for (Rule rule : originalOrder) { + if (remaining.contains(rule.getName())) { + result.add(rule); + } + } + break; + } + + for (String ruleName : available) { + RuleImpl rule = ruleMap.get(ruleName); + if (rule != null) { + result.add(rule); + remaining.remove(ruleName); + + Set<String> dependents = dependencyGraph.get(ruleName); + if (dependents != null) { + for (String dependent : dependents) { + inDegree.put(dependent, inDegree.get(dependent) - 1); + } + } + } + } + } Review Comment: The topologicalSort method has a potential infinite loop if the circular dependency detection logic fails. The while loop at line 138 continues as long as 'remaining' is not empty, but if the cycle detection at line 148 doesn't properly break, and no rules are added to 'available', the loop will continue indefinitely. Consider adding a safety counter to break after a maximum number of iterations. ########## drools-core/src/main/java/org/drools/core/reteoo/builder/BiLinearLeftInputWrapper.java: ########## @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo.builder; + +import org.drools.base.common.NetworkNode; +import org.drools.base.common.RuleBasePartitionId; +import org.drools.base.reteoo.BaseTerminalNode; +import org.drools.core.common.*; +import org.drools.core.reteoo.*; +import org.drools.util.bitmask.BitMask; +import org.drools.util.bitmask.EmptyBitMask; +import org.kie.api.definition.rule.Rule; + +import java.util.Objects; + +public class BiLinearLeftInputWrapper implements BetaRightInput { + + private LeftTupleSource wrappedSecondLeftInput; + private BiLinearJoinNode betaNode; + public LeftTupleSource getWrappedSecondLeftInput() { + return wrappedSecondLeftInput; + } + + public void setWrappedSecondLeftInput(LeftTupleSource source) { + this.wrappedSecondLeftInput = source; + } + + @Override + public void setBetaNode(BetaNode betaNode) { + this.betaNode = (BiLinearJoinNode) betaNode; + } + + @Override + public void setPartitionId(BuildContext context, RuleBasePartitionId partitionId) { + + } + + @Override + public void initInferredMask() { + + } + + @Override + public boolean inputIsTupleToObjectNode() { + return false; + } + + @Override + public ObjectSource getParent() { + // BiLinear wraps LeftTupleSource, not ObjectSource + // BiLinearJoinNode overrides setPartitionId() to handle this properly + // Return null since LeftTupleSource cannot be cast to ObjectSource + return null; + } + + @Override + public ObjectTypeNode getObjectTypeNode() { + return null; + } + + @Override + public void doAttach(BuildContext context) { + // Register BiLinearJoinNode as sink on second left input + // Only register if it's different from first input to avoid duplicate registration + if (wrappedSecondLeftInput != null && betaNode != null && + !wrappedSecondLeftInput.equals(betaNode.getLeftTupleSource())) { + wrappedSecondLeftInput.addTupleSink(betaNode, context); + } + } Review Comment: The doAttach method in BiLinearLeftInputWrapper checks if secondLeftInput equals leftTupleSource but doesn't handle the case where both could be null. If both are null, the equals check will pass but secondLeftInput.addTupleSink will throw NullPointerException on line 83. Add null checks before the equals comparison. ########## drools-core/src/main/java/org/drools/core/common/BiLinearBetaConstraints.java: ########## @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.common; + +import org.drools.base.base.ObjectType; +import org.drools.base.base.ValueResolver; +import org.drools.base.reteoo.BaseTuple; +import org.drools.base.rule.Pattern; +import org.drools.base.rule.constraint.BetaConstraint; +import org.drools.core.RuleBaseConfiguration; +import org.drools.core.reteoo.*; +import org.drools.core.reteoo.builder.BuildContext; +import org.drools.core.util.index.TupleList; +import org.drools.util.bitmask.BitMask; +import org.kie.api.runtime.rule.FactHandle; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.List; +import java.util.Objects; + +/** + * BiLinearBetaConstraints wraps standard BetaConstraints to provide cross-network + * variable resolution for BiLinearJoinNode. It intercepts constraint evaluation + * calls and ensures that variables from both input networks are available during + * evaluation. + */ +public class BiLinearBetaConstraints implements BetaConstraints<BiLinearContextEntry> { + + private final BetaConstraints wrappedConstraints; + + private BiLinearDeclarationContext declarationContext; + + public BiLinearBetaConstraints(BetaConstraints wrappedConstraints) { + this.wrappedConstraints = wrappedConstraints; + } + + public BiLinearBetaConstraints(BetaConstraints wrappedConstraints, + BiLinearDeclarationContext declarationContext) { + this(wrappedConstraints); + this.declarationContext = declarationContext; + } + public void setDeclarationContext(BiLinearDeclarationContext declarationContext) { + this.declarationContext = declarationContext; + } + + @Override + public void init(BuildContext context, int betaNodeType) { + wrappedConstraints.init(context, betaNodeType); + } + + @Override + public void initIndexes(int depth, int betaNodeType, RuleBaseConfiguration config) { + wrappedConstraints.initIndexes(depth, betaNodeType, config); + } + + @Override + public BiLinearContextEntry createContext() { + return new BiLinearContextEntry(declarationContext); + } + + @Override + public boolean isIndexed() { + return wrappedConstraints.isIndexed(); + } + + @Override + public int getIndexCount() { + return wrappedConstraints.getIndexCount(); + } + + @Override + public boolean isEmpty() { + return wrappedConstraints.isEmpty(); + } + + @Override + public BetaMemory createBetaMemory(RuleBaseConfiguration config, int betaNodeType) { + return new BetaMemory<BiLinearContextEntry>( + config.isSequential() ? null : new TupleList(), + new TupleList(), + createContext(), + betaNodeType + ); + } + + @Override + public void updateFromTuple(BiLinearContextEntry context, ValueResolver valueResolver, Tuple tuple) { + context.updateFromTuple(valueResolver, tuple); + + Object wrappedContext = getWrappedContext(context); + if (tuple != null) { + wrappedConstraints.updateFromTuple(wrappedContext, valueResolver, tuple); + } + } + + public void updateFromBiLinearTuples(BiLinearContextEntry context, + ValueResolver valueResolver, + Tuple firstNetworkTuple, + Tuple secondNetworkTuple) { + context.updateFromBiLinearTuples(valueResolver, firstNetworkTuple, secondNetworkTuple); + + Object wrappedContext = getWrappedContext(context); + wrappedConstraints.updateFromTuple(wrappedContext, valueResolver, (Tuple) firstNetworkTuple); + } + + @Override + public void updateFromFactHandle(BiLinearContextEntry context, ValueResolver valueResolver, FactHandle handle) { + context.updateFromFactHandle(valueResolver, handle); + + Object wrappedContext = getWrappedContext(context); + wrappedConstraints.updateFromFactHandle(wrappedContext, valueResolver, handle); + } + + @Override + public void resetTuple(BiLinearContextEntry context) { + context.resetTuple(); + + Object wrappedContext = getWrappedContext(context); + wrappedConstraints.resetTuple(wrappedContext); + } + + @SuppressWarnings("unchecked") + public void resetTupleContext(Object context) { + if (context instanceof BiLinearContextEntry) { + resetTuple((BiLinearContextEntry) context); + } else { + wrappedConstraints.resetTuple(context); + } + } + + @Override + public void resetFactHandle(BiLinearContextEntry context) { + context.resetFactHandle(); + + Object wrappedContext = getWrappedContext(context); + wrappedConstraints.resetFactHandle(wrappedContext); + } + + @Override + public boolean isAllowedCachedLeft(BiLinearContextEntry context, FactHandle handle) { + if (wrappedConstraints instanceof org.drools.core.common.EmptyBetaConstraints) { + return wrappedConstraints.isAllowedCachedLeft(wrappedConstraints.createContext(), handle); + } + + Object wrappedContext = getWrappedContext(context); + return wrappedConstraints.isAllowedCachedLeft(wrappedContext, handle); + } + + @Override + public boolean isAllowedCachedRight(BaseTuple tuple, BiLinearContextEntry context) { + if (wrappedConstraints instanceof org.drools.core.common.EmptyBetaConstraints) { + return wrappedConstraints.isAllowedCachedRight(tuple, wrappedConstraints.createContext()); + } + + Object wrappedContext = getWrappedContext(context); + return wrappedConstraints.isAllowedCachedRight(tuple, wrappedContext); + } + + @SuppressWarnings("unchecked") + private Object getWrappedContext(BiLinearContextEntry context) { + Object wrappedContext = wrappedConstraints.createContext(); + + if (context.getFirstNetworkTuple() != null) { + wrappedConstraints.updateFromTuple(wrappedContext, context.getValueResolver(), (Tuple) context.getFirstNetworkTuple()); + } + if (context.getRightHandle() != null) { + wrappedConstraints.updateFromFactHandle(wrappedContext, context.getValueResolver(), context.getRightHandle()); + } + + return wrappedContext; + } + + @Override + public BetaConstraint[] getConstraints() { + return wrappedConstraints.getConstraints(); + } + + @Override + public BetaConstraints getOriginalConstraint() { + return wrappedConstraints.getOriginalConstraint(); + } + + @Override + public <T> T cloneIfInUse() { + BetaConstraints clonedWrapped = (BetaConstraints) wrappedConstraints.cloneIfInUse(); + BiLinearBetaConstraints cloned = new BiLinearBetaConstraints(clonedWrapped, declarationContext); + return (T) cloned; + } + + @Override + public BitMask getListenedPropertyMask(Pattern pattern, ObjectType modifiedType, List<String> settableProperties) { + return wrappedConstraints.getListenedPropertyMask(pattern, modifiedType, settableProperties); + } + + @Override + public boolean isLeftUpdateOptimizationAllowed() { + return wrappedConstraints.isLeftUpdateOptimizationAllowed(); + } + + @Override + public void registerEvaluationContext(BuildContext buildContext) { + wrappedConstraints.registerEvaluationContext(buildContext); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(wrappedConstraints); + out.writeObject(declarationContext); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + throw new UnsupportedOperationException("Deserialization not yet implemented for BiLinearBetaConstraints"); Review Comment: The readExternal method throws UnsupportedOperationException, which means BiLinearBetaConstraints cannot be deserialized. This could break session serialization/persistence. Either implement proper deserialization or ensure this is documented and that BiLinear nodes are excluded from serialization flows. ```suggestion @SuppressWarnings("unchecked") public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { wrappedConstraints = (BetaConstraints<BiLinearContextEntry>) in.readObject(); declarationContext = in.readObject(); ``` ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearTuple.java: ########## @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.rule.Declaration; +import org.drools.core.common.InternalFactHandle; +import org.kie.api.runtime.rule.FactHandle; + +/** + * BiLinearTuple represents a tuple that combines facts from two separate left input networks. + * This specialized tuple enables cross-network variable resolution for BiLinearJoinNode, + * allowing constraints to reference variables from both input networks. + * + * The tuple maintains references to both source network tuples and provides unified + * variable access across networks through enhanced Declaration resolution. + */ +public class BiLinearTuple extends TupleImpl { + + private static final long serialVersionUID = 540l; + + /** First network tuple (primary left input) */ + private final TupleImpl firstNetworkTuple; + + /** Second network tuple (secondary left input) */ + private final TupleImpl secondNetworkTuple; + + /** + * Creates a BiLinearTuple combining tuples from two networks + * + * @param firstNetworkTuple Tuple from the first left input network + * @param secondNetworkTuple Tuple from the second left input network + * @param rightFactHandle Right input fact handle (may be null for some scenarios) + * @param sink The sink node for this tuple + */ + public BiLinearTuple(TupleImpl firstNetworkTuple, + TupleImpl secondNetworkTuple, + InternalFactHandle rightFactHandle, + Sink sink) { + super(rightFactHandle, sink, false); + + this.firstNetworkTuple = firstNetworkTuple; + this.secondNetworkTuple = secondNetworkTuple; + + // Set up parent chain for proper traversal by code generator + // The combined index is firstSize + secondSize - 1 (0-based) + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + + // Create a virtual parent chain that allows proper index traversal + // Start from the end of second network and chain through first network + if (secondSize > 0) { + // Set this tuple's handle to the last fact of second network + this.handle = secondNetworkTuple.getFactHandle(); + } + + setIndex(firstSize + secondSize - 1); + } + + /** + * Override getParent to provide virtual parent chain for code generator traversal. + * Returns a BiLinearParentView that continues the parent chain across both networks. + */ + @Override + public TupleImpl getParent() { + int currentIdx = getIndex(); + if (currentIdx <= 0) { + return null; + } + return new BiLinearParentView(this, currentIdx - 1); + } + + /** + * Inner class that provides a virtual parent view for a specific index. + * This allows the code generator to traverse the parent chain correctly. + */ + private static class BiLinearParentView extends TupleImpl { + private final BiLinearTuple biLinearTuple; + private final int viewIndex; + + BiLinearParentView(BiLinearTuple biLinearTuple, int viewIndex) { + this.biLinearTuple = biLinearTuple; + this.viewIndex = viewIndex; + } + + @Override + public int getIndex() { + return viewIndex; + } + + @Override + public InternalFactHandle getFactHandle() { + return (InternalFactHandle) biLinearTuple.get(viewIndex); + } + + @Override + public TupleImpl getParent() { + if (viewIndex <= 0) { + return null; + } + return new BiLinearParentView(biLinearTuple, viewIndex - 1); + } + + @Override + public FactHandle get(int index) { + return biLinearTuple.get(index); + } + + @Override + public FactHandle get(Declaration declaration) { + return biLinearTuple.get(declaration); + } + + @Override + public InternalFactHandle getOriginalFactHandle() { + InternalFactHandle fh = getFactHandle(); + if (fh != null && fh.isEvent()) { + InternalFactHandle linkedFH = + ((org.drools.core.common.DefaultEventHandle)fh).getLinkedFactHandle(); + return linkedFH != null ? linkedFH : fh; + } + return fh; + } + + @Override + public ObjectTypeNodeId getInputOtnId() { + return biLinearTuple.getInputOtnId(); + } + + @Override + public boolean isLeftTuple() { + return true; + } + + @Override + public void reAdd() { + // No-op for view + } + } + + @Override + public FactHandle get(Declaration declaration) { + return get(declaration.getTupleIndex()); + } + + /** + * Enhanced get method that resolves indices across both networks + * + * Index mapping: + * - 0 to firstNetworkSize-1: First network facts + * - firstNetworkSize to firstNetworkSize+secondNetworkSize-1: Second network facts + * - firstNetworkSize+secondNetworkSize: Right fact (if present) + */ + @Override + public FactHandle get(int index) { + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + + // First network range + if (index < firstSize) { + return firstNetworkTuple.get(index); + } + + // Second network range + if (index < firstSize + secondSize) { + int secondNetworkIndex = index - firstSize; + return secondNetworkTuple.get(secondNetworkIndex); + } + + // Right fact + if (index == firstSize + secondSize && this.handle != null) { + return this.handle; + } + + throw new IndexOutOfBoundsException("Tuple index " + index + " is out of bounds. " + + "First network size: " + firstSize + ", Second network size: " + secondSize + + ", Has right fact: " + (this.handle != null)); + } + + /** + * Enhanced getObject method for cross-network object access + */ + @Override + public Object getObject(Declaration declaration) { + return getObject(declaration.getTupleIndex()); + } + + @Override + public Object getObject(int index) { + FactHandle handle = get(index); + return handle != null ? handle.getObject() : null; + } + + @Override + public int size() { + int firstSize = firstNetworkTuple != null ? firstNetworkTuple.size() : 0; + int secondSize = secondNetworkTuple != null ? secondNetworkTuple.size() : 0; + int rightSize = this.handle != null ? 1 : 0; + return firstSize + secondSize + rightSize; + } + + @Override + public int getIndex() { + return size() - 1; Review Comment: The getIndex() method returns size() - 1, which will return -1 when size() is 0. This could cause issues in code that expects getIndex() to return non-negative values. Consider returning 0 for empty tuples or document this behavior clearly. ```suggestion int tupleSize = size(); return tupleSize == 0 ? 0 : tupleSize - 1; ``` ########## drools-core/src/main/java/org/drools/core/phreak/BiLinearRoutingHelper.java: ########## @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.phreak; + +import org.drools.core.common.Memory; +import org.drools.core.common.TupleSets; +import org.drools.core.reteoo.BetaMemory; +import org.drools.core.reteoo.PathMemory; +import org.drools.core.reteoo.SegmentMemory; +import org.drools.core.reteoo.TupleImpl; +import org.drools.core.reteoo.builder.BiLinearDetector; + +import java.util.List; + +public final class BiLinearRoutingHelper { + + private BiLinearRoutingHelper() { + } + + /** + * Check if tuples should be routed to BiLinearJoinNode's right memory instead of left memory. + * This happens when the source segment's tip node is a BiLinearJoinNode's second left input. + */ + public static boolean shouldRouteToBiLinearRightMemory(SegmentMemory sourceSegment, SegmentMemory targetSegment) { + if (!BiLinearDetector.isBiLinearEnabled()) { + return false; + } + + SegmentMemory.SegmentPrototype proto = targetSegment.getSegmentPrototype(); + + return proto.hasBiLinearNode() && + sourceSegment.getTipNode().getId() == proto.getBiLinearSecondInputId(); + } + + /** + * Routes tuples to BiLinearJoinNode's right memory (staged right tuples) + * instead of the normal left memory staging. + */ + public static void routeToBiLinearRightMemory(SegmentMemory targetSegment, TupleSets leftTuples) { + SegmentMemory.SegmentPrototype proto = targetSegment.getSegmentPrototype(); + int biLinearIndex = proto.getBiLinearNodeIndex(); + + Memory[] nodeMemories = targetSegment.getNodeMemories(); + BetaMemory bm = (BetaMemory) nodeMemories[biLinearIndex]; + + if (bm.getStagedRightTuples().isEmpty()) { + bm.setNodeDirtyWithoutNotify(); + } + + bm.getStagedRightTuples().addAll(leftTuples); + + targetSegment.notifyRuleLinkSegment(); + + List<PathMemory> pathMems = targetSegment.getPathMemories(); + for (PathMemory pmem : pathMems) { + pmem.getOrCreateRuleAgendaItem(); + pmem.queueRuleAgendaItem(); + } + } + + /** + * Routes a single peer tuple to BiLinearJoinNode's right memory. + * Used when processing peer inserts for segments that need BiLinear right memory routing. + */ + public static void routePeerToBiLinearRightMemory(SegmentMemory targetSegment, TupleImpl peerTuple) { + SegmentMemory.SegmentPrototype proto = targetSegment.getSegmentPrototype(); + int biLinearIndex = proto.getBiLinearNodeIndex(); + + Memory[] nodeMemories = targetSegment.getNodeMemories(); + BetaMemory bm = (BetaMemory) nodeMemories[biLinearIndex]; Review Comment: The routeToBiLinearRightMemory and routePeerToBiLinearRightMemory methods don't validate that biLinearIndex is within bounds before accessing nodeMemories array. If getBiLinearNodeIndex() returns -1 (indicating no BiLinear node) or an invalid index, this will cause ArrayIndexOutOfBoundsException. Add bounds checking before array access. ########## drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/BiLinearTest.java: ########## @@ -0,0 +1,836 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.mvel.integrationtests; + +import org.drools.base.common.NetworkNode; +import org.drools.mvel.integrationtests.phreak.A; +import org.drools.mvel.integrationtests.phreak.B; +import org.drools.mvel.integrationtests.phreak.C; +import org.drools.mvel.integrationtests.phreak.D; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kie.api.KieBase; +import org.kie.api.io.ResourceType; +import org.kie.api.runtime.KieSession; +import org.kie.internal.utils.KieHelper; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BiLinearTest { + + @BeforeEach + public void setUp() { + System.setProperty("drools.bilinear.enabled", "true"); + } + + @AfterEach + public void cleanup() { + System.clearProperty("drools.bilinear.enabled"); + } + + @Test + public void testBiLinear() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearSwapOrder() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + + @Test + public void testBiLinear3RuleSetup() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinear3RuleSetupShort() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinear3RuleSetup2() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 2); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinearSayAAAAAAAAA() { + System.out.println("\nš Testing BiLinear functionality..."); + + System.setProperty("drools.bilinear.enabled", "false"); Review Comment: The test testBiLinearSayAAAAAAAAA sets the system property "drools.bilinear.enabled" to "false" at line 350, but the setUp() method in the test class sets it to "true" for all tests. This test-specific override could cause issues if tests run in parallel or if the cleanup doesn't properly restore state. Consider using a dedicated test method with @AfterEach to restore the property, or use a different approach for disabling BiLinear in specific tests. ########## drools-core/src/main/java/org/drools/core/reteoo/TupleImpl.java: ########## @@ -671,6 +671,10 @@ public int getIndex() { return this.index; } + protected void setIndex(int index) { + this.index = index; + } Review Comment: The setIndex method was added as protected, breaking encapsulation of the index field. This could allow subclasses to set inconsistent index values. Consider whether this is necessary or if there's a safer way to initialize the index for BiLinear tuples. ########## drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/BiLinearTest.java: ########## @@ -0,0 +1,836 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.mvel.integrationtests; + +import org.drools.base.common.NetworkNode; +import org.drools.mvel.integrationtests.phreak.A; +import org.drools.mvel.integrationtests.phreak.B; +import org.drools.mvel.integrationtests.phreak.C; +import org.drools.mvel.integrationtests.phreak.D; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kie.api.KieBase; +import org.kie.api.io.ResourceType; +import org.kie.api.runtime.KieSession; +import org.kie.internal.utils.KieHelper; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BiLinearTest { + + @BeforeEach + public void setUp() { + System.setProperty("drools.bilinear.enabled", "true"); + } + + @AfterEach + public void cleanup() { + System.clearProperty("drools.bilinear.enabled"); + } + + @Test + public void testBiLinear() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearSwapOrder() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + + @Test + public void testBiLinear3RuleSetup() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinear3RuleSetupShort() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinear3RuleSetup2() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 2); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinearSayAAAAAAAAA() { Review Comment: The testBiLinearSayAAAAAAAAA test name contains repeated 'A's which appears to be a debugging artifact or placeholder name. Consider renaming to something more descriptive like 'testBiLinearDisabledWithMultipleSameTypePatterns'. ########## drools-core/src/test/java/org/drools/core/reteoo/MockTupleSource.java: ########## @@ -69,4 +71,13 @@ public ObjectTypeNode getObjectTypeNode() { public boolean isLeftTupleMemoryEnabled() { return true; } + + public void setObjectCount(int objectCount) { + this.objectCount = objectCount; + } Review Comment: The setObjectCount method is added to MockTupleSource but there's no corresponding getter override documentation. Consider adding a JavaDoc comment explaining when and why this method should be called in mock scenarios. ########## drools-core/src/main/java/org/drools/core/reteoo/BiLinearJoinNode.java: ########## @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.core.reteoo; + +import org.drools.base.reteoo.NodeTypeEnums; +import org.drools.core.common.BetaConstraints; +import org.drools.core.common.BiLinearBetaConstraints; +import org.drools.core.reteoo.builder.BuildContext; +import org.drools.core.util.FastIterator; + +public class BiLinearJoinNode extends JoinNode { + + private static final long serialVersionUID = 510l; + + // Cross-network declaration context for variable resolution + protected BiLinearDeclarationContext declarationContext; + + public BiLinearJoinNode() { + } + + public BiLinearJoinNode(final int id, + final LeftTupleSource leftInput, + final BetaRightInput rightInput, + final BetaConstraints constraints, + final BuildContext context) { + super(id, leftInput, rightInput, createBiLinearConstraints(constraints, leftInput), context); + } + + @Override + public void doAttach(BuildContext context) { + super.doAttach(context); + } + + /** + * Override setPartitionId to handle LeftTupleSource as second input instead of ObjectSource. + * BiLinear has LeftTupleSource as second input, not ObjectSource, so we need special handling. + */ + @Override + public void setPartitionId(BuildContext context, org.drools.base.common.RuleBasePartitionId partitionId) { + LeftTupleSource secondInput = getSecondLeftInput(); + if (secondInput != null) { + org.drools.base.common.RuleBasePartitionId parentId = secondInput.getPartitionId(); + if (parentId != org.drools.base.common.RuleBasePartitionId.MAIN_PARTITION && !parentId.equals(partitionId)) { + this.partitionId = parentId; + rightInput.setPartitionId(context, this.partitionId); + context.setPartitionId(this.partitionId); + leftInput.setSourcePartitionId(context, this.partitionId); + return; + } + } + this.partitionId = partitionId; + } + + private static BetaConstraints createBiLinearConstraints(BetaConstraints originalConstraints, + LeftTupleSource leftInput) { + return new BiLinearBetaConstraints(originalConstraints); + } + + private BiLinearDeclarationContext createDeclarationContext(LeftTupleSource leftInput, + LeftTupleSource secondLeftInput) { + int secondNetworkOffset = leftInput != null ? leftInput.getObjectCount() : 0; + + return new BiLinearDeclarationContext( + leftInput, + secondLeftInput, + secondNetworkOffset + ); + } + + public LeftTupleSource getSecondLeftInput() { + return ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).getWrappedSecondLeftInput(); + } + + public int getFirstNetworkSize() { + return getLeftTupleSource().getObjectCount(); + } + + public void linkOutsideLeftInput(LeftTupleSource secondLeftInput) { + + ((org.drools.core.reteoo.builder.BiLinearLeftInputWrapper) rightInput).setWrappedSecondLeftInput(secondLeftInput); + + if (secondLeftInput != null && !secondLeftInput.equals(getLeftTupleSource())) { + secondLeftInput.addTupleSink(this); + } + + this.setObjectCount(leftInput.getObjectCount() + secondLeftInput.getObjectCount()); + + this.declarationContext = createDeclarationContext(getLeftTupleSource(), secondLeftInput); + getBiLinearConstraints().setDeclarationContext( + declarationContext + ); + } + + public BiLinearBetaConstraints getBiLinearConstraints() { + return (BiLinearBetaConstraints) getRawConstraints(); Review Comment: The getBiLinearConstraints method casts without checking if the constraints are actually BiLinearBetaConstraints. If getRawConstraints() returns a different type, this will throw a ClassCastException. Add a type check or document that this should only be called after proper initialization in the constructor. ```suggestion BetaConstraints rawConstraints = getRawConstraints(); if (rawConstraints instanceof BiLinearBetaConstraints) { return (BiLinearBetaConstraints) rawConstraints; } throw new IllegalStateException( "Expected BiLinearBetaConstraints but found " + (rawConstraints != null ? rawConstraints.getClass().getName() : "null") ); ``` ########## drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/BiLinearTest.java: ########## @@ -0,0 +1,836 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.drools.mvel.integrationtests; + +import org.drools.base.common.NetworkNode; +import org.drools.mvel.integrationtests.phreak.A; +import org.drools.mvel.integrationtests.phreak.B; +import org.drools.mvel.integrationtests.phreak.C; +import org.drools.mvel.integrationtests.phreak.D; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kie.api.KieBase; +import org.kie.api.io.ResourceType; +import org.kie.api.runtime.KieSession; +import org.kie.internal.utils.KieHelper; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BiLinearTest { + + @BeforeEach + public void setUp() { + System.setProperty("drools.bilinear.enabled", "true"); + } + + @AfterEach + public void cleanup() { + System.clearProperty("drools.bilinear.enabled"); + } + + @Test + public void testBiLinear() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearSwapOrder() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(2, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + + @Test + public void testBiLinear3RuleSetup() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinear3RuleSetupShort() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 1); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinear3RuleSetup2() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(3, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 2); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinearSayAAAAAAAAA() { + System.out.println("\nš Testing BiLinear functionality..."); + + System.setProperty("drools.bilinear.enabled", "false"); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : A()\n" + + " $d : A()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $b : A()\n" + + " $c : A()\n" + + " $d : A()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $a : A()\n" + + " $b : A()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertNoBiLinearNodes(visitor, kieBase); + + assertEquals(3, firedRules); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + @Test + public void testBiLinearShortNoBiLinear() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $b : B()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $c : B()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertNoBiLinearNodes(visitor, kieBase); + + assertEquals(2, firedRules); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearEqualRulesNoBiLinear() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $a : A()\n" + + " $c : B()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertNoBiLinearNodes(visitor, kieBase); + + assertEquals(2, firedRules); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearChain() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n" + + "\n" + + "rule \"Rule4\"\n" + + "when\n" + + " $d : D()\n" + + "then\n" + + " System.out.println(\"Rule4 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert(new A(5)); + session.insert(new B(10)); + session.insert(new C(10)); + session.insert(new D(10)); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertEquals(4, firedRules); + + assertBiLinearNodeCount(visitor, kieBase, 2); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + + @Test + public void testBiLinearNoNetworkShouldBeMade() { + System.out.println("\nš Testing BiLinear functionality..."); + + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $s1 : String()\n" + + " $s2 : String()\n" + + " eval( 1 == 1 )\n" + + "then\n" + + " System.out.println(\"Rule1 fired\" );\n" + + "end\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $s1 : String()\n" + + "then\n" + + " System.out.println(\"Rule2 fired\" );\n" + + "end\n" + + "\n" + + "\n" + + "rule \"Rule3\"\n" + + "when\n" + + " $s1 : String()\n" + + " $s2 : String()\n" + + " $s3 : String()\n" + + " eval( 1 == 1 )\n" + + "then\n" + + " System.out.println(\"Rule3 fired\" );\n" + + "end\n"; + + System.out.println("š§ Building KieBase with BiLinear enabled..."); + KieBase kieBase = buildKieBase(drl); + + System.out.println("š Network structure:"); + NetworkVisitor visitor = new NetworkVisitor(); + visitor.debugNetworkStructure(kieBase); + + System.out.println("\nš Testing execution..."); + KieSession session = kieBase.newKieSession(); + session.insert("test"); + + int firedRules = session.fireAllRules(); + System.out.println("š Rules fired: " + firedRules); + + assertNoBiLinearNodes(visitor, kieBase); + + assertEquals(3, firedRules); + + session.dispose(); + System.out.println("ā BiLinear test completed"); + } + + private KieBase buildKieBase(String drl) { + // Note: BiLinear property should be set by the calling test before calling this method + KieHelper kieHelper = new KieHelper(); + kieHelper.addContent(drl, ResourceType.DRL); + return kieHelper.build(); + } + + @Test + public void testBiLinearJoinNodesPresentInNetwork() { + String drl = + "import " + A.class.getCanonicalName() + "\n" + + "import " + B.class.getCanonicalName() + "\n" + + "import " + C.class.getCanonicalName() + "\n" + + "import " + D.class.getCanonicalName() + "\n" + + "\n" + + "rule \"Rule1\"\n" + + "when\n" + + " $a : A()\n" + + " $b : B()\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + "end\n" + + "\n" + + "rule \"Rule2\"\n" + + "when\n" + + " $c : C()\n" + + " $d : D()\n" + + "then\n" + + "end\n"; + + KieBase kieBase = buildKieBase(drl); + + NetworkVisitor visitor = new NetworkVisitor(); + assertBiLinearNodeCount(visitor, kieBase, 1); + + System.out.println("BiLinear network structure:"); + visitor.debugNetworkStructure(kieBase); + } + + private void assertBiLinearNodeCount(NetworkVisitor visitor, KieBase kieBase, int bilinearNodeCount) { + + List<NetworkNode> biLinearNodes = visitor.findBiLinearJoinNodes(kieBase); + + assertThat(biLinearNodes) + .as("BiLinear optimization should create BiLinearJoinNode(s)") + .isNotEmpty(); + + assertThat(biLinearNodes).hasSize(bilinearNodeCount); + } + + @Test + public void testNoBiLinearJoinNodesWhenDisabled() { + System.setProperty("drools.bilinear.enabled", "false"); Review Comment: Similar to the previous issue, testNoBiLinearJoinNodesWhenDisabled sets the property to "false" without ensuring it's restored. This could affect subsequent tests. The cleanup() method in @AfterEach clears all properties, but if this test fails before reaching cleanup, the property state could be corrupted for other tests. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
