Repository: microsoft/onnxscript
Pull request #2284: Returning choice values in patterns
Author: login: gramalingam
Created: 2025-05-07T23:57:08Z | Merged: 2025-05-09T16:21:54Z

A fix for the use of Choice values (i.e., `OrValue([choice1, choice2])` as a return-value in the pattern (that is, as a root of the pattern). This is a bit complicated to support with the current implementation, which is oriented towards iterating over the nodes in the graph, and matching them against pattern-nodes. This PR provides a limited extension (which handles the case in PR 2277): when the returned choice-values are already covered by the other output values, they can be supported by the existing matcher.

Also, on a related note, fix how value-bindings are handled in the pattern-matcher to make it easier to return these values as the outputs of the pattern.


Diff:
diff --git a/.gitignore b/.gitignore
index 23ce89a464..3344aa7659 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ test-output.xml
 
 # Sphinx documentation
 docs/_build/
+docs/sg_execution_times.rst
 
 # Jupyter Notebook
 .ipynb_checkpoints
diff --git a/docs/ir/tensors.md b/docs/ir/tensors.md
index 7b46ac2094..4e1130ba3b 100644
--- a/docs/ir/tensors.md
+++ b/docs/ir/tensors.md
@@ -188,7 +188,7 @@ To fully support arrays from other frameworks, it is usually a good idea to crea
 
 ```{eval-rst}
 .. exec_code::
-
+    from __future__ import annotations
     import ctypes
     from typing import Any
 
diff --git a/docs/tutorial/rewriter/attributes.md b/docs/tutorial/rewriter/attributes.md
new file mode 100644
index 0000000000..12f1834241
--- /dev/null
+++ b/docs/tutorial/rewriter/attributes.md
@@ -0,0 +1,22 @@
+# Specifying attributes in the pattern
+
+This section demonstrates the use of attribute values in pattern-based rewriting.
+First, write a target pattern and replacement pattern in a similar way to the previous examples.
+The example pattern below will match successfully only against Dropout nodes with the
+attribute value `training_mode` set to `False`.
+The `_allow_other_attributes` option allows the pattern to match nodes that have additional attributes
+not specified in the pattern. If it is set to `False`, then the node must have only the specified
+attribute values, and no other attributes, for a successful match. The default value for this
+option is `True`.
+
+```{literalinclude} examples/allow_other_attributes.py
+:pyobject: add_pattern
+```
+
+```{literalinclude} examples/allow_other_attributes.py
+:pyobject: add_replacement
+```
+
+```{literalinclude} examples/allow_other_attributes.py
+:pyobject: apply_rewrite
+```
diff --git a/docs/tutorial/rewriter/commute.md b/docs/tutorial/rewriter/commute.md
new file mode 100644
index 0000000000..38b4b178aa
--- /dev/null
+++ b/docs/tutorial/rewriter/commute.md
@@ -0,0 +1,71 @@
+(heading-target-commute)=
+# Utilizing `commute` parameter for pattern-matching
+Extending the previous [simple example](heading-target-simple), assumming a scenario where we have a graph with the following structure.
+
+![commute](examples/img/erfgelu_03_commute.png){align=center width=500px}
+
+In this graph, there exist two node pattern that constitute a `GELU` op. However, there is a subtle difference between the two. Focusing on the parent `Mul` nodes in either patterns, the order of the input values being multiplied is switched.
+
+![gelu_pattern_1](examples/img/erfgelu_04_commute.png){width=330px align=left} ![gelu_pattern_2](examples/img/erfgelu_05_commute.png){width=330px align=center}
+
+
+If we utilize the same `target_pattern` created for the earlier [simple example](heading-target-simple) (shown below), only one of two `GELU` pattern will be matched.
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: erf_gelu_pattern
+```
+
+```{image} examples/img/erfgelu_06_commute.png
+:alt: The resulting graph after matching.
+:width: 400px
+:align: center
+```
+
+Only one of the patterns has been successfully matched and replaced by a `GELU` node. In order to rewrite both the existing patterns in the graph, there are two methods.
+
+(heading-target-commute-ruleset)=
+
+## 1. Creating a rule-set with different patterns.
+
+This method requires creating two separate rules and packing them into either a sequence of `PatternRewriteRule`s or a `RewriteRuleSet`. Creating a `RewriteRuleSet` is the preferable option but either can be used. In order to create a `RewriteRuleSet` with multiple rules `rule1` and `rule2` for example:
+
+```python
+from onnxscript.rewriter import pattern
+rewrite_rule_set = pattern.RewriteRuleSet(rules=[rule1, rule2])
+```
+
+In order to apply this method to the example above, first create the two separate target patterns as follows:
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: erf_gelu_pattern
+```
+```{literalinclude} examples/erfgelu.py
+:pyobject: erf_gelu_pattern_2
+```
+
+:::{note}
+:name: rule-application-order-matters
+
+When you pass multiple rules in `pattern_rewrite_rules`, the **order in which they appear is important**.
+This is because some rules may depend on patterns created or modified by earlier rules. For example, if `rule2` can only match after `rule1` has made a specific change in the model, then `rule1` must come **before** `rule2` in the list.
+If you're not seeing expected results, try adjusting the order or applying the rule set in a loop until no more changes occur.
+:::
+
+
+Then, create two separate `PatternRewriteRule`s, one for each target pattern. Pack these rules into a `RewriteRuleSet` object and apply rewrites by passing the created `RewriteRuleSet` for the `pattern_rewrite_rules` parameter.
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: apply_rewrite_with_ruleset
+```
+
+## 2. Using the `commute` parameter while creating a rule.
+
+Creating multiple target patterns for similar patterns can be tedious. In order to avoid this, the `commute` parameter can be utilized while creating the `RewriteRuleSet`. Simply set `commute=True` in order to avoid creating multiple target pattern for cases where patterns are different due to commutativity. Multiple rules with the different patterns emerging due to satisfying the commutativity property are automatically packed into a `RewriteRuleSet` object. Then apply rewrites by passing the created `RewriteRuleSet` for the `pattern_rewrite_rules` parameter.
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: apply_rewrite_with_commute
+```
+
+For the both of the aforementioned methods, the final graph with both rewrites applied should look as follows:
+
+![commute](examples/img/erfgelu_07_commute.png){align=center width=300px}
diff --git a/docs/tutorial/rewriter/conditional_rewrite.md b/docs/tutorial/rewriter/conditional_rewrite.md
new file mode 100644
index 0000000000..07dc7793c9
--- /dev/null
+++ b/docs/tutorial/rewriter/conditional_rewrite.md
@@ -0,0 +1,49 @@
+# Using the `match_condition` parameter for pattern-matching
+
+This section talks about how to utilize the `match_condition` parameter. The `match_condition` parameter checks if the pattern matches the target pattern with certain constraints in consideration.
+
+Let us consider a model which consists of the following pattern.
+
+![target_pattern](examples/img/broadcast_01.png){align=center}
+
+Based on the [ONNX Matmul spec](https://github.com/onnx/onnx/blob/main/docs/Operators.md#MatMul), onnx `Matmul` behaves like `numpy.matmul` and also follows numpy broadcasting. So in this particular pattern if matmul broadcasting is enough, then we don't need the reshapes. To validate this, we need to check the following:
+
+1. Input shapes check: `input_a` and `input_b` should be broadcastable
+2. Output shape check: `shape_c` should be the same as the output shape from the `matmul(input_a, input_b)`
+
+If the above are true, then we don't need the reshapes and we can eliminate them using a pattern based rewrite.
+
+First, write a target pattern and replacement pattern in a similar way to the first example.
+
+```{literalinclude} examples/broadcast_matmul.py
+:pyobject: two_reshapes_matmul_reshape_pattern
+```
+
+```{literalinclude} examples/broadcast_matmul.py
+:pyobject: matmul_pattern
+```
+
+:::{note}
+:name: omitting inputs in signature
+
+The target pattern in this case has 5 inputs `input_a`, `input_b`, `shape_a`, `shape_b`, `shape_c`. However, the replacement pattern only utilizes `input_a` and `input_b`. To avoid referencing all the unused parameters in the replacement pattern signature, pass only `input_a` and `input_b` and use `**_` to represent all the unused parameters.
+
+Similarly for writing the condition checking function, we require only `input_a`, `input_b` and `shape_c`. Use `**_` to represent all the unused parameters in the condition matching function signature.
+:::
+
+In order to validate whether matmul broadcast is sufficient, we write a condition checking function as follows:
+
+```{literalinclude} examples/broadcast_matmul.py
+:pyobject: check_if_not_need_reshape
+```
+
+With all the necessary components in place, the pattern rewrite rule with the `match_condition` function is created and then the `rewriter.rewrite` is called to apply the rewrite.
+
+```{literalinclude} examples/broadcast_matmul.py
+:pyobject: apply_rewrite
+```
+
+The final graph with the applied rewrite looks as follows:
+
+![broadcast_rewrite](examples/img/broadcast_02.png){align=center}
+
diff --git a/docs/tutorial/rewriter/examples/or_pattern.py b/docs/tutorial/rewriter/examples/or_pattern.py
new file mode 100644
index 0000000000..0e9231cc1f
--- /dev/null
+++ b/docs/tutorial/rewriter/examples/or_pattern.py
@@ -0,0 +1,93 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+"""OR-patterns.
+
+This script shows how to define a rewriting rule based on OR-patterns.
+"""
+
+import onnx
+
+import onnxscript
+from onnxscript import FLOAT, opset18, script
+from onnxscript.rewriter import pattern
+
+####################################
+# The target pattern
+# =====================
+
+
+def scaled_matmul(op, x, y, factor):
+    xy = op.MatMul(x, y)
+    choice1 = op.Mul(xy, factor)
+    choice2 = op.Div(xy, factor)
+    scaled_xy = pattern.OrValue(
+        [choice1, choice2], tag_var="op_type", tag_values=["Mul", "Div"]
+    )
+    return op.Relu(scaled_xy)
+
+
+####################################
+# The replacement pattern
+# =====================
+
+
+def scaled_matmul_replacement(op, x, y, factor, op_type):
+    if op_type == "Mul":
+        return op.MatMulMulRelu(x, y, factor, _domain="some.domain")
+    elif op_type == "Div":
+        return op.MatMulDivRelu(x, y, factor, _domain="some.domain")
+    else:
+        raise ValueError(f"Unknown operation type: {op_type}")
+
+
+####################################
+# Rewrite Rule
+# =====================
+def apply_rewrite(model):
+    rule = pattern.RewriteRule(
+        scaled_matmul,  # target pattern
+        scaled_matmul_replacement,  # replacement pattern
+    )
+    # Create a Rewrite Rule Set
+    rewrite_rule_set = pattern.RewriteRuleSet([rule])
+    return onnxscript.rewriter.rewrite(
+        model,
+        pattern_rewrite_rules=rewrite_rule_set,
+    )
+
+
+@script()
+def original_model1(A: FLOAT[2, 2], B: FLOAT[2, 2]) -> FLOAT[2, 2]:
+    t1 = opset18.MatMul(A, B)
+    c = opset18.Constant(value_float=2.0)
+    t2 = opset18.Mul(t1, c)
+    t3 = opset18.Relu(t2)
+    return t3
+
+
+_model = original_model1.to_model_proto()
+onnx.checker.check_model(_model)
+
+_model_with_rewrite = apply_rewrite(_model)
+onnx.checker.check_model(_model_with_rewrite)
+
+assert [n.op_type for n in _model_with_rewrite.graph.node] == ["Constant", "MatMulMulRelu"]
+
+
+@script()
+def original_model2(A: FLOAT[2, 2], B: FLOAT[2, 2]) -> FLOAT[2, 2]:
+    t1 = opset18.MatMul(A, B)
+    c = opset18.Constant(value_float=2.0)
+    t2 = opset18.Div(t1, c)
+    t3 = opset18.Relu(t2)
+    return t3
+
+
+_model = original_model2.to_model_proto()
+onnx.checker.check_model(_model)
+
+_model_with_rewrite = apply_rewrite(_model)
+onnx.checker.check_model(_model_with_rewrite)
+
+assert [n.op_type for n in _model_with_rewrite.graph.node] == ["Constant", "MatMulDivRelu"]
diff --git a/docs/tutorial/rewriter/index.md b/docs/tutorial/rewriter/index.md
index 3b4e01e149..d86ae9a474 100644
--- a/docs/tutorial/rewriter/index.md
+++ b/docs/tutorial/rewriter/index.md
@@ -1,4 +1,4 @@
-# Rewriter Tutorials
+# Rewriter Tutorial
 
 ```{toctree}
 rewrite_patterns
diff --git a/docs/tutorial/rewriter/or_pattern.md b/docs/tutorial/rewriter/or_pattern.md
new file mode 100644
index 0000000000..6c42112467
--- /dev/null
+++ b/docs/tutorial/rewriter/or_pattern.md
@@ -0,0 +1,20 @@
+# OR Patterns
+
+*Note* : This feature is work-in-progress.
+
+Consider the following pattern:
+
+```{literalinclude} examples/or_pattern.py
+:pyobject: scaled_matmul
+```
+
+This pattern will successfully match against the sequence "MatMul => Mul => Relu" as
+well as the sequence "MatMul => Div => Relu". The matcher will bind the variable
+specified in `tag_var` (`op_type` in the above example) to a value from those
+listed in `tag_values` to indicate which of the alternatives was used for a
+successful match. We can use this in the rewrite function to determine how
+we want to rewrite the matched sub-graph, as illustrated by the following code:
+
+```{literalinclude} examples/or_pattern.py
+:pyobject: scaled_matmul_replacement
+```
diff --git a/docs/tutorial/rewriter/rewrite_patterns.md b/docs/tutorial/rewriter/rewrite_patterns.md
index d84d6b0f40..9627dc9a39 100644
--- a/docs/tutorial/rewriter/rewrite_patterns.md
+++ b/docs/tutorial/rewriter/rewrite_patterns.md
@@ -1,10 +1,8 @@
-# Pattern-based Rewrite Using Rules
-
-## Introduction
+# Introduction
 
 The ONNX Rewriter tool provides the user with the functionality to replace certain patterns in an ONNX graph with another pattern based on rewrite rules provided by the user.
 
-## Usage
+# Usage
 
 There are three main components needed when rewriting patterns in the graph:
 
@@ -12,220 +10,17 @@ There are three main components needed when rewriting patterns in the graph:
 2. `replacement_pattern` : Pattern to replace the original pattern with. This pattern is also written as a function using ONNXScript-like operators.
 3. `match_condition` (optional) : Pattern rewrite will occur only if the match condition is satisfied.
 
-(heading-target-simple)=
-## A Simple Example
-
-An simple example demonstrating the usage of this functionality using the `GELU` activation function:
-
-`GELU` activation function can be computed using a Gauss Error Function using the given formula:
-
-```{math}
-\text{GELU} = x\Phi(x) = x \cdot \frac{1}{2} [1 + \text{erf}(x / \sqrt{2})]
-```
-
-We will show how we can find a subgraph matching this computation and replace it by a call to the function.
-
-Firstly, include all the rewriter relevant imports.
-
-```python
-from onnxscript.rewriter import pattern
-from onnxscript import ir
-
-```
-
-Then create a target pattern that needs to be replaced using onnxscript operators.
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: erf_gelu_pattern
-```
-
-After this, create a replacement pattern that consists of the GELU onnxscript operator.
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: gelu
-```
-:::{note}
-:name: type annotate ir.Value
-
-The inputs to the replacement pattern are of type `ir.Value`. For detailed usage of `ir.Value` refer to the {py:class}`ir.Value <onnxscript.ir._core.Value>` class.
-:::
-
-
-For this example, we do not require a `match_condition` so that option is skipped for now. Then the rewrite rule is created using the `RewriteRule` function.
-
-```python
-rule = pattern.RewriteRule(
-    erf_gelu_pattern,  # Target Pattern
-    gelu,  # Replacement Pattern
-)
-```
-
-Now that the rewrite rule has been created, the next step is to apply these pattern-based rewrite rules. The `rewriter.rewrite` call consists of three main components:
-
-1. `model` : The original model on which the pattern rewrite rules are to be applied. This is of type `onnx.ModelProto`.
-2. `function_rewrite_rules` : `(Optional)` This parameter is used to pass rewrite rules based on function names. Steps on how to use this parameter will be covered in a different tutorial. This parameter is of type `Sequence[type[FunctionRewriteRule]]`
-3. `pattern_rewrite_rules` : `(Optional)` This parameter is used to pass rewrite rules based on a provided replacement pattern. For the purpose of this tutorial, we will be using only this parameter in conjunction with `model`. This parameter is of either one of these types:
-    - `Sequence[PatternRewriteRule]`
-    - `RewriteRuleSet`
-
-:::{note}
-:name: pattern_rewrite_rules input formatting
-
-`pattern_rewrite_rules` takes a sequence of `PatternRewriteRule` types or a RewriteRuleSet which is also essentially a rule set created using a sequence of `PatternRewriteRule` types, so if only a singular rewrite rule is to be passed, it needs to passed as part of a sequence. For steps on how to create and use Rule-sets, refer to the example in the section [Creating a rule-set with different patterns](#heading-target-commute-ruleset).
-:::
-
-The snippet below below demonstrates how to use the `rewriter.rewrite` call for the rewrite rule created above:
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: apply_rewrite
-```
-
-The graph (on the left) consists of the target pattern before the rewrite rule is applied. Once the rewrite rule is applied, the graph (on the right) shows that the target pattern has been successfully replaced by a GELU node as intended.
-
-![target_pattern](examples/img/erfgelu_01.png) ![replacement_pattern](examples/img/erfgelu_02.png)
-
-## Specifying attributes in the pattern
-
-This section demonstrates the use of attribute values in pattern-based rewriting.
-First, write a target pattern and replacement pattern in a similar way to the previous examples.
-The example pattern below will match successfully only against Dropout nodes with the
-attribute value `training_mode` set to `False`.
-The `_allow_other_attributes` option allows the pattern to match nodes that have additional attributes
-not specified in the pattern. If it is set to `False`, then the node must have only the specified
-attribute values, and no other attributes, for a successful match. The default value for this
-option is `True`.
-
-```{literalinclude} examples/allow_other_attributes.py
-:pyobject: add_pattern
-```
-
-```{literalinclude} examples/allow_other_attributes.py
-:pyobject: add_replacement
-```
-
-```{literalinclude} examples/allow_other_attributes.py
-:pyobject: apply_rewrite
-```
-
-
-(heading-target-commute)=
-## Utilizing `commute` parameter for pattern-matching
-Extending the previous [simple example](heading-target-simple), assumming a scenario where we have a graph with the following structure.
-
-![commute](examples/img/erfgelu_03_commute.png){align=center width=500px}
-
-In this graph, there exist two node pattern that constitute a `GELU` op. However, there is a subtle difference between the two. Focusing on the parent `Mul` nodes in either patterns, the order of the input values being multiplied is switched.
-
-![gelu_pattern_1](examples/img/erfgelu_04_commute.png){width=330px align=left} ![gelu_pattern_2](examples/img/erfgelu_05_commute.png){width=330px align=center}
-
-
-If we utilize the same `target_pattern` created for the earlier [simple example](heading-target-simple) (shown below), only one of two `GELU` pattern will be matched.
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: erf_gelu_pattern
+```{include} simple_example.md
 ```
 
-```{image} examples/img/erfgelu_06_commute.png
-:alt: The resulting graph after matching.
-:width: 400px
-:align: center
+```{include} attributes.md
 ```
 
-Only one of the patterns has been successfully matched and replaced by a `GELU` node. In order to rewrite both the existing patterns in the graph, there are two methods.
-
-(heading-target-commute-ruleset)=
-### 1. Creating a rule-set with different patterns.
-
-This method requires creating two separate rules and packing them into either a sequence of `PatternRewriteRule`s or a `RewriteRuleSet`. Creating a `RewriteRuleSet` is the preferable option but either can be used. In order to create a `RewriteRuleSet` with multiple rules `rule1` and `rule2` for example:
-
-```python
-from onnxscript.rewriter import pattern
-rewrite_rule_set = pattern.RewriteRuleSet(rules=[rule1, rule2])
+```{include} conditional_rewrite.md
 ```
 
-In order to apply this method to the example above, first create the two separate target patterns as follows:
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: erf_gelu_pattern
-```
-```{literalinclude} examples/erfgelu.py
-:pyobject: erf_gelu_pattern_2
-```
-
-:::{note}
-:name: rule-application-order-matters
-
-When you pass multiple rules in `pattern_rewrite_rules`, the **order in which they appear is important**.
-This is because some rules may depend on patterns created or modified by earlier rules. For example, if `rule2` can only match after `rule1` has made a specific change in the model, then `rule1` must come **before** `rule2` in the list.
-If you're not seeing expected results, try adjusting the order or applying the rule set in a loop until no more changes occur.
-:::
-
-
-Then, create two separate `PatternRewriteRule`s, one for each target pattern. Pack these rules into a `RewriteRuleSet` object and apply rewrites by passing the created `RewriteRuleSet` for the `pattern_rewrite_rules` parameter.
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: apply_rewrite_with_ruleset
+```{include} or_pattern.md
 ```
 
-
-### 2. Using the `commute` parameter while creating a rule.
-
-Creating multiple target patterns for similar patterns can be tedious. In order to avoid this, the `commute` parameter can be utilized while creating the `RewriteRuleSet`. Simply set `commute=True` in order to avoid creating multiple target pattern for cases where patterns are different due to commutativity. Multiple rules with the different patterns emerging due to satisfying the commutativity property are automatically packed into a `RewriteRuleSet` object. Then apply rewrites by passing the created `RewriteRuleSet` for the `pattern_rewrite_rules` parameter.
-
-```{literalinclude} examples/erfgelu.py
-:pyobject: apply_rewrite_with_commute
+```{include} commute.md
 ```
-
-For the both of the aforementioned methods, the final graph with both rewrites applied should look as follows:
-
-![commute](examples/img/erfgelu_07_commute.png){align=center width=300px}
-
-## Using the `match_condition` parameter for pattern-matching
-
-This section talks about how to utilize the `match_condition` parameter. The `match_condition` parameter checks if the pattern matches the target pattern with certain constraints in consideration.
-
-Let us consider a model which consists of the following pattern.
-
-![target_pattern](examples/img/broadcast_01.png){align=center}
-
-Based on the [ONNX Matmul spec](https://github.com/onnx/onnx/blob/main/docs/Operators.md#MatMul), onnx `Matmul` behaves like `numpy.matmul` and also follows numpy broadcasting. So in this particular pattern if matmul broadcasting is enough, then we don't need the reshapes. To validate this, we need to check the following:
-
-1. Input shapes check: `input_a` and `input_b` should be broadcastable
-2. Output shape check: `shape_c` should be the same as the output shape from the `matmul(input_a, input_b)`
-
-If the above are true, then we don't need the reshapes and we can eliminate them using a pattern based rewrite.
-
-First, write a target pattern and replacement pattern in a similar way to the first example.
-
-```{literalinclude} examples/broadcast_matmul.py
-:pyobject: two_reshapes_matmul_reshape_pattern
-```
-
-```{literalinclude} examples/broadcast_matmul.py
-:pyobject: matmul_pattern
-```
-
-:::{note}
-:name: omitting inputs in signature
-
-The target pattern in this case has 5 inputs `input_a`, `input_b`, `shape_a`, `shape_b`, `shape_c`. However, the replacement pattern only utilizes `input_a` and `input_b`. To avoid referencing all the unused parameters in the replacement pattern signature, pass only `input_a` and `input_b` and use `**_` to represent all the unused parameters.
-
-Similarly for writing the condition checking function, we require only `input_a`, `input_b` and `shape_c`. Use `**_` to represent all the unused parameters in the condition matching function signature.
-:::
-
-In order to validate whether matmul broadcast is sufficient, we write a condition checking function as follows:
-
-```{literalinclude} examples/broadcast_matmul.py
-:pyobject: check_if_not_need_reshape
-```
-
-With all the necessary components in place, the pattern rewrite rule with the `match_condition` function is created and then the `rewriter.rewrite` is called to apply the rewrite.
-
-```{literalinclude} examples/broadcast_matmul.py
-:pyobject: apply_rewrite
-```
-
-The final graph with the applied rewrite looks as follows:
-
-![broadcast_rewrite](examples/img/broadcast_02.png){align=center}
-
diff --git a/docs/tutorial/rewriter/simple_example.md b/docs/tutorial/rewriter/simple_example.md
new file mode 100644
index 0000000000..942f0ad48f
--- /dev/null
+++ b/docs/tutorial/rewriter/simple_example.md
@@ -0,0 +1,71 @@
+(heading-target-simple)=
+# A Simple Example
+
+An simple example demonstrating the usage of this functionality using the `GELU` activation function:
+
+`GELU` activation function can be computed using a Gauss Error Function using the given formula:
+
+```{math}
+\text{GELU} = x\Phi(x) = x \cdot \frac{1}{2} [1 + \text{erf}(x / \sqrt{2})]
+```
+
+We will show how we can find a subgraph matching this computation and replace it by a call to the function.
+
+Firstly, include all the rewriter relevant imports.
+
+```python
+from onnxscript.rewriter import pattern
+from onnxscript import ir
+
+```
+
+Then create a target pattern that needs to be replaced using onnxscript operators.
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: erf_gelu_pattern
+```
+
+After this, create a replacement pattern that consists of the GELU onnxscript operator.
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: gelu
+```
+:::{note}
+:name: type annotate ir.Value
+
+The inputs to the replacement pattern are of type `ir.Value`. For detailed usage of `ir.Value` refer to the {py:class}`ir.Value <onnxscript.ir._core.Value>` class.
+:::
+
+
+For this example, we do not require a `match_condition` so that option is skipped for now. Then the rewrite rule is created using the `RewriteRule` function.
+
+```python
+rule = pattern.RewriteRule(
+    erf_gelu_pattern,  # Target Pattern
+    gelu,  # Replacement Pattern
+)
+```
+
+Now that the rewrite rule has been created, the next step is to apply these pattern-based rewrite rules. The `rewriter.rewrite` call consists of three main components:
+
+1. `model` : The original model on which the pattern rewrite rules are to be applied. This is of type `onnx.ModelProto`.
+2. `function_rewrite_rules` : `(Optional)` This parameter is used to pass rewrite rules based on function names. Steps on how to use this parameter will be covered in a different tutorial. This parameter is of type `Sequence[type[FunctionRewriteRule]]`
+3. `pattern_rewrite_rules` : `(Optional)` This parameter is used to pass rewrite rules based on a provided replacement pattern. For the purpose of this tutorial, we will be using only this parameter in conjunction with `model`. This parameter is of either one of these types:
+    - `Sequence[PatternRewriteRule]`
+    - `RewriteRuleSet`
+
+:::{note}
+:name: pattern_rewrite_rules input formatting
+
+`pattern_rewrite_rules` takes a sequence of `PatternRewriteRule` types or a RewriteRuleSet which is also essentially a rule set created using a sequence of `PatternRewriteRule` types, so if only a singular rewrite rule is to be passed, it needs to passed as part of a sequence. For steps on how to create and use Rule-sets, refer to the example in the section [Creating a rule-set with different patterns](#heading-target-commute-ruleset).
+:::
+
+The snippet below below demonstrates how to use the `rewriter.rewrite` call for the rewrite rule created above:
+
+```{literalinclude} examples/erfgelu.py
+:pyobject: apply_rewrite
+```
+
+The graph (on the left) consists of the target pattern before the rewrite rule is applied. Once the rewrite rule is applied, the graph (on the right) shows that the target pattern has been successfully replaced by a GELU node as intended.
+
+![target_pattern](examples/img/erfgelu_01.png) ![replacement_pattern](examples/img/erfgelu_02.png)
diff --git a/onnxscript/rewriter/pattern.py b/onnxscript/rewriter/pattern.py
index 4815e0a2b4..b78ba367ea 100644
--- a/onnxscript/rewriter/pattern.py
+++ b/onnxscript/rewriter/pattern.py
@@ -379,6 +379,26 @@ def add_node(self, node: ir.Node) -> None:
         """Adds a node to the list of matched nodes."""
         self._current_match.add_node(node)
 
+    def bind_value(self, pattern_value: ValuePattern, value: Any) -> bool:
+        var_name = pattern_value.name
+        # TODO(rama): Simplify the following. We currently bind values to
+        # pattern variables in two different ways: via their name, or via the
+        # pattern-value itself.
+        if var_name is None:
+            for match in self._partial_matches:
+                if pattern_value in match.value_bindings:
+                    # TODO(rama): Use appropriate equality-check here.
+                    if match.value_bindings[pattern_value] == value:
+                        return True
+                    self._current_match.fail(
+                        f"Binding failure: {pattern_value} bound to two different values.",
+                        [match.value_bindings[pattern_value], value],
+                    )
+                    return False
+            self._current_match.value_bindings[pattern_value] = value
+            return True
+        return self.bind(var_name, value)
+
     def bind(self, var: str, value: Any) -> bool:
         for match in self._partial_matches:
             if var in match.bindings:
@@ -400,6 +420,13 @@ def bindings(self) -> dict[str, Any]:
             raise ValueError("Bindings can be accessed only at the top-level match.")
         return self._current_match.bindings
 
+    @property
+    def value_bindings(self) -> dict[ValuePattern, ir.Value]:
+        """Returns the bindings for the value variables."""
+        if len(self._partial_matches) > 1:
+            raise ValueError("Value bindings can be accessed only at the top-level match.")
+        return self._current_match.value_bindings
+
     @property
     def outputs(self) -> MutableSequence[ir.Value]:
         """Returns the list of output values that matched the pattern."""
@@ -437,7 +464,9 @@ def __init__(self) -> None:
         # For a successful match, bindings is a dictionary of mapping pattern-variable-names
         # to values.
         self._bindings: dict[str, Any] = {}
+        self._value_bindings: dict[ValuePattern, ir.Value] = {}
         self._node_bindings: dict[NodePattern, ir.Node] = {}
+
         self._outputs: list[ir.Value] = []
         # For a failed match, _reason is a string that describes the reason for the failure.
         self._reason: str = ""
@@ -472,24 +501,14 @@ def add_node(self, node: ir.Node) -> None:
         """Adds a node to the list of matched nodes."""
         self._matched_nodes.append(node)
 
-    def bind(self, var: str, value: Any) -> bool:
-        """Binds a pattern variable name to a value from the matched IR.
-
-        Returns True if the binding is successful, False otherwise (when the binding is inconsistent).
-        """
-        if var in self._bindings:
-            # TODO(rama): Use appropriate equality-check here.
-            if self._bindings[var] == value:
-                return True
-            self._success = False
-            return False
-        self._bindings[var] = value
-        return True
-
     @property
     def bindings(self) -> dict[str, Any]:
         return self._bindings
 
+    @property
+    def value_bindings(self) -> dict[ValuePattern, ir.Value]:
+        return self._value_bindings
+
     @property
     def outputs(self) -> MutableSequence[ir.Value]:
         return self._outputs
@@ -954,7 +973,11 @@ def visit(value_patterns: Sequence[ValuePattern | None]) -> None:
     return node_patterns
 
 
-def _add_backward_slice(node: NodePattern, backward_slice: set[NodePattern]) -> None:
+def _add_backward_slice(
+    node: NodePattern,
+    backward_slice: set[NodePattern],
+    backward_slice_values: set[ValuePattern],
+) -> None:
     """Adds all nodes in the backward slice of given node to the set `backward_slice`.
 
     The backward slice of a node is the set of all nodes that are reachable from the node
@@ -965,7 +988,11 @@ def _add_backward_slice(node: NodePattern, backward_slice: set[NodePattern]) ->
     backward_slice.add(node)
     for value_pattern in node.inputs:
         if isinstance(value_pattern, NodeOutputPattern):
-            _add_backward_slice(value_pattern.producer(), backward_slice)
+            _add_backward_slice(
+                value_pattern.producer(), backward_slice, backward_slice_values
+            )
+        elif isinstance(value_pattern, (_OpIdDispatchOr, _BacktrackingOr)):
+            backward_slice_values.add(value_pattern)
 
 
 class GraphPattern:
@@ -987,20 +1014,26 @@ def __init__(
         # whose backward-slices cover the entire pattern.
         output_nodes: set[NodePattern] = set()
         covered: set[NodePattern] = set()
+        choice_values_returned: set[ValuePattern] = set()
+        covered_choice_values: set[ValuePattern] = set()
         for value_pattern in outputs:
             if not isinstance(value_pattern, ValuePattern):
                 raise TypeError(
                     f"Invalid type {type(value_pattern)} for graph pattern output."
                 )
-            if isinstance(value_pattern, Constant):
-                raise NotImplementedError(
-                    "Constant values are not allowed as graph pattern outputs."
-                )
             if isinstance(value_pattern, NodeOutputPattern):
                 candidate = value_pattern.producer()
                 if candidate not in covered:
                     output_nodes.add(candidate)
-                    _add_backward_slice(candidate, covered)
+                    _add_backward_slice(candidate, covered, covered_choice_values)
+            elif isinstance(value_pattern, (_OpIdDispatchOr, _BacktrackingOr)):
+                choice_values_returned.add(value_pattern)
+
+        # check if all choice_values_returned are contained in covered_choice_values:
+        # We don't yet support the use of a choice-value as a "root" of the search.
+        # This is a limitation of the current implementation, and will be fixed in the future.
+        if not (choice_values_returned <= covered_choice_values):
+            raise NotImplementedError("Returning uncovered choice-values is not supported.")
 
         self.output_nodes: list[NodePattern] = list(output_nodes)
 
@@ -1322,23 +1355,17 @@ def _match_node(self, pattern_node: NodePattern, node: ir.Node) -> bool:
                 return False
 
         for i, output_value_pattern in enumerate(pattern_node.outputs):
-            if not self._bind_value(output_value_pattern, node.outputs[i]):
+            if not self._match.bind_value(output_value_pattern, node.outputs[i]):
                 return False
 
         return True
 
-    def _bind_value(self, pattern_value: ValuePattern, value: ir.Value | None) -> bool:
-        """Bind a ValuePattern var to ir Value."""
-        if pattern_value.name is not None:
-            return self._match.bind(pattern_value.name, value)
-        return True
-
     def _match_value(self, pattern_value: ValuePattern, value: ir.Value | None) -> bool:
         """Match an IR value against a ValuePattern instance."""
         if isinstance(pattern_value, AnyValue):
             return True
 
-        if not self._bind_value(pattern_value, value):
+        if not self._match.bind_value(pattern_value, value):
             return False
 
         if isinstance(pattern_value, NodeOutputPattern):
@@ -1402,16 +1429,11 @@ def _get_output_values(self) -> list[ir.Value] | None:
                     output_values.append(self._match.bindings[value_pattern.name])
                 else:
                     unbound_values.append(value_pattern.name)
-            elif isinstance(value_pattern, NodeOutputPattern):
-                i = value_pattern.output_index
-                node = value_pattern.producer()
-                matched_node = self._match.lookup_node(node)
-                if matched_node is not None:
-                    output_values.append(matched_node.outputs[i])
+            else:
+                if value_pattern in self._match.value_bindings:
+                    output_values.append(self._match.value_bindings[value_pattern])
                 else:
                     unbound_values.append(f"output_{j}")
-            elif isinstance(value_pattern, Constant):
-                raise NotImplementedError("Constant values as return-values not supported.")
         if unbound_values:
             self._match.fail(f"Error: Output values not found: {unbound_values}")
             return None
diff --git a/onnxscript/rewriter/pattern_test.py b/onnxscript/rewriter/pattern_test.py
index edfff6bc13..6706eea193 100644
--- a/onnxscript/rewriter/pattern_test.py
+++ b/onnxscript/rewriter/pattern_test.py
@@ -754,6 +754,31 @@ def test_model2(x: FLOAT[16, 32], y: FLOAT[32, 16], bias: FLOAT[16]) -> FLOAT[16
         self.assertEqual([x.op_type for x in model.graph], ["GemmRelu"])
         self.assertEqual([x.name for x in model.graph.node(0).inputs], ["x", "y", "bias"])
 
+    def test_or_pattern_return_value(self):
+        """Test that an OrValue can be used as a return value from the source pattern."""
+
+        def source_pattern(op, x, y):
+            choice1 = op.Add(x, y)
+            choice2 = op.Mul(x, y)
+            t = pattern.OrValue([choice1, choice2])
+            z = op.Relu(t)
+            return z, t
+
+        def replacement(op, x, y):
+            z, t = op.ReluPlus(x, y, _outputs=2)
+            return z, t
+
+        rule = pattern.RewriteRule(source_pattern, replacement)
+
+        @script()
+        def test_model1(x: FLOAT[16, 32], y: FLOAT[16, 32]) -> FLOAT[16, 32]:
+            return op.Relu(op.Add(x, y))
+
+        model_proto = test_model1.to_model_proto()
+        model = ir.serde.deserialize_model(model_proto)
+        rule.apply_to_model(model)
+        self.assertEqual([x.op_type for x in model.graph], ["ReluPlus"])
+
 
 class PatternBuilderTest(unittest.TestCase):
     def test_pattern_builder_context(self):


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes the handling of Choice values when used as return values in patterns and refines value-binding logic to facilitate returning these values. In addition, it extends the backward slice computation to include value patterns and updates the matching and output retrieval methods accordingly.
- Extend the _add_backward_slice API to incorporate backward_slice_values.
- Update GraphPattern initialization to check for uncovered choice values.
- Refactor binding logic (bind_value and _get_output_values) for improved consistency.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

| File                                         | Description                                                       |
| -------------------------------------------- | ----------------------------------------------------------------- |
| onnxscript/rewriter/pattern_test.py          | Adds a new test to verify that OrValue can be returned from a pattern. |
| onnxscript/rewriter/pattern.py               | Updates various methods (e.g., bind_value, _add_backward_slice, GraphPattern.__init__, _get_output_values) to support returning choice values. |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**onnxscript/rewriter/pattern.py:973**
* The new parameter 'backward_slice_values' is not documented. Consider updating the docstring to explain its purpose and usage.
```
def _add_backward_slice(node: NodePattern, backward_slice: set[NodePattern], backward_slice_values: set[ValuePattern]) -> None:
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T00:01:17Z
user: login: justinchuby
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-08T00:01:30Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T00:02:13Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T00:09:18Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T22:05:52Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:32:38Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:32:56Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:33:11Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T00:33:54Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:14:39Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:26:44Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:35:01Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:52:27Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:02:04Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:10:14Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:11:17Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:47:46Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:53:41Z
user: login: justinchuby
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:53:48Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:54:59Z
user: login: justinchuby
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:56:09Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:21:27Z
user: login: gramalingam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:21:50Z

Review comments:
diff_hunk: @@ -987,20 +1011,24 @@ def __init__(
         # whose backward-slices cover the entire pattern.
         output_nodes: set[NodePattern] = set()
         covered: set[NodePattern] = set()
+        choice_values_returned: set[ValuePattern] = set()
+        covered_choice_values: set[ValuePattern] = set()
         for value_pattern in outputs:
             if not isinstance(value_pattern, ValuePattern):
                 raise TypeError(
                     f"Invalid type {type(value_pattern)} for graph pattern output."
                 )
-            if isinstance(value_pattern, Constant):
-                raise NotImplementedError(
-                    "Constant values are not allowed as graph pattern outputs."
-                )
             if isinstance(value_pattern, NodeOutputPattern):
                 candidate = value_pattern.producer()
                 if candidate not in covered:
                     output_nodes.add(candidate)
-                    _add_backward_slice(candidate, covered)
+                    _add_backward_slice(candidate, covered, covered_choice_values)
+            elif isinstance(value_pattern, (_OpIdDispatchOr, _BacktrackingOr)):
+                choice_values_returned.add(value_pattern)
+
+        # check if choice_values_returned are contained in covered_choice_values:
path: onnxscript/rewriter/pattern.py
user: login: Copilot
body: Consider adding a comment or updating the docstring to clarify what it means for choice-values to be 'covered' and the rationale for raising NotImplementedError when they are not.
```suggestion
        # Check if all choice-values returned by the pattern are covered.
        # A "choice-value" is considered "covered" if it is included in the set of
        # `covered_choice_values`, which represents values that are fully accounted for
        # during the backward-slice traversal of the pattern. This ensures that all
        # possible execution paths are handled.
        # 
        # Raising a NotImplementedError here indicates that the current implementation
        # does not support patterns where some choice-values are not covered. This is
        # likely because such cases would require additional logic to handle them safely.
```
created_at: 2025-05-08T00:01:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1396,22 +1418,17 @@ def _get_output_values(self) -> list[ir.Value] | None:
         """Get values bound to the output variables of the pattern."""
         output_values: list[ir.Value] = []
         unbound_values: list[str] = []
-        for j, value_pattern in enumerate(self.pattern.outputs):
+        for value_pattern in self.pattern.outputs:
             if value_pattern.name is not None:
                 if value_pattern.name in self._match.bindings:
                     output_values.append(self._match.bindings[value_pattern.name])
                 else:
                     unbound_values.append(value_pattern.name)
-            elif isinstance(value_pattern, NodeOutputPattern):
-                i = value_pattern.output_index
-                node = value_pattern.producer()
-                matched_node = self._match.lookup_node(node)
-                if matched_node is not None:
-                    output_values.append(matched_node.outputs[i])
+            else:
+                if value_pattern in self._match.value_bindings:
+                    output_values.append(self._match.value_bindings[value_pattern])
                 else:
-                    unbound_values.append(f"output_{j}")
-            elif isinstance(value_pattern, Constant):
-                raise NotImplementedError("Constant values as return-values not supported.")
+                    unbound_values.append(value_pattern)
path: onnxscript/rewriter/pattern.py
user: login: Copilot
body: [nitpick] Ensure that the fallback for unbound values in _get_output_values provides a clear, informative error message (e.g. by converting value_pattern to a string) to aid debugging.
created_at: 2025-05-08T00:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -379,6 +379,23 @@ def add_node(self, node: ir.Node) -> None:
         """Adds a node to the list of matched nodes."""
         self._current_match.add_node(node)
 
+    def bind_value(self, pattern_value: ValuePattern, value: Any) -> bool:
path: onnxscript/rewriter/pattern.py
user: login: Copilot
body: [nitpick] Consider adding a docstring for bind_value to explain its behavior, especially how it handles named versus unnamed patterns, to improve code maintainability.
```suggestion
    def bind_value(self, pattern_value: ValuePattern, value: Any) -> bool:
        """
        Attempts to bind a value to a pattern value, handling both named and unnamed patterns.

        If the pattern value has a name, it delegates the binding to the `bind` method.
        If the pattern value is unnamed (name is None), it checks for existing bindings
        in the partial matches. If a conflict is found (i.e., the pattern value is already
        bound to a different value), the current match fails. Otherwise, it creates a new
        binding for the pattern value.

        Args:
            pattern_value (ValuePattern): The pattern value to bind.
            value (Any): The value to bind to the pattern value.

        Returns:
            bool: True if the binding is successful, False otherwise.

        Raises:
            ValueError: If a binding conflict occurs.
        """
```
created_at: 2025-05-08T00:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1396,22 +1418,17 @@
         """Get values bound to the output variables of the pattern."""
         output_values: list[ir.Value] = []
         unbound_values: list[str] = []
-        for j, value_pattern in enumerate(self.pattern.outputs):
+        for value_pattern in self.pattern.outputs:
             if value_pattern.name is not None:
                 if value_pattern.name in self._match.bindings:
                     output_values.append(self._match.bindings[value_pattern.name])
                 else:
                     unbound_values.append(value_pattern.name)
-            elif isinstance(value_pattern, NodeOutputPattern):
-                i = value_pattern.output_index
-                node = value_pattern.producer()
-                matched_node = self._match.lookup_node(node)
-                if matched_node is not None:
-                    output_values.append(matched_node.outputs[i])
+            else:
+                if value_pattern in self._match.value_bindings:
+                    output_values.append(self._match.value_bindings[value_pattern])
                 else:
-                    unbound_values.append(f"output_{j}")
-            elif isinstance(value_pattern, Constant):
-                raise NotImplementedError("Constant values as return-values not supported.")
+                    unbound_values.append(value_pattern)
path: onnxscript/rewriter/pattern.py
user: login: github-advanced-security[bot]
body: ## MYPY/arg-type

Argument 1 to "append" of "list" has incompatible type "ValuePattern"; expected "str" 

To disable, use `  # type: ignore[arg-type]`

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17000)
created_at: 2025-05-08T00:02:13Z
author_association: CONTRIBUTOR
path: onnxscript/rewriter/pattern.py
user: login: justinchuby
body: Would be helpful to document the limitation somewhere in the documentation
created_at: 2025-05-08T00:09:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corporation.
path: docs/tutorial/rewriter/examples/or_pattern.py
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Final newline expected

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17025)
created_at: 2025-05-08T22:05:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corporation.
path: docs/tutorial/rewriter/examples/or_pattern.py
user: login: github-advanced-security[bot]
body: ## RUFF/format

Run `lintrunner -a` to apply this patch.

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17027)
created_at: 2025-05-08T22:05:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corporation.
path: docs/tutorial/rewriter/examples/or_pattern.py
user: login: github-advanced-security[bot]
body: ## RUFF-FORMAT/format

