Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 87 additions & 14 deletions onnxscript/_internal/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@
# and allows sharing them across different layers/contexts.
self._constant_cache: dict[tuple[Any, ir.DataType | None], ir.Value] = {}

self._functions: dict[ir.OperatorIdentifier, ir.Function] = {}

def opset(self, domain: str, version: int = 1) -> OpBuilder:
"""Create an OpBuilder bound to the given domain and version."""
return OpBuilder(self, domain, version)
Expand All @@ -241,6 +243,10 @@
def graph(self) -> ir.Graph:
return self._graph

@property
def functions(self) -> dict[ir.OperatorIdentifier, ir.Function]:
return self._functions

def initializer(
self, tensor: ir.TensorProtocol, name: str | None = None, *, qualify: bool = True
) -> ir.Value:
Expand Down Expand Up @@ -505,13 +511,13 @@
self,
op_type: str,
inputs: Sequence[ir.Value | ir.TensorProtocol],
kwargs: dict[str, Any],
kwargs: dict[str, ir.Value | ir.TensorProtocol],
/,
domain: str = "",
version: int | None = None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we expect different versions of the same opset?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We this is supported when building an ONNX IR model, even though we don't do this in practice

outputs: int | Sequence[str | ir.Value] = 1,
Comment on lines 513 to +518
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call_op signature now annotates kwargs as dict[str, ir.Value | ir.TensorProtocol], but kwargs is used as a generic argument/attribute bag (passed into _partition_inputs_attributes / _cast_attributes) and can include non-value attribute types (ints, floats, strings, sequences, etc.). This type annotation is too narrow and will cause incorrect type-checking; it should stay as dict[str, Any] (or a dedicated attribute type alias) to match actual usage.

Copilot uses AI. Check for mistakes.
):
"""Create an ONNX node and add it to the graph, returning its output value(s)."""
domain = kwargs.pop("_domain", "")
version = kwargs.pop("_version", None)
outputs = kwargs.pop("_outputs", 1)

count = self.graph.num_nodes()
node_name = self._qualify_node_name(f"{op_type}_node_{count}")

Expand Down Expand Up @@ -543,7 +549,45 @@

def call(
self,
function,
function: ir.Function | onnxscript.OnnxFunction,
*args,
_outputs: int | Sequence[str | ir.Value] | None = None,
**kwargs,
):
"""Call a function as a single function node."""
if isinstance(function, ir.Function):
graph = function.graph
Comment on lines 550 to +559
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says call/OpBuilder.call gained an _inline option with default inlining, but the implementation changes call() to non-inlining semantics and introduces a separate call_inline() API. This is a behavior-breaking change for existing users of call() and doesn’t match the described API. Either add the _inline flag (defaulting to current inlining behavior) or keep call() as inline and introduce a new explicit non-inlining method (or flag) for function-call nodes.

Copilot uses AI. Check for mistakes.
elif isinstance(function, onnxscript.OnnxFunction):
graph = function.graph()
function = function.function_ir
else:
raise TypeError("Function must be an ir.Function or onnxscript.OnnxFunction")

if _outputs is None:
_outputs = len(graph.outputs)
output_values = self._adapt_outputs(_outputs, function.name)

