Open
Description
Problem:
There are currently two Exo statements that have a LHS: reduce and assign. However, the LHS data is fused within the statement node:
stmt = Assign( sym name, type type, string? cast, expr* idx, expr rhs )
| Reduce( sym name, type type, string? cast, expr* idx, expr rhs )
| ...
Which leads to similar behavior of the equivalent cursors at the API:
class AssignCursor(StmtCursor):
def name(self) -> str:
def idx(self) -> ExprListCursor:
def rhs(self) -> ExprCursor:
This generally leads to less-ergonomic code. Consider the following example:
get_symbol_dependencies(proc, cursor) # Gets all the symbols that some cursor depends on
@proc
def foo(a: f32[2], b: f32[2]):
for i in seq(0, 2):
a[i] = b[i]
# I want the symbol dependencies of the assign statement
get_symbol_dependencies(proc, assign) # Okay great I gave a cursor that points to the statement and got {'a', 'i', 'b'} back!
# I want the symbol dependencies of the LHS of the assign statement
# but no way for me to point to the LHS!
# I could write the following
{assign.name()} + get_symbol_dependencies(proc, assign.idx()) # Should give me {'a'} + {'i'} = {'a', 'i'}
# But that's less-ergonomic than writing:
get_symbol_dependencies(proc, assign.lhs()) # I get the result {'a', 'i'}
# This final version is clean, direct, and self-explanatory
Consider this other example:
def get_buffer_accesses(proc, cursor, name):
"""
Returns a list of cursors to all accesses to the buffer named `name` in the subtree of `cursor`
"""
cursors = get_cursors_in_subtree(proc, cursor)
check = lambda c: isinstance(c, (ReadCursor, ReduceCursor, AssignCursor)) and c.name() == name
# ugh I had to specify three types
return filter(check, cursors)
# Not only I had to specify three types which seems excessive,
# I have also had to go into the mental effort of remembering all the types that can access a buffer.
# Alternatively, I would like to write something like the following:
check = lambda c: isinstance(c, AccessCursor) and c.name() == name
Proposal:
I have a few options below, but these are just ideas I came up with; I am not really a big fan of any of them. I am sure this problem isn't related to Exo in particular and there is a more canonical way of dealing with this if anyone can provide pointers.
Option 1: Extend the expression type
module LoopIR {
proc = ...
fnarg = ...
stmt = Assign( sym name, type type, string? cast, expr* idx, expr rhs ) # Current
| Reduce( sym name, type type, string? cast, expr* idx, expr rhs ) # Current
Assign( expr lhs, string? cast, expr rhs ) # Proposal
| Reduce( expr lhs, string? cast, expr rhs ) # Proposal
| ....
expr = Read( sym name, expr* idx ) # Current
Access(sym, name, expr *idx, read bool, write bool) # Proposal
| ...
Advantages:
- Packages all types of accesses to buffer under one type
Disadvantages:
- This will require some dynamic checking to make sure that LHS is always an Access and not some other random expression.
- Expressions were side-effect free, but it might be confusing to have an expressions that can be written to?
- It is unclear what the implications of having access as a type of expression on scheduling operation:
- It might actually make it clearer in some cases: e.g. bind_expr could potentially now bind a LHS.
- In some other cases, it might not make sense to operate on a LHS which will require the scheduling op to reject LHS nodes
Option 2: Add a LHS type (just decoupling the LHS from the statements)
module LoopIR {
proc = ...
fnarg = ...
stmt = Assign( sym name, type type, string? cast, expr* idx, expr rhs ) # Current
| Reduce( sym name, type type, string? cast, expr* idx, expr rhs ) # Current
Assign( LHS lhs, string? cast, expr rhs ) # Proposal
| Reduce( LHS lhs, string? cast, expr rhs ) # Proposal
| ....
lhs = LHS(sym name, type type, expr *idx) # Proposal
Advantages:
- Leaves expressions untouched
Disadvantages:
- Feels hacky
- There are still two types that could access a buffer: LHS and Read