-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redesign Proposal #22
Conversation
Codecov ReportAttention:
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## master #22 +/- ##
===========================================
+ Coverage 42.85% 57.50% +14.64%
===========================================
Files 3 2 -1
Lines 28 40 +12
===========================================
+ Hits 12 23 +11
- Misses 16 17 +1 ☔ View full report in Codecov by Sentry. |
I'm happy to review this week, but I don't have time today. |
No worries! |
SU mostly uses function instantiate(left, pat::PatTerm, mem)
args = []
for parg in arguments(pat)
enqueue = parg isa PatSegment ? append! : push!
enqueue(args, instantiate(left, parg, mem))
end
reference = istree(left) ? left : Expr(:call, :_)
similarterm(reference, operation(pat), args; exprhead = exprhead(pat))
end Where You can see that it is rather hacky and defaults to Patterns for rules have their own term type, so it means that under this redesign they should have their own If we change the last line of There's no easy solution that comes to my mind, other than adding some machinery to "infer" the head type from a reference. |
I guess the solution is having some mechanism to connect In the case If it's a r = @rule ~a + ~b -> f(~a)
@syms x::Int y::Int
t = x + 2 # should be a SymbolicUtils.Add
r(t) # what do we get out of instantiate? What should be the call to |
I propose that the provider of a This means that Metatheory/SU/friends should have to implement
We could override |
So it turns out that In Metatheory we introduced We need to match against the head! Let's consider If we use My intuition is that we will always need to store some I will introduce a |
I've moved |
Ready for review I've made adjustments for Metatheory.jl in JuliaSymbolics/Metatheory.jl#174 |
> r = @rule ~a + ~b -> f(~a)
> @syms x::Int y::Int
> t = x + 2 # should be a SymbolicUtils.Add
> r(t) # what do we get out of instantiate? It seems what you want is to create a term based on the head type of LHS. So yeah in MT you can have something like It makes sense for MT to have a different |
I think we should go with As discussed |
|
||
""" | ||
operation(x) | ||
|
||
If `x` is a term as defined by `istree(x)`, `operation(x)` returns the | ||
head of the term if `x` represents a function call, for example, the head | ||
operation of the term if `x` represents a function call, for example, the head | ||
is the function being called. | ||
""" | ||
function operation end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again should not be in this package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I believe this one should stay in this package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package is used to define an AST interface so that pattern matchers can match against the AST and traverse it. I think it's a little bit of a mistake to introduce @rule
with the example of +(~a, ~b)
, because in many IRs we would want to write this as :call(+, ~a, ~b)
. Not all ASTs will have something analogous to a function call, and it's unclear what benefit would be derived from standardizing this notion here. operation
and arguments
should live in packages that define ASTs that can give these functions meaning.
TL;DR: I don't think we can define what the new meaning of operation
is supposed to do in a way that captures all of its possible use cases in downstream packages, so I think it should not be in this package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all ASTs will have something analogous to a function call
I guess most of what the (old/current) dependents of this package do:
- Metatheory.jl
AbstractPat
AST for patterns - Julia Exprs
- SymbolicUtils IIRC terms have function calls and array indexing
- Everything depending on Symbolics
- Most other symbolic mathematics packages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can use trait like abstractrees? Will make a larger comment below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shashi, do we agree now that function operation end
is fine as-is, just adjust the documentation to say that this one is optional?
Heya! Thanks for the quick reply :)
Hmm, I'm not really sure we have to separate out the If you remember from the ideas in https://arxiv.org/pdf/2112.14714.pdf - MT
The point here is to have something such as
This currently works in JuliaSymbolics/Metatheory.jl#174 The whole idea was to abstract away the logic required for patterns and rewrite systems from the specific implementation of the symbolic types, LMK what you think. |
Applied some suggestions. So you'd say that First, thanks for the idea, it really simplified a lot of MT logic and makes thing a lot lispier than before. I would still go towards the layered architecture as a high-level goal
RN there's a lot of code duplication across MT and SU, but that's fine, if we reintegrate this as in the original project it should be broken down in smaller steps. Some concepts
If you take a look at JuliaSymbolics/Metatheory.jl#174 in You can see what i had in mind for the difference between head/operation. It's the pretty much the same code and logic as in SU matchers with some minor differences. |
@shashi thought a bit about it. It's fine to move |
@willow-ahrens what do you think? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great start! At a high level, I suggest:
- Remove operation and arguments, but leave documentation about where they went and why.
- Let's talk about an AST builder macro in a separate PR, I like the idea and might use it myself but it deserves a separate PR.
|
||
""" | ||
operation(x) | ||
|
||
If `x` is a term as defined by `istree(x)`, `operation(x)` returns the | ||
head of the term if `x` represents a function call, for example, the head | ||
operation of the term if `x` represents a function call, for example, the head | ||
is the function being called. | ||
""" | ||
function operation end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package is used to define an AST interface so that pattern matchers can match against the AST and traverse it. I think it's a little bit of a mistake to introduce @rule
with the example of +(~a, ~b)
, because in many IRs we would want to write this as :call(+, ~a, ~b)
. Not all ASTs will have something analogous to a function call, and it's unclear what benefit would be derived from standardizing this notion here. operation
and arguments
should live in packages that define ASTs that can give these functions meaning.
TL;DR: I don't think we can define what the new meaning of operation
is supposed to do in a way that captures all of its possible use cases in downstream packages, so I think it should not be in this package.
src/TermInterface.jl
Outdated
|
||
Has to be implemented by the provider of H. | ||
Returns a term that is in the same closure of types as `typeof(x)`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
x
is no longer defined here.
Count the nodes in a symbolic expression tree satisfying `istree` and `arguments`. | ||
""" | ||
node_count(t) = istree(t) ? reduce(+, node_count(x) for x in arguments(t), init = 0) + 1 : 1 | ||
export node_count | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, though I wonder if AbstractTrees could help here.
end |> esc | ||
end | ||
export @matchable | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach to building ASTs results in huge slowdowns in compilers, because compilers specialize on each combination of node types. I appreciate the idea of adding an optional AST easy implementation feature, could we add it in a separate PR?
A few ideas:
- enforce symbols or enums to differentiate heads in the AST (runtime not inference time)
- make it clear that
head_symbol
is not part of the TermInterface interface, it's just part of the easy-mode ast builder. - add a simplified approach towards types with dynamic field information. Because we would emit types that have the same type for every head, we would need to use dynamic field information.
If we feel like an optimized AST implementation is out of scope, we shouldn't include programming patterns that will inconvenience users later on with performance issues.
src/expr.jl
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file seems like the right approach to me
Since we came to the point that there are basically two styles of encoding operations (function calls) in ASTs and most downstream packages will use If an AST doesn't encode function calls, then there's no need to define or use such trait or functions, one can just use This is the trait definition# First, I would define an `AbstractASTHead`
abstract type AbstractASTHead end
# trait def in TermInterface
abstract type CallStyle end
# Provider of expression type has to define CallStyle on `head(x)` how to access the "operation" of `x`
struct OperationInNode <: CallStyle
# Operation is straightforward
struct OperationInFirstChild <: CallStyle
operation(x) = operation(CallStyle(head(x)), x)
operation(::OperationInFirstChild, x) = first(children(x))
# operation(::OperationInNode, x) depends on the expression type
arguments(x) = arguments(CallStyle(head(x)), x)
arguments(::OperationInFirstChild, x) = children(x)[2:end] # or some efficient alternative
arguments(::OperationInNode, x) = children(x) User definitionsstruct ExprHead <: AbstractASTHead
head::Symbol
end
istree(x::Expr) = true
head(ex::Expr) = ExprHead(ex.head)
children(ex::Expr) = ex.args
CallStyle(h::ExprHead) = h.head == :call ? OperationInFirstChild() : OperationInNode() # or better def for macrocall and friends
maketerm(h::ExprHead, children) = Expr(h.head, children...)
# definition of operation(::OperationInFirstChild, ::Expr) and arguments comes for free
operation(::OperationInNode, ex::Expr) = ex.head
# =========================
struct SUHead <: AbstractASTHead
operation
end
maketerm(h::SUHead, children; kw...) = h.operation(children...; kw...) # as old similarterm worked
istree(x::Symbolic) = true # replace Symbolic with concrete types that are tree
head(x::Symbolic) = SUHead(directly_access_the_operation(x)) # replace Symbolic with concrete types that are tree
children(x::Symbolic) = ... # directly access the children of concrete types that are tree
CallStyle(h::SUHead) = OperationInNode()
operation(x::Symbolic) = head(x).operation
# definition of arguments comes for free. Although this seems convoluted, it may solve the problem? |
Could you make a separate optional package that defines an ast for function calls that people can opt in to? What are the upsides to formalize operations and arguments in this package?
|
That can be an idea, but then we would have |
Any ideas on how to proceed? |
We're going to have a meeting to discuss changing the Symbolics internals. I think making sure things hit the same interface is fine, but what went wrong last time is this depended on metatheory. The interface should have no dependence on any implementation. |
Thanks, let me know if it's possible to join the meeting. I agree that we first have to focus on designing this interface so that it can capture all different styles of term construction in a performant way, abstracting away the implementation details to allow package interoperability. I think I propose that this package should have very little to zero external dependencies (@willow-ahrens), and we should use traits (or something analogous) to abstract away how terms are constructed/accessed. This is to avoid abstraction leaks and the exprhead situation again. This is not just for In the meanwhile I'll temporarily drop the TermInterface dependency in Metatheory, and use this current version. I have some large overhauls and performance improvements pending, I'l focuse on those and wait until we have consensus on this package to reintegrate TermInterface. I'm aiming to have it perform close to the rust implementation such that we can proceed exploring real world optimizations. I also agree that we first focused on unifying the rewrite systems instead of real world applications. While it will be possible to rewrite on SU terms in MT, SU rules will not be available to be used in MT and vice-versa, even though Summarizing, I propose some concrete goals for the TermInterface redesign proposals:
|
I'm generally against Making the Symbolics dependency chain include Metatheory. I think SU and Metatheory should both depend on TermInterface and not on each other. That's kinda the whole point of this package. I support repeating code for I think the trait situation is fine but we can solve the problem by having both |
With time, I'll take back the part of my review asking to remove |
@shashi Yeah, I agree now that at the current state of things, maintaining them separate is fine :) - didn't know at the time that it was going to cause headaches, there's still room for lots of performance improvements and Metatheory.jl 's
I've just solved the whole "exprhead" issue, and aligned more to your original proposal with a simple trick: an extra function Thus I have a working version of
No traits, no exprhead, no definition of I've tested it and it helped me make Metatheory 15 times faster than the rust implementation, this is big! I'll soon update this proposal with more motivations and explanations, file is here https://github.com/JuliaSymbolics/Metatheory.jl/blob/db4664de6e22dfa63b72d27c43f5f512210cbef4/src/TermInterface.jl |
I will reiterate that I would like to review the @Matchable macro in a separate PR. Its an unrelated change and deserves discussion and I wouldn't want that to hold this PR up. I'm fine with an is_function_call trait, provided it specifies that only if true, the user should define operation and arguments. I'll make a PR to this one with my proposed compromise (just a subset of what's here already), and if people are okay with what I propose I'll test that I can use it with Finch and friends. |
@willow-ahrens I've pushed my update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This redesign appears to directly contradict #21 (comment), specifically, that the old arguments(x)
is the new tail(x)[2:end]
and the old operation(x)
is the new arguments[x](1)
.
|
||
""" | ||
operation(x) | ||
|
||
If `x` is a term as defined by `istree(x)`, `operation(x)` returns the | ||
head of the term if `x` represents a function call, for example, the head | ||
operation of the term if `x` represents a function call, for example, the head | ||
is the function being called. | ||
""" | ||
function operation end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shashi, do we agree now that function operation end
is fine as-is, just adjust the documentation to say that this one is optional?
|
||
If `x` is a term as defined by `istree(x)`, `head(x)` returns the head of the | ||
term. If `x` represents a function call term like `f(a,b)`, the head | ||
is the function being called, `f`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly what this redesign had hoped to avoid! We don't want head
to be the function being called. We want it to be :call
and we want children(x)[1]
to be the function being called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we wanted to avoid the concept of :call
as "exprhead".
In my latest commit head
is the old operation
, and children
is the old arguments
.
If we add both head,children
and operation,arguments
Metatheory.jl would just rely on operation
and arguments
for pattern matching. I guess the same for SU.
But what about SymbolicUtils terms? t = f(a,b)
in SU would have operation(t) == f
, arguments(t) == [a,b]
, children(t) = [f,a,b]
, what about head(t)
? Should it be SUHead()
?
I kinda dislike the idea that the users should define a struct to define the head
of an AST node, all the information required to inspect, manipulate and create new terms is already contained in the type of the term.
src/TermInterface.jl
Outdated
Must be defined if `istree(x)` is defined. | ||
Can be true only if `istree(x)` is true. | ||
""" | ||
function is_function_call end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not default this to false? If it's an optional interface, why do we require users to define it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not make this optional. Defaulting it to false may cause users to forget to set it on specific term types, and thus cause hard to catch bugs as it would propagate through all the rewriting steps (it happened to me and i lost a few hours just to understand i was dispatching on the wrong type and was defaulting to false).
In comparison to the previous state of this redesign, where we had Head
types, in this latest version, the provider of a term types T <: AbstractT
just has to set is_function_call(<:AbstractT) = true
if the whole language is functional, or set it to false if the entire AST does not have function calls
@willow-ahrens @shashi - if i rename Together with a mandatory Can we do this redesign gradually? |
Do we need a call to go through the different re-design proposals? |
That would be great. I can prepare some notes comparing the proposals. I'm planning to release MT 3.0 before the summer as I'd like to present it at the e-graphs community workshop. This is fundamental for the progress of the project. |
Just pushed with the latest proposal we agreed in the call |
cc @shashi @willow-ahrens @YingboMa - based on #21 :)