node = ir.node(
op_type=function.name,
inputs=args,
Comment on lines +570 to +572
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphBuilder.call() passes inputs=args directly into ir.node(...) without adapting inputs the way call_op does (e.g., promoting python scalars / TensorProtocol into initializers and ensuring all inputs are ir.Value). This will break callers that pass constants or tensors (which call_op explicitly supports). Consider running the same input-adaptation path used by call_op (at least _input_to_ir_value for each arg) before constructing the node.

Suggested change
node = ir.node(
op_type=function.name,
inputs=args,
# Adapt inputs similarly to call_op: promote constants/tensors to ir.Value.
adapted_args = [self._input_to_ir_value(arg) for arg in args]
node = ir.node(
op_type=function.name,
inputs=adapted_args,

Copilot uses AI. Check for mistakes.
attributes=kwargs or None,
outputs=output_values,
domain=function.domain,
name=self._qualify_node_name(function.name),
Comment on lines +570 to +576
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphBuilder.call() uses name=self._qualify_node_name(function.name), which will produce identical node names for repeated calls to the same function in the same scope (unlike call_op, which appends a monotonic counter). If node names are expected to be stable/unique for debugging or downstream tooling, incorporate the node index (e.g., ..._node_{count}) similarly to call_op.

Suggested change
node = ir.node(
op_type=function.name,
inputs=args,
attributes=kwargs or None,
outputs=output_values,
domain=function.domain,
name=self._qualify_node_name(function.name),
call_index = getattr(self, "_call_node_index", 0)
base_name = f"{function.name}_node_{call_index}"
self._call_node_index = call_index + 1
node = ir.node(
op_type=function.name,
inputs=args,
attributes=kwargs or None,
outputs=output_values,
domain=function.domain,
name=self._qualify_node_name(base_name),

Copilot uses AI. Check for mistakes.
)
# Attach scope metadata to the node
node.metadata_props["namespace"] = self._build_namespace()
node.metadata_props["pkg.onnxscript.class_hierarchy"] = repr(self._scope_classes())
node.metadata_props["pkg.onnxscript.name_scopes"] = repr(self._scope_names())

self.add_node(node)
self._functions[function.identifier()] = function
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same function can be called multiple times, so the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess two different functions cannot have the same identifier.


return node.outputs if len(node.outputs) > 1 else node.outputs[0]

def call_inline(
self,
function: ir.Function | onnxscript.OnnxFunction,
*args,
_outputs: Sequence[str] | None = None,
_prefix: str = "",
Expand All @@ -553,6 +597,7 @@
graph = function.graph
elif isinstance(function, onnxscript.OnnxFunction):
graph = function.graph()
function = function.function_ir

Check notice

Code scanning / CodeQL

Unused local variable Note

Variable function is not used.

Copilot Autofix

AI 22 days ago

In general, to fix an unused local variable, either remove the assignment if the value isn’t needed, or, if the assignment is required for its side effects, keep the right‑hand expression and drop or rename the left‑hand variable to an “unused” name (for documentation).

Here, function = function.function_ir merely rebinds function to function.function_ir. That has no side effects and the new value is never used. The best behavior‑preserving fix is to delete this line in call_inline, unlike in call where function.function_ir is later required. We only change onnxscript/_internal/builder.py in the shown region: remove line 600 (function = function.function_ir) and keep everything else the same.

No new imports, methods, or definitions are required.

Suggested changeset 1
onnxscript/_internal/builder.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/onnxscript/_internal/builder.py b/onnxscript/_internal/builder.py
--- a/onnxscript/_internal/builder.py
+++ b/onnxscript/_internal/builder.py
@@ -597,7 +597,6 @@
             graph = function.graph
         elif isinstance(function, onnxscript.OnnxFunction):
             graph = function.graph()
-            function = function.function_ir
         else:
             raise TypeError("Function must be an ir.Function or onnxscript.OnnxFunction")
         output_renaming: dict[str, str] = {}
EOF
@@ -597,7 +597,6 @@
graph = function.graph
elif isinstance(function, onnxscript.OnnxFunction):
graph = function.graph()
function = function.function_ir
else:
raise TypeError("Function must be an ir.Function or onnxscript.OnnxFunction")
output_renaming: dict[str, str] = {}
Copilot is powered by AI and may make mistakes. Always verify output.
else:
raise TypeError("Function must be an ir.Function or onnxscript.OnnxFunction")
output_renaming: dict[str, str] = {}
Expand All @@ -567,9 +612,12 @@
else:
for output in graph.outputs:
output_renaming[output.name] = self._qualify_value_name(output.name)

nodes, outputs = _inliner.instantiate(graph, args, kwargs)

if _prefix:
self.push_module(_prefix)

for node in nodes:
node.name = self._qualify_node_name(node.name)
for output in node.outputs:
Expand All @@ -579,6 +627,7 @@
else:
output.name = self._qualify_value_name(output.name)
self.add_node(node)

if _prefix:
self.pop_module()
return outputs if len(outputs) > 1 else outputs[0]
Expand Down Expand Up @@ -672,27 +721,51 @@
return self._version

def _call_op(self, op_type: str, inputs: Sequence[Any], kwargs: dict[str, Any]):
if "_domain" not in kwargs:
kwargs["_domain"] = self._domain
if self._version is not None and "_version" not in kwargs:
kwargs["_version"] = self._version
return self._builder.call_op(op_type, inputs, kwargs)
domain = kwargs.pop("_domain", self._domain)
version = kwargs.pop("_version", self._version)
outputs = kwargs.pop("_outputs", 1)
return self._builder.call_op(
op_type, inputs, kwargs, domain=domain, version=version, outputs=outputs
)

def __getattr__(self, op_type: str) -> Callable:
return lambda *args, **kwargs: self._call_op(op_type, args, kwargs)

def initializer(self, tensor: ir.TensorProtocol, name: str | None = None) -> ir.Value:
return self._builder.initializer(tensor, name)

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpBuilder.functions is defined as a plain method, but GraphBuilder.functions is a @property and the PR description says the property is exposed on both builders. This is an API inconsistency that will surprise users (they’d have to call op.functions() instead of accessing op.functions). Consider making this a @property for parity with GraphBuilder.

Suggested change
@property

Copilot uses AI. Check for mistakes.
def functions(self) -> dict[ir.OperatorIdentifier, ir.Function]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no property ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch thanks

return self._builder.functions

def call(
self,
function,
*args,
_outputs: Sequence[str] | int | None = None,
**kwargs,
):
"""Call a function as a single function node.

Args:
function: The function to call (ir.Function or onnxscript.OnnxFunction).
*args: Positional arguments to pass to the function.
_outputs: Optional sequence of output names, or an integer specifying the number of outputs.
**kwargs: Keyword arguments to pass to the function.

Returns:
The output value(s) from the function call.
"""
return self._builder.call(function, *args, _outputs=_outputs, **kwargs)

def call_inline(
self,
function,
*args,
_outputs: Sequence[str] | None = None,
_prefix: str = "",
**kwargs,
):
"""Call a function and inline it into the graph.
"""Inline a function body into the current graph.

Args:
function: The function to call (ir.Function or onnxscript.OnnxFunction).
Expand All @@ -703,8 +776,8 @@
**kwargs: Keyword arguments to pass to the function.

Returns:
The output value(s) from the function call.
The output value(s) from the inlined function body.
"""
return self._builder.call(
return self._builder.call_inline(
function, *args, _outputs=_outputs, _prefix=_prefix, **kwargs
)
Loading
Loading