Run `lintrunner -a` to apply this patch.

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17028)
created_at: 2025-05-08T22:05:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+"""OR-patterns.
+
+This script shows how to define a rewriting rule based on OR-patterns.
+"""
+
+import onnx
+
+import onnxscript
+from onnxscript import FLOAT, opset18, script
+from onnxscript.rewriter import pattern
+
+####################################
+# The target pattern
+# =====================
+
+
+def scaled_matmul(op, x, y, factor):
+    xy = op.MatMul(x, y)
+    choice1 = op.Mul(xy, factor)
+    choice2 = op.Div(xy, factor)
+    scaled_xy = pattern.OrValue([choice1, choice2], tag_var="op_type", tag_values=["Mul", "Div"])
+    return op.Relu(scaled_xy)
+
+
+####################################
+# The replacement pattern
+# =====================
+
+
+def scaled_matmul_replacement(op, x, y, factor, op_type):
+    if op_type == "Mul":
+        return op.MatMulMulRelu(x, y, factor, _domain="some.domain")
+    elif op_type == "Div":
+        return op.MatMulDivRelu(x, y, factor, _domain="some.domain")
+    else:
+        raise ValueError(f"Unknown operation type: {op_type}")
+
+
+####################################
+# Rewrite Rule
+# =====================
+def apply_rewrite(model):
+    rule = pattern.RewriteRule(
+        scaled_matmul,  # target pattern
+        scaled_matmul_replacement,  # replacement pattern
+    )
+    # Create a Rewrite Rule Set
+    rewrite_rule_set = pattern.RewriteRuleSet([rule])
+    return onnxscript.rewriter.rewrite(
+        model,
+        pattern_rewrite_rules=rewrite_rule_set,
+    )
+
+
+@script()
+def original_model1(A: FLOAT[2, 2], B: FLOAT[2, 2]) -> FLOAT[2, 2]:
+    t1 = opset18.MatMul(A, B)
+    c = opset18.Constant(value_float=2.0)
+    t2 = opset18.Mul(t1, c)
+    t3 = opset18.Relu(t2)
+    return t3
+
+
+_model = original_model1.to_model_proto()
+onnx.checker.check_model(_model)
+
+_model_with_rewrite = apply_rewrite(_model)
+onnx.checker.check_model(_model_with_rewrite)
+
+assert [n.op_type for n in _model_with_rewrite.graph.node] == ["Constant", "MatMulMulRelu"]
+
+@script()
+def original_model2(A: FLOAT[2, 2], B: FLOAT[2, 2]) -> FLOAT[2, 2]:
+    t1 = opset18.MatMul(A, B)
+    c = opset18.Constant(value_float=2.0)
+    t2 = opset18.Div(t1, c)
+    t3 = opset18.Relu(t2)
+    return t3
+
+
+_model = original_model2.to_model_proto()
+onnx.checker.check_model(_model)
+
+_model_with_rewrite = apply_rewrite(_model)
+onnx.checker.check_model(_model_with_rewrite)
+
+assert [n.op_type for n in _model_with_rewrite.graph.node] == ["Constant", "MatMulDivRelu"]
path: docs/tutorial/rewriter/examples/or_pattern.py
user: login: github-advanced-security[bot]
body: ## RUFF/W292

