slyubomirsky commented on code in PR #14148:
URL: https://github.com/apache/tvm/pull/14148#discussion_r1320327448


##########
relax_spec.md:
##########
@@ -0,0 +1,914 @@
+# Informal Relax Language Specification
+
+Note: Text in «double chevrons» indicates features not present in the current 
prototype.
+
+In order to develop and test Relax, it is important for compiler developers to 
agree on what a given program in Relax means and what makes it valid so that 
test cases can be evaluated independently of any particular Relax 
implementation. This document is intended to describe Relax's grammar 
constructs (its [abstract syntax 
tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree), or AST), the 
semantics of its grammar (what the different constructs mean), Relax's type 
system and type-checking rules (what makes a Relax program valid), and its 
rules for reasoning about structural information (such as tensor shapes) in 
detailed though still informal terms. If necessary, we may encode these rules 
more formally to allow for more automated analysis.
+
+Though this document will use the TVMScript front end for some examples, 
specifying the mapping from Python's AST to Relax's AST will be deferred until 
the parser becomes more stable.
+
+# Table of Contents
+
+1. [Overview](#overview)
+2. [Top-Level Program Organization](#top-level-program-organization-irmodule)
+3. [Values in Relax](#values-in-relax)
+4. [Variable Scoping](#variable-scoping)
+5. [Normal Form](#normal-form)
+6. [Well-Formedness Criteria](#well-formedness-criteria)
+7. [Structural Information in Relax](#structural-information-in-relax)
+8. [Semantics](#detailed-semantics)
+
+# Overview
+
+This section will outline the grammar of Relax and give very brief 
descriptions of the different components, including the semantics and 
structural information (`StructInfo`) system. The rest of this document will 
provide more detailed descriptions of these facets of the language, including 
the validity conditions that the `StructInfo` system upholds.
+
+## Differences from Relay
+
+Per the [original workshop paper](https://arxiv.org/abs/1810.00952) and the 
[later report](https://arxiv.org/abs/1904.08368), Relay was designed to be a 
high-level functional language for expressing deep learning models at a high 
level. While Relay is not entirely pure (the `Ref` type is modeled after 
reference types in SML and similar functional languages), the assumption in 
Relay is that tensor operators are generally pure, meaning that they do not 
change the program state other than by producing new values. Additionally, 
Relay's type system also requires operators to have type relations that infer 
static tensor types or conclude that a dimension is unknown at compile time 
(`Any`). The need to register type relations and ensure operators' purity makes 
it difficult to add new operators to Relay and particularly difficult to call 
directly into TIR or external libraries, which are often not pure; any such 
extension requires adding new operators and abstracting over any impurity.
+
+While Relax aims to be as general and expressive as Relay, Relax is intended 
to make it much easier to interoperate with external libraries and especially 
with TIR. In particular, Relax includes a mechanism for calling arbitrary TVM 
`PackedFunc`s (which can call external libraries) and special support for TIR. 
The language accordingly does not assume that such operations are pure, though 
this does require reasoning about aliasing and similar issues. Additionally, 
tensor shapes are no longer handled during type checking; each expression has 
associated structural information associated with it, in addition to a type. 
This structural information supports static reasoning about tensor shapes in 
many cases, but also facilitates a fallback to dynamic checking when that is 
not possible. This approach to shapes allows for richer shape constraints and 
other structural properties to be checked at run time (such as with _symbolic_ 
shapes, where some dimensions are variables) and allows for mor
 e quickly integrating calls into TIR or external libraries into Relax code by 
obviating the need for type relations.
+
+## Grammar
+
+Below is a diagram of the various AST constructs in Relax, including types. In 
code, these are defined on the C++ side in `include/tvm/relax/{expr.h, type.h}` 
and in Python in `python/tvm/relax/{expr.py, ty.py}`. This diagram will give 
the names of the AST nodes and the types and names of their members. The 
semantics will describe what computation each construct represents; an AST is 
simply data. A Relax program consists of an `IRModule` with global variables 
bound to Relax functions that implement the computations of interest.
+
+(On the notation: `[x]` means "a list of `x`," `x?` means "optionally `x`," 
`{x: y}` means "a map of `x` to `y`," `x | y` means "`x` or `y`," and `#` is 
used for comments. For the definition of `PrimFunc`, AST constructs are 
prefixed with `tir::` to indicate that these are the TIR versions of these AST 
nodes rather than the Relax ones.)
+
+```
+# PrimExprs are defined in TIR, see include/tvm/tir/expr.h
+# They are intended to have the same semantics as in TIR
+PrimExpr ::=
+           Var(name: string) # shape variables
+         | IntImm(value: int64)
+         | Add(a: PrimExpr, b: PrimExpr)
+         | Sub(a: PrimExpr, b: PrimExpr)
+         | Mul(a: PrimExpr, b: PrimExpr)
+         | Div(a: PrimExpr, b: PrimExpr)
+         | Min(a: PrimExpr, b: PrimExpr)
+         | Max(a: PrimExpr, b: PrimExpr)
+         | Not(a: PrimExpr)
+         | And(a: PrimExpr, b: PrimExpr)
+         | Or(a: PrimExpr, b: PrimExpr)
+         | Select(condition: PrimExpr, true_value: PrimExpr, false_value: 
PrimExpr)
+         # (others may be added later, as deemed necessary)
+
+# See include/tvm/tir/function.h
+# Can appear at the module level but otherwise do not interact with any Relax 
constructs;
+# intended to have the same semantics as in TIR
+PrimFunc ::= PrimFunc(params: [tir::Var], body: tir::Stmt, ret_type: 
tir::Type?,
+                      buffer_map: {tir::Var: tir::Buffer}, attrs: Attrs)
+
+# Also from TIR
+DataType ::= Int(bits: int, lanes: int)
+           | UInt(bits: int, lanes: int)
+           | Float(bits: int, lanes: int)
+           | Handle(bits: int, lanes: int)
+
+StructInfo ::= TensorStructInfo(shape: Expr?, dtype: DataType, ndim: int)
+             | ShapeStructInfo(values: [PrimExpr]?, ndim: int)
+             | PrimStructInfo(dtype: DataType, value: PrimExpr?)
+             | ObjectStructInfo()
+             | TupleStructInfo(fields: [StructInfo])
+             | FuncStructInfo(params: [StructInfo]?, ret: StructInfo, purity: 
bool, derive_func: EnvFunc?*)
+
+# expressions
+Expr ::=   Constant(data: NDArray)
+           # scoped to functions or SeqExprs
+         | Var(name_hint: string, struct_info_annotation: StructInfo?)
+           # scoped to DataflowBlocks
+         | DataflowVar(name_hint: string, struct_info_annotation: StructInfo?)
+         | GlobalVar(name_hint: string)
+         | Tuple(fields: [Expr])
+         | SeqExpr(blocks: [BindingBlock], body: Expr)
+         | PrimValue(value: PrimExpr)
+         | StringImm(value: string)
+         | DataTypeImm(value: DataType)
+         | Function(params: [Var], body: Expr, ret_struct_info: StructInfo?, 
is_pure: bool?, attrs: Attrs?)
+         | If(cond: Expr, true_branch: Expr, false_branch: Expr)
+         | ExternFunc(global_symbol: string)
+         | Call(op: Expr, args: [Expr], sinfo_args: [StructInfo], attrs: 
Attrs?)
+         | ShapeExpr(values: [PrimExpr])
+         | TupleGetItem(tuple_value: Expr, index: int)
+         | Op(op_name: string)
+
+# binding blocks (analogous to sequence of statements)
+BindingBlock ::= 
+           BindingBlock(bindings: [Binding])
+         | DataflowBlock(bindings: [Binding])
+
+# bindings (analogous to statements)
+Binding ::= 
+           VarBinding(var: Var|DataflowVar, value: Expr)
+         | MatchCast(var: (Var|DataflowVar)?, struct_info: StructInfo, value: 
Expr)
+
+# Relax programs are IRModules. Modules may bind global variables either to
+# Relax functions or TIR PrimFuncs.
+# The Relax compiler may analyze and modify the TIR PrimFuncs as well.
+Program ::= IRModule(funcs: {GlobalVar: Function|PrimFunc})
+```
+
+### Notes on `derive_func`
+
+The `derive_func` field of `FuncStructInfo` is a macro in the meta-language: 
Given a function call and the variable mapping context, return the `StructInfo` 
of the result. This field is used only at compile time for reasoning about the 
`StructInfo` of calls to `ExternFunc`s.
+
+### Notes on `DataType` and Related Terminology
+
+The representation of datatypes, `DataType`, in the above AST is taken 
directly from TIR. However, the usage of datatypes in Relax is more restricted 
than in TIR.
+1. The `lanes` field for the `Int`, `UInt`, and `Float` datatypes must always 
be 1; we do not directly consider vectorized values in Relax.
+2. The `bits` field for the `Handle` datatype must always be 0, indicating 
that it is `Void` (see below). The `lanes` field for `Handle` should always be 
set to 0 (it will not be used by Relax).
+
+We also define the following special notation for datatypes, to be used in the 
rest of the specification:
+1. `Bool()`: This is shorthand for `UInt(bits=1, lanes=1)`, since TIR does not 
have a separate Boolean type. "True" refers to a value of 1 in this datatype 
and "false" refers to a value of 0. For convenience, we will refer to Boolean 
values as a separate datatype in the specification, due to their significance 
in `If` nodes.
+2. `Void()`: This is shorthand for `Handle(bits=0, lanes=0)`. TIR uses this 
datatype to refer to opaque objects; in Relax, it is used to denote an unknown 
datatype.
+
+## Expression Survey
+
+This specification provides a more detailed description of what each 
expression and `StructInfo` represents and what conditions make them valid. To 
motivate and provide more context for the full specification later in this 
document, this section will briefly summarize the purpose of each node.
+
+1. `Constant` nodes construct tensor constants (n-dimensional arrays of 
scalars).
+2. `Tuple` nodes construct a tuple (immutable fixed-size ordered grouping) of 
Relax values.
+3. `Var`, `DataflowVar`, and `GlobalVar` nodes are all variables, referring to 
named stored values of different kinds. Variables in Relax must be bound 
exactly once. `GlobalVar`s are bound in the `IRModule` itself and refer to 
Relax functions or TIR `PrimFunc`s. `Var` nodes are bound either within 
functions, where they represent function parameters, or in `VarBinding` or 
`MatchCast` nodes in `BindingBlock`s, as we will discuss below. `DataflowVar`s 
are similar to `Var`s and can be bound only within `DataflowBlock`s.
+4. `PrimExpr`s are used to represent dimensions of shapes in `ShapeExpr` and 
`MatchCast` nodes. These represent operations on integers with their own `Var` 
nodes (`tir::Var`), which we will refer to as "shape variables". Shape 
variables can only be used in other `PrimExpr`s and are scoped like `Var` nodes 
(`relax::Var`), which we will call "Relax variables."
+5. `ExternFunc` nodes evaluate into `PackedFunc`s; the implementation will 
look up the registered `PackedFunc` by its global symbol.
+6. `PrimValue` nodes construct immutable scalar values from `PrimExpr`s, 
primarily for interacting with `ExternFunc`s or operators. These scalars are 
boxed within TVM objects, allowing them to be nested inside TVM's containers. 
(By contrast, zero-dimensional tensors defined via `Constant` are mutable.)
+7. `StringImm` nodes construct strings, intended primarily for interacting 
with `ExternFunc`s or operators.
+8. `DataTypeImm` nodes construct representations of TIR datatypes, intended 
primarily for interacting with `ExternFunc`s or operators (e.g., for TIR 
intrinsics that take a datatype as an input).
+9. `Call` nodes represent function calls. The callee argument (the `op`) can 
be an `ExternFunc` node (representing a call to a `PackedFunc`), an `Op` node 
(representing a call to a Relax operator), or an arbitrary expression. 
+    1. `Op` nodes refer to built-in Relax operators, which the compiler is 
free to implement as is deemed appropriate. Certain operators implement 
important operations, like `call_tir` (allows for calling TIR `PrimFunc`s).
+    2. Any other expression must evaluate to a `PackedFunc` or a closure; the 
result of evaluating `op` will then be called with the given arguments. 
+    
+    Calls to `ExternFunc`s and operators may perform side effects, hence it is 
important to reason about whether a function call is permitted inside a 
`DataflowBlock`.
+    
+10. `If` nodes represent branching control flow. First the condition 
expression is evaluated, and it must evaluate to a Boolean scalar. If the 
condition is true, the true branch is evaluated and its result is used; 
otherwise, the false branch is evaluated and its result is used.
+11. `TupleGetItem` nodes represent tuple indexing. The `tuple_value` 
expression must evaluate to a tuple with at least `index + 1` items and the 
item with the given index will be returned.
+12. `SeqExpr` describes a sequence of binding blocks followed by a return 
expression. The `SeqExpr` opens a new scope. Its binding blocks are evaluated 
in order and add new variables to the scope. Binding blocks are either ordinary 
`BindingBlock`s or `DataflowBlock`s and both consist of a series of bindings. 
`DataflowBlock`s are the only kind allowed to introduce bindings with 
`DataflowVar`s and it does not permit any constructs featuring control flow 
(`If` nodes or recursive calls) or calls to (possibly) impure functions. There 
are two different kinds of bindings:
+    1. `VarBinding`s: The `value` expression (the right-hand side of the 
binding) of the binding is evaluated first and is bound to the `var` 
expression, which must be a new `Var` or `DataflowVar` (in a dataflow block). 
The newly bound variable will have that value for the remainder of the scope: 
`DataflowVar`s are scoped only to any later bindings in the `DataflowBlock` in 
which they were defined; `Var`s are scoped to any later bindings within the 
`BindingBlock` in which they were defined, as well as any bindings in 
subsequent `BindingBlock`s in the `SeqExpr` and in the `body` field of the 
`SeqExpr`.
+    2. `MatchCast`s: The `value` expression is evaluated and the result is 
dynamically checked against the structural information given in the 
`struct_info` field.
+        1. The types must match: All `StructInfo` variants correspond to a 
category of value value (`TensorStructInfo` to a tensor value, 
`ShapeStructInfo` to shape values, etc.), so if the structure of `value` does 
not correspond to `struct_info`, an error is triggered. The structure of 
`value` is compared recursively with `struct_info`, so all components of 
`value` must match up with any nested structural information. Special 
comparison rules:
+            1. For comparing tensor values to `TensorStructInfo`, `ndim` must 
match the number of dimensions in the tensor value (unless `ndim` is -1) and 
`dtype` must match the datatype used (unless `dtype` is `Void`). If `shape` has 
been specified, the shape of the value must match that encoded by `shape`; if 
specified, `shape` must be either a `Var` already bound in the current scope or 
a `ShapeExpr`.
+            2. For comparing shape values to `ShapeStructInfo`, `ndim` must 
match the number of dimensions in the shape value (unless `ndim` is -1). If 
`values` has been specified, the shape value must match that encoded by 
`values`.
+            3. «For comparing closures (function values) to `FuncStructInfo`, 
it is necessary for the compiled program to track run-time structural 
information for closures, since it is not possible to introspect the closure; 
this subject will be discussed in further detail later in the document.»
+        2. When comparing tensor values with `TensorStructInfo` or shape 
values with `ShapeStructInfo`, any member of `shape` in `TensorStructInfo` (if 
`shape` is a `ShapeExpr`) or `values` in `ShapeStructInfo` that consists of a 
single new (hitherto unbound) shape variable is treated as a binding: The shape 
variable is bound to the size of the corresponding dimension of the value being 
matched. 
+        3. If there is a variable provided, the value is bound to the `var` 
expression (if the variable is omitted, the structural check is performed and 
any shape variables are updated, but no new binding is introduced). Shape 
variables introduced in a `SeqExpr` are similarly scoped to the `SeqExpr`.
+    
+    The `SeqExpr`'s `body` expression is allowed to reference any `Var`s 
introduced within the `SeqExpr`'s binding blocks in addition to those that were 
in the outer scope; the `body` expression is evaluated after the binding blocks 
and its value is what is returned. Any Relax variables and shape variables 
introduced in the `SeqExpr` are removed from scope after the expression 
finishes evaluating.
+    
+13. `ShapeExpr` nodes construct shape literals, which are immutable 
collections of shape dimensions. The `PrimExpr`s within it describe how to 
compute each dimension; they are free to use any shape variables that are in 
scope.
+14. `Function` nodes represent function definitions, taking in the listed 
parameters and evaluating the body expression in a new scope (meaning any 
variables defined from within the function cannot be referenced outside it). 
Function definitions may be nested in any other expression and they evaluate 
into closure values, ensuring that functions are first-class. Closures capture 
any variables from the outer scope that are used in their body, both Relax 
variables and shape variables. Note that function definitions themselves are 
anonymous—a function must be registered in the `IRModule` (bound to a 
`GlobalVar`) or appear on the right-hand side of a binding to have a name in 
order to be called recursively. 
+    
+    The function can have structural annotations on the parameters and a 
structural annotation for the return value. When the function is called, the 
annotations on parameters are checked against the argument values in similar 
fashion to `MatchCast` and can introduce new shape variables that are scoped to 
the function. Additionally, the structural information of the return value is 
checked against the annotation before the call returns.
+
+    In addition to the structural annotations for the parameters and the 
return value, the `is_pure` field on a `Function` node serves to annotate 
whether the `Function` itself is pure (has no visible side effects) or not. The 
`StructInfo` system tracks purity in order to judge what calls are permitted 
inside `DataflowBlock`s. At this time, Relax makes no attempt to infer the 
purity of functions, so it is required for users to annotate the purity (if no 
annotation is provided, `is_pure` will be treated as true; since this is by far 
the most common case for deep learning applications, it is in practice 
necessarily to annotate purity if the function is _impure_).
+    
+    «A function mapped bound to a `GlobalVar` can have a `global_symbol` 
attribute defined to indicate that it should be externally linked externally 
(be accessible outside the `IRModule`). The absence of a `global_symbol` 
attribute on a function definition bound to a `GlobalVar` indicates that it is 
"private" and hence can be called only within the `IRModule`.»
+    
+## Purity and Dataflow Blocks
+
+A function or operator is called "pure" if it does not have side effects, 
which refers to any change in program state besides returning a result. Side 
effects include mutating values other than those they create, aborting the 
program, or file I/O (including writing to the console). Purity is a useful 
property for compiler optimizations, since calls to pure functions can be 
reordered or duplicated or (if the result is unused) eliminated without 
changing any other program behavior. Most deep learning operators are pure, as 
they perform arithmetic on tensors and return a new tensor containing the 
result.
+
+Above, it is mentioned that `DataflowBlock`s are not allowed to contain 
constructs featuring control flow (`If` nodes or recursive calls to the current 
function) or calls to impure functions. This ensures that `DataflowBlock`s 
represent a directed acyclic graph of pure operations, which is similar to the 
graph-like abstractions of traditional deep learning frameworks. This allows 
many common optimizations from past frameworks to be directly adapted to 
`DataflowBlock`s without having to accommodate additional reasoning about more 
expressive features like control flow and side effects.
+
+There is one visible side effect that Relax permits inside otherwise "pure" 
functions, namely exiting the program with an error. This can arise in the 
following cases:
+
+- Casting errors (from `MatchCast` or from implicit structural information 
checks upon calling a Relax function)
+- Errors raised by otherwise pure Relax operators or `PackedFunc`s. Since the 
purity of operators or `PackedFunc`s must be manually registered, this means 
that it is permissible to register an operator or `PackedFunc` as being pure if 
its only side effect is issuing an error in some cases.
+
+Even though an abnormal program exit is a visible side effect and removing or 
reordering it changes the observable semantics, it would be too great a 
restriction to prohibit error checking inside `DataflowBlock`s. Relax does not 
have any notion of exception handling, so the only consequence of a failed 
safety check can be exiting the program. It is permissible for the compiler to 
reorder, duplicate, or eliminate `MatchCast`, or otherwise pure operations that 
have the potential of failing, provided that doing so does not change the value 
returned by the program or any other visible behavior.
+
+Note that in some programming languages like Koka, non-termination is also 
considered a side effect, since it can in some sense be "observed" by a user 
and affects the visible behavior of a program (e.g., if there is an infinite 
loop before a print statement, the print will never happen). However, since 
non-termination cannot be automatically detected in general and is unlikely to 
arise in deep learning models, we do not attempt to systematically track 
non-termination in Relax. In general, the Relax compiler is allowed to reorder 
or remove otherwise pure function calls even if they may not terminate. For 
example, if a pure function `f` that returns an integer scalar does not 
terminate, it is permissible in principle to rewrite `f() - f()` to 0.
+
+Exiting with an error and infinitely looping are traditionally considered 
"[divergence](https://en.wikipedia.org/wiki/Divergence_(computer_science))" in 
the programming languages literature. As a general principle, Relax's compiler 
is permitted to turn a program that diverges into a program that does not 
diverge (provided that no other visible effects change) so long as it never 
transforms a program that does not diverge into one that diverges.
+
+## Structural Information (`StructInfo`) System Survey
+
+Analogously to a type system in most languages, Relax tracks structural 
information (referred to as `StructInfo` in the implementation) related to the 
categories of values in Relax:
+1. `TensorStructInfo` corresponds to tensor values, giving the scalar data 
type, the number of dimensions (rank), and an expression that computes the 
tensor's shape (either a `ShapeExpr` or a `Var`), all of which are optional.
+2. `TupleStructInfo` corresponds to tuple values, giving the `StructInfo` for 
each member of the tuple.
+3. `PrimStructInfo` corresponds to `PrimValue`s (immutable scalar values), 
giving their TIR datatype.
+4. `ShapeStructInfo` corresponds to shape values, optionally giving the number 
of dimensions in the shape and an expression that computes the shape's 
dimensions (either a `ShapeExpr` or a `Var`).
+5. `FunctionStructInfo` corresponds to function values (closures) and 
`PackedFunc`s (external functions), giving the types of the parameters, the 
return type, and whether the function is pure.
+6. `ObjectStructInfo` is a parent to all Relax `StructInfo` and corresponds to 
all the values above as well as any values returned by `PackedFunc` calls that 
do not fit in the above categories.
+
+`StructInfo` is assigned to every variable in scope and every type of 
expression based on the values it returns via a set of inference rules defined 
later in the specification, making use of subtyping to assign more general 
`StructInfo` when a more specific one cannot be determined. «Relax is strongly 
typed, meaning that if the `StructInfo` inferred is less specific than the one 
expected, an error will be issued and an explicit check via `MatchCast` will be 
required.»
+
+In Relax, tensor shapes are not statically handled in the type system, even 
though it would be greatly beneficial for the compiler to make use of shape 
information for static optimizations. Instead, shape information is tracked 
using Relax's structural information system, in which every expression has 
structural information associated with it (like tensor shapes) that is more 
expressive than its type. `StructInfo` can convey richer properties about 
expressions, like tensor shapes, and can facilitate a greater degree of static 
reasoning. However, when it is not feasible for the compiler to draw 
conclusions about structural information, this information can be checked 
dynamically via `MatchCast`. The structural information is essentially an 
extended type system, so `MatchCast` also serves to handle type casting.
+
+---
+
+# Top-level Program Organization: `IRModule`
+
+As with Relay, the top level of organization for a Relax program is an 
`IRModule`. An `IRModule` contains mappings of global variables to functions, 
both Relax functions as well as TIR functions (which can be called from Relax). 
The global function called `main` is usually considered the entry point to the 
program (meaning that execution starts by calling that function), though any 
function with a `global_symbol` attribute can be specified as the entry point 
during compilation. In the AST (see below), the names of Relax functions in the 
`IRModule`s are `GlobalVar` nodes.
+
+Oftentimes, compiler passes operate only on particular functions or add new 
functions to the `IRModule`, but a pass can operate over the entirety of a 
Relax program by iterating through all the functions in an `IRModule`.
+
+# Values in Relax
+
+Here are the classes of values that Relax operates over, meaning that they can 
be assigned to variables or be the result of evaluating expressions.
+
+- *Tensors* are n-dimensional arrays of scalar values (which can be signed or 
unsigned integers of fixed bitwidths, floats of fixed bitwidths, or Boolean 
values). A tensor's *shape* is a tuple of the size of each dimension; the 
number of dimensions is a tensor's *rank*. For example, a vector (1, 2, 3) is a 
rank-1 tensor of shape `(3,)`. Note that scalars are tensor values with a rank 
of 0, meaning that their shape is `()`.
+- *Tuples* represent a fixed-size immutable grouping of other Relax values 
(tensors, closures, shapes, objects, or other tuples, to an arbitrary degree of 
nesting). Note that an empty tuple, i.e., `()`, also called "unit" in 
functional programming, is commonly used as the return value for operations not 
intended to return a value (as may be the case in some `PackedFunc` or operator 
calls that have side effects).
+- *Closures* are the values resulting from evaluating Relax function 
expressions; closures can be passed around like other values, ensuring that 
functions are first-class in Relax. Functions defined in Relax can capture 
variables from outer scopes. A 
[closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) 
consists of a function and a mapping of any variables "captured" (those are 
*free variables* in the function body, variables from an outer scope that are 
neither arguments nor defined within the function but are used in the function) 
to their values. Closures capture both Relax-level local variables and shape 
variables from outer scopes. A closure also stores a name for itself when the 
body contains recursive calls. «Closures additionally carry some *run-time 
structural information* (RTSI) indicating their argument and result structures, 
in order to facilitate dynamic structural checks (since it is not otherwise 
possible to introspect the function contained within 
 a closure); the precise form of the RTSI is left up to the compiler 
implementation to determine so long as `MatchCast` can verify the structure of 
a closure, including whether it is pure. Closures can be evaluated in a call 
node, which results in calling the function with the call's arguments and the 
captured values.»
+- *Tensor shapes* (shape values) are immutable tuples of integers describing a 
tensor shape, obtained by evaluating `ShapeExpr`s.
+- *Packed functions* (`PackedFunc`s or external functions) represent arbitrary 
opaque functions implemented in TVM. That is, packed functions are routines 
that are defined outside of Relax and cannot be inspected by the compiler. They 
can perform side effects and return arbitrary values.
+- *TIR `PrimFuncs`* are functions in TIR. They are usually invoked using the 
`call_tir` operator, but can be called on their own as first-class functions.
+- *Primitive values* (`PrimValue`s) represent immutable scalar values that are 
primarily intended for being passed to external procedures, like calls to 
`PackedFunc`s. As a rule of thumb, scalar values intended for arithmetical 
computations should be 0-rank tensors while scalar values meant to serve as 
metadata should be `PrimValue`s.
+- Additionally, there are further  *arbitrary objects* that do not belong in 
the above categories. These can be returned by `PackedFunc`s and operators. 
Though Relax expressions other than `PackedFunc` and operator calls cannot use 
those objects, Relax should pass around these values faithfully. In the future 
we may add more value types in order to distinguish between different objects, 
but at present we treat these all as arbitrary values with `ObjectStructInfo`. 
Note that, for now, strings and TIR datatypes are also treated as opaque 
objects. Another noteworthy value in this category is the _null object_ (the 
result of returning a null pointer in C++ or passing in `None` through the 
Python FFI), which is returned by the `null_value()` operator.
+
+## Representation of Values at Run Time
+
+Because Relax supports calls to arbitrary `PackedFunc`s that can operate on a 
low level, it is necessary to define a convention for how values will be 
represented at run time. At this time, the specification does not require any 
specific representation and permits compiler implementations to choose their 
own representations, provided that each value type listed above can be 
recognized at run time (for dynamic `StructInfo` checks). This means that Relax 
programs that call `PackedFunc`s directly are not portable across compiler 
implementations: The `PackedFunc`s used must be able to operate on the run-time 
representations of values.
+
+Possible specification in terms of the TVM object system:
+
+- Tensors are represented at run time as `NDArray`s (see 
`include/tvm/NDArray.h`).
+- Tuples are represented using TVM `Array`s (in contrast to `NDArray`s), which 
are immutable (see `include/tvm/runtime/container/array.h`).
+- At run time, closures are represented as a `ClosureObj` (see 
`include/tvm/runtime/container/closure.h`); in the Relax VM these more 
specifically use the `VMClosureObj` (see 
[`https://github.com/tlc-pack/relax/blob/relax/include/tvm/runtime/relax_vm/executable.h`](https://github.com/tlc-pack/relax/blob/relax/include/tvm/runtime/relax_vm/executable.h)).
+- Shape values are represented at run time as a `ShapeTuple` (see 
`include/tvm/runtime/container/shape_tuple.h`).
+- Strings are represented using TVM's `String` container (see 
`include/tvm/runtime/container/string.h`).
+- We require objects other than the above values used by and returned by 
`PackedFunc` to inherit from TVM's `Object` class (defined in 
`include/tvm/runtime/Object.h`). Note that `PackedFunc`s are capable of using 
and returning all TVM POD (plain-old data) values (see 
`include/tvm/runtimes/packed_func.h`), which includes some representations that 
do not inherit from `Object`. In the future, we may define semantics for other 
values, but at present, these are *unsupported* in Relax and we make no 
guarantees about the semantics of calling `PackedFunc`s that use or return 
anything that does not inherit from `Object`.
+
+# Variable Scoping
+
+There are four relevant scopes in Relax, which determine where variables are 
visible and can be used:
+
+1. Global: `GlobalVar`s can be referenced from any function in the `IRModule`, 
whether a Relax function or a TIR `PrimFunc`. All global functions are visible 
to each other and to themselves, allowing for mutual recursion.
+2. Function: The parameters to a function (ordinary `Var` nodes) can be 
referenced from anywhere in that function. In a recursive binding (a `Binding` 
node where the RHS is a `Function` node or `GlobalVar` being mapped to a 
function at the `IRModule` level), the variable being bound is also scoped to 
that function, allowing for defining a recursive function.
+3. `SeqExpr`: `Var` nodes defined in a `BindingBlock` in a `SeqExpr` node can 
be referenced in any later binding within the same `BindingBlock`, in any 
binding within any later `BindingBlock` in that `SeqExpr` node, or in the 
`SeqExpr`'s body expression. The variables defined in the `BindingBlock`s leave 
scope once the `SeqExpr` returns.
+4. `DataflowBlock`: `DataflowVar`s introduced in a `DataflowBlock` can be 
referenced in any later binding within that `DataflowBlock`, but leave scope 
*once that `DataflowBlock` finishes executing*. Definitions in a 
`DataflowBlock` that are intended to leave the `DataflowBlock` should be bound 
to an ordinary `Var`.
+
+Note that Relax variables must be bound _exactly_ once. A global variable is 
bound if it is mapped to a function in the `IRModule` and a local variable is 
bound if it appears as a function parameter or if it appears on the left-hand 
side (LHS) of a binding (`VarBinding` or `MatchCast`).
+
+«If there is another binding to a local variable with the same name as an 
already-bound variable, that is binding is considered to _shadow_ the previous 
binding, i.e., it is a binding to a new, distinct variable that happens to have 
the same name as the existing variable. The new, shadowing variable will exist 
only in the current scope; if the older variable was defined in an outer scope, 
then future uses of that name will refer to the older variable. [See the 
Wikipedia page for more information on variable 
shadowing.](https://en.wikipedia.org/wiki/Variable_shadowing)»
+
+Below is an example of shadowing, in pseudocode:
+
+```python
[email protected]
+def func(x: Tensor) -> Tensor:
+    if True:
+        # the true branch will be a nested SeqExpr and hence a new scope
+        # this x will shadow the function parameter x
+        x = R.const(1)
+        R.print(x) # prints 1
+        # the inner x goes out of scope
+    else:
+        R.print("not executed")
+    R.print(x) # this x is the function parameter
+    return x
+```
+
+# Normal Form
+
+To simplify the writing of Relax passes, we define a normal form for Relax 
programs, based on the [administrative normal 
form](https://en.wikipedia.org/wiki/A-normal_form) (A-normal form, or ANF). See 
[this post](https://matt.might.net/articles/a-normalization/) by Matt Might for 
a discussion of some of the advantages of ANF in traditional compilation; in 
particular, ANF results in programs without nesting, which is very convenient 
for writing program transformations. Because the `StructInfo`-checking rules 
for operators rely on macros (`FInferShapeInfo`), _this means that the 
structure of the program can affect `StructInfo` inference_. Putting programs 
into normal form (and lacking nesting) not only simplifies the writing of these 
macros but it also ensures that these `StructInfo`-checking rules will be 
predictable, hence _it is required to transform programs into normal form_ 
before applying `StructInfo` checking.
+
+The normal form for Relax is very similar to ANF; differences will be noted. 
Here are the criteria required for a program to be in normal form:
+1. Within a `SeqExpr`, the right-hand side of any binding (the `value` field 
in the AST) must either be a "leaf expression" or a non-leaf expression where 
all subexpressions are leaf expressions. Leaf expressions are the following: 
Variables (`Var`, `DataflowVar`, or `GlobalVar`), `Constant`, `ShapeExpr`, 
`PrimValue`, `StringImm`, `DataTypeImm`, or (_unlike_ ANF) `Tuple`. `Tuple` 
nodes are considered "leaf" expressions even though they contain nesting purely 
for convenience in writing passes; many operators rely on grouping arguments 
using tuples, so that is a form of nesting permitted and expected. Otherwise, 
non-leaf expressions used as subexpressions must be bound to variables; this 
includes any non-leaf expressions nested inside a `Tuple`.
+2. `SeqExpr`s may appear only in the following locations:
+    1. In the `body` field of a `Function` node.
+    2. In the `true_branch` and `false_branch` fields of `If` nodes.
+3. In fact, the `body` field of a `Function` node and the `true_branch` and 
`false_branch` fields of `If` nodes _must_ be `SeqExpr`s. If these fields are 
not `SeqExpr`s, they must be "wrapped" in a `SeqExpr`.
+4. Within a `SeqExpr`, `BindingBlock`s must be consolidated. For example, if 
there is a `BindingBlock` that comes after another `BindingBlock`, the two 
blocks should be combined to form a single `BindingBlock` with all the bindings 
in the same order. Consecutive `DataflowBlock`s should be consolidated as well. 
Empty `BindingBlock`s should be dropped. However, a `DataflowBlock` cannot be 
consolidated with an ordinary `BindingBlock`. If all the `BindingBlock`s are 
empty, then the `blocks` field of the `SeqExpr` should be set to an empty list.
+
+Programs that are parsed should be "normalized" before performing `StructInfo` 
checking or before doing any further optimizations. Note that the process of 
"flattening" `SeqExpr`s and consolidating `BindingBlock`s does increase the 
visibility of the variables in those `SeqExpr`s and `BindingBlock`s, but this 
is safe, since it will not cause any variable to be referenced outside of its 
original scope. The specification does not require any particular method of 
normalizing a program so long as the final program conforms to the above-listed 
criteria. Here is a general approach:
+1. For each function in the `IRModule`, ensure that the body is a `SeqExpr`. 
If the body is not a `SeqExpr`, wrap the function body in a `SeqExpr`, creating 
a new `BindingBlock` to hold `VarBinding`s for any non-leaf expressions that 
need to be bound to variables.
+2. If the function body is already a `SeqExpr`, consolidate all 
`BindingBlock`s, then check if the `body` field of the `SeqExpr` is a leaf 
expression. If not, bind it to a new var in the final `BindingBlock` and 
replace the `SeqExpr` body with the new var.
+3. If the function body is not a `SeqExpr`, then recurse down the body's AST, 
binding any nested non-leaf expressions to a var in the current scope (doing 
this process in breadth-first order from left to right will respect the 
evaluation order in the semantics). If the body itself is a non-leaf 
expression, finally bind it to a var and have the final `SeqExpr` return the 
new var.
+4. If an `If` node is encountered, ensure the `true_branch` and `false_branch` 
fields are `SeqExpr`s (consolidate `BindingBlock`s if necessary) or "wrap" them 
in `SeqExpr`s in the same manner as the function body.
+5. If a `SeqExpr` node is encountered as the `value` node in a binding, 
"flatten" the `SeqExpr` by adding its bindings to the current scope and 
replacing the `SeqExpr` with its body. If the `SeqExpr` body is a non-leaf 
expression, normalize it recursively in the same manner as in step 3 before 
replacing the binding. Note that if the current scope (the location of the 
binding) is a `DataflowBlock` and the nested `SeqExpr` contains an ordinary 
`BindingBlock`, that indicates a malformed program.
+
+
+# Well-Formedness Criteria
+
+Prior to `StructInfo` checking, Relax programs must conform to certain 
syntactic criteria to be valid, which includes conforming to the expectations 
of the above-described normal form.
+
+The following criteria apply to all programs (including before normalization):
+1. `DataflowVar`s can be bound only inside `DataflowBlock`s. Additionally, a 
`DataflowVar` may not be used outside of the `DataflowBlock` in which it is 
defined.
+2. A `Var` of any kind used in the program must be either a function parameter 
or appear on the LHS of a binding exactly once. In the binding where a `Var` is 
defined, the same `Var` is permitted to occur in the RHS of the binding only if 
the binding is defining a function (i.e., local functions are permitted to be 
recursive).
+3. A `Var` of any kind may not appear before it is bound. Namely, if a `Var` 
is bound in a `BindingBlock` in a `SeqExpr`, that `Var` may not appear in 
bindings that precede the one where it appears on the LHS.
+4. «A return structural annotation for a function is not allowed to use any 
shape variables that are not in scope at the function definition. That is, the 
only shape variables that can appear on the return structural annotation are 
those defined in the outer scope or those introduced in the argument structural 
annotations.»
+5. In each function, `PrimExpr` variables (shape variables) similarly may not 
appear in `ShapeExpr`s or shape annotations before the shape variables are 
bound (either in function signatures or `MatchCast` bindings). A shape variable 
is bound only when it appears in a dimension by itself (for example, a 
dimension consisting of `x` will bind `x`; however, `2*x` is not a binding and 
is considered an error if `x` has not yet been bound) in a `MatchCast` node or 
a function argument shape annotation.
+6. In a function signature, every shape variable must appear in a binding 
position at least once; however, (for convenience) we do not enforce any 
ordering amongst the function arguments—for example, it is permitted to have a 
shape `x * y` in the first argument and have `x` and `y` appear in binding 
positions in later arguments. In such a case, the dimensions corresponding to 
the binding positions will be checked first, allowing the variables to be 
bound. Then the other dimensions will be checked.
+7. The following constructs are not permitted to occur inside 
`DataflowBlock`s, which must be side effect– and control flow–free: 
+    1. Recursive calls to the current function
+    2. Calls to a global function that is mutually recursive with the current 
function
+    3. `If` nodes
+    
+    Calls to Relax functions, `ExternFuncs`, or `Op`s that are not pure are 
also not permitted, but this must be detected during `StructInfo` checking.
+    
+8. «For functions that contain recursive calls to themselves or mutually 
recursive global functions (i.e., those where function `a` calls function `b` 
and function `b` calls function `a`), a return structural annotation is 
*required*.»
+9. `Op` nodes may appear only as the `op` argument to `Call` nodes. 
+10. If a variable has a `StructInfo` annotation, the `ndim` of any 
`TensorStructInfo` and `ShapeStructInfo`s must match the number of dimensions 
in their `shape` and `values` fields, respectively.
+11. A function definition inside a `DataflowBlock` may not use `DataflowVar`s 
from the outer scope in its body. We do not define closure capturing for 
`DataflowVar`s.
+12. «At least one global function in the `IRModule` must be externally linked 
(have a `global_symbol` attribute) in order to serve as a program entry point.»
+13. «If a global function has a defined `global_symbol` attribute, the 
`global_symbol` name must be the same as the `GlobalVar`'s name hint.»
+14. If the `shape` field of a `TensorStructInfo` in any structural annotation 
is given, the only permissible expressions are `Var` (the variable must be in 
scope at the location of the annotation) or `ShapeExpr` (in which any shape 
variables used must already be in scope, unless the `TensorStructInfo` is the 
`struct_info` field of a `MatchCast`, in which case a new shape variable is 
allowed to appear in a dimension by itself). Additionally, if the `shape` field 
is a `ShapeExpr`, the number of dimensions must match the `ndim` field.
+15. If the `values` field of a `ShapeStructInfo` in any structural annotation 
is given, any shape variables used in it must already be in scope, unless the 
`ShapeStructInfo` is the `struct_info` field of a `MatchCast`, in which case a 
new shape variable is allowed to appear by itself as a member of `values`. 
Additionally, the `ndim` field must match the length of `values`. 
+16. Similarly, if the `value` field of `PrimStructInfo` is defined, any shape 
variables used in it must already be in scope, unless the `PrimStructInfo` is 
the `struct_info` field of a `MatchCast`, in which case a new shape variable is 
allowed to appear by itself as `value`.
+17. The `params` and `derive_func` field may not be simultaneously defined in 
a `FuncStructInfo` annotation; that is, if one is defined, the other must not 
be defined. Additionally, at least one of `params` and `derive_func` _must_ be 
defined for each `FuncStructInfo` in an annotation.
+18. `PrimValue` nodes are intended only to be used with `value`s consisting of 
TIR `IntImm`s and `FloatImm`s (with `lanes` set to 1).
+19. `PrimStructInfo` annotations should use only the `Int`, `UInt`, or `Float` 
datatypes for their `dtype` fields.
+20. Per [the notes on `DataType`](#notes-on-datatype-and-related-terminology), 
any `DataType` annotation must have a `lanes` value of 1 for the `Int`, `UInt`, 
or `Float` datatypes and a `lanes` value of 0 for the `Handle` (`Void`) 
datatype. Additionally, `bits` must be 64 for `Void`. The supported bitwidths 
for `Int` and `UInt` are 1, 8, 16, 32, and 64; the supported bitwidths for 
`Float` are 16, 32, and 64.
+21. If a `Function` `f` has an `attrs` field that includes the attribute 
`relax.force_pure`, `f`'s `is_pure` field must be set to `True`.
+22. For `PrimStructInfo`, if the `value` field is defined, the TIR `dtype` for 
the `PrimExpr` must match the `PrimStructInfo`'s `dtype` field (i.e., the 
datatypes must be consistent).
+
+Additionally, the criteria for normal form listed in [the previous 
section](#normal-form) must apply to any program that has been normalized.
+
+# Structural Information (`StructInfo`) in Relax
+
+Structural information in Relax is intended to enforce basic guarantees that 
values are passed correctly between expressions, while also analyzing more 
complex properties like tensor shapes in a _"best-effort"_ fashion. Namely, 
anything that cannot be proved statically can instead be checked at run time. 
Each Relax expression has structural information associated with it. The 
best-effort nature of the structural system in Relax means that the analysis 
may detect _some_ errors at compile time and report them, but it may give 
warnings when it _cannot_ draw conclusions, perhaps suggesting that dynamic 
checks via `MatchCast` should be inserted. Note that the precision of the 
static analysis can potentially be improved by some compile-time optimizations 
like constant propagation, function inlining, and other partial evaluation–like 
transformations.
+
+Tensor shapes are the primary motivation for including structural information 
in Relax, as shape information is particularly important for memory planning. 
In Relay, shapes are part of tensor types and there is much analysis of tensor 
shapes done at compile time. While this allows Relay's type system to make 
strong guarantees about tensor shapes, it results in greater complexity in type 
checking and makes it difficult to implement new operators or handle cases like 
tensors with symbolic shapes. By contrast, Relax's `StructInfo` system uses 
expressions to encode tensor shapes, which allows for using shape variables and 
arithmetic expressions to encode a rich variety of shape constraints. Note, 
however, that the structural system could potentially be extended to encode and 
analyze further information, like tensor sparsity or density.
+
+## Defining Structural Information
+
+The structural information in Relax corresponds to the values in the language:
+* `TensorStructInfo` describes tensor values. The `dtype` field gives the 
datatype (with `Void` indicating a statically unknown datatype), the `ndim` 
field gives the rank (with -1 indicating a statically unknown rank). Unlike 
`DynTensorType`, there is an optional `shape` field which, if defined, 
describes the shape of the tensor using either a `ShapeExpr` or a `Var` (with 
`ShapeStructInfo`). If `shape` is a `ShapeExpr`, the `PrimExpr`s in the 
`ShapeExpr`'s dimensions describe how to compute each dimension of the shape 
(or are constants). If `shape` is a `Var`, the `Var` can assign the result of 
an arbitrary computation that returns a shape value, which can be useful for 
memory planning.
+* `ShapeStructInfo` describes shape values. It has an `ndim` field that gives 
the number of dimensions in the shape (with -1 indicating that it is statically 
unknown). It additionally has an optional `values` field. If defined, `values` 
gives a list of `PrimExpr`s that indicate how to compute the dimensions of the 
shape, potentially providing further information for static analyses.
+* `PrimStructInfo` describes `PrimValue`s, giving their TIR datatype.
+* `TupleStructInfo` describes tuple values, namely by giving the `StructInfo` 
for each of the tuple's members via `fields`.
+* `FuncStructInfo` describes closure values or `PackedFunc`s. There are two 
ways in which to specify `FuncStructInfo`:
+    1. By specifying `params` and `ret` (for closures). `params` gives the 
`StructInfo` corresponding to each of the function's parameters and `ret` gives 
the `StructInfo` corresponding to the result of calling the function. In this 
case, the `derive_func` field is left undefined.
+    2. By giving a `derive_func` macro (for `PackedFunc`s). The `derive_func` 
macro is takes a call to the corresponding `PackedFunc` and the variable 
mapping context and returns the `StructInfo` of the result. In this case, the 
`params` field is left undefined and the `ret` field is ignored.
+* `ObjectStructInfo` describes arbitrary object values.
+
+### Expressing Shape Dimensions
+
+A tensor shape is a tuple of TIR `PrimExpr`s, where each `PrimExpr` 
corresponds to a dimension. The use of TIR `PrimExpr`s for shape dimensions 
allows shape computations to express complex constraints that include variables 
and integer arithmetic expressions in addition to just constant dimensions.
+
+**Scope of Shape Variables**
+
+New shape variables can be bound in two places in a Relax program: In 
`TensorStructInfo` or `ShapeStructInfo` annotations on function parameters or 
as the `struct_info` parameter in a `MatchCast` binding. Shape variables used 
in the function signature are scoped to the entire function in which they 
appear (including in the return structural annotation). Shape variables used in 
`MatchCast` bindings are scoped only to the `SeqExpr` in which they appear.
+
+**Informal Semantics of `PrimExpr`s for Dimensions**
+
+1. Shape variables can be bound to a value exactly once, either at the start 
of a function for shape annotations on function arguments or in `MatchCast` 
bindings. In particular, matching a `PrimExpr` consisting only of an 
uninitialized shape variable is treated as its binding (see below on 
`MatchCast`). After a shape variable has been bound for the first time, future 
uses of it will refer to the same value.
+2. It is not legal to use a shape var that has not yet been bound. This 
results in an error at compile time.
+3. «Local functions will "capture" defined shape variables from the parent 
scope with their present values in the resulting closure.»
+4. If all variables in the `PrimExpr` are defined, `PrimExpr` arithmetic will 
generally be evaluated according to the semantics of TIR.
+
+### Evaluating `MatchCast`
+
+Because structural information is checked in a "best-effort" fashion, it is 
not always possible for the compiler to statically draw conclusions about all 
details of a given value's structural information. Hence, `MatchCast` allows 
for checking this information at run time, similar to a typecast. However, 
`MatchCast` also allows for binding shape variables in the process of pattern 
matching, hence the "match" portion of its name.
+
+This section describes the run-time checking performed by `MatchCast(var, 
value, struct_info)`, for each combination of value and structural annotation 
(if `var` is defined, then `value` will be bound to `var` as discussed in the 
[general section on semantics](#detailed-semantics)). If any check given below 
fails, an error is raised by the `MatchCast`.
+
+1. If `struct_info` is `ObjectStructInfo`, then no additional check is 
performed. All values in Relax are objects.
+2. If `struct_info` is `TensorStructInfo(ndim, dtype, shape)`, then check that 
`value` is a tensor value, that it has a rank of `ndim` (if `ndim` is not -1), 
a datatype of `dtype` (if `dtype` is not `Void`). If `shape` is defined, 
consider the following cases:
+    1. If `shape` is a `Var`, then check that the concrete shape of `value` 
matches the value bound to the `Var`.
+    2. If `shape` is a `ShapeExpr`, then compare the fields of the `ShapeExpr` 
to the concrete shape of `value`, dimension by dimension (comparing the `i`th 
field of the `ShapeExpr` to the `i`th dimension of the shape of `value`). Give 
an error if the number of the dimensions does not match the number of fields in 
the `ShapeExpr`.
+        1. If a field of the `ShapeExpr` consists of only an unbound shape 
variable, then bind that variable to the value of the dimension.
+        2. Otherwise, evaluate the field of the `ShapeExpr` and ensure that it 
matches the concrete value of the dimension.
+3. If `struct_info` is `PrimStructInfo(dtype, v)`, then check that `value` is 
a `PrimValue` and that the underlying scalar has datatype `dtype` in TIR 
(according to TIR's type-checking rules). If `v` is defined, then check that 
`value` and `v` match numerically.
+4. If `struct_info` is `ShapeStructInfo(ndim, values)`, then check that 
`value` is a shape value, that it has `ndim` dimensions (if `ndim` is not -1). 
If `values` is defined, then compare it to the concrete shape value (comparing 
the `i`th member of `values` to the `i`th field of the shape value):
+    1. If the `i`th member of `values` consists of only an unbound shape 
variable, then bind that variable to the `i`th field of the the concrete shape 
value.
+    2. Otherwise, evaluate the `i`th member of `values` and check that it is 
equal to teh `i`th field of the concrete shape value.
+5. If `struct_info` is `TupleStructInfo(fields)`, then check that `value` is a 
tuple value with `n` fields, where `n` is the length of `fields`. Also 
recursively check the `i`th field of the tuple value against the `i`th member 
of `fields`.
+6. If `struct_info` is `FuncStructInfo(params, ret, purity, derive_func)`, 
then if `params` is defined, check that `value` is a closure value; if 
`derive_func` is defined, check that `value` is a `PackedFunc`. No further 
validation may be done on a `PackedFunc`. «If `value` is a closure value, then 
it can contain run-time structural information indicating its purity and the 
structural information of its intended arguments and return value that can be 
compared against `purity`, `params`, and `ret`.»
+
+### Checking Structural Information at the Start and End of a Function
+
+«Shape variables are bound at the start and end of a function or in 
`MatchCast` bindings. This checking is done similarly to `MatchCast`, though 
with a slight difference: per rule #6 in under the [well-formedness 
criteria](#well-formedness-criteria), we allow shape variables to appear in 
arguments in any order so long as shape variables appear in a binding position 
at least once. This requires us to check the shapes of arguments dimension by 
dimension in a specific order.

Review Comment:
   This approach was discussed here: 
https://github.com/apache/tvm/pull/15577#discussion_r1307947120
   
   I am not sure this is the best policy, since it makes the checking of 
function signatures more complex. However, I am led to understand that this is 
more convenient in certain situations. We should be sure the implementation 
follows this policy.



-- 
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]

Reply via email to