No newline at end of file.
See https://docs.astral.sh/ruff/rules/missing-newline-at-end-of-file

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17026)
created_at: 2025-05-08T22:05:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+## OR Patterns
path: docs/tutorial/rewriter/or_pattern.md
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Final newline expected

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17029)
created_at: 2025-05-08T22:05:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+(heading-target-simple)=
path: docs/tutorial/rewriter/simple_example.md
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Final newline expected

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17024)
created_at: 2025-05-08T22:05:52Z
author_association: CONTRIBUTOR
path: docs/sg_execution_times.rst
user: login: justinchuby
body: nit: Before to exclude this file
created_at: 2025-05-09T00:32:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,22 @@
+## Specifying attributes in the pattern
path: docs/tutorial/rewriter/attributes.md
user: login: justinchuby
body: ```suggestion
# Specifying attributes in the pattern
```
created_at: 2025-05-09T00:32:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,71 @@
+(heading-target-commute)=
+## Utilizing `commute` parameter for pattern-matching
path: docs/tutorial/rewriter/commute.md
user: login: justinchuby
body: ```suggestion
# Utilizing `commute` parameter for pattern-matching
```
created_at: 2025-05-09T00:33:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,71 @@
+(heading-target-simple)=
+## A Simple Example
path: docs/tutorial/rewriter/simple_example.md
user: login: justinchuby
body: ```suggestion
# A Simple Example
```

etc.
created_at: 2025-05-09T00:33:54Z
author_association: COLLABORATOR
path: docs/sg_execution_times.rst
user: login: gramalingam
body: I was surprised to see this file, wasn't sure of its origin/intent. Are you suggesting excluding it? 
created_at: 2025-05-09T14:14:38Z
author_association: COLLABORATOR
path: docs/sg_execution_times.rst
user: login: justinchuby
body: It's generated by sphinx build. I think we can add it to gitignore
created_at: 2025-05-09T14:26:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,22 @@
+## Specifying attributes in the pattern
path: docs/tutorial/rewriter/attributes.md
user: login: gramalingam
body: Sounds good. Looked at the generated html, looks as below: I think the existing heading-depth makes sense if we anticipate having multiple tutorials under "Rewriter Tutorials", but I don't see that happening anytime soon. We should rename this to be "Rewriter Tutorial" and eliminate the top-level "Pattern-based Rewrite Using Rules".
![image](https://github.com/user-attachments/assets/62937149-166b-419a-b293-078b1801faf3)

created_at: 2025-05-09T14:35:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,22 @@
+## Specifying attributes in the pattern
path: docs/tutorial/rewriter/attributes.md
user: login: gramalingam
body: Did it. But, on the flip side, when you go into the tutorial sections, there is no top-level title, as below: so, it is not so clear-cut which is better: 
![image](https://github.com/user-attachments/assets/28629995-c212-4389-9a4a-57bed4281dab)

created_at: 2025-05-09T14:52:26Z
author_association: COLLABORATOR
diff_hunk: @@ -188,7 +188,7 @@
 
 ```{eval-rst}
 .. exec_code::
-
+    from __future__ import annotations 
path: docs/ir/tensors.md
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Trailing whitespace

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17031)
created_at: 2025-05-09T15:02:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+# OR Patterns
path: docs/tutorial/rewriter/or_pattern.md
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Final newline expected

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17030)
created_at: 2025-05-09T15:02:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+(heading-target-simple)=
path: docs/tutorial/rewriter/simple_example.md
user: login: github-advanced-security[bot]
body: ## EDITORCONFIG-CHECKER/editorconfig

Final newline expected

[Show more details](https://github.com/microsoft/onnxscript/security/code-scanning/17032)
created_at: 2025-05-09T15:02:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+## Specifying attributes in the pattern
path: docs/tutorial/rewriter/attributes.md
user: login: justinchuby
body: I see. Are the pages intended to be sections of the same page?
created_at: 2025-05-09T15:10:14Z
author_association: COLLABORATOR
diff_hunk: @@ -188,7 +188,7 @@ To fully support arrays from other frameworks, it is usually a good idea to crea
 
 ```{eval-rst}
 .. exec_code::
-
+    from __future__ import annotations 
path: docs/ir/tensors.md
user: login: justinchuby
body: ```suggestion
```

Is this needed? We run the doc gen using a newer version of python. Maybe a newer version of python is needed locally as well? (3.10 or higher)
created_at: 2025-05-09T15:11:17Z
author_association: COLLABORATOR
diff_hunk: @@ -188,7 +188,7 @@ To fully support arrays from other frameworks, it is usually a good idea to crea
 
 ```{eval-rst}
 .. exec_code::
-
+    from __future__ import annotations 
path: docs/ir/tensors.md
user: login: gramalingam
body: Yes, I needed it locally. Else, I need to recreate a new conda environment. Is there any harm in leaving this? Is there a strong dependence on 3.10 or higher now?
created_at: 2025-05-09T15:47:46Z
author_association: COLLABORATOR
diff_hunk: @@ -188,7 +188,7 @@ To fully support arrays from other frameworks, it is usually a good idea to crea
 
 ```{eval-rst}
 .. exec_code::
-
+    from __future__ import annotations 
path: docs/ir/tensors.md
user: login: justinchuby
body: I guess it's ok. onnxscript is still 3.9&up
created_at: 2025-05-09T15:53:40Z
author_association: COLLABORATOR
diff_hunk: @@ -1,231 +1,26 @@
-# Pattern-based Rewrite Using Rules
-
-## Introduction
+# Introduction
 
 The ONNX Rewriter tool provides the user with the functionality to replace certain patterns in an ONNX graph with another pattern based on rewrite rules provided by the user.
 
-## Usage
+# Usage
path: docs/tutorial/rewriter/rewrite_patterns.md
user: login: justinchuby
body: I think there should only be one level 1 heading in each file?
created_at: 2025-05-09T15:54:58Z
author_association: COLLABORATOR
path: docs/tutorial/rewriter/attributes.md
user: login: justinchuby
body: I didn't realize the file is included as part of the main file. (though it was an independent page) If so having the top most heading to be level 2 makes sense.
created_at: 2025-05-09T15:56:09Z
author_association: COLLABORATOR
path: docs/tutorial/rewriter/attributes.md
user: login: gramalingam
body: Will revisit this later.
created_at: 2025-05-09T16:21:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1,231 +1,26 @@
-# Pattern-based Rewrite Using Rules
-
-## Introduction
+# Introduction
 
 The ONNX Rewriter tool provides the user with the functionality to replace certain patterns in an ONNX graph with another pattern based on rewrite rules provided by the user.
 
-## Usage
+# Usage
path: docs/tutorial/rewriter/rewrite_patterns.md
user: login: gramalingam
body: Will revisit this later.
created_at: 2025-05-09T16:21:50Z
author_association: COLLABORATOR

PR comments:
user: login: justinchuby
created_at: 2025-05-08T00:01:05Z
body: For my understanding, could you explain more on what "returned choice-values are already covered by the other output values" means?
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-05-08T00:01:05Z
body: ### :x: 3 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 14697 | 3 | 14694 | 1698 |
<details><summary>View the top 3 failed test(s) by shortest run time</summary>

> 
> ```python
> onnxscript.backend.onnx_export_test.TestOnnxBackEnd::test_export2python_produces_correct_onnx_script_model_0981_test_ai_onnx_ml_tree_ensemble_set_membership
> ```
> 
> <details><summary>Stack Traces | 0.012s run time</summary>
> 
> > 
> > ```python
> > onnxscript/converter.py:460: in _eval_constant_expr
> >     return eval(cpl, self.globals, locals)  # pylint: disable=eval-used
> > E   NameError: name 'nan' is not defined
> > 
> > The above exception was the direct cause of the following exception:
> > ..../test_ort_nightly/lib/python3.11.../site-packages/parameterized/parameterized.py:620: in standalone_func
> >     return func(*(a + p.args), **p.kwargs, **kw)
> > onnxscript/backend/onnx_export_test.py:271: in test_export2python_produces_correct_onnx_script_model
> >     functions = extract_functions(backend_test.name, code, self.test_folder)
> > onnxscript/backend/onnx_export_test.py:137: in extract_functions
> >     mod = importlib.import_module(import_name)
> > .../hostedtoolcache/Python/3.11.12.../x64/lib/python3.11/importlib/__init__.py:126: in import_module
> >     return _bootstrap._gcd_import(name[level:], package, level)
> > <frozen importlib._bootstrap>:1204: in _gcd_import
> >     ???
> > <frozen importlib._bootstrap>:1176: in _find_and_load
> >     ???
> > <frozen importlib._bootstrap>:1147: in _find_and_load_unlocked
> >     ???
> > <frozen importlib._bootstrap>:690: in _load_unlocked
> >     ???
> > ..../test_ort_nightly/lib/python3.11.../_pytest/assertion/rewrite.py:185: in exec_module
> >     exec(co, module.__dict__)
> > tests/onnx_backend_test_code/test_ai_onnx_ml_tree_ensemble_set_membership.py:9: in <module>
> >     @script()
> > onnxscript/main.py:94: in transform
> >     result = script_check(f_ast, opset, env, src, default_opset=default_opset)
> > onnxscript/main.py:38: in script_check
> >     return convert.translate_function_def(f)
> > onnxscript/converter.py:1452: in translate_function_def
> >     fn_ir = self._translate_function_def_common(stmt)
> > onnxscript/converter.py:1439: in _translate_function_def_common
> >     self._translate_stmt(s, index_of_stmt=i)
> > onnxscript/converter.py:961: in _translate_stmt
> >     return self._translate_assign_stmt(node)
> > onnxscript/converter.py:1048: in _translate_assign_stmt
> >     assign(lhs, rhs)
> > onnxscript/converter.py:992: in assign
> >     t = self._translate_expr(rhs, lhs).name
> > onnxscript/converter.py:546: in _translate_expr
> >     r = self._translate_call_expr(node)
> > onnxscript/converter.py:825: in _translate_call_expr
> >     attrs = [
> > onnxscript/converter.py:826: in <listcomp>
> >     self._translate_attr(x, y, callee.op_schema.attributes[x])
> > onnxscript/converter.py:510: in _translate_attr
> >     val = self._eval_constant_expr(expr)
> > onnxscript/converter.py:462: in _eval_constant_expr
> >     raise NameError(
> > E   NameError: ERROR: Missing names, globals contains ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', '@py_builtins', '@pytest_ar', 'numpy', 'TensorProto', 'make_tensor', 'script', 'external_tensor', 'Opset', 'FLOAT', 'ai_onnx_ml5'], locals [].
> > E   at: Function 'bck_test_ai_onnx_ml_tree_ensemble_set_membership', line 3
> > E       Y = ai_onnx_ml5.TreeEnsemble(X, aggregate_function=1, leaf_targetids=[0, 1, 2, 3], leaf_weights=make_tensor("value", 1, dims=[4], vals=[1.0, 10.0, 1000.0, 100.0]), membership_values=make_tensor("value", 1, dims=[8], vals=[1.2000000476837158, 3.700000047683716, 8.0, 9.0, nan, 12.0, 7.0, nan]), n_targets=4, nodes_falseleafs=[1, 0, 1], nodes_falsenodeids=[2, 2, 3], nodes_featureids=[0, 0, 0], nodes_modes=make_tensor("value", 2, dims=[3], vals=[0, 6, 6]), nodes_splits=make_tensor("value", 1, dims=[3], vals=[11.0, 232344.0, nan]), nodes_trueleafs=[0, 1, 1], nodes_truenodeids=[1, 0, 1], post_transform=0, tree_roots=[0])
> > E                                                                                                                                                                                             ^
> > ```
> 
> </details>


> 
> ```python
> onnxscript.backend.onnx_export_test.TestOnnxBackEnd::test_export2python_produces_correct_onnx_script_model_0026_test_ai_onnx_ml_tree_ensemble_set_membership
> ```
> 
> <details><summary>Stack Traces | 0.02s run time</summary>
> 
> > 
> > ```python
> > onnxscript\converter.py:460: in _eval_constant_expr
> >     return eval(cpl, self.globals, locals)  # pylint: disable=eval-used
> > E   NameError: name 'nan' is not defined
> > 
> > The above exception was the direct cause of the following exception:
> > .nox\test_ort_nightly\Lib\site-packages\parameterized\parameterized.py:620: in standalone_func
> >     return func(*(a + p.args), **p.kwargs, **kw)
> > onnxscript\backend\onnx_export_test.py:271: in test_export2python_produces_correct_onnx_script_model
> >     functions = extract_functions(backend_test.name, code, self.test_folder)
> > onnxscript\backend\onnx_export_test.py:137: in extract_functions
> >     mod = importlib.import_module(import_name)
> > C:\hostedtoolcache\windows\Python\3.11.9\x64\Lib\importlib\__init__.py:126: in import_module
> >     return _bootstrap._gcd_import(name[level:], package, level)
> > <frozen importlib._bootstrap>:1204: in _gcd_import
> >     ???
> > <frozen importlib._bootstrap>:1176: in _find_and_load
> >     ???
> > <frozen importlib._bootstrap>:1147: in _find_and_load_unlocked
> >     ???
> > <frozen importlib._bootstrap>:690: in _load_unlocked
> >     ???
> > .nox\test_ort_nightly\Lib\site-packages\_pytest\assertion\rewrite.py:185: in exec_module
> >     exec(co, module.__dict__)
> > tests\onnx_backend_test_code\test_ai_onnx_ml_tree_ensemble_set_membership.py:9: in <module>
> >     @script()
> > onnxscript\main.py:94: in transform
> >     result = script_check(f_ast, opset, env, src, default_opset=default_opset)
> > onnxscript\main.py:38: in script_check
> >     return convert.translate_function_def(f)
> > onnxscript\converter.py:1452: in translate_function_def
> >     fn_ir = self._translate_function_def_common(stmt)
> > onnxscript\converter.py:1439: in _translate_function_def_common
> >     self._translate_stmt(s, index_of_stmt=i)
> > onnxscript\converter.py:961: in _translate_stmt
> >     return self._translate_assign_stmt(node)
> > onnxscript\converter.py:1048: in _translate_assign_stmt
> >     assign(lhs, rhs)
> > onnxscript\converter.py:992: in assign
> >     t = self._translate_expr(rhs, lhs).name
> > onnxscript\converter.py:546: in _translate_expr
> >     r = self._translate_call_expr(node)
> > onnxscript\converter.py:825: in _translate_call_expr
> >     attrs = [
> > onnxscript\converter.py:826: in <listcomp>
> >     self._translate_attr(x, y, callee.op_schema.attributes[x])
> > onnxscript\converter.py:510: in _translate_attr
> >     val = self._eval_constant_expr(expr)
> > onnxscript\converter.py:462: in _eval_constant_expr
> >     raise NameError(
> > E   NameError: ERROR: Missing names, globals contains ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', '@py_builtins', '@pytest_ar', 'numpy', 'TensorProto', 'make_tensor', 'script', 'external_tensor', 'Opset', 'FLOAT', 'ai_onnx_ml5'], locals [].
> > E   at: Function 'bck_test_ai_onnx_ml_tree_ensemble_set_membership', line 3
> > E       Y = ai_onnx_ml5.TreeEnsemble(X, aggregate_function=1, leaf_targetids=[0, 1, 2, 3], leaf_weights=make_tensor("value", 1, dims=[4], vals=[1.0, 10.0, 1000.0, 100.0]), membership_values=make_tensor("value", 1, dims=[8], vals=[1.2000000476837158, 3.700000047683716, 8.0, 9.0, nan, 12.0, 7.0, nan]), n_targets=4, nodes_falseleafs=[1, 0, 1], nodes_falsenodeids=[2, 2, 3], nodes_featureids=[0, 0, 0], nodes_modes=make_tensor("value", 2, dims=[3], vals=[0, 6, 6]), nodes_splits=make_tensor("value", 1, dims=[3], vals=[11.0, 232344.0, nan]), nodes_trueleafs=[0, 1, 1], nodes_truenodeids=[1, 0, 1], post_transform=0, tree_roots=[0])
> > E                                                                                                                                                                                             ^
> > ```
> 
> </details>


> 
> ```python
> onnxscript.backend.onnx_export_test.TestOnnxBackEnd::test_export2python_produces_correct_onnx_script_model_0125_test_ai_onnx_ml_tree_ensemble_set_membership
> ```
> 
> <details><summary>Stack Traces | 0.04s run time</summary>
> 
> > 
> > ```python
> > onnxscript/converter.py:460: in _eval_constant_expr
> >     return eval(cpl, self.globals, locals)  # pylint: disable=eval-used
> > E   NameError: name 'nan' is not defined
> > 
> > The above exception was the direct cause of the following exception:
> > ..../test_ort_nightly/lib/python3.11.../site-packages/parameterized/parameterized.py:620: in standalone_func
> >     return func(*(a + p.args), **p.kwargs, **kw)
> > onnxscript/backend/onnx_export_test.py:271: in test_export2python_produces_correct_onnx_script_model
> >     functions = extract_functions(backend_test.name, code, self.test_folder)
> > onnxscript/backend/onnx_export_test.py:137: in extract_functions
> >     mod = importlib.import_module(import_name)
> > .../Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/importlib/__init__.py:126: in import_module
> >     return _bootstrap._gcd_import(name[level:], package, level)
> > <frozen importlib._bootstrap>:1204: in _gcd_import
> >     ???
> > <frozen importlib._bootstrap>:1176: in _find_and_load
> >     ???
> > <frozen importlib._bootstrap>:1147: in _find_and_load_unlocked
> >     ???
> > <frozen importlib._bootstrap>:690: in _load_unlocked
> >     ???
> > ..../test_ort_nightly/lib/python3.11.../_pytest/assertion/rewrite.py:185: in exec_module
> >     exec(co, module.__dict__)
> > tests/onnx_backend_test_code/test_ai_onnx_ml_tree_ensemble_set_membership.py:9: in <module>
> >     @script()
> > onnxscript/main.py:94: in transform
> >     result = script_check(f_ast, opset, env, src, default_opset=default_opset)
> > onnxscript/main.py:38: in script_check
> >     return convert.translate_function_def(f)
> > onnxscript/converter.py:1452: in translate_function_def
> >     fn_ir = self._translate_function_def_common(stmt)
> > onnxscript/converter.py:1439: in _translate_function_def_common
> >     self._translate_stmt(s, index_of_stmt=i)
> > onnxscript/converter.py:961: in _translate_stmt
> >     return self._translate_assign_stmt(node)
> > onnxscript/converter.py:1048: in _translate_assign_stmt
> >     assign(lhs, rhs)
> > onnxscript/converter.py:992: in assign
> >     t = self._translate_expr(rhs, lhs).name
> > onnxscript/converter.py:546: in _translate_expr
> >     r = self._translate_call_expr(node)
> > onnxscript/converter.py:825: in _translate_call_expr
> >     attrs = [
> > onnxscript/converter.py:826: in <listcomp>
> >     self._translate_attr(x, y, callee.op_schema.attributes[x])
> > onnxscript/converter.py:510: in _translate_attr
> >     val = self._eval_constant_expr(expr)
> > onnxscript/converter.py:462: in _eval_constant_expr
> >     raise NameError(
> > E   NameError: ERROR: Missing names, globals contains ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', '@py_builtins', '@pytest_ar', 'numpy', 'TensorProto', 'make_tensor', 'script', 'external_tensor', 'Opset', 'FLOAT', 'ai_onnx_ml5'], locals [].
> > E   at: Function 'bck_test_ai_onnx_ml_tree_ensemble_set_membership', line 3
> > E       Y = ai_onnx_ml5.TreeEnsemble(X, aggregate_function=1, leaf_targetids=[0, 1, 2, 3], leaf_weights=make_tensor("value", 1, dims=[4], vals=[1.0, 10.0, 1000.0, 100.0]), membership_values=make_tensor("value", 1, dims=[8], vals=[1.2000000476837158, 3.700000047683716, 8.0, 9.0, nan, 12.0, 7.0, nan]), n_targets=4, nodes_falseleafs=[1, 0, 1], nodes_falsenodeids=[2, 2, 3], nodes_featureids=[0, 0, 0], nodes_modes=make_tensor("value", 2, dims=[3], vals=[0, 6, 6]), nodes_splits=make_tensor("value", 1, dims=[3], vals=[11.0, 232344.0, nan]), nodes_trueleafs=[0, 1, 1], nodes_truenodeids=[1, 0, 1], post_transform=0, tree_roots=[0])
> > E                                                                                                                                                                                             ^
> > ```
> 
> </details>

</details>

To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/microsoft/onnxscript/tests/rama%2Fpattern-output-fix)
<sub>📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.</sub>
author_association: NONE
user: login: gramalingam
created_at: 2025-05-08T16:28:15Z
body: > For my understanding, could you explain more on what "returned choice-values are already covered by the other output values" means?

"value1 is covered by value2" is just shorthand here for "value1 is transitively used to compute value2". In the pattern-matching situation, a successful backward match for "value2" would also have found a match for "value1". Consider a pattern that includes
```
   value1 = ...
   value2 = op.Relu(value1)
   return value2, value1
```
Here, if we have successfully matched a subgraph for value2, we no longer need to do any more search to match value1, it is guaranteed to have already been found.
author_association: COLLABORATOR
user: login: justinchuby
created_at: 2025-05-10T02:45:52Z
body: @gramalingam looks like the docs workflow failed: https://github.com/microsoft/onnxscript/actions/runs/14933269595/job/41954640986
author_association: COLLABORATOR